linebender / linebender/parley
Add `Layout` functionality to compute overall bounds of glyphs, not the typographic bounds (`Layout::height()`).
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 736
- Forks
- 120
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 53
Description
The following example, adapted from `swash_render`, fails on my machine (Windows 11)
Code
```rust
//! A simple example that lays out some text using Parley, rasterises the glyph using Swash
//! 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, PositionedLayoutItem};
use parley::style::{FontStack, FontWeight, StyleProperty, TextStyle};
use parley::{FontContext, InlineBox, 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("Sample Text");
// The display scale for HiDPI rendering
let display_scale = 1.0;
// The width for line wrapping
let max_advance = Some(400.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 = 1;
// 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();
// Setup some Parley text styles
let font_stack = FontStack::Source("Inter");
let mut layout = {
// TREE BUILDER
// ============
let root_style = TextStyle {
brush: text_color,
font_stack,
line_height: 1.0,
font_size: 40.0,
..Default::default()
};
let mut builder = layout_cx.tree_builder(&mut font_cx, display_scale, &root_style);
builder.push_text(&text);
// Build the builder into a Layout
// let mut layout: Layout = builder.build(&text);
let (layout, _text): (Layout, String) = builder.build();
layout
};
// Perform layout (including bidi resolution and shaping) with start alignment
layout.break_all_lines(max_advance);
layout.align(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 item in line.items() {
match item {
PositionedLayoutItem::GlyphRun(glyph_run) => {
render_glyph_run(&mut scale_cx, &glyph_run, &mut img, padding);
}
PositionedLayoutItem::InlineBox(inline_box) => {
for x_off in 0..(inline_box.width.floor() as u32) {
for y_off in 0..(inline_box.height.floor() as u32) {
let x = inline_box.x as u32 + x_off + padding;
let y = inline_box.y as u32 + y_off + padding;
img.put_pixel(x, y, Rgba([0, 0, 0, 255]));
}
}
}
};
}
}
// 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);
}
}
}
};
}
```
with `thread 'main' panicked at examples\swash_render\src/main.rs:197:25: Image index (86, 42) out of bounds (235, 42)`. It seems that the allocated image isn't tall enough. It does work correctly if I change the text to `Samle Text` (without the `p`). It's as if the height calculation didn't take into account the descender of the `p`.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the failure from examples/swash_render/src/main.rs with “Sample Text”, then compare it with “Samle Text”. Start at the Layout::height() call used to size the image and trace how glyph descenders affect the calculated bounds. Done means the rendered image includes descenders without an out-of-bounds panic.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- computer-graphics
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100