Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
211 changes: 186 additions & 25 deletions sparse_strips/vello_common/src/glyph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Processing and drawing glyphs.

use crate::peniko::Font;
use skrifa::OutlineGlyphCollection;
use skrifa::instance::Size;
use skrifa::outline::DrawSettings;
use skrifa::{
Expand All @@ -27,8 +28,8 @@ pub enum PreparedGlyph<'a> {
pub struct OutlineGlyph<'a> {
/// The path of the glyph.
pub path: &'a BezPath,
/// The local transform of the glyph.
pub local_transform: Affine,
/// The global transform of the glyph.
pub transform: Affine,
}

/// Trait for types that can render glyphs.
Expand Down Expand Up @@ -56,6 +57,7 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
font_size: 16.0,
transform,
glyph_transform: None,
horizontal_skew: None,
hint: true,
normalized_coords: &[],
},
Expand All @@ -69,12 +71,18 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
self
}

/// Set the per-glyph transform. Can be used to apply skew to simulate italic text.
/// Set the per-glyph transform. Use `horizontal_skew` to simulate italic text.
pub fn glyph_transform(mut self, transform: Affine) -> Self {
self.run.glyph_transform = Some(transform);
self
}

/// Set the horizontal skew angle in radians to simulate italic/oblique text.
pub fn horizontal_skew(mut self, angle: f32) -> Self {
self.run.horizontal_skew = Some(angle);
self
}

/// Set whether font hinting is enabled.
pub fn hint(mut self, hint: bool) -> Self {
self.run.hint = hint;
Expand All @@ -101,22 +109,16 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
let font =
skrifa::FontRef::from_index(self.run.font.data.as_ref(), self.run.font.index).unwrap();
let outlines = font.outline_glyphs();
let size = Size::new(self.run.font_size);
let hinting_instance = if self.run.hint {
// Only apply hinting if the transform is a simple translation.
// Scaled, rotated, skewed, and other transformations cannot be hinted.
let [a, b, c, d, _, _] = self.run.transform.as_coeffs();
// TODO: Consider scaling the font size if the transform is a uniform scale.
if a == 1.0 && d == 1.0 && b == 0.0 && c == 0.0 {
// TODO: Cache hinting instance.
HintingInstance::new(&outlines, size, self.run.normalized_coords, HINTING_OPTIONS)
.ok()
} else {
None
}
} else {
None
};

let PreparedGlyphRun {
transform,
glyph_transform,
size,
scale,
horizontal_skew,
normalized_coords,
hinting_instance,
} = prepare_glyph_run(&self.run, &outlines);

let render_glyph = match style {
Style::Fill => GlyphRenderer::fill_glyph,
Expand All @@ -128,7 +130,7 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
let draw_settings = if let Some(hinting_instance) = &hinting_instance {
DrawSettings::hinted(hinting_instance, false)
} else {
DrawSettings::unhinted(size, self.run.normalized_coords)
DrawSettings::unhinted(size, normalized_coords)
};
let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else {
continue;
Expand All @@ -137,16 +139,21 @@ impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
if outline.draw(draw_settings, &mut path).is_err() {
continue;
}
let mut transform = Affine::translate(Vec2::new(glyph.x as f64, glyph.y as f64));
if let Some(glyph_transform) = self.run.glyph_transform {
transform *= glyph_transform;

let mut local_transform =
Affine::translate(Vec2::new(glyph.x as f64 * scale, glyph.y as f64 * scale));
if let Some(skew) = horizontal_skew {
local_transform *= Affine::skew(skew.tan() as f64, 0.0);
}
if let Some(glyph_transform) = glyph_transform {
local_transform *= glyph_transform;
}

render_glyph(
self.renderer,
PreparedGlyph::Outline(OutlineGlyph {
path: &path.0,
local_transform: transform,
transform: transform * local_transform,
}),
);
}
Expand All @@ -167,14 +174,97 @@ struct GlyphRun<'a> {
font_size: f32,
/// Global transform.
transform: Affine,
/// Per-glyph transform. Can be used to apply skew to simulate italic text.
/// Per-glyph transform. Use `horizontal_skew` to simulate italic text.
glyph_transform: Option<Affine>,
/// Horizontal skew angle in radians for simulating italic/oblique text.
horizontal_skew: Option<f32>,
/// Normalized variation coordinates for variable fonts.
normalized_coords: &'a [skrifa::instance::NormalizedCoord],
/// Controls whether font hinting is enabled.
hint: bool,
}

struct PreparedGlyphRun<'a> {
transform: Affine,
glyph_transform: Option<Affine>,
size: Size,
scale: f64,
horizontal_skew: Option<f32>,
normalized_coords: &'a [skrifa::instance::NormalizedCoord],
hinting_instance: Option<HintingInstance>,
}

/// Prepare a glyph run for rendering.
///
/// This function calculates the appropriate transform, size, and scaling parameters
/// for proper font hinting when enabled and possible.
fn prepare_glyph_run<'a>(
run: &GlyphRun<'a>,
outlines: &OutlineGlyphCollection<'_>,
) -> PreparedGlyphRun<'a> {
// TODO: Consider extracting the scale from the glyph transform and applying it to the font size.
if !run.hint || run.glyph_transform.is_some() {
return PreparedGlyphRun {
transform: run.transform,
glyph_transform: run.glyph_transform,
size: Size::new(run.font_size),
scale: 1.0,
horizontal_skew: run.horizontal_skew,
normalized_coords: run.normalized_coords,
hinting_instance: None,
};
}

