#[cfg(feature = "codecs")]
pub mod jpeg_io;
#[cfg(feature = "codecs")]
pub mod png_io;
#[cfg(feature = "codecs")]
pub mod qoi_io;
#[cfg(feature = "codecs")]
#[allow(
non_upper_case_globals,
non_camel_case_types,
non_snake_case,
dead_code
)]
pub mod spng_ffi;
#[cfg(feature = "codecs")]
#[allow(
non_upper_case_globals,
non_camel_case_types,
non_snake_case,
dead_code
)]
mod turbojpeg_ffi;
pub mod yiq;
#[cfg(feature = "codecs")]
use rayon::prelude::*;
use std::path::Path;
#[cfg(feature = "codecs")]
pub use jpeg_io::{decode_jpeg, load_jpeg, load_jpegs, save_jpeg};
#[cfg(feature = "codecs")]
pub use png_io::{
decode_png, encode_png, load_png, load_pngs, save_png, save_png_with_compression,
};
#[cfg(feature = "codecs")]
pub use qoi_io::{decode_qoi, load_qoi, load_qois, save_qoi};
pub struct Image {
pub data: Vec<u8>, pub width: u32,
pub height: u32,
}
impl Image {
pub fn new(width: u32, height: u32) -> Self {
Self {
data: vec![0u8; (width * height * 4) as usize],
width,
height,
}
}
pub fn new_uninit(width: u32, height: u32) -> Self {
let size = (width as usize) * (height as usize) * 4;
let mut data: Vec<u8> = Vec::with_capacity(size);
unsafe {
data.set_len(size);
}
Self {
data,
width,
height,
}
}
#[inline]
pub fn as_u32(&self) -> &[u32] {
bytemuck::cast_slice(&self.data)
}
#[inline]
pub fn as_u32_mut(&mut self) -> &mut [u32] {
bytemuck::cast_slice_mut(&mut self.data)
}
#[inline]
pub fn get_pixel(&self, x: u32, y: u32) -> u32 {
let idx = (y * self.width + x) as usize;
self.as_u32()[idx]
}
#[inline]
pub fn set_pixel(&mut self, x: u32, y: u32, pixel: u32) {
let idx = (y * self.width + x) as usize;
self.as_u32_mut()[idx] = pixel;
}
}
#[derive(Debug)]
pub enum ImageError {
Io(std::io::Error),
Png(String),
Jpeg(String),
Qoi(String),
UnsupportedFormat(String),
}
impl std::fmt::Display for ImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageError::Io(e) => write!(f, "IO error: {}", e),
ImageError::Png(e) => write!(f, "PNG error: {}", e),
ImageError::Jpeg(e) => write!(f, "JPEG error: {}", e),
ImageError::Qoi(e) => write!(f, "QOI error: {}", e),
ImageError::UnsupportedFormat(e) => write!(f, "Unsupported format: {}", e),
}
}
}
impl std::error::Error for ImageError {}
impl From<std::io::Error> for ImageError {
fn from(e: std::io::Error) -> Self {
ImageError::Io(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Png,
Jpeg,
Qoi,
}
impl ImageFormat {
pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
let ext = path.as_ref().extension()?.to_str()?.to_lowercase();
match ext.as_str() {
"png" => Some(ImageFormat::Png),
"jpg" | "jpeg" => Some(ImageFormat::Jpeg),
"qoi" => Some(ImageFormat::Qoi),
_ => None,
}
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.starts_with(b"\x89PNG\r\n\x1a\n") {
return Some(ImageFormat::Png);
}
if data.starts_with(&[0xff, 0xd8, 0xff]) {
return Some(ImageFormat::Jpeg);
}
if data.starts_with(b"qoif") {
return Some(ImageFormat::Qoi);
}
None
}
pub fn as_str(&self) -> &'static str {
match self {
ImageFormat::Png => "png",
ImageFormat::Jpeg => "jpeg",
ImageFormat::Qoi => "qoi",
}
}
}
#[cfg(feature = "codecs")]
pub fn blazediff_png_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var("BLAZEDIFF_PNG_ENABLED").is_ok_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
})
}
#[cfg(not(feature = "codecs"))]
pub fn blazediff_png_enabled() -> bool {
false
}
#[cfg(feature = "codecs")]
fn unsupported_path<P: AsRef<Path>>(path: P) -> ImageError {
ImageError::UnsupportedFormat(path.as_ref().display().to_string())
}
#[cfg(feature = "codecs")]
pub fn load_image<P: AsRef<Path>>(path: P) -> Result<Image, ImageError> {
match ImageFormat::from_path(&path).ok_or_else(|| unsupported_path(&path))? {
ImageFormat::Png => load_png(path),
ImageFormat::Jpeg => load_jpeg(path),
ImageFormat::Qoi => load_qoi(path),
}
}
#[cfg(feature = "codecs")]
pub fn load_image_pair<P1: AsRef<Path> + Sync, P2: AsRef<Path> + Sync>(
path1: P1,
path2: P2,
) -> Result<(Image, Image), ImageError> {
let fmt1 = ImageFormat::from_path(&path1).ok_or_else(|| unsupported_path(&path1))?;
let fmt2 = ImageFormat::from_path(&path2).ok_or_else(|| unsupported_path(&path2))?;
if fmt1 == fmt2 {
return match fmt1 {
ImageFormat::Png => load_pngs(&path1, &path2),
ImageFormat::Jpeg => load_jpegs(&path1, &path2),
ImageFormat::Qoi => load_qois(&path1, &path2),
};
}
let results: Vec<Result<Image, ImageError>> = [
(path1.as_ref().to_path_buf(), fmt1),
(path2.as_ref().to_path_buf(), fmt2),
]
.par_iter()
.map(|(path, fmt)| match fmt {
ImageFormat::Png => load_png(path),
ImageFormat::Jpeg => load_jpeg(path),
ImageFormat::Qoi => load_qoi(path),
})
.collect();
let mut iter = results.into_iter();
Ok((iter.next().unwrap()?, iter.next().unwrap()?))
}
#[cfg(feature = "codecs")]
pub fn decode_image(data: &[u8]) -> Result<Image, ImageError> {
match ImageFormat::from_bytes(data) {
Some(ImageFormat::Png) => decode_png(data),
Some(ImageFormat::Jpeg) => decode_jpeg(data),
Some(ImageFormat::Qoi) => decode_qoi(data),
None => Err(ImageError::UnsupportedFormat(
"unrecognized image buffer, expected PNG, JPEG or QOI".to_string(),
)),
}
}
#[cfg(feature = "codecs")]
pub fn decode_image_pair(image1: &[u8], image2: &[u8]) -> Result<(Image, Image), ImageError> {
let (result1, result2) = rayon::join(|| decode_image(image1), || decode_image(image2));
Ok((result1?, result2?))
}
#[cfg(feature = "codecs")]
pub fn save_image<P: AsRef<Path>>(
image: &Image,
path: P,
compression: u8,
quality: u8,
) -> Result<(), ImageError> {
match ImageFormat::from_path(&path).ok_or_else(|| unsupported_path(&path))? {
ImageFormat::Png => save_png_with_compression(image, path, compression),
ImageFormat::Jpeg => save_jpeg(image, path, quality),
ImageFormat::Qoi => save_qoi(image, path),
}
}
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
pub fn decode_spng_reference(data: &[u8]) -> Result<Image, ImageError> {
png_io::decode_spng(data)
}
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
pub fn decode_spng_reference_fmt(
data: &[u8],
fmt: std::os::raw::c_int,
flags: std::os::raw::c_int,
) -> Result<(u32, u32, u8, u8, Vec<u8>), ImageError> {
png_io::decode_spng_fmt(data, fmt, flags)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_format_from_extension() {
assert_eq!(ImageFormat::from_path("a.PNG"), Some(ImageFormat::Png));
assert_eq!(ImageFormat::from_path("a.jpg"), Some(ImageFormat::Jpeg));
assert_eq!(ImageFormat::from_path("a.jpeg"), Some(ImageFormat::Jpeg));
assert_eq!(ImageFormat::from_path("a.qoi"), Some(ImageFormat::Qoi));
assert_eq!(ImageFormat::from_path("a.webp"), None);
assert_eq!(ImageFormat::from_path("noext"), None);
}
#[test]
fn detects_format_from_magic_bytes() {
assert_eq!(
ImageFormat::from_bytes(b"\x89PNG\r\n\x1a\n\x00"),
Some(ImageFormat::Png)
);
assert_eq!(
ImageFormat::from_bytes(&[0xff, 0xd8, 0xff, 0xe0]),
Some(ImageFormat::Jpeg)
);
assert_eq!(ImageFormat::from_bytes(b"qoifXXXX"), Some(ImageFormat::Qoi));
assert_eq!(ImageFormat::from_bytes(b"RIFF"), None);
assert_eq!(ImageFormat::from_bytes(b""), None);
}
#[test]
fn error_display_strings_are_stable() {
assert_eq!(
ImageError::Png("boom".into()).to_string(),
"PNG error: boom"
);
assert_eq!(
ImageError::Jpeg("boom".into()).to_string(),
"JPEG error: boom"
);
assert_eq!(
ImageError::Qoi("boom".into()).to_string(),
"QOI error: boom"
);
assert_eq!(
ImageError::UnsupportedFormat("a.webp".into()).to_string(),
"Unsupported format: a.webp"
);
}
}