Initial work on font map generation

This commit is contained in:
G2-Games 2024-05-14 11:21:34 -05:00
parent 8675b252f3
commit 45d6c1cbf1
13 changed files with 185 additions and 128 deletions

6
.gitignore vendored
View file

@ -9,9 +9,13 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# Ignore CZ image files # Ignore text files
*.txt
# Ignore testing files
*.cz* *.cz*
*.CZ* *.CZ*
*.png *.png
*.ttf
test_files/* test_files/*

View file

@ -1,7 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"cz", "cz", "font",
"utils", "utils",
] ]

View file

@ -10,4 +10,4 @@ A encoder/decoder for CZ# image files used in
[dependencies] [dependencies]
byteorder = "1.5.0" byteorder = "1.5.0"
thiserror = "1.0.59" thiserror = "1.0.59"
image = "0.25.1" image = {version = "0.25.1", default-features = false}

View file

@ -6,6 +6,7 @@ use std::{
}; };
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use image::Rgba;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -371,25 +372,25 @@ impl Default for ExtendedHeader {
pub fn get_palette<T: Seek + ReadBytesExt + Read>( pub fn get_palette<T: Seek + ReadBytesExt + Read>(
input: &mut T, input: &mut T,
num_colors: usize, num_colors: usize,
) -> Result<Vec<[u8; 4]>, CzError> { ) -> Result<Vec<Rgba<u8>>, CzError> {
let mut colormap = Vec::with_capacity(num_colors); let mut colormap = Vec::with_capacity(num_colors);
let mut rgba_buf = [0u8; 4]; let mut rgba_buf = [0u8; 4];
for _ in 0..num_colors { for _ in 0..num_colors {
input.read_exact(&mut rgba_buf)?; input.read_exact(&mut rgba_buf)?;
colormap.push(rgba_buf); colormap.push(rgba_buf.into());
} }
Ok(colormap) Ok(colormap)
} }
pub fn apply_palette(input: &[u8], palette: &[[u8; 4]]) -> Result<Vec<u8>, CzError> { pub fn apply_palette(input: &[u8], palette: &[Rgba<u8>]) -> Result<Vec<u8>, CzError> {
let mut output_map = Vec::new(); let mut output_map = Vec::new();
for byte in input.iter() { for byte in input.iter() {
let color = palette.get(*byte as usize); let color = palette.get(*byte as usize);
if let Some(color) = color { if let Some(color) = color {
output_map.extend_from_slice(color); output_map.extend_from_slice(&color.0);
} else { } else {
return Err(CzError::PaletteError); return Err(CzError::PaletteError);
} }
@ -398,7 +399,7 @@ pub fn apply_palette(input: &[u8], palette: &[[u8; 4]]) -> Result<Vec<u8>, CzErr
Ok(output_map) Ok(output_map)
} }
pub fn rgba_to_indexed(input: &[u8], palette: &[[u8; 4]]) -> Result<Vec<u8>, CzError> { pub fn rgba_to_indexed(input: &[u8], palette: &[Rgba<u8>]) -> Result<Vec<u8>, CzError> {
let mut output_map = Vec::new(); let mut output_map = Vec::new();
let mut cache = HashMap::new(); let mut cache = HashMap::new();
@ -406,7 +407,7 @@ pub fn rgba_to_indexed(input: &[u8], palette: &[[u8; 4]]) -> Result<Vec<u8>, CzE
let value = match cache.get(rgba) { let value = match cache.get(rgba) {
Some(val) => *val, Some(val) => *val,
None => { None => {
let value = palette.iter().position(|e| e == rgba).unwrap_or_default() as u8; let value = palette.iter().position(|e| e.0 == rgba).unwrap_or_default() as u8;
cache.insert(rgba, value); cache.insert(rgba, value);
value value
} }

View file

@ -5,7 +5,7 @@ use std::{
}; };
use crate::binio::BitIO; use crate::binio::BitIO;
use crate::common::{CzError, CzHeader}; use crate::common::CzError;
/// The size of compressed data in each chunk /// The size of compressed data in each chunk
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -227,82 +227,6 @@ fn copy_one(input: &[u8], src: usize) -> u8 {
} }
} }
pub fn line_diff<T: CzHeader>(header: &T, data: &[u8]) -> Vec<u8> {
let width = header.width() as u32;
let height = header.height() as u32;
let mut output_buf = data.to_vec();
let block_height = (f32::ceil(height as f32 / 3.0) as u16) as usize;
let pixel_byte_count = header.depth() >> 3;
let line_byte_count = (width * pixel_byte_count as u32) as usize;
let mut curr_line: Vec<u8>;
let mut prev_line: Vec<u8> = Vec::with_capacity(line_byte_count);
let mut i = 0;
for y in 0..height {
curr_line = data[i..i + line_byte_count].to_vec();
if y % block_height as u32 != 0 {
for x in 0..line_byte_count {
curr_line[x] = u8::wrapping_add(curr_line[x], prev_line[x])
}
}
prev_line.clone_from(&curr_line);
if pixel_byte_count == 4 {
output_buf[i..i + line_byte_count].copy_from_slice(&curr_line);
} else if pixel_byte_count == 3 {
for x in 0..line_byte_count {
let loc = ((y * width) as usize + x) * 4;
output_buf[loc..loc + 4].copy_from_slice(&[
curr_line[x],
curr_line[x + 1],
curr_line[x + 2],
0xFF,
])
}
}
i += line_byte_count;
}
output_buf
}
pub fn diff_line<T: CzHeader>(header: &T, input: &[u8]) -> Vec<u8> {
let width = header.width() as u32;
let height = header.height() as u32;
let mut data = Vec::with_capacity(input.len());
let block_height = (f32::ceil(height as f32 / 3.0) as u16) as usize;
let pixel_byte_count = header.depth() >> 3;
let line_byte_count = (width * pixel_byte_count as u32) as usize;
let mut curr_line;
let mut prev_line: Vec<u8> = Vec::with_capacity(line_byte_count);
let mut i = 0;
for y in 0..height {
curr_line = input[i..i + line_byte_count].to_vec();
if y % block_height as u32 != 0 {
for x in 0..line_byte_count {
curr_line[x] = curr_line[x].wrapping_sub(prev_line[x]);
prev_line[x] = prev_line[x].wrapping_add(curr_line[x]);
}
} else {
prev_line.clone_from(&curr_line);
}
data.extend_from_slice(&curr_line);
i += line_byte_count;
}
data
}
pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) { pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) {
let mut size = size; let mut size = size;
if size == 0 { if size == 0 {
@ -354,22 +278,21 @@ pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) {
fn compress_lzw(data: &[u8], size: usize, last: Vec<u8>) -> (usize, Vec<u16>, Vec<u8>) { fn compress_lzw(data: &[u8], size: usize, last: Vec<u8>) -> (usize, Vec<u16>, Vec<u8>) {
let mut count = 0; let mut count = 0;
let mut dictionary = HashMap::new(); let mut dictionary = HashMap::with_capacity(size);
for i in 0..=255 { for i in 0..=255 {
dictionary.insert(vec![i], i as u16); dictionary.insert(vec![i], i as u16);
} }
let mut dictionary_count = (dictionary.len() + 1) as u16; let mut dictionary_count = (dictionary.len() + 1) as u16;
let mut element = Vec::new(); let mut element = Vec::with_capacity(512);
if !last.is_empty() { if !last.is_empty() {
element = last element = last
} }
let mut compressed = Vec::with_capacity(size); let mut compressed = Vec::new();
for c in data { for c in data {
let mut entry = element.clone(); let mut entry = element.clone();
entry.push(*c); entry.push(*c);
if dictionary.contains_key(&entry) { if dictionary.contains_key(&entry) {
element = entry element = entry
} else { } else {

View file

@ -1,4 +1,5 @@
use byteorder::ReadBytesExt; use byteorder::ReadBytesExt;
use image::Rgba;
use std::{ use std::{
fs::File, fs::File,
io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
@ -16,7 +17,7 @@ use crate::{
pub struct DynamicCz { pub struct DynamicCz {
header_common: CommonHeader, header_common: CommonHeader,
header_extended: Option<ExtendedHeader>, header_extended: Option<ExtendedHeader>,
palette: Option<Vec<[u8; 4]>>, palette: Option<Vec<Rgba<u8>>>,
bitmap: Vec<u8>, bitmap: Vec<u8>,
} }
@ -81,7 +82,9 @@ impl DynamicCz {
} }
impl DynamicCz { impl DynamicCz {
pub fn decode<T: Seek + ReadBytesExt + Read>(input: &mut T) -> Result<Self, CzError> { pub fn decode<T: Seek + ReadBytesExt + Read>(
input: &mut T
) -> Result<Self, CzError> {
// Get the header common to all CZ images // Get the header common to all CZ images
let header_common = CommonHeader::from_bytes(input)?; let header_common = CommonHeader::from_bytes(input)?;
let mut header_extended = None; let mut header_extended = None;
@ -144,7 +147,7 @@ impl DynamicCz {
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?; output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
for rgba in pal { for rgba in pal {
out_file.write_all(rgba)?; out_file.write_all(&rgba.0)?;
} }
} }
_ => output_bitmap = self.bitmap().clone(), _ => output_bitmap = self.bitmap().clone(),

View file

@ -9,8 +9,6 @@ pub fn decode<T: Seek + ReadBytesExt + Read>(bytes: &mut T) -> Result<Vec<u8>, C
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
dbg!(&block_info);
// Get the bitmap // Get the bitmap
let bitmap = decompress(bytes, &block_info).unwrap(); let bitmap = decompress(bytes, &block_info).unwrap();
@ -20,8 +18,6 @@ pub fn decode<T: Seek + ReadBytesExt + Read>(bytes: &mut T) -> Result<Vec<u8>, C
pub fn encode<T: WriteBytesExt + Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> { pub fn encode<T: WriteBytesExt + Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> {
let (compressed_data, compressed_info) = compress(bitmap, 0xFEFD); let (compressed_data, compressed_info) = compress(bitmap, 0xFEFD);
dbg!(&compressed_info);
compressed_info.write_into(output)?; compressed_info.write_into(output)?;
output.write_all(&compressed_data)?; output.write_all(&compressed_data)?;

View file

@ -2,7 +2,7 @@ use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use crate::common::{CommonHeader, CzError, CzHeader}; use crate::common::{CommonHeader, CzError, CzHeader};
use crate::compression::{compress, decompress, diff_line, get_chunk_info, line_diff}; use crate::compression::{compress, decompress, get_chunk_info};
pub fn decode<T: Seek + ReadBytesExt + Read>( pub fn decode<T: Seek + ReadBytesExt + Read>(
bytes: &mut T, bytes: &mut T,
@ -32,3 +32,86 @@ pub fn encode<T: WriteBytesExt + Write, H: CzHeader>(
Ok(()) Ok(())
} }
/// Function to extract the data from a CZ3 file after compression
///
/// Uses the previous line to determine the characterisitcs of the
/// following lines
fn line_diff<T: CzHeader>(header: &T, data: &[u8]) -> Vec<u8> {
let width = header.width() as u32;
let height = header.height() as u32;
let mut output_buf = data.to_vec();
let block_height = (f32::ceil(height as f32 / 3.0) as u16) as usize;
let pixel_byte_count = header.depth() >> 3;
let line_byte_count = (width * pixel_byte_count as u32) as usize;
let mut curr_line: Vec<u8>;
let mut prev_line: Vec<u8> = Vec::with_capacity(line_byte_count);
let mut i = 0;
for y in 0..height {
curr_line = data[i..i + line_byte_count].to_vec();
if y % block_height as u32 != 0 {
for x in 0..line_byte_count {
curr_line[x] = u8::wrapping_add(curr_line[x], prev_line[x])
}
}
prev_line.clone_from(&curr_line);
if pixel_byte_count == 4 {
output_buf[i..i + line_byte_count].copy_from_slice(&curr_line);
} else if pixel_byte_count == 3 {
for x in 0..line_byte_count {
let loc = ((y * width) as usize + x) * 4;
output_buf[loc..loc + 4].copy_from_slice(&[
curr_line[x],
curr_line[x + 1],
curr_line[x + 2],
0xFF,
])
}
}
i += line_byte_count;
}
output_buf
}
/// Function to encode data into the CZ3 format before compression
///
/// Read more in [`line_diff`]
fn diff_line<T: CzHeader>(header: &T, input: &[u8]) -> Vec<u8> {
let width = header.width() as u32;
let height = header.height() as u32;
let mut data = Vec::with_capacity(input.len());
let block_height = (f32::ceil(height as f32 / 3.0) as u16) as usize;
let pixel_byte_count = header.depth() >> 3;
let line_byte_count = (width * pixel_byte_count as u32) as usize;
let mut curr_line;
let mut prev_line: Vec<u8> = Vec::with_capacity(line_byte_count);
let mut i = 0;
for y in 0..height {
curr_line = input[i..i + line_byte_count].to_vec();
if y % block_height as u32 != 0 {
for x in 0..line_byte_count {
curr_line[x] = curr_line[x].wrapping_sub(prev_line[x]);
prev_line[x] = prev_line[x].wrapping_add(curr_line[x]);
}
} else {
prev_line.clone_from(&curr_line);
}
data.extend_from_slice(&curr_line);
i += line_byte_count;
}
data
}

10
font/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "font"
version = "0.1.0"
edition = "2021"
authors.workspace = true
[dependencies]
[lints]
workspace = true

0
font/src/lib.rs Normal file
View file

View file

@ -7,5 +7,6 @@ edition = "2021"
[dependencies] [dependencies]
cz = { path = "../cz" } cz = { path = "../cz" }
fontdue = { version = "0.8.0", features = ["parallel"] }
image = "0.25.1" image = "0.25.1"
walkdir = "2.5.0" walkdir = "2.5.0"

View file

@ -0,0 +1,22 @@
use std::{
fs,
io::Error,
path::Path
};
use fontdue::{Font, FontSettings};
pub fn load_font<P: ?Sized + AsRef<Path>>(
path: &P
) -> Result<Font, Error> {
let font_file: Vec<u8> = fs::read(path)?;
let font = Font::from_bytes(
font_file,
FontSettings {
scale: 72.0,
..Default::default()
}
).unwrap();
Ok(font)
}

View file

@ -1,38 +1,52 @@
use std::time::Instant; mod font_generation;
use cz::{ use font_generation::load_font;
common::{CzVersion, ExtendedHeader}, use image::{ColorType, DynamicImage, GenericImage, GenericImageView};
dynamic::DynamicCz,
};
fn main() { fn main() {
let timer = Instant::now(); let font = load_font("NotoSans-Regular.ttf").unwrap();
let mio = image::open("mio_inverted.png").unwrap().to_rgba8();
println!("Opening PNG took {:?}", timer.elapsed());
let timer = Instant::now(); let mut characters = vec![];
let cz_mio = DynamicCz::from_raw( for ascii_char in 32..2048 {
CzVersion::CZ3, characters.push(char::from_u32(ascii_char).unwrap())
mio.width() as u16, }
mio.height() as u16,
mio.into_raw(),
)
.with_extended_header(
ExtendedHeader::new()
.with_crop(1280, 960)
.with_bounds(1280, 960),
);
println!("Constructing CZ3 took {:?}", timer.elapsed());
let timer = Instant::now(); const FONT_SIZE: f32 = 24.0;
cz_mio.save_as_png("test_save.png").unwrap(); const FONT_BOX: f32 = 25.0;
println!("Saving CZ3 as PNG took {:?}", timer.elapsed());
let timer = Instant::now(); let mut font_grid = DynamicImage::new(2560, 1800, ColorType::L8);
cz_mio.save_as_cz("test1.cz3").unwrap();
println!("Saving CZ3 as CZ3 took {:?}", timer.elapsed());
let timer = Instant::now(); let mut x_offset = 0.0;
let img = DynamicCz::open("test1.cz3").unwrap(); let mut y_offset = 0.0;
println!("Opening saved CZ3 took {:?}", timer.elapsed()); for (_i, character) in characters.iter().enumerate() {
let (metrics, char_bitmap) = font.rasterize(*character, FONT_SIZE);
let mut char_image: DynamicImage = image::GrayImage::from_raw(
metrics.width as u32,
metrics.height as u32,
char_bitmap
).unwrap().into();
let char_image = char_image.crop(0, 0, FONT_BOX as u32, FONT_BOX as u32);
let char_x_offset = (((FONT_BOX / 2.0) - metrics.advance_width) + metrics.bounds.xmin).ceil() as u32;
let char_y_offset = (((FONT_BOX / 1.5) - metrics.bounds.height) - metrics.bounds.ymin).ceil() as u32;
for y in 0..char_image.height() {
for x in 0..char_image.width() {
font_grid.put_pixel(
x + char_x_offset + x_offset as u32,
y + char_y_offset + y_offset as u32,
char_image.get_pixel(x, y)
);
}
}
x_offset += FONT_BOX;
if x_offset + FONT_BOX >= font_grid.width() as f32 {
x_offset = 0.0;
y_offset += FONT_BOX;
}
}
font_grid.save("grid.png").unwrap();
} }