use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
#[serde(skip_serializing)]
pub data: Vec<u8>,
pub mime_type: String,
pub resource_type: ResourceType,
pub filename: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub color_space: Option<String>,
pub bits_per_component: Option<u8>,
}
impl Resource {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>, resource_type: ResourceType) -> Self {
Self {
data,
mime_type: mime_type.into(),
resource_type,
filename: None,
width: None,
height: None,
color_space: None,
bits_per_component: None,
}
}
pub fn image(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self::new(data, mime_type, ResourceType::Image)
}
pub fn jpeg(data: Vec<u8>) -> Self {
Self::image(data, "image/jpeg")
}
pub fn png(data: Vec<u8>) -> Self {
Self::image(data, "image/png")
}
pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
pub fn with_color_space(mut self, color_space: impl Into<String>) -> Self {
self.color_space = Some(color_space.into());
self
}
pub fn with_bits_per_component(mut self, bits: u8) -> Self {
self.bits_per_component = Some(bits);
self
}
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
pub fn size(&self) -> usize {
self.data.len()
}
pub fn is_image(&self) -> bool {
matches!(self.resource_type, ResourceType::Image)
}
pub fn is_font(&self) -> bool {
matches!(self.resource_type, ResourceType::Font)
}
pub fn suggested_filename(&self, id: &str) -> String {
if let Some(ref filename) = self.filename {
return filename.clone();
}
let extension = self.extension();
format!("{}.{}", id, extension)
}
pub fn extension(&self) -> &str {
match self.mime_type.as_str() {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/gif" => "gif",
"image/tiff" => "tiff",
"image/bmp" => "bmp",
"image/webp" => "webp",
"image/jp2" | "image/jpeg2000" => "jp2",
"application/pdf" => "pdf",
"font/ttf" | "font/truetype" => "ttf",
"font/otf" | "font/opentype" => "otf",
"font/woff" => "woff",
"font/woff2" => "woff2",
_ if self.is_image() => "raw",
_ => "bin",
}
}
pub fn detect_mime_type(data: &[u8]) -> Option<&'static str> {
if data.len() < 8 {
return None;
}
if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some("image/jpeg");
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return Some("image/png");
}
if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") {
return Some("image/gif");
}
if data.starts_with(&[0x49, 0x49, 0x2A, 0x00])
|| data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A])
{
return Some("image/tiff");
}
if data.starts_with(b"BM") {
return Some("image/bmp");
}
if data.len() >= 12 && data.starts_with(b"RIFF") && &data[8..12] == b"WEBP" {
return Some("image/webp");
}
if data.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20]) {
return Some("image/jp2");
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ResourceType {
Image,
Font,
Attachment,
Other,
}
impl std::fmt::Display for ResourceType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResourceType::Image => write!(f, "image"),
ResourceType::Font => write!(f, "font"),
ResourceType::Attachment => write!(f, "attachment"),
ResourceType::Other => write!(f, "other"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_new() {
let res = Resource::jpeg(vec![0xFF, 0xD8, 0xFF]);
assert!(res.is_image());
assert_eq!(res.mime_type, "image/jpeg");
assert_eq!(res.extension(), "jpg");
}
#[test]
fn test_detect_mime_type() {
let jpeg_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
assert_eq!(Resource::detect_mime_type(&jpeg_data), Some("image/jpeg"));
let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(Resource::detect_mime_type(&png_data), Some("image/png"));
let unknown = vec![0x00, 0x00, 0x00, 0x00];
assert_eq!(Resource::detect_mime_type(&unknown), None);
}
#[test]
fn test_suggested_filename() {
let res = Resource::jpeg(vec![]).with_filename("photo.jpg");
assert_eq!(res.suggested_filename("img1"), "photo.jpg");
let res2 = Resource::png(vec![]);
assert_eq!(res2.suggested_filename("img2"), "img2.png");
}
}