fcft 0.1.0

Bindings to fcft: library for font loading and glyph rasterization
Documentation
//! Tests require fontconfig and the Open Sans + Twemoji fonts.

use fcft::{Canvas, Color, Font, GlyphFormat, Subpixel};

const OPEN_SANS: &str = "Open Sans:pixelsize=24";

#[test]
fn capabilities_include_text_run_shaping() {
    let caps = fcft::capabilities();
    assert!(caps.text_run_shaping());
    assert!(caps.grapheme_shaping());
}

#[test]
fn nonsense_family_still_matches() {
    // fontconfig always matches something.
    let font = Font::new(&["No Such Font Family Exists:pixelsize=16"], None);
    assert!(font.is_ok());
}

#[test]
fn metrics_are_positive() {
    let font = Font::new(&[OPEN_SANS], None).unwrap();
    assert!(font.height() > 0);
    assert!(font.ascent() > 0);
    assert!(font.descent() > 0);
    assert!(font.max_advance().0 > 0);
}

#[test]
fn shaping_yields_expected_glyphs() {
    let font = Font::new(&[OPEN_SANS], None).unwrap();
    let run = font.shape("Hello 123", Subpixel::None);
    assert_eq!(run.len(), 9);
    for g in run.glyphs() {
        assert!(g.advance.0 > 0);
    }
    assert!(run.width() > 0);
}

#[test]
fn measuring_is_monotonic() {
    let font = Font::new(&[OPEN_SANS], None).unwrap();
    let text = "Hailing frequencies open";
    let mut previous = 0;
    for end in text.char_indices().map(|(i, _)| i).chain([text.len()]) {
        let width = font.shape(&text[..end], Subpixel::None).width();
        assert!(width >= previous, "width shrank at {:?}", &text[..end]);
        previous = width;
    }
}

#[test]
fn truncation_fits_max_width() {
    let font = Font::new(&[OPEN_SANS], None).unwrap();
    let text = "Hailing frequencies open";
    let full_width = font.shape(text, Subpixel::None).width();

    // Wide enough: untruncated.
    let run = font.shape_truncated(text, Subpixel::None, full_width);
    assert_eq!(run.width(), full_width);

    // Truncated: fits, and still ends up non-trivial.
    let max = full_width / 2;
    let run = font.shape_truncated(text, Subpixel::None, max);
    assert!(run.width() <= max);
    assert!(run.len() > 2);

    // Absurdly narrow: degenerates to nothing rather than panicking.
    let run = font.shape_truncated(text, Subpixel::None, 1);
    assert!(run.width() <= 1);
}

#[test]
fn drawing_covers_expected_pixels() {
    const WIDTH: usize = 200;
    const HEIGHT: usize = 40;
    const STRIDE: usize = WIDTH * 4;

    let font = Font::new(&["Open Sans:pixelsize=20"], None).unwrap();
    let run = font.shape("Hi", Subpixel::None);
    let text_width = run.width();
    assert!(text_width > 0 && text_width < 60);

    let mut buf = vec![0u8; STRIDE * HEIGHT];
    let mut canvas = Canvas::from_xrgb8888(&mut buf, WIDTH as u32, HEIGHT as u32, STRIDE as u32);
    let baseline = font.ascent();
    let (pen_x, _) = canvas.draw(&run, 10, baseline, Color::from_argb8888(0xffffffff));
    assert_eq!(pen_x, 10 + text_width);
    drop(canvas);

    let painted = |x: usize, y: usize| -> bool {
        let p = y * STRIDE + x * 4;
        buf[p] != 0 || buf[p + 1] != 0 || buf[p + 2] != 0
    };
    let painted_in = |x0: usize, x1: usize, y0: usize, y1: usize| -> usize {
        (y0..y1)
            .flat_map(|y| (x0..x1).map(move |x| (x, y)))
            .filter(|&(x, y)| painted(x, y))
            .count()
    };

    // Ink inside the text box, none to the left of the pen or past the end.
    assert!(painted_in(10, 10 + text_width as usize, 0, HEIGHT) > 0);
    assert_eq!(painted_in(0, 9, 0, HEIGHT), 0);
    assert_eq!(
        painted_in(10 + text_width as usize + 2, WIDTH, 0, HEIGHT),
        0
    );
}

#[test]
fn color_emoji_is_a_color_glyph() {
    let font = Font::new(&["Twemoji:pixelsize=24"], None).unwrap();
    let glyph = font.glyph('😀', Subpixel::None).unwrap();
    assert!(glyph.is_color_glyph());
    assert_eq!(glyph.format(), GlyphFormat::A8R8G8B8);

    // And a regular grayscale-AA glyph is an a8 mask.
    let font = Font::new(&[OPEN_SANS], None).unwrap();
    let glyph = font.glyph('A', Subpixel::None).unwrap();
    assert!(!glyph.is_color_glyph());
    assert_eq!(glyph.format(), GlyphFormat::A8);
}

#[test]
fn kerning_is_queryable() {
    let font = Font::new(&[OPEN_SANS], None).unwrap();
    // XXX: Why does this return None?
    let _ = font.kerning('A', 'V');
}