mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
156 lines
6.9 KiB
Rust
156 lines
6.9 KiB
Rust
//! Colour bitmap glyphs (`sbix`, `CBDT`): laid out as images in place of outlines, in the box
|
|
//! their strike gives, moving with the type.
|
|
|
|
use kurbo::{Point, Rect};
|
|
use skrifa::MetadataProvider;
|
|
use skrifa::instance::{LocationRef, Size};
|
|
use vectorcraft_doc::CharStyle;
|
|
|
|
use super::test_fonts::{COLOUR_CBDT_BEARINGS, COLOUR_CHAR, COLOUR_PPEM, colour_bitmap_family, colour_bitmap_font};
|
|
use super::*;
|
|
|
|
fn db(sbix: bool) -> FontDb {
|
|
let db = FontDb::with_font_dirs(vec![]);
|
|
assert_eq!(db.add_font(colour_bitmap_font(sbix).expect("the test font builds")), 1);
|
|
db
|
|
}
|
|
|
|
fn style(sbix: bool) -> CharStyle {
|
|
CharStyle { font_family: colour_bitmap_family(sbix).into(), size: 100.0, ..CharStyle::default() }
|
|
}
|
|
|
|
/// The text-space box of glyph `g`'s image.
|
|
fn image_box(g: &PositionedGlyph) -> Rect {
|
|
let (image, xf) = g.placed_image().expect("the glyph is an image");
|
|
xf.transform_rect_bbox(image.rect())
|
|
}
|
|
|
|
fn assert_near(a: Rect, b: Rect) {
|
|
let d = [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1];
|
|
assert!(d.iter().all(|d| d.abs() < 1e-6), "{a:?} is not {b:?}");
|
|
}
|
|
|
|
/// The left side bearing of [`COLOUR_CHAR`] in the bundled Source Sans 3 (font units of 1000).
|
|
fn lsb() -> f64 {
|
|
let font = skrifa::FontRef::new(include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf")).unwrap();
|
|
let gid = font.charmap().map(COLOUR_CHAR).unwrap();
|
|
f64::from(font.glyph_metrics(Size::unscaled(), LocationRef::default()).left_side_bearing(gid).unwrap())
|
|
}
|
|
|
|
#[test]
|
|
fn an_sbix_glyph_is_its_largest_strike_one_em_wide_sitting_on_the_shifted_baseline() {
|
|
let db = db(true);
|
|
let l = layout(&db, &TextObject::point(Point::new(10.0, 50.0), &COLOUR_CHAR.to_string(), style(true)));
|
|
let g = &l.glyphs[0];
|
|
assert!(g.outline.elements().is_empty(), "the image replaces the outline");
|
|
let image = g.image.as_ref().unwrap();
|
|
assert_eq!((image.width, image.height), (u32::from(COLOUR_PPEM), u32::from(COLOUR_PPEM)), "the largest strike");
|
|
// From the glyph's left side bearing, its bottom 0.128 em below the baseline.
|
|
let x = g.origin.x + lsb() * 0.1;
|
|
assert_near(image_box(g), Rect::new(x, g.origin.y - 87.2, x + 100.0, g.origin.y + 12.8));
|
|
assert!(l.bounds.contains_rect(image_box(g)), "the bounds hold the image: {:?}", l.bounds);
|
|
}
|
|
|
|
#[test]
|
|
fn a_cbdt_glyph_sits_where_its_bearings_put_it() {
|
|
let db = db(false);
|
|
let l = layout(&db, &TextObject::point(Point::new(10.0, 50.0), &COLOUR_CHAR.to_string(), style(false)));
|
|
let g = &l.glyphs[0];
|
|
assert!(g.outline.elements().is_empty());
|
|
let (bx, by) = COLOUR_CBDT_BEARINGS;
|
|
let px = 100.0 / f64::from(COLOUR_PPEM);
|
|
let (x, top) = (g.origin.x + f64::from(bx) * px, g.origin.y - f64::from(by) * px);
|
|
assert_near(image_box(g), Rect::new(x, top, x + 100.0, top + 100.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_bitmap_glyph_scales_with_the_type() {
|
|
let db = db(false);
|
|
let st = CharStyle { h_scale: 200.0, ..style(false) };
|
|
let l = layout(&db, &TextObject::point(Point::ZERO, &COLOUR_CHAR.to_string(), st));
|
|
let b = image_box(&l.glyphs[0]);
|
|
assert!((b.width() - 200.0).abs() < 1e-6 && (b.height() - 100.0).abs() < 1e-6, "{b:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_bitmap_glyph_moves_when_type_snaps_to_pixels() {
|
|
let db = db(false);
|
|
let mut l = layout(&db, &TextObject::point(Point::ZERO, &COLOUR_CHAR.to_string(), style(false)));
|
|
let (before, origin) = (image_box(&l.glyphs[0]), l.glyphs[0].origin);
|
|
assert!(l.snap_to_pixels(Affine::translate((0.3, 0.4))));
|
|
let g = &l.glyphs[0];
|
|
assert!(g.origin != origin);
|
|
assert_near(image_box(g), before + (g.origin - origin));
|
|
}
|
|
|
|
#[test]
|
|
fn other_glyphs_of_a_colour_font_keep_their_outlines() {
|
|
let db = db(true);
|
|
let l = layout(&db, &TextObject::point(Point::ZERO, "F", style(true)));
|
|
assert!(l.glyphs[0].image.is_none());
|
|
assert!(!l.glyphs[0].outline.elements().is_empty());
|
|
}
|
|
|
|
/// A strike PNG past the caps, or whose header doesn't read, gives the glyph no image: it is not
|
|
/// decoded (a font is untrusted input).
|
|
#[test]
|
|
fn a_bitmap_past_the_caps_or_without_a_header_is_no_image() {
|
|
use super::bitmap::{MAX_PNG_BYTES, MAX_SIDE};
|
|
use super::test_fonts::{COLOUR_LARGE, colour_bitmap_font_with, solid_png};
|
|
let good = solid_png(COLOUR_PPEM.into(), COLOUR_LARGE);
|
|
// A header claiming a side past the cap (the IHDR width).
|
|
let mut wide = good.clone();
|
|
wide[16..20].copy_from_slice(&(MAX_SIDE + 1).to_be_bytes());
|
|
// More bytes than a glyph's PNG may have.
|
|
let mut long = good.clone();
|
|
long.resize(MAX_PNG_BYTES + 1, 0);
|
|
let headless = good[8..].to_vec();
|
|
for sbix in [true, false] {
|
|
for (what, png) in [("wide", &wide), ("long", &long), ("headless", &headless), ("good", &good)] {
|
|
let db = FontDb::with_font_dirs(vec![]);
|
|
assert_eq!(db.add_font(colour_bitmap_font_with(sbix, png).unwrap()), 1);
|
|
let l = layout(&db, &TextObject::point(Point::ZERO, &COLOUR_CHAR.to_string(), style(sbix)));
|
|
assert_eq!(l.glyphs[0].image.is_some(), what == "good", "{what} (sbix {sbix})");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A colour bitmap font without outline tables (Noto Color Emoji has `CBDT` but no `glyf`) is an
|
|
/// installed font like any other.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[test]
|
|
fn a_bitmap_only_font_file_is_cataloged() {
|
|
use crate::fontdb::file_face_names_within;
|
|
let full = colour_bitmap_font(false).unwrap();
|
|
let font = skrifa::FontRef::new(&full).unwrap();
|
|
let mut builder = write_fonts::FontBuilder::new();
|
|
for r in font.table_directory.table_records() {
|
|
if ![*b"glyf", *b"loca"].contains(&r.tag().to_be_bytes()) {
|
|
builder.add_raw(write_fonts::types::Tag::new(&r.tag().to_be_bytes()), font.table_data(r.tag()).unwrap().as_bytes());
|
|
}
|
|
}
|
|
let dir = std::env::temp_dir().join(format!("vc-bitmaps-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let file = dir.join("Bitmaps.ttf");
|
|
std::fs::write(&file, builder.build()).unwrap();
|
|
let faces = file_face_names_within(&file, 4, u64::MAX);
|
|
assert_eq!(faces.len(), 1, "the face is cataloged");
|
|
}
|
|
|
|
/// The system's colour emoji font, when installed: an emoji draws as an image, and a ZWJ sequence
|
|
/// as one glyph.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[test]
|
|
fn an_installed_colour_emoji_font_draws_emoji_as_images() {
|
|
let db = FontDb::with_font_dirs(system_font_dirs());
|
|
let Some(family) = ["Apple Color Emoji", "Noto Color Emoji"].into_iter().find(|f| db.has_family(f)) else {
|
|
eprintln!("no colour emoji font installed: skipped");
|
|
return;
|
|
};
|
|
let st = CharStyle { font_family: family.into(), size: 40.0, ..CharStyle::default() };
|
|
let l = layout(&db, &TextObject::point(Point::ZERO, "\u{1F600}", st.clone()));
|
|
assert!(l.glyphs[0].image.is_some(), "{family} draws 😀 as an image");
|
|
let l = layout(&db, &TextObject::point(Point::ZERO, "\u{1F469}\u{200D}\u{1F4BB}", st));
|
|
assert_eq!(l.glyphs.iter().filter(|g| g.image.is_some()).count(), 1, "the sequence is one glyph");
|
|
}
|