Support bitmap fonts (#641)
Resolve bitmap Emoji fonts at scene construction time (rather than the
ideal resolve time for glyph caching) for expedience.
Current status: Layout details aren't entirely correct
---------
Co-authored-by: Kaur Kuut <strom@nevermore.ee>
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cb8574b..dcfb4c4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -230,7 +230,7 @@
- name: cargo test
# TODO: Maybe use --release; the CPU shaders are extremely slow when unoptimised
- run: cargo nextest run --workspace --locked --all-features
+ run: cargo nextest run --workspace --locked --all-features --no-fail-fast
env:
VELLO_CI_GPU_SUPPORT: ${{ matrix.gpu }}
# We are experimenting with git lfs, and we don't expect to run out of bandwidth.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76dfa8f..08a2f44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@
This release has an [MSRV][] of 1.75.
+### Highlights
+
+- Support for most Emoji ([#615][], [#641][] by [@DJMcNab])
+
### Added
- Support blends more than four layers deep ([#657][] by [@DJMcNab][])
@@ -117,10 +121,12 @@
[#575]: https://github.com/linebender/vello/pull/575
[#589]: https://github.com/linebender/vello/pull/589
[#612]: https://github.com/linebender/vello/pull/612
+[#615]: https://github.com/linebender/vello/pull/615
[#619]: https://github.com/linebender/vello/pull/619
[#630]: https://github.com/linebender/vello/pull/630
[#631]: https://github.com/linebender/vello/pull/631
[#635]: https://github.com/linebender/vello/pull/635
+[#641]: https://github.com/linebender/vello/pull/641
[#657]: https://github.com/linebender/vello/pull/657
<!-- Note that this still comparing against 0.2.0, because 0.2.1 is a cherry-picked patch -->
diff --git a/Cargo.lock b/Cargo.lock
index 2c2699f..5c7d5cf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2416,6 +2416,7 @@
"futures-intrusive",
"log",
"peniko",
+ "png",
"raw-window-handle",
"skrifa",
"static_assertions",
diff --git a/examples/assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf b/examples/assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf
new file mode 100644
index 0000000..a87b8c3
--- /dev/null
+++ b/examples/assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf
Binary files differ
diff --git a/examples/assets/noto_color_emoji/README.md b/examples/assets/noto_color_emoji/README.md
index c9d0d06..66e6a39 100644
--- a/examples/assets/noto_color_emoji/README.md
+++ b/examples/assets/noto_color_emoji/README.md
@@ -2,10 +2,12 @@
This folder contains a small subset of [Noto Color Emoji](https://fonts.google.com/noto/specimen/Noto+Color+Emoji), licensed under the [OFL version 1.1](LICENSE).
We do not include the full set of Emoji, because including the entire Emoji set would increase the repository size too much.
-Note that Vello *does* support any COLR emoji (but not Emoji in other formats at the moment).
Included emoji are:
- ✅ Check Mark - \u{2705}/`:white_check_mark:`
- 👀 Eyes - \u{1f440}/`:eyes:`
- 🎉 Party Popper - \u{1f389}/`:party_popper:`
- 🤠 Face with Cowboy Hat - \u{1f920}/`cowboy_hat_face`
+
+These are in the COLR format in `NotoColorEmoji-Subset` and in the CBTF format in `NotoColorEmoji-CBTF-Subset`.
+This covers all ways that Emoji are commonly packaged, and both are supported by Vello.
diff --git a/examples/scenes/src/simple_text.rs b/examples/scenes/src/simple_text.rs
index 842fb8c..8f23569 100644
--- a/examples/scenes/src/simple_text.rs
+++ b/examples/scenes/src/simple_text.rs
@@ -14,13 +14,16 @@
// On Windows, can set this to "c:\\Windows\\Fonts\\seguiemj.ttf" to get color emoji
const ROBOTO_FONT: &[u8] = include_bytes!("../../assets/roboto/Roboto-Regular.ttf");
const INCONSOLATA_FONT: &[u8] = include_bytes!("../../assets/inconsolata/Inconsolata.ttf");
-const NOTO_EMOJI_SUBSET: &[u8] =
+const NOTO_EMOJI_CBTF_SUBSET: &[u8] =
+ include_bytes!("../../assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf");
+const NOTO_EMOJI_COLR_SUBSET: &[u8] =
include_bytes!("../../assets/noto_color_emoji/NotoColorEmoji-Subset.ttf");
pub struct SimpleText {
roboto: Font,
inconsolata: Font,
- noto_emoji_subset: Font,
+ noto_emoji_colr_subset: Font,
+ noto_emoji_cbtf_subset: Font,
}
impl SimpleText {
@@ -29,7 +32,8 @@
Self {
roboto: Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0),
inconsolata: Font::new(Blob::new(Arc::new(INCONSOLATA_FONT)), 0),
- noto_emoji_subset: Font::new(Blob::new(Arc::new(NOTO_EMOJI_SUBSET)), 0),
+ noto_emoji_colr_subset: Font::new(Blob::new(Arc::new(NOTO_EMOJI_COLR_SUBSET)), 0),
+ noto_emoji_cbtf_subset: Font::new(Blob::new(Arc::new(NOTO_EMOJI_CBTF_SUBSET)), 0),
}
}
@@ -42,7 +46,7 @@
/// Note that Vello does support COLR emoji, but does not currently support
/// any other forms of emoji.
#[allow(clippy::too_many_arguments)]
- pub fn add_emoji_run<'a>(
+ pub fn add_colr_emoji_run<'a>(
&mut self,
scene: &mut Scene,
size: f32,
@@ -51,7 +55,39 @@
style: impl Into<StyleRef<'a>>,
text: &str,
) {
- let font = self.noto_emoji_subset.clone();
+ let font = self.noto_emoji_colr_subset.clone();
+ self.add_var_run(
+ scene,
+ Some(&font),
+ size,
+ &[],
+ // This should be unused
+ &Brush::Solid(Color::WHITE),
+ transform,
+ glyph_transform,
+ style,
+ text,
+ );
+ }
+
+ /// Add a text run which supports some emoji.
+ ///
+ /// The supported Emoji are ✅, 👀, 🎉, and 🤠.
+ /// This subset is chosen to demonstrate the emoji support, whilst
+ /// not significantly increasing repository size.
+ ///
+ /// This will use a CBTF font, which Vello supports.
+ #[allow(clippy::too_many_arguments)]
+ pub fn add_bitmap_emoji_run<'a>(
+ &mut self,
+ scene: &mut Scene,
+ size: f32,
+ transform: Affine,
+ glyph_transform: Option<Affine>,
+ style: impl Into<StyleRef<'a>>,
+ text: &str,
+ ) {
+ let font = self.noto_emoji_cbtf_subset.clone();
self.add_var_run(
scene,
Some(&font),
diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs
index 40db189..5e895e0 100644
--- a/examples/scenes/src/test_scenes.rs
+++ b/examples/scenes/src/test_scenes.rs
@@ -99,10 +99,18 @@
pub(super) fn emoji(scene: &mut Scene, params: &mut SceneParams) {
let text_size = 120. + 20. * (params.time * 2.).sin() as f32;
let s = "🎉🤠✅";
- params.text.add_emoji_run(
+ params.text.add_colr_emoji_run(
scene,
text_size,
- Affine::translate(Vec2::new(100., 400.)),
+ Affine::translate(Vec2::new(100., 250.)),
+ None,
+ Fill::NonZero,
+ s,
+ );
+ params.text.add_bitmap_emoji_run(
+ scene,
+ text_size,
+ Affine::translate(Vec2::new(100., 500.)),
None,
Fill::NonZero,
s,
diff --git a/vello/Cargo.toml b/vello/Cargo.toml
index 83ba98d..7857c0c 100644
--- a/vello/Cargo.toml
+++ b/vello/Cargo.toml
@@ -38,3 +38,5 @@
futures-intrusive = { workspace = true }
wgpu-profiler = { workspace = true, optional = true }
thiserror = { workspace = true }
+# TODO: Add feature for built-in bitmap emoji support?
+png = { version = "0.17.13" }
diff --git a/vello/src/scene.rs b/vello/src/scene.rs
index 92aa373..6c558ef 100644
--- a/vello/src/scene.rs
+++ b/vello/src/scene.rs
@@ -1,15 +1,21 @@
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
+mod bitmap;
+
+use std::sync::Arc;
+
use peniko::{
kurbo::{Affine, BezPath, Point, Rect, Shape, Stroke, Vec2},
- BlendMode, Brush, BrushRef, Color, ColorStop, ColorStops, ColorStopsSource, Compose, Extend,
- Fill, Font, Gradient, Image, Mix, StyleRef,
+ BlendMode, Blob, Brush, BrushRef, Color, ColorStop, ColorStops, ColorStopsSource, Compose,
+ Extend, Fill, Font, Gradient, Image, Mix, StyleRef,
};
+use png::{BitDepth, ColorType, Transformations};
use skrifa::{
- color::ColorPainter,
+ color::{ColorGlyph, ColorPainter},
instance::{LocationRef, NormalizedCoord},
outline::{DrawSettings, OutlinePen},
+ prelude::Size,
raw::{tables::cpal::Cpal, TableProvider},
GlyphId, MetadataProvider, OutlineGlyphCollection,
};
@@ -354,17 +360,18 @@
/// Encodes a fill or stroke for the given sequence of glyphs and consumes the builder.
///
- /// The `style` parameter accepts either `Fill` or `&Stroke` types.
+ /// The `style` parameter accepts either `Fill` or `Stroke` types.
///
- /// If the font has COLR support, it will try to draw each glyph using that table first,
- /// falling back to non-COLR rendering. `style` is ignored for COLR glyphs.
+ /// This supports emoji fonts in COLR and bitmap formats.
+ /// `style` is ignored for these fonts.
///
/// For these glyphs, the given [brush](Self::brush) is used as the "foreground colour", and should
/// be [`Solid`](Brush::Solid) for maximum compatibility.
pub fn draw(mut self, style: impl Into<StyleRef<'a>>, glyphs: impl Iterator<Item = Glyph>) {
let font_index = self.run.font.index;
let font = skrifa::FontRef::from_index(self.run.font.data.as_ref(), font_index).unwrap();
- if font.colr().is_ok() && font.cpal().is_ok() {
+ let bitmaps = bitmap::BitmapStrikes::new(&font);
+ if font.colr().is_ok() && font.cpal().is_ok() || !bitmaps.is_empty() {
self.try_draw_colr(style.into(), glyphs);
} else {
// Shortcut path - no need to test each glyph for a colr outline
@@ -410,11 +417,13 @@
let font = skrifa::FontRef::from_index(blob.as_ref(), font_index).unwrap();
let upem: f32 = font.head().map(|h| h.units_per_em()).unwrap().into();
let run_transform = self.run.transform.to_kurbo();
- let scale = Affine::scale_non_uniform(
+ let colr_scale = Affine::scale_non_uniform(
(self.run.font_size / upem).into(),
(-self.run.font_size / upem).into(),
);
+
let colour_collection = font.color_glyphs();
+ let bitmaps = bitmap::BitmapStrikes::new(&font);
let mut final_glyph = None;
let mut outline_count = 0;
// We copy out of the variable font coords here because we need to call an exclusive self method
@@ -423,48 +432,175 @@
.to_vec();
let location = LocationRef::new(coords);
loop {
+ let ppem = self.run.font_size;
let outline_glyphs = (&mut glyphs).take_while(|glyph| {
- match colour_collection.get(GlyphId::new(glyph.id.try_into().unwrap())) {
+ let glyph_id = GlyphId::new(glyph.id.try_into().unwrap());
+ match colour_collection.get(glyph_id) {
Some(color) => {
- final_glyph = Some((color, *glyph));
+ final_glyph = Some((EmojiLikeGlyph::Colr(color), *glyph));
false
}
- None => true,
+ None => match bitmaps.glyph_for_size(Size::new(ppem), glyph_id) {
+ Some(bitmap) => {
+ final_glyph = Some((EmojiLikeGlyph::Bitmap(bitmap), *glyph));
+ false
+ }
+ None => true,
+ },
}
});
self.run.glyphs.start = self.run.glyphs.end;
self.run.stream_offsets = self.scene.encoding.stream_offsets();
outline_count += self.draw_outline_glyphs(clone_style_ref(&style), outline_glyphs);
- let Some((color, glyph)) = final_glyph.take() else {
+ let Some((emoji, glyph)) = final_glyph.take() else {
// All of the remaining glyphs were outline glyphs
break;
};
- let transform = run_transform
- * Affine::translate(Vec2::new(glyph.x.into(), glyph.y.into()))
- * scale
- * self
- .run
- .glyph_transform
- .unwrap_or(Transform::IDENTITY)
- .to_kurbo();
+ match emoji {
+ // TODO: This really needs to be moved to resolve time to get proper caching, etc.
+ EmojiLikeGlyph::Bitmap(bitmap) => {
+ let image = match bitmap.data {
+ bitmap::BitmapData::Bgra(data) => {
+ if bitmap.width * bitmap.height * 4 != data.len().try_into().unwrap() {
+ // TODO: Error once?
+ log::error!("Invalid font");
+ continue;
+ }
+ let data: Box<[u8]> = data
+ .chunks_exact(4)
+ .flat_map(|bytes| {
+ let [b, g, r, a] = bytes.try_into().unwrap();
+ [r, g, b, a]
+ })
+ .collect();
+ Image::new(
+ // TODO: The design of the Blob type forces the double boxing
+ Blob::new(Arc::new(data)),
+ peniko::Format::Rgba8,
+ bitmap.width,
+ bitmap.height,
+ )
+ }
+ bitmap::BitmapData::Png(data) => {
+ let mut decoder = png::Decoder::new(data);
+ decoder.set_transformations(
+ Transformations::ALPHA | Transformations::STRIP_16,
+ );
+ let Ok(mut reader) = decoder.read_info() else {
+ log::error!("Invalid PNG in font");
+ continue;
+ };
- color
- .paint(
- location,
- &mut DrawColorGlyphs {
- scene: self.scene,
- cpal: &font.cpal().unwrap(),
- outlines: &font.outline_glyphs(),
- transform_stack: vec![Transform::from_kurbo(&transform)],
- clip_box: DEFAULT_CLIP_RECT,
- clip_depth: 0,
+ if reader.output_color_type() != (ColorType::Rgba, BitDepth::Eight) {
+ log::error!("Unsupported `output_color_type`");
+ continue;
+ }
+ let mut buf = vec![0; reader.output_buffer_size()].into_boxed_slice();
+
+ let info = reader.next_frame(&mut buf).unwrap();
+ if info.width != bitmap.width || info.height != bitmap.height {
+ log::error!("Unexpected width and height");
+ continue;
+ }
+ Image::new(
+ // TODO: The design of the Blob type forces the double boxing
+ Blob::new(Arc::new(buf)),
+ peniko::Format::Rgba8,
+ bitmap.width,
+ bitmap.height,
+ )
+ }
+ bitmap::BitmapData::Mask(mask) => {
+ // TODO: Is this code worth having?
+ let Some(masks) = bitmap_masks(mask.bpp) else {
+ // TODO: Error once?
+ log::warn!("Invalid bpp in bitmap glyph");
+ continue;
+ };
+
+ if !mask.is_packed {
+ // TODO: Error once?
+ log::warn!("Unpacked mask data in font not yet supported");
+ // TODO: How do we get the font name here?
+ continue;
+ }
+ let alphas = mask.data.iter().flat_map(|it| {
+ masks
+ .iter()
+ .map(move |mask| (it & mask.mask) >> mask.right_shift)
+ });
+ let data: Box<[u8]> = alphas
+ .flat_map(|alpha| [u8::MAX, u8::MAX, u8::MAX, alpha])
+ .collect();
+
+ Image::new(
+ // TODO: The design of the Blob type forces the double boxing
+ Blob::new(Arc::new(data)),
+ peniko::Format::Rgba8,
+ bitmap.width,
+ bitmap.height,
+ )
+ }
+ };
+ // Split into multiple statements because rustfmt breaks
+ let transform =
+ run_transform.then_translate(Vec2::new(glyph.x.into(), glyph.y.into()));
+
+ // Logic copied from Skia without examination or careful understanding:
+ // https://github.com/google/skia/blob/61ac357e8e3338b90fb84983100d90768230797f/src/ports/SkTypeface_fontations.cpp#L664
+
+ let image_scale_factor = self.run.font_size / bitmap.ppem_y;
+ let font_units_to_size = self.run.font_size / upem;
+ let transform = transform
+ .pre_translate(Vec2 {
+ x: (-bitmap.bearing_x * font_units_to_size).into(),
+ y: (bitmap.bearing_y * font_units_to_size).into(),
+ })
+ // Unclear why this isn't non-uniform
+ .pre_scale(image_scale_factor.into())
+ .pre_translate(Vec2 {
+ x: (-bitmap.inner_bearing_x).into(),
+ y: (-bitmap.inner_bearing_y).into(),
+ });
+ let mut transform = match bitmap.placement_origin {
+ bitmap::Origin::TopLeft => transform,
+ bitmap::Origin::BottomLeft => transform.pre_translate(Vec2 {
+ x: 0.,
+ y: f64::from(image.height),
+ }),
+ };
+ if let Some(glyph_transform) = self.run.glyph_transform {
+ transform *= glyph_transform.to_kurbo();
+ }
+ self.scene.draw_image(&image, transform);
+ }
+ EmojiLikeGlyph::Colr(colr) => {
+ let transform = run_transform
+ * Affine::translate(Vec2::new(glyph.x.into(), glyph.y.into()))
+ * colr_scale
+ * self
+ .run
+ .glyph_transform
+ .unwrap_or(Transform::IDENTITY)
+ .to_kurbo();
+ colr.paint(
location,
- foreground_brush: self.brush.clone(),
- },
- )
- .unwrap();
+ &mut DrawColorGlyphs {
+ scene: self.scene,
+ cpal: &font.cpal().unwrap(),
+ outlines: &font.outline_glyphs(),
+ transform_stack: vec![Transform::from_kurbo(&transform)],
+ clip_box: DEFAULT_CLIP_RECT,
+ clip_depth: 0,
+ location,
+ foreground_brush: self.brush.clone(),
+ },
+ )
+ .unwrap();
+ }
+ }
}
if outline_count == 0 {
// If we didn't draw any outline glyphs, the encoded variable font parameters were never used
@@ -477,6 +613,64 @@
}
}
}
+
+struct BitmapMask {
+ mask: u8,
+ right_shift: u8,
+}
+
+fn bitmap_masks(bpp: u8) -> Option<&'static [BitmapMask]> {
+ const fn m(mask: u8, right_shift: u8) -> BitmapMask {
+ BitmapMask { mask, right_shift }
+ }
+ const fn byte(value: u8) -> BitmapMask {
+ BitmapMask {
+ mask: 1 << value,
+ right_shift: value,
+ }
+ }
+ match bpp {
+ 1 => {
+ const BPP_1_MASK: &[BitmapMask] = &[
+ byte(0),
+ byte(1),
+ byte(2),
+ byte(3),
+ byte(4),
+ byte(5),
+ byte(6),
+ byte(7),
+ ];
+ Some(BPP_1_MASK)
+ }
+
+ 2 => {
+ const BPP_2_MASK: &[BitmapMask] = {
+ &[
+ m(0b0000_0011, 0),
+ m(0b0000_1100, 2),
+ m(0b0011_0000, 4),
+ m(0b1100_0000, 6),
+ ]
+ };
+ Some(BPP_2_MASK)
+ }
+ 4 => {
+ const BPP_4_MASK: &[BitmapMask] = &[m(0b0000_1111, 0), m(0b1111_0000, 4)];
+ Some(BPP_4_MASK)
+ }
+ 8 => {
+ const BPP_8_MASK: &[BitmapMask] = &[m(u8::MAX, 0)];
+ Some(BPP_8_MASK)
+ }
+ _ => None,
+ }
+}
+
+enum EmojiLikeGlyph<'a> {
+ Bitmap(bitmap::BitmapGlyph<'a>),
+ Colr(ColorGlyph<'a>),
+}
const BOUND: f64 = 100_000.;
// Hack: If we don't have a clip box, we guess a rectangle we hope is big enough
const DEFAULT_CLIP_RECT: Rect = Rect::new(-BOUND, -BOUND, BOUND, BOUND);
diff --git a/vello/src/scene/bitmap.rs b/vello/src/scene/bitmap.rs
new file mode 100644
index 0000000..3795f55
--- /dev/null
+++ b/vello/src/scene/bitmap.rs
@@ -0,0 +1,366 @@
+// Copyright 2024 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+// Based on https://github.com/googlefonts/fontations/blob/cbdf8b485e955e3acee40df1344e33908805ed31/skrifa/src/bitmap.rs
+#![allow(warnings)]
+
+//! Bitmap strikes and glyphs.
+use skrifa::{
+ instance::{LocationRef, Size},
+ metrics::GlyphMetrics,
+ raw::{
+ tables::{bitmap, cbdt, cblc, ebdt, eblc, sbix},
+ types::{GlyphId, Tag},
+ FontData, TableProvider,
+ },
+ MetadataProvider,
+};
+
+/// Set of strikes, each containing embedded bitmaps of a single size.
+#[derive(Clone)]
+pub struct BitmapStrikes<'a>(StrikesKind<'a>);
+
+impl<'a> BitmapStrikes<'a> {
+ /// Creates a new `BitmapStrikes` for the given font.
+ ///
+ /// This will prefer `sbix`, `CBDT`, and `CBLC` formats in that order.
+ ///
+ /// To select a specific format, use [`with_format`](Self::with_format).
+ pub fn new(font: &impl TableProvider<'a>) -> Self {
+ for format in [BitmapFormat::Sbix, BitmapFormat::Cbdt, BitmapFormat::Ebdt] {
+ if let Some(strikes) = Self::with_format(font, format) {
+ return strikes;
+ }
+ }
+ Self(StrikesKind::None)
+ }
+
+ /// Creates a new `BitmapStrikes` for the given font and format.
+ ///
+ /// Returns `None` if the requested format is not available.
+ pub fn with_format(font: &impl TableProvider<'a>, format: BitmapFormat) -> Option<Self> {
+ let kind = match format {
+ BitmapFormat::Sbix => StrikesKind::Sbix(
+ font.sbix().ok()?,
+ font.glyph_metrics(Size::unscaled(), LocationRef::default()),
+ ),
+ BitmapFormat::Cbdt => {
+ StrikesKind::Cbdt(CbdtTables::new(font.cblc().ok()?, font.cbdt().ok()?))
+ }
+ BitmapFormat::Ebdt => {
+ StrikesKind::Ebdt(EbdtTables::new(font.eblc().ok()?, font.ebdt().ok()?))
+ }
+ };
+ Some(Self(kind))
+ }
+
+ /// Returns the format representing the underlying table for this set of
+ /// strikes.
+ pub fn format(&self) -> Option<BitmapFormat> {
+ match &self.0 {
+ StrikesKind::None => None,
+ StrikesKind::Sbix(..) => Some(BitmapFormat::Sbix),
+ StrikesKind::Cbdt(..) => Some(BitmapFormat::Cbdt),
+ StrikesKind::Ebdt(..) => Some(BitmapFormat::Ebdt),
+ }
+ }
+
+ /// Returns the number of available strikes.
+ pub fn len(&self) -> usize {
+ match &self.0 {
+ StrikesKind::None => 0,
+ StrikesKind::Sbix(sbix, _) => sbix.strikes().len(),
+ StrikesKind::Cbdt(cbdt) => cbdt.location.bitmap_sizes().len(),
+ StrikesKind::Ebdt(ebdt) => ebdt.location.bitmap_sizes().len(),
+ }
+ }
+
+ /// Returns true if there are no available strikes.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Returns the strike at the given index.
+ pub fn get(&self, index: usize) -> Option<BitmapStrike<'a>> {
+ let kind = match &self.0 {
+ StrikesKind::None => return None,
+ StrikesKind::Sbix(sbix, metrics) => {
+ StrikeKind::Sbix(sbix.strikes().get(index).ok()?, metrics.clone())
+ }
+ StrikesKind::Cbdt(tables) => StrikeKind::Cbdt(
+ tables.location.bitmap_sizes().get(index).copied()?,
+ tables.clone(),
+ ),
+ StrikesKind::Ebdt(tables) => StrikeKind::Ebdt(
+ tables.location.bitmap_sizes().get(index).copied()?,
+ tables.clone(),
+ ),
+ };
+ Some(BitmapStrike(kind))
+ }
+
+ /// Returns the best matching glyph for the given size and glyph
+ /// identifier.
+ ///
+ /// In this case, "best" means a glyph of the exact size, nearest larger
+ /// size, or nearest smaller size, in that order.
+ pub fn glyph_for_size(&self, size: Size, glyph_id: GlyphId) -> Option<BitmapGlyph<'a>> {
+ // Return the largest size for an unscaled request
+ let size = size.ppem().unwrap_or(f32::MAX);
+ self.iter()
+ .fold(None, |best: Option<BitmapGlyph<'a>>, entry| {
+ let entry_size = entry.ppem();
+ if let Some(best) = best {
+ let best_size = best.ppem_y;
+ if (entry_size >= size && entry_size < best_size)
+ || (best_size < size && entry_size > best_size)
+ {
+ entry.get(glyph_id).or(Some(best))
+ } else {
+ Some(best)
+ }
+ } else {
+ entry.get(glyph_id)
+ }
+ })
+ }
+
+ /// Returns an iterator over all available strikes.
+ pub fn iter(&self) -> impl Iterator<Item = BitmapStrike<'a>> + 'a + Clone {
+ let this = self.clone();
+ (0..this.len()).filter_map(move |ix| this.get(ix))
+ }
+}
+
+#[derive(Clone)]
+enum StrikesKind<'a> {
+ None,
+ Sbix(sbix::Sbix<'a>, GlyphMetrics<'a>),
+ Cbdt(CbdtTables<'a>),
+ Ebdt(EbdtTables<'a>),
+}
+
+/// Set of embedded bitmap glyphs of a specific size.
+#[derive(Clone)]
+pub struct BitmapStrike<'a>(StrikeKind<'a>);
+
+impl<'a> BitmapStrike<'a> {
+ /// Returns the pixels-per-em (size) of this strike.
+ pub fn ppem(&self) -> f32 {
+ match &self.0 {
+ StrikeKind::Sbix(sbix, _) => sbix.ppem() as f32,
+ StrikeKind::Cbdt(size, _) => size.ppem_y() as f32,
+ StrikeKind::Ebdt(size, _) => size.ppem_y() as f32,
+ }
+ }
+
+ /// Returns a bitmap glyph for the given identifier, if available.
+ pub fn get(&self, glyph_id: GlyphId) -> Option<BitmapGlyph<'a>> {
+ match &self.0 {
+ StrikeKind::Sbix(sbix, metrics) => {
+ let glyph = sbix.glyph_data(glyph_id).ok()??;
+ if glyph.graphic_type() != Tag::new(b"PNG ") {
+ return None;
+ }
+ let glyf_bb = metrics.bounds(glyph_id).unwrap_or_default();
+ let lsb = metrics.left_side_bearing(glyph_id).unwrap_or_default();
+ let ppem = sbix.ppem() as f32;
+ let png_data = glyph.data();
+ // PNG format:
+ // 8 byte header, IHDR chunk (4 byte length, 4 byte chunk type), width, height
+ let reader = FontData::new(png_data);
+ let width = reader.read_at::<u32>(16).ok()?;
+ let height = reader.read_at::<u32>(20).ok()?;
+ Some(BitmapGlyph {
+ data: BitmapData::Png(glyph.data()),
+ bearing_x: lsb,
+ bearing_y: glyf_bb.y_min as f32,
+ inner_bearing_x: glyph.origin_offset_x() as f32,
+ inner_bearing_y: glyph.origin_offset_y() as f32,
+ ppem_x: ppem,
+ ppem_y: ppem,
+ width,
+ height,
+ advance: metrics.advance_width(glyph_id).unwrap_or_default(),
+ placement_origin: Origin::BottomLeft,
+ })
+ }
+ StrikeKind::Cbdt(size, tables) => {
+ let location = size
+ .location(tables.location.offset_data(), glyph_id)
+ .ok()?;
+ let data = tables.data.data(&location).ok()?;
+ BitmapGlyph::from_bdt(&size, &data)
+ }
+ StrikeKind::Ebdt(size, tables) => {
+ let location = size
+ .location(tables.location.offset_data(), glyph_id)
+ .ok()?;
+ let data = tables.data.data(&location).ok()?;
+ BitmapGlyph::from_bdt(&size, &data)
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+enum StrikeKind<'a> {
+ Sbix(sbix::Strike<'a>, GlyphMetrics<'a>),
+ Cbdt(bitmap::BitmapSize, CbdtTables<'a>),
+ Ebdt(bitmap::BitmapSize, EbdtTables<'a>),
+}
+
+#[derive(Clone)]
+struct BdtTables<L, D> {
+ location: L,
+ data: D,
+}
+
+impl<L, D> BdtTables<L, D> {
+ fn new(location: L, data: D) -> Self {
+ Self { location, data }
+ }
+}
+
+type CbdtTables<'a> = BdtTables<cblc::Cblc<'a>, cbdt::Cbdt<'a>>;
+type EbdtTables<'a> = BdtTables<eblc::Eblc<'a>, ebdt::Ebdt<'a>>;
+
+/// An embedded bitmap glyph.
+#[derive(Clone)]
+pub struct BitmapGlyph<'a> {
+ pub data: BitmapData<'a>,
+ pub bearing_x: f32,
+ pub bearing_y: f32,
+ pub inner_bearing_x: f32,
+ pub inner_bearing_y: f32,
+ pub ppem_x: f32,
+ pub ppem_y: f32,
+ pub advance: f32,
+ pub width: u32,
+ pub height: u32,
+ pub placement_origin: Origin,
+}
+
+impl<'a> BitmapGlyph<'a> {
+ fn from_bdt(
+ bitmap_size: &bitmap::BitmapSize,
+ bitmap_data: &bitmap::BitmapData<'a>,
+ ) -> Option<Self> {
+ let metrics = BdtMetrics::new(&bitmap_data);
+ let (ppem_x, ppem_y) = (bitmap_size.ppem_x() as f32, bitmap_size.ppem_y() as f32);
+ let bpp = bitmap_size.bit_depth();
+ let data = match bpp {
+ 32 => {
+ match &bitmap_data.content {
+ bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::Png, bytes) => {
+ BitmapData::Png(bytes)
+ }
+ // 32-bit formats are always byte aligned
+ bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::ByteAligned, bytes) => {
+ BitmapData::Bgra(bytes)
+ }
+ _ => return None,
+ }
+ }
+ 1 | 2 | 4 | 8 => {
+ let (data, is_packed) = match &bitmap_data.content {
+ bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::ByteAligned, bytes) => {
+ (bytes, false)
+ }
+ bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::BitAligned, bytes) => {
+ (bytes, true)
+ }
+ _ => return None,
+ };
+ BitmapData::Mask(MaskData {
+ bpp,
+ is_packed,
+ data,
+ })
+ }
+ // All other bit depth values are invalid
+ _ => return None,
+ };
+ Some(Self {
+ data,
+ bearing_x: 0.0,
+ bearing_y: 0.0,
+ inner_bearing_x: metrics.inner_bearing_x,
+ inner_bearing_y: metrics.inner_bearing_y,
+ ppem_x,
+ ppem_y,
+ width: metrics.width,
+ height: metrics.height,
+ advance: metrics.advance,
+ placement_origin: Origin::TopLeft,
+ })
+ }
+}
+
+struct BdtMetrics {
+ inner_bearing_x: f32,
+ inner_bearing_y: f32,
+ advance: f32,
+ width: u32,
+ height: u32,
+}
+
+impl BdtMetrics {
+ fn new(data: &bitmap::BitmapData) -> Self {
+ match data.metrics {
+ bitmap::BitmapMetrics::Small(metrics) => Self {
+ inner_bearing_x: metrics.bearing_x() as f32,
+ inner_bearing_y: metrics.bearing_y() as f32,
+ advance: metrics.advance() as f32,
+ width: metrics.width() as u32,
+ height: metrics.height() as u32,
+ },
+ bitmap::BitmapMetrics::Big(metrics) => Self {
+ inner_bearing_x: metrics.hori_bearing_x() as f32,
+ inner_bearing_y: metrics.hori_bearing_y() as f32,
+ advance: metrics.hori_advance() as f32,
+ width: metrics.width() as u32,
+ height: metrics.height() as u32,
+ },
+ }
+ }
+}
+
+/// Determines the origin point for drawing a bitmap glyph.
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum Origin {
+ TopLeft,
+ BottomLeft,
+}
+
+/// Data content of a bitmap.
+#[derive(Clone)]
+pub enum BitmapData<'a> {
+ /// Uncompressed 32-bit color bitmap data, pre-multiplied in BGRA order
+ /// and encoded in the sRGB color space.
+ Bgra(&'a [u8]),
+ /// Compressed PNG bitmap data.
+ Png(&'a [u8]),
+ /// Data representing a single channel alpha mask.
+ Mask(MaskData<'a>),
+}
+
+/// A single channel alpha mask.
+#[derive(Clone)]
+pub struct MaskData<'a> {
+ /// Number of bits-per-pixel. Always 1, 2, 4 or 8.
+ pub bpp: u8,
+ /// True if each row of the data is bit-aligned. Otherwise, each row
+ /// is padded to the next byte.
+ pub is_packed: bool,
+ /// Raw bitmap data.
+ pub data: &'a [u8],
+}
+
+/// The format (or table) containing the data backing a set of bitmap strikes.
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+pub enum BitmapFormat {
+ Sbix,
+ Cbdt,
+ Ebdt,
+}
diff --git a/vello_tests/smoke_snapshots/single_emoji.png b/vello_tests/smoke_snapshots/single_emoji.png
deleted file mode 100644
index 90c7888..0000000
--- a/vello_tests/smoke_snapshots/single_emoji.png
+++ /dev/null
Binary files differ
diff --git a/vello_tests/smoke_snapshots/two_emoji.png b/vello_tests/smoke_snapshots/two_emoji.png
new file mode 100644
index 0000000..2ae63c5
--- /dev/null
+++ b/vello_tests/smoke_snapshots/two_emoji.png
Binary files differ
diff --git a/vello_tests/snapshots/big_bitmap.png b/vello_tests/snapshots/big_bitmap.png
new file mode 100644
index 0000000..f27109e
--- /dev/null
+++ b/vello_tests/snapshots/big_bitmap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:261a21526b5f4f95751422baadea7b19dc4eb3a49be48fba21e3c0bbf3d24a9d
+size 14683
diff --git a/vello_tests/snapshots/big_colr.png b/vello_tests/snapshots/big_colr.png
new file mode 100644
index 0000000..40386f9
--- /dev/null
+++ b/vello_tests/snapshots/big_colr.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f10d642ad14b33815998273a8a44fb6ddb1d7b8e161b550d498200a584f8370f
+size 15853
diff --git a/vello_tests/snapshots/bitmap_undef.png b/vello_tests/snapshots/bitmap_undef.png
new file mode 100644
index 0000000..1a124e0
--- /dev/null
+++ b/vello_tests/snapshots/bitmap_undef.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f88a18a4b17eb2f3e4e8057a7ebc47930f2d4a94e8fc0bb397264bac93b29725
+size 289
diff --git a/vello_tests/snapshots/colr_undef.png b/vello_tests/snapshots/colr_undef.png
new file mode 100644
index 0000000..1a124e0
--- /dev/null
+++ b/vello_tests/snapshots/colr_undef.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f88a18a4b17eb2f3e4e8057a7ebc47930f2d4a94e8fc0bb397264bac93b29725
+size 289
diff --git a/vello_tests/snapshots/little_bitmap.png b/vello_tests/snapshots/little_bitmap.png
new file mode 100644
index 0000000..542f8b1
--- /dev/null
+++ b/vello_tests/snapshots/little_bitmap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcee2e5d10e4a95a3566de5d4d97d8b3ad02e749567c0faf6d67259932309df0
+size 1562
diff --git a/vello_tests/snapshots/little_colr.png b/vello_tests/snapshots/little_colr.png
new file mode 100644
index 0000000..64a1122
--- /dev/null
+++ b/vello_tests/snapshots/little_colr.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4aabea4b8cbf754fdf29c9d454f0880bfb9b0d380818d2ddb1d9128aa80fddb9
+size 2039
diff --git a/vello_tests/tests/emoji.rs b/vello_tests/tests/emoji.rs
new file mode 100644
index 0000000..1d8f9b8
--- /dev/null
+++ b/vello_tests/tests/emoji.rs
@@ -0,0 +1,134 @@
+// Copyright 2024 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//! Snapshot tests for Emoji [`scenes`].
+
+use scenes::SimpleText;
+use vello::{kurbo::Affine, peniko::Fill, Scene};
+use vello_tests::{snapshot_test_sync, TestParams};
+
+fn encode_noto_colr(text: &str, font_size: f32) -> Scene {
+ let mut scene = Scene::new();
+ let mut simple_text = SimpleText::new();
+ simple_text.add_colr_emoji_run(
+ &mut scene,
+ font_size,
+ Affine::translate((0., f64::from(font_size))),
+ None,
+ Fill::EvenOdd,
+ text,
+ );
+ scene
+}
+
+fn encode_noto_bitmap(text: &str, font_size: f32) -> Scene {
+ let mut scene = Scene::new();
+ let mut simple_text = SimpleText::new();
+ simple_text.add_bitmap_emoji_run(
+ &mut scene,
+ font_size,
+ Affine::translate((0., f64::from(font_size))),
+ None,
+ Fill::EvenOdd,
+ text,
+ );
+ scene
+}
+
+/// The Emoji supported by our font subset.
+const TEXT: &str = "✅👀🎉🤠";
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn big_colr() {
+ let font_size = 48.;
+ let scene = encode_noto_colr(TEXT, font_size);
+ let params = TestParams::new(
+ "big_colr",
+ (font_size * 10.) as _,
+ // Noto Emoji seem to be about 25% bigger than the actual font_size suggests
+ (font_size * 1.25).ceil() as _,
+ );
+ snapshot_test_sync(scene, ¶ms)
+ .unwrap()
+ .assert_mean_less_than(0.001);
+}
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn little_colr() {
+ let font_size = 10.;
+ let scene = encode_noto_colr(TEXT, font_size);
+ let params = TestParams::new(
+ "little_colr",
+ (font_size * 10.) as _,
+ (font_size * 1.25).ceil() as _,
+ );
+ snapshot_test_sync(scene, ¶ms)
+ .unwrap()
+ .assert_mean_less_than(0.002);
+}
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn colr_undef() {
+ let font_size = 10.;
+ // This emoji isn't in the subset we have made
+ let scene = encode_noto_colr("🤷", font_size);
+ let params = TestParams::new(
+ "colr_undef",
+ (font_size * 10.) as _,
+ (font_size * 1.25).ceil() as _,
+ );
+ // TODO: Work out why the undef glyph is nothing - is it an issue with our font subset or with our renderer?
+ snapshot_test_sync(scene, ¶ms)
+ .unwrap()
+ .assert_mean_less_than(0.001);
+}
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn big_bitmap() {
+ let font_size = 48.;
+ let scene = encode_noto_bitmap(TEXT, font_size);
+ let params = TestParams::new(
+ "big_bitmap",
+ (font_size * 10.) as _,
+ (font_size * 1.25).ceil() as _,
+ );
+ snapshot_test_sync(scene, ¶ms)
+ .unwrap()
+ .assert_mean_less_than(0.001);
+}
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn little_bitmap() {
+ let font_size = 10.;
+ let scene = encode_noto_bitmap(TEXT, font_size);
+ let params = TestParams::new(
+ "little_bitmap",
+ (font_size * 10.) as _,
+ (font_size * 1.25).ceil() as _,
+ );
+ snapshot_test_sync(scene, ¶ms)
+ .unwrap()
+ .assert_mean_less_than(0.001);
+}
+
+#[test]
+#[cfg_attr(skip_gpu_tests, ignore)]
+fn bitmap_undef() {
+ let font_size = 10.;
+ // This emoji isn't in the subset we have made
+ let scene = encode_noto_bitmap("🤷", font_size);
+ let params = TestParams::new(
+ "bitmap_undef",
+ (font_size * 10.) as _,
+ (font_size * 1.25).ceil() as _,
+ );
+ // TODO: Work out why the undef glyph is nothing - is it an issue with our font subset or with our renderer?
+ snapshot_test_sync(scene, ¶ms)
+ .unwrap()
+ .assert_mean_less_than(0.001);
+}
diff --git a/vello_tests/tests/smoke_snapshots.rs b/vello_tests/tests/smoke_snapshots.rs
index 9bac177..1301581 100644
--- a/vello_tests/tests/smoke_snapshots.rs
+++ b/vello_tests/tests/smoke_snapshots.rs
@@ -47,10 +47,10 @@
.assert_mean_less_than(0.01);
}
-fn single_emoji(use_cpu: bool) {
+fn two_emoji(use_cpu: bool) {
let mut scene = Scene::new();
let mut text = SimpleText::new();
- text.add_emoji_run(
+ text.add_colr_emoji_run(
&mut scene,
24.,
Affine::translate((0., 24.)),
@@ -58,9 +58,17 @@
Fill::NonZero,
"🤠",
);
+ text.add_bitmap_emoji_run(
+ &mut scene,
+ 24.,
+ Affine::translate((30., 24.)),
+ None,
+ Fill::NonZero,
+ "🤠",
+ );
let params = TestParams {
use_cpu,
- ..TestParams::new("single_emoji", 30, 30)
+ ..TestParams::new("two_emoji", 60, 30)
};
smoke_snapshot_test_sync(scene, ¶ms)
.unwrap()
@@ -95,12 +103,12 @@
#[test]
#[cfg_attr(skip_gpu_tests, ignore)]
-fn single_emoji_gpu() {
- single_emoji(false);
+fn two_emoji_gpu() {
+ two_emoji(false);
}
#[test]
#[cfg_attr(skip_gpu_tests, ignore)]
-fn single_emoji_cpu() {
- single_emoji(true);
+fn two_emoji_cpu() {
+ two_emoji(true);
}
diff --git a/vello_tests/tests/snapshots.rs b/vello_tests/tests/snapshot_test_scenes.rs
similarity index 96%
rename from vello_tests/tests/snapshots.rs
rename to vello_tests/tests/snapshot_test_scenes.rs
index 98894de..97090c8 100644
--- a/vello_tests/tests/snapshots.rs
+++ b/vello_tests/tests/snapshot_test_scenes.rs
@@ -1,8 +1,7 @@
// Copyright 2024 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
-// Copyright 2024 the Vello Authors
-// SPDX-License-Identifier: Apache-2.0 OR MIT
+//! Snapshot tests using the test scenes from [`scenes`].
use scenes::{test_scenes, ExampleScene};
use vello_tests::{encode_test_scene, snapshot_test_sync, TestParams};