From 98cb2ccded1084f015ab2a1d1a4db8d1169541f9 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 21 May 2024 13:24:01 +0100 Subject: [PATCH 01/15] Implement simple example using swash to render to png --- Cargo.lock | 44 ++++++ Cargo.toml | 3 +- examples/simple.rs | 236 ++++++++++++++++++++++++++++++ examples/swash_render/Cargo.toml | 18 +++ examples/swash_render/src/main.rs | 236 ++++++++++++++++++++++++++++++ 5 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 examples/simple.rs create mode 100644 examples/swash_render/Cargo.toml create mode 100644 examples/swash_render/src/main.rs 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/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/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..20ee8f58 --- /dev/null +++ b/examples/swash_render/src/main.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) +} From c47fa5063bf8ab9be52695dc145bd51373ac2466 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 21 May 2024 23:17:16 +0100 Subject: [PATCH 02/15] Simplify swash FontRef creation --- examples/simple.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 20ee8f58..76e3f9d0 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -10,11 +10,10 @@ 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::zeno; use swash::{FontRef, GlyphId}; fn main() { @@ -114,21 +113,8 @@ fn render_glyph_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 - }; + let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); // Iterates over the glyphs in the GlyphRun for glyph in glyph_run.glyphs() { From 8cef302d712fd1d4f6758ea0f19ba1f381a5d7c2 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 21 May 2024 23:28:11 +0100 Subject: [PATCH 03/15] Gamma correction WIP --- examples/simple.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/simple.rs b/examples/simple.rs index 76e3f9d0..6cea2cc8 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -16,6 +16,22 @@ use swash::scale::{Render, ScaleContext, Source, StrikeWith}; use swash::zeno; use swash::{FontRef, GlyphId}; +// fn u8_to_float(num: u8) -> f32 { +// num as f32 / 255.0 +// } + +// fn float_to_u8(num: f32) -> u8 { +// (num * 255.0) as u8 +// } + +// fn to_linear_rgb(srgb: f32) -> f32 { +// if srgb <= 0.04045 { +// srgb / 12.92 +// } else { +// ((srgb + 0.055) / 1.055).powf(2.4) +// } +// } + fn main() { // The text we are going to style and lay out let text = String::from( From cea8aa8e09a5cfb14da3af1395dca44c09a2377a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 22 May 2024 00:13:21 +0100 Subject: [PATCH 04/15] Implement gamma correction --- examples/simple.rs | 75 ++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 6cea2cc8..a9b78946 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -16,21 +16,55 @@ use swash::scale::{Render, ScaleContext, Source, StrikeWith}; use swash::zeno; use swash::{FontRef, GlyphId}; -// fn u8_to_float(num: u8) -> f32 { -// num as f32 / 255.0 -// } - -// fn float_to_u8(num: f32) -> u8 { -// (num * 255.0) as u8 -// } - -// fn to_linear_rgb(srgb: f32) -> f32 { -// if srgb <= 0.04045 { -// srgb / 12.92 -// } else { -// ((srgb + 0.055) / 1.055).powf(2.4) -// } -// } +fn u8_to_float(num: u8) -> f32 { + num as f32 / 255.0 +} + +fn float_to_u8(num: f32) -> u8 { + (num * 255.0) as u8 +} + +fn to_linear_rgb(srgb: f32) -> f32 { + if srgb <= 0.04045 { + srgb / 12.92 + } else { + ((srgb + 0.055) / 1.055).powf(2.4) + } +} + +fn to_srgb(linear_rgb: f32) -> f32 { + if linear_rgb <= 0.0031308 { + linear_rgb * 12.92 + } else { + 1.055 * linear_rgb.powf(1.0 / 2.4) - 0.055 + } +} + +fn blend_px_gamma_corrected(existing: Rgba, color: Color, alpha: u8) -> Rgba { + let [r1, g1, b1, _] = existing.0.map(|x| to_linear_rgb(u8_to_float(x))); + let [r2, g2, b2] = [color.r, color.g, color.b].map(|x| to_linear_rgb(u8_to_float(x))); + + let a = u8_to_float(alpha); + let inv_a = 1.0 - a; + + // Blend pixel with underlying color + let r = a * r2 + inv_a * r1; + let g = a * g2 + inv_a * g1; + let b = a * b2 + inv_a * b1; + + Rgba([r, g, b, 1.0].map(|x| float_to_u8(to_srgb(x)))) +} + +fn blend_px_naive(existing: Rgba, color: Color, alpha: u8) -> Rgba { + 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] = existing.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; + + Rgba([r as u8, g as u8, b as u8, 255]) +} fn main() { // The text we are going to style and lay out @@ -168,16 +202,7 @@ fn render_glyph_run( 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]); - + let color = blend_px_naive(*img.get_pixel(x, y), color, alpha); img.put_pixel(x, y, color); } i += 1; From c78fbcd3e29c196729a8614d5be5d9aa19714929 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 22 May 2024 16:18:27 +0100 Subject: [PATCH 05/15] Fix bitmap emoji rasterisation --- examples/simple.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index a9b78946..5265b55e 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -211,17 +211,22 @@ fn render_glyph_run( } Content::SubpixelMask => unimplemented!(), Content::Color => { - for (off_y, row) in rendered_glyph.data.chunks_exact(glyph_width).enumerate() { + for (off_y, row) in rendered_glyph + .data + .chunks_exact(glyph_width * 4) + .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") + panic!("Not RGBA") }; - 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, - ); + let x = (glyph_origin_x + off_x as i32) as u32; + let y = (glyph_origin_y + off_y as i32) as u32; + if a > 0 { + let color = + blend_px_naive(*img.get_pixel(x, y), Color::rgb8(r, g, b), a); + img.put_pixel(x, y, color); + } } } } From 3a8438b8752ab2cb1f02347b154e442bcc1f3a87 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 22 May 2024 16:56:30 +0100 Subject: [PATCH 06/15] Use blend function from the image crate --- examples/simple.rs | 61 ++++------------------------------------------ 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 5265b55e..a163f5de 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -5,7 +5,7 @@ //! and and then renders it into a PNG using the `image` crate. use image::codecs::png::PngEncoder; -use image::{self, Rgba, RgbaImage}; +use image::{self, Pixel, Rgba, RgbaImage}; use parley::layout::{Alignment, GlyphRun, Layout}; use parley::style::{FontStack, FontWeight, StyleProperty}; use parley::{FontContext, LayoutContext}; @@ -16,56 +16,6 @@ use swash::scale::{Render, ScaleContext, Source, StrikeWith}; use swash::zeno; use swash::{FontRef, GlyphId}; -fn u8_to_float(num: u8) -> f32 { - num as f32 / 255.0 -} - -fn float_to_u8(num: f32) -> u8 { - (num * 255.0) as u8 -} - -fn to_linear_rgb(srgb: f32) -> f32 { - if srgb <= 0.04045 { - srgb / 12.92 - } else { - ((srgb + 0.055) / 1.055).powf(2.4) - } -} - -fn to_srgb(linear_rgb: f32) -> f32 { - if linear_rgb <= 0.0031308 { - linear_rgb * 12.92 - } else { - 1.055 * linear_rgb.powf(1.0 / 2.4) - 0.055 - } -} - -fn blend_px_gamma_corrected(existing: Rgba, color: Color, alpha: u8) -> Rgba { - let [r1, g1, b1, _] = existing.0.map(|x| to_linear_rgb(u8_to_float(x))); - let [r2, g2, b2] = [color.r, color.g, color.b].map(|x| to_linear_rgb(u8_to_float(x))); - - let a = u8_to_float(alpha); - let inv_a = 1.0 - a; - - // Blend pixel with underlying color - let r = a * r2 + inv_a * r1; - let g = a * g2 + inv_a * g1; - let b = a * b2 + inv_a * b1; - - Rgba([r, g, b, 1.0].map(|x| float_to_u8(to_srgb(x)))) -} - -fn blend_px_naive(existing: Rgba, color: Color, alpha: u8) -> Rgba { - 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] = existing.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; - - Rgba([r as u8, g as u8, b as u8, 255]) -} - fn main() { // The text we are going to style and lay out let text = String::from( @@ -202,8 +152,8 @@ fn render_glyph_run( let y = (glyph_origin_y + off_y) as u32; let alpha = rendered_glyph.data[i]; if alpha > 0 { - let color = blend_px_naive(*img.get_pixel(x, y), color, alpha); - img.put_pixel(x, y, color); + let color = Rgba::from([color.r, color.g, color.b, alpha]); + img.get_pixel_mut(x, y).blend(&color); } i += 1; } @@ -223,9 +173,8 @@ fn render_glyph_run( let x = (glyph_origin_x + off_x as i32) as u32; let y = (glyph_origin_y + off_y as i32) as u32; if a > 0 { - let color = - blend_px_naive(*img.get_pixel(x, y), Color::rgb8(r, g, b), a); - img.put_pixel(x, y, color); + let color = Rgba::from([r, g, b, a]); + img.get_pixel_mut(x, y).blend(&color); } } } From 389b28880c39ddf7ac6fa8591685c1a430bae2f1 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 22 May 2024 17:00:09 +0100 Subject: [PATCH 07/15] Pass normalized_coords through to swash --- examples/simple.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index a163f5de..26eef597 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -13,7 +13,7 @@ use peniko::Color; use std::fs::File; use swash::scale::image::{Content, Image as SwashImage}; use swash::scale::{Render, ScaleContext, Source, StrikeWith}; -use swash::zeno; +use swash::{zeno, NormalizedCoord}; use swash::{FontRef, GlyphId}; fn main() { @@ -112,6 +112,7 @@ fn render_glyph_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(); @@ -128,6 +129,7 @@ fn render_glyph_run( font_size, true, glyph_id, + normalized_coords, glyph_x.fract(), glyph_y.fract(), ) else { @@ -190,13 +192,19 @@ fn render_glyph( font_size: f32, hint: bool, glyph_id: GlyphId, + normalized_coords: &[NormalizedCoord], x: f32, y: f32, ) -> Option { use zeno::{Format, Vector}; // Build the scaler - let mut scaler = context.builder(*font).size(font_size).hint(hint).build(); + let mut scaler = context + .builder(*font) + .size(font_size) + .hint(hint) + .normalized_coords(normalized_coords) + .build(); // Compute the fractional offset // You'll likely want to quantize this in a real renderer From 3a88a7a7c25c33098b0c3afbeb1e66c9240ac627 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 22 May 2024 17:04:30 +0100 Subject: [PATCH 08/15] Make hinting always-on to reduce arg count to make clippy happy --- examples/simple.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 26eef597..3e845cf9 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -127,7 +127,6 @@ fn render_glyph_run( context, &font_ref, font_size, - true, glyph_id, normalized_coords, glyph_x.fract(), @@ -190,7 +189,6 @@ fn render_glyph( context: &mut ScaleContext, font: &FontRef, font_size: f32, - hint: bool, glyph_id: GlyphId, normalized_coords: &[NormalizedCoord], x: f32, @@ -202,7 +200,7 @@ fn render_glyph( let mut scaler = context .builder(*font) .size(font_size) - .hint(hint) + .hint(true) .normalized_coords(normalized_coords) .build(); From a73d530972d872ca379b43dc023e8bef00ade7f4 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 15:31:44 +0100 Subject: [PATCH 09/15] Apply image based blending --- examples/swash_render/src/main.rs | 46 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 20ee8f58..2973a559 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -5,7 +5,7 @@ //! and and then renders it into a PNG using the `image` crate. use image::codecs::png::PngEncoder; -use image::{self, Rgba, RgbaImage}; +use image::{self, Rgba, RgbaImage, Pixel}; use parley::layout::{Alignment, GlyphRun, Layout}; use parley::style::{FontStack, FontWeight, StyleProperty}; use parley::{FontContext, LayoutContext}; @@ -89,8 +89,19 @@ fn main() { } } - // Write image to PNG file - let output_file = File::create("output.png").unwrap(); + // 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(); } @@ -162,39 +173,26 @@ fn render_glyph_run( 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 x = (glyph_origin_x + off_x as i32) as u32; + let y = (glyph_origin_y + off_y as i32) 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); - } + 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 => { - for (off_y, row) in rendered_glyph.data.chunks_exact(glyph_width).enumerate() { + for (off_y, row) in rendered_glyph.data.chunks_exact(glyph_width * 4).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 x = (glyph_origin_x + off_x as i32) as u32; + let y = (glyph_origin_y + off_y as i32) as u32; 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, - ); + img.get_pixel_mut(x, y).blend(&color); } } } From a5c5ef7ce9941a82517e52ecd25fbe582788af0e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 22:27:42 +0100 Subject: [PATCH 10/15] Reapply changes lost in rebase --- examples/swash_render/src/main.rs | 38 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 2973a559..270e3b0d 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -5,16 +5,15 @@ //! and and then renders it into a PNG using the `image` crate. use image::codecs::png::PngEncoder; -use image::{self, Rgba, RgbaImage, Pixel}; +use image::{self, Pixel, 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::{zeno, NormalizedCoord}; use swash::{FontRef, GlyphId}; fn main() { @@ -124,22 +123,10 @@ fn render_glyph_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; + let normalized_coords = run.normalized_coords(); // Convert from parley::Font to swash::FontRef - let font_ref = FontRef { - data: font.data.as_ref(), - offset: offset as u32, - key: CacheKey::new(), // ignored - }; + let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap(); // Iterates over the glyphs in the GlyphRun for glyph in glyph_run.glyphs() { @@ -151,7 +138,7 @@ fn render_glyph_run( context, &font_ref, font_size, - true, + normalized_coords, glyph_id, glyph_x.fract(), glyph_y.fract(), @@ -184,7 +171,11 @@ fn render_glyph_run( } Content::SubpixelMask => unimplemented!(), Content::Color => { - for (off_y, row) in rendered_glyph.data.chunks_exact(glyph_width * 4).enumerate() { + for (off_y, row) in rendered_glyph + .data + .chunks_exact(glyph_width * 4) + .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") @@ -205,7 +196,7 @@ fn render_glyph( context: &mut ScaleContext, font: &FontRef, font_size: f32, - hint: bool, + normalized_coords: &[NormalizedCoord], glyph_id: GlyphId, x: f32, y: f32, @@ -213,7 +204,12 @@ fn render_glyph( use zeno::{Format, Vector}; // Build the scaler - let mut scaler = context.builder(*font).size(font_size).hint(hint).build(); + let mut scaler = context + .builder(*font) + .size(font_size) + .hint(true) + .normalized_coords(normalized_coords) + .build(); // Compute the fractional offset // You'll likely want to quantize this in a real renderer From 399a44755cdda2b0ee75875e98a2354d317505d1 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 22:30:48 +0100 Subject: [PATCH 11/15] Fix clippy lints --- examples/swash_render/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 270e3b0d..e47d28c1 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -160,8 +160,8 @@ fn render_glyph_run( 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 i32) as u32; - let y = (glyph_origin_y + off_y as i32) as u32; + let x = (glyph_origin_x + off_x) as u32; + let y = (glyph_origin_y + off_y) as u32; let alpha = rendered_glyph.data[i]; let color = Rgba([color.r, color.g, color.b, alpha]); img.get_pixel_mut(x, y).blend(&color); From a671f69af3cf6d7c3176db0a296251dd43e944a2 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 22:35:35 +0100 Subject: [PATCH 12/15] Simplify background fill --- examples/swash_render/src/main.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index e47d28c1..9635dee2 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -29,8 +29,8 @@ fn main() { 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); + let text_color = Color::rgb8(0, 0, 0); + let bg_color = Rgba([255, 255, 255, 255]); // Padding around the output image let padding = 20; @@ -47,7 +47,7 @@ fn main() { 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); + let brush_style = StyleProperty::Brush(text_color); builder.push_default(&brush_style); // Set default font family @@ -67,18 +67,11 @@ fn main() { // 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, - ]); - } + + // 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() { From bcd23585a03bd8c18aed31b025e517e233b6a1bd Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 23:09:35 +0100 Subject: [PATCH 13/15] Remove duplicate file (rebase conflict) --- examples/simple.rs | 223 --------------------------------------------- 1 file changed, 223 deletions(-) delete mode 100644 examples/simple.rs diff --git a/examples/simple.rs b/examples/simple.rs deleted file mode 100644 index 3e845cf9..00000000 --- a/examples/simple.rs +++ /dev/null @@ -1,223 +0,0 @@ -// 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, GlyphRun, Layout}; -use parley::style::{FontStack, FontWeight, StyleProperty}; -use parley::{FontContext, LayoutContext}; -use peniko::Color; -use std::fs::File; -use swash::scale::image::{Content, Image as SwashImage}; -use swash::scale::{Render, ScaleContext, Source, StrikeWith}; -use swash::{zeno, NormalizedCoord}; -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(); - 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(); - - // 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, - glyph_id, - normalized_coords, - 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 { - let color = Rgba::from([color.r, color.g, color.b, alpha]); - img.get_pixel_mut(x, y).blend(&color); - } - i += 1; - } - } - } - Content::SubpixelMask => unimplemented!(), - Content::Color => { - for (off_y, row) in rendered_glyph - .data - .chunks_exact(glyph_width * 4) - .enumerate() - { - for (off_x, pixel) in row.chunks_exact(4).enumerate() { - let &[r, g, b, a] = pixel else { - panic!("Not RGBA") - }; - let x = (glyph_origin_x + off_x as i32) as u32; - let y = (glyph_origin_y + off_y as i32) as u32; - if a > 0 { - let color = Rgba::from([r, g, b, a]); - img.get_pixel_mut(x, y).blend(&color); - } - } - } - } - }; - } -} - -/// Render a glyph using Swash -fn render_glyph( - context: &mut ScaleContext, - font: &FontRef, - font_size: f32, - glyph_id: GlyphId, - normalized_coords: &[NormalizedCoord], - x: f32, - y: f32, -) -> Option { - use zeno::{Format, Vector}; - - // Build the scaler - let mut scaler = context - .builder(*font) - .size(font_size) - .hint(true) - .normalized_coords(normalized_coords) - .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) -} From 3e45a9a17ba8cd422f3c018aa3a143f465b418c9 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 23:17:07 +0100 Subject: [PATCH 14/15] Only build one scaler per glyph run --- examples/swash_render/src/main.rs | 85 ++++++++++++------------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 9635dee2..2ec97cc1 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -11,10 +11,11 @@ use parley::style::{FontStack, FontWeight, StyleProperty}; use parley::{FontContext, LayoutContext}; use peniko::Color; use std::fs::File; -use swash::scale::image::{Content, Image as SwashImage}; +use swash::scale::image::Content; use swash::scale::{Render, ScaleContext, Source, StrikeWith}; -use swash::{zeno, NormalizedCoord}; +use swash::zeno; use swash::{FontRef, GlyphId}; +use zeno::{Format, Vector}; fn main() { // The text we are going to style and lay out @@ -121,24 +122,42 @@ fn render_glyph_run( // 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_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, - normalized_coords, - glyph_id, - glyph_x.fract(), - glyph_y.fract(), - ) else { - println!("No glyph"); - continue; - }; + + // 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(&mut scaler, glyph_id) + .unwrap(); let glyph_width = usize::try_from(rendered_glyph.placement.width).expect("usize < 32 bits"); let glyph_height = @@ -183,41 +202,3 @@ fn render_glyph_run( }; } } - -/// Render a glyph using Swash -fn render_glyph( - context: &mut ScaleContext, - font: &FontRef, - font_size: f32, - normalized_coords: &[NormalizedCoord], - 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(true) - .normalized_coords(normalized_coords) - .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) -} From 4b42666be962dcbf0a81128d5c88db20db54b95b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 23 May 2024 23:30:24 +0100 Subject: [PATCH 15/15] Move render_glyph back into it's own function --- examples/swash_render/src/main.rs | 131 +++++++++++++++--------------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 2ec97cc1..d09d6499 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -6,15 +6,15 @@ use image::codecs::png::PngEncoder; use image::{self, Pixel, Rgba, RgbaImage}; -use parley::layout::{Alignment, GlyphRun, Layout}; +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, Source, StrikeWith}; +use swash::scale::{Render, ScaleContext, Scaler, Source, StrikeWith}; use swash::zeno; -use swash::{FontRef, GlyphId}; +use swash::FontRef; use zeno::{Format, Vector}; fn main() { @@ -133,72 +133,73 @@ fn render_glyph_run( // 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; + let glyph_x = run_x + glyph.x + (padding as f32); + let glyph_y = run_y - glyph.y + (padding as f32); run_x += glyph.advance; - // 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(&mut scaler, glyph_id) - .unwrap(); - - 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]; - let color = Rgba([color.r, color.g, color.b, alpha]); - img.get_pixel_mut(x, y).blend(&color); - i += 1; - } + 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 => { - for (off_y, row) in rendered_glyph - .data - .chunks_exact(glyph_width * 4) - .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 x = (glyph_origin_x + off_x as i32) as u32; - let y = (glyph_origin_y + off_y as i32) as u32; - let color = Rgba([r, g, b, a]); - img.get_pixel_mut(x, y).blend(&color); - } + } + 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); } } - }; - } + } + }; }