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