diff --git a/Cargo.lock b/Cargo.lock index 11f5d31bd..52a2d3984 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.0" @@ -676,6 +682,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -862,6 +879,21 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "fontique" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5fcb214137f01bc842c4fd633236255c51f8a24c6d3923eb8361c6d0940737" +dependencies = [ + "bytemuck", + "hashbrown 0.15.2", + "icu_locid", + "memmap2", + "peniko", + "read-fonts", + "smallvec", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1092,6 +1124,8 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1139,6 +1173,18 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", +] + [[package]] name = "id-arena" version = "2.2.1" @@ -1401,6 +1447,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + [[package]] name = "litrs" version = "0.4.1" @@ -1744,9 +1796,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -1931,6 +1983,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parley" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d1c2b33b240c246f06cfceac48dc6c96040cb177d2aa5348899982b298b5577" +dependencies = [ + "fontique", + "hashbrown 0.15.2", + "peniko", + "skrifa", + "swash", +] + [[package]] name = "paste" version = "1.0.15" @@ -2484,9 +2549,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.26.4" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6667e349abb2e6e850b31bc638a11f7fadd7e4cf113b71947c46bf6d5fe0dbc9" +checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d" dependencies = [ "bytemuck", "read-fonts", @@ -2512,9 +2577,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "smithay-client-toolkit" @@ -2630,6 +2695,17 @@ dependencies = [ "siphasher", ] +[[package]] +name = "swash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5bbc2aa266907ed8ee977c9c9e16363cc2b001266104e13397b57f1d15f71" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "syn" version = "2.0.96" @@ -2745,6 +2821,15 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2994,7 +3079,9 @@ dependencies = [ name = "vello_common" version = "0.4.0" dependencies = [ + "bytemuck", "roxmltree", + "skrifa", "vello_api", ] @@ -3004,6 +3091,7 @@ version = "0.4.0" dependencies = [ "image", "oxipng", + "skrifa", "vello_common", ] @@ -3023,9 +3111,11 @@ name = "vello_hybrid" version = "0.4.0" dependencies = [ "bytemuck", + "parley", "png", "pollster", "roxmltree", + "skrifa", "vello_common", "wgpu", "winit", @@ -3966,6 +4056,12 @@ dependencies = [ "winit", ] +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -4044,6 +4140,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "zeno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc0de2315dc13d00e5df3cd6b8d2124a6eaec6a2d4b6a1c5f37b7efad17fcc17" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/sparse_strips/vello_api/src/glyph.rs b/sparse_strips/vello_api/src/glyph.rs new file mode 100644 index 000000000..28d8b020d --- /dev/null +++ b/sparse_strips/vello_api/src/glyph.rs @@ -0,0 +1,18 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Types for glyphs. + +/// Positioned glyph. +#[derive(Copy, Clone, Default, Debug)] +pub struct Glyph { + /// The font-specific identifier for this glyph. + /// + /// This ID is specific to the font being used and corresponds to the + /// glyph index within that font. It is *not* a Unicode code point. + pub id: u32, + /// X-offset in run, relative to transform. + pub x: f32, + /// Y-offset in run, relative to transform. + pub y: f32, +} diff --git a/sparse_strips/vello_api/src/lib.rs b/sparse_strips/vello_api/src/lib.rs index 5973f4c44..7abb751e8 100644 --- a/sparse_strips/vello_api/src/lib.rs +++ b/sparse_strips/vello_api/src/lib.rs @@ -12,4 +12,5 @@ pub use peniko; pub use peniko::color; pub use peniko::kurbo; pub mod execute; +pub mod glyph; pub mod paint; diff --git a/sparse_strips/vello_common/Cargo.toml b/sparse_strips/vello_common/Cargo.toml index 0ca6bdc96..2b639c482 100644 --- a/sparse_strips/vello_common/Cargo.toml +++ b/sparse_strips/vello_common/Cargo.toml @@ -15,6 +15,8 @@ publish = false vello_api = { workspace = true, default-features = true } # for pico_svg roxmltree = "0.20.0" +bytemuck = { workspace = true, features = [] } +skrifa = { workspace = true } [features] simd = ["vello_api/simd"] diff --git a/sparse_strips/vello_common/src/glyph.rs b/sparse_strips/vello_common/src/glyph.rs new file mode 100644 index 000000000..550bb042f --- /dev/null +++ b/sparse_strips/vello_common/src/glyph.rs @@ -0,0 +1,325 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Processing and drawing glyphs. + +use crate::peniko::Font; +use skrifa::instance::Size; +use skrifa::outline::DrawSettings; +use skrifa::{ + GlyphId, MetadataProvider, + outline::{HintingInstance, HintingOptions, OutlinePen}, +}; +use vello_api::kurbo::{Affine, BezPath, Vec2}; + +pub use vello_api::glyph::*; + +/// A glyph prepared for rendering. +#[derive(Debug)] +pub enum PreparedGlyph<'a> { + /// A glyph defined by its outline. + Outline(OutlineGlyph<'a>), + // TODO: Image and Colr variants. +} + +/// A glyph defined by a path (its outline) and a local transform. +#[derive(Debug)] +pub struct OutlineGlyph<'a> { + /// The path of the glyph. + pub path: &'a BezPath, + /// The global transform of the glyph. + pub transform: Affine, +} + +/// Trait for types that can render glyphs. +pub trait GlyphRenderer { + /// Fill glyphs with the current paint and fill rule. + fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>); + + /// Stroke glyphs with the current paint and stroke settings. + fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>); +} + +/// A builder for configuring and drawing glyphs. +#[derive(Debug)] +pub struct GlyphRunBuilder<'a, T: GlyphRenderer + 'a> { + run: GlyphRun<'a>, + renderer: &'a mut T, +} + +impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> { + /// Creates a new builder for drawing glyphs. + pub fn new(font: Font, transform: Affine, renderer: &'a mut T) -> Self { + Self { + run: GlyphRun { + font, + font_size: 16.0, + transform, + glyph_transform: None, + hint: true, + normalized_coords: &[], + }, + renderer, + } + } + + /// Set the font size in pixels per em. + pub fn font_size(mut self, size: f32) -> Self { + self.run.font_size = size; + self + } + + /// Set the per-glyph transform. Can be used to apply skew to simulate italic text. + pub fn glyph_transform(mut self, transform: Affine) -> Self { + self.run.glyph_transform = Some(transform); + self + } + + /// Set whether font hinting is enabled. + pub fn hint(mut self, hint: bool) -> Self { + self.run.hint = hint; + self + } + + /// Set normalized variation coordinates for variable fonts. + pub fn normalized_coords(mut self, coords: &'a [NormalizedCoord]) -> Self { + self.run.normalized_coords = bytemuck::cast_slice(coords); + self + } + + /// Consumes the builder and fills the glyphs with the current configuration. + pub fn fill_glyphs(self, glyphs: impl Iterator) { + self.render(glyphs, Style::Fill); + } + + /// Consumes the builder and strokes the glyphs with the current configuration. + pub fn stroke_glyphs(self, glyphs: impl Iterator) { + self.render(glyphs, Style::Stroke); + } + + fn render(self, glyphs: impl Iterator, style: Style) { + let run = self.run; + let font = skrifa::FontRef::from_index(run.font.data.as_ref(), run.font.index).unwrap(); + let outlines = font.outline_glyphs(); + let (transform, size, scale, hinting_instance) = if run.hint { + // Hinting doesn't make sense if we later scale the glyphs via `transform`. So, if this glyph can be + // scaled uniformly, we scale its font size and use that for hinting. If the glyph is rotated or skewed, + // hinting is not applicable. + if let Some((scale, transform)) = take_uniform_scale(run.transform) { + let size = Size::new(run.font_size * scale as f32); + ( + transform, + size, + scale, + HintingInstance::new(&outlines, size, run.normalized_coords, HINTING_OPTIONS) + .ok(), + ) + } else { + (run.transform, Size::new(run.font_size), 1.0, None) + } + } else { + (run.transform, Size::new(run.font_size), 1.0, None) + }; + + let render_glyph = match style { + Style::Fill => GlyphRenderer::fill_glyph, + Style::Stroke => GlyphRenderer::stroke_glyph, + }; + // Reuse the same `path` allocation for each glyph. + let mut path = OutlinePath(BezPath::new()); + for glyph in glyphs { + let draw_settings = if let Some(hinting_instance) = &hinting_instance { + DrawSettings::hinted(hinting_instance, false) + } else { + DrawSettings::unhinted(size, run.normalized_coords) + }; + let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else { + continue; + }; + path.0.truncate(0); + if outline.draw(draw_settings, &mut path).is_err() { + continue; + } + let mut local_transform = + Affine::translate(Vec2::new(glyph.x as f64 * scale, glyph.y as f64 * scale)); + if let Some(glyph_transform) = run.glyph_transform { + local_transform *= glyph_transform; + } + + render_glyph( + self.renderer, + PreparedGlyph::Outline(OutlineGlyph { + path: &path.0, + transform: transform * local_transform, + }), + ); + } + } +} + +enum Style { + Fill, + Stroke, +} + +/// A sequence of glyphs with shared rendering properties. +#[derive(Clone, Debug)] +struct GlyphRun<'a> { + /// Font for all glyphs in the run. + font: Font, + /// Size of the font in pixels per em. + font_size: f32, + /// Global transform. + transform: Affine, + /// Per-glyph transform. Can be used to apply skew to simulate italic text. + glyph_transform: Option, + /// Normalized variation coordinates for variable fonts. + normalized_coords: &'a [skrifa::instance::NormalizedCoord], + /// Controls whether font hinting is enabled. + hint: bool, +} + +/// If `transform` has a uniform scale without rotation or skew, return the scale factor and the +/// transform with the scale factored out. Translation is unchanged. +fn take_uniform_scale(transform: Affine) -> Option<(f64, Affine)> { + let [a, b, c, d, e, f] = transform.as_coeffs(); + if a == d && b == 0.0 && c == 0.0 { + let scale = a; + let transform_without_scale = Affine::new([1.0, 0.0, 0.0, 1.0, e, f]); + + Some((scale, transform_without_scale)) + } else { + None + } +} + +// TODO: Although these are sane defaults, we might want to make them +// configurable. +const HINTING_OPTIONS: HintingOptions = HintingOptions { + engine: skrifa::outline::Engine::AutoFallback, + target: skrifa::outline::Target::Smooth { + mode: skrifa::outline::SmoothMode::Lcd, + symmetric_rendering: false, + preserve_linear_metrics: true, + }, +}; + +struct OutlinePath(BezPath); + +// Note that we flip the y-axis to match our coordinate system. +impl OutlinePen for OutlinePath { + #[inline] + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, -y)); + } + + #[inline] + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x, -y)); + } + + #[inline] + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0.curve_to((cx0, -cy0), (cx1, -cy1), (x, -y)); + } + + #[inline] + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + self.0.quad_to((cx, -cy), (x, -y)); + } + + #[inline] + fn close(&mut self) { + self.0.close_path(); + } +} + +/// A normalized variation coordinate (for variable fonts) in 2.14 fixed point format. +/// +/// In most cases, this can be [cast](bytemuck::cast_slice) from the +/// normalised coords provided by your text layout library. +/// +/// Equivalent to [`skrifa::instance::NormalizedCoord`], but defined +/// in Vello so that Skrifa is not part of Vello's public API. +/// This allows Vello to update its Skrifa in a patch release, and limits +/// the need for updates only to align Skrifa versions. +pub type NormalizedCoord = i16; + +#[cfg(test)] +mod tests { + use super::*; + + const _NORMALISED_COORD_SIZE_MATCHES: () = + assert!(size_of::() == size_of::()); + + mod take_uniform_scale { + use super::*; + + #[test] + fn identity_transform() { + let identity = Affine::IDENTITY; + let result = take_uniform_scale(identity); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 1.0).abs() < 1e-10); + assert!(transform == Affine::IDENTITY); + } + + #[test] + fn pure_uniform_scale() { + let scale_transform = Affine::scale(2.5); + let result = take_uniform_scale(scale_transform); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 2.5).abs() < 1e-10); + assert!(transform == Affine::IDENTITY); + } + + #[test] + fn scale_with_translation() { + let scale_translate = Affine::scale(3.0).then_translate(Vec2::new(10.0, 20.0)); + let result = take_uniform_scale(scale_translate); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 3.0).abs() < 1e-10); + // The translation should be adjusted by the scale factor + assert!(transform == Affine::translate(Vec2::new(10.0, 20.0))); + } + + #[test] + fn pure_translation() { + let translation = Affine::translate(Vec2::new(5.0, 7.0)); + let result = take_uniform_scale(translation); + assert!(result.is_some()); + let (scale, transform) = result.unwrap(); + assert!((scale - 1.0).abs() < 1e-10); + assert!(transform == translation); + } + + #[test] + fn non_uniform_scale() { + let non_uniform = Affine::scale_non_uniform(2.0, 3.0); + assert!(take_uniform_scale(non_uniform).is_none()); + } + + #[test] + fn rotation_transform() { + let rotation = Affine::rotate(std::f64::consts::PI / 4.0); + assert!(take_uniform_scale(rotation).is_none()); + } + + #[test] + fn skew_transform() { + let skew = Affine::skew(0.5, 0.0); + assert!(take_uniform_scale(skew).is_none()); + } + + #[test] + fn complex_transform() { + let complex = Affine::translate(Vec2::new(10.0, 20.0)) + .then_rotate(std::f64::consts::PI / 6.0) + .then_scale(2.0); + assert!(take_uniform_scale(complex).is_none()); + } + } +} diff --git a/sparse_strips/vello_common/src/lib.rs b/sparse_strips/vello_common/src/lib.rs index c130bb0ce..6d7c3caa4 100644 --- a/sparse_strips/vello_common/src/lib.rs +++ b/sparse_strips/vello_common/src/lib.rs @@ -14,6 +14,7 @@ only break in edge cases, and some of them are also only related to conversions pub mod coarse; pub mod flatten; +pub mod glyph; pub mod pico_svg; pub mod pixmap; pub mod strip; diff --git a/sparse_strips/vello_cpu/Cargo.toml b/sparse_strips/vello_cpu/Cargo.toml index 2a994ac49..c2dba6fed 100644 --- a/sparse_strips/vello_cpu/Cargo.toml +++ b/sparse_strips/vello_cpu/Cargo.toml @@ -17,6 +17,7 @@ vello_common = { workspace = true } [dev-dependencies] oxipng = { workspace = true, features = ["freestanding", "parallel"] } image = { workspace = true, features = ["png"] } +skrifa = { workspace = true } [lints] workspace = true diff --git a/sparse_strips/vello_cpu/snapshots/filled_glyphs.png b/sparse_strips/vello_cpu/snapshots/filled_glyphs.png new file mode 100644 index 000000000..2b9a0a614 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/filled_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a876a4537bdc5812158aff76475b4e50ca70a8423c3e418bd8a640ca43b218e +size 2391 diff --git a/sparse_strips/vello_cpu/snapshots/scaled_glyphs.png b/sparse_strips/vello_cpu/snapshots/scaled_glyphs.png new file mode 100644 index 000000000..6e3611855 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/scaled_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5305569218595bbe61303802b495c4a2104b96daf228d9fd3cb2ac6c9d9a5fe5 +size 2434 diff --git a/sparse_strips/vello_cpu/snapshots/skewed_glyphs.png b/sparse_strips/vello_cpu/snapshots/skewed_glyphs.png new file mode 100644 index 000000000..5de5f6724 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/skewed_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ba918c5de97f7b2dbdb7e282cd2f57b26fd0d8b43e576c388a738b01462df79 +size 3161 diff --git a/sparse_strips/vello_cpu/snapshots/stroked_glyphs.png b/sparse_strips/vello_cpu/snapshots/stroked_glyphs.png new file mode 100644 index 000000000..15ad462f9 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/stroked_glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbed811f8c94c1cea5b7dbe17fb69b83b66f47cef0a59ba678b55225f13dfe24 +size 3343 diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 960d1950e..912360de9 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -6,8 +6,10 @@ use crate::fine::Fine; use vello_common::coarse::Wide; use vello_common::flatten::Line; +use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder, PreparedGlyph}; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke}; use vello_common::paint::Paint; +use vello_common::peniko::Font; use vello_common::peniko::color::palette::css::BLACK; use vello_common::peniko::{BlendMode, Compose, Fill, Mix}; use vello_common::pixmap::Pixmap; @@ -93,6 +95,11 @@ impl RenderContext { self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE)); } + /// Creates a builder for drawing a run of glyphs that have the same attributes. + pub fn glyph_run(&mut self, font: &Font) -> GlyphRunBuilder<'_, Self> { + GlyphRunBuilder::new(font.clone(), self.transform, self) + } + /// Set the current blend mode. pub fn set_blend_mode(&mut self, blend_mode: BlendMode) { self.blend_mode = blend_mode; @@ -174,3 +181,28 @@ impl RenderContext { self.wide.generate(&self.strip_buf, fill_rule, paint); } } + +impl GlyphRenderer for RenderContext { + fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>) { + match glyph { + PreparedGlyph::Outline(glyph) => { + flatten::fill(glyph.path, glyph.transform, &mut self.line_buf); + self.render_path(Fill::NonZero, self.paint.clone()); + } + } + } + + fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>) { + match glyph { + PreparedGlyph::Outline(glyph) => { + flatten::stroke( + glyph.path, + &self.stroke, + glyph.transform, + &mut self.line_buf, + ); + self.render_path(Fill::NonZero, self.paint.clone()); + } + } + } +} diff --git a/sparse_strips/vello_cpu/tests/basic.rs b/sparse_strips/vello_cpu/tests/basic.rs index 19db95bbf..1db147abe 100644 --- a/sparse_strips/vello_cpu/tests/basic.rs +++ b/sparse_strips/vello_cpu/tests/basic.rs @@ -4,13 +4,17 @@ //! Tests for basic functionality. use crate::util::{check_ref, get_ctx, render_pixmap}; +use skrifa::MetadataProvider; +use skrifa::raw::FileRef; use std::f64::consts::PI; +use std::sync::Arc; use vello_common::color::palette::css::{ BEIGE, BLUE, DARK_GREEN, GREEN, LIME, MAROON, REBECCA_PURPLE, RED, YELLOW, }; +use vello_common::glyph::Glyph; use vello_common::kurbo::{Affine, BezPath, Circle, Join, Point, Rect, Shape, Stroke}; use vello_common::peniko; -use vello_common::peniko::Compose; +use vello_common::peniko::{Blob, Compose, Font}; use vello_cpu::RenderContext; mod util; @@ -522,6 +526,121 @@ fn filled_vertical_hairline_rect_2() { check_ref(&ctx, "filled_vertical_hairline_rect_2"); } +#[test] +fn filled_glyphs() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "filled_glyphs"); +} + +#[test] +fn stroked_glyphs() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .stroke_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "stroked_glyphs"); +} + +#[test] +fn skewed_glyphs() { + let mut ctx = get_ctx(300, 70, false); + let font_size: f32 = 50_f32; + let (font, glyphs) = layout_glyphs("Hello, world!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size)))); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.0)) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "skewed_glyphs"); +} + +#[test] +fn scaled_glyphs() { + let mut ctx = get_ctx(150, 125, false); + let font_size: f32 = 25_f32; + let (font, glyphs) = layout_glyphs("Hello,\nworld!", font_size); + + ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0)); + ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into()); + ctx.glyph_run(&font) + .font_size(font_size) + .fill_glyphs(glyphs.into_iter()); + + check_ref(&ctx, "scaled_glyphs"); +} + +/// ***DO NOT USE THIS OUTSIDE OF THESE TESTS*** +/// +/// This function is used for _TESTING PURPOSES ONLY_. If you need to layout and shape +/// text for your application, use a proper text shaping library like `Parley`. +/// +/// We use this function as a convenience for testing; to get some glyphs shaped and laid +/// out in a small amount of code without having to go through the trouble of setting up a +/// full text layout pipeline, which you absolutely should do in application code. +fn layout_glyphs(text: &str, font_size: f32) -> (Font, Vec) { + const ROBOTO_FONT: &[u8] = include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf"); + let font = Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0); + + let font_ref = { + let file_ref = FileRef::new(font.data.as_ref()).unwrap(); + match file_ref { + FileRef::Font(f) => f, + FileRef::Collection(collection) => collection.get(font.index).unwrap(), + } + }; + let font_size = skrifa::instance::Size::new(font_size); + let axes = font_ref.axes(); + let variations: Vec<(&str, f32)> = vec![]; + let var_loc = axes.location(variations.as_slice()); + let charmap = font_ref.charmap(); + let metrics = font_ref.metrics(font_size, &var_loc); + let line_height = metrics.ascent - metrics.descent + metrics.leading; + let glyph_metrics = font_ref.glyph_metrics(font_size, &var_loc); + + let mut pen_x = 0_f32; + let mut pen_y = 0_f32; + + let glyphs = text + .chars() + .filter_map(|ch| { + if ch == '\n' { + pen_y += line_height; + pen_x = 0.0; + return None; + } + let gid = charmap.map(ch).unwrap_or_default(); + let advance = glyph_metrics.advance_width(gid).unwrap_or_default(); + let x = pen_x; + pen_x += advance; + Some(Glyph { + id: gid.to_u32(), + x, + y: pen_y, + }) + }) + .collect::>(); + + (font, glyphs) +} + fn miter_stroke_2() -> Stroke { Stroke { width: 2.0, diff --git a/sparse_strips/vello_hybrid/Cargo.toml b/sparse_strips/vello_hybrid/Cargo.toml index 2f5f48364..5ababc051 100644 --- a/sparse_strips/vello_hybrid/Cargo.toml +++ b/sparse_strips/vello_hybrid/Cargo.toml @@ -21,6 +21,8 @@ wgpu = { workspace = true } [dev-dependencies] winit = "0.30.9" +skrifa = { workspace = true } +parley = { version = "0.3.0", default-features = false, features = ["std"] } pollster = { workspace = true } png = "0.17.14" roxmltree = "0.20.0" diff --git a/sparse_strips/vello_hybrid/examples/text.rs b/sparse_strips/vello_hybrid/examples/text.rs new file mode 100644 index 000000000..bc19c8954 --- /dev/null +++ b/sparse_strips/vello_hybrid/examples/text.rs @@ -0,0 +1,258 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Draws some text on screen. + +mod common; + +use common::{RenderContext, RenderSurface, create_vello_renderer, create_winit_window}; +use parley::FontFamily; +use parley::{ + Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, + PositionedLayoutItem, StyleProperty, +}; +use std::sync::Arc; +use vello_common::color::palette::css::WHITE; +use vello_common::color::{AlphaColor, Srgb}; +use vello_common::glyph::Glyph; +use vello_hybrid::{RenderParams, Renderer, Scene}; +use wgpu::RenderPassDescriptor; +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + window::{Window, WindowId}, +}; + +const ROBOTO_FONT: &[u8] = include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf"); + +#[derive(Clone, Copy, Debug, PartialEq)] +struct ColorBrush { + color: AlphaColor, +} + +impl Default for ColorBrush { + fn default() -> Self { + Self { color: WHITE } + } +} + +fn main() { + let mut app = App { + context: RenderContext::new(), + font_cx: FontContext::new(), + layout_cx: LayoutContext::new(), + renderers: vec![], + state: RenderState::Suspended(None), + scene: Scene::new(900, 600), + }; + + // Note: If you set `default-features = true` in the `parley` dependency, you automatically + // get access to system fonts. Since we want to ensure this example can be compiled to Wasm, + // we are passing the font data directly to the font context. + app.font_cx.collection.register_fonts(ROBOTO_FONT.to_vec()); + + let event_loop = EventLoop::new().unwrap(); + event_loop + .run_app(&mut app) + .expect("Couldn't run event loop"); +} + +#[derive(Debug)] +enum RenderState<'s> { + Active { + surface: Box>, + window: Arc, + }, + Suspended(Option>), +} + +struct App<'s> { + context: RenderContext, + font_cx: FontContext, + layout_cx: LayoutContext, + renderers: Vec>, + state: RenderState<'s>, + scene: Scene, +} + +impl ApplicationHandler for App<'_> { + fn suspended(&mut self, _event_loop: &ActiveEventLoop) { + if let RenderState::Active { window, .. } = &self.state { + self.state = RenderState::Suspended(Some(window.clone())); + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let RenderState::Suspended(cached_window) = &mut self.state else { + return; + }; + + let window = cached_window.take().unwrap_or_else(|| { + create_winit_window( + event_loop, + self.scene.width().into(), + self.scene.height().into(), + true, + ) + }); + + let size = window.inner_size(); + let surface = pollster::block_on(self.context.create_surface( + window.clone(), + size.width, + size.height, + wgpu::PresentMode::AutoVsync, + wgpu::TextureFormat::Bgra8Unorm, + )); + + self.renderers + .resize_with(self.context.devices.len(), || None); + self.renderers[surface.dev_id] + .get_or_insert_with(|| create_vello_renderer(&self.context, &surface)); + + self.state = RenderState::Active { + surface: Box::new(surface), + window, + }; + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let surface = match &mut self.state { + RenderState::Active { surface, window } if window.id() == window_id => surface, + _ => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::Resized(size) => { + self.context + .resize_surface(surface, size.width, size.height); + } + WindowEvent::RedrawRequested => { + self.scene.reset(); + + draw_text( + &mut self.scene, + "Hello from Vello Hybrid and Parley!", + &mut self.font_cx, + &mut self.layout_cx, + ); + + let device_handle = &self.context.devices[surface.dev_id]; + let render_params = RenderParams { + width: surface.config.width, + height: surface.config.height, + }; + self.renderers[surface.dev_id].as_mut().unwrap().prepare( + &device_handle.device, + &device_handle.queue, + &self.scene, + &render_params, + ); + + let surface_texture = surface + .surface + .get_current_texture() + .expect("failed to get surface texture"); + + let texture_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = + device_handle + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Vello Render to Surface pass"), + }); + { + let mut pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("Render to Texture Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + self.renderers[surface.dev_id].as_mut().unwrap().render( + &self.scene, + &mut pass, + &render_params, + ); + } + + device_handle.queue.submit([encoder.finish()]); + surface_texture.present(); + + device_handle.device.poll(wgpu::Maintain::Poll); + } + _ => {} + } + } +} + +fn draw_text( + ctx: &mut Scene, + text: &str, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, +) { + let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0); + builder.push_default(FontFamily::parse("Roboto").unwrap()); + builder.push_default(StyleProperty::LineHeight(1.3)); + builder.push_default(StyleProperty::FontSize(32.0)); + + let mut layout: Layout = builder.build(text); + let max_advance = Some(400.0); + layout.break_all_lines(max_advance); + layout.align(max_advance, Alignment::Middle, AlignmentOptions::default()); + + for line in layout.lines() { + for item in line.items() { + if let PositionedLayoutItem::GlyphRun(glyph_run) = item { + render_glyph_run(ctx, &glyph_run, 30); + } + } + } +} + +fn render_glyph_run(ctx: &mut Scene, glyph_run: &GlyphRun<'_, ColorBrush>, padding: u32) { + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let glyphs = glyph_run.glyphs().map(|glyph| { + let glyph_x = run_x + glyph.x + padding as f32; + let glyph_y = run_y - glyph.y + padding as f32; + run_x += glyph.advance; + + Glyph { + id: glyph.id as u32, + x: glyph_x, + y: glyph_y, + } + }); + + let run = glyph_run.run(); + let font = run.font(); + let font_size = run.font_size(); + let normalized_coords = bytemuck::cast_slice(run.normalized_coords()); + + let style = glyph_run.style(); + ctx.set_paint(style.brush.color.into()); + ctx.glyph_run(font) + .font_size(font_size) + .normalized_coords(normalized_coords) + .hint(true) + .fill_glyphs(glyphs); +} diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index a69e80b7a..9c4cb803b 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -7,8 +7,10 @@ use crate::render::{GpuStrip, RenderData}; use vello_common::coarse::{Wide, WideTile}; use vello_common::color::PremulRgba8; use vello_common::flatten::Line; +use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder, PreparedGlyph}; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Shape, Stroke}; use vello_common::paint::Paint; +use vello_common::peniko::Font; use vello_common::peniko::color::palette::css::BLACK; use vello_common::peniko::{BlendMode, Compose, Fill, Mix}; use vello_common::strip::Strip; @@ -113,6 +115,11 @@ impl Scene { self.stroke_path(&rect.to_path(DEFAULT_TOLERANCE)); } + /// Creates a builder for drawing a run of glyphs that have the same attributes. + pub fn glyph_run(&mut self, font: &Font) -> GlyphRunBuilder<'_, Self> { + GlyphRunBuilder::new(font.clone(), self.transform, self) + } + /// Set the blend mode for subsequent rendering operations. pub fn set_blend_mode(&mut self, blend_mode: BlendMode) { self.blend_mode = blend_mode; @@ -259,3 +266,28 @@ impl Scene { } } } + +impl GlyphRenderer for Scene { + fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>) { + match glyph { + PreparedGlyph::Outline(glyph) => { + flatten::fill(glyph.path, glyph.transform, &mut self.line_buf); + self.render_path(Fill::NonZero, self.paint.clone()); + } + } + } + + fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>) { + match glyph { + PreparedGlyph::Outline(glyph) => { + flatten::stroke( + glyph.path, + &self.stroke, + glyph.transform, + &mut self.line_buf, + ); + self.render_path(Fill::NonZero, self.paint.clone()); + } + } + } +}