#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub width: u32,
pub height: u32,
}
impl Cell {
pub const fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
pub const fn area(self) -> usize {
self.width as usize * self.height as usize
}
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BitOrder {
LsbFirst,
MsbFirst,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Painted {
pub source: &'static str,
pub source_cell: Option<Cell>,
pub ink: InkKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InkKind {
Bitmap,
Coverage,
Color,
}
impl Painted {
pub const fn bytes_per_pixel(self) -> usize {
match self.ink {
InkKind::Bitmap | InkKind::Coverage => 1,
InkKind::Color => 4,
}
}
pub const fn buffer_len(self, cell: Cell) -> usize {
cell.area() * self.bytes_per_pixel()
}
pub const fn is_coverage(self) -> bool {
matches!(self.ink, InkKind::Bitmap | InkKind::Coverage)
}
pub const fn is_color(self) -> bool {
matches!(self.ink, InkKind::Color)
}
const fn bitmap(source: &'static str, source_cell: Option<Cell>) -> Self {
Self { source, source_cell, ink: InkKind::Bitmap }
}
}
pub fn paint_bitmap(glyph: &GlyphBitmap, cell: Cell, out: &mut [u8]) -> bool {
if cell.is_empty() {
return false;
}
let needed = cell.area();
if out.len() < needed {
return false;
}
out[..needed].fill(0);
let (width, height) = (cell.width as i32, cell.height as i32);
let gw = glyph.width.max(1) as i32;
let gh = glyph.height.max(1) as i32;
let mut any = false;
for gy in 0..gh {
if !(0..gh).contains(&gy) {
continue;
}
let y0 = gy * height / gh;
let y1 = ((gy + 1) * height / gh).max(y0 + 1).min(height);
for gx in 0..gw {
if !glyph.bit(gx as u32, gy as u32) {
continue;
}
let x0 = gx * width / gw;
let x1 = ((gx + 1) * width / gw).max(x0 + 1).min(width);
for y in y0..y1 {
for x in x0..x1 {
out[(y * width + x) as usize] = 255;
any = true;
}
}
}
}
any
}
pub trait GlyphSource: Send + Sync {
fn glyph(&self, ch: char) -> Option<GlyphBitmap>;
fn name(&self) -> &'static str;
fn covers(&self, ch: char) -> bool {
self.glyph(ch).is_some()
}
fn paint(&self, ch: char, cell: Cell, out: &mut [u8]) -> Option<Painted> {
let glyph = self.glyph(ch)?;
let source_cell = Cell::new(glyph.width, glyph.height);
paint_bitmap(&glyph, cell, out);
Some(Painted::bitmap(self.name(), Some(source_cell)))
}
}
#[derive(Debug, Clone, Copy)]
pub struct GlyphBitmap {
pub width: u32,
pub height: u32,
pub order: BitOrder,
rows: GlyphRows,
}
#[derive(Debug, Clone, Copy)]
enum GlyphRows {
Inline8([u8; 8]),
Borrowed(&'static [u8]),
}
impl GlyphBitmap {
pub const fn inline8(rows: [u8; 8]) -> Self {
Self { width: 8, height: 8, order: BitOrder::LsbFirst, rows: GlyphRows::Inline8(rows) }
}
pub const fn borrowed(width: u32, height: u32, order: BitOrder, rows: &'static [u8]) -> Self {
Self { width, height, order, rows: GlyphRows::Borrowed(rows) }
}
pub fn bit(&self, x: u32, y: u32) -> bool {
if x >= self.width || y >= self.height {
return false;
}
let row_bytes = self.width.div_ceil(8) as usize;
let index = y as usize * row_bytes + (x / 8) as usize;
let byte = match &self.rows {
GlyphRows::Inline8(rows) => match rows.get(index) {
Some(byte) => *byte,
None => return false,
},
GlyphRows::Borrowed(rows) => match rows.get(index) {
Some(byte) => *byte,
None => return false,
},
};
let shift = match self.order {
BitOrder::LsbFirst => x % 8,
BitOrder::MsbFirst => 7 - (x % 8),
};
byte & (1u8 << shift) != 0
}
}
pub struct Font8x8Source;
impl Font8x8Source {
pub const INSTANCE: Self = Self;
}
impl GlyphSource for Font8x8Source {
fn glyph(&self, ch: char) -> Option<GlyphBitmap> {
use font8x8::{UnicodeFonts, BASIC_FONTS};
if let Some(rows) = BASIC_FONTS.get(ch) {
return Some(GlyphBitmap::inline8(rows));
}
if let Some(rows) = BASIC_FONTS.get(ch.to_ascii_uppercase()) {
return Some(GlyphBitmap::inline8(rows));
}
if let Some(rows) = BASIC_FONTS.get(ch.to_ascii_lowercase()) {
return Some(GlyphBitmap::inline8(rows));
}
None
}
fn name(&self) -> &'static str {
"font8x8"
}
}
#[cfg(feature = "fonts-emoji-color")]
pub struct ColorBitmapSource;
#[cfg(feature = "fonts-emoji-color")]
impl ColorBitmapSource {
pub const INSTANCE: Self = Self;
fn face_for(&self, ch: char) -> Option<crate::render::text::font_assets::ColorFaceBytes> {
let codepoint = ch as u32;
crate::render::text::font_assets::active_color_faces()
.iter()
.copied()
.find(|face| has_glyph(face.bytes, codepoint))
}
}
#[cfg(feature = "fonts-emoji-color")]
fn has_glyph(face: &[u8], codepoint: u32) -> bool {
ttf_parser::Face::parse(face, 0)
.ok()
.and_then(|parsed| parsed.glyph_index(char::from_u32(codepoint)?))
.is_some()
}
#[cfg(feature = "fonts-emoji-color")]
impl GlyphSource for ColorBitmapSource {
fn glyph(&self, ch: char) -> Option<GlyphBitmap> {
let _ = self.face_for(ch);
None
}
fn name(&self) -> &'static str {
"color-emoji"
}
fn covers(&self, ch: char) -> bool {
self.face_for(ch).is_some()
}
fn paint(&self, ch: char, cell: Cell, out: &mut [u8]) -> Option<Painted> {
let face_bytes = self.face_for(ch)?;
let parsed = ttf_parser::Face::parse(face_bytes.bytes, 0).ok()?;
let glyph_id = parsed.glyph_index(ch)?;
let face = crate::render::text::ColorBitmapFace::parse(face_bytes.bytes)?;
if cell.is_empty() || out.len() < cell.area() * 4 {
return None;
}
face.paint(glyph_id.0, cell, out)?;
Some(Painted {
source: face_bytes.name,
source_cell: None,
ink: InkKind::Color,
})
}
}
pub const TOFU: [u8; 8] = [
0b11111111, 0b10000001, 0b10111101, 0b10100101, 0b10111101, 0b10000001, 0b11111111, 0b00000000,
];
pub struct FontStack {
sources: &'static [&'static dyn GlyphSource],
}
impl FontStack {
pub const fn new(sources: &'static [&'static dyn GlyphSource]) -> Self {
Self { sources }
}
pub fn sources(&self) -> &'static [&'static dyn GlyphSource] {
self.sources
}
pub fn resolve(&self, ch: char) -> Option<(&'static dyn GlyphSource, GlyphBitmap)> {
for source in self.sources {
if let Some(glyph) = source.glyph(ch) {
return Some((*source, glyph));
}
}
None
}
pub fn resolve_or_tofu(&self, ch: char) -> (GlyphBitmap, Option<&'static str>) {
match self.resolve(ch) {
Some((source, glyph)) => (glyph, Some(source.name())),
None => (GlyphBitmap::inline8(TOFU), None),
}
}
}
pub fn active_stack() -> FontStack {
#[cfg(all(
feature = "fonts-emoji-color",
feature = "fonts-cjk-bitmap",
any(feature = "fonts-vector-latin", feature = "fonts-complex")
))]
static STACK: [&dyn GlyphSource; 4] = [
&ColorBitmapSource::INSTANCE,
&cjk::CjkBitmapSource::INSTANCE,
&super::raster::VectorSource::INSTANCE,
&Font8x8Source::INSTANCE,
];
#[cfg(all(
feature = "fonts-emoji-color",
feature = "fonts-cjk-bitmap",
not(any(feature = "fonts-vector-latin", feature = "fonts-complex"))
))]
static STACK: [&dyn GlyphSource; 3] =
[&ColorBitmapSource::INSTANCE, &cjk::CjkBitmapSource::INSTANCE, &Font8x8Source::INSTANCE];
#[cfg(all(
feature = "fonts-emoji-color",
not(feature = "fonts-cjk-bitmap"),
any(feature = "fonts-vector-latin", feature = "fonts-complex")
))]
static STACK: [&dyn GlyphSource; 3] = [
&ColorBitmapSource::INSTANCE,
&super::raster::VectorSource::INSTANCE,
&Font8x8Source::INSTANCE,
];
#[cfg(all(
feature = "fonts-emoji-color",
not(feature = "fonts-cjk-bitmap"),
not(any(feature = "fonts-vector-latin", feature = "fonts-complex"))
))]
static STACK: [&dyn GlyphSource; 2] = [&ColorBitmapSource::INSTANCE, &Font8x8Source::INSTANCE];
#[cfg(all(
not(feature = "fonts-emoji-color"),
feature = "fonts-cjk-bitmap",
any(feature = "fonts-vector-latin", feature = "fonts-complex")
))]
static STACK: [&dyn GlyphSource; 3] = [
&cjk::CjkBitmapSource::INSTANCE,
&super::raster::VectorSource::INSTANCE,
&Font8x8Source::INSTANCE,
];
#[cfg(all(
not(feature = "fonts-emoji-color"),
feature = "fonts-cjk-bitmap",
not(any(feature = "fonts-vector-latin", feature = "fonts-complex"))
))]
static STACK: [&dyn GlyphSource; 2] =
[&cjk::CjkBitmapSource::INSTANCE, &Font8x8Source::INSTANCE];
#[cfg(all(
not(feature = "fonts-emoji-color"),
not(feature = "fonts-cjk-bitmap"),
any(feature = "fonts-vector-latin", feature = "fonts-complex")
))]
static STACK: [&dyn GlyphSource; 2] =
[&super::raster::VectorSource::INSTANCE, &Font8x8Source::INSTANCE];
#[cfg(all(
feature = "fonts-cjk",
not(feature = "fonts-emoji-color"),
not(feature = "fonts-cjk-bitmap"),
not(any(feature = "fonts-vector-latin", feature = "fonts-complex"))
))]
static STACK: [&dyn GlyphSource; 2] =
[&super::raster::VectorSource::INSTANCE, &Font8x8Source::INSTANCE];
#[cfg(not(any(
feature = "fonts-cjk-bitmap",
feature = "fonts-vector-latin",
feature = "fonts-complex",
feature = "fonts-cjk",
feature = "fonts-emoji-color"
)))]
static STACK: [&dyn GlyphSource; 1] = [&Font8x8Source::INSTANCE];
FontStack::new(&STACK)
}
pub fn resolve(ch: char) -> (GlyphBitmap, Option<&'static str>) {
active_stack().resolve_or_tofu(ch)
}
pub fn paint_active(ch: char, cell: Cell, out: &mut [u8]) -> Option<Painted> {
let stack = active_stack();
for source in stack.sources() {
if let Some(painted) = source.paint(ch, cell, out) {
return Some(painted);
}
}
let tofu = GlyphBitmap::inline8(TOFU);
paint_bitmap(&tofu, cell, out);
Some(Painted::bitmap("tofu", Some(Cell::new(8, 8))))
}
pub fn source_for(ch: char) -> Option<&'static str> {
active_stack().resolve(ch).map(|(source, _)| source.name())
}
#[cfg(feature = "fonts-cjk-bitmap")]
pub mod cjk {
use super::{BitOrder, GlyphBitmap, GlyphSource};
const GLYPH_BYTES: usize = 32;
pub struct CjkBitmapSource;
impl CjkBitmapSource {
pub const INSTANCE: Self = Self;
}
impl GlyphSource for CjkBitmapSource {
fn glyph(&self, ch: char) -> Option<GlyphBitmap> {
let codepoint = ch as u32;
let index =
crate::render::text::cjk_bitmap_data::CODEPOINTS.binary_search(&codepoint).ok()?;
let start = index.checked_mul(GLYPH_BYTES)?;
let end = start.checked_add(GLYPH_BYTES)?;
let rows = crate::render::text::cjk_bitmap_data::ROWS.get(start..end)?;
Some(GlyphBitmap::borrowed(16, 16, BitOrder::MsbFirst, rows))
}
fn name(&self) -> &'static str {
"cjk-bitmap"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_ascii_character_resolves_through_the_8x8_face() {
let (glyph, source) = resolve('A');
assert_eq!(source, Some("font8x8"), "Latin is the base face's job");
assert_eq!((glyph.width, glyph.height), (8, 8));
assert!(!glyph.bit(0, 0), "the 'A' cell's corner is empty");
}
#[test]
fn whitespace_and_missing_characters_still_resolve_to_something_drawable() {
let (glyph, source) = resolve('\u{10FFFF}');
assert_eq!(source, None, "a noncharacter has no glyph in any face");
assert_eq!((glyph.width, glyph.height), (8, 8), "the tofu cell is 8x8");
}
#[test]
fn a_source_that_does_not_cover_a_character_returns_none() {
assert!(Font8x8Source::INSTANCE.glyph('中').is_none());
assert!(!Font8x8Source::INSTANCE.covers('中'));
assert!(Font8x8Source::INSTANCE.covers('A'));
}
#[test]
fn the_stack_prefers_its_first_covering_source() {
struct Only(char);
impl GlyphSource for Only {
fn glyph(&self, ch: char) -> Option<GlyphBitmap> {
(ch == self.0).then(|| GlyphBitmap::inline8([0xFF; 8]))
}
fn name(&self) -> &'static str {
"only"
}
}
static FIRST: Only = Only('x');
static SECOND: Only = Only('y');
static SOURCES: [&dyn GlyphSource; 2] = [&FIRST, &SECOND];
let stack = FontStack::new(&SOURCES);
assert_eq!(stack.resolve('x').map(|(s, _)| s.name()), Some("only"));
assert_eq!(stack.resolve('y').map(|(s, _)| s.name()), Some("only"));
assert!(stack.resolve('z').is_none());
}
#[test]
fn bit_order_reads_both_conventions_correctly() {
let lsb = GlyphBitmap::inline8([0b0000_0001, 0, 0, 0, 0, 0, 0, 0]);
let msb = GlyphBitmap::borrowed(8, 1, BitOrder::MsbFirst, &[0b1000_0000]);
assert!(lsb.bit(0, 0) && !lsb.bit(7, 0), "LSB-first: bit 0 is leftmost");
assert!(msb.bit(0, 0) && !msb.bit(7, 0), "MSB-first: the high bit is leftmost");
}
#[test]
fn out_of_bounds_reads_are_blank_rather_than_a_panic() {
let glyph = GlyphBitmap::inline8([0xFF; 8]);
assert!(!glyph.bit(8, 0), "past the right edge");
assert!(!glyph.bit(0, 8), "past the bottom edge");
let short = GlyphBitmap::borrowed(16, 16, BitOrder::MsbFirst, &[0xFF, 0xFF]);
assert!(short.bit(0, 0));
assert!(!short.bit(0, 5), "beyond the supplied rows");
}
#[cfg(feature = "fonts-cjk-bitmap")]
#[test]
fn the_cjk_face_covers_an_ideograph_and_the_stack_finds_it() {
assert_eq!(source_for('中'), Some("cjk-bitmap"), "the CJK face must answer for CJK");
assert_eq!(source_for('A'), Some("font8x8"), "and must not shadow Latin");
let (glyph, _) = resolve('中');
assert_eq!((glyph.width, glyph.height), (16, 16), "the CJK cell is 16x16");
let ink: [u16; 16] = core::array::from_fn(|y| {
(0..16u32)
.fold(0u16, |acc, x| if glyph.bit(x, y as u32) { acc | (1 << x) } else { acc })
});
assert_eq!(ink[0], 1 << 7, "the vertical stroke's tip");
assert_eq!(ink[4], 0x1FFC, "the top bar of the boxed radical");
assert_eq!(ink[15], 1 << 7, "and the stroke continues to the last row");
}
#[cfg(not(any(
feature = "fonts-vector-latin",
feature = "fonts-complex",
feature = "fonts-cjk"
)))]
#[test]
fn the_bitmap_view_and_the_painted_coverage_agree() {
for ch in ['A', 'g', '5', '\u{4e2d}', '\u{10ffff}'] {
for cell in [Cell::new(8, 8), Cell::new(5, 13), Cell::new(19, 23), Cell::new(64, 64)] {
let mut coverage = vec![0u8; cell.area()];
paint_active(ch, cell, &mut coverage);
let mut expected = vec![0u8; cell.area()];
if !ch.is_whitespace() {
for (x0, y0, x1, y1) in
crate::render::glyph_rects(ch, 0, 0, cell.width, cell.height)
{
for y in y0..y1 {
for x in x0..x1 {
expected[(y * cell.width as i32 + x) as usize] = 255;
}
}
}
}
assert_eq!(
coverage, expected,
"U+{:04X} in {}x{}: painted coverage and the bitmap view must be one picture",
ch as u32, cell.width, cell.height
);
}
}
}
#[test]
fn a_short_buffer_is_refused_rather_than_written_past() {
let cell = Cell::new(8, 8);
let mut short = [0u8; 63];
assert!(!paint_bitmap(&GlyphBitmap::inline8([0xFF; 8]), cell, &mut short));
assert!(short.iter().all(|byte| *byte == 0), "nothing may be written");
}
}