diff --git a/.gitignore b/.gitignore index 269c4cd..7896074 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,13 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk -# Ignore CZ image files +# Ignore text files +*.txt + +# Ignore testing files *.cz* *.CZ* *.png +*.ttf test_files/* diff --git a/Cargo.toml b/Cargo.toml index 31eb562..e8ef275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = [ - "cz", + "cz", "font", "utils", ] diff --git a/cz/Cargo.toml b/cz/Cargo.toml index 8348135..fc9246f 100644 --- a/cz/Cargo.toml +++ b/cz/Cargo.toml @@ -10,4 +10,4 @@ A encoder/decoder for CZ# image files used in [dependencies] byteorder = "1.5.0" thiserror = "1.0.59" -image = "0.25.1" +image = {version = "0.25.1", default-features = false} diff --git a/cz/src/common.rs b/cz/src/common.rs index 35a0793..11670b9 100644 --- a/cz/src/common.rs +++ b/cz/src/common.rs @@ -6,6 +6,7 @@ use std::{ }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use image::Rgba; use thiserror::Error; #[derive(Error, Debug)] @@ -371,25 +372,25 @@ impl Default for ExtendedHeader { pub fn get_palette( input: &mut T, num_colors: usize, -) -> Result, CzError> { +) -> Result>, CzError> { let mut colormap = Vec::with_capacity(num_colors); let mut rgba_buf = [0u8; 4]; for _ in 0..num_colors { input.read_exact(&mut rgba_buf)?; - colormap.push(rgba_buf); + colormap.push(rgba_buf.into()); } Ok(colormap) } -pub fn apply_palette(input: &[u8], palette: &[[u8; 4]]) -> Result, CzError> { +pub fn apply_palette(input: &[u8], palette: &[Rgba]) -> Result, CzError> { let mut output_map = Vec::new(); for byte in input.iter() { let color = palette.get(*byte as usize); if let Some(color) = color { - output_map.extend_from_slice(color); + output_map.extend_from_slice(&color.0); } else { return Err(CzError::PaletteError); } @@ -398,7 +399,7 @@ pub fn apply_palette(input: &[u8], palette: &[[u8; 4]]) -> Result, CzErr Ok(output_map) } -pub fn rgba_to_indexed(input: &[u8], palette: &[[u8; 4]]) -> Result, CzError> { +pub fn rgba_to_indexed(input: &[u8], palette: &[Rgba]) -> Result, CzError> { let mut output_map = Vec::new(); let mut cache = HashMap::new(); @@ -406,7 +407,7 @@ pub fn rgba_to_indexed(input: &[u8], palette: &[[u8; 4]]) -> Result, CzE let value = match cache.get(rgba) { Some(val) => *val, 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); value } diff --git a/cz/src/compression.rs b/cz/src/compression.rs index 0b02242..8f7f90d 100644 --- a/cz/src/compression.rs +++ b/cz/src/compression.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::binio::BitIO; -use crate::common::{CzError, CzHeader}; +use crate::common::CzError; /// The size of compressed data in each chunk #[derive(Debug, Clone, Copy)] @@ -227,82 +227,6 @@ fn copy_one(input: &[u8], src: usize) -> u8 { } } -pub fn line_diff(header: &T, data: &[u8]) -> Vec { - 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; - let mut prev_line: Vec = 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(header: &T, input: &[u8]) -> Vec { - 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 = 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, CompressionInfo) { let mut size = size; if size == 0 { @@ -354,22 +278,21 @@ pub fn compress(data: &[u8], size: usize) -> (Vec, CompressionInfo) { fn compress_lzw(data: &[u8], size: usize, last: Vec) -> (usize, Vec, Vec) { let mut count = 0; - let mut dictionary = HashMap::new(); + let mut dictionary = HashMap::with_capacity(size); for i in 0..=255 { dictionary.insert(vec![i], i 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() { element = last } - let mut compressed = Vec::with_capacity(size); + let mut compressed = Vec::new(); for c in data { let mut entry = element.clone(); entry.push(*c); - if dictionary.contains_key(&entry) { element = entry } else { diff --git a/cz/src/dynamic.rs b/cz/src/dynamic.rs index a0eaec9..30308ad 100644 --- a/cz/src/dynamic.rs +++ b/cz/src/dynamic.rs @@ -1,4 +1,5 @@ use byteorder::ReadBytesExt; +use image::Rgba; use std::{ fs::File, io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, @@ -16,7 +17,7 @@ use crate::{ pub struct DynamicCz { header_common: CommonHeader, header_extended: Option, - palette: Option>, + palette: Option>>, bitmap: Vec, } @@ -81,7 +82,9 @@ impl DynamicCz { } impl DynamicCz { - pub fn decode(input: &mut T) -> Result { + pub fn decode( + input: &mut T + ) -> Result { // Get the header common to all CZ images let header_common = CommonHeader::from_bytes(input)?; let mut header_extended = None; @@ -144,7 +147,7 @@ impl DynamicCz { output_bitmap = rgba_to_indexed(self.bitmap(), pal)?; for rgba in pal { - out_file.write_all(rgba)?; + out_file.write_all(&rgba.0)?; } } _ => output_bitmap = self.bitmap().clone(), diff --git a/cz/src/formats/cz1.rs b/cz/src/formats/cz1.rs index 2711feb..2c53cf2 100644 --- a/cz/src/formats/cz1.rs +++ b/cz/src/formats/cz1.rs @@ -9,8 +9,6 @@ pub fn decode(bytes: &mut T) -> Result, C let block_info = get_chunk_info(bytes)?; bytes.seek(SeekFrom::Start(block_info.length as u64))?; - dbg!(&block_info); - // Get the bitmap let bitmap = decompress(bytes, &block_info).unwrap(); @@ -20,8 +18,6 @@ pub fn decode(bytes: &mut T) -> Result, C pub fn encode(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> { let (compressed_data, compressed_info) = compress(bitmap, 0xFEFD); - dbg!(&compressed_info); - compressed_info.write_into(output)?; output.write_all(&compressed_data)?; diff --git a/cz/src/formats/cz3.rs b/cz/src/formats/cz3.rs index 31a35e3..07d5308 100644 --- a/cz/src/formats/cz3.rs +++ b/cz/src/formats/cz3.rs @@ -2,7 +2,7 @@ use byteorder::{ReadBytesExt, WriteBytesExt}; use std::io::{Read, Seek, SeekFrom, Write}; 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( bytes: &mut T, @@ -32,3 +32,86 @@ pub fn encode( 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(header: &T, data: &[u8]) -> Vec { + 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; + let mut prev_line: Vec = 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(header: &T, input: &[u8]) -> Vec { + 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 = 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 +} diff --git a/font/Cargo.toml b/font/Cargo.toml new file mode 100644 index 0000000..c77f2d1 --- /dev/null +++ b/font/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "font" +version = "0.1.0" +edition = "2021" +authors.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/font/src/lib.rs b/font/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/utils/Cargo.toml b/utils/Cargo.toml index c4f9f29..013f741 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" [dependencies] cz = { path = "../cz" } +fontdue = { version = "0.8.0", features = ["parallel"] } image = "0.25.1" walkdir = "2.5.0" diff --git a/utils/src/font_generation.rs b/utils/src/font_generation.rs new file mode 100644 index 0000000..d298c1c --- /dev/null +++ b/utils/src/font_generation.rs @@ -0,0 +1,22 @@ +use std::{ + fs, + io::Error, + path::Path +}; +use fontdue::{Font, FontSettings}; + +pub fn load_font>( + path: &P +) -> Result { + let font_file: Vec = fs::read(path)?; + + let font = Font::from_bytes( + font_file, + FontSettings { + scale: 72.0, + ..Default::default() + } + ).unwrap(); + + Ok(font) +} diff --git a/utils/src/main.rs b/utils/src/main.rs index dd06e88..36911fb 100644 --- a/utils/src/main.rs +++ b/utils/src/main.rs @@ -1,38 +1,52 @@ -use std::time::Instant; +mod font_generation; -use cz::{ - common::{CzVersion, ExtendedHeader}, - dynamic::DynamicCz, -}; +use font_generation::load_font; +use image::{ColorType, DynamicImage, GenericImage, GenericImageView}; fn main() { - let timer = Instant::now(); - let mio = image::open("mio_inverted.png").unwrap().to_rgba8(); - println!("Opening PNG took {:?}", timer.elapsed()); + let font = load_font("NotoSans-Regular.ttf").unwrap(); - let timer = Instant::now(); - let cz_mio = DynamicCz::from_raw( - CzVersion::CZ3, - 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 mut characters = vec![]; + for ascii_char in 32..2048 { + characters.push(char::from_u32(ascii_char).unwrap()) + } - let timer = Instant::now(); - cz_mio.save_as_png("test_save.png").unwrap(); - println!("Saving CZ3 as PNG took {:?}", timer.elapsed()); + const FONT_SIZE: f32 = 24.0; + const FONT_BOX: f32 = 25.0; - let timer = Instant::now(); - cz_mio.save_as_cz("test1.cz3").unwrap(); - println!("Saving CZ3 as CZ3 took {:?}", timer.elapsed()); + let mut font_grid = DynamicImage::new(2560, 1800, ColorType::L8); - let timer = Instant::now(); - let img = DynamicCz::open("test1.cz3").unwrap(); - println!("Opening saved CZ3 took {:?}", timer.elapsed()); + let mut x_offset = 0.0; + let mut y_offset = 0.0; + 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(); }