mirror of
https://github.com/G2-Games/lbee-utils.git
synced 2025-05-01 21:22:53 -05:00
Fixed bit depth saving and opening, added documentation
This commit is contained in:
parent
233219d70b
commit
c33c54beeb
10 changed files with 324 additions and 261 deletions
|
@ -11,3 +11,4 @@ A encoder/decoder for CZ# image files used in
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
thiserror = "1.0.59"
|
thiserror = "1.0.59"
|
||||||
image = {version = "0.25.1", default-features = false}
|
image = {version = "0.25.1", default-features = false}
|
||||||
|
quantizr = "1.4.2"
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
|
use quantizr::Image;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -99,9 +100,7 @@ pub trait CzHeader {
|
||||||
/// The bit depth of the image (BPP)
|
/// The bit depth of the image (BPP)
|
||||||
fn depth(&self) -> u16;
|
fn depth(&self) -> u16;
|
||||||
|
|
||||||
fn set_depth(&mut self) {
|
fn set_depth(&mut self, depth: u16);
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An unknown value?
|
/// An unknown value?
|
||||||
fn color_block(&self) -> u8;
|
fn color_block(&self) -> u8;
|
||||||
|
@ -218,6 +217,10 @@ impl CzHeader for CommonHeader {
|
||||||
self.depth
|
self.depth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_depth(&mut self, depth: u16) {
|
||||||
|
self.depth = depth
|
||||||
|
}
|
||||||
|
|
||||||
fn color_block(&self) -> u8 {
|
fn color_block(&self) -> u8 {
|
||||||
self.unknown
|
self.unknown
|
||||||
}
|
}
|
||||||
|
@ -384,6 +387,8 @@ pub fn get_palette<T: Seek + ReadBytesExt + Read>(
|
||||||
Ok(colormap)
|
Ok(colormap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take a bitmap of indicies, and map a given palette to it, returning a new
|
||||||
|
/// RGBA bitmap
|
||||||
pub fn apply_palette(
|
pub fn apply_palette(
|
||||||
input: &[u8],
|
input: &[u8],
|
||||||
palette: &[Rgba<u8>]
|
palette: &[Rgba<u8>]
|
||||||
|
@ -395,7 +400,6 @@ pub fn apply_palette(
|
||||||
if let Some(color) = color {
|
if let Some(color) = color {
|
||||||
output_map.extend_from_slice(&color.0);
|
output_map.extend_from_slice(&color.0);
|
||||||
} else {
|
} else {
|
||||||
dbg!(byte);
|
|
||||||
return Err(CzError::PaletteError);
|
return Err(CzError::PaletteError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,6 +430,33 @@ pub fn rgba_to_indexed(
|
||||||
Ok(output_map)
|
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>> {
|
pub fn default_palette() -> Vec<Rgba<u8>> {
|
||||||
let mut colormap = Vec::new();
|
let mut colormap = Vec::new();
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,16 @@ use std::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{
|
common::{
|
||||||
apply_palette, get_palette, rgba_to_indexed, CommonHeader, CzError, CzHeader, CzVersion,
|
apply_palette, get_palette, indexed_gen_palette,
|
||||||
ExtendedHeader,
|
rgba_to_indexed, CommonHeader, CzError, CzHeader,
|
||||||
|
CzVersion, ExtendedHeader
|
||||||
},
|
},
|
||||||
formats::{cz0, cz1, cz2, cz3, cz4},
|
formats::{cz0, cz1, cz2, cz3, cz4},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A CZ# interface which abstracts the CZ# generic file interface for
|
||||||
|
/// convenience.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct DynamicCz {
|
pub struct DynamicCz {
|
||||||
header_common: CommonHeader,
|
header_common: CommonHeader,
|
||||||
header_extended: Option<ExtendedHeader>,
|
header_extended: Option<ExtendedHeader>,
|
||||||
|
@ -22,67 +26,19 @@ pub struct DynamicCz {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicCz {
|
impl DynamicCz {
|
||||||
|
/// Open a CZ# file from a path
|
||||||
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<Self, CzError> {
|
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<Self, CzError> {
|
||||||
let mut img_file = BufReader::new(std::fs::File::open(path)?);
|
let mut img_file = BufReader::new(std::fs::File::open(path)?);
|
||||||
|
|
||||||
Self::decode(&mut img_file)
|
Self::decode(&mut img_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_as_png<P: ?Sized + AsRef<Path>>(
|
/// Decode a CZ# file from anything which implements [`Read`] and [`Seek`]
|
||||||
&self,
|
///
|
||||||
path: &P,
|
/// The input must begin with the
|
||||||
) -> Result<(), image::error::EncodingError> {
|
/// [magic bytes](https://en.wikipedia.org/wiki/File_format#Magic_number)
|
||||||
let image = image::RgbaImage::from_raw(
|
/// of the file
|
||||||
self.header_common.width() as u32,
|
fn decode<T: Seek + ReadBytesExt + Read>(
|
||||||
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>(
|
|
||||||
input: &mut T
|
input: &mut T
|
||||||
) -> Result<Self, CzError> {
|
) -> Result<Self, CzError> {
|
||||||
// Get the header common to all CZ images
|
// Get the header common to all CZ images
|
||||||
|
@ -117,8 +73,26 @@ impl DynamicCz {
|
||||||
return Err(CzError::Corrupt);
|
return Err(CzError::Corrupt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match header_common.depth() {
|
||||||
|
4 => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
8 => {
|
||||||
if let Some(palette) = &palette {
|
if let Some(palette) = &palette {
|
||||||
bitmap = apply_palette(&bitmap, 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 {
|
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>>(
|
pub fn save_as_cz<T: Into<std::path::PathBuf>>(
|
||||||
&self,
|
&self,
|
||||||
path: T
|
path: T
|
||||||
|
@ -142,6 +119,11 @@ impl DynamicCz {
|
||||||
}
|
}
|
||||||
|
|
||||||
let output_bitmap;
|
let output_bitmap;
|
||||||
|
match self.header_common.depth() {
|
||||||
|
4 => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
8 => {
|
||||||
match &self.palette {
|
match &self.palette {
|
||||||
Some(pal) if self.header_common.depth() <= 8 => {
|
Some(pal) if self.header_common.depth() <= 8 => {
|
||||||
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
|
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
|
||||||
|
@ -149,9 +131,35 @@ impl DynamicCz {
|
||||||
for rgba in pal {
|
for rgba in pal {
|
||||||
out_file.write_all(&rgba.0)?;
|
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(),
|
_ => 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() {
|
match self.header_common.version() {
|
||||||
CzVersion::CZ0 => cz0::encode(&mut out_file, &output_bitmap)?,
|
CzVersion::CZ0 => cz0::encode(&mut out_file, &output_bitmap)?,
|
||||||
|
@ -165,6 +173,92 @@ impl DynamicCz {
|
||||||
Ok(())
|
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 {
|
pub fn header(&self) -> &CommonHeader {
|
||||||
&self.header_common
|
&self.header_common
|
||||||
}
|
}
|
||||||
|
@ -185,16 +279,7 @@ impl DynamicCz {
|
||||||
self.bitmap
|
self.bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_bitmap(&mut self, bitmap: Vec<u8>, width: u16, height: u16) -> Result<(), CzError> {
|
pub fn set_bitmap(&mut self, bitmap: Vec<u8>) {
|
||||||
if bitmap.len() != width as usize * height as usize {
|
self.bitmap = bitmap
|
||||||
return Err(CzError::BitmapFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.bitmap = bitmap;
|
|
||||||
|
|
||||||
self.header_mut().set_width(width);
|
|
||||||
self.header_mut().set_height(height);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,16 +63,21 @@ fn line_diff<T: CzHeader>(header: &T, data: &[u8]) -> Vec<u8> {
|
||||||
if pixel_byte_count == 4 {
|
if pixel_byte_count == 4 {
|
||||||
output_buf[i..i + line_byte_count].copy_from_slice(&curr_line);
|
output_buf[i..i + line_byte_count].copy_from_slice(&curr_line);
|
||||||
} else if pixel_byte_count == 3 {
|
} else if pixel_byte_count == 3 {
|
||||||
for x in 0..line_byte_count {
|
for x in (0..line_byte_count).step_by(3) {
|
||||||
let loc = ((y * width) as usize + x) * 4;
|
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],
|
||||||
curr_line[x + 1],
|
curr_line[x + 1],
|
||||||
curr_line[x + 2],
|
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;
|
i += line_byte_count;
|
||||||
|
|
|
@ -4,14 +4,17 @@ mod compression;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod dynamic;
|
pub mod dynamic;
|
||||||
|
|
||||||
pub mod formats {
|
mod formats {
|
||||||
pub mod cz0;
|
pub(crate) mod cz0;
|
||||||
pub mod cz1;
|
pub(crate) mod cz1;
|
||||||
pub mod cz2;
|
pub(crate) mod cz2;
|
||||||
pub mod cz3;
|
pub(crate) mod cz3;
|
||||||
pub mod cz4;
|
pub(crate) mod cz4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use dynamic::DynamicCz;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use formats::cz0::Cz0Image;
|
pub use formats::cz0::Cz0Image;
|
||||||
|
|
|
@ -5,6 +5,8 @@ edition = "2021"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bimap = "0.6.3"
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,10 +6,7 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bimap = "0.6.3"
|
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
cz = { path = "../cz" }
|
cz = { path = "../cz" }
|
||||||
encoding_rs = "0.8.34"
|
|
||||||
fontdue = { version = "0.8.0", features = ["parallel"] }
|
fontdue = { version = "0.8.0", features = ["parallel"] }
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
walkdir = "2.5.0"
|
|
||||||
|
|
|
@ -1,175 +1,26 @@
|
||||||
mod font_generation;
|
mod font_generation;
|
||||||
|
|
||||||
use std::{fs::{self, File}, io::{self, Write}, path::Path};
|
use cz::{
|
||||||
use bimap::BiMap;
|
common::{CzHeader, CzVersion},
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
dynamic::DynamicCz
|
||||||
use cz::{common::default_palette, dynamic::DynamicCz};
|
};
|
||||||
use font_generation::load_font;
|
|
||||||
use image::{ColorType, DynamicImage, GenericImage, GenericImageView};
|
|
||||||
|
|
||||||
fn main() {
|
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();
|
gallery_cz.set_bitmap(new_bitmap.into_vec());
|
||||||
let fallback = load_font("NotoSans-Regular.ttf").unwrap();
|
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;
|
// Save the newly decoded CZ3 as another PNG as a test
|
||||||
const BASELINE: f32 = FONT_BOX * 0.84;
|
cz_image_test.save_as_png("CGGALLERY_CH01_003-MODIFIED.png").unwrap();
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue