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;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct GlyphStroke {
20 pub width: f32,
21 pub cap: Cap,
22 pub join: Join,
23 pub miter_limit: f32,
25}
26
27pub struct GlyphImage {
31 pub width: u32,
32 pub height: u32,
33 pub left: i32,
34 pub top: i32,
35 pub data: Vec<u8>,
36}
37
38#[derive(Default)]
41pub struct Rasterizer {
42 context: swash::scale::ScaleContext,
43 stroker: tiny_skia::PathStroker,
44}
45
46impl Rasterizer {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn alpha(&mut self, font: &Font, glyph: u32, px: f32, dx: f32) -> Option<GlyphImage> {
55 let image = self.render(font, glyph, px, dx)?;
56 Some(GlyphImage {
57 width: image.placement.width,
58 height: image.placement.height,
59 left: image.placement.left,
60 top: image.placement.top,
61 data: image.data,
62 })
63 }
64
65 pub fn stroked(
81 &mut self,
82 font: &Font,
83 glyph: u32,
84 px: f32,
85 dx: f32,
86 stroke: &GlyphStroke,
87 ) -> Option<GlyphImage> {
88 let outline = glyph_outline(font, glyph, px, dx)?;
89 let stroked = self.stroker.stroke(&outline, &skia_stroke(stroke), 1.0)?;
90 mask_of(&stroked)
91 }
92
93 pub fn sdf(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
98 let alpha = self.render(font, glyph, px, 0.0)?;
99 let pad = SDF_PAD;
100 let w = alpha.placement.width + 2 * pad;
101 let h = alpha.placement.height + 2 * pad;
102 let mut coverage = vec![0u8; (w * h) as usize];
103 for y in 0..alpha.placement.height {
104 for x in 0..alpha.placement.width {
105 coverage[((y + pad) * w + x + pad) as usize] =
106 alpha.data[(y * alpha.placement.width + x) as usize];
107 }
108 }
109 let field = crate::sdf::signed_distances(&coverage, w as usize, h as usize);
110 Some(GlyphImage {
111 width: w,
112 height: h,
113 left: alpha.placement.left - pad as i32,
114 top: alpha.placement.top + pad as i32,
115 data: crate::sdf::encode(&field, SDF_PAD as f32),
116 })
117 }
118
119 pub fn color(&mut self, font: &Font, glyph: u32, px: f32) -> Option<GlyphImage> {
124 let font_ref = swash::FontRef::from_index(font.data(), font.face_index() as usize)?;
125 let mut scaler = self
126 .context
127 .builder(font_ref)
128 .size(px)
129 .hint(false)
130 .variations(swash_variations(font))
131 .build();
132 let image = swash::scale::Render::new(&[
133 swash::scale::Source::ColorOutline(0),
134 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
135 ])
136 .render(&mut scaler, glyph as swash::GlyphId);
137 let Some(image) = image else {
138 return crate::colr::raster(font, glyph, px);
141 };
142 if image.content != swash::scale::image::Content::Color {
143 return crate::colr::raster(font, glyph, px);
144 }
145 let mut data = image.data;
146 for px in data.chunks_exact_mut(4) {
147 let a = px[3] as u32;
148 px[0] = ((px[0] as u32 * a + 127) / 255) as u8;
151 px[1] = ((px[1] as u32 * a + 127) / 255) as u8;
152 px[2] = ((px[2] as u32 * a + 127) / 255) as u8;
153 }
154 Some(GlyphImage {
155 width: image.placement.width,
156 height: image.placement.height,
157 left: image.placement.left,
158 top: image.placement.top,
159 data,
160 })
161 }
162
163 pub(crate) fn color_bounds(&mut self, font: &Font, glyph: u32, px: f32) -> Option<Rect> {
167 let image = self.color(font, glyph, px)?;
168 let mut left = image.width;
169 let mut top = image.height;
170 let mut right = 0;
171 let mut bottom = 0;
172 for y in 0..image.height {
173 for x in 0..image.width {
174 let alpha = image.data[((y * image.width + x) * 4 + 3) as usize];
175 if alpha == 0 {
176 continue;
177 }
178 left = left.min(x);
179 top = top.min(y);
180 right = right.max(x + 1);
181 bottom = bottom.max(y + 1);
182 }
183 }
184 (left < right && top < bottom).then(|| {
185 Rect::new(
186 image.left as f32 + left as f32,
187 -image.top as f32 + top as f32,
188 (right - left) as f32,
189 (bottom - top) as f32,
190 )
191 })
192 }
193
194 fn render(
195 &mut self,
196 font: &Font,
197 glyph: u32,
198 px: f32,
199 dx: f32,
200 ) -> Option<swash::scale::image::Image> {
201 let font_ref = swash::FontRef::from_index(font.data(), font.face_index() as usize)?;
202 let mut scaler = self
203 .context
204 .builder(font_ref)
205 .size(px)
206 .hint(false)
207 .variations(swash_variations(font))
208 .build();
209 swash::scale::Render::new(&[swash::scale::Source::Outline])
210 .offset(swash::zeno::Vector::new(dx, 0.0))
211 .render(&mut scaler, glyph as swash::GlyphId)
212 }
213}
214
215fn skia_stroke(stroke: &GlyphStroke) -> tiny_skia::Stroke {
216 tiny_skia::Stroke {
217 width: stroke.width,
218 miter_limit: stroke.miter_limit,
219 line_cap: match stroke.cap {
220 Cap::Butt => tiny_skia::LineCap::Butt,
221 Cap::Round => tiny_skia::LineCap::Round,
222 Cap::Square => tiny_skia::LineCap::Square,
223 },
224 line_join: match stroke.join {
225 Join::Miter => tiny_skia::LineJoin::Miter,
226 Join::Round => tiny_skia::LineJoin::Round,
227 Join::Bevel => tiny_skia::LineJoin::Bevel,
228 },
229 dash: None,
230 }
231}
232
233fn glyph_outline(font: &Font, glyph: u32, px: f32, dx: f32) -> Option<tiny_skia::Path> {
237 let font_ref = skrifa::FontRef::from_index(font.data(), font.face_index()).ok()?;
238 let outline = font_ref.outline_glyphs().get(skrifa::GlyphId::new(glyph))?;
239 let mut pen = TsPathPen::default();
240 outline
241 .draw(
242 DrawSettings::unhinted(Size::new(px), font.variation_location()),
243 &mut pen,
244 )
245 .ok()?;
246 pen.builder
247 .finish()?
248 .transform(tiny_skia::Transform::from_row(1.0, 0.0, 0.0, -1.0, dx, 0.0))
249}
250
251fn mask_of(path: &tiny_skia::Path) -> Option<GlyphImage> {
256 let bounds = path.compute_tight_bounds()?;
257 let (left, top) = (bounds.left().floor(), bounds.top().floor());
258 let width = (bounds.right().ceil() - left) as u32;
259 let height = (bounds.bottom().ceil() - top) as u32;
260 let mut mask = tiny_skia::Mask::new(width, height)?;
261 mask.fill_path(
262 path,
263 tiny_skia::FillRule::Winding,
264 true,
265 tiny_skia::Transform::from_translate(-left, -top),
266 );
267 Some(GlyphImage {
268 width,
269 height,
270 left: left as i32,
271 top: -top as i32,
272 data: mask.data().to_vec(),
273 })
274}
275
276pub fn glyph_path(font: &Font, glyph: u32, px: f32) -> Option<Arc<Path>> {
279 let font_ref = skrifa::FontRef::from_index(font.data(), font.face_index()).ok()?;
280 let outline = font_ref.outline_glyphs().get(skrifa::GlyphId::new(glyph))?;
281 let mut pen = PathPen {
282 builder: PathBuilder::new(),
283 };
284 outline
285 .draw(
286 DrawSettings::unhinted(Size::new(px), font.variation_location()),
287 &mut pen,
288 )
289 .ok()?;
290 let path = pen.builder.build();
291 (!path.is_empty()).then_some(path)
295}
296
297#[derive(Default)]
300pub(crate) struct TsPathPen {
301 pub(crate) builder: tiny_skia::PathBuilder,
302}
303
304impl OutlinePen for TsPathPen {
305 fn move_to(&mut self, x: f32, y: f32) {
306 self.builder.move_to(x, y);
307 }
308
309 fn line_to(&mut self, x: f32, y: f32) {
310 self.builder.line_to(x, y);
311 }
312
313 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
314 self.builder.quad_to(cx, cy, x, y);
315 }
316
317 fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) {
318 self.builder.cubic_to(c0x, c0y, c1x, c1y, x, y);
319 }
320
321 fn close(&mut self) {
322 self.builder.close();
323 }
324}
325
326struct PathPen {
328 builder: PathBuilder,
329}
330
331impl OutlinePen for PathPen {
332 fn move_to(&mut self, x: f32, y: f32) {
333 self.builder.move_to((x, -y));
334 }
335
336 fn line_to(&mut self, x: f32, y: f32) {
337 self.builder.line_to((x, -y));
338 }
339
340 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
341 self.builder.quad_to((cx, -cy), (x, -y));
342 }
343
344 fn curve_to(&mut self, c0x: f32, c0y: f32, c1x: f32, c1y: f32, x: f32, y: f32) {
345 self.builder.cubic_to((c0x, -c0y), (c1x, -c1y), (x, -y));
346 }
347
348 fn close(&mut self) {
349 self.builder.close();
350 }
351}
352
353fn swash_variations(font: &Font) -> impl Iterator<Item = (&str, f32)> + '_ {
356 font.variation_coordinates()
357 .iter()
358 .filter_map(|(tag, value)| Some((std::str::from_utf8(tag).ok()?, *value)))
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::font::FaceSet;
365
366 fn fira() -> FaceSet {
367 let path = concat!(
368 env!("CARGO_MANIFEST_DIR"),
369 "/../../assets/fonts/fira_sans.ttf"
370 );
371 let mut c = FaceSet::default();
372 c.register("Fira Sans", std::fs::read(path).unwrap())
373 .unwrap();
374 c
375 }
376
377 #[test]
383 fn stroked_raster_bounds_hold_the_miter_spikes() {
384 let fonts = fira();
385 let font = fonts.family("Fira Sans").unwrap();
386 let mut raster = Rasterizer::new();
387 let stroke = GlyphStroke {
388 width: 5.0,
389 cap: Cap::Butt,
390 join: Join::Miter,
391 miter_limit: 16.0,
392 };
393 let glyph = fonts.get(font).glyph_for('M').unwrap();
394 let fill = raster.alpha(fonts.get(font), glyph, 72.0, 0.0).unwrap();
395 let stroked = raster
396 .stroked(fonts.get(font), glyph, 72.0, 0.0, &stroke)
397 .unwrap();
398 let reach = (stroked.top - fill.top) as f32;
399 assert!(
400 reach > stroke.width,
401 "the stroked cell reaches only {reach}px above the fill — a \
402 miter spike of 8.6px does not fit"
403 );
404
405 let bevelled = raster
409 .stroked(
410 fonts.get(font),
411 glyph,
412 72.0,
413 0.0,
414 &GlyphStroke {
415 join: Join::Bevel,
416 ..stroke
417 },
418 )
419 .unwrap();
420 assert!(
421 bevelled.top < stroked.top,
422 "bevel {} vs miter {}",
423 bevelled.top,
424 stroked.top
425 );
426 }
427
428 #[test]
433 fn sdf_and_mask_tiers_agree_on_placement() {
434 let fonts = fira();
435 let font = fonts.family("Fira Sans").unwrap();
436 let mut raster = Rasterizer::new();
437 for ch in ['H', 'g', 'x', 'Q'] {
438 let glyph = fonts.get(font).glyph_for(ch).unwrap();
439 let alpha = raster.alpha(fonts.get(font), glyph, 64.0, 0.0).unwrap();
440 let sdf = raster.sdf(fonts.get(font), glyph, 64.0).unwrap();
441 let pad = SDF_PAD as i32;
442 for (axis, a, s) in [
443 ("top", alpha.top, sdf.top - pad),
444 ("left", alpha.left, sdf.left + pad),
445 ] {
446 assert!(
447 (a - s).abs() <= 1,
448 "'{ch}' {axis}: mask {a} vs sdf-adjusted {s}"
449 );
450 }
451 }
452 }
453}