use crate::image::format::{ColorSpace, DecodedAnimation, DecodedImage, ImageData, ImageFormat};
#[cfg(not(any(feature = "image-codecs", feature = "svg-rasterizer")))]
fn not_implemented(format: &str) -> String {
format!("decoding {format} is not implemented (no codec); refusing to return fabricated pixels")
}
pub fn detect_format(data: &[u8]) -> ImageFormat {
if data.is_empty() {
return ImageFormat::Unknown;
}
if data.len() >= 2 && data[0] == b'P' && (b'1'..=b'6').contains(&data[1]) {
return ImageFormat::Pnm;
}
if data.len() >= 3 && data[0] == 0x1F && data[1] == 0x8B && data[2] == 0x08 {
return ImageFormat::Svgz;
}
if data.len() < 4 {
return ImageFormat::Unknown;
}
if data.len() >= 8 && data[0] == 0x89 && data[1] == b'P' && data[2] == b'N' && data[3] == b'G' {
return ImageFormat::Png;
}
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return ImageFormat::Jpeg;
}
if data.len() >= 6
&& &data[0..3] == b"GIF"
&& data[3] == b'8'
&& (data[4] == b'7' || data[4] == b'9')
&& data[5] == b'a'
{
return ImageFormat::Gif;
}
if data[0] == b'B' && data[1] == b'M' {
return ImageFormat::Bmp;
}
if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
return ImageFormat::WebP;
}
if &data[0..4] == b"II\x2a\x00" {
return ImageFormat::Tiff;
}
if &data[0..4] == b"MM\x00\x2a" {
return ImageFormat::Tiff;
}
if data.len() >= 12 && &data[4..8] == b"ftyp" && data[8..12].windows(4).any(|w| w == b"avif") {
return ImageFormat::Avif;
}
if data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 && data[3] == 0x00 {
return ImageFormat::Ico;
}
if &data[0..4] == b"qoif" {
return ImageFormat::Qoi;
}
if data.len() >= 8 && &data[0..8] == b"farbfeld" {
return ImageFormat::Farbfeld;
}
let start = if data.len() >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
3 } else {
0
};
if data.len() > start + 4 {
let slice = &data[start..];
if slice.starts_with(b"<?xml") || slice.starts_with(b"<svg") || slice.starts_with(b"<!DOC")
{
return ImageFormat::Svg;
}
}
ImageFormat::Unknown
}
pub fn decode(data: &[u8]) -> Result<DecodedImage, String> {
let format = detect_format(data);
match format {
ImageFormat::Png => decode_png(data),
ImageFormat::Jpeg => decode_jpeg(data),
ImageFormat::Bmp => decode_bmp(data),
ImageFormat::Gif => decode_gif(data),
ImageFormat::WebP => decode_webp(data),
ImageFormat::Tiff => decode_tiff(data),
ImageFormat::Avif => decode_avif(data),
ImageFormat::Ico => decode_ico(data),
ImageFormat::Pnm => decode_pnm(data),
ImageFormat::Qoi => decode_qoi(data),
ImageFormat::Farbfeld => decode_farbfeld(data),
ImageFormat::Svg => decode_svg(data),
ImageFormat::Svgz => decode_svgz(data),
ImageFormat::Unknown | ImageFormat::Rgba8 | ImageFormat::Rgb8 => {
Err(format!("Unsupported image format: {format:?}"))
}
}
}
pub fn decode_to_rgba8(data: &[u8]) -> Result<DecodedImage, String> {
let mut img = decode(data)?;
img.data = img.data.to_rgba8(img.width, img.height);
img.format = ImageFormat::Rgba8;
Ok(img)
}
pub fn decode_animation(data: &[u8]) -> Result<DecodedAnimation, String> {
match detect_format(data) {
ImageFormat::Gif | ImageFormat::WebP => decode_animation_codec(data),
format => Err(format!("Animation is not supported for {format:?}")),
}
}
#[cfg(feature = "image-codecs")]
fn decode_animation_codec(data: &[u8]) -> Result<DecodedAnimation, String> {
use image_codecs::AnimationDecoder;
use std::io::Cursor;
let format = detect_format(data);
let (frames, loop_count) = match format {
ImageFormat::Gif => {
let decoder = image_codecs::codecs::gif::GifDecoder::new(Cursor::new(data))
.map_err(|error| format!("GIF animation decoder error: {error}"))?;
let loop_count = match decoder.loop_count() {
image_codecs::metadata::LoopCount::Infinite => None,
image_codecs::metadata::LoopCount::Finite(count) => Some(count.get()),
};
(decoder.into_frames().collect_frames(), loop_count)
}
ImageFormat::WebP => {
let decoder = image_codecs::codecs::webp::WebPDecoder::new(Cursor::new(data))
.map_err(|error| format!("WebP animation decoder error: {error}"))?;
let loop_count = match decoder.loop_count() {
image_codecs::metadata::LoopCount::Infinite => None,
image_codecs::metadata::LoopCount::Finite(count) => Some(count.get()),
};
(decoder.into_frames().collect_frames(), loop_count)
}
_ => unreachable!("decode_animation validates the format before dispatch"),
};
let frames = frames.map_err(|error| format!("{format:?} frame decode error: {error}"))?;
let mut decoded_frames = Vec::with_capacity(frames.len());
let mut delays = Vec::with_capacity(frames.len());
for frame in frames {
let (numerator, denominator) = frame.delay().numer_denom_ms();
let nanoseconds =
(u128::from(numerator) * 1_000_000 / u128::from(denominator)).min(u128::from(u64::MAX));
let buffer = frame.into_buffer();
let width = buffer.width();
let height = buffer.height();
decoded_frames.push(DecodedImage::new(
format,
ImageData::Rgba8(buffer.into_raw()),
width,
height,
));
delays.push(std::time::Duration::from_nanos(nanoseconds as u64));
}
DecodedAnimation::new(decoded_frames, delays, loop_count)
}
#[cfg(not(feature = "image-codecs"))]
fn decode_animation_codec(_data: &[u8]) -> Result<DecodedAnimation, String> {
Err("GIF/WebP animation decoding requires the `image-codecs` feature".into())
}
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
let a = a as i32;
let b = b as i32;
let c = c as i32;
let p = a + b - c;
let pa = (p - a).abs();
let pb = (p - b).abs();
let pc = (p - c).abs();
if pa <= pb && pa <= pc {
a as u8
} else if pb <= pc {
b as u8
} else {
c as u8
}
}
fn decode_png(data: &[u8]) -> Result<DecodedImage, String> {
const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
if data.len() < PNG_SIGNATURE.len() + 12 || !data.starts_with(PNG_SIGNATURE) {
return Err("Invalid PNG signature".into());
}
let mut pos = 8usize;
let mut width = 0u32;
let mut height = 0u32;
let mut bit_depth = 0u8;
let mut color_type = 0u8;
let mut have_ihdr = false;
let mut saw_idat = false;
let mut raw_data: Vec<u8> = Vec::new();
let mut palette: Vec<[u8; 4]> = Vec::new();
let mut trns: Option<Vec<u8>> = None;
while pos < data.len() {
if data.len() - pos < 12 {
return Err("PNG chunk header truncated".into());
}
let chunk_len =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
let chunk_type = [data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]];
let body = pos + 8;
if chunk_len > data.len().saturating_sub(body) {
return Err(format!(
"PNG chunk {chunk_type:?} declares {chunk_len} bytes but only {} remain",
data.len().saturating_sub(body)
));
}
match &chunk_type {
b"IHDR" => {
if chunk_len != 13 {
return Err("Invalid IHDR chunk length".into());
}
width = u32::from_be_bytes([
data[body],
data[body + 1],
data[body + 2],
data[body + 3],
]);
height = u32::from_be_bytes([
data[body + 4],
data[body + 5],
data[body + 6],
data[body + 7],
]);
bit_depth = data[body + 8];
color_type = data[body + 9];
let interlace = data[body + 12];
have_ihdr = true;
if width == 0 || height == 0 {
return Err("Invalid PNG dimensions".into());
}
if interlace != 0 {
return Err("Interlaced PNG (Adam7) is not supported".into());
}
}
b"PLTE" => {
if chunk_len == 0 || !chunk_len.is_multiple_of(3) || chunk_len > 256 * 3 {
return Err("Invalid PLTE chunk length".into());
}
palette.clear();
for i in 0..chunk_len / 3 {
let off = body + i * 3;
palette.push([data[off], data[off + 1], data[off + 2], 255]);
}
}
b"tRNS" => {
trns = Some(data[body..body + chunk_len].to_vec());
}
b"IDAT" => {
saw_idat = true;
raw_data.extend_from_slice(&data[body..body + chunk_len]);
}
b"IEND" => break,
_ => {}
}
pos = body + chunk_len + 4;
}
if !have_ihdr {
return Err("Missing IHDR chunk".into());
}
if !saw_idat {
return Err("No IDAT chunks found".into());
}
let channels = match color_type {
0 => 1, 2 => 3, 3 => 1, 4 => 2, 6 => 4, _ => return Err(format!("Unsupported PNG color type: {color_type}")),
};
let depth_ok = match color_type {
0 => matches!(bit_depth, 1 | 2 | 4 | 8 | 16),
2 => matches!(bit_depth, 8 | 16),
3 => matches!(bit_depth, 1 | 2 | 4 | 8),
4 => matches!(bit_depth, 8 | 16),
6 => matches!(bit_depth, 8 | 16),
_ => false,
};
if !depth_ok {
return Err(format!("Unsupported PNG bit depth {bit_depth} for color type {color_type}"));
}
if color_type == 3 && palette.is_empty() {
return Err("Indexed PNG is missing a PLTE chunk".into());
}
let pixel_count = width as u64 * height as u64;
if pixel_count > (1u64 << 27) {
return Err(format!("PNG dimensions too large: {width}x{height}"));
}
let decompressed = miniz_oxide::inflate::decompress_to_vec_zlib(&raw_data)
.map_err(|e| format!("PNG decompress error: {e:?}"))?;
let bits_per_pixel = channels * bit_depth as usize;
let row_bytes = (width as usize * bits_per_pixel).div_ceil(8);
let stride = row_bytes + 1; let expected = stride.checked_mul(height as usize).ok_or("PNG scanline size overflow")?;
if decompressed.len() < expected {
return Err(format!(
"PNG scanline data truncated: need {expected} bytes, got {}",
decompressed.len()
));
}
let bpp = (bits_per_pixel).div_ceil(8).max(1);
let unfiltered_len = row_bytes * height as usize;
let mut unfiltered = vec![0u8; unfiltered_len];
let mut prev_row = vec![0u8; row_bytes];
for y in 0..height as usize {
let row_start = y * stride;
let filter = decompressed[row_start];
let row = &decompressed[row_start + 1..row_start + stride];
let out_start = y * row_bytes;
for x in 0..row_bytes {
let raw_byte = row[x];
let left = if x >= bpp { unfiltered[out_start + x - bpp] } else { 0 };
let up = prev_row[x];
let up_left = if x >= bpp { prev_row[x - bpp] } else { 0 };
unfiltered[out_start + x] = match filter {
0 => raw_byte, 1 => raw_byte.wrapping_add(left), 2 => raw_byte.wrapping_add(up), 3 => raw_byte.wrapping_add(((left as u16 + up as u16) / 2) as u8), 4 => raw_byte.wrapping_add(paeth_predictor(left, up, up_left)), _ => return Err(format!("Invalid PNG filter type {filter} at row {y}")),
};
}
prev_row.copy_from_slice(&unfiltered[out_start..out_start + row_bytes]);
}
if let Some(t) = &trns {
match color_type {
3 => {
for (i, a) in t.iter().enumerate() {
if let Some(p) = palette.get_mut(i) {
p[3] = *a;
}
}
}
0 | 2 => {
return Err("PNG tRNS for grayscale/truecolor images is not supported".into());
}
_ => {}
}
}
let w = width as usize;
let h = height as usize;
let bpc = if bit_depth == 16 { 2usize } else { 1usize };
let out = match color_type {
0 => {
let mut pixels = Vec::with_capacity(w * h);
for y in 0..h {
let row = &unfiltered[y * row_bytes..(y + 1) * row_bytes];
for x in 0..w {
let v = if bit_depth == 16 {
row[x * 2]
} else if bit_depth == 8 {
row[x]
} else {
let pbb = 8 / bit_depth as usize;
let shift = 8 - bit_depth as usize - (x % pbb) * bit_depth as usize;
let v = (row[x / pbb] >> shift) & ((1u8 << bit_depth) - 1);
(v as u16 * 255 / ((1u16 << bit_depth) - 1)) as u8
};
pixels.push(v);
}
}
ImageData::Grayscale8(pixels)
}
2 => {
let mut pixels = Vec::with_capacity(w * h * 3);
for y in 0..h {
let row = &unfiltered[y * row_bytes..(y + 1) * row_bytes];
for x in 0..w {
let base = x * 3 * bpc;
pixels.push(row[base]);
pixels.push(row[base + bpc]);
pixels.push(row[base + 2 * bpc]);
}
}
ImageData::Rgb8(pixels)
}
3 => {
let mut pixels = Vec::with_capacity(w * h * 4);
for y in 0..h {
let row = &unfiltered[y * row_bytes..(y + 1) * row_bytes];
for x in 0..w {
let idx = if bit_depth == 8 {
row[x] as usize
} else {
let pbb = 8 / bit_depth as usize;
let shift = 8 - bit_depth as usize - (x % pbb) * bit_depth as usize;
((row[x / pbb] >> shift) & ((1u8 << bit_depth) - 1)) as usize
};
let p = palette
.get(idx)
.copied()
.ok_or_else(|| format!("PNG palette index {idx} is out of range"))?;
pixels.extend_from_slice(&p);
}
}
ImageData::Rgba8(pixels)
}
4 => {
let mut pixels = Vec::with_capacity(w * h * 4);
for y in 0..h {
let row = &unfiltered[y * row_bytes..(y + 1) * row_bytes];
for x in 0..w {
let base = x * 2 * bpc;
let g = row[base];
let a = row[base + bpc];
pixels.extend_from_slice(&[g, g, g, a]);
}
}
ImageData::Rgba8(pixels)
}
_ => {
let mut pixels = Vec::with_capacity(w * h * 4);
for y in 0..h {
let row = &unfiltered[y * row_bytes..(y + 1) * row_bytes];
for x in 0..w {
let base = x * 4 * bpc;
pixels.extend_from_slice(&[
row[base],
row[base + bpc],
row[base + 2 * bpc],
row[base + 3 * bpc],
]);
}
}
ImageData::Rgba8(pixels)
}
};
Ok(DecodedImage::new(ImageFormat::Png, out, width, height))
}
fn decode_jpeg(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
return Err("Invalid JPEG signature".into());
}
let mut pos = 2;
let mut width = 0u32;
let mut height = 0u32;
let mut components: Vec<JpegComponent> = Vec::new();
let mut dc_huff: [Option<HuffTable>; 4] = [None, None, None, None];
let mut ac_huff: [Option<HuffTable>; 4] = [None, None, None, None];
let mut quant_tables: [Option<[u16; 64]>; 4] = [None, None, None, None];
let mut sos_components: Vec<(u8, u8, u8)> = Vec::new();
let mut scan_data_start = 0;
let mut scan_data_end = 0;
while pos + 2 <= data.len() {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
if marker == 0xD9 {
break;
}
if (0xD0..=0xD7).contains(&marker) {
pos += 2;
continue;
}
if marker == 0xDA {
if pos + 4 > data.len() {
break;
}
let seg_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
if pos + seg_len > data.len() {
break;
}
let num_sos_comp = data[pos + 4] as usize;
let mut offset = pos + 5;
for _ in 0..num_sos_comp {
if offset + 2 > pos + seg_len {
break;
}
let comp_id = data[offset];
let dc_ac = data[offset + 1];
let dc_table = (dc_ac >> 4) & 0x0F;
let ac_table = dc_ac & 0x0F;
sos_components.push((comp_id, dc_table, ac_table));
offset += 2;
}
scan_data_start = pos + seg_len;
scan_data_end = data.len();
for i in scan_data_start..data.len().saturating_sub(1) {
if data[i] == 0xFF {
let next_marker = data[i + 1];
if next_marker != 0x00 && next_marker != 0xFF {
scan_data_end = i;
break;
}
}
}
break;
}
if pos + 4 > data.len() {
break;
}
let seg_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
if seg_len < 2 || pos + seg_len > data.len() {
break;
}
let seg_data = &data[pos + 4..pos + seg_len];
match marker {
0xC0..=0xC2 => {
if seg_data.len() >= 6 {
let precision = seg_data[0];
if precision != 8 {
return Err(format!(
"JPEG precision {precision} not supported (only 8-bit)"
));
}
height = u16::from_be_bytes([seg_data[1], seg_data[2]]) as u32;
width = u16::from_be_bytes([seg_data[3], seg_data[4]]) as u32;
let _num_components = seg_data[5];
let mut off = 6;
for _ in 0.._num_components {
if off + 3 > seg_data.len() {
break;
}
components.push(JpegComponent {
_id: seg_data[off],
h_sampling: (seg_data[off + 1] >> 4) & 0x0F,
v_sampling: seg_data[off + 1] & 0x0F,
quant_table: seg_data[off + 2],
});
off += 3;
}
}
}
0xDB => {
let mut off = 0;
while off + 65 <= seg_data.len() {
let precision = (seg_data[off] >> 4) & 0x0F;
let table_id = seg_data[off] & 0x0F;
if precision == 0 {
let mut table = [0u16; 64];
for i in 0..64 {
table[ZIGZAG[i]] = seg_data[off + 1 + i] as u16;
}
quant_tables[table_id as usize] = Some(table);
off += 65;
} else {
let mut table = [0u16; 64];
for i in 0..64 {
table[ZIGZAG[i]] = u16::from_be_bytes([
seg_data[off + 1 + i * 2],
seg_data[off + 2 + i * 2],
]);
}
quant_tables[table_id as usize] = Some(table);
off += 129;
}
}
}
0xC4 => {
let mut off = 0;
while off + 17 <= seg_data.len() {
let table_class = (seg_data[off] >> 4) & 0x0F;
let table_id = seg_data[off] & 0x0F;
off += 1;
let mut counts = [0usize; 16];
let mut total_symbols = 0;
for i in 0..16 {
counts[i] = seg_data[off + i] as usize;
total_symbols += counts[i];
}
off += 16;
if off + total_symbols > seg_data.len() {
break;
}
let symbols = seg_data[off..off + total_symbols].to_vec();
off += total_symbols;
let table = build_huff_table(&counts, &symbols);
if table_class == 0 {
dc_huff[table_id as usize] = Some(table);
} else {
ac_huff[table_id as usize] = Some(table);
}
}
}
_ => {}
}
pos += seg_len;
}
if width == 0 || height == 0 {
return Err("Could not determine JPEG dimensions".into());
}
if components.is_empty() {
return Err("No components found in JPEG".into());
}
let mcu_width = components.iter().map(|c| c.h_sampling).max().unwrap_or(1) as u32 * 8;
let mcu_height = components.iter().map(|c| c.v_sampling).max().unwrap_or(1) as u32 * 8;
let mcus_x = width.div_ceil(mcu_width);
let mcus_y = height.div_ceil(mcu_height);
let mut comp_bufs: Vec<Vec<Vec<i16>>> = Vec::new();
for comp in &components {
let cw = width.div_ceil((1 << comp.h_sampling) * 8) * ((1 << comp.h_sampling) * 8);
let ch = height.div_ceil((1 << comp.v_sampling) * 8) * ((1 << comp.v_sampling) * 8);
comp_bufs.push(vec![vec![0i16; cw as usize * ch as usize]; 1]);
}
let scan_data = if scan_data_start < scan_data_end && scan_data_end <= data.len() {
&data[scan_data_start..scan_data_end]
} else {
return Err("No scan data found in JPEG".into());
};
let mut bit_pos = 0;
let mut dc_pred: [i32; 4] = [0; 4];
for mcu_y in 0..mcus_y {
for mcu_x in 0..mcus_x {
for (ci, comp) in components.iter().enumerate() {
let qt = quant_tables[comp.quant_table as usize]
.ok_or_else(|| format!("Missing quantization table {}", comp.quant_table))?;
let sos =
*sos_components.get(ci).ok_or("JPEG scan is missing a component selector")?;
if sos.1 >= 4 || sos.2 >= 4 {
return Err("JPEG Huffman table selector is out of range".into());
}
let dc_table =
dc_huff[sos.1 as usize].as_ref().ok_or("Missing DC Huffman table")?;
let ac_table =
ac_huff[sos.2 as usize].as_ref().ok_or("Missing AC Huffman table")?;
let dbw = ((comp.h_sampling as u32) * 8) as usize;
let dbh = ((comp.v_sampling as u32) * 8) as usize;
for by in 0..comp.v_sampling as usize {
for bx in 0..comp.h_sampling as usize {
let mut block = [0i32; 64];
let (cat, _extra_bits) =
decode_huff_symbol(scan_data, &mut bit_pos, dc_table)
.ok_or("JPEG entropy data truncated in DC coefficient")?;
if cat > 0 {
let mag = receive_extended(scan_data, &mut bit_pos, cat as usize)?;
dc_pred[ci] += mag;
}
block[0] = dc_pred[ci];
let mut k = 1;
while k < 64 {
let (symbol, _extra_bits) =
decode_huff_symbol(scan_data, &mut bit_pos, ac_table)
.ok_or("JPEG entropy data truncated in AC coefficient")?;
if symbol == 0 {
break;
}
let run = (symbol >> 4) as usize;
let cat = (symbol & 0x0F) as usize;
if cat == 0 && run != 15 {
return Err("Invalid JPEG AC run-length symbol".into());
}
k += run;
if k >= 64 {
return Err("JPEG AC run exceeds block boundary".into());
}
if cat > 0 {
let mag = receive_extended(scan_data, &mut bit_pos, cat)?;
block[ZIGZAG[k]] = mag;
}
k += 1;
}
for i in 0..64 {
block[i] *= qt[i] as i32;
}
let mut pixels = [0i32; 64];
idct_8x8(&block, &mut pixels);
let cw = width.div_ceil((1 << comp.h_sampling) * 8)
* ((1 << comp.h_sampling) * 8);
let buf_width = cw as usize;
for yy in 0..8 {
for xx in 0..8 {
let px = (mcu_x as usize * dbw + bx * 8 + xx).min(buf_width - 1);
let py = (mcu_y as usize * dbh + by * 8 + yy)
.min(comp_bufs[ci][0].len() / buf_width - 1);
let idx = py * buf_width + px;
if idx < comp_bufs[ci][0].len() {
comp_bufs[ci][0][idx] =
pixels[yy * 8 + xx].clamp(-128, 127) as i16 + 128;
}
}
}
}
}
}
}
}
let mut pixels = vec![0u8; (width * height * 4) as usize];
let buf_width = (width.div_ceil(mcu_width) * mcu_width) as usize;
let _buf_height = (height.div_ceil(mcu_height) * mcu_height) as usize;
for y in 0..height as usize {
for x in 0..width as usize {
let idx = y * width as usize + x;
let bidx = y * buf_width + x;
let y_val = comp_bufs
.first()
.and_then(|b| b.first())
.and_then(|row| row.get(bidx))
.copied()
.ok_or("JPEG decoded luma component is incomplete")? as i32;
let (cb_val, cr_val) = if components.len() >= 3 {
let cb = comp_bufs
.get(1)
.and_then(|b| b.first())
.and_then(|row| row.get(bidx))
.copied()
.ok_or("JPEG decoded Cb component is incomplete")?;
let cr = comp_bufs
.get(2)
.and_then(|b| b.first())
.and_then(|row| row.get(bidx))
.copied()
.ok_or("JPEG decoded Cr component is incomplete")?;
(cb as i32, cr as i32)
} else {
(128, 128)
};
let r = (y_val + (359 * (cr_val - 128)) / 256).clamp(0, 255) as u8;
let g =
(y_val - (88 * (cb_val - 128) + 183 * (cr_val - 128)) / 256).clamp(0, 255) as u8;
let b = (y_val + (454 * (cb_val - 128)) / 256).clamp(0, 255) as u8;
let poff = idx * 4;
pixels[poff] = r;
pixels[poff + 1] = g;
pixels[poff + 2] = b;
pixels[poff + 3] = 255;
}
}
let mut img = DecodedImage::new(ImageFormat::Jpeg, ImageData::Rgba8(pixels), width, height);
img.color_space = ColorSpace::Srgb;
Ok(img)
}
type HuffCode = u16;
#[derive(Clone, Copy)]
struct HuffEntry {
value: u8,
bits: u8,
code: HuffCode,
}
struct HuffTable {
entries: Vec<HuffEntry>,
}
fn build_huff_table(counts: &[usize; 16], symbols: &[u8]) -> HuffTable {
let mut entries = Vec::new();
let mut code: HuffCode = 0;
let mut si = 0;
for bits in 1..=16 {
for _ in 0..counts[bits - 1] {
if si < symbols.len() {
entries.push(HuffEntry { value: symbols[si], bits: bits as u8, code });
si += 1;
}
code += 1;
}
code <<= 1;
}
HuffTable { entries }
}
fn decode_huff_symbol(data: &[u8], bit_pos: &mut usize, table: &HuffTable) -> Option<(u8, usize)> {
let mut code: HuffCode = 0;
for bits in 1..=16 {
if *bit_pos >= data.len() * 8 {
return None;
}
let byte_idx = *bit_pos / 8;
let bit_idx = *bit_pos % 8;
let b = (data[byte_idx] >> (7 - bit_idx)) & 1;
*bit_pos += 1;
code = (code << 1) | b as HuffCode;
for entry in &table.entries {
if entry.bits == bits as u8 && entry.code == code {
return Some((entry.value, bits));
}
}
}
None
}
fn receive_extended(data: &[u8], bit_pos: &mut usize, cat: usize) -> Result<i32, String> {
if cat == 0 {
return Ok(0);
}
let mut value = 0i32;
for _ in 0..cat {
if *bit_pos >= data.len() * 8 {
return Err("JPEG entropy data truncated in coefficient magnitude".into());
}
let byte_idx = *bit_pos / 8;
let bit_idx = *bit_pos % 8;
let b = ((data[byte_idx] >> (7 - bit_idx)) & 1) as i32;
*bit_pos += 1;
value = (value << 1) | b;
}
let sv_range = 1i32 << (cat - 1);
if value < sv_range {
value -= (1 << cat) - 1;
}
Ok(value)
}
fn idct_8x8(input: &[i32; 64], output: &mut [i32; 64]) {
let mut tmp = [0i32; 64];
for y in 0..8 {
for x in 0..8 {
let mut sum = 0i32;
for u in 0..8 {
let cu = if u == 0 { 1 } else { 2 };
let val = input[y * 8 + u];
sum += val * cu * icosph(u, x);
}
tmp[y * 8 + x] = sum;
}
}
for x in 0..8 {
for y in 0..8 {
let mut sum = 0i32;
for v in 0..8 {
let cv = if v == 0 { 1 } else { 2 };
let val = tmp[v * 8 + x];
sum += val * cv * icosph(v, y);
}
output[y * 8 + x] = sum / 4;
}
}
}
fn icosph(u: usize, v: usize) -> i32 {
let pi = std::f64::consts::PI;
let cos = ((2.0 * v as f64 + 1.0) * u as f64 * pi / 16.0).cos();
(cos * 10000.0) as i32
}
const ZIGZAG: [usize; 64] = [
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20,
13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59,
52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
];
#[derive(Clone)]
struct JpegComponent {
_id: u8,
h_sampling: u8,
v_sampling: u8,
quant_table: u8,
}
fn decode_bmp(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 54 || &data[0..2] != b"BM" {
return Err("Invalid BMP signature format".into());
}
let pixel_offset = u32::from_le_bytes([data[10], data[11], data[12], data[13]]) as usize;
let width = u32::from_le_bytes([data[18], data[19], data[20], data[21]]);
let raw_height_signed = i32::from_le_bytes([data[22], data[23], data[24], data[25]]);
let height = raw_height_signed.unsigned_abs();
let _top_down = raw_height_signed < 0;
let bit_count = u16::from_le_bytes([data[28], data[29]]);
let compression = u32::from_le_bytes([data[30], data[31], data[32], data[33]]);
if width == 0 || height == 0 || !matches!(bit_count, 24 | 32) {
return Err("Unsupported BMP dimensions or bit depth".into());
}
if compression != 0 {
return Err("Compressed BMP images are not supported".into());
}
let row_size = (width as usize)
.checked_mul(bit_count as usize)
.and_then(|bits| bits.checked_add(31))
.map(|bits| bits / 32 * 4)
.ok_or("BMP row size overflow")?;
let pixel_bytes = row_size.checked_mul(height as usize).ok_or("BMP dimensions overflow")?;
let pixel_end = pixel_offset.checked_add(pixel_bytes).ok_or("BMP pixel offset overflow")?;
let pixel_data = data.get(pixel_offset..pixel_end).ok_or("BMP data truncated")?;
let bytes_per_pixel = (bit_count / 8) as usize;
let mut pixels = Vec::with_capacity(width as usize * height as usize * 4);
for y in 0..height as usize {
let row = if raw_height_signed > 0 {
height as usize - 1 - y } else {
y };
let row_start = row * row_size;
for x in 0..width as usize {
let off = row_start + x * bytes_per_pixel;
let (b, g, r, a) = if bytes_per_pixel == 4 {
(pixel_data[off], pixel_data[off + 1], pixel_data[off + 2], pixel_data[off + 3])
} else {
(pixel_data[off], pixel_data[off + 1], pixel_data[off + 2], 255)
};
pixels.push(r);
pixels.push(g);
pixels.push(b);
pixels.push(a);
}
}
let mut img = DecodedImage::new(ImageFormat::Bmp, ImageData::Rgba8(pixels), width, height);
img.color_space = ColorSpace::Srgb;
Ok(img)
}
fn decode_gif(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 6 || !(data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a")) {
return Err("Invalid GIF signature".into());
}
decode_with_image_codecs(data, ImageFormat::Gif)
}
fn decode_webp(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 12 || &data[0..4] != b"RIFF" || &data[8..12] != b"WEBP" {
return Err("Invalid WebP signature".into());
}
decode_with_image_codecs(data, ImageFormat::WebP)
}
fn decode_tiff(data: &[u8]) -> Result<DecodedImage, String> {
let valid_le = data.len() >= 4 && &data[0..4] == b"II\x2a\x00";
let valid_be = data.len() >= 4 && &data[0..4] == b"MM\x00\x2a";
if !valid_le && !valid_be {
return Err("Invalid TIFF signature".into());
}
decode_with_image_codecs(data, ImageFormat::Tiff)
}
fn decode_avif(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 12 || &data[4..8] != b"ftyp" {
return Err("Invalid AVIF data".into());
}
decode_with_image_codecs(data, ImageFormat::Avif)
}
fn decode_ico(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 6 || data[0] != 0 || data[1] != 0 || data[2] != 1 || data[3] != 0 {
return Err("Invalid ICO signature".into());
}
decode_with_image_codecs(data, ImageFormat::Ico)
}
#[cfg(feature = "image-codecs")]
fn decode_with_image_codecs(data: &[u8], format: ImageFormat) -> Result<DecodedImage, String> {
let image = image_codecs::load_from_memory(data)
.map_err(|error| format!("{format:?} decode error: {error}"))?;
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
let mut decoded = DecodedImage::new(format, ImageData::Rgba8(rgba.into_raw()), width, height);
decoded.color_space = ColorSpace::Srgb;
Ok(decoded)
}
#[cfg(not(feature = "image-codecs"))]
fn decode_with_image_codecs(_data: &[u8], format: ImageFormat) -> Result<DecodedImage, String> {
Err(not_implemented(format.extension()))
}
fn decode_pnm(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 3 || data[0] != b'P' || !(b'1'..=b'6').contains(&data[1]) {
return Err("Invalid PNM signature".into());
}
let format_type = data[1];
if matches!(format_type, b'1' | b'2' | b'3') {
let header = std::str::from_utf8(&data[2..]).map_err(|_| "PNM: non-UTF-8 ASCII data")?;
let mut tokens = header
.lines()
.flat_map(|line| line.split('#').next().unwrap_or_default().split_whitespace());
let w = tokens
.next()
.ok_or("PNM: missing width")?
.parse::<u32>()
.map_err(|_| "Invalid PNM width")?;
let h = tokens
.next()
.ok_or("PNM: missing height")?
.parse::<u32>()
.map_err(|_| "Invalid PNM height")?;
if w == 0 || h == 0 {
return Err("Invalid PNM dimensions".into());
}
let maxval = if format_type == b'1' {
1
} else {
tokens
.next()
.ok_or("PNM: missing maxval")?
.parse::<u32>()
.map_err(|_| "Invalid PNM maxval")?
};
if maxval == 0 || maxval > 65535 {
return Err("Invalid PNM maxval".into());
}
let pixel_count = (w as usize).checked_mul(h as usize).ok_or("PNM dimensions overflow")?;
let channel_count = if format_type == b'3' { 3 } else { 1 };
let sample_count =
pixel_count.checked_mul(channel_count).ok_or("PNM sample count overflow")?;
let mut pixels = Vec::with_capacity(sample_count * 3);
for sample_index in 0..sample_count {
let sample = tokens
.next()
.ok_or_else(|| format!("PNM: missing sample at index {sample_index}"))?
.parse::<u32>()
.map_err(|_| format!("PNM: invalid sample at index {sample_index}"))?;
if sample > maxval {
return Err(format!("PNM: sample {sample} exceeds maxval {maxval}"));
}
let scaled = (sample * 255 / maxval) as u8;
if format_type == b'1' {
let value = if scaled == 0 { 255 } else { 0 };
pixels.extend_from_slice(&[value, value, value]);
} else if format_type == b'2' {
pixels.extend_from_slice(&[scaled, scaled, scaled]);
} else {
pixels.push(scaled);
}
}
if tokens.next().is_some() {
return Err("PNM: too many samples".into());
}
let mut img = DecodedImage::new(ImageFormat::Pnm, ImageData::Rgb8(pixels), w, h);
img.color_space = ColorSpace::Srgb;
return Ok(img);
}
let first_nl = data[2..]
.iter()
.position(|&b| b == b'\n')
.map(|p| p + 2)
.ok_or("PNM: missing first newline")?;
let second_nl = data[first_nl + 1..]
.iter()
.position(|&b| b == b'\n')
.map(|p| p + first_nl + 1)
.ok_or("PNM: missing second newline")?;
if format_type == b'4' {
let w = std::str::from_utf8(&data[first_nl + 1..second_nl])
.map_err(|_| "PNM: non-UTF-8 in dimension line")?
.split_whitespace()
.next()
.ok_or("Cannot parse PNM width")?
.parse::<u32>()
.map_err(|_| "Invalid PNM width")?;
let h = std::str::from_utf8(&data[first_nl + 1..second_nl])
.map_err(|_| "PNM: non-UTF-8 in dimension line")?
.split_whitespace()
.nth(1)
.ok_or("Cannot parse PNM height")?
.parse::<u32>()
.map_err(|_| "Invalid PNM height")?;
if w == 0 || h == 0 {
return Err("Invalid PNM dimensions".into());
}
let row_bytes = (w as usize).div_ceil(8);
let packed_len = row_bytes.checked_mul(h as usize).ok_or("PNM dimensions overflow")?;
let data_start = second_nl + 1;
let packed = data.get(data_start..data_start + packed_len).ok_or_else(|| {
format!(
"PNM P4 bitmap truncated: need {packed_len} bytes, got {}",
data.len().saturating_sub(data_start)
)
})?;
let mut pixels = Vec::with_capacity((w as usize) * (h as usize) * 3);
for y in 0..h as usize {
for x in 0..w as usize {
let bit = (packed[y * row_bytes + x / 8] >> (7 - (x % 8))) & 1;
let value = if bit == 0 { 255 } else { 0 };
pixels.extend_from_slice(&[value, value, value]);
}
}
let mut img = DecodedImage::new(ImageFormat::Pnm, ImageData::Rgb8(pixels), w, h);
img.color_space = ColorSpace::Srgb;
return Ok(img);
}
let third_nl = data[second_nl + 1..]
.iter()
.position(|&b| b == b'\n')
.map(|p| p + second_nl + 1)
.unwrap_or(data.len());
let dim_line = std::str::from_utf8(&data[first_nl + 1..second_nl])
.map_err(|_| "PNM: non-UTF-8 in dimension line")?;
let dim_parts: Vec<&str> = dim_line.split_whitespace().collect();
if dim_parts.len() < 2 {
return Err("Cannot parse PNM dimensions".into());
}
let w = dim_parts[0].parse::<u32>().map_err(|_| "Invalid PNM width")?;
let h = dim_parts[1].parse::<u32>().map_err(|_| "Invalid PNM height")?;
let maxval_line = std::str::from_utf8(&data[second_nl + 1..third_nl])
.map_err(|_| "PNM: non-UTF-8 in maxval line")?;
let maxval = maxval_line
.split_whitespace()
.next()
.ok_or("PNM: missing maxval")?
.parse::<u32>()
.map_err(|_| "Invalid PNM maxval")?;
if maxval == 0 || maxval > 65535 {
return Err("Invalid PNM maxval".into());
}
let data_start = if third_nl < data.len() { third_nl + 1 } else { data.len() };
if format_type == b'5' || format_type == b'6' {
if w == 0 || h == 0 {
return Err("Invalid PNM dimensions".into());
}
let pixel_count = (w as usize).checked_mul(h as usize).ok_or("PNM dimensions overflow")?;
let sample_count = if format_type == b'5' {
pixel_count
} else {
pixel_count.checked_mul(3).ok_or("PNM sample count overflow")?
};
let available = data.len().saturating_sub(data_start);
if available < sample_count {
return Err(format!(
"PNM P{} data truncated: need {sample_count} bytes, got {available}",
format_type as char
));
}
let pixel_data = &data[data_start..data_start + sample_count];
let mut pixels = Vec::with_capacity(pixel_count * 3);
let maxval_f = maxval as f32;
if format_type == b'5' {
for &v in pixel_data {
let scaled =
if maxval != 255 { (v as f32 / maxval_f * 255.0).round() as u8 } else { v };
pixels.extend_from_slice(&[scaled, scaled, scaled]);
}
} else {
pixels.extend_from_slice(pixel_data);
}
let mut img = DecodedImage::new(ImageFormat::Pnm, ImageData::Rgb8(pixels), w, h);
img.color_space = ColorSpace::Srgb;
Ok(img)
} else {
Err("Unsupported binary PNM format".into())
}
}
fn decode_qoi(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 18 || &data[0..4] != b"qoif" {
return Err("Invalid QOI signature".into());
}
let width = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let height = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let _channels = data[12];
let _colorspace = data[13];
if width == 0 || height == 0 {
return Err("Invalid QOI dimensions".into());
}
let total = (width * height) as usize;
let mut pixels = Vec::with_capacity(total * 4);
let mut index = [[0u8; 4]; 64];
let mut r = 0u8;
let mut g = 0u8;
let mut b = 0u8;
let mut a = 255u8;
let mut pos = 14;
while pixels.len() / 4 < total && pos < data.len() {
let byte = data[pos];
pos += 1;
if byte == 0xFE {
let end = pos.checked_add(3).ok_or("QOI RGB opcode overflow")?;
let rgb = data.get(pos..end).ok_or("QOI RGB opcode truncated")?;
r = rgb[0];
g = rgb[1];
b = rgb[2];
pos = end;
} else if byte == 0xFF {
let end = pos.checked_add(4).ok_or("QOI RGBA opcode overflow")?;
let rgba = data.get(pos..end).ok_or("QOI RGBA opcode truncated")?;
r = rgba[0];
g = rgba[1];
b = rgba[2];
a = rgba[3];
pos = end;
} else if byte >> 6 == 0b00 {
let idx = (byte & 0x3F) as usize;
let c = index[idx];
r = c[0];
g = c[1];
b = c[2];
a = c[3];
} else if byte >> 6 == 0b01 {
let dr = ((byte >> 4) & 0x03).wrapping_sub(2);
let dg = ((byte >> 2) & 0x03).wrapping_sub(2);
let db = (byte & 0x03).wrapping_sub(2);
r = r.wrapping_add(dr);
g = g.wrapping_add(dg);
b = b.wrapping_add(db);
} else if byte >> 6 == 0b10 {
let byte2 = *data.get(pos).ok_or("QOI luma opcode truncated")?;
pos += 1;
let dg = (byte & 0x3F).wrapping_sub(32);
let dr = ((byte2 >> 4) & 0x0F).wrapping_sub(8).wrapping_add(dg);
let db = (byte2 & 0x0F).wrapping_sub(8).wrapping_add(dg);
r = r.wrapping_add(dr);
g = g.wrapping_add(dg);
b = b.wrapping_add(db);
} else if byte >> 6 == 0b11 {
let run = (byte & 0x3F) as usize + 1;
for _ in 0..run {
pixels.push(r);
pixels.push(g);
pixels.push(b);
pixels.push(a);
}
continue;
}
pixels.push(r);
pixels.push(g);
pixels.push(b);
pixels.push(a);
let hash = (r as usize * 3 + g as usize * 5 + b as usize * 7 + a as usize * 11) & 63;
index[hash] = [r, g, b, a];
}
if pixels.len() < total * 4 {
return Err(format!(
"QOI data truncated: got {} bytes of pixels, need {}",
pixels.len(),
total * 4
));
}
if pixels.len() > total * 4 {
return Err("QOI run exceeds declared image dimensions".into());
}
let end_marker = [0, 0, 0, 0, 0, 0, 0, 1];
if data.len().saturating_sub(pos) < end_marker.len()
|| data[data.len() - end_marker.len()..] != end_marker
{
return Err("QOI end marker is missing or invalid".into());
}
let mut img = DecodedImage::new(ImageFormat::Qoi, ImageData::Rgba8(pixels), width, height);
img.color_space = ColorSpace::Srgb;
Ok(img)
}
fn decode_farbfeld(data: &[u8]) -> Result<DecodedImage, String> {
if data.len() < 16 || &data[0..8] != b"farbfeld" {
return Err("Invalid Farbfeld signature".into());
}
let width = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let height = u32::from_be_bytes([data[12], data[13], data[14], data[15]]);
if width == 0 || height == 0 || width > 16384 || height > 16384 {
return Err("Invalid Farbfeld dimensions".into());
}
let total = (width * height) as usize;
let required = 16 + total * 8;
if data.len() < required {
return Err(format!("Farbfeld data truncated: need {required} bytes, got {}", data.len()));
}
let mut pixels = Vec::with_capacity(total * 4);
for i in 0..total {
let off = 16 + i * 8;
let r = (u16::from_be_bytes([data[off], data[off + 1]]) >> 8) as u8;
let g = (u16::from_be_bytes([data[off + 2], data[off + 3]]) >> 8) as u8;
let b = (u16::from_be_bytes([data[off + 4], data[off + 5]]) >> 8) as u8;
let a = (u16::from_be_bytes([data[off + 6], data[off + 7]]) >> 8) as u8;
pixels.push(r);
pixels.push(g);
pixels.push(b);
pixels.push(a);
}
let mut img = DecodedImage::new(ImageFormat::Farbfeld, ImageData::Rgba8(pixels), width, height);
img.color_space = ColorSpace::Srgb;
Ok(img)
}
fn decode_svg(data: &[u8]) -> Result<DecodedImage, String> {
#[cfg(feature = "svg-rasterizer")]
{
let tree = resvg::usvg::Tree::from_data(data, &resvg::usvg::Options::default())
.map_err(|error| format!("SVG parse error: {error}"))?;
let size = tree.size().to_int_size();
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height())
.ok_or("SVG raster dimensions are invalid")?;
resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pixmap.as_mut());
let mut decoded = DecodedImage::new(
ImageFormat::Svg,
ImageData::Rgba8(pixmap.take()),
size.width(),
size.height(),
);
decoded.color_space = ColorSpace::Srgb;
Ok(decoded)
}
#[cfg(not(feature = "svg-rasterizer"))]
{
std::str::from_utf8(data).map_err(|_| "Invalid UTF-8 in SVG".to_string())?;
Err(not_implemented("SVG"))
}
}
fn decode_svgz(data: &[u8]) -> Result<DecodedImage, String> {
#[cfg(feature = "svg-rasterizer")]
{
let tree = resvg::usvg::Tree::from_data(data, &resvg::usvg::Options::default())
.map_err(|error| format!("SVGZ parse error: {error}"))?;
let size = tree.size().to_int_size();
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height())
.ok_or("SVGZ raster dimensions are invalid")?;
resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pixmap.as_mut());
let mut decoded = DecodedImage::new(
ImageFormat::Svgz,
ImageData::Rgba8(pixmap.take()),
size.width(),
size.height(),
);
decoded.color_space = ColorSpace::Srgb;
Ok(decoded)
}
#[cfg(not(feature = "svg-rasterizer"))]
{
let decompressed = miniz_oxide::inflate::decompress_to_vec(data)
.map_err(|_| "SVGZ decompression failed".to_string())?;
decode_svg(&decompressed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_png_format() {
let magic = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR";
assert_eq!(detect_format(magic), ImageFormat::Png);
}
#[test]
fn detect_jpeg_format() {
let magic = b"\xFF\xD8\xFF\xE0\x00\x10JFIF";
assert_eq!(detect_format(magic), ImageFormat::Jpeg);
}
#[test]
fn detect_gif_format() {
assert_eq!(detect_format(b"GIF89a"), ImageFormat::Gif);
assert_eq!(detect_format(b"GIF87a"), ImageFormat::Gif);
}
#[test]
fn detect_bmp_format() {
assert_eq!(detect_format(b"BM\x00\x00"), ImageFormat::Bmp);
}
#[test]
fn detect_webp_format() {
let webp = b"RIFF\x00\x00\x00\x00WEBP".to_vec();
assert_eq!(detect_format(&webp), ImageFormat::WebP);
}
#[test]
fn detect_tiff_format() {
assert_eq!(detect_format(b"II\x2a\x00"), ImageFormat::Tiff);
assert_eq!(detect_format(b"MM\x00\x2a"), ImageFormat::Tiff);
}
#[test]
fn detect_qoi_format() {
assert_eq!(
detect_format(b"qoif\x00\x00\x00\x01\x00\x00\x00\x01\x03\x00"),
ImageFormat::Qoi
);
}
#[test]
fn detect_farbfeld_format() {
assert_eq!(detect_format(b"farbfeld"), ImageFormat::Farbfeld);
}
#[test]
fn detect_ico_format() {
assert_eq!(detect_format(b"\x00\x00\x01\x00"), ImageFormat::Ico);
}
#[test]
fn detect_pnm_format() {
assert_eq!(detect_format(b"P6\n"), ImageFormat::Pnm);
assert_eq!(detect_format(b"P5\n"), ImageFormat::Pnm);
assert_eq!(detect_format(b"P1\n"), ImageFormat::Pnm);
}
#[test]
fn detect_svg_format() {
assert_eq!(detect_format(b"<svg xmlns"), ImageFormat::Svg);
assert_eq!(detect_format(b"<?xml version"), ImageFormat::Svg);
}
#[test]
fn detect_svgz_format() {
assert_eq!(detect_format(b"\x1F\x8B\x08"), ImageFormat::Svgz);
}
#[test]
fn detect_unknown_format() {
assert_eq!(detect_format(b"not an image"), ImageFormat::Unknown);
}
#[test]
fn detect_empty_data() {
assert_eq!(detect_format(b""), ImageFormat::Unknown);
}
#[test]
fn decode_qoi_small() {
let mut qoi_data = b"qoif".to_vec();
qoi_data.extend_from_slice(&1u32.to_be_bytes()); qoi_data.extend_from_slice(&1u32.to_be_bytes()); qoi_data.push(3); qoi_data.push(0); qoi_data.push(0xFF); qoi_data.push(255); qoi_data.push(255); qoi_data.push(255); qoi_data.push(255); qoi_data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 1]);
let result = decode_qoi(&qoi_data);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.width, 1);
assert_eq!(img.height, 1);
assert_eq!(img.format, ImageFormat::Qoi);
}
#[test]
fn decode_qoi_rejects_truncated_opcode_and_bad_end_marker() {
let mut truncated = b"qoif".to_vec();
truncated.extend_from_slice(&1u32.to_be_bytes());
truncated.extend_from_slice(&1u32.to_be_bytes());
truncated.extend_from_slice(&[4, 0, 0xFF, 255]);
assert!(decode_qoi(&truncated).is_err());
let mut bad_end = b"qoif".to_vec();
bad_end.extend_from_slice(&1u32.to_be_bytes());
bad_end.extend_from_slice(&1u32.to_be_bytes());
bad_end.extend_from_slice(&[4, 0, 0xFF, 255, 255, 255, 255, 255]);
bad_end.extend_from_slice(&[0; 8]);
assert!(decode_qoi(&bad_end).is_err());
}
#[test]
fn decode_farbfeld_small() {
let mut ff = b"farbfeld".to_vec();
ff.extend_from_slice(&1u32.to_be_bytes()); ff.extend_from_slice(&1u32.to_be_bytes()); ff.push(255);
ff.push(128); ff.push(0);
ff.push(0); ff.push(0);
ff.push(128); ff.push(255);
ff.push(255);
let result = decode_farbfeld(&ff);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.width, 1);
assert_eq!(img.height, 1);
assert_eq!(img.format, ImageFormat::Farbfeld);
}
#[test]
fn decode_bmp_minimal() {
let mut bmp = b"BM".to_vec();
let row_size = ((2 * 24 + 31) / 32 * 4) as usize; let pixel_data_size = row_size * 2; let file_size = 54 + pixel_data_size;
bmp.extend_from_slice(&(file_size as u32).to_le_bytes()); bmp.extend_from_slice(&[0u8; 4]); bmp.extend_from_slice(&54u32.to_le_bytes()); bmp.extend_from_slice(&40u32.to_le_bytes()); bmp.extend_from_slice(&2u32.to_le_bytes()); bmp.extend_from_slice(&2i32.to_le_bytes()); bmp.extend_from_slice(&1u16.to_le_bytes()); bmp.extend_from_slice(&24u16.to_le_bytes()); bmp.extend_from_slice(&[0u8; 24]);
bmp.extend_from_slice(&[0, 0, 255, 0, 0, 0, 0, 0]); bmp.extend_from_slice(&[0, 255, 0, 0, 0, 0, 0, 0]);
let result = decode_bmp(&bmp);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.width, 2);
assert_eq!(img.height, 2);
}
#[test]
fn decode_bmp_rejects_truncated_pixels_and_compression() {
let mut truncated = b"BM".to_vec();
truncated.extend_from_slice(&58u32.to_le_bytes());
truncated.extend_from_slice(&[0u8; 4]);
truncated.extend_from_slice(&54u32.to_le_bytes());
truncated.extend_from_slice(&40u32.to_le_bytes());
truncated.extend_from_slice(&2u32.to_le_bytes());
truncated.extend_from_slice(&2i32.to_le_bytes());
truncated.extend_from_slice(&1u16.to_le_bytes());
truncated.extend_from_slice(&24u16.to_le_bytes());
truncated.extend_from_slice(&[0u8; 24]);
truncated.extend_from_slice(&[0u8; 4]);
assert!(decode_bmp(&truncated).unwrap_err().contains("truncated"));
let mut compressed = truncated;
compressed[30..34].copy_from_slice(&1u32.to_le_bytes());
assert!(decode_bmp(&compressed).unwrap_err().contains("Compressed"));
}
fn make_png(
width: u32,
height: u32,
bit_depth: u8,
color_type: u8,
interlace: u8,
scanlines: &[u8],
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"\x89PNG\r\n\x1a\n");
let mut ihdr = Vec::with_capacity(13);
ihdr.extend_from_slice(&width.to_be_bytes());
ihdr.extend_from_slice(&height.to_be_bytes());
ihdr.push(bit_depth);
ihdr.push(color_type);
ihdr.push(0); ihdr.push(0); ihdr.push(interlace);
write_test_chunk(&mut out, b"IHDR", &ihdr);
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(scanlines, 0);
write_test_chunk(&mut out, b"IDAT", &compressed);
write_test_chunk(&mut out, b"IEND", &[]);
out
}
fn write_test_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
out.extend_from_slice(chunk_type);
out.extend_from_slice(data);
out.extend_from_slice(&[0u8; 4]);
}
fn append_palette_trns(out: &mut Vec<u8>, entries: &[[u8; 3]], alphas: &[u8]) {
let palette_bytes: Vec<u8> = entries.iter().flatten().copied().collect();
write_test_chunk(out, b"PLTE", &palette_bytes);
write_test_chunk(out, b"tRNS", alphas);
}
#[test]
fn decode_png_minimal_header() {
let png = make_png(1, 1, 8, 6, 0, &[0, 0, 0, 0, 255]);
assert_eq!(detect_format(&png), ImageFormat::Png);
let img = decode_png(&png).unwrap();
assert_eq!(img.format, ImageFormat::Png);
assert_eq!(img.width, 1);
assert_eq!(img.height, 1);
}
#[cfg(not(feature = "svg-rasterizer"))]
#[test]
fn decode_svg_returns_not_implemented() {
let svg = b"<svg width=\"100\" height=\"50\" xmlns=\"http://www.w3.org/2000/svg\"></svg>";
let err = decode_svg(svg).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[cfg(not(feature = "svg-rasterizer"))]
#[test]
fn decode_svgz_returns_not_implemented() {
let svg = b"<svg width=\"100\" height=\"50\" xmlns=\"http://www.w3.org/2000/svg\"></svg>";
let compressed = miniz_oxide::deflate::compress_to_vec(svg, 6);
let err = decode_svgz(&compressed).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[test]
fn decode_to_rgba8_converts() {
let png = make_png(1, 1, 8, 6, 0, &[0, 12, 34, 56, 78]);
let img = decode_to_rgba8(&png).unwrap();
assert_eq!(img.format, ImageFormat::Rgba8);
assert_eq!(img.width, 1);
assert_eq!(img.height, 1);
}
#[test]
fn decode_jpeg_detects_dimensions() {
let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xE0];
let app0_len = 16u16;
jpeg.extend_from_slice(&app0_len.to_be_bytes());
jpeg.extend_from_slice(b"JFIF\x00");
jpeg.extend_from_slice(&[0u8; 9]); jpeg.push(0xFF);
jpeg.push(0xC0); jpeg.extend_from_slice(&17u16.to_be_bytes()); jpeg.push(8); jpeg.extend_from_slice(&200u16.to_be_bytes()); jpeg.extend_from_slice(&300u16.to_be_bytes()); jpeg.push(3); jpeg.extend_from_slice(&[0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01]); jpeg.extend_from_slice(&[0xFF, 0xD9]);
let result = decode_jpeg(&jpeg);
assert!(result.is_err(), "JPEG decoder should return error for incomplete data");
}
#[cfg(not(feature = "image-codecs"))]
#[test]
fn decode_tiff_returns_not_implemented() {
let tiff = b"II\x2a\x00\x08\x00\x00\x00";
let err = decode_tiff(tiff).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[cfg(not(feature = "image-codecs"))]
#[test]
fn decode_webp_returns_not_implemented() {
let mut webp = b"RIFF".to_vec();
webp.extend_from_slice(&26u32.to_le_bytes()); webp.extend_from_slice(b"WEBP");
webp.extend_from_slice(b"VP8 ");
webp.extend_from_slice(&[0x00; 10]); let err = decode_webp(&webp).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[cfg(not(feature = "image-codecs"))]
#[test]
fn decode_ico_returns_not_implemented() {
let mut ico = vec![0x00, 0x00, 0x01, 0x00, 0x01, 0x00]; ico.extend_from_slice(&[
16, 16, 0, 0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00,
]);
let err = decode_ico(&ico).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[test]
fn decode_pnm_binary_grayscale() {
let pnm = b"P5\n3 2\n255\n\x00\x80\xFF\x10\x20\x30";
let result = decode_pnm(pnm);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.format, ImageFormat::Pnm);
}
#[test]
fn decode_pnm_binary_rgb() {
let pnm = b"P6\n1 2\n255\n\x00\x00\x00\xFF\xFF\xFF";
let img = decode_pnm(pnm).unwrap();
assert_eq!(img.format, ImageFormat::Pnm);
assert_eq!(img.data.as_bytes(), &[0, 0, 0, 255, 255, 255]);
}
#[test]
fn decode_pnm_binary_rejects_truncated_data() {
let grayscale = b"P5\n3 2\n255\n\x00\x80";
let grayscale_err = decode_pnm(grayscale).unwrap_err();
assert!(grayscale_err.contains("truncated"), "unexpected error: {grayscale_err}");
let rgb = b"P6\n1 1\n255\n\xFF\x00";
let rgb_err = decode_pnm(rgb).unwrap_err();
assert!(rgb_err.contains("truncated"), "unexpected error: {rgb_err}");
}
#[test]
fn decode_pnm_binary_bitmap_uses_msb_first_bits() {
let pnm = b"P4\n10 2\n\xA0\x00\x40\x00";
let img = decode_pnm(pnm).unwrap();
assert_eq!(img.width, 10);
assert_eq!(img.height, 2);
let expected_values = [
0, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255,
255, 255,
];
let expected_rgb: Vec<u8> =
expected_values.into_iter().flat_map(|value| [value; 3]).collect();
assert_eq!(img.data.as_bytes(), expected_rgb.as_slice());
}
#[test]
fn decode_pnm_binary_bitmap_rejects_truncated_data() {
let pnm = b"P4\n10 2\n\xA0";
let err = decode_pnm(pnm).unwrap_err();
assert!(err.contains("truncated"), "unexpected error: {err}");
}
#[test]
fn decode_pnm_ascii_bitmap_grayscale_and_rgb() {
let bitmap = b"P1\n# bitmap comment\n2 1\n0 1\n";
let bitmap = decode_pnm(bitmap).unwrap();
assert_eq!(bitmap.data.as_bytes(), &[255, 255, 255, 0, 0, 0]);
let grayscale = b"P2\n2 1\n100\n0 50\n";
let grayscale = decode_pnm(grayscale).unwrap();
assert_eq!(grayscale.data.as_bytes(), &[0, 0, 0, 127, 127, 127]);
let rgb = b"P3\n1 1\n255\n12 34 56\n";
let rgb = decode_pnm(rgb).unwrap();
assert_eq!(rgb.data.as_bytes(), &[12, 34, 56]);
}
#[test]
fn decode_pnm_ascii_rejects_invalid_sample_values() {
let pnm = b"P2\n1 1\n10\n11\n";
let err = decode_pnm(pnm).unwrap_err();
assert!(err.contains("exceeds maxval"), "unexpected error: {err}");
}
#[cfg(not(feature = "image-codecs"))]
#[test]
fn decode_avif_returns_not_implemented() {
let avif = b"\x00\x00\x00\x20ftypavif\x00\x00\x00\x00";
let err = decode_avif(avif).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[cfg(not(feature = "image-codecs"))]
#[test]
fn decode_gif_returns_not_implemented() {
let mut gif = b"GIF89a".to_vec();
gif.extend_from_slice(&2u16.to_le_bytes()); gif.extend_from_slice(&2u16.to_le_bytes()); gif.push(0xF0); gif.push(0); gif.push(0); for i in 0..16 {
let c = (i * 16) as u8;
gif.push(c);
gif.push(c);
gif.push(c);
}
gif.push(0x3B);
let err = decode_gif(&gif).unwrap_err();
assert!(err.contains("not implemented"), "unexpected error: {err}");
}
#[cfg(feature = "image-codecs")]
#[test]
fn decode_extended_image_codecs_from_library_outputs() {
let image =
DecodedImage::new(ImageFormat::Rgba8, ImageData::Rgba8(vec![255, 0, 0, 255]), 1, 1);
for format in [ImageFormat::Gif, ImageFormat::Tiff] {
let encoded = crate::image::encoder::encode(&image, format).unwrap();
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.as_rgba8().as_bytes().len(), 4);
}
}
#[cfg(feature = "image-codecs")]
#[test]
fn decode_gif_animation_preserves_frame_metadata() {
let image =
DecodedImage::new(ImageFormat::Rgba8, ImageData::Rgba8(vec![255, 0, 0, 255]), 1, 1);
let encoded = crate::image::encoder::encode(&image, ImageFormat::Gif).unwrap();
let animation = decode_animation(&encoded).unwrap();
assert_eq!(animation.frame_count(), 1);
assert_eq!(animation.delays.len(), 1);
assert_eq!(animation.frames[0].width, 1);
}
#[cfg(feature = "svg-rasterizer")]
#[test]
fn decode_svg_with_rasterizer_from_library_output() {
let image =
DecodedImage::new(ImageFormat::Rgba8, ImageData::Rgba8(vec![255, 0, 0, 255]), 1, 1);
let encoded = crate::image::encoder::encode(&image, ImageFormat::Svg).unwrap();
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.as_rgba8().as_bytes().len(), 4);
}
#[test]
fn png_roundtrip_matches_encoder_output() {
let src = DecodedImage::new(
ImageFormat::Rgba8,
ImageData::Rgba8(vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, 12, 34, 56, 78,
90, 12, 34, 56, 200, 100, 50, 25, 1, 2, 3, 4,
]),
4,
2,
);
let encoded = crate::image::encoder::encode(&src, ImageFormat::Png).unwrap();
let decoded = decode_png(&encoded).unwrap();
assert_eq!(decoded.width, 4);
assert_eq!(decoded.height, 2);
let rgba = decoded.as_rgba8();
assert_eq!(rgba.as_bytes(), src.data.as_bytes());
}
#[test]
fn png_decodes_rgb_filter_none() {
let w = 4usize;
let h = 4usize;
let mut scanlines = Vec::new();
let mut expected = Vec::new();
for y in 0..h {
scanlines.push(0); for x in 0..w {
let v = (x * 31 + y * 7) as u8;
scanlines.extend_from_slice(&[v, v.wrapping_mul(2), v.wrapping_mul(3)]);
expected.extend_from_slice(&[v, v.wrapping_mul(2), v.wrapping_mul(3)]);
}
}
let png = make_png(4, 4, 8, 2, 0, &scanlines);
let img = decode_png(&png).unwrap();
assert_eq!(img.data.as_bytes(), &expected);
}
#[test]
fn png_decodes_rgb_filter_sub() {
let w = 4usize;
let h = 4usize;
let colors = [[200u8, 30, 90], [10, 220, 40], [5, 9, 250], [128, 128, 128]];
let mut scanlines = Vec::new();
let mut expected = Vec::new();
for &c in colors.iter() {
scanlines.push(1); for x in 0..w {
if x == 0 {
scanlines.extend_from_slice(&c);
} else {
scanlines.extend_from_slice(&[0, 0, 0]);
}
expected.extend_from_slice(&c);
}
}
let png = make_png(w as u32, h as u32, 8, 2, 0, &scanlines);
let img = decode_png(&png).unwrap();
assert_eq!(img.data.as_bytes(), &expected);
}
#[test]
fn png_decodes_rgba_filter_up_average_paeth() {
let w = 2usize;
let h = 3usize;
let rows: Vec<[u8; 8]> = vec![
[10, 20, 30, 255, 40, 50, 60, 255],
[70, 80, 90, 255, 100, 110, 120, 255],
[130, 140, 150, 255, 160, 170, 180, 255],
];
let mut scanlines = Vec::new();
let mut expected = Vec::new();
for (fi, row) in rows.iter().enumerate() {
let filter = match fi {
0 => 2, 1 => 3, _ => 4, };
scanlines.push(filter);
let up = if fi == 0 { [0u8; 8] } else { rows[fi - 1] };
for x in 0..w {
for c in 0..4 {
let raw = row[x * 4 + c];
let a = if x > 0 { row[(x - 1) * 4 + c] } else { 0 };
let b = up[x * 4 + c];
let c_prev = if x > 0 { up[(x - 1) * 4 + c] } else { 0 };
let filt = match filter {
2 => raw.wrapping_sub(b),
3 => {
let pred = ((a as u16 + b as u16) / 2) as u8;
raw.wrapping_sub(pred)
}
_ => {
let rec = paeth_predictor(a, b, c_prev);
raw.wrapping_sub(rec)
}
};
scanlines.push(filt);
}
}
expected.extend_from_slice(&row[..]);
}
let png = make_png(w as u32, h as u32, 8, 6, 0, &scanlines);
let img = decode_png(&png).unwrap();
let rgba = img.as_rgba8();
assert_eq!(rgba.as_bytes(), &expected);
}
#[test]
fn png_grayscale_16bit_keeps_high_byte() {
let scanlines = [
0, 0xAB, 0xCD, 0x12, 0x34, 0xFF, 0x00, 0x00, 0x00, 0, 0x80, 0x00, 0x01, 0xFF, 0x42, 0x42, 0x99, 0x00,
];
let png = make_png(4, 2, 16, 0, 0, &scanlines);
let img = decode_png(&png).unwrap();
assert_eq!(img.data.as_bytes(), &[0xAB, 0x12, 0xFF, 0x00, 0x80, 0x01, 0x42, 0x99]);
}
#[test]
fn png_rgba_16bit_keeps_high_byte() {
let scanlines = [
0, 0x11, 0x00, 0x22, 0x00, 0x33, 0x00, 0x44, 0x00, 0x55, 0x00, 0x66, 0x00, 0x77, 0x00, 0x88, 0x00, ];
let png = make_png(2, 1, 16, 6, 0, &scanlines);
let img = decode_png(&png).unwrap();
assert_eq!(img.data.as_bytes(), &[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
}
#[test]
fn png_indexed_palette_with_trns() {
let mut out = Vec::new();
out.extend_from_slice(b"\x89PNG\r\n\x1a\n");
let mut ihdr = Vec::with_capacity(13);
ihdr.extend_from_slice(&3u32.to_be_bytes());
ihdr.extend_from_slice(&1u32.to_be_bytes());
ihdr.push(8); ihdr.push(3); ihdr.push(0);
ihdr.push(0);
ihdr.push(0); write_test_chunk(&mut out, b"IHDR", &ihdr);
append_palette_trns(&mut out, &[[255, 0, 0], [0, 255, 0], [0, 0, 255]], &[255, 128, 0]);
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&[0u8, 0, 1, 2], 0); write_test_chunk(&mut out, b"IDAT", &compressed);
write_test_chunk(&mut out, b"IEND", &[]);
let img = decode_png(&out).unwrap();
assert_eq!(img.data.as_bytes(), &[255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0]);
}
#[test]
fn png_rejects_interlaced() {
let scanlines = [0u8, 1, 2, 3]; let png = make_png(2, 1, 8, 6, 1, &scanlines);
let err = decode_png(&png).unwrap_err();
assert!(err.contains("interlaced") || err.contains("Interlaced"), "{err}");
}
#[test]
fn png_chunk_declared_length_out_of_bounds_is_err() {
let mut bad = b"\x89PNG\r\n\x1a\n".to_vec();
bad.extend_from_slice(&u32::MAX.to_be_bytes()); bad.extend_from_slice(b"IDAT");
bad.extend_from_slice(&[0u8; 4]); let err = decode_png(&bad).unwrap_err();
assert!(err.contains("declares"), "unexpected error: {err}");
}
#[test]
fn png_truncated_scanline_data_is_err() {
let png = make_png(8, 8, 8, 6, 0, &[0, 1, 2, 3, 4, 5, 6, 7, 8]);
let err = decode_png(&png).unwrap_err();
assert!(err.contains("truncated") || err.contains("decompress"), "unexpected error: {err}");
}
}