mirror of
https://github.com/G2-Games/lbee-utils.git
synced 2025-04-19 07:12:55 -05:00
Moved to crate rgb
, moved cz::open()
out of DynamicCz
, made writing generic
This commit is contained in:
parent
9e6ba57dee
commit
4f2f192fa1
7 changed files with 99 additions and 63 deletions
|
@ -15,3 +15,6 @@ unsafe_code = "forbid"
|
||||||
[profile.production]
|
[profile.production]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = true
|
lto = true
|
||||||
|
strip = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
|
@ -14,3 +14,4 @@ byteorder = "1.5.0"
|
||||||
thiserror = "1.0.59"
|
thiserror = "1.0.59"
|
||||||
imagequant = "4.3.1"
|
imagequant = "4.3.1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||||
|
rgb = "0.8.40"
|
||||||
|
|
|
@ -2,26 +2,39 @@ use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::{Read, Seek},
|
io::{Read, Seek},
|
||||||
};
|
};
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
use imagequant::Attributes;
|
use imagequant::Attributes;
|
||||||
|
use rgb::{ComponentSlice, RGBA8};
|
||||||
|
|
||||||
use crate::common::{CommonHeader, CzError};
|
use crate::common::{CommonHeader, CzError};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
/// A palette of RGBA values for indexed color
|
||||||
pub struct Rgba(pub [u8; 4]);
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Palette {
|
||||||
|
colors: Vec<RGBA8>
|
||||||
|
}
|
||||||
|
|
||||||
impl From<[u8; 4]> for Rgba {
|
impl Palette {
|
||||||
fn from(value: [u8; 4]) -> Self {
|
/// Get the list of colors from the palette
|
||||||
Self([value[0], value[1], value[2], value[3]])
|
pub fn colors(&self) -> &Vec<RGBA8> {
|
||||||
|
&self.colors
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume the palette, returning a list of colors
|
||||||
|
pub fn into_colors(self) -> Vec<RGBA8> {
|
||||||
|
self.colors
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.colors.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, index: usize) -> Option<&RGBA8> {
|
||||||
|
self.colors.get(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// Get a palette from the input stream, beginning where the palette starts.
|
||||||
pub struct Palette {
|
|
||||||
pub colors: Vec<Rgba>
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
||||||
|
@ -37,15 +50,15 @@ pub fn get_palette<T: Seek + ReadBytesExt + Read>(
|
||||||
Ok(Palette { colors: colormap })
|
Ok(Palette { colors: colormap })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take a bitmap of indicies, and map a given palette to it, returning a new
|
/// Takes an indexed color bitmap and maps a given palette to it, returning an
|
||||||
/// RGBA bitmap
|
/// RGBA bitmap.
|
||||||
pub fn apply_palette(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> {
|
pub fn indexed_to_rgba(input: &[u8], palette: &Palette) -> 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.colors.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.0);
|
output_map.extend_from_slice(color.as_slice());
|
||||||
} else {
|
} else {
|
||||||
return Err(CzError::PaletteError);
|
return Err(CzError::PaletteError);
|
||||||
}
|
}
|
||||||
|
@ -54,6 +67,7 @@ pub fn apply_palette(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError
|
||||||
Ok(output_map)
|
Ok(output_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Takes an RGBA bitmap and maps the colors in it to indices of an indexed bitmap.
|
||||||
pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> {
|
pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> 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();
|
||||||
|
@ -62,7 +76,11 @@ pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
|
||||||
let value = match cache.get(rgba) {
|
let value = match cache.get(rgba) {
|
||||||
Some(val) => *val,
|
Some(val) => *val,
|
||||||
None => {
|
None => {
|
||||||
let value = palette.colors.iter().position(|e| e.0 == rgba).unwrap_or_default() as u8;
|
let value = palette.colors()
|
||||||
|
.iter()
|
||||||
|
.position(|e| e.as_slice() == rgba)
|
||||||
|
.unwrap_or_default() as u8;
|
||||||
|
|
||||||
cache.insert(rgba, value);
|
cache.insert(rgba, value);
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
@ -74,10 +92,11 @@ pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
|
||||||
Ok(output_map)
|
Ok(output_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate and a bitmap for a given input of RGBA pixels.
|
||||||
pub fn indexed_gen_palette(
|
pub fn indexed_gen_palette(
|
||||||
input: &[u8],
|
input: &[u8],
|
||||||
header: &CommonHeader,
|
header: &CommonHeader,
|
||||||
) -> Result<(Vec<u8>, Vec<Rgba>), CzError> {
|
) -> Result<(Vec<u8>, Vec<RGBA8>), CzError> {
|
||||||
let size = (header.width() as u32 * header.height() as u32) * 4;
|
let size = (header.width() as u32 * header.height() as u32) * 4;
|
||||||
|
|
||||||
let mut buf: Vec<u8> = vec![0; size as usize];
|
let mut buf: Vec<u8> = vec![0; size as usize];
|
||||||
|
@ -102,22 +121,22 @@ pub fn indexed_gen_palette(
|
||||||
|
|
||||||
let (palette, indicies) = quant_result.remapped(&mut image).unwrap();
|
let (palette, indicies) = quant_result.remapped(&mut image).unwrap();
|
||||||
|
|
||||||
let gen_palette: Vec<Rgba> = palette
|
let gen_palette: Vec<RGBA8> = palette
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| Rgba([c.r, c.g, c.b, c.a]))
|
.map(|c| RGBA8::from([c.r, c.g, c.b, c.a]))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut output_palette = vec![Rgba([0, 0, 0, 0]); 256];
|
let mut output_palette = vec![RGBA8::from([0, 0, 0, 0]); 256];
|
||||||
output_palette[0..gen_palette.len()].copy_from_slice(&gen_palette);
|
output_palette[0..gen_palette.len()].copy_from_slice(&gen_palette);
|
||||||
|
|
||||||
Ok((indicies, output_palette))
|
Ok((indicies, output_palette))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _default_palette() -> Vec<Rgba> {
|
pub fn _default_palette() -> Vec<RGBA8> {
|
||||||
let mut colormap = Vec::new();
|
let mut colormap = Vec::new();
|
||||||
|
|
||||||
for i in 0..=0xFF {
|
for i in 0..=0xFF {
|
||||||
colormap.push(Rgba([0xFF, 0xFF, 0xFF, i]))
|
colormap.push(RGBA8::from([0xFF, 0xFF, 0xFF, i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
colormap
|
colormap
|
||||||
|
|
|
@ -1,34 +1,30 @@
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
use rgb::ComponentSlice;
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
io::{BufWriter, Read, Seek, SeekFrom, Write},
|
||||||
path::Path,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
color::{apply_palette, get_palette, indexed_gen_palette, rgba_to_indexed, Palette},
|
color::{get_palette, indexed_gen_palette, indexed_to_rgba, rgba_to_indexed, Palette},
|
||||||
common::{CommonHeader, CzError, CzVersion, ExtendedHeader},
|
common::{CommonHeader, CzError, 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
|
/// A CZ# interface which can open and save any CZ file type.
|
||||||
/// convenience.
|
#[derive(Debug, Clone)]
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DynamicCz {
|
pub struct DynamicCz {
|
||||||
header_common: CommonHeader,
|
header_common: CommonHeader,
|
||||||
header_extended: Option<ExtendedHeader>,
|
header_extended: Option<ExtendedHeader>,
|
||||||
|
|
||||||
|
/// A palette of RGBA values for indexed color
|
||||||
palette: Option<Palette>,
|
palette: Option<Palette>,
|
||||||
|
|
||||||
|
/// 32bpp RGBA bitmap representation of the file contents
|
||||||
bitmap: Vec<u8>,
|
bitmap: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a CZ# file from anything which implements [`Read`] and [`Seek`]
|
/// Decode a CZ# file from anything which implements [`Read`] and [`Seek`]
|
||||||
///
|
///
|
||||||
/// The input must begin with the
|
/// The input must begin with the
|
||||||
|
@ -74,12 +70,11 @@ impl DynamicCz {
|
||||||
|
|
||||||
match header_common.depth() {
|
match header_common.depth() {
|
||||||
4 => {
|
4 => {
|
||||||
eprintln!("Files with a bit depth of 4 are not yet supported");
|
todo!("Files with a bit depth of 4 are not yet supported")
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
8 => {
|
8 => {
|
||||||
if let Some(palette) = &palette {
|
if let Some(palette) = &palette {
|
||||||
bitmap = apply_palette(&bitmap, palette)?;
|
bitmap = indexed_to_rgba(&bitmap, palette)?;
|
||||||
} else {
|
} else {
|
||||||
return Err(CzError::PaletteError);
|
return Err(CzError::PaletteError);
|
||||||
}
|
}
|
||||||
|
@ -111,20 +106,30 @@ impl DynamicCz {
|
||||||
/// Save the `DynamicCz` as a CZ# file. The format saved in is determined
|
/// Save the `DynamicCz` as a CZ# file. The format saved in is determined
|
||||||
/// from the format in the header. Check [`CommonHeader::set_version()`]
|
/// from the format in the header. Check [`CommonHeader::set_version()`]
|
||||||
/// to change the CZ# version.
|
/// to change the CZ# version.
|
||||||
pub fn save_as_cz<T: Into<std::path::PathBuf>>(&self, path: T) -> Result<(), CzError> {
|
pub fn save_as_cz<P: ?Sized + AsRef<std::path::Path>>(
|
||||||
let mut out_file = BufWriter::new(File::create(path.into())?);
|
&self,
|
||||||
|
path: &P,
|
||||||
|
) -> Result<(), CzError> {
|
||||||
|
let mut out_file = BufWriter::new(File::create(path.as_ref())?);
|
||||||
|
|
||||||
|
self.write(&mut out_file)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<T: Write + Seek>(&self, mut output: &mut T) -> Result<(), CzError> {
|
||||||
let mut header = *self.header();
|
let mut header = *self.header();
|
||||||
|
|
||||||
if header.version() == CzVersion::CZ2 {
|
if header.version() == CzVersion::CZ2 {
|
||||||
header.set_length(0x12)
|
header.set_length(0x12)
|
||||||
}
|
}
|
||||||
header.write_into(&mut out_file)?;
|
header.write_into(&mut output)?;
|
||||||
|
|
||||||
if header.version() == CzVersion::CZ2 {
|
if header.version() == CzVersion::CZ2 {
|
||||||
// CZ2 files have this odd section instead of an extended header...?
|
// CZ2 files have this odd section instead of an extended header...?
|
||||||
out_file.write_all(&[0, 0, 0])?;
|
output.write_all(&[0, 0, 0])?;
|
||||||
} else if let Some(ext) = self.header_extended {
|
} else if let Some(ext) = self.header_extended {
|
||||||
ext.write_into(&mut out_file)?;
|
ext.write_into(&mut output)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let output_bitmap;
|
let output_bitmap;
|
||||||
|
@ -139,8 +144,8 @@ impl DynamicCz {
|
||||||
// Use the existing palette to palette the image
|
// Use the existing palette to palette the image
|
||||||
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
|
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
|
||||||
|
|
||||||
for rgba in &pal.colors {
|
for rgba in pal.colors() {
|
||||||
out_file.write_all(&rgba.0)?;
|
output.write_all(rgba.as_slice())?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Generate a palette and corresponding indexed bitmap if there is none
|
// Generate a palette and corresponding indexed bitmap if there is none
|
||||||
|
@ -150,12 +155,7 @@ impl DynamicCz {
|
||||||
let palette = result.1;
|
let palette = result.1;
|
||||||
|
|
||||||
for rgba in palette {
|
for rgba in palette {
|
||||||
let mut rgba_clone = rgba.0;
|
output.write_all(rgba.as_slice())?;
|
||||||
if false {
|
|
||||||
// TODO: Make a toggle for this
|
|
||||||
rgba_clone[0..3].reverse();
|
|
||||||
}
|
|
||||||
out_file.write_all(&rgba_clone)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,11 +179,11 @@ impl DynamicCz {
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.header_common.version() {
|
match self.header_common.version() {
|
||||||
CzVersion::CZ0 => cz0::encode(&mut out_file, &output_bitmap)?,
|
CzVersion::CZ0 => cz0::encode(&mut output, &output_bitmap)?,
|
||||||
CzVersion::CZ1 => cz1::encode(&mut out_file, &output_bitmap)?,
|
CzVersion::CZ1 => cz1::encode(&mut output, &output_bitmap)?,
|
||||||
CzVersion::CZ2 => cz2::encode(&mut out_file, &output_bitmap)?,
|
CzVersion::CZ2 => cz2::encode(&mut output, &output_bitmap)?,
|
||||||
CzVersion::CZ3 => cz3::encode(&mut out_file, &output_bitmap, &self.header_common)?,
|
CzVersion::CZ3 => cz3::encode(&mut output, &output_bitmap, &self.header_common)?,
|
||||||
CzVersion::CZ4 => cz4::encode(&mut out_file, &output_bitmap, &self.header_common)?,
|
CzVersion::CZ4 => cz4::encode(&mut output, &output_bitmap, &self.header_common)?,
|
||||||
CzVersion::CZ5 => todo!(),
|
CzVersion::CZ5 => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ impl DynamicCz {
|
||||||
/// which is the highest encountered in CZ# files, therefore saving them
|
/// which is the highest encountered in CZ# files, therefore saving them
|
||||||
/// as a PNG of the same or better quality is lossless.
|
/// as a PNG of the same or better quality is lossless.
|
||||||
#[cfg(feature = "png")]
|
#[cfg(feature = "png")]
|
||||||
pub fn save_as_png<P: ?Sized + AsRef<Path>>(
|
pub fn save_as_png<P: ?Sized + AsRef<std::path::Path>>(
|
||||||
&self,
|
&self,
|
||||||
path: &P,
|
path: &P,
|
||||||
) -> Result<(), image::error::EncodingError> {
|
) -> Result<(), image::error::EncodingError> {
|
||||||
|
|
|
@ -13,6 +13,16 @@ mod formats {
|
||||||
pub(crate) mod cz4;
|
pub(crate) mod cz4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use std::{io::BufReader, path::Path};
|
||||||
|
use common::CzError;
|
||||||
|
|
||||||
|
/// Open a CZ# file from a path
|
||||||
|
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<DynamicCz, CzError> {
|
||||||
|
let mut img_file = BufReader::new(std::fs::File::open(path)?);
|
||||||
|
|
||||||
|
DynamicCz::decode(&mut img_file)
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use dynamic::DynamicCz;
|
pub use dynamic::DynamicCz;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
let cz_file = cz::open("test_file.cz3").unwrap();
|
||||||
|
|
||||||
|
cz_file.save_as_png("test_file.png").unwrap();
|
||||||
|
|
||||||
|
cz_file.save_as_cz("test_file.cz").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use cz::DynamicCz;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use clap::{error::ErrorKind, Error, Parser, Subcommand};
|
use clap::{error::ErrorKind, Error, Parser, Subcommand};
|
||||||
|
|
||||||
|
@ -87,7 +86,7 @@ fn main() {
|
||||||
let mut final_path = output.clone().unwrap();
|
let mut final_path = output.clone().unwrap();
|
||||||
final_path.push(filename);
|
final_path.push(filename);
|
||||||
|
|
||||||
let cz = match DynamicCz::open(&path) {
|
let cz = match cz::open(&path) {
|
||||||
Ok(cz) => cz,
|
Ok(cz) => cz,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Error::raw(
|
Error::raw(
|
||||||
|
@ -101,7 +100,7 @@ fn main() {
|
||||||
cz.save_as_png(&final_path).unwrap();
|
cz.save_as_png(&final_path).unwrap();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let cz = DynamicCz::open(input).unwrap();
|
let cz = cz::open(input).unwrap();
|
||||||
|
|
||||||
if let Some(output) = output {
|
if let Some(output) = output {
|
||||||
cz.save_as_png(output).unwrap();
|
cz.save_as_png(output).unwrap();
|
||||||
|
@ -209,7 +208,7 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
|
||||||
let repl_img = image::open(&replacement_path)?.to_rgba8();
|
let repl_img = image::open(&replacement_path)?.to_rgba8();
|
||||||
|
|
||||||
// Open the original CZ file
|
// Open the original CZ file
|
||||||
let mut cz = DynamicCz::open(&path)?;
|
let mut cz = cz::open(&path)?;
|
||||||
|
|
||||||
// Set CZ header parameters and the new bitmap
|
// Set CZ header parameters and the new bitmap
|
||||||
cz.header_mut().set_width(repl_img.width() as u16);
|
cz.header_mut().set_width(repl_img.width() as u16);
|
||||||
|
|
Loading…
Reference in a new issue