use std::io::{Cursor, Write};
use std::sync::Arc;
use crate::{Error, Result};
#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)]
#[serde(remote = "Self", deny_unknown_fields)]
#[schemars(rename = "Bitmap")]
pub struct Image {
pub size: [u32; 2],
pub rgba: Arc<[u8]>,
}
impl<'de> serde::Deserialize<'de> for Image {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Image, D::Error> {
let Image { size, rgba } = Image::deserialize(deserializer)?;
Image::new(size, rgba).map_err(serde::de::Error::custom)
}
}
impl Image {
pub fn new(size: [u32; 2], rgba: impl Into<Arc<[u8]>>) -> Result<Image> {
let rgba = rgba.into();
let expected = u64::from(size[0]).checked_mul(u64::from(size[1])).and_then(|pixels| pixels.checked_mul(4));
if size.contains(&0) || expected != Some(rgba.len() as u64) {
return Err(Error::invalid("an image needs positive dimensions and exactly four bytes per pixel"));
}
Ok(Image { size, rgba })
}
pub fn encode(&self, format: ImageFormat) -> Result<Vec<u8>> {
let mut out = Vec::new();
self.write(format, &mut out)?;
Ok(out)
}
pub fn write(&self, format: ImageFormat, mut output: impl Write) -> Result<()> {
Self::new(self.size, self.rgba.clone())?;
let [width, height] = self.size;
let failed = |error: &dyn std::fmt::Display| Error::Io(format!("cannot write {format}: {error}").into());
match format {
ImageFormat::Png => {
let mut encoder = png::Encoder::new(output, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_source_srgb(png::SrgbRenderingIntent::Perceptual);
encoder.set_compression(png::Compression::Fast);
let mut writer = encoder.write_header().map_err(|e| failed(&e))?;
writer.write_image_data(&self.rgba).map_err(|e| failed(&e))?;
writer.finish().map_err(|e| failed(&e))
}
ImageFormat::Jpeg { quality } => {
if !(1..=100).contains(&quality) {
return Err(Error::invalid("a JPEG's quality runs from 1 to 100").at("quality"));
}
let side = |v: u32| u16::try_from(v).map_err(|_| failed(&"a JPEG is at most 65535 pixels a side"));
let mut bytes = Vec::new();
jpeg_encoder::Encoder::new(&mut bytes, quality)
.encode(&self.rgba, side(width)?, side(height)?, jpeg_encoder::ColorType::Rgba)
.map_err(|e| failed(&e))?;
output.write_all(&bytes).map_err(|e| failed(&e))
}
ImageFormat::WebP => image_webp::WebPEncoder::new(output)
.encode(&self.rgba, width, height, image_webp::ColorType::Rgba8)
.map_err(|e| failed(&e)),
}
}
pub fn from_png(bytes: &[u8]) -> Result<Image> {
let failed = |e: png::DecodingError| Error::invalid(format!("not a PNG image: {e}"));
let mut decoder = png::Decoder::new(Cursor::new(bytes));
decoder.set_transformations(png::Transformations::normalize_to_color8());
let mut reader = decoder.read_info().map_err(failed)?;
let mut buffer = vec![0; reader.output_buffer_size().ok_or_else(|| Error::invalid("a PNG too large"))?];
let info = reader.next_frame(&mut buffer).map_err(failed)?;
let texels = (info.width * info.height) as usize;
let rgba: Vec<u8> = match info.color_type {
png::ColorType::Rgba => buffer[..texels * 4].to_vec(),
png::ColorType::Rgb => {
buffer[..texels * 3].as_chunks::<3>().0.iter().flat_map(|c| [c[0], c[1], c[2], 255]).collect()
}
png::ColorType::GrayscaleAlpha => {
buffer[..texels * 2].as_chunks::<2>().0.iter().flat_map(|c| [c[0], c[0], c[0], c[1]]).collect()
}
png::ColorType::Grayscale => buffer[..texels].iter().flat_map(|&g| [g, g, g, 255]).collect(),
png::ColorType::Indexed => return Err(Error::invalid("a PNG palette was not expanded")),
};
Image::new([info.width, info.height], rgba)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ImageFormat {
#[default]
Png,
Jpeg {
quality: u8,
},
WebP,
}
impl ImageFormat {
pub fn of_extension(extension: &str) -> Option<ImageFormat> {
match extension.to_ascii_lowercase().as_str() {
"png" => Some(ImageFormat::Png),
"jpg" | "jpeg" => Some(ImageFormat::Jpeg { quality: 90 }),
"webp" => Some(ImageFormat::WebP),
_ => None,
}
}
}
impl std::fmt::Display for ImageFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
ImageFormat::Png => "PNG",
ImageFormat::Jpeg { .. } => "JPEG",
ImageFormat::WebP => "WebP",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn png_holds_what_it_was_given() {
let rgba: Vec<u8> = (0..3 * 2 * 4).map(|i| (i * 11) as u8).collect();
let image = Image::new([3, 2], rgba).expect("whole");
let back = Image::from_png(&image.encode(ImageFormat::Png).expect("written")).expect("a PNG");
assert_eq!(back, image);
}
#[test]
fn every_format_writes_its_file() {
let rgba: Vec<u8> = (0..16 * 8 * 4).map(|i| (i * 7) as u8).collect();
let image = Image::new([16, 8], rgba).expect("whole");
let jpeg = image.encode(ImageFormat::Jpeg { quality: 90 }).expect("written");
assert_eq!(jpeg[..3], [0xff, 0xd8, 0xff], "a JPEG starts with its marker");
let webp = image.encode(ImageFormat::WebP).expect("written");
assert!(webp.starts_with(b"RIFF") && &webp[8..12] == b"WEBP", "a WebP is a RIFF file");
assert_eq!(ImageFormat::of_extension("JPG"), Some(ImageFormat::Jpeg { quality: 90 }));
assert_eq!(ImageFormat::of_extension("gif"), None);
}
#[test]
fn an_image_holds_four_bytes_a_texel() {
assert!(Image::new([2, 2], vec![0; 15]).is_err());
assert!(Image::from_png(b"not a png").is_err());
assert!(Image::new([0, 0], Vec::new()).is_err());
assert!(Image::new([u32::MAX, u32::MAX], Vec::new()).is_err());
}
#[test]
fn writing_reports_stream_failures() {
struct Broken;
impl Write for Broken {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::ErrorKind::BrokenPipe.into())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let image = Image::new([1, 1], vec![0; 4]).unwrap();
assert!(image.write(ImageFormat::Png, Broken).is_err());
}
}