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.variations(std::iter::once(swash::Setting {
37            tag: swash::Tag::from_be_bytes(*b"wght"),
38            value: f32::from(cache_key.font_weight.0)
39                .clamp(variation.min_value(), variation.max_value()),
40        }));
41    }
42    let mut scaler = scaler.build();
43
44    // Compute the fractional offset-- you'll likely want to quantize this
45    // in a real renderer
46    let offset = if cache_key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
47        Vector::new(
48            cache_key.x_bin.as_float().round() + 1.0,
49            cache_key.y_bin.as_float().round(),
50        )
51    } else {
52        Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float())
53    };
54
55    // Select our source order
56    Render::new(&[
57        // Color outline with the first palette
58        Source::ColorOutline(0),
59        // Color bitmap with best fit selection mode
60        Source::ColorBitmap(StrikeWith::BestFit),
61        // Standard scalable outline
62        Source::Outline,
63    ])
64    // Select a subpixel format
65    .format(Format::Alpha)
66    // Apply the fractional offset
67    .offset(offset)
68    .transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
69        Some(Transform::skew(
70            Angle::from_degrees(14.0),
71            Angle::from_degrees(0.0),
72        ))
73    } else {
74        None
75    })
76    // Render the image
77    .render(&mut scaler, cache_key.glyph_id)
78}
79
80fn swash_outline_commands(
81    font_system: &mut FontSystem,
82    context: &mut ScaleContext,
83    cache_key: CacheKey,
84) -> Option<Box<[swash::zeno::Command]>> {
85    use swash::zeno::PathData as _;
86
87    let Some(font) = font_system.get_font(cache_key.font_id, cache_key.font_weight) else {
88        log::warn!("did not find font {:?}", cache_key.font_id);
89        return None;
90    };
91
92    // Build the scaler
93    let mut scaler = context
94        .builder(font.as_swash())
95        .size(f32::from_bits(cache_key.font_size_bits))
96        .hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING))
97        .build();
98
99    // Scale the outline
100    let mut outline = scaler
101        .scale_outline(cache_key.glyph_id)
102        .or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?;
103
104    if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
105        outline.transform(&Transform::skew(
106            Angle::from_degrees(14.0),
107            Angle::from_degrees(0.0),
108        ));
109    }
110
111    // Get the path information of the outline
112    let path = outline.path();
113
114    // Return the commands
115    Some(path.commands().collect())
116}
117
118/// Cache for rasterizing with the swash scaler
119pub struct SwashCache {
120    context: ScaleContext,
121    pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
122    pub outline_command_cache: HashMap<CacheKey, Option<Box<[swash::zeno::Command]>>>,
123}
124
125impl fmt::Debug for SwashCache {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        f.pad("SwashCache { .. }")
128    }
129}
130
131impl SwashCache {
132    /// Create a new swash cache
133    pub fn new() -> Self {
134        Self {
135            context: ScaleContext::new(),
136            image_cache: HashMap::default(),
137            outline_command_cache: HashMap::default(),
138        }
139    }
140
141    /// Create a swash Image from a cache key, without caching results
142    pub fn get_image_uncached(
143        &mut self,
144        font_system: &mut FontSystem,
145        cache_key: CacheKey,
146    ) -> Option<SwashImage> {
147        swash_image(font_system, &mut self.context, cache_key)
148    }
149
150    /// Create a swash Image from a cache key, caching results
151    pub fn get_image(
152        &mut self,
153        font_system: &mut FontSystem,
154        cache_key: CacheKey,
155    ) -> &Option<SwashImage> {
156        self.image_cache
157            .entry(cache_key)
158            .or_insert_with(|| swash_image(font_system, &mut self.context, cache_key))
159    }
160
161    /// Creates outline commands
162    pub fn get_outline_commands(
163        &mut self,
164        font_system: &mut FontSystem,
165        cache_key: CacheKey,
166    ) -> Option<&[swash::zeno::Command]> {
167        self.outline_command_cache
168            .entry(cache_key)
169            .or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key))
170            .as_deref()
171    }
172
173    /// Creates outline commands, without caching results
174    pub fn get_outline_commands_uncached(
175        &mut self,
176        font_system: &mut FontSystem,
177        cache_key: CacheKey,
178    ) -> Option<Box<[swash::zeno::Command]>> {
179        swash_outline_commands(font_system, &mut self.context, cache_key)
180    }
181
182    /// Enumerate pixels in an Image, use `with_image` for better performance
183    pub fn with_pixels<F: FnMut(i32, i32, Color)>(
184        &mut self,
185        font_system: &mut FontSystem,
186        cache_key: CacheKey,
187        base: Color,
188        mut f: F,
189    ) {
190        if let Some(image) = self.get_image(font_system, cache_key) {
191            let x = image.placement.left;
192            let y = -image.placement.top;
193
194            match image.content {
195                Content::Mask => {
196                    let mut i = 0;
197                    for off_y in 0..image.placement.height as i32 {
198                        for off_x in 0..image.placement.width as i32 {
199                            //TODO: blend base alpha?
200                            f(
201                                x + off_x,
202                                y + off_y,
203                                Color((u32::from(image.data[i]) << 24) | base.0 & 0xFF_FF_FF),
204                            );
205                            i += 1;
206                        }
207                    }
208                }
209                Content::Color => {
210                    let mut i = 0;
211                    for off_y in 0..image.placement.height as i32 {
212                        for off_x in 0..image.placement.width as i32 {
213                            //TODO: blend base alpha?
214                            f(
215                                x + off_x,
216                                y + off_y,
217                                Color::rgba(
218                                    image.data[i],
219                                    image.data[i + 1],
220                                    image.data[i + 2],
221                                    image.data[i + 3],
222                                ),
223                            );
224                            i += 4;
225                        }
226                    }
227                }
228                Content::SubpixelMask => {
229                    log::warn!("TODO: SubpixelMask");
230                }
231            }
232        }
233    }
234}