Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/fonts/font_kit.rs
1//! A text rasterizer backed by [`font_kit`] that supports rasterizing at subpixel offsets
2 
3use std::sync::Arc;
4 
5use anyhow::Result;
6use dashmap::DashMap;
7use font_kit::canvas::{Canvas, RasterizationOptions};
8use font_kit::font::Font;
9use font_kit::hinting::HintingOptions;
10use pathfinder_geometry::rect::RectI;
11use pathfinder_geometry::transform2d::Transform2F;
12use pathfinder_geometry::vector::{vec2i, Vector2F, Vector2I};
13use strato_ui_core::fonts::canvas::RasterFormat;
14use strato_ui_core::fonts::{
15 FontId, GlyphId, Properties, RasterizedGlyph, Style, SubpixelAlignment, Weight,
16};
17use strato_ui_core::rendering;
18 
19#[cfg(target_os = "macos")]
20use crate::platform::mac::AutoreleasePoolGuard;
21 
22/// A simpler rasterizer backed by font-kit.
23pub(crate) struct Rasterizer {
24 fonts: DashMap<FontId, Arc<Font>>,
25}
26 
27impl Rasterizer {
28 pub fn new() -> Self {
29 Self {
30 fonts: Default::default(),
31 }
32 }
33 
34 pub fn insert(&self, font_id: FontId, font: Arc<Font>) {
35 self.fonts.insert(font_id, font);
36 }
37 
38 pub fn font_for_id(&self, font_id: FontId) -> Arc<Font> {
39 self.fonts.get(&font_id).expect("Font must exist").clone()
40 }
41 
42 pub fn glyph_raster_bounds(
43 &self,
44 font_id: FontId,
45 point_size: f32,
46 glyph_id: GlyphId,
47 scale: Vector2F,
48 _glyph_config: &rendering::GlyphConfig,
49 ) -> Result<RectI> {
50 let raw_raster_bounds = self.font_for_id(font_id).raster_bounds(
51 glyph_id,
52 point_size,
53 Transform2F::from_scale(scale),
54 HintingOptions::None,
55 RasterizationOptions::GrayscaleAa,
56 )?;
57 if raw_raster_bounds.size() == Vector2I::zero() {
58 // Don't adjust the size of a glyph with a default size of zero.
59 return Ok(raw_raster_bounds);
60 }
61 // The default raster bounds provided by font-kit sometimes clip pixels
62 // off of anti-aliased glyphs; add one pixel to the glyph bounds to
63 // compensate. We only adjust the origin vertically because the extra
64 // pixel of height changes the baseline; the extra pixel on the right
65 // side doesn't change positioning (as the origin is on the left edge of
66 // the glyph).
67 let fudge_factor = vec2i(1, 1);
68 let offset = vec2i(0, 1);
69 Ok(RectI::new(
70 raw_raster_bounds.origin() - offset,
71 raw_raster_bounds.size() + fudge_factor,
72 ))
73 }
74 
75 #[allow(clippy::too_many_arguments)]
76 pub fn rasterize_glyph(
77 &self,
78 font_id: FontId,
79 point_size: f32,
80 glyph_id: GlyphId,
81 scale: Vector2F,
82 subpixel_alignment: SubpixelAlignment,
83 glyph_config: &rendering::GlyphConfig,
84 format: RasterFormat,
85 ) -> Result<RasterizedGlyph> {
86 // On macOS, this function calls into Core Graphics and Core Text
87 // (`CGBitmapContextCreate` per glyph plus `raster_bounds` reading font
88 // metadata), each of which leaves transient bookkeeping objects in the
89 // thread's autorelease pool. Because this is invoked during Metal
90 // frame rendering on the main thread, hundreds of those objects can
91 // accumulate between run-loop turns before AppKit's outer pool
92 // drains. A local pool bounds that peak without relying on the outer
93 // pool. The guard drains on `Drop`, covering the error paths from `?`
94 // below and any panics from `font_kit`.
95 #[cfg(target_os = "macos")]
96 let _pool = AutoreleasePoolGuard::new();
97 
98 let bounds =
99 self.glyph_raster_bounds(font_id, point_size, glyph_id, scale, glyph_config)?;
100 let mut canvas = Canvas::new(bounds.size(), raster_format_to_font_kit(format));
101 
102 let base_transform = Transform2F::from_scale(scale).translate(-bounds.origin().to_f32());
103 let aligned_transform = base_transform.translate(subpixel_alignment.to_offset());
104 
105 self.font_for_id(font_id).rasterize_glyph(
106 &mut canvas,
107 glyph_id,
108 point_size,
109 aligned_transform,
110 HintingOptions::None,
111 RasterizationOptions::GrayscaleAa,
112 )?;
113 
114 Ok(RasterizedGlyph {
115 canvas: canvas.into(),
116 // Crates.io font-kit does not expose Warp's color-font helper. Keep
117 // emoji/color-glyph handling disabled until Strato owns that path.
118 is_emoji: false,
119 })
120 }
121}
122 
123pub fn properties_to_font_kit(properties: Properties) -> font_kit::properties::Properties {
124 font_kit::properties::Properties {
125 style: style_to_font_kit(properties.style),
126 weight: weight_to_font_kit(properties.weight),
127 stretch: Default::default(),
128 }
129}
130 
131fn raster_format_to_font_kit(format: RasterFormat) -> font_kit::canvas::Format {
132 use font_kit::canvas::Format as FKFormat;
133 match format {
134 RasterFormat::Rgba32 => FKFormat::Rgba32,
135 RasterFormat::Rgb24 => FKFormat::Rgb24,
136 RasterFormat::A8 => FKFormat::Rgb24,
137 }
138}
139 
140fn weight_to_font_kit(weight: Weight) -> font_kit::properties::Weight {
141 match weight {
142 Weight::Thin => font_kit::properties::Weight::THIN,
143 Weight::ExtraLight => font_kit::properties::Weight::EXTRA_LIGHT,
144 Weight::Light => font_kit::properties::Weight::LIGHT,
145 Weight::Normal => font_kit::properties::Weight::NORMAL,
146 Weight::Medium => font_kit::properties::Weight::MEDIUM,
147 Weight::Semibold => font_kit::properties::Weight::SEMIBOLD,
148 Weight::Bold => font_kit::properties::Weight::BOLD,
149 Weight::ExtraBold => font_kit::properties::Weight::EXTRA_BOLD,
150 Weight::Black => font_kit::properties::Weight::BLACK,
151 }
152}
153 
154fn style_to_font_kit(value: Style) -> font_kit::properties::Style {
155 match value {
156 Style::Normal => font_kit::properties::Style::Normal,
157 Style::Italic => font_kit::properties::Style::Italic,
158 }
159}
160