able to split tablerized image and encode group of images into sff

This commit is contained in:
uosfz 2025-07-30 20:02:14 +02:00
commit d7c2435fdd
Signed by: uosfz
SSH key fingerprint: SHA256:FlktuluyhTQg3jHZNLKwxOOC5hbfrUXM0tz3IA3lGJo
6 changed files with 1520 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/font
/res

1076
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

7
Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "karlos-helper"
version = "0.1.0"
edition = "2021"
[dependencies]
image = "0.25.6"

42
src/image_wrapper.rs Normal file
View file

@ -0,0 +1,42 @@
use std::{
fs::OpenOptions,
io::{BufWriter, Write},
path::Path,
};
use image::{DynamicImage, GenericImage, ImageReader};
#[derive(Debug, Clone)]
pub struct BWImage {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
pub fn load_bw_image<P: AsRef<Path>>(p: P) -> BWImage {
let img = ImageReader::open(p).unwrap().decode().unwrap();
match img {
DynamicImage::ImageRgba8(buf) => BWImage {
width: buf.width(),
height: buf.height(),
data: buf
.pixels()
.map(|px| ((px.0[0] as usize + px.0[1] as usize + px.0[2] as usize) / 3) as u8)
.collect(),
},
DynamicImage::ImageRgb8(buf) => BWImage {
width: buf.width(),
height: buf.height(),
data: buf
.pixels()
.map(|px| ((px.0[0] as usize + px.0[1] as usize + px.0[2] as usize) / 3) as u8)
.collect(),
},
DynamicImage::ImageLuma8(buf) => BWImage {
width: buf.width(),
height: buf.height(),
data: buf.into_raw(),
},
_ => unreachable!(),
}
}

144
src/main.rs Normal file
View file

@ -0,0 +1,144 @@
use std::collections::HashMap;
use std::path::Path;
mod image_wrapper;
use image_wrapper::BWImage;
use sff::FileDescOut;
mod sff;
/// Iterates over a directory which shall contain images named by their number in the font.
/// Asserts that all images have the same dimensions.
fn load_font_dir<P: AsRef<Path>>(dir_with_images: P) -> HashMap<u32, BWImage> {
let mut map = HashMap::new();
let (mut found_width, mut found_height) = (None, None);
for elem in std::fs::read_dir(dir_with_images).unwrap() {
let elem = elem.unwrap();
let img = image_wrapper::load_bw_image(elem.path());
match (found_width, found_height) {
(None, None) => {
found_width = Some(img.width);
found_height = Some(img.height);
}
(Some(fw), Some(fh)) => {
assert!(img.width == fw);
assert!(img.height == fh);
}
_ => unreachable!(),
}
let number: u32 = elem
.path()
.file_stem()
.unwrap()
.to_str()
.unwrap()
.parse::<u32>()
.unwrap();
assert!(!map.contains_key(&number));
map.insert(number, img);
}
map
}
fn image_table_split<P, Q>(
src_image: P,
dst_dir: Q,
start_number: usize,
nrows: usize,
ncols: usize,
width_per_elem: usize,
height_per_elem: usize,
) where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let src_img = image::ImageReader::open(src_image)
.unwrap()
.decode()
.unwrap();
assert!(src_img.width() as usize == ncols * width_per_elem);
assert!(src_img.height() as usize == nrows * height_per_elem);
let mut number_count = start_number;
for row in 0..nrows {
for col in 0..ncols {
let img = src_img.crop_imm(
(col * width_per_elem) as u32,
(row * height_per_elem) as u32,
width_per_elem as u32,
height_per_elem as u32,
);
let file_name = format!("{}.png", number_count);
img.save(dst_dir.as_ref().join(file_name)).unwrap();
number_count += 1;
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct TableRange {
start: u32,
len: u32,
}
fn sorted_numbers_into_ranges(sorted_numbers: &[u32]) -> Vec<TableRange> {
let mut curr_range = TableRange {
start: sorted_numbers[0],
len: 1,
};
let mut out: Vec<TableRange> = Vec::new();
for elem in &sorted_numbers[1..] {
if curr_range.start + curr_range.len == *elem {
curr_range.len += 1;
} else {
out.push(curr_range);
curr_range.start = *elem;
curr_range.len = 1;
}
}
out.push(curr_range);
out
}
fn main() {
image_table_split("res/test.png", "font", 32, 6, 16, 8, 18);
let map = load_font_dir("font");
let mut sorted_keys: Vec<u32> = map.keys().copied().collect();
sorted_keys.sort();
let ranges = sorted_numbers_into_ranges(&sorted_keys);
let fdo = sff::FileDescOut {
char_width: 8,
char_height: 18,
encoding: sff::Encoding::BitPerPix4,
tables: ranges
.into_iter()
.map(|rng| sff::TableOut {
first_codepoint: rng.start,
num_codepoints: rng.len,
// TODO shitty clone
data: (rng.start..rng.start + rng.len)
.map(|codepoint| map.get(&codepoint).unwrap().data.clone())
.collect(),
})
.collect(),
};
std::fs::write("font.sff", fdo.encode()).unwrap();
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_ranger() {
let numbers = [3, 4, 5, 6, 8];
let ranges = sorted_numbers_into_ranges(&numbers);
assert_eq!(
ranges,
[
TableRange { start: 3, len: 4 },
TableRange { start: 8, len: 1 }
]
);
}
}

248
src/sff.rs Normal file
View file

@ -0,0 +1,248 @@
#[derive(Debug)]
struct Table {
first_codepoint: u32,
num_codepoints: u32,
offset: u32,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Encoding {
BitPerPix1,
BitPerPix2,
BitPerPix4,
BitPerPix8,
}
struct Access {
byte_index: usize,
shift: u8,
mask: u8,
}
impl Encoding {
fn into_code(self) -> u16 {
match self {
Encoding::BitPerPix1 => 0,
Encoding::BitPerPix2 => 1,
Encoding::BitPerPix4 => 2,
Encoding::BitPerPix8 => 3,
}
}
fn access_from_index(self, px_index: usize) -> Access {
match self {
Encoding::BitPerPix1 => Access {
byte_index: px_index / 8,
shift: (7 - (px_index % 8)) as u8,
mask: 0x01,
},
Encoding::BitPerPix2 => Access {
byte_index: px_index / 4,
shift: ((3 - (px_index % 4)) << 1) as u8,
mask: 0x03,
},
Encoding::BitPerPix4 => Access {
byte_index: px_index / 2,
shift: ((1 - (px_index % 2)) << 2) as u8,
mask: 0x0f,
},
Encoding::BitPerPix8 => Access {
byte_index: px_index,
shift: 0,
mask: 0xff,
},
}
}
/// Compresses standard brightness data (0-255) into a suitable form for sff.
fn compress(self, input: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
match self {
Encoding::BitPerPix1 => todo!(),
Encoding::BitPerPix2 => todo!(),
Encoding::BitPerPix4 => {
assert!(input.len() % 2 == 0); // TODO not strictly necessary
for c in 0..input.len() / 2 {
let one = input[2 * c] >> 4;
let two = input[2 * c + 1] >> 4;
out.push((one << 4) | two);
}
}
Encoding::BitPerPix8 => Vec::extend_from_slice(&mut out, input),
}
out
}
}
fn get_u16(input: &[u8], byte_offset: usize) -> u16 {
assert!(byte_offset % 2 == 0);
input[byte_offset] as u16 | (input[byte_offset + 1] as u16) << 8
}
fn get_u32(input: &[u8], byte_offset: usize) -> u32 {
assert!(byte_offset % 4 == 0);
input[byte_offset] as u32
| (input[byte_offset + 1] as u32) << 8
| (input[byte_offset + 2] as u32) << 16
| (input[byte_offset + 3] as u32) << 24
}
#[derive(Debug)]
struct FileDesc<'a> {
input: &'a [u8],
char_width: usize,
char_height: usize,
encoding: Encoding,
tables: Vec<Table>,
}
impl<'a> FileDesc<'a> {
fn bytes_per_codepoint(&self) -> usize {
let px_per_codepoint = self.char_width * self.char_height;
match self.encoding {
Encoding::BitPerPix1 => px_per_codepoint >> 3,
Encoding::BitPerPix2 => px_per_codepoint >> 2,
Encoding::BitPerPix4 => px_per_codepoint >> 1,
Encoding::BitPerPix8 => px_per_codepoint,
}
}
fn find_codepoint(&self, codepoint: u32) -> Option<&[u8]> {
self.tables
.iter()
.find(|tab| {
tab.first_codepoint <= codepoint
&& tab.first_codepoint + tab.num_codepoints > codepoint
})
.map(|tab| {
let off = tab.offset as usize
+ (codepoint - tab.first_codepoint) as usize * self.bytes_per_codepoint();
&self.input[off..off + self.bytes_per_codepoint()]
})
}
// input should be result of find_codepoint here
fn get_pixel_value(&self, input: &[u8], row: u8, col: u8) -> Option<u8> {
if row as usize >= self.char_height || col as usize >= self.char_width {
return None;
}
let px_index = row as usize * self.char_width + col as usize;
let Access {
byte_index,
shift,
mask,
} = self.encoding.access_from_index(px_index);
Some((input[byte_index] >> shift) & mask)
}
}
fn decode(input: &[u8]) -> FileDesc<'_> {
if input[0] != b's' || input[1] != b'f' || input[2] != b'f' || input[3] != b'\0' {
panic!("invalid header magic");
}
if get_u32(input, 4) != 1 {
panic!("invalid version");
}
let char_width = get_u16(input, 0x8) as usize;
let char_height = get_u16(input, 0xa) as usize;
let encoding = match get_u16(input, 0xc) {
0 => Encoding::BitPerPix1,
1 => Encoding::BitPerPix2,
2 => Encoding::BitPerPix4,
3 => Encoding::BitPerPix8,
_ => unreachable!(),
};
let num_tables = get_u16(input, 0xe);
let mut tables = Vec::new();
let mut coff = 0x10;
for _ in 0..num_tables {
let tab = Table {
first_codepoint: get_u32(input, coff),
num_codepoints: get_u32(input, coff + 4),
offset: get_u32(input, coff + 8),
};
tables.push(tab);
coff += 3 * 4;
}
FileDesc {
input,
char_width,
char_height,
encoding,
tables,
}
}
/// data holds unencoded values, from 0 to 255. They are scaled down/rounded internally.
#[derive(Debug)]
pub struct TableOut {
pub first_codepoint: u32,
pub num_codepoints: u32,
pub data: Vec<Vec<u8>>,
}
#[derive(Debug)]
pub struct FileDescOut {
pub char_width: usize,
pub char_height: usize,
pub encoding: Encoding,
pub tables: Vec<TableOut>,
}
fn push_u16(v: &mut Vec<u8>, value: u16) {
v.push((value & 0xff) as u8);
v.push(((value >> 8) & 0xff) as u8);
}
fn push_u32(v: &mut Vec<u8>, value: u32) {
v.push((value & 0xff) as u8);
v.push(((value >> 8) & 0xff) as u8);
v.push(((value >> 16) & 0xff) as u8);
v.push(((value >> 24) & 0xff) as u8);
}
fn put_u16(output: &mut [u8], byte_offset: usize, value: u16) {
assert!(byte_offset % 2 == 0);
output[byte_offset] = (value & 0xff) as u8;
output[byte_offset + 1] = ((value >> 8) & 0xff) as u8;
}
fn put_u32(output: &mut [u8], byte_offset: usize, value: u32) {
assert!(byte_offset % 4 == 0);
output[byte_offset] = (value & 0xff) as u8;
output[byte_offset + 1] = ((value >> 8) & 0xff) as u8;
output[byte_offset + 2] = ((value >> 16) & 0xff) as u8;
output[byte_offset + 3] = ((value >> 24) & 0xff) as u8;
}
impl FileDescOut {
pub fn encode(&self) -> Vec<u8> {
let mut v = vec![b's', b'f', b'f', b'\0'];
push_u32(&mut v, 1);
assert!(self.char_width < 1 << 16);
assert!(self.char_height < 1 << 16);
push_u16(&mut v, self.char_width as u16);
push_u16(&mut v, self.char_height as u16);
push_u16(&mut v, self.encoding.into_code());
push_u16(&mut v, self.tables.len().try_into().unwrap());
for _ in 0..self.tables.len() {
// first, reserve space for the table headers
push_u32(&mut v, 0);
push_u32(&mut v, 0);
push_u32(&mut v, 0);
}
for (i, table) in self.tables.iter().enumerate() {
let header_off = 0x10 + i * 3 * 4;
put_u32(&mut v[..], header_off, table.first_codepoint);
put_u32(&mut v[..], header_off + 4, table.num_codepoints);
let curr_len = v.len() as u32;
put_u32(&mut v[..], header_off + 8, curr_len);
for glyph_data in table.data.iter() {
// TODO ugly clone
v.extend_from_slice(&self.encoding.compress(glyph_data)[..]);
}
}
v
}
}