From 4cc54f591589780cba4e73fee214387a083be846 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 21 May 2024 13:24:01 +0100 Subject: [PATCH] Implement simple example using swash to render to png --- Cargo.lock | 97 +++++++++++++++++++ Cargo.toml | 3 + examples/simple.rs | 236 +++++++++++++++++++++++++++++++++++++++++++++ output.png | Bin 0 -> 19009 bytes src/style/brush.rs | 1 + 5 files changed, 337 insertions(+) create mode 100644 examples/simple.rs create mode 100644 output.png diff --git a/Cargo.lock b/Cargo.lock index a90ba5d5..745be6cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.8.11" @@ -26,6 +32,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" @@ -52,6 +64,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" @@ -119,6 +137,15 @@ dependencies = [ "libm", ] +[[package]] +name = "crc32fast" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ebf8d6963185c7625d2c3c3962d99eb8936637b1427536d21dc36ae402ebad" +dependencies = [ + "cfg-if", +] + [[package]] name = "displaydoc" version = "0.2.4" @@ -144,6 +171,25 @@ dependencies = [ "wio", ] +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "font-types" version = "0.5.3" @@ -316,6 +362,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" @@ -360,6 +418,25 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", + "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" @@ -371,6 +448,7 @@ name = "parley" version = "0.1.0" dependencies = [ "fontique", + "image", "peniko", "skrifa", "swash", @@ -386,6 +464,19 @@ dependencies = [ "smallvec", ] +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -440,6 +531,12 @@ dependencies = [ "syn", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "skrifa" version = "0.19.1" diff --git a/Cargo.toml b/Cargo.toml index 65ac3dd8..cf1eac08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ skrifa = { workspace = true } peniko = { workspace = true } fontique = { workspace = true } +[dev-dependencies] +image = { version = "0.25.1", default-features = false, features = ["png"] } + [workspace.dependencies] fontique = { version = "0.1.0", default-features = false, path = "fontique" } skrifa = { version = "0.19.1", default-features = false } diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 00000000..20ee8f58 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,236 @@ +// 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, Rgba, RgbaImage}; +use parley::layout::{Alignment, GlyphRun, Layout}; +use parley::style::{FontStack, FontWeight, StyleProperty}; +use parley::{FontContext, LayoutContext}; +use peniko::Color; +use skrifa::raw::FontRef as ReadFontsRef; +use std::fs::File; +use swash::scale::image::{Content, Image as SwashImage}; +use swash::scale::{Render, ScaleContext, Source, StrikeWith}; +use swash::{zeno, CacheKey}; +use swash::{FontRef, GlyphId}; + +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 = Color::rgb8(0, 0, 0); + let background_color = Color::rgb8(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(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 = 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 mut img = RgbaImage::new(width + (padding * 2), height + (padding * 2)); + for pixel in img.pixels_mut() { + *pixel = Rgba([ + background_color.r, + background_color.g, + background_color.b, + 255, + ]); + } + + // 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 + let output_file = File::create("output.png").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(); + + // Get byte offset of font within collection (0 if font file is not a collection) + // TODO: expose directly in read-fonts + let font_collection_ref = font.data.as_ref(); + let raw_font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap(); + let font_ref = raw_font_ref.table_directory.offset_data().as_bytes(); + let addr_of_font_collection = font_collection_ref as *const [u8] as *const () as usize; + let addr_of_font = font_ref as *const [u8] as *const () as usize; + let offset = addr_of_font - addr_of_font_collection; + + // Convert from parley::Font to swash::FontRef + let font_ref = FontRef { + data: font.data.as_ref(), + offset: offset as u32, + key: CacheKey::new(), // ignored + }; + + // Iterates over the glyphs in the GlyphRun + for glyph in glyph_run.glyphs() { + let glyph_id: GlyphId = glyph.id; + let glyph_x = run_x + glyph.x; + let glyph_y = run_y - glyph.y; + run_x += glyph.advance; + let Some(rendered_glyph) = render_glyph( + context, + &font_ref, + font_size, + true, + glyph_id, + glyph_x.fract(), + glyph_y.fract(), + ) else { + println!("No glyph"); + continue; + }; + + let glyph_width = usize::try_from(rendered_glyph.placement.width).expect("usize < 32 bits"); + let glyph_height = + usize::try_from(rendered_glyph.placement.height).expect("usize < 32 bits"); + let glyph_origin_x = + glyph_x.floor() as i32 + rendered_glyph.placement.left + padding as i32; + let glyph_origin_y = + (glyph_y.floor() as i32) - rendered_glyph.placement.top + padding as i32; + + match rendered_glyph.content { + Content::Mask => { + let mut i = 0; + for off_y in 0..glyph_height as i32 { + for off_x in 0..glyph_width as i32 { + let x = (glyph_origin_x + off_x) as u32; + let y = (glyph_origin_y + off_y) as u32; + let alpha = rendered_glyph.data[i]; + if alpha > 0 { + // Blend pixel with underlying color + let inv_a = (255 - alpha) as u32; + let [r2, g2, b2, a2] = + [color.r, color.g, color.b, alpha].map(u32::from); + let [r1, g1, b1, _a1] = img.get_pixel(x, y).0.map(u32::from); + let r = (a2 * r2 + inv_a * r1) >> 8; + let g = (a2 * g2 + inv_a * g1) >> 8; + let b = (a2 * b2 + inv_a * b1) >> 8; + let color = Rgba([r as u8, g as u8, b as u8, 255]); + + img.put_pixel(x, y, color); + } + i += 1; + } + } + } + Content::SubpixelMask => unimplemented!(), + Content::Color => { + for (off_y, row) in rendered_glyph.data.chunks_exact(glyph_width).enumerate() { + for (off_x, pixel) in row.chunks_exact(4).enumerate() { + let &[r, g, b, a] = pixel else { + panic!("Pixel doesn't have 4 components") + }; + let color = Rgba([r, g, b, a]); + img.put_pixel( + (glyph_origin_x + off_x as i32) as u32, + (glyph_origin_y + off_y as i32) as u32, + color, + ); + } + } + } + }; + } +} + +/// Render a glyph using Swash +fn render_glyph( + context: &mut ScaleContext, + font: &FontRef, + font_size: f32, + hint: bool, + glyph_id: GlyphId, + x: f32, + y: f32, +) -> Option { + use zeno::{Format, Vector}; + + // Build the scaler + let mut scaler = context.builder(*font).size(font_size).hint(hint).build(); + + // Compute the fractional offset + // You'll likely want to quantize this in a real renderer + let offset = Vector::new(x.fract(), y.fract()); + + // Select our source order + Render::new(&[ + 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(&mut scaler, glyph_id) +} diff --git a/output.png b/output.png new file mode 100644 index 0000000000000000000000000000000000000000..d6575fc41902ebc91e16bbe12fd79ec072f3432b GIT binary patch literal 19009 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 literal 0 HcmV?d00001 diff --git a/src/style/brush.rs b/src/style/brush.rs index 75b162c0..c0961ee0 100644 --- a/src/style/brush.rs +++ b/src/style/brush.rs @@ -14,3 +14,4 @@ impl Brush for [u8; 4] {} impl Brush for [u8; 3] {} impl Brush for peniko::Brush {} +impl Brush for peniko::Color {}