diff --git a/Cargo.lock b/Cargo.lock index a7fe13d1..d7b1eb49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bitflags" version = "1.3.2" @@ -64,6 +70,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cfg-if" version = "1.0.0" @@ -356,6 +368,18 @@ dependencies = [ "syn", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "num-traits", + "png", +] + [[package]] name = "kurbo" version = "0.11.0" @@ -416,6 +440,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -555,6 +588,17 @@ dependencies = [ "zeno", ] +[[package]] +name = "swash_render" +version = "0.1.0" +dependencies = [ + "image", + "parley", + "peniko", + "skrifa", + "swash", +] + [[package]] name = "syn" version = "2.0.65" diff --git a/Cargo.toml b/Cargo.toml index 229a4c3e..cf4439ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ resolver = "2" members = [ "fontique", "parley", - "examples/tiny_skia_render" + "examples/tiny_skia_render", + "examples/swash_render", ] [workspace.package] diff --git a/examples/swash_render/Cargo.toml b/examples/swash_render/Cargo.toml new file mode 100644 index 00000000..ae5031b6 --- /dev/null +++ b/examples/swash_render/Cargo.toml @@ -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 diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs new file mode 100644 index 00000000..d09d6499 --- /dev/null +++ b/examples/swash_render/src/main.rs @@ -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 = 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, + 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); + } + } + } + }; +}