// Hinting doesn't make sense if we later scale the glyphs via some transform. So, if
// this glyph can be scaled uniformly, we extract the scale from its global and glyph
// transform and apply it to font size for hinting. Note that this extracted scale
// should be later applied to the glyph's position.
//
// If the glyph is rotated or skewed, hinting is not applicable.

// Attempt to extract uniform scale from the run's transform.
if let Some((scale, transform)) = take_uniform_scale(run.transform) {
let font_size = run.font_size * scale as f32;

let size = Size::new(font_size);
let hinting_instance =
HintingInstance::new(outlines, size, run.normalized_coords, HINTING_OPTIONS).ok();

return PreparedGlyphRun {
transform,
glyph_transform: run.glyph_transform,
size,
scale,
horizontal_skew: run.horizontal_skew,
normalized_coords: run.normalized_coords,
hinting_instance,
};
}

PreparedGlyphRun {
transform: run.transform,
glyph_transform: run.glyph_transform,
size: Size::new(run.font_size),
scale: 1.0,
horizontal_skew: run.horizontal_skew,
normalized_coords: run.normalized_coords,
hinting_instance: None,
}
}

/// 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 extracted_scale = a;
let transform_without_scale = Affine::new([1.0, 0.0, 0.0, 1.0, e, f]);
Some((extracted_scale, transform_without_scale))
} else {
None
}
}

// TODO: Although these are sane defaults, we might want to make them
// configurable.
const HINTING_OPTIONS: HintingOptions = HintingOptions {
Expand Down Expand Up @@ -233,4 +323,75 @@ mod tests {

const _NORMALISED_COORD_SIZE_MATCHES: () =
assert!(size_of::<skrifa::instance::NormalizedCoord>() == size_of::<NormalizedCoord>());

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_eq!(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_eq!(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_eq!(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_eq!(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());
}
}
}
3 changes: 3 additions & 0 deletions sparse_strips/vello_cpu/snapshots/glyphs_filled_unhinted.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/glyphs_glyph_transform.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions sparse_strips/vello_cpu/snapshots/glyphs_scaled.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/glyphs_scaled_unhinted.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/glyphs_skewed_unhinted.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/glyphs_stroked_unhinted.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 7 additions & 4 deletions sparse_strips/vello_cpu/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,7 @@ impl GlyphRenderer for RenderContext {
fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>) {
match glyph {
PreparedGlyph::Outline(glyph) => {
let transform = self.transform * glyph.local_transform;
flatten::fill(glyph.path, transform, &mut self.line_buf);
flatten::fill(glyph.path, glyph.transform, &mut self.line_buf);
self.render_path(Fill::NonZero, self.paint.clone());
}
}
Expand All @@ -231,8 +230,12 @@ impl GlyphRenderer for RenderContext {
fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>) {
match glyph {
PreparedGlyph::Outline(glyph) => {
let transform = self.transform * glyph.local_transform;
flatten::stroke(glyph.path, &self.stroke, transform, &mut self.line_buf);
flatten::stroke(
glyph.path,
&self.stroke,
glyph.transform,
&mut self.line_buf,
);
self.render_path(Fill::NonZero, self.paint.clone());
}
}
Expand Down
Loading