Skip to content

Instantly share code, notes, and snippets.

@Gronis
Last active January 6, 2024 01:28
Show Gist options
  • Save Gronis/f433521d879f2487d2d67323bb69be0c to your computer and use it in GitHub Desktop.
Save Gronis/f433521d879f2487d2d67323bb69be0c to your computer and use it in GitHub Desktop.
mkfont - A small cli utility app for converting png images to 1-bit per pixel binary representations. Typically used for pixel art fonts that need to embed graphics into the executable binary

What

Use to convert png image of 8x8 sized tiles into 1-bit per pixel binary representation. This can for example be used to convert a 1-bit per pixel font into a binary representation that is easily read by embedded devices and can easily be converted into the correct format on device. Each byte represent a single line of pixels where each bit represents the color for the particular pixel. Each tile uses 8 bytes. This binary can then be embedded into the executable file and used to render graphics on screen.

Why

mkfont was made as a small script to convert a variable width pixel art styled font with 8x8 sized tiles for each character into a binary format to embed into the executable binary. This font was then used in a Gameboy Advance homebrew application.

The font: https://drive.google.com/drive/folders/1F8IqUY6G66n1ZGSLD2FclDNIpygWuJvz?usp=sharing

Usage

cargo run my-pixel-art.png Will create file: my-pixel-art.bin in current directory.

[package]
name = "mkfont"
version = "0.1.0"
edition = "2021"
[dependencies]
minipng = "0.1.1"
use std::{cmp::Ordering, env, fs, path::PathBuf};
#[derive(Debug)]
struct ColorCount<const N: usize> {
count: usize,
color: [u8; N],
}
fn main() {
let mut args_it = env::args();
args_it.next();
let Some(path) = args_it.next() else {
eprintln!("Path not provided");
return;
};
let Ok(bytes) = fs::read(&path) else {
eprintln!("Unable to read file from disk: \"{path}\"");
return;
};
let Ok(header) = minipng::decode_png_header(&bytes) else {
eprintln!("Unable to read png header of file: \"{path}\"");
return;
};
let mut buffer = vec![0; header.required_bytes()];
let Ok(img) = minipng::decode_png(&bytes, &mut buffer) else {
eprintln!("Unable to decode png file: \"{path}\"");
return;
};
let pixels = img.pixels();
let bit_depth = pixels.len() as u32 / img.width() / img.height() * 8;
let mut colors: Vec<ColorCount<1>> = vec![];
if bit_depth != 8 {
eprintln!("Error, bit depth is {bit_depth} is unsuppored. Consider using bit depth of 8");
return;
}
for x in 0..img.width() {
for y in 0..img.height() {
let index = (y * img.width() + x) as usize;
let color = pixels[index];
if let Some(ref mut color_count) = colors
.iter_mut()
.find(|color_count| color == color_count.color[0])
{
color_count.count += 1;
} else {
let color_count = ColorCount {
color: [color],
count: 0,
};
colors.push(color_count);
}
}
}
colors.sort_by(|left, right| {
if left.count < right.count {
Ordering::Greater
} else if left.count > right.count {
Ordering::Less
} else {
Ordering::Equal
}
});
let bpp = match colors.len() {
..=2 => 1,
..=4 => 2,
// ..=8 => 3,
..=16 => 4,
// ..=32 => 5,
// ..=64 => 6,
// ..=128 => 7,
..=256 => 8,
_ => {
eprintln!("Incorrect color count. Can handle 256 colors in palette at max. Counted {} different colors", colors.len());
return;
}
};
let mut pixel_colors = vec![0u8; (img.width() * img.height()) as usize];
let tile_width = 8;
let tile_height = 8;
let mut i = 0;
for y in 0..img.height() / tile_height {
for x in 0..img.width() / tile_width {
for iy in 0..tile_height {
for ix in 0..tile_width {
let index =
(y * img.width() * tile_height + iy * img.width() + x * tile_width + ix)
as usize;
let color = pixels[index];
if let Some((color_index, _)) = colors
.iter()
.enumerate()
.find(|(_, color_count)| color == color_count.color[0])
{
pixel_colors[i] = color_index as u8;
i += 1;
} else {
eprintln!("Could not find color {}", color);
return;
}
}
}
}
}
let pixels_per_chunk = 8 / bpp;
let pixel_bits_reduced = pixel_colors
.chunks(pixels_per_chunk)
.into_iter()
.map(|chunks| match pixels_per_chunk {
1 => chunks[0],
2 => ((chunks[0] << 4) & 0b11110000) | ((chunks[1] << 0) & 0b00001111),
4 => {
((chunks[0] << 6) & 0b11000000)
| ((chunks[1] << 4) & 0b00110000)
| ((chunks[2] << 2) & 0b00001100)
| ((chunks[3] << 0) & 0b00000011)
}
8 => {
((chunks[0] << 7) & 0b10000000)
| ((chunks[1] << 6) & 0b01000000)
| ((chunks[2] << 5) & 0b00100000)
| ((chunks[3] << 4) & 0b00010000)
| ((chunks[4] << 3) & 0b00001000)
| ((chunks[5] << 2) & 0b00000100)
| ((chunks[6] << 1) & 0b00000010)
| ((chunks[7] << 0) & 0b00000001)
}
_ => {
panic!("Incorrect chunk size: {}", pixels_per_chunk);
}
})
.collect::<Vec<_>>();
println!(
"img: {}x{}, bit depth: {bit_depth}, uncompressed file size: {} bytes, colors in palette: {}",
img.width(),
img.height(),
pixels.len(),
colors.len(),
);
let out_path: PathBuf = path.as_str().into();
let out_path: String = out_path.with_extension("bin").to_string_lossy().into();
let Ok(_) = fs::write(&out_path, &pixel_bits_reduced) else {
eprintln!("Unable to save binary output to \"{out_path}\"");
return;
};
eprintln!(
"Saved bitmap to: \"{out_path}\", {} bytes",
pixel_bits_reduced.len()
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment