diff --git a/.github/workflows/cargo-doc-pages.yml b/.github/workflows/cargo-doc-pages.yml new file mode 100644 index 0000000..208db1d --- /dev/null +++ b/.github/workflows/cargo-doc-pages.yml @@ -0,0 +1,33 @@ +name: Deploy `cargo doc` to GitHub Pages + +on: + push: + branches: + - main + +jobs: + deploy-doc: + runs-on: ubuntu-latest + + environment: + name: github-pages + url: ${{steps.deployment.outputs.page_url}} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Run `cargo doc` + run: cargo doc --lib --no-deps + + - name: Upload Artifact + uses: actions/upload-pages-artifact@v2 + with: + path: 'target/doc' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/examples/pff2.rs b/examples/pff2.rs index 0aed5fb..c3c66c6 100644 --- a/examples/pff2.rs +++ b/examples/pff2.rs @@ -1,24 +1,21 @@ -//! A minimal font.pf2 parser impl that prints the parsed Rust struct -#![feature(iter_array_chunks)] +//! A minimal font.pf2 parser impl that prints a glyph from a PFF2 font -use std::fs::read; +use std::{fs::read, path::PathBuf}; -use args::Args; use clap::Parser as _; use theme_parser::parser::pff2::{Glyph, Parser}; -mod args { - use std::path::PathBuf; +#[derive(clap::Parser)] +struct Args { + #[clap(long, short = 'f')] + pub font_file: PathBuf, - use clap::Parser; - - #[derive(Parser)] - pub struct Args { - #[clap(long, short = 'f')] - pub font_file: PathBuf, - } + #[clap(long = "char", short)] + pub character: char, } +const SQUARE_BLOCK: &str = "\u{2588}\u{2588}"; + fn main() -> anyhow::Result<()> { let args = Args::parse(); @@ -26,157 +23,18 @@ fn main() -> anyhow::Result<()> { let font = Parser::parse(&data)?.validate()?; 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 {:-8} {:-8} -0 | - 0x0 - -", - glyph.code, - glyph.code.escape_unicode(), - ); - - 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!( - " {} {:-8} | {:-8}", - (0..glyph.width).fold(String::new(), |acc, i| format!("{acc}{}", i % 8)), - glyph.code.escape_unicode(), - glyph.code, - ); - - 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 glyph = font.glyph(args.character).unwrap(); - 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; - } - }; + render_glyph(glyph); - 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 - ); + Ok(()) } -fn print_glyph(glyph: &Glyph, ascent: u16, descent: u16) { - println!( - "{c:2} {u} | {w:2}w {h:2}h | {dx:3}dx {dy:3}dy | {W:2}W", - c = glyph.code, - u = glyph.code.escape_unicode(), - 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!("*"); - } +fn render_glyph(glyph: &Glyph) { + for row in &glyph.bitmap { + for col in row { + print!("{}", if col { SQUARE_BLOCK } else { " " }); } println!(); } diff --git a/src/lib.rs b/src/lib.rs index 9b5d2e3..34d82cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,5 @@ pub mod render { } pub type OwnedSlice = Rc; + +trait Sealed {} diff --git a/src/parser/pff2.rs b/src/parser/pff2.rs index ee00897..7b755c1 100644 --- a/src/parser/pff2.rs +++ b/src/parser/pff2.rs @@ -18,7 +18,7 @@ use std::{marker::PhantomData, rc::Rc, string::FromUtf8Error}; use nom::{InputLength, ToUsize}; use thiserror::Error; -use crate::OwnedSlice; +use crate::{render::pff2::bitmap::Bitmap, OwnedSlice, Sealed}; /// A font object that is supposed to be validated after the file was read. Instead of constructing this youself, make a /// `Parser` and call validate on it. @@ -29,7 +29,7 @@ pub type Font = Pff2; /// on the requiements. pub type Parser = Pff2; -/// The PFF2 font. +/// The PFF2 font, see [`Parser`] and [`Font`] type aliases. /// /// Only contains relevant to GRUB metadata about the font as well as the glyph list. #[allow(private_bounds)] @@ -60,20 +60,17 @@ pub struct Pff2 { } /// A single glyph inside the font. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Glyph { /// The UTF codepoint of the character pub code: char, - // TODO: document these params - 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]>, + pub bitmap: Bitmap, } impl Parser { @@ -96,12 +93,12 @@ impl Parser { input = 'input: { let (section, length, input) = Self::parse_section_header(input)?; - let Ok(section) = SectionName::try_from(section) else { + let Ok(section) = Section::try_from(section) else { warn!("Skipping section {section:?} because it is not supported"); break 'input &input[length..]; }; - use SectionName::*; + use Section::*; match section { FontName => font.name = Self::parse_string(&input[..length])?, Family => font.family = Self::parse_string(&input[..length])?, @@ -169,8 +166,10 @@ impl Parser { Ok(u16::from_be_bytes([input[0], input[1]])) } - /// Parses the `CHIX` section and returns the glyph lookup table. Errors out if the character index is not alligned - /// properly for reading (length doesnt divide perfectly by size of an entry) + /// Parses the [`Section::CharIndex`] section and returns the glyph lookup table. Requires that: + /// - The input length is divisible by the size of an entry + /// - The codepoints are valid unicode + /// - The codepoints are stored in ascending order (`char as u32`) fn parse_char_indexes(input: &[u8]) -> Result, ParserError> { const ENTRY_SIZE: usize = 4 + 1 + 4; @@ -178,23 +177,30 @@ impl Parser { return Err(ParserError::EmptyCharacterIndex); } - let mut last_codepoint = 0; + let mut last_codepoint = None; input .chunks(ENTRY_SIZE) .map(|chunk| { let codepoint = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - if codepoint > last_codepoint { - last_codepoint = codepoint; + if last_codepoint.is_none() || codepoint > last_codepoint.unwrap() { + last_codepoint = Some(codepoint); } else { return Err(ParserError::CharacterIndexNotSortedAscending); } + let storage_flags = chunk[4]; + const DEFAULT_STORAGE_FLAGS: u8 = 0; + if storage_flags != DEFAULT_STORAGE_FLAGS { + warn!( + "Codepoint {codepoint:x} has non-default storage flags: 0b{storage_flags:b}, the encoded \ + value may not be decoded correctly" + ); + } + Ok::<_, ParserError>(CharIndex { code: char::from_u32(codepoint).ok_or(ParserError::InvalidCodepoint(codepoint))?, - //code: u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]), - // skip [4], it's a `storage_flags`, and GRUB never uses that field anyway offset: u32::from_be_bytes([chunk[5], chunk[6], chunk[7], chunk[8]]).to_usize(), }) }) @@ -234,12 +240,11 @@ impl Parser { let glyph = Glyph { code: index.code, - width, - height, 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)]), + bitmap: Bitmap::new(width, height, &input[(offset + 10)..(offset + 10 + bitmap_len)]) + .expect("input slice should be long enough to represent the bitmap data"), }; glyphs.push(glyph); @@ -251,17 +256,17 @@ impl Parser { /// Validates [`Self`] to be a valid font that can be used for rendering. See [`FontValidationError`] for reasons a /// font may be invalid. pub fn validate(self) -> Result { - use FontValidationError::*; + use FontValidationError as E; if self.name.is_empty() { - return Err(EmptyName); + return Err(E::EmptyName); } for (prop, err) in [ - (self.max_char_width, ZeroMaxCharWidth), - (self.max_char_height, ZeroMaxCharHeight), - (self.ascent, ZeroAscent), - (self.descent, ZeroDescent), + (self.max_char_width, E::ZeroMaxCharWidth), + (self.max_char_height, E::ZeroMaxCharHeight), + (self.ascent, E::ZeroAscent), + (self.descent, E::ZeroDescent), ] { if prop == 0 { return Err(err); @@ -269,7 +274,7 @@ impl Parser { } if self.glyphs.len() == 0 { - return Err(NoGlyphs); + return Err(E::NoGlyphs); } let mut last_codepoint = self.glyphs[0].code as u32; @@ -280,7 +285,7 @@ impl Parser { if code > last_codepoint { last_codepoint = code; } else { - return Err(GlyphsNotSortedAscending); + return Err(E::GlyphsNotSortedAscending); } } @@ -300,8 +305,17 @@ impl Parser { } } +impl Pff2 { + pub fn glyph(&self, c: char) -> Option<&Glyph> { + self.glyphs + .binary_search_by(|g| (g.code as u32).cmp(&(c as u32))) + .map(|i| &self.glyphs[i]) + .ok() + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum SectionName { +enum Section { FontName, Family, PointSize, @@ -354,11 +368,11 @@ pub enum ParserError { CharacterIndexNotSortedAscending, /// The codepoint for a glyph entry was not valid UTF-8 - #[error("Invalid unicode codepoint encountered: {0}")] + #[error("Invalid unicode codepoint encountered: 0x{0:x}")] InvalidCodepoint(u32), } -/// Convertion from [`Parser`] into [`Font`] failed +/// Convertion from [`Parser`] into [`Font`] failed, meaning some data is invalid. #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum FontValidationError { /// Font has no name @@ -390,7 +404,7 @@ pub enum FontValidationError { GlyphsNotSortedAscending, } -impl TryFrom<[u8; 4]> for SectionName { +impl TryFrom<[u8; 4]> for Section { /// Unknown section names are usually ignored so no point returning them to the caller. type Error = (); @@ -398,16 +412,16 @@ impl TryFrom<[u8; 4]> for SectionName { /// The [`Err(())`] indicates that this section name is unknown. fn try_from(bytes: [u8; 4]) -> Result { match bytes.as_ref() { - b"NAME" => Ok(SectionName::FontName), - b"FAMI" => Ok(SectionName::Family), - b"PTSZ" => Ok(SectionName::PointSize), - b"WEIG" => Ok(SectionName::Weight), - b"MAXW" => Ok(SectionName::MaxCharWidth), - b"MAXH" => Ok(SectionName::MaxCharHeight), - b"ASCE" => Ok(SectionName::Ascent), - b"DESC" => Ok(SectionName::Descent), - b"CHIX" => Ok(SectionName::CharIndex), - b"DATA" => Ok(SectionName::Data), + b"NAME" => Ok(Section::FontName), + b"FAMI" => Ok(Section::Family), + b"PTSZ" => Ok(Section::PointSize), + b"WEIG" => Ok(Section::Weight), + b"MAXW" => Ok(Section::MaxCharWidth), + b"MAXH" => Ok(Section::MaxCharHeight), + b"ASCE" => Ok(Section::Ascent), + b"DESC" => Ok(Section::Descent), + b"CHIX" => Ok(Section::CharIndex), + b"DATA" => Ok(Section::Data), _ => Err(()), } } @@ -433,30 +447,17 @@ impl Default for Pff2 { } } -impl Default for Glyph { - fn default() -> Self { - Self { - code: Default::default(), - - width: Default::default(), - height: Default::default(), - x_offset: Default::default(), - y_offset: Default::default(), - device_width: Default::default(), - - bitmap: [].into(), - } - } -} - -trait FontValidation: Clone + PartialEq + Eq + Debug {} +#[allow(private_bounds)] +pub trait FontValidation: Sealed + Clone + PartialEq + Eq + Debug {} #[doc(hidden)] #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Validated; impl FontValidation for Validated {} +impl Sealed for Validated {} #[doc(hidden)] #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Unchecked; impl FontValidation for Unchecked {} +impl Sealed for Unchecked {} diff --git a/src/render/pff2.rs b/src/render/pff2.rs index 7e89644..ce58f3c 100644 --- a/src/render/pff2.rs +++ b/src/render/pff2.rs @@ -1,28 +1,47 @@ //! PFF2 font rendering functionality -use crate::parser::pff2::{Font, Glyph}; +use self::bitmap::Bitmap; +use crate::parser::pff2::Glyph; -impl Font { - // TODO: make an abstraction over the bitmap - - pub fn render(&self, s: &str) -> () {} -} +pub mod bitmap; impl Glyph { - pub const FILLED_PIXEL: bool = true; - pub const TRANSPARENT_PIXEL: bool = false; - - pub fn pixel(&self, x: usize, y: usize) -> Option { - if !(x < self.width && y < self.height) { - return None; + const UNKNOWN_GLYPH_BITMAP: [u8; 16] = [ + 0b01111100, // + 0b10000010, // + 0b10111010, // + 0b10101010, // + 0b10101010, // + 0b10001010, // + 0b10011010, // + 0b10010010, // + 0b10010010, // + 0b10010010, // + 0b10010010, // + 0b10000010, // + 0b10010010, // + 0b10000010, // + 0b01111100, // + 0b00000000, // + ]; + const UNKNOWN_GLYPH_HEIGHT: usize = 16; + const UNKNOWN_GLYPH_WIDTH: usize = 8; + + /// Creates a [`Glyph`] that can be used to represent a character that was not found in a font during rendering. + pub fn unknown() -> Self { + Self { + code: Default::default(), + + x_offset: 0, + y_offset: 0, + device_width: Self::UNKNOWN_GLYPH_WIDTH as i16, + bitmap: Bitmap::new( + Self::UNKNOWN_GLYPH_WIDTH, + Self::UNKNOWN_GLYPH_HEIGHT, + &Self::UNKNOWN_GLYPH_BITMAP, + ) + .unwrap(), } - - 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) } } @@ -30,27 +49,11 @@ impl Glyph { mod tests { #[allow(non_snake_case)] mod Glyph { - 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 pixel(width: usize, height: usize, bitmap: &[u8], x: usize, y: usize) -> u8 { - let glyph = Glyph { - width, - height, - bitmap: bitmap.into(), - ..Default::default() - }; + use super::super::Glyph; - glyph.pixel(x, y).unwrap() as u8 + #[test] + fn unknown_doesnt_panic() { + Glyph::unknown(); } } } diff --git a/src/render/pff2/bitmap.rs b/src/render/pff2/bitmap.rs new file mode 100644 index 0000000..15bcb2f --- /dev/null +++ b/src/render/pff2/bitmap.rs @@ -0,0 +1,169 @@ +use crate::OwnedSlice; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bitmap { + width: usize, + height: usize, + bitmap: OwnedSlice<[u8]>, +} + +impl Default for Bitmap { + fn default() -> Self { + Self { + width: 0, + height: 0, + bitmap: [].into(), + } + } +} + +impl Bitmap { + /// What the PFF2 format considers as filled. The users of this library should always recieve `true` as filled, + /// regardless of the changes in the format. + const FILLED_PIXEL: bool = true; + /// What the PFF2 format considers as transparent. The users of this library should always recieve `false` as + /// trasnparent, regardless of the changes in the format. + #[allow(unused)] + const TRANSPARENT_PIXEL: bool = !Self::FILLED_PIXEL; + + /// Constructs self by wrapping the bitmap slice in a [`Rc`] and verifying that it's length is sufficient to + /// represent a bitmap with the `width * height` dimentions. + /// + /// [`Rc`]: std::rc::Rc + pub fn new(width: usize, height: usize, bitmap: &[u8]) -> Option { + if bitmap.len() < (width * height + 7) / 8 { + return None; + } + + Some(Self { + width, + height, + bitmap: bitmap.into(), + }) + } + + /// Returns [`Some(true)`] if the pixel is filled, [`Some(false)`] if the pixel if transparent, [`None`] if out of + /// bounds. + pub fn pixel(&self, x: usize, y: usize) -> Option { + 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 bit = (self.bitmap[byte_index] >> bit_index & 1) != 0; + + Some(bit == Self::FILLED_PIXEL) + } + + /// Bitmap width (line length) + pub fn width(&self) -> usize { + self.width + } + + /// Bitmap height (number of lines) + pub fn height(&self) -> usize { + self.height + } + + pub fn iter(&self) -> BitmapIter { + self.clone().into_iter() + } +} + +impl IntoIterator for Bitmap { + type IntoIter = BitmapIter; + type Item = BitmapRowIter; + + fn into_iter(self) -> Self::IntoIter { + BitmapIter { bitmap: self, row: 0 } + } +} + +impl IntoIterator for &Bitmap { + type IntoIter = BitmapIter; + type Item = BitmapRowIter; + + fn into_iter(self) -> Self::IntoIter { + BitmapIter { + bitmap: self.clone(), + row: 0, + } + } +} + +pub struct BitmapIter { + bitmap: Bitmap, + row: usize, +} + +impl Iterator for BitmapIter { + type Item = BitmapRowIter; + + /// Yields rows of the bitmap + fn next(&mut self) -> Option { + if self.row >= self.bitmap.height { + return None; + } + + let row_iter = BitmapRowIter { + bitmap: self.bitmap.clone(), + row: self.row, + col: 0, + }; + self.row += 1; + + Some(row_iter) + } +} + +pub struct BitmapRowIter { + bitmap: Bitmap, + row: usize, + col: usize, +} + +impl Iterator for BitmapRowIter { + type Item = bool; + + /// Yields an element in the bitmap's row + fn next(&mut self) -> Option { + if self.col >= self.bitmap.width { + return None; + } + + let pixel = self.bitmap.pixel(self.col, self.row); + self.col += 1; + + pixel + } +} + +#[cfg(test)] +mod tests { + #[allow(non_snake_case)] + mod Bitmap { + use super::super::Bitmap; + + #[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 pixel(width: usize, height: usize, bitmap: &[u8], x: usize, y: usize) -> u8 { + let glyph = Bitmap { + width, + height, + bitmap: bitmap.into(), + }; + + glyph.pixel(x, y).unwrap() as u8 + } + } +}