StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Module that rasterizes text using `swash`. |
| 2 | |
| 3 | use crate::fonts::canvas::{Canvas, RasterFormat}; |
| 4 | use crate::fonts::{FontId, GlyphId, RasterizedGlyph, SubpixelAlignment}; |
| 5 | use crate::platform::FontDB as _; |
| 6 | use crate::rendering::GlyphConfig; |
| 7 | use crate::windowing::winit::fonts::FontDB; |
| 8 | use anyhow::{anyhow, Result}; |
| 9 | use cosmic_text::{CacheKey, CacheKeyFlags}; |
| 10 | use pathfinder_geometry::rect::RectI; |
| 11 | use pathfinder_geometry::vector::{vec2i, Vector2F, Vector2I}; |
| 12 | |
| 13 | impl FontDB { |
| 14 | pub(super) fn glyph_raster_bounds( |
| 15 | &self, |
| 16 | font_id: FontId, |
| 17 | size: f32, |
| 18 | glyph_id: GlyphId, |
| 19 | scale: Vector2F, |
| 20 | _glyph_config: &GlyphConfig, |
| 21 | ) -> Result<RectI> { |
| 22 | let Ok(_typographic_bounds) = self |
| 23 | .glyph_typographic_bounds(font_id, glyph_id) |
| 24 | .map(|bounds| bounds.to_f32()) |
| 25 | else { |
| 26 | // We can't render this glyph using this font, return an empty rect to indicate we |
| 27 | // don't need to rasterize this glyph. This can happen if the font doesn't contain |
| 28 | // a glyph _or_ if the glyph isn't renderable (some fonts contain a glyph for the |
| 29 | // space character, but don't provide outlines for it). |
| 30 | return Ok(RectI::new(Vector2I::zero(), Vector2I::zero())); |
| 31 | }; |
| 32 | |
| 33 | let id = *self |
| 34 | .text_layout_system |
| 35 | .font_id_map |
| 36 | .read() |
| 37 | .get_by_left(&font_id) |
| 38 | .unwrap(); |
| 39 | let image = self |
| 40 | .swash_cache |
| 41 | .write() |
| 42 | .get_image_uncached( |
| 43 | &mut self.text_layout_system.font_store.write(), |
| 44 | CacheKey::new( |
| 45 | id, |
| 46 | glyph_id as u16, |
| 47 | size * scale.x(), |
| 48 | (0., 0.), |
| 49 | CacheKeyFlags::empty(), |
| 50 | ) |
| 51 | .0, |
| 52 | ) |
| 53 | .clone() |
| 54 | .ok_or_else(|| anyhow!("Failed to get raster image"))?; |
| 55 | |
| 56 | let origin = vec2i(image.placement.left, -image.placement.top); |
| 57 | let size = vec2i(image.placement.width as i32, image.placement.height as i32); |
| 58 | Ok(RectI::new(origin, size)) |
| 59 | } |
| 60 | |
| 61 | #[allow(clippy::too_many_arguments)] |
| 62 | pub(super) fn rasterize_glyph( |
| 63 | &self, |
| 64 | font_id: FontId, |
| 65 | size: f32, |
| 66 | glyph_id: GlyphId, |
| 67 | scale: Vector2F, |
| 68 | subpixel_alignment: SubpixelAlignment, |
| 69 | glyph_config: &GlyphConfig, |
| 70 | requested_format: RasterFormat, |
| 71 | ) -> Result<RasterizedGlyph> { |
| 72 | let raster_bounds = |
| 73 | self.glyph_raster_bounds(font_id, size, glyph_id, scale, glyph_config)?; |
| 74 | |
| 75 | let id = *self |
| 76 | .text_layout_system |
| 77 | .font_id_map |
| 78 | .read() |
| 79 | .get_by_left(&font_id) |
| 80 | .unwrap(); |
| 81 | // Get the raster image without caching--the parent FontDB handles all caching for us. |
| 82 | let image = self |
| 83 | .swash_cache |
| 84 | .write() |
| 85 | .get_image_uncached( |
| 86 | &mut self.text_layout_system.font_store.write(), |
| 87 | CacheKey::new( |
| 88 | id, |
| 89 | glyph_id as u16, |
| 90 | size * scale.x(), |
| 91 | (subpixel_alignment.to_offset().x(), 0.), |
| 92 | CacheKeyFlags::empty(), |
| 93 | ) |
| 94 | .0, |
| 95 | ) |
| 96 | .clone() |
| 97 | .unwrap(); |
| 98 | |
| 99 | let (original_format, is_color) = match image.content { |
| 100 | cosmic_text::SwashContent::Mask => (RasterFormat::A8, false), |
| 101 | cosmic_text::SwashContent::SubpixelMask => (RasterFormat::Rgba32, false), |
| 102 | cosmic_text::SwashContent::Color => (RasterFormat::Rgba32, true), |
| 103 | }; |
| 104 | |
| 105 | // Ensure the pixmap is in the correct requested format (in practice this converts A8 to |
| 106 | // RGBA32). |
| 107 | // TODO(alokedesai): Ensure our font rasterization code is robust to returned formats that |
| 108 | // are different than incoming formats. Right now, we create text bounds based on the |
| 109 | // _incoming_ format. |
| 110 | let pixmap = if original_format == RasterFormat::A8 { |
| 111 | let bytes_per_pixel = requested_format.bytes_per_pixel() as usize; |
| 112 | let mut pixmap = Vec::with_capacity(image.data.len() * bytes_per_pixel); |
| 113 | for byte in image.data { |
| 114 | for _ in 0..bytes_per_pixel { |
| 115 | pixmap.push(byte); |
| 116 | } |
| 117 | } |
| 118 | pixmap |
| 119 | } else { |
| 120 | image.data |
| 121 | }; |
| 122 | |
| 123 | let canvas = Canvas { |
| 124 | pixels: pixmap, |
| 125 | size: raster_bounds.size(), |
| 126 | row_stride: image.placement.width as usize * original_format.bytes_per_pixel() as usize, |
| 127 | format: RasterFormat::Rgba32, |
| 128 | }; |
| 129 | |
| 130 | anyhow::Ok(RasterizedGlyph { |
| 131 | canvas, |
| 132 | is_emoji: is_color, |
| 133 | }) |
| 134 | } |
| 135 | } |
| 136 |