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