Writing to the Bitfenix ICON display with Rust (part 2, writing text)
May 23 2021 ยท Random
Bitmap Fonts
Picking up from being able to successfully write images to the Bitfenix ICON display, I started looking into how to render textual content. Despite their lack of versatility, using a bitmap font was a natural option: they’re easy to work with, they’re a good fit for fixed-resolution displays, and rendering is fast.
I pulled up the following bitmap font from an old project (I don’t remember the source used to create this):
A few points about this font:
- Each character glyph is 16×16, with a total of 256 characters in the 256×256 bitmap
- Characters are arranged/indexed left-to-right, top-to-bottom, and the index of a 16×16 block will correspond with an ASCII or UTF8 codepoint value (e.g. the character at index 33 = “!”, which also maps to UTF8 codepoint 33)
- Despite space for 256 characters, there’s a very limited set of characters here, but there’s enough for simple US English strings
- While each character is 16×16 pixels, this is not a monospaced font, there is data for accompanying widths for each character
- The glyphs are simply black and white (i.e. there’s no antialiasing on the character glyphs, we can simply ignore black pixels and not worry about blending into the background)
Rendering Strings
We need to render characters from the bitmap font onto something. We could create a new image but, building upon what was done in part 1, I decided to render atop this background image. The composite of the background image + characters will be the image written to the ICON display.
To start, we’ll load the background image just as we did in part 1, but we need to make it mutable, as we’ll be writing character pixels direct to it:
// Background image needs to be 240x320 (24bpp, no alpha channel)
let mut background_image = reduce_image_to_16bit_color(&load_png_image("assets/1.png"));
Next, we’ll load the font PNG in the same manner (it doesn’t need to be mutable, as we’re not modifying the character pixels):
let font_image = reduce_image_to_16bit_color(&load_png_image("assets/fonts/font1.png"));
To handle text rendering a TextRenderer
struct is declared with the rendering logic encapsulated in TextRenderer.render_string()
, which loops through each character in the input string, looks up the location of the character in the bitmap font, and renders the 16×16 block of pixels for the character onto the background image. The x-location of where to render a character is incremented by the width of the previous character (found via lookup into TextRenderer::font_widths
vector).
pub struct TextRenderer {
font_widths: Vec<u8>,
}
impl TextRenderer {
pub fn new() -> TextRenderer {
TextRenderer {
font_widths: build_font_width_vec()
}
}
pub fn render_string(&self, txt: &str, x: u64, y: u64, fontimg: &[u8], outbuf: &mut [u8]) {
// x position of where character should be rendered
let mut cur_x = x;
for ch in txt.chars() {
// From codepoint, lookup x, y of character in font and width of character from self.font_widths
let ch_idx = ch as u64;
let ch_width = self.font_widths[(ch as u32 % 256) as usize];
let ch_x = (ch_idx % 16) * 16;
let ch_y = ((ch_idx as f32 / 16.0) as u64) * 16;
// For each character, copy the 16x16 block of pixels into outbuf
for fy in ch_y..ch_y+16 {
for fx in ch_x..ch_x+16 {
let fidx: usize = ((fx + fy*256) * 2) as usize;
let fdx = fx - ch_x;
let fdy = fy - ch_y;
let outbuf_idx: usize = (((cur_x + fdx) + (y + fdy)*240) * 2) as usize;
// If the pixel from the font bitmap is not black, write it out to outbuf
if fontimg[fidx] != 0x00 && fontimg[fidx+1] != 0x00 {
outbuf[outbuf_idx] = fontimg[fidx];
outbuf[outbuf_idx + 1] = fontimg[fidx + 1];
}
}
}
cur_x = cur_x + (ch_width as u64);
}
}
}
The TextRenderer::font_widths
vector is built from the build_font_width_vec()
method, which simply builds and returns a vector of hardcoded values for the character widths:
fn build_font_width_vec() -> Vec<u8> {
let result = vec![
7, 7, 7, 7, 7, 7, 7, 7, 7, 30, 0, 7, 7, 0, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
5, 3, 5, 9, 7, 18, 8, 3, 5, 5, 7, 9, 4, 8, 3, 5,
8, 4, 7, 7, 8, 7, 7, 7, 7, 7, 3, 4, 6, 9, 6, 7,
9, 8, 8, 8, 8, 8, 8, 8, 8, 5, 7, 8, 7, 9, 9, 9,
8, 9, 8, 8, 9, 8, 9, 10, 9, 9, 8, 4, 6, 4, 8, 9,
5, 7, 7, 6, 7, 7, 6, 7, 7, 3, 5, 7, 3, 9, 7, 7,
7, 7, 6, 6, 6, 7, 7, 10, 7, 7, 6, 5, 3, 5, 8, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 3, 3, 5, 5, 3, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 3, 6, 7, 7, 13, 3, 11, 10, 13, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
];
result
}
Writing to the ICON display
The final piece is simply creating a TextRenderer
instance and calling render_string()
with the necessary parameters. Here we’ll write out “Hello world!” to position 10, 20 on the display:
let tr = TextRenderer::new();
tr.render_string("Hello world!", 10, 20, &font_image, &mut background_image);
All of the code presented is up on the bitfenix-icon-sysstatus repo.
Next, I’m looking to play around with the systemstat library to print out something useful to the display.