use anyhow::{Context, Result};
use image::{DynamicImage, GenericImageView};
use std::io::Cursor;
pub fn convert_to_png(bytes: &[u8], mime: &str) -> Result<Vec<u8>> {
if mime == "image/png" {
if bytes.len() >= 8 && bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return Ok(bytes.to_vec());
}
}
let img = decode_image(bytes, mime)?;
let img = apply_exif_orientation(&img, bytes);
let mut buf = Vec::new();
img.write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Png)
.context("Failed to encode PNG")?;
Ok(buf)
}
fn decode_image(bytes: &[u8], mime: &str) -> Result<DynamicImage> {
let format = match mime {
"image/png" => Some(image::ImageFormat::Png),
"image/jpeg" | "image/jpg" => Some(image::ImageFormat::Jpeg),
"image/gif" => Some(image::ImageFormat::Gif),
"image/webp" => Some(image::ImageFormat::WebP),
"image/bmp" => Some(image::ImageFormat::Bmp),
"image/tiff" => Some(image::ImageFormat::Tiff),
_ => None,
};
if let Some(fmt) = format {
image::load_from_memory_with_format(bytes, fmt)
.context(format!("Failed to decode {} image", mime))
} else {
image::load_from_memory(bytes)
.with_context(|| format!("Failed to decode image (guessed: {})", mime))
}
}
pub fn get_image_dimensions(bytes: &[u8], mime: &str) -> Result<(u32, u32)> {
if mime == "image/png" && bytes.len() >= 24 {
if let Some(dims) = get_png_dimensions_fast(&bytes) {
return Ok(dims);
}
}
let img = decode_image(bytes, mime)?;
Ok(img.dimensions())
}
fn get_png_dimensions_fast(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 32 {
return None;
}
if !bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return None;
}
let length = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
if &bytes[12..16] != b"IHDR" {
return None;
}
if length < 13 {
return None;
}
let width = u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
let height = u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
Some((width, height))
}
fn apply_exif_orientation(img: &DynamicImage, _bytes: &[u8]) -> DynamicImage {
img.clone()
}
pub fn ensure_upright(img: &DynamicImage) -> DynamicImage {
img.clone()
}
pub fn to_data_uri(bytes: &[u8], mime: &str) -> String {
use base64::Engine as _;
let base64_data = base64::engine::general_purpose::STANDARD.encode(bytes);
format!("data:{};base64,{}", mime, base64_data)
}
pub fn parse_data_uri(uri: &str) -> Option<(String, Vec<u8>)> {
use base64::Engine as _;
if !uri.starts_with("data:") {
return None;
}
let (mime_part, data_part) = uri.split_once(',')?;
let mime = mime_part
.strip_prefix("data:")
.and_then(|s| s.split(';').next())
.unwrap_or("image/png")
.to_string();
let bytes = base64::engine::general_purpose::STANDARD
.decode(data_part)
.ok()?;
Some((mime, bytes))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Png,
Jpeg,
Gif,
WebP,
Bmp,
Unknown,
}
impl ImageFormat {
pub fn mime_type(&self) -> &'static str {
match self {
ImageFormat::Png => "image/png",
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::Gif => "image/gif",
ImageFormat::WebP => "image/webp",
ImageFormat::Bmp => "image/bmp",
ImageFormat::Unknown => "application/octet-stream",
}
}
}
pub fn detect_format(bytes: &[u8]) -> ImageFormat {
if bytes.len() >= 8 {
if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return ImageFormat::Png;
}
if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
return ImageFormat::Jpeg;
}
if bytes.starts_with(&[0x47, 0x49, 0x46]) {
return ImageFormat::Gif;
}
if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
return ImageFormat::WebP;
}
if bytes.starts_with(&[0x42, 0x4D]) {
return ImageFormat::Bmp;
}
}
ImageFormat::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, RgbaImage, Rgba};
fn create_test_png() -> Vec<u8> {
let img: RgbaImage = ImageBuffer::from_pixel(10, 10, Rgba([255, 0, 0, 255]));
let mut buf = Vec::new();
img.write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Png)
.unwrap();
buf
}
fn create_test_jpeg() -> Vec<u8> {
use image::{ImageBuffer, RgbImage, Rgb};
let img: RgbImage = ImageBuffer::from_pixel(10, 10, Rgb([0, 255, 0]));
let mut buf = Vec::new();
img.write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Jpeg)
.unwrap();
buf
}
#[test]
fn test_convert_png_to_png() {
let png = create_test_png();
let result = convert_to_png(&png, "image/png").unwrap();
assert_eq!(result, png);
}
#[test]
fn test_convert_jpeg_to_png() {
let jpeg = create_test_jpeg();
let result = convert_to_png(&jpeg, "image/jpeg").unwrap();
assert!(result.starts_with(&[0x89, 0x50, 0x4E, 0x47]));
}
#[test]
fn test_get_png_dimensions_fast() {
let png = create_test_png();
let dims = get_png_dimensions_fast(&png).unwrap();
assert_eq!(dims, (10, 10));
}
#[test]
fn test_get_png_dimensions_fast_invalid() {
let data = vec![0x00, 0x01, 0x02];
assert!(get_png_dimensions_fast(&data).is_none());
}
#[test]
fn test_get_image_dimensions() {
let png = create_test_png();
let (w, h) = get_image_dimensions(&png, "image/png").unwrap();
assert_eq!(w, 10);
assert_eq!(h, 10);
}
#[test]
fn test_get_image_dimensions_from_jpeg() {
let jpeg = create_test_jpeg();
let (w, h) = get_image_dimensions(&jpeg, "image/jpeg").unwrap();
assert_eq!(w, 10);
assert_eq!(h, 10);
}
#[test]
fn test_detect_format_png() {
let png = create_test_png();
assert_eq!(detect_format(&png), ImageFormat::Png);
}
#[test]
fn test_detect_format_jpeg() {
let jpeg = create_test_jpeg();
assert_eq!(detect_format(&jpeg), ImageFormat::Jpeg);
}
#[test]
fn test_detect_format_unknown() {
let data = vec![0x00, 0x01, 0x02, 0x03];
assert_eq!(detect_format(&data), ImageFormat::Unknown);
}
#[test]
fn test_to_data_uri() {
let png = create_test_png();
let uri = to_data_uri(&png, "image/png");
assert!(uri.starts_with("data:image/png;base64,"));
assert!(uri.len() > png.len());
}
#[test]
fn test_parse_data_uri() {
let png = create_test_png();
let uri = to_data_uri(&png, "image/png");
let (mime, bytes) = parse_data_uri(&uri).unwrap();
assert_eq!(mime, "image/png");
assert_eq!(bytes, png);
}
#[test]
fn test_parse_data_uri_invalid() {
assert!(parse_data_uri("not a data uri").is_none());
}
#[test]
fn test_image_format_mime_types() {
assert_eq!(ImageFormat::Png.mime_type(), "image/png");
assert_eq!(ImageFormat::Jpeg.mime_type(), "image/jpeg");
assert_eq!(ImageFormat::Gif.mime_type(), "image/gif");
assert_eq!(ImageFormat::WebP.mime_type(), "image/webp");
assert_eq!(ImageFormat::Bmp.mime_type(), "image/bmp");
assert_eq!(ImageFormat::Unknown.mime_type(), "application/octet-stream");
}
#[test]
fn test_convert_empty_bytes() {
let result = convert_to_png(&[], "image/png");
assert!(result.is_err());
}
#[test]
fn test_ensure_upright() {
let png = create_test_png();
let img = decode_image(&png, "image/png").unwrap();
let result = ensure_upright(&img);
let (w, h) = result.dimensions();
assert_eq!((w, h), (10, 10));
}
}