diff --git a/cz/Cargo.toml b/cz/Cargo.toml index fc9246f..e7aaa11 100644 --- a/cz/Cargo.toml +++ b/cz/Cargo.toml @@ -11,3 +11,4 @@ A encoder/decoder for CZ# image files used in byteorder = "1.5.0" thiserror = "1.0.59" image = {version = "0.25.1", default-features = false} +quantizr = "1.4.2" diff --git a/cz/src/common.rs b/cz/src/common.rs index a2a61f3..96c73dd 100644 --- a/cz/src/common.rs +++ b/cz/src/common.rs @@ -7,6 +7,7 @@ use std::{ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use image::Rgba; +use quantizr::Image; use thiserror::Error; #[derive(Error, Debug)] @@ -99,9 +100,7 @@ pub trait CzHeader { /// The bit depth of the image (BPP) fn depth(&self) -> u16; - fn set_depth(&mut self) { - unimplemented!() - } + fn set_depth(&mut self, depth: u16); /// An unknown value? fn color_block(&self) -> u8; @@ -218,6 +217,10 @@ impl CzHeader for CommonHeader { self.depth } + fn set_depth(&mut self, depth: u16) { + self.depth = depth + } + fn color_block(&self) -> u8 { self.unknown } @@ -384,6 +387,8 @@ pub fn get_palette( Ok(colormap) } +/// Take a bitmap of indicies, and map a given palette to it, returning a new +/// RGBA bitmap pub fn apply_palette( input: &[u8], palette: &[Rgba] @@ -395,7 +400,6 @@ pub fn apply_palette( if let Some(color) = color { output_map.extend_from_slice(&color.0); } else { - dbg!(byte); return Err(CzError::PaletteError); } } @@ -426,6 +430,33 @@ pub fn rgba_to_indexed( Ok(output_map) } +pub fn indexed_gen_palette( + input: &[u8], + header: &CommonHeader, +) -> Result<(Vec, Vec>), CzError> { + + let image = Image::new( + input, + header.width() as usize, + header.height() as usize + ).unwrap(); + + let mut opts = quantizr::Options::default(); + opts.set_max_colors(1 << header.depth()).unwrap(); + + let mut result = quantizr::QuantizeResult::quantize(&image, &opts); + result.set_dithering_level(0.5).unwrap(); + + let mut indicies = vec![0u8; header.width() as usize * header.height() as usize]; + result.remap_image(&image, indicies.as_mut_slice()).unwrap(); + + let palette = result.get_palette(); + + let gen_palette = palette.entries.as_slice().iter().map(|c| Rgba([c.r, c.g, c.b, c.a])).collect(); + + Ok((indicies, gen_palette)) +} + pub fn default_palette() -> Vec> { let mut colormap = Vec::new(); diff --git a/cz/src/dynamic.rs b/cz/src/dynamic.rs index 30308ad..33a745d 100644 --- a/cz/src/dynamic.rs +++ b/cz/src/dynamic.rs @@ -8,12 +8,16 @@ use std::{ use crate::{ common::{ - apply_palette, get_palette, rgba_to_indexed, CommonHeader, CzError, CzHeader, CzVersion, - ExtendedHeader, + apply_palette, get_palette, indexed_gen_palette, + rgba_to_indexed, CommonHeader, CzError, CzHeader, + CzVersion, ExtendedHeader }, formats::{cz0, cz1, cz2, cz3, cz4}, }; +/// A CZ# interface which abstracts the CZ# generic file interface for +/// convenience. +#[derive(Debug)] pub struct DynamicCz { header_common: CommonHeader, header_extended: Option, @@ -22,67 +26,19 @@ pub struct DynamicCz { } impl DynamicCz { + /// Open a CZ# file from a path pub fn open>(path: &P) -> Result { let mut img_file = BufReader::new(std::fs::File::open(path)?); Self::decode(&mut img_file) } - pub fn save_as_png>( - &self, - path: &P, - ) -> Result<(), image::error::EncodingError> { - let image = image::RgbaImage::from_raw( - self.header_common.width() as u32, - self.header_common.height() as u32, - self.bitmap.clone(), - ) - .unwrap(); - - image - .save_with_format(path, image::ImageFormat::Png) - .unwrap(); - - Ok(()) - } - - pub fn from_raw( - version: CzVersion, - width: u16, - height: u16, - bitmap: Vec - ) -> Self { - let header_common = CommonHeader::new(version, width, height); - - Self { - header_common, - header_extended: None, - palette: None, - bitmap, - } - } - - pub fn with_header(mut self, header: CommonHeader) -> Self { - self.header_common = header; - - self - } - - pub fn with_extended_header(mut self, ext_header: ExtendedHeader) -> Self { - if ext_header.offset_width.is_some() { - self.header_common.set_length(36) - } else { - self.header_common.set_length(28) - } - - self.header_extended = Some(ext_header); - - self - } -} - -impl DynamicCz { - pub fn decode( + /// Decode a CZ# file from anything which implements [`Read`] and [`Seek`] + /// + /// The input must begin with the + /// [magic bytes](https://en.wikipedia.org/wiki/File_format#Magic_number) + /// of the file + fn decode( input: &mut T ) -> Result { // Get the header common to all CZ images @@ -117,8 +73,26 @@ impl DynamicCz { return Err(CzError::Corrupt); } - if let Some(palette) = &palette { - bitmap = apply_palette(&bitmap, palette)?; + match header_common.depth() { + 4 => { + todo!() + } + 8 => { + if let Some(palette) = &palette { + bitmap = apply_palette(&bitmap, palette)?; + } else { + return Err(CzError::PaletteError) + } + }, + 24 => { + bitmap = bitmap + .windows(3) + .step_by(3) + .flat_map(|p| [p[0], p[1], p[2], 0xFF]) + .collect(); + } + 32 => (), + _ => panic!() } Ok(Self { @@ -129,6 +103,9 @@ impl DynamicCz { }) } + /// Save the `DynamicCz` as a CZ# file. The format saved in is determined + /// from the format in the header. Check [`CommonHeader::set_version()`] + /// to change the CZ# version. pub fn save_as_cz>( &self, path: T @@ -142,15 +119,46 @@ impl DynamicCz { } let output_bitmap; - match &self.palette { - Some(pal) if self.header_common.depth() <= 8 => { - output_bitmap = rgba_to_indexed(self.bitmap(), pal)?; - - for rgba in pal { - out_file.write_all(&rgba.0)?; - } + match self.header_common.depth() { + 4 => { + todo!() } - _ => output_bitmap = self.bitmap().clone(), + 8 => { + match &self.palette { + Some(pal) if self.header_common.depth() <= 8 => { + output_bitmap = rgba_to_indexed(self.bitmap(), pal)?; + + for rgba in pal { + out_file.write_all(&rgba.0)?; + } + }, + // Generate a palette if there is none + None if self.header_common.depth() <= 8 => { + let result = indexed_gen_palette( + self.bitmap(), + self.header() + )?; + + output_bitmap = result.0; + let palette = result.1; + + for rgba in palette { + out_file.write_all(&rgba.0)?; + } + }, + _ => output_bitmap = self.bitmap().clone(), + } + }, + 24 => { + output_bitmap = self.bitmap + .windows(4) + .step_by(4) + .flat_map(|p| &p[0..3]) + .copied() + .collect(); + }, + 32 => output_bitmap = self.bitmap.clone(), + _ => return Err(CzError::Corrupt) } match self.header_common.version() { @@ -165,6 +173,92 @@ impl DynamicCz { Ok(()) } + /// Save the CZ# image as a lossless PNG file. + /// + /// Internally, the [`DynamicCz`] struct operates on 32-bit RGBA values, + /// which is the highest encountered in CZ# files, therefore saving them + /// as a PNG of the same or better quality is lossless. + pub fn save_as_png>( + &self, + path: &P, + ) -> Result<(), image::error::EncodingError> { + let image = image::RgbaImage::from_raw( + self.header_common.width() as u32, + self.header_common.height() as u32, + self.bitmap.clone(), + ) + .unwrap(); + + image + .save_with_format(path, image::ImageFormat::Png) + .unwrap(); + + Ok(()) + } + + /// Create a CZ# image from RGBA bytes. The bytes *must* be RGBA, as it is + /// used internally for operations + pub fn from_raw( + version: CzVersion, + depth: u16, + width: u16, + height: u16, + bitmap: Vec + ) -> Self { + let mut header_common = CommonHeader::new( + version, + width, + height + ); + header_common.set_depth(depth); + + Self { + header_common, + header_extended: None, + palette: None, + bitmap, + } + } + + /// Set a specific header for the image, this basica + pub fn with_header(mut self, header: CommonHeader) -> Self { + self.header_common = header; + + self + } + + /// Add an [`ExtendedHeader`] to the image. This header controls things like + /// cropping and offsets in the game engine + pub fn with_extended_header(mut self, ext_header: ExtendedHeader) -> Self { + if ext_header.offset_width.is_some() { + self.header_common.set_length(36) + } else { + self.header_common.set_length(28) + } + + self.header_extended = Some(ext_header); + + self + } + + /// Retrieve a reference to the palette if it exists, otherwise [`None`] + /// is returned + pub fn palette(&self) -> &Option>> { + &self.palette + } + + /// Retrieve a mutable reference to the palette if it exists, otherwise + /// [`None`] is returned + pub fn palette_mut(&mut self) -> &mut Option>> { + &mut self.palette + } + + /// Remove the image palette, which forces palette regeneration on save + /// for bit depths 8 or less + pub fn remove_palette(&mut self) { + *self.palette_mut() = None + } + pub fn header(&self) -> &CommonHeader { &self.header_common } @@ -185,16 +279,7 @@ impl DynamicCz { self.bitmap } - pub fn set_bitmap(&mut self, bitmap: Vec, width: u16, height: u16) -> Result<(), CzError> { - if bitmap.len() != width as usize * height as usize { - return Err(CzError::BitmapFormat); - } - - self.bitmap = bitmap; - - self.header_mut().set_width(width); - self.header_mut().set_height(height); - - Ok(()) + pub fn set_bitmap(&mut self, bitmap: Vec) { + self.bitmap = bitmap } } diff --git a/cz/src/formats/cz3.rs b/cz/src/formats/cz3.rs index 07d5308..e0febbf 100644 --- a/cz/src/formats/cz3.rs +++ b/cz/src/formats/cz3.rs @@ -63,16 +63,21 @@ fn line_diff(header: &T, data: &[u8]) -> Vec { 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; + for x in (0..line_byte_count).step_by(3) { + let loc = (y * 3 * width) as usize + x; - output_buf[loc..loc + 4].copy_from_slice(&[ + output_buf[loc..loc + 3].copy_from_slice(&[ curr_line[x], curr_line[x + 1], curr_line[x + 2], - 0xFF, ]) } + } else if pixel_byte_count == 1 { + for x in 0..line_byte_count { + let loc = (y * width) as usize + x; + + output_buf[loc] = curr_line[x]; + } } i += line_byte_count; diff --git a/cz/src/lib.rs b/cz/src/lib.rs index ed7d095..b9277d6 100644 --- a/cz/src/lib.rs +++ b/cz/src/lib.rs @@ -4,14 +4,17 @@ mod compression; pub mod common; pub mod dynamic; -pub mod formats { - pub mod cz0; - pub mod cz1; - pub mod cz2; - pub mod cz3; - pub mod cz4; +mod formats { + pub(crate) mod cz0; + pub(crate) mod cz1; + pub(crate) mod cz2; + pub(crate) mod cz3; + pub(crate) mod cz4; } +#[doc(inline)] +pub use dynamic::DynamicCz; + /* #[doc(inline)] pub use formats::cz0::Cz0Image; diff --git a/font/Cargo.toml b/font/Cargo.toml index c77f2d1..fd36622 100644 --- a/font/Cargo.toml +++ b/font/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" authors.workspace = true [dependencies] +bimap = "0.6.3" +byteorder = "1.5.0" [lints] workspace = true diff --git a/font/src/lib.rs b/font/src/lib.rs index e69de29..b630636 100644 --- a/font/src/lib.rs +++ b/font/src/lib.rs @@ -0,0 +1,88 @@ +use std::{fs, io, path::Path}; + +use bimap::BiMap; +use byteorder::{LittleEndian, ReadBytesExt}; + +#[derive(Debug)] +struct FontInfo { + font_size: u16, + font_box: u16, + character_count: u16, + character_count2: u16, + position_map: BiMap, + draw_sizes: Vec, + char_sizes: Vec, +} + +#[derive(Debug)] +struct DrawSize { + x: u8, // x offset + w: u8, // width + y: u8, // y offset +} + +#[derive(Debug)] +struct CharSize { + x: u8, // x offset + w: u8, // width +} + +fn parse_info>( + path: &P +) -> Result { + let mut file = fs::File::open(path).unwrap(); + + let font_size = file.read_u16::().unwrap(); + let font_box = file.read_u16::().unwrap(); + + let character_count = file.read_u16::().unwrap(); + let character_count2 = file.read_u16::().unwrap(); + + // If the character count is 100, the other character count is correct? + let real_char_count = if character_count == 100 { + character_count2 + } else { + character_count + }; + + let mut draw_sizes = Vec::new(); + for _ in 0..real_char_count { + draw_sizes.push(DrawSize { + x: file.read_u8().unwrap(), + w: file.read_u8().unwrap(), + y: file.read_u8().unwrap(), + }) + } + + let mut utf16_index = BiMap::new(); + utf16_index.insert(' ', 0); + let mut list = vec![]; + for index in 0..65535 { + let map_position = file.read_u16::().unwrap(); + if map_position == 0 { + continue + } + + list.push((char::from_u32(index).unwrap(), map_position)); + utf16_index.insert(char::from_u32(index).unwrap(), map_position); + } + dbg!(utf16_index.get_by_left(&'!')); + + let mut char_sizes = vec![]; + for _ in 0..65535 { + char_sizes.push(CharSize { + x: file.read_u8().unwrap(), + w: file.read_u8().unwrap(), + }) + } + + Ok(FontInfo { + font_size, + font_box, + character_count, + character_count2, + position_map: utf16_index, + draw_sizes, + char_sizes, + }) +} diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 0f21e95..6b7a350 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -6,10 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bimap = "0.6.3" byteorder = "1.5.0" cz = { path = "../cz" } -encoding_rs = "0.8.34" fontdue = { version = "0.8.0", features = ["parallel"] } image = "0.25.1" -walkdir = "2.5.0" diff --git a/utils/src/character_list b/utils/font_things/character_list similarity index 100% rename from utils/src/character_list rename to utils/font_things/character_list diff --git a/utils/src/main.rs b/utils/src/main.rs index dc84488..2469b46 100644 --- a/utils/src/main.rs +++ b/utils/src/main.rs @@ -1,175 +1,26 @@ mod font_generation; -use std::{fs::{self, File}, io::{self, Write}, path::Path}; -use bimap::BiMap; -use byteorder::{LittleEndian, ReadBytesExt}; -use cz::{common::default_palette, dynamic::DynamicCz}; -use font_generation::load_font; -use image::{ColorType, DynamicImage, GenericImage, GenericImageView}; +use cz::{ + common::{CzHeader, CzVersion}, + dynamic::DynamicCz +}; fn main() { - DynamicCz::open("24-style1.cz1").unwrap().save_as_png("24-style1.png").unwrap(); + // Open the desired PNG + let new_bitmap = image::open("CGGALLERY_CH01_003.png") + .unwrap() + .to_rgba8(); - parse_info("info24-lbee").unwrap(); + let mut gallery_cz = DynamicCz::open("CGGALLERY_CH01_003").unwrap(); - let font = load_font("RodinNTLG Pro M.otf").unwrap(); - let fallback = load_font("NotoSans-Regular.ttf").unwrap(); + gallery_cz.set_bitmap(new_bitmap.into_vec()); + gallery_cz.header_mut().set_depth(32); + gallery_cz.header_mut().set_version(CzVersion::CZ0); + gallery_cz.save_as_cz("CGGALLERY_CH01_003-MODIFIED").unwrap(); - let characters = fs::read_to_string("character_list").unwrap(); + // Open that same CZ3 again to test decoding + let cz_image_test = DynamicCz::open("CGGALLERY_CH01_003-MODIFIED").unwrap(); - const FONT_SIZE: f32 = 24.0; - const BASELINE: f32 = FONT_BOX * 0.84; - const FONT_BOX: f32 = 25.0; - - let mut font_grid = DynamicImage::new(2504, 1800, ColorType::L8); - - let mut x_offset = 0; - let mut y_offset = 0; - for (_i, character) in characters.chars().enumerate() { - if character == '\n' { - continue - } - - let (metrics, char_bitmap) = match font.has_glyph(character) { - true => font.rasterize(character, FONT_SIZE), - false => fallback.rasterize(character, FONT_SIZE), - }; - - let char_image: DynamicImage = image::GrayImage::from_raw( - metrics.width as u32, - metrics.height as u32, - char_bitmap - ).unwrap().into(); - - let char_x_offset = metrics.xmin as i32; - let char_y_offset = ((BASELINE as isize - metrics.height as isize) - metrics.ymin as isize) as i32; - - for y in 0..char_image.height() as i32 { - for x in 0..char_image.width() as i32 { - let x_pos = x + x_offset + char_x_offset; - let y_pos = y + y_offset + char_y_offset; - - if !font_grid.in_bounds( - x_pos as u32, - y_pos as u32 - ) { - continue - } - - if x_pos > x_offset + FONT_BOX as i32 || x_pos < x_offset { - continue - } else if y_pos > y_offset + FONT_BOX as i32 || y_pos < y_offset { - continue - } - - font_grid.put_pixel( - x_pos as u32, - y_pos as u32, - char_image.get_pixel(x as u32, y as u32) - ); - } - } - - x_offset += FONT_BOX as i32; - if x_offset + FONT_BOX as i32 >= font_grid.width() as i32 { - x_offset = 0; - y_offset += FONT_BOX as i32; - } - } - - let result_image = cz::common::apply_palette(font_grid.as_bytes(), &default_palette()).unwrap(); - let cz1_font = DynamicCz::from_raw( - cz::common::CzVersion::CZ1, - font_grid.width() as u16, - font_grid.height() as u16, - result_image - ); - - cz1_font.save_as_cz("replacement_24.cz1").unwrap(); - - cz1_font.save_as_png("grid.png").unwrap(); -} - -#[derive(Debug)] -struct FontInfo { - font_size: u16, - font_box: u16, - character_count: u16, - character_count2: u16, - position_map: BiMap, - draw_sizes: Vec, - char_sizes: Vec, -} - -#[derive(Debug)] -struct DrawSize { - x: u8, // x offset - w: u8, // width - y: u8, // y offset -} - -#[derive(Debug)] -struct CharSize { - x: u8, // x offset - w: u8, // width -} - -fn parse_info>( - path: &P -) -> Result { - let mut file = fs::File::open(path).unwrap(); - - let font_size = file.read_u16::().unwrap(); - let font_box = file.read_u16::().unwrap(); - - let character_count = file.read_u16::().unwrap(); - let character_count2 = file.read_u16::().unwrap(); - - // If the character count is 100, the other character count is correct? - let real_char_count = if character_count == 100 { - character_count2 - } else { - character_count - }; - - let mut draw_sizes = Vec::new(); - for _ in 0..real_char_count { - draw_sizes.push(DrawSize { - x: file.read_u8().unwrap(), - w: file.read_u8().unwrap(), - y: file.read_u8().unwrap(), - }) - } - - let mut utf16_index = BiMap::new(); - utf16_index.insert(' ', 0); - let mut list = vec![]; - for index in 0..65535 { - let map_position = file.read_u16::().unwrap(); - if map_position == 0 { - continue - } - - list.push((char::from_u32(index).unwrap(), map_position)); - utf16_index.insert(char::from_u32(index).unwrap(), map_position); - } - dbg!(utf16_index.get_by_left(&'!')); - - let mut char_sizes = vec![]; - for _ in 0..65535 { - char_sizes.push(CharSize { - x: file.read_u8().unwrap(), - w: file.read_u8().unwrap(), - }) - } - - Ok(FontInfo { - font_size, - font_box, - character_count, - character_count2, - position_map: utf16_index, - draw_sizes, - char_sizes, - }) + // Save the newly decoded CZ3 as another PNG as a test + cz_image_test.save_as_png("CGGALLERY_CH01_003-MODIFIED.png").unwrap(); }