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::elements::texture_page::BZ2_QOI_HEADER;
use crate::wad::serialize::builder::DataBuilder;
#[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(raw_png_data))
}
#[must_use]
pub(super) const fn from_qoi(raw_qoi_data: Vec<u8>) -> Self {
Self(Img::Qoi(raw_qoi_data))
}
#[must_use]
pub(super) const fn from_bz2_qoi(raw_bz2_qoi_data: Vec<u8>, header: BZip2QoiHeader) -> Self {
Self(Img::Bz2Qoi(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).context("converting PNG image to DynamicImage")?,
Img::Qoi(raw) => qoi::decode(raw).context("converting QOI image to DynamicImage")?,
Img::Bz2Qoi(raw, _) => {
bz2::decode_image(raw).context("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)
.with_context(|| format!("converting GMImage from {old} to {format}"))?;
Ok(true)
}
pub fn deserialize(&mut self) -> Result<&DynamicImage> {
self.change_format(Format::Dyn)?;
Ok(self.dynamic_image_ref().unwrap())
}
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(data, bz2_header);
return Ok(());
}
(Img::Bz2Qoi(raw_data, _), Format::Qoi) => {
let data: Vec<u8> = bz2::decompress(raw_data)?;
self.0 = Img::Qoi(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(png::encode(&dyn_img)?),
Format::Qoi => Img::Qoi(qoi::encode(&dyn_img)?),
Format::Bz2Qoi => {
let (data, header) = bz2::encode_image(&dyn_img)?;
Img::Bz2Qoi(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.is_version_at_least((2022, 2));
if is_qoi && !is_qoi_eligible {
bail!("Cannot serialize QOI images before GM 2022.2");
}
match &self.0 {
Img::Dyn(dyn_img) => {
write_dyn_img(dyn_img, builder).context("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).context("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(Clone)]
enum Img {
Dyn(DynamicImage),
Png(Vec<u8>),
Qoi(Vec<u8>),
Bz2Qoi(Vec<u8>, BZip2QoiHeader),
}
impl fmt::Debug for Img {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dyn(_) => f.debug_struct("Dyn").finish_non_exhaustive(),
Self::Png(_) => f.debug_struct("Png").finish_non_exhaustive(),
Self::Qoi(_) => f.debug_struct("Qoi").finish_non_exhaustive(),
Self::Bz2Qoi(_, header) => f
.debug_tuple("Bz2Qoi")
.field(header)
.finish_non_exhaustive(),
}
}
}
fn write_dyn_img(dyn_img: &DynamicImage, builder: &mut DataBuilder) -> Result<()> {
if builder.is_version_at_least((2022, 1)) {
qoi::build(dyn_img, builder).context("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(())
}