mod bz2;
mod png;
mod qoi;
use std::borrow::Cow;
use std::fmt;
pub use bz2::BZip2QoiHeader;
use image::DynamicImage;
use crate::prelude::*;
use crate::wad::Blob;
use crate::wad::build::builder::DataBuilder;
use crate::wad::elem::texture_page::BZ2_QOI_HEADER;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format {
Dyn,
Png,
Qoi,
Bz2Qoi,
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let string: &str = match self {
Self::Dyn => "Dynamic Image",
Self::Png => "PNG",
Self::Qoi => "QOI",
Self::Bz2Qoi => "BZip2 QOI",
};
f.write_str(string)
}
}
#[derive(Debug, Clone)]
pub struct GMImage(Img);
impl GMImage {
#[must_use]
pub const fn from_dynamic_image(dynamic_image: DynamicImage) -> Self {
Self(Img::Dyn(dynamic_image))
}
#[must_use]
pub(super) const fn from_png(raw_png_data: Vec<u8>) -> Self {
Self(Img::Png(Blob(raw_png_data)))
}
#[must_use]
pub(super) const fn from_qoi(raw_qoi_data: Vec<u8>) -> Self {
Self(Img::Qoi(Blob(raw_qoi_data)))
}
#[must_use]
pub(super) const fn from_bz2_qoi(raw_bz2_qoi_data: Vec<u8>, header: BZip2QoiHeader) -> Self {
Self(Img::Bz2Qoi(Blob(raw_bz2_qoi_data), header))
}
#[must_use]
pub const fn format(&self) -> Format {
match self.0 {
Img::Dyn(_) => Format::Dyn,
Img::Png(_) => Format::Png,
Img::Qoi(_) => Format::Qoi,
Img::Bz2Qoi(_, _) => Format::Bz2Qoi,
}
}
#[must_use]
pub const fn is_dynamic_image(&self) -> bool {
matches!(self.format(), Format::Dyn)
}
pub fn to_dynamic_image(&'_ self) -> Result<Cow<'_, DynamicImage>> {
let image: DynamicImage = match &self.0 {
Img::Dyn(dyn_img) => return Ok(Cow::Borrowed(dyn_img)),
Img::Png(raw) => png::decode(raw).ctx("converting PNG image to DynamicImage")?,
Img::Qoi(raw) => qoi::decode(raw).ctx("converting QOI image to DynamicImage")?,
Img::Bz2Qoi(raw, _) => {
bz2::decode_image(raw).ctx("converting Bz2Qoi image to DynamicImage")?
}
};
Ok(Cow::Owned(image))
}
#[must_use]
pub const fn dynamic_image_ref(&self) -> Option<&DynamicImage> {
match &self.0 {
Img::Dyn(dynamic_image) => Some(dynamic_image),
_ => None,
}
}
#[must_use]
pub const fn dynamic_image_mut(&mut self) -> Option<&mut DynamicImage> {
match &mut self.0 {
Img::Dyn(dynamic_image) => Some(dynamic_image),
_ => None,
}
}
pub fn change_format(&mut self, format: Format) -> Result<bool> {
let old = self.format();
if old == format {
return Ok(false);
}
self.change_format_(format)
.ctx(|| format!("converting GMImage from {old} to {format}"))?;
Ok(true)
}
pub fn deserialize(&mut self) -> Result<&DynamicImage> {
self.change_format(Format::Dyn)?;
match &self.0 {
Img::Dyn(dynamic_image) => Ok(dynamic_image),
_ => unreachable!("image was just converted to Format::Dyn"),
}
}
fn change_format_(&mut self, format: Format) -> Result<()> {
match (&self.0, format) {
(Img::Qoi(raw_data), Format::Bz2Qoi) => {
let qoi_header = qoi::read_header(raw_data)?;
let size = Some(raw_data.len() as u32);
let bz2_header = BZip2QoiHeader::new(qoi_header.width, qoi_header.height, size);
let data: Vec<u8> = bz2::compress(raw_data)?;
self.0 = Img::Bz2Qoi(Blob(data), bz2_header);
return Ok(());
}
(Img::Bz2Qoi(raw_data, _), Format::Qoi) => {
let data: Vec<u8> = bz2::decompress(raw_data)?;
self.0 = Img::Qoi(Blob(data));
return Ok(());
}
_ => {}
}
let dyn_img = self.to_dynamic_image()?;
let new_image = match format {
Format::Dyn => Img::Dyn(dyn_img.into_owned()),
Format::Png => Img::Png(Blob(png::encode(&dyn_img)?)),
Format::Qoi => Img::Qoi(Blob(qoi::encode(&dyn_img)?)),
Format::Bz2Qoi => {
let (data, header) = bz2::encode_image(&dyn_img)?;
Img::Bz2Qoi(Blob(data), header)
}
};
self.0 = new_image;
Ok(())
}
pub(crate) fn optimize_memory(&mut self) -> usize {
fn shrink(buffer: &mut Vec<u8>) -> usize {
let before = buffer.capacity();
buffer.shrink_to_fit();
let after = buffer.capacity();
before - after
}
match &mut self.0 {
Img::Dyn(_) => 0, Img::Png(buffer) | Img::Qoi(buffer) | Img::Bz2Qoi(buffer, _) => shrink(buffer),
}
}
pub(super) fn serialize(&self, builder: &mut DataBuilder) -> Result<()> {
let is_qoi = matches!(self.0, Img::Qoi(_) | Img::Bz2Qoi(_, _));
let is_qoi_eligible = builder.version() >= (2022, 1);
if is_qoi && !is_qoi_eligible {
bail!("Cannot serialize QOI images before GM 2022.1");
}
match &self.0 {
Img::Dyn(dyn_img) => {
write_dyn_img(dyn_img, builder).ctx("serializing DynamicImage")?;
}
Img::Png(raw_png_data) => builder.write_bytes(raw_png_data),
Img::Qoi(raw_qoi_data) => builder.write_bytes(raw_qoi_data),
Img::Bz2Qoi(raw_bz2_qoi_data, header) => {
write_bz2qoi_header(header, builder).ctx("writing Bz2Qoi image header")?;
builder.write_bytes(raw_bz2_qoi_data);
}
}
Ok(())
}
}
impl PartialEq for GMImage {
fn eq(&self, other: &Self) -> bool {
let Ok(img1) = self.to_dynamic_image() else {
log::warn!("Deserialization failed while comparing GMImage");
return false;
};
let Ok(img2) = other.to_dynamic_image() else {
log::warn!("Deserialization failed while comparing GMImage");
return false;
};
img1 == img2
}
}
#[derive(Debug, Clone)]
enum Img {
Dyn(DynamicImage),
Png(Blob<Vec<u8>>),
Qoi(Blob<Vec<u8>>),
Bz2Qoi(Blob<Vec<u8>>, BZip2QoiHeader),
}
fn write_dyn_img(dyn_img: &DynamicImage, builder: &mut DataBuilder) -> Result<()> {
if builder.version() >= (2022, 1) {
qoi::build(dyn_img, builder).ctx("serializing DynamicImage as QOI")?;
return Ok(());
}
if !cfg!(feature = "png-image") {
bail!("Crate feature `png-image` is disabled and the game is too old to use QOI images.");
}
let data: Vec<u8> = png::encode(dyn_img)?;
builder.write_bytes(&data);
Ok(())
}
fn write_bz2qoi_header(header: &BZip2QoiHeader, builder: &mut DataBuilder) -> Result<()> {
builder.write_bytes(BZ2_QOI_HEADER);
builder.write_u16(header.width);
builder.write_u16(header.height);
builder.write_if_ver(&header.uncompressed_size, "Uncompressed Size", (2022, 5))?;
Ok(())
}