fontdue icon indicating copy to clipboard operation
fontdue copied to clipboard

Wrong glyphs returned for 0x80..0x100 character range

Open jwodder opened this issue 1 month ago • 0 comments

The following code uses fontdue to create a PNG showing a grid of all characters in a given font (assumed to be monospace) up through U+01FF:

use clap::Parser;
use font_kit::source::SystemSource;
use std::path::PathBuf;

const LINES: u32 = 0x200 / 16;

#[derive(Clone, Debug, Parser, PartialEq)]
struct Arguments {
    #[arg(short, long, default_value = "charchart.png")]
    outfile: PathBuf,

    font: String,

    size: f32,
}

fn main() -> anyhow::Result<()> {
    let args = Arguments::parse();
    let font = SystemSource::new()
        .select_by_postscript_name(&args.font)?
        .load()?;
    let font = fontdue::Font::from_bytes(
        &**font.copy_font_data().unwrap(),
        fontdue::FontSettings::default(),
    )
    .map_err(anyhow::Error::msg)?;
    let line_metrics = font.horizontal_line_metrics(args.size).unwrap();
    let baseline_height = (-line_metrics.descent).max(0.0).round() as u32;
    let char_height = line_metrics.new_line_size.round() as u32;
    let char_width = font.rasterize('m', args.size).0.advance_width.round() as u32;
    let mut img = image::GrayImage::new(char_width * 16, char_height * LINES);
    for i in 0..LINES {
        let y = (i + 1) * char_height - baseline_height;
        for j in 0..16 {
            let chr = char::try_from(i * 16 + j).unwrap();
            let (metrics, data) = font.rasterize(chr, args.size);
            let ystart = (y as i32) - metrics.ymin - (metrics.height as i32);
            let xstart = ((j * char_width) as i32) + metrics.xmin;
            for ry in 0..metrics.height {
                let Ok(py) = u32::try_from(ystart + (ry as i32)) else {
                    continue;
                };
                if py >= img.height() {
                    continue;
                }
                for rx in 0..metrics.width {
                    let Ok(px) = u32::try_from(xstart + (rx as i32)) else {
                        continue;
                    };
                    if px >= img.width() {
                        continue;
                    }
                    let value = data[rx + ry * metrics.width];
                    img.put_pixel(px, py, image::Luma([value]));
                }
            }
        }
    }
    img.save(args.outfile)?;
    Ok(())
}

Dependencies:


[dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.52", default-features = false, features = ["derive", "error-context", "help", "std", "suggestions", "usage", "wrap_help"] }
font-kit = "0.14.3"
fontdue = "0.9.3"
image = "0.25.9"

When doing cargo run -- Courier 12 on macOS Sonoma 14.7.8, the following is produced:

Image

Observe that the characters in the eight rows after the ASCII characters do not match the characters in the Latin-1 Supplement block. This happens with other fonts as well (I've mainly tested Monaco and Menlo-Regular), not always with the same characters in the rendered result.

I'm assuming that fontdue is at fault here because using ab_glyph instead:

Rendering code using ab_glyph
use ab_glyph::{Font, ScaleFont};
use clap::Parser;
use font_kit::source::SystemSource;
use std::path::PathBuf;

const LINES: u32 = 0x200 / 16;

#[derive(Clone, Debug, Parser, PartialEq)]
struct Arguments {
    #[arg(short, long, default_value = "charchart.png")]
    outfile: PathBuf,

    font: String,

    size: f32,
}

fn main() -> anyhow::Result<()> {
    let args = Arguments::parse();
    let font = SystemSource::new()
        .select_by_postscript_name(&args.font)?
        .load()?;
    let font_data = font.copy_font_data().unwrap();
    let font = ab_glyph::FontRef::try_from_slice(&font_data)?;
    let font = font.as_scaled(args.size);
    let baseline_height = (-font.descent()).max(0.0).round() as u32;
    let char_height = font.height().round() as u32;
    let char_width = font.h_advance(font.glyph_id('m')).round() as u32;
    let mut img = image::GrayImage::new(char_width * 16, char_height * LINES);
    for i in 0..LINES {
        let y = (i + 1) * char_height - baseline_height;
        for j in 0..16 {
            let chr = char::try_from(i * 16 + j).unwrap();
            let Some(glyph) = font.outline_glyph(font.scaled_glyph(chr)) else {
                continue;
            };
            let metrics = glyph.px_bounds();
            let ystart = (y as i32) + (metrics.min.y.round() as i32);
            let xstart = ((j * char_width) as i32) + (metrics.min.x.round() as i32);
            glyph.draw(|dx, dy, coverage| {
                let Ok(py) = u32::try_from(ystart + (dy as i32)) else {
                    return;
                };
                if py >= img.height() {
                    return;
                }
                let Ok(px) = u32::try_from(xstart + (dx as i32)) else {
                    return;
                };
                if px >= img.width() {
                    return;
                }
                let value = (255.0 * coverage).round() as u8;
                img.put_pixel(px, py, image::Luma([value]));
            });
        }
    }
    img.save(args.outfile)?;
    Ok(())
}

Dependencies:


[dependencies]
ab_glyph = "0.2.32"
anyhow = "1.0.100"
clap = { version = "4.5.52", default-features = false, features = ["derive", "error-context", "help", "std", "suggestions", "usage", "wrap_help"] }
font-kit = "0.14.3"
image = "0.25.9"

produces an image with the correct Latin-1 characters:

Image

jwodder avatar Nov 18 '25 13:11 jwodder