Skip to content
3 changes: 3 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,
/// X-offset in run, relative to transform.
pub x: f32,
/// Y-offset in run, relative to transform.
pub y: f32,
}
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;
1 change: 1 addition & 0 deletions sparse_strips/vello_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ publish = false
vello_api = { workspace = true, default-features = true }
# for pico_svg
roxmltree = "0.20.0"
skrifa = { workspace = true }

[features]
simd = ["vello_api/simd"]
Expand Down
190 changes: 190 additions & 0 deletions sparse_strips/vello_common/src/glyph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Processing and drawing glyphs.

use crate::peniko::Font;
use skrifa::instance::{NormalizedCoord, 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,
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: Vec::new(),
},
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: Vec<NormalizedCoord>) -> Self {
self.run.normalized_coords = 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,
glyphs: impl Iterator<Item = &'a Glyph>,
) -> impl Iterator<Item = PreparedGlyph> {
let font = skrifa::FontRef::from_index(run.font.data.as_ref(), run.font.index).unwrap();
let outlines = font.outline_glyphs();
let size = Size::new(run.font_size);
let normalized_coords = run.normalized_coords.as_slice();
let hinting_instance = if run.hint {
// TODO: Cache hinting instance.
HintingInstance::new(&outlines, size, 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, 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 {
/// Font for all glyphs in the run.
pub font: Font,
/// 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: Vec<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();
}
}
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(self.fill_rule, 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