cosmic_text/
swash.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[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    // Build the scaler
34    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    // Compute the fractional offset-- you'll likely want to quantize this
47    // in a real renderer
48    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    // Select our source order
58    Render::new(&[
59        // Color outline with the first palette
60        Source::ColorOutline(0),
61        // Color bitmap with best fit selection mode
62        Source::ColorBitmap(StrikeWith::BestFit),
63        // Standard scalable outline
64        Source::Outline,
65    ])
66    // Select a subpixel format
67    .format(Format::Alpha)
68    // Apply the fractional offset
69    .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 the image
79    .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    // Build the scaler
100    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    // Scale the outline
113    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    // Get the path information of the outline
125    let path = outline.path();
126
127    // Return the commands
128    Some(path.commands().collect())
129}
130
131/// Cache for rasterizing with the swash scaler
132pub 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    /// Create a new swash cache
146    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    /// Create a swash Image from a cache key, without caching results
155    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    /// Create a swash Image from a cache key, caching results
164    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    /// Creates outline commands
175    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    /// Creates outline commands, without caching results
187    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    /// Enumerate pixels in an Image, use `with_image` for better performance
196    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                            //TODO: blend base alpha?
213                            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                            //TODO: blend base alpha?
227                            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    // variations() resizes context.coords in place (stale values persist),
255    // whereas using normalized_coords() clears and replaces them.
256    #[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        // reference: regular@400 with no prior context
285        let mut ctx = ScaleContext::new();
286        let reference = render(&mut ctx, regular, 400.0, false).map(|i| i.data);
287
288        // variations(): pollute ctx with italic@700, then render regular@400
289        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        // normalized_coords(): same sequence
294        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}