Skip to content

Commit

Permalink
feat(pff2): Draft font renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
max-ishere committed Jan 31, 2024
1 parent 089da3a commit 0f7ced4
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 24 deletions.
168 changes: 160 additions & 8 deletions examples/pff2.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<bool> = Some(Glyph::FILLED_PIXEL);
const TRANSPARENT: Option<bool> = 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!();
}
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ pub mod parser {
pub mod theme_txt;
}

pub mod render {
pub mod pff2;
}

pub type OwnedSlice<T> = Rc<T>;
35 changes: 19 additions & 16 deletions src/parser/pff2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]>,
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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<String, FromUtf8Error> {
if input.len() == 0 {
if input.is_empty() {
return Ok(String::new());
}

Expand Down Expand Up @@ -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
Expand All @@ -241,26 +240,30 @@ 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;
}

let glyph = Glyph {
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()))
Expand Down Expand Up @@ -377,7 +380,7 @@ impl<T: FontValidation> Default for Pff2<T> {
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(),

Expand Down
67 changes: 67 additions & 0 deletions src/render/pff2.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
// 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
}
}

0 comments on commit 0f7ced4

Please sign in to comment.