diff --git a/Cargo.lock b/Cargo.lock index a90ba5d5..ce39ffd8 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" @@ -20,6 +26,12 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" version = "0.7.4" @@ -119,6 +131,15 @@ dependencies = [ "libm", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "displaydoc" version = "0.2.4" @@ -144,6 +165,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" @@ -351,6 +391,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d642685b028806386b2b6e75685faadd3eb65a85fff7df711ce18446a422da" +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + [[package]] name = "memmap2" version = "0.9.4" @@ -360,6 +406,16 @@ 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 = "once_cell" version = "1.19.0" @@ -374,6 +430,7 @@ dependencies = [ "peniko", "skrifa", "swash", + "tiny-skia", ] [[package]] @@ -386,6 +443,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 +510,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" @@ -463,6 +539,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "swash" version = "0.1.15" @@ -516,6 +598,32 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 65ac3dd8..dbf3340f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ skrifa = { workspace = true } peniko = { workspace = true } fontique = { workspace = true } +[dev-dependencies] +tiny-skia = "0.11.4" + [workspace.dependencies] fontique = { version = "0.1.0", default-features = false, path = "fontique" } skrifa = { version = "0.19.1", default-features = false } diff --git a/examples/tiny-skia.rs b/examples/tiny-skia.rs new file mode 100644 index 00000000..27533f67 --- /dev/null +++ b/examples/tiny-skia.rs @@ -0,0 +1,211 @@ +// 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 = 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, 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(pixmap: PixmapMut) -> TinySkiaPen { + 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) { + self.open_path.close(); + 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::Winding, + Transform::identity(), + None, + ); + } +} diff --git a/src/style/brush.rs b/src/style/brush.rs index 75b162c0..ad671f06 100644 --- a/src/style/brush.rs +++ b/src/style/brush.rs @@ -4,13 +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 T {}