use std::fmt;
use std::io::Cursor;
use std::path::Path;
use image::{codecs::png::PngDecoder, ColorType, DynamicImage, ImageDecoder};
use crate::image_io::buffer::{ColorSpace, ImageBuffer};
use crate::image_io::jpeg_detect;
const MIN_DIMENSION: u32 = 2000;
const MIN_HEADER_LEN: usize = 12;
const PNG_MAGIC: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
#[derive(Debug)]
pub enum ValidationError {
IoError(std::io::Error),
JpegDetected,
WebpDetected,
NotPng,
UnsupportedColorSpace {
found: String,
},
ImageTooSmall {
width: u32,
height: u32,
min: u32,
},
DecodingError(String),
JpegArtifactsDetected {
ratio: f32,
},
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::IoError(err) => write!(f, "failed to read the image file: {err}"),
ValidationError::JpegDetected => {
write!(
f,
"the file is a JPEG; only lossless PNG containers are supported"
)
}
ValidationError::WebpDetected => {
write!(
f,
"the file is a WebP; only lossless PNG containers are supported"
)
}
ValidationError::NotPng => write!(f, "the file is not a PNG image"),
ValidationError::UnsupportedColorSpace { found } => {
write!(f, "unsupported pixel layout: {found}")
}
ValidationError::ImageTooSmall { width, height, min } => write!(
f,
"image is {width}x{height}; both sides must be at least {min} pixels"
),
ValidationError::DecodingError(message) => {
write!(f, "failed to decode the PNG stream: {message}")
}
ValidationError::JpegArtifactsDetected { ratio } => write!(
f,
"image shows an 8x8 JPEG block structure (blocking ratio {ratio:.2}, a clean \
image scores about 1.00); use a container that was never JPEG-compressed"
),
}
}
}
impl std::error::Error for ValidationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ValidationError::IoError(err) => Some(err),
_ => None,
}
}
}
impl From<std::io::Error> for ValidationError {
fn from(err: std::io::Error) -> Self {
ValidationError::IoError(err)
}
}
struct RawBytes(Vec<u8>);
struct VerifiedPngFile(Vec<u8>);
struct DecodedPng {
pixels: Vec<u8>,
width: u32,
height: u32,
color_space: ColorSpace,
}
fn validate_magic_bytes(raw: RawBytes) -> Result<VerifiedPngFile, ValidationError> {
let bytes = raw.0;
if bytes.len() < MIN_HEADER_LEN {
return Err(ValidationError::NotPng);
}
if bytes[0..3] == [0xFF, 0xD8, 0xFF] {
return Err(ValidationError::JpegDetected);
}
if &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
return Err(ValidationError::WebpDetected);
}
if bytes[0..8] != PNG_MAGIC {
return Err(ValidationError::NotPng);
}
Ok(VerifiedPngFile(bytes))
}
fn decode_png(file: VerifiedPngFile) -> Result<DecodedPng, ValidationError> {
let decoder = PngDecoder::new(Cursor::new(file.0))
.map_err(|err| ValidationError::DecodingError(err.to_string()))?;
let (width, height) = decoder.dimensions();
if width < MIN_DIMENSION || height < MIN_DIMENSION {
return Err(ValidationError::ImageTooSmall {
width,
height,
min: MIN_DIMENSION,
});
}
let color_type = decoder.color_type();
let color_space = match color_type {
ColorType::Rgb8 => ColorSpace::Rgb8,
ColorType::Rgb16 => ColorSpace::Rgb16,
ColorType::Rgba8 => ColorSpace::Rgba8,
ColorType::L8 => ColorSpace::Luma8,
other => {
return Err(ValidationError::UnsupportedColorSpace {
found: format!("{other:?}"),
});
}
};
let decoded = DynamicImage::from_decoder(decoder)
.map_err(|err| ValidationError::DecodingError(err.to_string()))?;
let pixels = match color_space {
ColorSpace::Rgb8 => decoded.into_rgb8().into_raw(),
ColorSpace::Rgba8 => decoded.into_rgba8().into_raw(),
ColorSpace::Luma8 => decoded.into_luma8().into_raw(),
ColorSpace::Rgb16 => decoded
.into_rgb16()
.into_raw()
.into_iter()
.flat_map(u16::to_le_bytes)
.collect(),
};
Ok(DecodedPng {
pixels,
width,
height,
color_space,
})
}
fn validate_no_jpeg_artifacts(decoded: DecodedPng) -> Result<ImageBuffer, ValidationError> {
let DecodedPng {
pixels,
width,
height,
color_space,
} = decoded;
match jpeg_detect::detect_jpeg_artifacts(&pixels, width, height, color_space) {
Some(ratio) => Err(ValidationError::JpegArtifactsDetected { ratio }),
None => Ok(ImageBuffer::new(pixels, width, height, color_space)),
}
}
pub fn load_and_validate(path: &Path) -> Result<ImageBuffer, ValidationError> {
let raw = RawBytes(std::fs::read(path)?);
let verified = validate_magic_bytes(raw)?;
let decoded = decode_png(verified)?;
validate_no_jpeg_artifacts(decoded)
}