linebender / linebender/parley

Add `Layout` functionality to compute overall bounds of glyphs, not the typographic bounds (`Layout::height()`).

Open
#119 2 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.