alexheretic / ab-glyph

Rust API for loading, scaling, positioning and rasterizing OpenType font glyphs
Apache License 2.0
372 stars 24 forks source link

panic (index out of bounds) for specific character in a specific font when rasterizing #29

Closed tigregalis closed 3 years ago

tigregalis commented 3 years ago

As per the title.

For the font "Airstrip Four", the 'v' character appears to not have a bounding box, so when the rasterizer runs, it tries to index into the Vec but it is out of bounds.

In more detail, I've done some research and did a write-up here: https://github.com/bevyengine/bevy/issues/1254

I cloned this repo, made a small change to the image example (below) to accept text as a second parameter, and downloaded airstrip.ttf into dev/fonts, and run cargo run --example image -- "dev/fonts/airstrip.ttf" "v". I put some dbg!()s in places, and the output is below:

    Finished dev [unoptimized + debuginfo] target(s) in 
1.48s
     Running `target\debug\examples\image.exe dev/fonts/airstrip.ttf v`
Using font: airstrip.ttf
[glyph\src\ttfp.rs:297] x_min = 0
[glyph\src\ttfp.rs:297] y_min = 0
[glyph\src\ttfp.rs:297] x_max = 0
[glyph\src\ttfp.rs:297] y_max = 0
[glyph\src\outlined.rs:36] &a = Rect {
    min: point(20.0, 56.0),
    max: point(20.0, 57.0),
}
[glyph\src\outlined.rs:101] h_factor = 0.019667832      
[glyph\src\outlined.rs:101] v_factor = -0.019667832     
[glyph\src\outlined.rs:101] offset = point(0.0, 0.46416092)
[glyph\src\outlined.rs:101] w = 0
[glyph\src\outlined.rs:101] h = 1
thread 'main' panicked at 'index out of bounds: the len 
is 4 but the index is 9', rasterizer\src\raster.rs:128:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\examples\image.exe dev/fonts/airstrip.ttf v` (exit code: 
101)
//! Draws text into `image_example.png`.
//!
//! Use a custom font file: `cargo run --example image /path/to/font.otf`
use ab_glyph::{point, Font, FontRef, FontVec, PxScale, ScaleFont};
use image::{DynamicImage, Rgba};

const TEXT: &str = "This is ab_glyph rendered into a png!";

fn main() {
    let maybe_text = std::env::args().nth(2);
    if let Some(font_path) = std::env::args().nth(1) {
        let font_path = std::env::current_dir().unwrap().join(font_path);
        let data = std::fs::read(&font_path).unwrap();
        let font = FontVec::try_from_vec(data).unwrap_or_else(|_| {
            panic!(format!(
                "error constructing a Font from data at {:?}",
                font_path
            ));
        });
        if let Some(name) = font_path.file_name().and_then(|n| n.to_str()) {
            eprintln!("Using font: {}", name);
        }
        draw_image(font, maybe_text);
    } else {
        eprintln!("No font specified ... using OpenSans-Italic.ttf");
        let font = FontRef::try_from_slice(include_bytes!("../fonts/OpenSans-Italic.ttf")).unwrap();
        draw_image(font, maybe_text);
    };
}

fn draw_image<F: Font>(font: F, maybe_text: Option<String>) {
    // The font size to use
    let scale = PxScale::from(45.0);

    let scaled_font = font.as_scaled(scale);

    let mut glyphs = Vec::new();
    dev::layout_paragraph(
        scaled_font,
        point(20.0, 20.0),
        9999.0,
        maybe_text.as_deref().unwrap_or(TEXT),
        &mut glyphs,
    );

    // Use a dark red colour
    let colour = (150, 0, 0);

    // work out the layout size
    let glyphs_height = scaled_font.height().ceil() as u32;
    let glyphs_width = {
        let min_x = glyphs.first().unwrap().position.x;
        let last_glyph = glyphs.last().unwrap();
        let max_x = last_glyph.position.x + scaled_font.h_advance(last_glyph.id);
        (max_x - min_x).ceil() as u32
    };

    // Create a new rgba image with some padding
    let mut image = DynamicImage::new_rgba8(glyphs_width + 40, glyphs_height + 40).to_rgba();

    // Loop through the glyphs in the text, positing each one on a line
    for glyph in glyphs {
        if let Some(outlined) = scaled_font.outline_glyph(glyph) {
            let bounds = outlined.px_bounds();
            // Draw the glyph into the image per-pixel by using the draw closure
            outlined.draw(|x, y, v| {
                // Offset the position by the glyph bounding box
                let px = image.get_pixel_mut(x + bounds.min.x as u32, y + bounds.min.y as u32);
                // Turn the coverage into an alpha value (blended with any previous)
                *px = Rgba([
                    colour.0,
                    colour.1,
                    colour.2,
                    px.0[3].saturating_add((v * 255.0) as u8),
                ]);
            });
        }
    }

    // Save the image to a png file
    image.save("image_example.png").unwrap();
    println!("Generated: image_example.png");
}
alexheretic commented 3 years ago

It seems ttf-parser is returning a zero bounding box for this glyph. The rasterizer fails as it can't draw the outline within the incorrect bounds. I've raised an issue upstream: https://github.com/RazrFalcon/ttf-parser/issues/49

alexheretic commented 3 years ago

As I'm not 100% sure if this will be fixed upstream, I'll queue up a workaround fix in the meantime (#30).

So this should be fixed in the next release that I'll publish at the end of the week.

image example (modified to include a 'v') image_example.png

tigregalis commented 3 years ago

As I'm not 100% sure if this will be fixed upstream, I'll queue up a workaround fix in the meantime (#30).

So this should be fixed in the next release that I'll publish at the end of the week.

image example (modified to include a 'v') image_example.png

Great, thanks for this. What approach did you take, out of curiosity?

If it did need to be calculated from the curves, I was doing some searching and found this: http://pomax.nihongoresources.com/pages/bezier/

alexheretic commented 3 years ago

Great, thanks for this. What approach did you take, out of curiosity?

If it did need to be calculated from the curves, I was doing some searching and found this: http://pomax.nihongoresources.com/pages/bezier/

Have a look at bbox.rs in the pr. It's just a simple bounding box covering all points, including control points, rather than a tighter calculation of curve bounds.

I did have a quick look at the maths, but decided not to bother as this is a bug-workaround rather than core functionality. I'm also not sure how often we'll see this specific ttf issue.

alexheretic commented 3 years ago

I've published 0.2.8 with the bbox fallback