Fixed bit depth saving and opening, added documentation

This commit is contained in:
G2-Games 2024-05-17 09:58:09 -05:00
parent 233219d70b
commit c33c54beeb
10 changed files with 324 additions and 261 deletions

View file

@ -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"

View file

@ -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<T: Seek + ReadBytesExt + Read>(
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<u8>]
@ -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<u8>, Vec<image::Rgba<u8>>), 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<Rgba<u8>> {
let mut colormap = Vec::new();

View file

@ -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<ExtendedHeader>,
@ -22,67 +26,19 @@ pub struct DynamicCz {
}
impl DynamicCz {
/// Open a CZ# file from a path
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<Self, CzError> {
let mut img_file = BufReader::new(std::fs::File::open(path)?);
Self::decode(&mut img_file)
}
pub fn save_as_png<P: ?Sized + AsRef<Path>>(
&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<u8>
) -> 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<T: Seek + ReadBytesExt + Read>(
/// 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<T: Seek + ReadBytesExt + Read>(
input: &mut T
) -> Result<Self, CzError> {
// Get the header common to all CZ images
@ -117,8 +73,26 @@ impl DynamicCz {
return Err(CzError::Corrupt);
}
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<T: Into<std::path::PathBuf>>(
&self,
path: T
@ -142,6 +119,11 @@ impl DynamicCz {
}
let output_bitmap;
match self.header_common.depth() {
4 => {
todo!()
}
8 => {
match &self.palette {
Some(pal) if self.header_common.depth() <= 8 => {
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
@ -149,9 +131,35 @@ impl DynamicCz {
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() {
CzVersion::CZ0 => cz0::encode(&mut out_file, &output_bitmap)?,
@ -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<P: ?Sized + AsRef<Path>>(
&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<u8>
) -> 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<Vec<Rgba<u8>>> {
&self.palette
}
/// Retrieve a mutable reference to the palette if it exists, otherwise
/// [`None`] is returned
pub fn palette_mut(&mut self) -> &mut Option<Vec<Rgba<u8>>> {
&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<u8>, 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<u8>) {
self.bitmap = bitmap
}
}

View file

@ -63,16 +63,21 @@ fn line_diff<T: CzHeader>(header: &T, data: &[u8]) -> Vec<u8> {
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;

View file

@ -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;

View file

@ -5,6 +5,8 @@ edition = "2021"
authors.workspace = true
[dependencies]
bimap = "0.6.3"
byteorder = "1.5.0"
[lints]
workspace = true

View file

@ -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<char, u16>,
draw_sizes: Vec<DrawSize>,
char_sizes: Vec<CharSize>,
}
#[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<P: ?Sized + AsRef<Path>>(
path: &P
) -> Result<FontInfo, io::Error> {
let mut file = fs::File::open(path).unwrap();
let font_size = file.read_u16::<LittleEndian>().unwrap();
let font_box = file.read_u16::<LittleEndian>().unwrap();
let character_count = file.read_u16::<LittleEndian>().unwrap();
let character_count2 = file.read_u16::<LittleEndian>().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::<LittleEndian>().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,
})
}

View file

@ -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"

View file

@ -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<char, u16>,
draw_sizes: Vec<DrawSize>,
char_sizes: Vec<CharSize>,
}
#[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<P: ?Sized + AsRef<Path>>(
path: &P
) -> Result<FontInfo, io::Error> {
let mut file = fs::File::open(path).unwrap();
let font_size = file.read_u16::<LittleEndian>().unwrap();
let font_box = file.read_u16::<LittleEndian>().unwrap();
let character_count = file.read_u16::<LittleEndian>().unwrap();
let character_count2 = file.read_u16::<LittleEndian>().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::<LittleEndian>().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();
}