use std::io::Cursor;
use anyhow::{Result, bail};
use image::{DynamicImage, ImageFormat, ImageReader, Limits};
use versatiles_core::{Blob, TileFormat};
use versatiles_derive::context;
use crate::{avif, jpeg, png, webp};
#[context("encoding {}x{} {:?} as {:?} (q={:?}, e={:?})", image.width(), image.height(), image.color(), format, quality, effort)]
pub fn encode(image: &DynamicImage, format: TileFormat, quality: Option<u8>, effort: Option<u8>) -> Result<Blob> {
match format {
TileFormat::AVIF => avif::encode(image, quality, effort),
TileFormat::JPG => jpeg::encode(image, quality),
TileFormat::PNG => png::encode(image, effort),
TileFormat::WEBP => webp::encode(image, quality, effort),
_ => bail!("Unsupported format '{format}' for image encoding"),
}
}
#[context("decoding {:?} image ({} bytes)", format, blob.len())]
pub fn decode(blob: &Blob, format: TileFormat) -> Result<DynamicImage> {
match format {
TileFormat::AVIF => avif::blob2image(blob),
TileFormat::JPG => jpeg::blob2image(blob),
TileFormat::PNG => png::blob2image(blob),
TileFormat::WEBP => webp::blob2image(blob),
_ => bail!("Unsupported format '{format}' for image decoding"),
}
}
pub(crate) fn decode_limited(blob: &Blob, format: ImageFormat) -> Result<DynamicImage> {
const MAX_SIDE: u32 = 16_384;
let mut limits = Limits::default();
limits.max_image_width = Some(MAX_SIDE);
limits.max_image_height = Some(MAX_SIDE);
let mut reader = ImageReader::new(Cursor::new(blob.as_slice()));
reader.set_format(format);
reader.limits(limits);
reader.decode().map_err(Into::into)
}
#[cfg(test)]
mod limit_tests {
use super::*;
fn png_header(width: u32, height: u32) -> Blob {
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for byte in data {
crc ^= u32::from(*byte);
for _ in 0..8 {
crc = if crc & 1 == 1 {
(crc >> 1) ^ 0xedb8_8320
} else {
crc >> 1
};
}
}
!crc
}
let mut ihdr = b"IHDR".to_vec();
ihdr.extend_from_slice(&width.to_be_bytes());
ihdr.extend_from_slice(&height.to_be_bytes());
ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
let mut png = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
png.extend_from_slice(&13u32.to_be_bytes());
png.extend_from_slice(&ihdr);
png.extend_from_slice(&crc32(&ihdr).to_be_bytes());
Blob::from(png)
}
#[test]
fn oversized_dimensions_are_refused() {
let error = decode_limited(&png_header(65_535, 65_535), ImageFormat::Png).unwrap_err();
let message = format!("{error:#}").to_lowercase();
assert!(message.contains("limit"), "expected a limits error, got: {error:#}");
}
#[test]
fn ordinary_dimensions_pass_the_limits() {
let error = decode_limited(&png_header(256, 256), ImageFormat::Png).unwrap_err();
let message = format!("{error:#}").to_lowercase();
assert!(
!message.contains("limit"),
"dimensions 256x256 must not hit a limit: {error:#}"
);
}
}