1#[cfg(feature = "codecs")]
23pub mod jpeg_io;
24#[cfg(feature = "codecs")]
25pub mod png_io;
26#[cfg(feature = "codecs")]
27pub mod qoi_io;
28#[cfg(feature = "codecs")]
29#[allow(
30 non_upper_case_globals,
31 non_camel_case_types,
32 non_snake_case,
33 dead_code
34)]
35pub mod spng_ffi;
36#[cfg(feature = "codecs")]
37#[allow(
38 non_upper_case_globals,
39 non_camel_case_types,
40 non_snake_case,
41 dead_code
42)]
43mod turbojpeg_ffi;
44pub mod yiq;
45
46#[cfg(feature = "codecs")]
47use rayon::prelude::*;
48use std::path::Path;
49
50#[cfg(feature = "codecs")]
51pub use jpeg_io::{decode_jpeg, load_jpeg, load_jpegs, save_jpeg};
52#[cfg(feature = "codecs")]
53pub use png_io::{
54 decode_png, encode_png, load_png, load_pngs, save_png, save_png_with_compression,
55};
56#[cfg(feature = "codecs")]
57pub use qoi_io::{decode_qoi, load_qoi, load_qois, save_qoi};
58
59pub struct Image {
61 pub data: Vec<u8>, pub width: u32,
63 pub height: u32,
64}
65
66impl Image {
67 pub fn new(width: u32, height: u32) -> Self {
68 Self {
69 data: vec![0u8; (width * height * 4) as usize],
70 width,
71 height,
72 }
73 }
74
75 pub fn new_uninit(width: u32, height: u32) -> Self {
92 let size = (width as usize) * (height as usize) * 4;
93 let mut data: Vec<u8> = Vec::with_capacity(size);
94 unsafe {
99 data.set_len(size);
100 }
101 Self {
102 data,
103 width,
104 height,
105 }
106 }
107
108 #[inline]
109 pub fn as_u32(&self) -> &[u32] {
110 bytemuck::cast_slice(&self.data)
111 }
112
113 #[inline]
114 pub fn as_u32_mut(&mut self) -> &mut [u32] {
115 bytemuck::cast_slice_mut(&mut self.data)
116 }
117
118 #[inline]
119 pub fn get_pixel(&self, x: u32, y: u32) -> u32 {
120 let idx = (y * self.width + x) as usize;
121 self.as_u32()[idx]
122 }
123
124 #[inline]
125 pub fn set_pixel(&mut self, x: u32, y: u32, pixel: u32) {
126 let idx = (y * self.width + x) as usize;
127 self.as_u32_mut()[idx] = pixel;
128 }
129}
130
131#[derive(Debug)]
138pub enum ImageError {
139 Io(std::io::Error),
140 Png(String),
141 Jpeg(String),
142 Qoi(String),
143 UnsupportedFormat(String),
144}
145
146impl std::fmt::Display for ImageError {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 match self {
149 ImageError::Io(e) => write!(f, "IO error: {}", e),
150 ImageError::Png(e) => write!(f, "PNG error: {}", e),
151 ImageError::Jpeg(e) => write!(f, "JPEG error: {}", e),
152 ImageError::Qoi(e) => write!(f, "QOI error: {}", e),
153 ImageError::UnsupportedFormat(e) => write!(f, "Unsupported format: {}", e),
154 }
155 }
156}
157
158impl std::error::Error for ImageError {}
159
160impl From<std::io::Error> for ImageError {
161 fn from(e: std::io::Error) -> Self {
162 ImageError::Io(e)
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum ImageFormat {
169 Png,
170 Jpeg,
171 Qoi,
172}
173
174impl ImageFormat {
175 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
177 let ext = path.as_ref().extension()?.to_str()?.to_lowercase();
178 match ext.as_str() {
179 "png" => Some(ImageFormat::Png),
180 "jpg" | "jpeg" => Some(ImageFormat::Jpeg),
181 "qoi" => Some(ImageFormat::Qoi),
182 _ => None,
183 }
184 }
185
186 pub fn from_bytes(data: &[u8]) -> Option<Self> {
188 if data.starts_with(b"\x89PNG\r\n\x1a\n") {
189 return Some(ImageFormat::Png);
190 }
191 if data.starts_with(&[0xff, 0xd8, 0xff]) {
192 return Some(ImageFormat::Jpeg);
193 }
194 if data.starts_with(b"qoif") {
195 return Some(ImageFormat::Qoi);
196 }
197 None
198 }
199
200 pub fn as_str(&self) -> &'static str {
201 match self {
202 ImageFormat::Png => "png",
203 ImageFormat::Jpeg => "jpeg",
204 ImageFormat::Qoi => "qoi",
205 }
206 }
207}
208
209#[cfg(feature = "codecs")]
223pub fn blazediff_png_enabled() -> bool {
224 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
225 *ENABLED.get_or_init(|| {
226 std::env::var("BLAZEDIFF_PNG_ENABLED").is_ok_and(|v| {
227 matches!(
228 v.trim().to_ascii_lowercase().as_str(),
229 "1" | "true" | "yes" | "on"
230 )
231 })
232 })
233}
234
235#[cfg(not(feature = "codecs"))]
236pub fn blazediff_png_enabled() -> bool {
237 false
238}
239
240#[cfg(feature = "codecs")]
244fn unsupported_path<P: AsRef<Path>>(path: P) -> ImageError {
245 ImageError::UnsupportedFormat(path.as_ref().display().to_string())
246}
247
248#[cfg(feature = "codecs")]
250pub fn load_image<P: AsRef<Path>>(path: P) -> Result<Image, ImageError> {
251 match ImageFormat::from_path(&path).ok_or_else(|| unsupported_path(&path))? {
252 ImageFormat::Png => load_png(path),
253 ImageFormat::Jpeg => load_jpeg(path),
254 ImageFormat::Qoi => load_qoi(path),
255 }
256}
257
258#[cfg(feature = "codecs")]
264pub fn load_image_pair<P1: AsRef<Path> + Sync, P2: AsRef<Path> + Sync>(
265 path1: P1,
266 path2: P2,
267) -> Result<(Image, Image), ImageError> {
268 let fmt1 = ImageFormat::from_path(&path1).ok_or_else(|| unsupported_path(&path1))?;
269 let fmt2 = ImageFormat::from_path(&path2).ok_or_else(|| unsupported_path(&path2))?;
270
271 if fmt1 == fmt2 {
272 return match fmt1 {
273 ImageFormat::Png => load_pngs(&path1, &path2),
274 ImageFormat::Jpeg => load_jpegs(&path1, &path2),
275 ImageFormat::Qoi => load_qois(&path1, &path2),
276 };
277 }
278
279 let results: Vec<Result<Image, ImageError>> = [
281 (path1.as_ref().to_path_buf(), fmt1),
282 (path2.as_ref().to_path_buf(), fmt2),
283 ]
284 .par_iter()
285 .map(|(path, fmt)| match fmt {
286 ImageFormat::Png => load_png(path),
287 ImageFormat::Jpeg => load_jpeg(path),
288 ImageFormat::Qoi => load_qoi(path),
289 })
290 .collect();
291
292 let mut iter = results.into_iter();
293 Ok((iter.next().unwrap()?, iter.next().unwrap()?))
294}
295
296#[cfg(feature = "codecs")]
298pub fn decode_image(data: &[u8]) -> Result<Image, ImageError> {
299 match ImageFormat::from_bytes(data) {
300 Some(ImageFormat::Png) => decode_png(data),
301 Some(ImageFormat::Jpeg) => decode_jpeg(data),
302 Some(ImageFormat::Qoi) => decode_qoi(data),
303 None => Err(ImageError::UnsupportedFormat(
304 "unrecognized image buffer, expected PNG, JPEG or QOI".to_string(),
305 )),
306 }
307}
308
309#[cfg(feature = "codecs")]
311pub fn decode_image_pair(image1: &[u8], image2: &[u8]) -> Result<(Image, Image), ImageError> {
312 let (result1, result2) = rayon::join(|| decode_image(image1), || decode_image(image2));
313 Ok((result1?, result2?))
314}
315
316#[cfg(feature = "codecs")]
321pub fn save_image<P: AsRef<Path>>(
322 image: &Image,
323 path: P,
324 compression: u8,
325 quality: u8,
326) -> Result<(), ImageError> {
327 match ImageFormat::from_path(&path).ok_or_else(|| unsupported_path(&path))? {
328 ImageFormat::Png => save_png_with_compression(image, path, compression),
329 ImageFormat::Jpeg => save_jpeg(image, path, quality),
330 ImageFormat::Qoi => save_qoi(image, path),
331 }
332}
333
334#[cfg(feature = "fuzzing")]
337#[doc(hidden)]
338pub fn decode_spng_reference(data: &[u8]) -> Result<Image, ImageError> {
339 png_io::decode_spng(data)
340}
341
342#[cfg(feature = "fuzzing")]
346#[doc(hidden)]
347pub fn decode_spng_reference_fmt(
348 data: &[u8],
349 fmt: std::os::raw::c_int,
350 flags: std::os::raw::c_int,
351) -> Result<(u32, u32, u8, u8, Vec<u8>), ImageError> {
352 png_io::decode_spng_fmt(data, fmt, flags)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn detects_format_from_extension() {
361 assert_eq!(ImageFormat::from_path("a.PNG"), Some(ImageFormat::Png));
362 assert_eq!(ImageFormat::from_path("a.jpg"), Some(ImageFormat::Jpeg));
363 assert_eq!(ImageFormat::from_path("a.jpeg"), Some(ImageFormat::Jpeg));
364 assert_eq!(ImageFormat::from_path("a.qoi"), Some(ImageFormat::Qoi));
365 assert_eq!(ImageFormat::from_path("a.webp"), None);
366 assert_eq!(ImageFormat::from_path("noext"), None);
367 }
368
369 #[test]
370 fn detects_format_from_magic_bytes() {
371 assert_eq!(
372 ImageFormat::from_bytes(b"\x89PNG\r\n\x1a\n\x00"),
373 Some(ImageFormat::Png)
374 );
375 assert_eq!(
376 ImageFormat::from_bytes(&[0xff, 0xd8, 0xff, 0xe0]),
377 Some(ImageFormat::Jpeg)
378 );
379 assert_eq!(ImageFormat::from_bytes(b"qoifXXXX"), Some(ImageFormat::Qoi));
380 assert_eq!(ImageFormat::from_bytes(b"RIFF"), None);
381 assert_eq!(ImageFormat::from_bytes(b""), None);
382 }
383
384 #[test]
385 fn error_display_strings_are_stable() {
386 assert_eq!(
387 ImageError::Png("boom".into()).to_string(),
388 "PNG error: boom"
389 );
390 assert_eq!(
391 ImageError::Jpeg("boom".into()).to_string(),
392 "JPEG error: boom"
393 );
394 assert_eq!(
395 ImageError::Qoi("boom".into()).to_string(),
396 "QOI error: boom"
397 );
398 assert_eq!(
399 ImageError::UnsupportedFormat("a.webp".into()).to_string(),
400 "Unsupported format: a.webp"
401 );
402 }
403}