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    let variable_width = font
93        .as_swash()
94        .variations()
95        .find_by_tag(swash::Tag::from_be_bytes(*b"wght"));
96
97    // Build the scaler
98    let mut scaler = context
99        .builder(font.as_swash())
100        .size(f32::from_bits(cache_key.font_size_bits))
101        .hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
102    if let Some(variation) = variable_width {
103        scaler = scaler.variations(std::iter::once(swash::Setting {
104            tag: swash::Tag::from_be_bytes(*b"wght"),
105            value: f32::from(cache_key.font_weight.0)
106                .clamp(variation.min_value(), variation.max_value()),
107        }));
108    }
109    let mut scaler = scaler.build();
110
111    // Scale the outline
112    let mut outline = scaler
113        .scale_outline(cache_key.glyph_id)
114        .or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?;
115
116    if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
117        outline.transform(&Transform::skew(
118            Angle::from_degrees(14.0),
119            Angle::from_degrees(0.0),
120        ));
121    }
122
123    // Get the path information of the outline
124    let path = outline.path();
125
126    // Return the commands
127    Some(path.commands().collect())
128}
129
130/// Cache for rasterizing with the swash scaler
131pub struct SwashCache {
132    context: ScaleContext,
133    pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
134    pub outline_command_cache: HashMap<CacheKey, Option<Box<[swash::zeno::Command]>>>,
135}
136
137impl fmt::Debug for SwashCache {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.pad("SwashCache { .. }")
140    }
141}
142
143impl SwashCache {
144    /// Create a new swash cache
145    pub fn new() -> Self {
146        Self {
147            context: ScaleContext::new(),
148            image_cache: HashMap::default(),
149            outline_command_cache: HashMap::default(),
150        }
151    }
152
153    /// Create a swash Image from a cache key, without caching results
154    pub fn get_image_uncached(
155        &mut self,
156        font_system: &mut FontSystem,
157        cache_key: CacheKey,
158    ) -> Option<SwashImage> {
159        swash_image(font_system, &mut self.context, cache_key)
160    }
161
162    /// Create a swash Image from a cache key, caching results
163    pub fn get_image(
164        &mut self,
165        font_system: &mut FontSystem,
166        cache_key: CacheKey,
167    ) -> &Option<SwashImage> {
168        self.image_cache
169            .entry(cache_key)
170            .or_insert_with(|| swash_image(font_system, &mut self.context, cache_key))
171    }
172
173    /// Creates outline commands
174    pub fn get_outline_commands(
175        &mut self,
176        font_system: &mut FontSystem,
177        cache_key: CacheKey,
178    ) -> Option<&[swash::zeno::Command]> {
179        self.outline_command_cache
180            .entry(cache_key)
181            .or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key))
182            .as_deref()
183    }
184
185    /// Creates outline commands, without caching results
186    pub fn get_outline_commands_uncached(
187        &mut self,
188        font_system: &mut FontSystem,
189        cache_key: CacheKey,
190    ) -> Option<Box<[swash::zeno::Command]>> {
191        swash_outline_commands(font_system, &mut self.context, cache_key)
192    }
193
194    /// Enumerate pixels in an Image, use `with_image` for better performance
195    pub fn with_pixels<F: FnMut(i32, i32, Color)>(
196        &mut self,
197        font_system: &mut FontSystem,
198        cache_key: CacheKey,
199        base: Color,
200        mut f: F,
201    ) {
202        if let Some(image) = self.get_image(font_system, cache_key) {
203            let x = image.placement.left;
204            let y = -image.placement.top;
205
206            match image.content {
207                Content::Mask => {
208                    let mut i = 0;
209                    for off_y in 0..image.placement.height as i32 {
210                        for off_x in 0..image.placement.width as i32 {
211                            //TODO: blend base alpha?
212                            f(
213                                x + off_x,
214                                y + off_y,
215                                Color((u32::from(image.data[i]) << 24) | base.0 & 0xFF_FF_FF),
216                            );
217                            i += 1;
218                        }
219                    }
220                }
221                Content::Color => {
222                    let mut i = 0;
223                    for off_y in 0..image.placement.height as i32 {
224                        for off_x in 0..image.placement.width as i32 {
225                            //TODO: blend base alpha?
226                            f(
227                                x + off_x,
228                                y + off_y,
229                                Color::rgba(
230                                    image.data[i],
231                                    image.data[i + 1],
232                                    image.data[i + 2],
233                                    image.data[i + 3],
234                                ),
235                            );
236                            i += 4;
237                        }
238                    }
239                }
240                Content::SubpixelMask => {
241                    log::warn!("TODO: SubpixelMask");
242                }
243            }
244        }
245    }
246}