Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions sparse_strips/vello_api/src/glyph.rs
Original file line number Diff line number Diff line change
@@ -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,
Comment thread
DJMcNab marked this conversation as resolved.
/// X-offset in run, relative to transform.
pub x: f32,
/// Y-offset in run, relative to transform.
pub y: f32,
}
Comment thread
taj-p marked this conversation as resolved.
1 change: 1 addition & 0 deletions sparse_strips/vello_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ pub use peniko;
pub use peniko::color;
pub use peniko::kurbo;
pub mod execute;
pub mod glyph;
pub mod paint;
2 changes: 2 additions & 0 deletions sparse_strips/vello_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ publish = false
vello_api = { workspace = true, default-features = true }
# for pico_svg
roxmltree = "0.20.0"
bytemuck = { workspace = true }
Comment thread
DJMcNab marked this conversation as resolved.
Outdated
skrifa = { workspace = true }

Comment thread
DJMcNab marked this conversation as resolved.
[features]
simd = ["vello_api/simd"]
Expand Down
208 changes: 208 additions & 0 deletions sparse_strips/vello_common/src/glyph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// 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 glyph defined by its outline.
Outline(OutlineGlyph),
// TODO: Image and Colr variants.
}

/// A glyph defined by a path (its outline) and a local transform.
#[derive(Debug)]
pub struct OutlineGlyph {
/// The path of the glyph.
pub path: BezPath,
/// The local transform of the glyph.
pub local_transform: Affine,
}

/// Trait for types that can render glyphs.
pub trait GlyphRenderer {
/// Fill glyphs with the current paint and fill rule.
fn fill_glyphs(&mut self, glyphs: impl Iterator<Item = PreparedGlyph>);

/// Stroke glyphs with the current paint and stroke settings.
fn stroke_glyphs(&mut self, glyphs: impl Iterator<Item = 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, renderer: &'a mut T) -> Self {
Self {
run: GlyphRun {
font,
font_size: 16.0,
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);
Comment thread
taj-p marked this conversation as resolved.
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<Item = &'a Glyph>) {
self.renderer
.fill_glyphs(Self::prepare_glyphs(&self.run, glyphs));
}

/// Consumes the builder and strokes the glyphs with the current configuration.
pub fn stroke_glyphs(self, glyphs: impl Iterator<Item = &'a Glyph>) {
self.renderer
.stroke_glyphs(Self::prepare_glyphs(&self.run, glyphs));
}

fn prepare_glyphs(
run: &GlyphRun<'a>,
glyphs: impl Iterator<Item = &'a Glyph>,
) -> impl Iterator<Item = PreparedGlyph> {
Comment thread
taj-p marked this conversation as resolved.
Outdated
let font = skrifa::FontRef::from_index(run.font.data.as_ref(), run.font.index).unwrap();
let outlines = font.outline_glyphs();
Comment thread
DJMcNab marked this conversation as resolved.
Outdated
let size = Size::new(run.font_size);
Comment thread
DJMcNab marked this conversation as resolved.
Outdated
let hinting_instance = if run.hint {
Comment thread
DJMcNab marked this conversation as resolved.
Outdated
// TODO: Cache hinting instance.
HintingInstance::new(&outlines, size, run.normalized_coords, HINTING_OPTIONS).ok()
} else {
None
};
glyphs.filter_map(move |glyph| {
let draw_settings = if let Some(hinting_instance) = &hinting_instance {
DrawSettings::hinted(hinting_instance, false)
} else {
DrawSettings::unhinted(size, run.normalized_coords)
};
let outline = outlines.get(GlyphId::new(glyph.id))?;
let mut path = OutlinePath(BezPath::new());
outline.draw(draw_settings, &mut path).ok()?;
let mut transform = Affine::translate(Vec2::new(glyph.x as f64, glyph.y as f64));
if let Some(glyph_transform) = run.glyph_transform {
transform *= glyph_transform;
}
Some(PreparedGlyph::Outline(OutlineGlyph {
path: path.0,
local_transform: transform,
}))
})
}
}

/// A sequence of glyphs with shared rendering properties.
#[derive(Clone, Debug)]
struct GlyphRun<'a> {
/// Font for all glyphs in the run.
pub font: Font,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not immediately obvious to me why the fields of this private struct are public. But OTOH, if clippy isn't complaining about it, that's probably my lack of understanding here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Made private in 6e661a4

/// Size of the font in pixels per em.
pub font_size: f32,
/// Per-glyph transform. Can be used to apply skew to simulate italic text.
pub glyph_transform: Option<Affine>,
/// Normalized variation coordinates for variable fonts.
pub normalized_coords: &'a [skrifa::instance::NormalizedCoord],
/// Controls whether font hinting is enabled.
pub hint: bool,
}

// 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::<skrifa::instance::NormalizedCoord>() == size_of::<NormalizedCoord>());
}
1 change: 1 addition & 0 deletions sparse_strips/vello_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions sparse_strips/vello_cpu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions sparse_strips/vello_cpu/snapshots/filled_glyphs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions sparse_strips/vello_cpu/snapshots/skewed_glyphs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions sparse_strips/vello_cpu/snapshots/stroked_glyphs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions sparse_strips/vello_cpu/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}

/// Set the current blend mode.
pub fn set_blend_mode(&mut self, blend_mode: BlendMode) {
self.blend_mode = blend_mode;
Expand Down Expand Up @@ -174,3 +181,29 @@ impl RenderContext {
self.wide.generate(&self.strip_buf, fill_rule, paint);
}
}

impl GlyphRenderer for RenderContext {
fn fill_glyphs(&mut self, glyphs: impl Iterator<Item = PreparedGlyph>) {
for glyph in glyphs {
match glyph {
PreparedGlyph::Outline(glyph) => {
let transform = self.transform * glyph.local_transform;
flatten::fill(&glyph.path, transform, &mut self.line_buf);
self.render_path(Fill::NonZero, self.paint.clone());
}
}
}
}

fn stroke_glyphs(&mut self, glyphs: impl Iterator<Item = PreparedGlyph>) {
for glyph in glyphs {
match glyph {
PreparedGlyph::Outline(glyph) => {
let transform = self.transform * glyph.local_transform;
flatten::stroke(&glyph.path, &self.stroke, transform, &mut self.line_buf);
self.render_path(Fill::NonZero, self.paint.clone());
}
}
}
}
}
Loading