diff --git a/Cargo.lock b/Cargo.lock index 288d7a0..ccce071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "memchr" version = "2.7.1" @@ -210,6 +216,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "log", "nom", "test-case", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 519f06c..aafff2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.79" clap = { version = "4.4.18", features = ["derive"] } +log = "0.4.20" nom = "7.1.3" thiserror = "1.0.56" [dev-dependencies] test-case = "3.3.1" +anyhow = "1.0.79" diff --git a/examples/pff2.rs b/examples/pff2.rs index 20ad6a4..c3c66c6 100644 --- a/examples/pff2.rs +++ b/examples/pff2.rs @@ -1,35 +1,41 @@ -//! A minimal font.pf2 parser impl that prints the parsed Rust struct +//! 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::Parser; +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(); let data = read(args.font_file)?; - let font = Parser::parse(&data)?.validate(); + let font = Parser::parse(&data)?.validate()?; + + println!("{}", font.name); - let print = format!("{font:#?}") - .split("\n") - .take(100) - .fold(String::new(), |print, line| print + line + "\n"); + let glyph = font.glyph(args.character).unwrap(); - println!("{print}"); + render_glyph(glyph); Ok(()) } + +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 b8883b8..34d82cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,9 @@ use std::rc::Rc; #[macro_use] extern crate test_case; +#[macro_use] +extern crate log; + extern crate thiserror; pub mod parser { @@ -11,4 +14,10 @@ pub mod parser { pub mod theme_txt; } +pub mod render { + pub mod pff2; +} + pub type OwnedSlice = Rc; + +trait Sealed {} diff --git a/src/parser/pff2.rs b/src/parser/pff2.rs index fd273a5..5fc8ccc 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, 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,10 +29,7 @@ pub type Font = Pff2; /// on the requiements. pub type Parser = Pff2; -/// The internal representation of the UTF code point. -type Codepoint = u32; - -/// 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)] @@ -59,24 +56,21 @@ pub struct Pff2 { /// A list of glyphs that are in the font pub glyphs: OwnedSlice<[Glyph]>, - _validation: PhantomData, + _phantom: PhantomData, } /// 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: Codepoint, + pub code: char, - // TODO: document these params - pub width: u16, - pub height: u16, - pub x_offset: u16, - pub y_offset: u16, - pub device_width: u16, + pub x_offset: isize, + pub y_offset: isize, + pub device_width: isize, /// The bitmap that represents a rendered glyph. - pub bitmap: OwnedSlice<[u8]>, + pub bitmap: Bitmap, } impl Parser { @@ -84,11 +78,9 @@ impl Parser { /// Constructs [`Self`] from a PFF2 buffer. pub fn parse(input: &[u8]) -> Result { - let input_for_data_section = input; // Save this because data offsets are absolute + let input_for_data_section = input; // Save this because SectionName::CharIndex offsets are absolute - let (magic, mut input) = input.split_at(4 + 4 + 4); - // This is technically a section, but because its always first and same content - // we just compare it in one go. + let (magic, mut input) = input.split_at(Self::MAGIC.len()); if magic != Self::MAGIC { return Err(ParserError::BadMagicBytes); } @@ -101,23 +93,24 @@ 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])?, - PointSize => font.point_size = Self::parse_u16(&input[..length])?, + PointSize => font.point_size = Self::parse_u16_be(&input[..length])?, Weight => font.weight = Self::parse_string(&input[..length])?, - MaxCharWidth => font.max_char_width = Self::parse_u16(&input[..length])?, - MaxCharHeight => font.max_char_height = Self::parse_u16(&input[..length])?, - Ascent => font.ascent = Self::parse_u16(&input[..length])?, - Descent => font.descent = Self::parse_u16(&input[..length])?, + MaxCharWidth => font.max_char_width = Self::parse_u16_be(&input[..length])?, + MaxCharHeight => font.max_char_height = Self::parse_u16_be(&input[..length])?, + Ascent => font.ascent = Self::parse_u16_be(&input[..length])?, + Descent => font.descent = Self::parse_u16_be(&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; } } @@ -133,15 +126,19 @@ impl Parser { Ok(font) } - /// Returns the section name, length as usize and the rest of the supplied input. + /// Returns the section name, length as usize and the rest of the supplied input starting at the beginning of + /// section content. fn parse_section_header(input: &[u8]) -> Result<([u8; 4], usize, &[u8]), ParserError> { let (section, input) = input.split_at(4); let section: [u8; 4] = section.try_into().map_err(|_| ParserError::InsufficientHeaderBytes)?; let (length, input) = input.split_at(4); - - let length = - u32::from_be_bytes(length.try_into().map_err(|_| ParserError::InsufficientHeaderBytes)?).to_usize(); + let length = u32::from_be_bytes( + length + .try_into() + .map_err(|_| ParserError::InsufficientLengthBytes { section })?, + ) + .to_usize(); Ok((section, length, input)) } @@ -149,7 +146,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()); } @@ -160,8 +157,8 @@ impl Parser { String::from_utf8(input[..input.len()].to_vec()) } - /// Converts the entirety of input into a u16. If the supplied slice is not 2b long, returns an error. - fn parse_u16(input: &[u8]) -> Result { + /// Converts the entirety of input into a u16. If the supplied slice is not 2 bytes long, returns an error. + fn parse_u16_be(input: &[u8]) -> Result { if input.len() != 2 { return Err(ParserError::InvalidU16Length(input.len())); } @@ -169,20 +166,108 @@ impl Parser { Ok(u16::from_be_bytes([input[0], input[1]])) } + /// 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; + + if input.len() % ENTRY_SIZE != 0 { + return Err(ParserError::EmptyCharacterIndex); + } + + 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 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" + ); + } + + // TODO: Investigate why unicode.pf2 contains invalid codepoint 0x40000626 and deal with it. + Ok::<_, ParserError>(CharIndex { + code: char::from_u32(codepoint).ok_or(ParserError::InvalidCodepoint(codepoint))?, + offset: u32::from_be_bytes([chunk[5], chunk[6], chunk[7], chunk[8]]).to_usize(), + }) + }) + .collect() + } + + /// Takes the glyph lookup section and combines it with the content of the data section to get the complete glyph + /// data. [`CharIndex`] offsets are file-global (absolute), so `input` should be the entirety of the file. + fn parse_data_section(indexes: Vec, input: &[u8]) -> Rc<[Glyph]> { + let mut glyphs = Vec::with_capacity(indexes.len()); + + for index in indexes { + let offset = index.offset; + + // make sure there are enough bytes to read the bitmap dimentions + if offset + 4 > input.len() { + warn!( + "Insufficient data to load a glyph for codepoint {}", + index.code.escape_unicode(), + ); + continue; + } + + 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 = Bitmap::byte_count_from_size(width, height); + + // make sure there are enough bytes to read the bitmap and the rest of the fields + if offset + 10 + bitmap_len > input.len() { + warn!( + "Insufficient data to load a glyph for codepoint {}", + index.code.escape_unicode() + ); + continue; + } + + let glyph = Glyph { + code: index.code, + 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 isize, + 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); + } + + glyphs.into() + } + /// 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); @@ -190,7 +275,19 @@ impl Parser { } if self.glyphs.len() == 0 { - return Err(NoGlyphs); + return Err(E::NoGlyphs); + } + + let mut last_codepoint = self.glyphs[0].code as u32; + + for Glyph { code, .. } in &self.glyphs[1..] { + let code = *code as u32; + + if code > last_codepoint { + last_codepoint = code; + } else { + return Err(E::GlyphsNotSortedAscending); + } } Ok(Font { @@ -204,71 +301,62 @@ impl Parser { descent: self.descent, leading: self.leading, glyphs: self.glyphs, - _validation: PhantomData, + _phantom: PhantomData, }) } +} - /// 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 allignment) - fn parse_char_indexes(input: &[u8]) -> Result, ParserError> { - const ALLIGNMENT: usize = 4 + 1 + 4; - - if input.len() % ALLIGNMENT != 0 { - return Err(ParserError::InvalidCharacterIndex); - } - - 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 - offset: u32::from_be_bytes([chunk[5], chunk[6], chunk[7], chunk[8]]).to_usize(), - }) - .collect()) +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() } +} - /// Takes the glyph lookup section and combines it with the content of the data section to get the complete glyph - /// data. - fn parse_data_section(indexes: Vec, input: &[u8]) -> Result, ParserError> { - let mut glyphs = Vec::with_capacity(input.len()); - - for index in indexes { - let offset = index.offset; - - // make sure there are enough bytes to read glyph data - if offset + 4 > input.len() { - continue; - } - - let width = Self::parse_u16(&input[offset..offset + 2])?; - let height = Self::parse_u16(&input[offset + 2..offset + 4])?; - - let bitmap_len = (width * height + 7) / 8; - - if offset + 12 + bitmap_len as usize > 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]), - }; +impl Glyph { + 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(), - glyphs.push(glyph); + x_offset: 0, + y_offset: 0, + device_width: Self::UNKNOWN_GLYPH_WIDTH as isize, + bitmap: Bitmap::new( + Self::UNKNOWN_GLYPH_WIDTH, + Self::UNKNOWN_GLYPH_HEIGHT, + &Self::UNKNOWN_GLYPH_BITMAP, + ) + .unwrap(), } - - Ok(Rc::from(glyphs.as_slice())) } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum SectionName { +enum Section { FontName, Family, PointSize, @@ -283,7 +371,9 @@ enum SectionName { /// An intermediate structure used for reading glyphs from a font file. This is discarded after the glyphs are read. struct CharIndex { - pub code: Codepoint, + /// The UCS-4 codepoint + pub code: char, + /// A file-level (absolute) offset to the glyph data pub offset: usize, } @@ -299,8 +389,8 @@ pub enum ParserError { InsufficientHeaderBytes, /// Expected to be able to read 4 bytes as a u32 length of the section - #[error("Insufficient section length bytes")] - InsufficientLengthBytes, + #[error("Insufficient bytes to read the length of section {section:?}")] + InsufficientLengthBytes { section: [u8; 4] }, /// String stored in a section had illegal UTF-8 bytes #[error("Invalid UTF-8 string: {0}")] @@ -312,10 +402,18 @@ pub enum ParserError { /// The size of the character index section doesnt divide evenly by the size of the individual elements #[error("Invalid data in the character index")] - InvalidCharacterIndex, + EmptyCharacterIndex, + + /// The character index section is required to contain codepoints in ascending order (`char as u32`) + #[error("Character index is not sorted in ascending order")] + CharacterIndexNotSortedAscending, + + /// The codepoint for a glyph entry was not valid UTF-8 + #[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 @@ -341,9 +439,13 @@ pub enum FontValidationError { /// Font contains no glyphs or they could not be read #[error("Font contains no glyphs")] NoGlyphs, + + /// Font must store glyphs in sorted ascending order + #[error("Glyphs are not sorted in ascending order")] + 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 = (); @@ -351,16 +453,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(()), } } @@ -377,39 +479,39 @@ 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(), - _validation: Default::default(), - } - } -} - -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(), + _phantom: Default::default(), } } } -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 {} + +#[cfg(test)] +mod tests { + #[allow(non_snake_case)] + mod Glyph { + use super::super::Glyph; + + #[test] + fn unknown_doesnt_panic() { + Glyph::unknown(); + } + } +} diff --git a/src/render/pff2.rs b/src/render/pff2.rs new file mode 100644 index 0000000..a95b0cb --- /dev/null +++ b/src/render/pff2.rs @@ -0,0 +1,174 @@ +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; + + /// Calculates the number of bytes required to + pub fn byte_count_from_size(width: usize, height: usize) -> usize { + (width * height + 7) / 8 + } + + /// Constructs self by wrapping the bitmap slice in a [`Rc`] and verifying that it's length is exactly + /// [`Self::byte_count_from_size`]. + /// + /// [`Rc`]: std::rc::Rc + pub fn new(width: usize, height: usize, bitmap: &[u8]) -> Option { + if bitmap.len() != Self::byte_count_from_size(width, height) { + 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 + } + } +}