From 0f7ced488a7cb49b51fd70d3e27bd5ca1f2deaf4 Mon Sep 17 00:00:00 2001 From: max-ishere <47008271+max-ishere@users.noreply.github.com> Date: Wed, 31 Jan 2024 23:03:15 +0200 Subject: [PATCH] feat(pff2): Draft font renderer --- examples/pff2.rs | 168 ++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 4 ++ src/parser/pff2.rs | 35 +++++----- src/render/pff2.rs | 67 ++++++++++++++++++ 4 files changed, 250 insertions(+), 24 deletions(-) create mode 100644 src/render/pff2.rs diff --git a/examples/pff2.rs b/examples/pff2.rs index 20ad6a4..6c7d3a5 100644 --- a/examples/pff2.rs +++ b/examples/pff2.rs @@ -1,10 +1,11 @@ //! A minimal font.pf2 parser impl that prints the parsed Rust struct +#![feature(iter_array_chunks)] use std::fs::read; use args::Args; use clap::Parser as _; -use theme_parser::parser::pff2::Parser; +use theme_parser::parser::pff2::{Glyph, Parser}; mod args { use std::path::PathBuf; @@ -22,14 +23,165 @@ fn main() -> anyhow::Result<()> { let args = Args::parse(); let data = read(args.font_file)?; - let font = Parser::parse(&data)?.validate(); + let font = Parser::parse(&data)?.validate()?; - let print = format!("{font:#?}") - .split("\n") - .take(100) - .fold(String::new(), |print, line| print + line + "\n"); - - println!("{print}"); + println!("{}", font.name); + render_glyphs(&font.glyphs, font.ascent, font.descent); Ok(()) } + +fn render_glyphs(glyphs: &[Glyph], ascent: u16, descent: u16) { + for glyph in glyphs { + render_glyph(glyph); + print_glyph(glyph, ascent, descent); + } +} + +fn render_glyph(glyph: &Glyph) { + if glyph.height == 0 || glyph.width == 0 { + println!( + r" 0 {:8x} {:8} +0 | + 0x0 + +", + glyph.code, + char::from_u32(glyph.code) + .map(|c| format!(r#""{}""#, c)) + .unwrap_or(r#""????""#.to_string()), + ); + + return; + } + + let w_range = 0..glyph.width; + let h_range = 0..glyph.height + 2; // to render the last row + + let h_range = h_range.array_chunks(); + + const FILLED: Option = Some(Glyph::FILLED_PIXEL); + const TRANSPARENT: Option = Some(Glyph::TRANSPARENT_PIXEL); + + println!( + " {} {:8x} {:8}", + (0..glyph.width).fold(String::new(), |acc, i| format!("{acc}{}", i % 8)), + glyph.code, + char::from_u32(glyph.code) + .map(|c| format!(r#""{}""#, c)) + .unwrap_or(r#""????""#.to_string()), + ); + + let mut bytes = glyph.bitmap.iter().enumerate(); + + for [y0, y1] in h_range { + if y0 >= glyph.height { + break; + } + print!("{} ", y0 % 10); + + for x in w_range.clone() { + let upper_pixel = glyph.pixel(x, y0); + let lower_pixel = glyph.pixel(x, y1); + + let rendered = match (upper_pixel, lower_pixel) { + (FILLED, FILLED) => '\u{2588}', // █ + (FILLED, TRANSPARENT) | (FILLED, None) => '\u{2580}', // ▀ + (TRANSPARENT, FILLED) => '\u{2584}', // ▄ + (TRANSPARENT, TRANSPARENT) | (TRANSPARENT, None) => '—', + (None, _) => { + break; + } + }; + + print!("{rendered}"); + } + + let should_be_at = ((glyph.width * (y1 + 1)) as f32 / 8.).ceil() as usize; + + loop { + let Some((i, byte)) = bytes.next() else { + break; + }; + print!(" | {:08b}", byte); + if i + 1 >= should_be_at && y1 + 1 < glyph.height { + break; + } + } + + println!(); + } + + println!( + " {}x{} {}B {}dx {}dy\n\n", + glyph.width, + glyph.height, + glyph.bitmap.len(), + glyph.x_offset, + glyph.y_offset + ); +} + +fn print_glyph(glyph: &Glyph, ascent: u16, descent: u16) { + println!( + "{c:2} U+{u:04x} | {w:2}w {h:2}h | {dx:3}dx {dy:3}dy | {W:2}W", + c = char::from_u32(glyph.code).unwrap_or('?'), + u = glyph.code, + w = glyph.width, + h = glyph.height, + dx = glyph.x_offset, + dy = glyph.y_offset, + W = glyph.device_width, + ); + + let mut xmax = glyph.x_offset + glyph.width as isize; + if xmax < glyph.device_width as isize { + xmax = glyph.device_width as isize; + } + let xmax = xmax; + + let mut xmin = glyph.x_offset; + if xmin > 0 { + xmin = 0 + } + let xmin = xmin; + + let mut ymax = glyph.y_offset + glyph.height as isize; + if ymax < ascent as isize { + ymax = ascent as isize; + } + let ymax = ymax; + + let mut ymin = glyph.y_offset; + if ymin > -(descent as isize) { + ymin = -(descent as isize); + } + + let mut bitmap = glyph.bitmap.iter(); + let mut byte = bitmap.next().unwrap_or(&0); + let mut mask = 0x80; + + // dbg!(xmin, xmax, ymin, ymax, glyph.x_offset, glyph.width, glyph.device_width); + + for y in (ymin..ymax).rev() { + for x in xmin..xmax { + if x >= glyph.x_offset + && x < glyph.x_offset + glyph.width as isize + && y >= glyph.y_offset + && y < glyph.y_offset + glyph.height as isize + { + print!("{}", if byte & mask != 0 { "%" } else { " " }); + mask >>= 1; + if mask == 0 { + mask = 0x80; + byte = bitmap.next().unwrap_or(&0); + } + } else if x >= 0 && x < glyph.device_width as isize && y >= -(descent as isize) && y < ascent as isize { + print!("{}", if x == 0 || y == 0 { "+" } else { "." }); + } else { + print!("*"); + } + } + println!(); + } +} diff --git a/src/lib.rs b/src/lib.rs index b8883b8..ab72e6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,4 +11,8 @@ pub mod parser { pub mod theme_txt; } +pub mod render { + pub mod pff2; +} + pub type OwnedSlice = Rc; diff --git a/src/parser/pff2.rs b/src/parser/pff2.rs index fd273a5..a991af1 100644 --- a/src/parser/pff2.rs +++ b/src/parser/pff2.rs @@ -69,11 +69,11 @@ pub struct Glyph { pub code: Codepoint, // TODO: document these params - pub width: u16, - pub height: u16, - pub x_offset: u16, - pub y_offset: u16, - pub device_width: u16, + pub width: usize, + pub height: usize, + pub x_offset: isize, + pub y_offset: isize, + pub device_width: i16, /// The bitmap that represents a rendered glyph. pub bitmap: OwnedSlice<[u8]>, @@ -117,7 +117,7 @@ impl Parser { Descent => font.descent = Self::parse_u16(&input[..length])?, CharIndex => char_indexes = Self::parse_char_indexes(&input[..length])?, Data => { - font.glyphs = Self::parse_data_section(char_indexes, &input_for_data_section)?; + font.glyphs = Self::parse_data_section(char_indexes, input_for_data_section)?; break 'parsing; } } @@ -149,7 +149,7 @@ impl Parser { /// Converts the entirety of input into a UTF-8 string. If the string is `\0` terminated, removes the `\0` before /// conversion. fn parse_string(input: &[u8]) -> Result { - if input.len() == 0 { + if input.is_empty() { return Ok(String::new()); } @@ -219,7 +219,6 @@ impl Parser { Ok(input .chunks(ALLIGNMENT) - .into_iter() .map(|chunk| CharIndex { code: u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]), // skipp [4], it's a `storage_flags`, and GRUB never uses that field anyway @@ -241,12 +240,12 @@ impl Parser { continue; } - let width = Self::parse_u16(&input[offset..offset + 2])?; - let height = Self::parse_u16(&input[offset + 2..offset + 4])?; + let width = u16::from_be_bytes([input[offset], input[offset + 1]]).to_usize(); + let height = u16::from_be_bytes([input[offset + 2], input[offset + 3]]).to_usize(); let bitmap_len = (width * height + 7) / 8; - if offset + 12 + bitmap_len as usize > input.len() { + if offset + 12 + bitmap_len > input.len() { continue; } @@ -254,13 +253,17 @@ impl Parser { code: index.code, width, height, - x_offset: Self::parse_u16(&input[offset + 6..offset + 8])?, - y_offset: Self::parse_u16(&input[offset + 8..offset + 10])?, - device_width: Self::parse_u16(&input[offset + 10..offset + 12])?, - bitmap: Rc::from(&input[offset + 12..offset + 12 + bitmap_len as usize]), + x_offset: i16::from_be_bytes([input[offset + 4], input[offset + 5]]) as isize, + y_offset: i16::from_be_bytes([input[offset + 6], input[offset + 7]]) as isize, + device_width: u16::from_be_bytes([input[offset + 8], input[offset + 9]]) as i16, + bitmap: Rc::from(&input[(offset + 10)..(offset + 10 + bitmap_len)]), }; glyphs.push(glyph); + + if index.code == 0x21 { + // dbg!([input[offset + 10], input[offset + 11]]); + } } Ok(Rc::from(glyphs.as_slice())) @@ -377,7 +380,7 @@ impl Default for Pff2 { max_char_height: Default::default(), ascent: Default::default(), descent: Default::default(), - leading: Default::default(), + leading: 1, // PFF2 files dont have a leading section. This is the default value for all PFF2 fonts glyphs: [].into(), diff --git a/src/render/pff2.rs b/src/render/pff2.rs new file mode 100644 index 0000000..78ea84f --- /dev/null +++ b/src/render/pff2.rs @@ -0,0 +1,67 @@ +//! PFF2 font rendering functionality + +use crate::parser::pff2::{Font, Glyph}; + +impl Font { + // TODO: make an abstraction over the bitmap + + pub fn render(&self, s: &str) -> () {} +} + +impl Glyph { + pub const FILLED_PIXEL: bool = true; + pub const TRANSPARENT_PIXEL: bool = false; + + pub fn pixel(&self, x: usize, y: usize) -> Option { + // dbg!(self.x_offset, self.y_offset); + + // let x = isize::try_from(x).ok()?; + // let x = x.checked_add(self.x_offset)?; + // let x = usize::try_from(x).ok()?; + + // let y = isize::try_from(y).ok()?; + // let y = y.checked_sub(self.y_offset)?; + // let y = usize::try_from(y).ok()?; + + if !(x < self.width && y < self.height) { + return None; + } + + let index = y * self.width + x; + let byte_index = index / 8; + let bit_index = 7 - (index % 8); + let mask =1 << bit_index; + + Some((self.bitmap[byte_index] & mask) != 0) + } +} + +#[cfg(test)] +mod tests { + use crate::parser::pff2::Glyph; + + #[test_case( + 8, 1, &[0b00000001], + 7, 0 => 1; + "right bit one line" + )] + #[test_case( + 8, 1, &[0b10000000], + 0, 0 => 1; + "left bit one line" + )] + fn foo(width: usize, height: usize, bitmap: &[u8], x: usize, y: usize) -> u8 { + let glyph = Glyph { + width, + height, + bitmap: bitmap.into(), + ..Default::default() + }; + + for (i, x) in (0..width).enumerate() { + //println!("{i}: {:?}", glyph.pixel(x, 0)); + } + + glyph.pixel(x, y).unwrap() as u8 + } +}