-
Notifications
You must be signed in to change notification settings - Fork 39
Implement simple example using swash to render to png #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
98cb2cc
Implement simple example using swash to render to png
nicoburns c47fa50
Simplify swash FontRef creation
nicoburns 8cef302
Gamma correction WIP
nicoburns cea8aa8
Implement gamma correction
nicoburns c78fbcd
Fix bitmap emoji rasterisation
nicoburns 3a8438b
Use blend function from the image crate
nicoburns 389b288
Pass normalized_coords through to swash
nicoburns 3a88a7a
Make hinting always-on to reduce arg count to make clippy happy
nicoburns a73d530
Apply image based blending
nicoburns a5c5ef7
Reapply changes lost in rebase
nicoburns 399a447
Fix clippy lints
nicoburns a671f69
Simplify background fill
nicoburns bcd2358
Remove duplicate file (rebase conflict)
nicoburns 3e45a9a
Only build one scaler per glyph run
nicoburns 4b42666
Move render_glyph back into it's own function
nicoburns File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "swash_render" | ||
version = "0.1.0" | ||
edition.workspace = true | ||
rust-version.workspace = true | ||
license.workspace = true | ||
repository.workspace = true | ||
publish = false | ||
|
||
[dependencies] | ||
parley = { workspace = true, default-features = true } | ||
skrifa = { workspace = true } | ||
peniko = { workspace = true } | ||
image = { version = "0.25.1", default-features = false, features = ["png"] } | ||
swash = "0.1.16" | ||
|
||
[lints] | ||
workspace = true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
// Copyright 2024 the Parley Authors | ||
// SPDX-License-Identifier: Apache-2.0 OR MIT | ||
|
||
//! A simple example that lays out some text using Parley, rasterises the glyph using Swash | ||
//! and and then renders it into a PNG using the `image` crate. | ||
|
||
use image::codecs::png::PngEncoder; | ||
use image::{self, Pixel, Rgba, RgbaImage}; | ||
use parley::layout::{Alignment, Glyph, GlyphRun, Layout}; | ||
use parley::style::{FontStack, FontWeight, StyleProperty}; | ||
use parley::{FontContext, LayoutContext}; | ||
use peniko::Color; | ||
use std::fs::File; | ||
use swash::scale::image::Content; | ||
use swash::scale::{Render, ScaleContext, Scaler, Source, StrikeWith}; | ||
use swash::zeno; | ||
use swash::FontRef; | ||
use zeno::{Format, Vector}; | ||
|
||
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 text_color = Color::rgb8(0, 0, 0); | ||
let bg_color = Rgba([255, 255, 255, 255]); | ||
|
||
// Padding around the output image | ||
let padding = 20; | ||
|
||
// Create a FontContext, LayoutContext and ScaleContext | ||
// | ||
// These are all 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(); | ||
let mut scale_cx = ScaleContext::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(text_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<Color> = builder.build(); | ||
|
||
// Perform layout (including bidi resolution and shaping) with start alignment | ||
layout.break_all_lines(max_advance, Alignment::Start); | ||
|
||
// Create image to render into | ||
let width = layout.width().ceil() as u32 + (padding * 2); | ||
let height = layout.height().ceil() as u32 + (padding * 2); | ||
let mut img = RgbaImage::from_pixel(width, height, bg_color); | ||
|
||
// 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(&mut scale_cx, &glyph_run, &mut img, padding); | ||
} | ||
} | ||
|
||
// Write image to PNG file in examples/_output dir | ||
let output_path = { | ||
let path = std::path::PathBuf::from(file!()); | ||
let mut path = std::fs::canonicalize(path).unwrap(); | ||
path.pop(); | ||
path.pop(); | ||
path.pop(); | ||
path.push("_output"); | ||
let _ = std::fs::create_dir(path.clone()); | ||
path.push("swash_render.png"); | ||
path | ||
}; | ||
let output_file = File::create(output_path).unwrap(); | ||
let png_encoder = PngEncoder::new(output_file); | ||
img.write_with_encoder(png_encoder).unwrap(); | ||
} | ||
|
||
fn render_glyph_run( | ||
context: &mut ScaleContext, | ||
glyph_run: &GlyphRun<Color>, | ||
img: &mut RgbaImage, | ||
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(); | ||
let normalized_coords = run.normalized_coords(); | ||
|
||
// Convert from parley::Font to swash::FontRef | ||
let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); | ||
|
||
// Build a scaler. As the font properties are constant across an entire run of glyphs | ||
// we can build one scaler for the run and reuse it for each glyph. | ||
let mut scaler = context | ||
.builder(font_ref) | ||
.size(font_size) | ||
.hint(true) | ||
.normalized_coords(normalized_coords) | ||
.build(); | ||
|
||
// 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; | ||
|
||
render_glyph(img, &mut scaler, color, glyph, glyph_x, glyph_y); | ||
} | ||
} | ||
|
||
fn render_glyph( | ||
img: &mut RgbaImage, | ||
scaler: &mut Scaler, | ||
color: Color, | ||
glyph: Glyph, | ||
glyph_x: f32, | ||
glyph_y: f32, | ||
) { | ||
// Compute the fractional offset | ||
// You'll likely want to quantize this in a real renderer | ||
let offset = Vector::new(glyph_x.fract(), glyph_y.fract()); | ||
|
||
// Render the glyph using swash | ||
let rendered_glyph = Render::new( | ||
// Select our source order | ||
&[ | ||
Source::ColorOutline(0), | ||
Source::ColorBitmap(StrikeWith::BestFit), | ||
Source::Outline, | ||
], | ||
) | ||
// Select the simple alpha (non-subpixel) format | ||
.format(Format::Alpha) | ||
// Apply the fractional offset | ||
.offset(offset) | ||
// Render the image | ||
.render(scaler, glyph.id) | ||
.unwrap(); | ||
|
||
let glyph_width = rendered_glyph.placement.width; | ||
let glyph_height = rendered_glyph.placement.height; | ||
let glyph_x = (glyph_x.floor() as i32 + rendered_glyph.placement.left) as u32; | ||
let glyph_y = (glyph_y.floor() as i32 - rendered_glyph.placement.top) as u32; | ||
|
||
match rendered_glyph.content { | ||
Content::Mask => { | ||
let mut i = 0; | ||
for pixel_y in 0..glyph_height { | ||
for pixel_x in 0..glyph_width { | ||
let x = glyph_x + pixel_x; | ||
let y = glyph_y + pixel_y; | ||
let alpha = rendered_glyph.data[i]; | ||
let color = Rgba([color.r, color.g, color.b, alpha]); | ||
img.get_pixel_mut(x, y).blend(&color); | ||
i += 1; | ||
} | ||
} | ||
} | ||
Content::SubpixelMask => unimplemented!(), | ||
Content::Color => { | ||
let row_size = glyph_width as usize * 4; | ||
for (pixel_y, row) in rendered_glyph.data.chunks_exact(row_size).enumerate() { | ||
for (pixel_x, pixel) in row.chunks_exact(4).enumerate() { | ||
let x = glyph_x + pixel_x as u32; | ||
let y = glyph_y + pixel_y as u32; | ||
let color = Rgba(pixel.try_into().expect("Not RGBA")); | ||
img.get_pixel_mut(x, y).blend(&color); | ||
} | ||
} | ||
} | ||
}; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.