cosmic_text/
swash.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[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    // Build the scaler
31    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    // Compute the fractional offset-- you'll likely want to quantize this
44    // in a real renderer
45    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    // Select our source order
55    Render::new(&[
56        // Color outline with the first palette
57        Source::ColorOutline(0),
58        // Color bitmap with best fit selection mode
59        Source::ColorBitmap(StrikeWith::BestFit),
60        // Standard scalable outline
61        Source::Outline,
62    ])
63    // Select a subpixel format
64    .format(Format::Alpha)
65    // Apply the fractional offset
66    .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 the image
76    .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    // Build the scaler
97    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    // Scale the outline
110    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    // Get the path information of the outline
122    let path = outline.path();
123
124    // Return the commands
125    Some(path.commands().collect())
126}
127
128/// Cache for rasterizing with the swash scaler
129pub 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    /// Create a new swash cache
143    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    /// Create a swash Image from a cache key, without caching results
152    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    /// Create a swash Image from a cache key, caching results
161    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    /// Creates outline commands
172    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    /// Creates outline commands, without caching results
184    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    /// Enumerate pixels in an Image, use `with_image` for better performance
193    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                            //TODO: blend base alpha?
210                            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                            //TODO: blend base alpha?
224                            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    // variations() resizes context.coords in place (stale values persist),
252    // whereas using normalized_coords() clears and replaces them.
253    #[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        // reference: regular@400 with no prior context
282        let mut ctx = ScaleContext::new();
283        let reference = render(&mut ctx, regular, 400.0, false).map(|i| i.data);
284
285        // variations(): pollute ctx with italic@700, then render regular@400
286        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        // normalized_coords(): same sequence
291        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}