Skip to content

Commit

Permalink
Implement Skrifa + Tiny-Skia example
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoburns committed May 21, 2024
1 parent 4cc54f5 commit 4ea250c
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 11 deletions.
45 changes: 45 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ fontique = { workspace = true }

[dev-dependencies]
image = { version = "0.25.1", default-features = false, features = ["png"] }
tiny-skia = "0.11.4"

[workspace.dependencies]
fontique = { version = "0.1.0", default-features = false, path = "fontique" }
Expand Down
210 changes: 210 additions & 0 deletions examples/tiny-skia.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Copyright 2024 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! A simple example that lays out some text using Parley, extracts outlines using Skrifa and
//! then paints those outlines using Tiny-Skia.
use parley::layout::{Alignment, GlyphRun, Layout};
use parley::style::{FontStack, FontWeight, StyleProperty};
use parley::{FontContext, LayoutContext};
use peniko::Color as PenikoColor;
use skrifa::instance::{LocationRef, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
use skrifa::raw::FontRef as ReadFontsRef;
use skrifa::{GlyphId, MetadataProvider, OutlineGlyph};
use tiny_skia::{
Color as TinySkiaColor, FillRule, Paint, PathBuilder, Pixmap, PixmapMut, Rect, Transform,
};

fn main() {
// The text we are going to style and lay out
let text = String::from(
"Some text here. Let's make it a bit longer so that line wrapping kicks in 😊. And also some اللغة العربية arabic text.",
);

// The display scale for HiDPI rendering
let display_scale = 1.0;

// The width for line wrapping
let max_advance = Some(200.0 * display_scale);

// Colours for rendering
let foreground_color = PenikoColor::rgb8(0, 0, 0);
let background_color = PenikoColor::rgb8(255, 255, 255);

// Padding around the output image
let padding = 20;

// Create a FontContext, LayoutContext
//
// These are both intended to be constructed rarely (perhaps even once per app (or once per thread))
// and provide caches and scratch space to avoid allocations
let mut font_cx = FontContext::default();
let mut layout_cx = LayoutContext::new();

// Create a RangedBuilder
let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, display_scale);

// Set default text colour styles (set foreground text color)
let brush_style = StyleProperty::Brush(foreground_color);
builder.push_default(&brush_style);

// Set default font family
let font_stack = FontStack::Source("system-ui");
let font_stack_style = StyleProperty::FontStack(font_stack);
builder.push_default(&font_stack_style);
builder.push_default(&StyleProperty::LineHeight(1.3));
builder.push_default(&StyleProperty::FontSize(16.0));

// Set the first 4 characters to bold
let bold = FontWeight::new(600.0);
let bold_style = StyleProperty::FontWeight(bold);
builder.push(&bold_style, 0..4);

// Build the builder into a Layout
let mut layout: Layout<PenikoColor> = builder.build();

// Perform layout (including bidi resolution and shaping) with start alignment
layout.break_all_lines(max_advance, Alignment::Start);
let width = layout.width().ceil() as u32;
let height = layout.height().ceil() as u32;
let padded_width = width + padding * 2;
let padded_height = height + padding * 2;

// Create TinySkia Pixmap
let mut img = Pixmap::new(padded_width, padded_height).unwrap();

// Fill background color
let mut bg_paint = Paint::default();
bg_paint.set_color(to_tiny_skia(background_color));
img.fill_rect(
Rect::from_xywh(0., 0., padded_width as f32, padded_height as f32).unwrap(),
&bg_paint,
Transform::identity(),
None,
);

let mut pen = TinySkiaPen::new(img.as_mut());

// Iterate over laid out lines
for line in layout.lines() {
// Iterate over GlyphRun's within each line
for glyph_run in line.glyph_runs() {
render_glyph_run(&glyph_run, &mut pen, padding);
}
}

// Write image to PNG file
img.save_png("output.png").unwrap();
}

fn to_tiny_skia(color: PenikoColor) -> TinySkiaColor {
TinySkiaColor::from_rgba8(color.r, color.g, color.b, color.a)
}

fn render_glyph_run(glyph_run: &GlyphRun<PenikoColor>, pen: &mut TinySkiaPen<'_>, padding: u32) {
// Resolve properties of the GlyphRun
let mut run_x = glyph_run.offset();
let run_y = glyph_run.baseline();
let style = glyph_run.style();
let color = style.brush;

// Get the "Run" from the "GlyphRun"
let run = glyph_run.run();

// Resolve properties of the Run
let font = run.font();
let font_size = run.font_size();

// Get glyph outlines using Skrifa. This can be cached in production code.
let font_collection_ref = font.data.as_ref();
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
let outlines = font_ref.outline_glyphs();

// Iterates over the glyphs in the GlyphRun
for glyph in glyph_run.glyphs() {
let glyph_x = run_x + glyph.x + padding as f32;
let glyph_y = run_y - glyph.y + padding as f32;
run_x += glyph.advance;

let glyph_id = GlyphId::from(glyph.id);
let glyph_outline = outlines.get(glyph_id).unwrap();

pen.set_origin(glyph_x, glyph_y);
pen.set_color(to_tiny_skia(color));
pen.draw_glyph(&glyph_outline, font_size);
}
}

struct TinySkiaPen<'a> {
pixmap: PixmapMut<'a>,
x: f32,
y: f32,
paint: Paint<'static>,
open_path: PathBuilder,
}

impl TinySkiaPen<'_> {
fn new<'a>(pixmap: PixmapMut<'a>) -> TinySkiaPen<'a> {
TinySkiaPen {
pixmap,
x: 0.0,
y: 0.0,
paint: Paint::default(),
open_path: PathBuilder::new(),
}
}

fn set_origin(&mut self, x: f32, y: f32) {
self.x = x;
self.y = y;
}

fn set_color(&mut self, color: TinySkiaColor) {
self.paint.set_color(color);
}

fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32) {
let settings = DrawSettings::unhinted(Size::new(size), LocationRef::default());
glyph.draw(settings, self).unwrap();
}
}

impl OutlinePen for TinySkiaPen<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.open_path.move_to(self.x + x, self.y - y);
}

fn line_to(&mut self, x: f32, y: f32) {
self.open_path.line_to(self.x + x, self.y - y);
}

fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.open_path
.quad_to(self.x + cx0, self.y - cy0, self.x + x, self.y - y)
}

fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.open_path.cubic_to(
self.x + cx0,
self.y - cy0,
self.x + cx1,
self.y - cy1,
self.x + x,
self.y - y,
)
}

fn close(&mut self) {
let new_builder = PathBuilder::new();
let builder = core::mem::replace(&mut self.open_path, new_builder);
let path = builder.finish().unwrap();
self.pixmap.fill_path(
&path,
&self.paint,
FillRule::EvenOdd,
Transform::identity(),
None,
);
}
}
Binary file modified output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 1 addition & 11 deletions src/style/brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,4 @@
/// Trait for types that represent the color of glyphs or decorations.
pub trait Brush: Clone + PartialEq + Default + core::fmt::Debug {}

/// Empty brush.
impl Brush for () {}

/// Brush for a 4-byte color value.
impl Brush for [u8; 4] {}

/// Brush for a 3-byte color value.
impl Brush for [u8; 3] {}

impl Brush for peniko::Brush {}
impl Brush for peniko::Color {}
impl<T: Clone + PartialEq + Default + core::fmt::Debug> Brush for T {}

0 comments on commit 4ea250c

Please sign in to comment.