#[derive(Debug, Clone, PartialEq)]
pub struct Asset {
pub id: String,
pub media_type: String,
pub bytes: Vec<u8>,
pub width_px: Option<u32>,
pub height_px: Option<u32>,
pub dpi: Option<f32>,
}
impl Asset {
pub fn new(id: impl Into<String>, bytes: Vec<u8>) -> Self {
let media_type = match ImageFormat::sniff(&bytes) {
ImageFormat::Supported { media_type, .. } => media_type,
ImageFormat::Unsupported(_) => "application/octet-stream",
};
let info = header_info(&bytes);
Self {
id: id.into(),
media_type: media_type.into(),
width_px: info.map(|i| i.0),
height_px: info.map(|i| i.1),
dpi: info.and_then(|i| i.2),
bytes,
}
}
pub fn is_valid_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= 255
&& !id.starts_with('.')
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Supported {
media_type: &'static str,
ext: &'static str,
},
Unsupported(&'static str),
}
impl ImageFormat {
pub fn sniff(b: &[u8]) -> Self {
let ok = |media_type, ext| ImageFormat::Supported { media_type, ext };
match b {
[0x89, b'P', b'N', b'G', ..] => ok("image/png", "png"),
[0xFF, 0xD8, 0xFF, ..] => ok("image/jpeg", "jpg"),
[b'G', b'I', b'F', b'8', ..] => ok("image/gif", "gif"),
[
b'R',
b'I',
b'F',
b'F',
_,
_,
_,
_,
b'W',
b'E',
b'B',
b'P',
..,
] => ok("image/webp", "webp"),
[0xD7, 0xCD, 0xC6, 0x9A, ..] | [0x01 | 0x02, 0x00, 0x09, 0x00, ..] => {
Self::Unsupported("WMF")
}
[0x01, 0x00, 0x00, 0x00, ..] if b.get(40..44) == Some(b" EMF") => {
Self::Unsupported("EMF")
}
[b'I', b'I', 0x2A, 0x00, ..] | [b'M', b'M', 0x00, 0x2A, ..] => {
Self::Unsupported("TIFF")
}
[b'B', b'M', ..] => Self::Unsupported("BMP"),
_ => {
let head = String::from_utf8_lossy(&b[..b.len().min(1024)]);
if head.contains("<svg") {
ok("image/svg+xml", "svg")
} else {
Self::Unsupported("an unknown")
}
}
}
}
}
fn header_info(b: &[u8]) -> Option<(u32, u32, Option<f32>)> {
let be16 = |i: usize| {
b.get(i..i + 2)
.map(|s| u32::from(u16::from_be_bytes([s[0], s[1]])))
};
let be32 = |i: usize| {
b.get(i..i + 4)
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
};
let le16 = |i: usize| {
b.get(i..i + 2)
.map(|s| u32::from(u16::from_le_bytes([s[0], s[1]])))
};
let le24 = |i: usize| {
b.get(i..i + 3)
.map(|s| u32::from(s[0]) | u32::from(s[1]) << 8 | u32::from(s[2]) << 16)
};
let ImageFormat::Supported { media_type, .. } = ImageFormat::sniff(b) else {
return None;
};
match media_type {
"image/png" => {
let (w, h) = (be32(16)?, be32(20)?);
let mut i = 8;
let mut dpi = None;
while let (Some(len), Some(kind)) = (be32(i), b.get(i + 4..i + 8)) {
match kind {
b"pHYs" if b.get(i + 16) == Some(&1) => {
dpi = be32(i + 8).map(|ppm| ppm as f32 * 0.0254);
break;
}
b"IDAT" | b"IEND" => break,
_ => i = i.checked_add(12 + len as usize)?,
}
}
Some((w, h, dpi))
}
"image/jpeg" => {
let mut i = 2;
let mut dpi = None;
loop {
if b.get(i) != Some(&0xFF) {
return None;
}
let marker = *b.get(i + 1)?;
if marker == 0xFF {
i += 1; continue;
}
let len = be16(i + 2)? as usize;
let data = i + 4;
match marker {
0xE0 if b.get(data..data + 5) == Some(b"JFIF\0") => {
let x = be16(data + 8)? as f32;
dpi = match b.get(data + 7) {
Some(1) => Some(x),
Some(2) => Some(x * 2.54),
_ => None,
};
}
0xC0..=0xCF if !matches!(marker, 0xC4 | 0xC8 | 0xCC) => {
let h = be16(data + 1)?;
let w = be16(data + 3)?;
return Some((w, h, dpi.filter(|d| *d > 1.0)));
}
_ => {}
}
i = data.checked_add(len.checked_sub(2)?)?;
}
}
"image/gif" => Some((le16(6)?, le16(8)?, None)),
"image/webp" => match b.get(12..16)? {
b"VP8X" => Some((le24(24)? + 1, le24(27)? + 1, None)),
b"VP8L" => {
let bits = b.get(21..25)?;
let v = u32::from_le_bytes([bits[0], bits[1], bits[2], bits[3]]);
Some(((v & 0x3FFF) + 1, ((v >> 14) & 0x3FFF) + 1, None))
}
b"VP8 " => Some((le16(26)? & 0x3FFF, le16(28)? & 0x3FFF, None)),
_ => None,
},
_ => None,
}
}
#[doc(hidden)]
pub mod testdata {
pub fn png_3x2_300dpi() -> Vec<u8> {
let mut b = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
b.extend_from_slice(&13u32.to_be_bytes());
b.extend_from_slice(b"IHDR");
b.extend_from_slice(&3u32.to_be_bytes());
b.extend_from_slice(&2u32.to_be_bytes());
b.extend_from_slice(&[8, 2, 0, 0, 0]);
b.extend_from_slice(&[0; 4]);
b.extend_from_slice(&9u32.to_be_bytes());
b.extend_from_slice(b"pHYs");
b.extend_from_slice(&11811u32.to_be_bytes());
b.extend_from_slice(&11811u32.to_be_bytes());
b.push(1);
b.extend_from_slice(&[0; 4]);
b.extend_from_slice(&0u32.to_be_bytes());
b.extend_from_slice(b"IEND");
b.extend_from_slice(&[0; 4]);
b
}
}
#[cfg(test)]
mod tests {
use super::testdata::png_3x2_300dpi;
use super::*;
#[test]
fn png_header() {
let (w, h, dpi) = header_info(&png_3x2_300dpi()).unwrap();
assert_eq!((w, h), (3, 2));
assert!((dpi.unwrap() - 300.0).abs() < 0.1);
}
#[test]
fn jpeg_header() {
let mut b = vec![0xFF, 0xD8];
b.extend_from_slice(&[0xFF, 0xE0, 0x00, 0x10]);
b.extend_from_slice(b"JFIF\0");
b.extend_from_slice(&[1, 1, 1, 0, 72, 0, 72, 0, 0]);
b.extend_from_slice(&[0xFF, 0xC0, 0x00, 0x11, 8, 0x01, 0xE0, 0x02, 0x80]);
b.extend_from_slice(&[0; 12]);
assert_eq!(header_info(&b), Some((640, 480, Some(72.0))));
}
#[test]
fn truncated_headers_do_not_panic() {
let png = png_3x2_300dpi();
for n in 0..png.len() {
let _ = header_info(&png[..n]);
}
for b in [
&[0xFF, 0xD8, 0xFF][..],
&[0xFF, 0xD8, 0xFF, 0xC0, 0, 1][..],
b"GIF8",
b"RIFF0000WEBPVP8X",
] {
let _ = header_info(b);
}
}
#[test]
fn formats_by_content() {
assert_eq!(
ImageFormat::sniff(b"GIF89a.."),
ImageFormat::Supported {
media_type: "image/gif",
ext: "gif"
}
);
assert_eq!(
ImageFormat::sniff(&[0xD7, 0xCD, 0xC6, 0x9A, 0]),
ImageFormat::Unsupported("WMF")
);
assert_eq!(
ImageFormat::sniff(
br#"<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"/>"#
),
ImageFormat::Supported {
media_type: "image/svg+xml",
ext: "svg"
}
);
}
#[test]
fn new_reads_metadata() {
let a = Asset::new("p.png", png_3x2_300dpi());
assert_eq!(a.media_type, "image/png");
assert_eq!((a.width_px, a.height_px), (Some(3), Some(2)));
let junk = Asset::new("x.bin", b"hello".to_vec());
assert_eq!(junk.media_type, "application/octet-stream");
assert_eq!(junk.width_px, None);
}
#[test]
fn asset_ids() {
for ok in ["image1.png", "Cover_2-final.jpg", "a"] {
assert!(Asset::is_valid_id(ok), "{ok}");
}
for bad in [
"", ".hidden", "../x.png", "a/b.png", "a\\b.png", "ΓΌ.png", "a b.png",
] {
assert!(!Asset::is_valid_id(bad), "{bad}");
}
}
}