1#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5use core::fmt;
6use swash::scale::{image::Content, ScaleContext};
7use swash::scale::{Render, Source, StrikeWith};
8use swash::zeno::{Format, Vector};
9
10use crate::{CacheKey, CacheKeyFlags, Color, FontSystem, HashMap};
11
12pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
13pub use swash::zeno::{Angle, Command, Placement, Transform};
14
15fn swash_image(
16 font_system: &mut FontSystem,
17 context: &mut ScaleContext,
18 cache_key: CacheKey,
19) -> Option<SwashImage> {
20 let Some(font) = font_system.get_font(cache_key.font_id, cache_key.font_weight) else {
21 log::warn!("did not find font {:?}", cache_key.font_id);
22 return None;
23 };
24
25 let variable_width = font
26 .as_swash()
27 .variations()
28 .find_by_tag(swash::Tag::from_be_bytes(*b"wght"));
29
30 let mut scaler = context
32 .builder(font.as_swash())
33 .size(f32::from_bits(cache_key.font_size_bits))
34 .hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
35 if let Some(variation) = variable_width {
36 scaler = scaler.normalized_coords(font.as_swash().variations().normalized_coords([(
37 swash::Tag::from_be_bytes(*b"wght"),
38 f32::from(cache_key.font_weight.0).clamp(variation.min_value(), variation.max_value()),
39 )]));
40 }
41 let mut scaler = scaler.build();
42
43 let offset = if cache_key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
46 Vector::new(
47 cache_key.x_bin.as_float().round() + 1.0,
48 cache_key.y_bin.as_float().round(),
49 )
50 } else {
51 Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float())
52 };
53
54 Render::new(&[
56 Source::ColorOutline(0),
58 Source::ColorBitmap(StrikeWith::BestFit),
60 Source::Outline,
62 ])
63 .format(Format::Alpha)
65 .offset(offset)
67 .transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
68 Some(Transform::skew(
69 Angle::from_degrees(14.0),
70 Angle::from_degrees(0.0),
71 ))
72 } else {
73 None
74 })
75 .render(&mut scaler, cache_key.glyph_id)
77}
78
79fn swash_outline_commands(
80 font_system: &mut FontSystem,
81 context: &mut ScaleContext,
82 cache_key: CacheKey,
83) -> Option<Box<[swash::zeno::Command]>> {
84 use swash::zeno::PathData as _;
85
86 let Some(font) = font_system.get_font(cache_key.font_id, cache_key.font_weight) else {
87 log::warn!("did not find font {:?}", cache_key.font_id);
88 return None;
89 };
90
91 let variable_width = font
92 .as_swash()
93 .variations()
94 .find_by_tag(swash::Tag::from_be_bytes(*b"wght"));
95
96 let mut scaler = context
98 .builder(font.as_swash())
99 .size(f32::from_bits(cache_key.font_size_bits))
100 .hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
101 if let Some(variation) = variable_width {
102 scaler = scaler.normalized_coords(font.as_swash().variations().normalized_coords([(
103 swash::Tag::from_be_bytes(*b"wght"),
104 f32::from(cache_key.font_weight.0).clamp(variation.min_value(), variation.max_value()),
105 )]));
106 }
107 let mut scaler = scaler.build();
108
109 let mut outline = scaler
111 .scale_outline(cache_key.glyph_id)
112 .or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?;
113
114 if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
115 outline.transform(&Transform::skew(
116 Angle::from_degrees(14.0),
117 Angle::from_degrees(0.0),
118 ));
119 }
120
121 let path = outline.path();
123
124 Some(path.commands().collect())
126}
127
128pub struct SwashCache {
130 context: ScaleContext,
131 pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
132 pub outline_command_cache: HashMap<CacheKey, Option<Box<[swash::zeno::Command]>>>,
133}
134
135impl fmt::Debug for SwashCache {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.pad("SwashCache { .. }")
138 }
139}
140
141impl SwashCache {
142 pub fn new() -> Self {
144 Self {
145 context: ScaleContext::new(),
146 image_cache: HashMap::default(),
147 outline_command_cache: HashMap::default(),
148 }
149 }
150
151 pub fn get_image_uncached(
153 &mut self,
154 font_system: &mut FontSystem,
155 cache_key: CacheKey,
156 ) -> Option<SwashImage> {
157 swash_image(font_system, &mut self.context, cache_key)
158 }
159
160 pub fn get_image(
162 &mut self,
163 font_system: &mut FontSystem,
164 cache_key: CacheKey,
165 ) -> &Option<SwashImage> {
166 self.image_cache
167 .entry(cache_key)
168 .or_insert_with(|| swash_image(font_system, &mut self.context, cache_key))
169 }
170
171 pub fn get_outline_commands(
173 &mut self,
174 font_system: &mut FontSystem,
175 cache_key: CacheKey,
176 ) -> Option<&[swash::zeno::Command]> {
177 self.outline_command_cache
178 .entry(cache_key)
179 .or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key))
180 .as_deref()
181 }
182
183 pub fn get_outline_commands_uncached(
185 &mut self,
186 font_system: &mut FontSystem,
187 cache_key: CacheKey,
188 ) -> Option<Box<[swash::zeno::Command]>> {
189 swash_outline_commands(font_system, &mut self.context, cache_key)
190 }
191
192 pub fn with_pixels<F: FnMut(i32, i32, Color)>(
194 &mut self,
195 font_system: &mut FontSystem,
196 cache_key: CacheKey,
197 base: Color,
198 mut f: F,
199 ) {
200 if let Some(image) = self.get_image(font_system, cache_key) {
201 let x = image.placement.left;
202 let y = -image.placement.top;
203
204 match image.content {
205 Content::Mask => {
206 let mut i = 0;
207 for off_y in 0..image.placement.height as i32 {
208 for off_x in 0..image.placement.width as i32 {
209 f(
211 x + off_x,
212 y + off_y,
213 Color((u32::from(image.data[i]) << 24) | base.0 & 0xFF_FF_FF),
214 );
215 i += 1;
216 }
217 }
218 }
219 Content::Color => {
220 let mut i = 0;
221 for off_y in 0..image.placement.height as i32 {
222 for off_x in 0..image.placement.width as i32 {
223 f(
225 x + off_x,
226 y + off_y,
227 Color::rgba(
228 image.data[i],
229 image.data[i + 1],
230 image.data[i + 2],
231 image.data[i + 3],
232 ),
233 );
234 i += 4;
235 }
236 }
237 }
238 Content::SubpixelMask => {
239 log::warn!("TODO: SubpixelMask");
240 }
241 }
242 }
243 }
244}
245
246#[cfg(test)]
247mod test {
248 use super::*;
249 use swash::{FontRef, Setting, Tag};
250
251 #[test]
254 fn no_coord_leakage_across_fonts() {
255 let [Ok(sfns), Ok(sfns_italic)] = [
256 "/System/Library/Fonts/SFNS.ttf",
257 "/System/Library/Fonts/SFNSItalic.ttf",
258 ]
259 .map(std::fs::read) else {
260 return;
261 };
262 let regular = FontRef::from_index(&sfns, 0).unwrap();
263 let italic = FontRef::from_index(&sfns_italic, 0).unwrap();
264 let wght = Tag::from_be_bytes(*b"wght");
265
266 let render = |ctx: &mut ScaleContext, font: FontRef, weight: f32, use_normalized| {
267 let mut b = ctx.builder(font).size(16.0).hint(true);
268 if use_normalized {
269 b = b.normalized_coords(font.variations().normalized_coords([(wght, weight)]));
270 } else {
271 b = b.variations(std::iter::once(Setting {
272 tag: wght,
273 value: weight,
274 }));
275 }
276 Render::new(&[Source::Outline])
277 .format(Format::Alpha)
278 .render(&mut b.build(), 36)
279 };
280
281 let mut ctx = ScaleContext::new();
283 let reference = render(&mut ctx, regular, 400.0, false).map(|i| i.data);
284
285 let mut ctx = ScaleContext::new();
287 render(&mut ctx, italic, 700.0, false);
288 let not_normalized = render(&mut ctx, regular, 400.0, false).map(|i| i.data);
289
290 let mut ctx = ScaleContext::new();
292 render(&mut ctx, italic, 700.0, true);
293 let normalized = render(&mut ctx, regular, 400.0, true).map(|i| i.data);
294
295 assert_ne!(not_normalized, reference, "variations leak across fonts");
296 assert_eq!(
297 normalized, reference,
298 "normalized_coords match clean render"
299 );
300 }
301}