use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ResourceType {
Image,
Audio,
Video,
Chart,
Ole,
Other,
}
impl ResourceType {
pub fn from_mime_type(mime: &str) -> Self {
let mime_lower = mime.to_lowercase();
if mime_lower.starts_with("image/") {
ResourceType::Image
} else if mime_lower.starts_with("audio/") {
ResourceType::Audio
} else if mime_lower.starts_with("video/") {
ResourceType::Video
} else if mime_lower.contains("chart") {
ResourceType::Chart
} else if mime_lower.contains("ole") || mime_lower.contains("oleobject") {
ResourceType::Ole
} else {
ResourceType::Other
}
}
pub fn from_extension(ext: &str) -> Self {
match ext.to_lowercase().as_str() {
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "tiff" | "tif" | "wmf" | "emf" | "svg" => {
ResourceType::Image
}
"mp3" | "wav" | "ogg" | "m4a" | "wma" => ResourceType::Audio,
"mp4" | "avi" | "mov" | "wmv" | "webm" => ResourceType::Video,
_ => ResourceType::Other,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub resource_type: ResourceType,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip)]
pub data: Vec<u8>,
pub size: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alt_text: Option<String>,
}
impl Resource {
pub fn new(resource_type: ResourceType, data: Vec<u8>) -> Self {
let size = data.len();
Self {
resource_type,
filename: None,
mime_type: None,
data,
size,
width: None,
height: None,
alt_text: None,
}
}
pub fn image(data: Vec<u8>, filename: Option<String>) -> Self {
let size = data.len();
let mime_type = filename.as_ref().and_then(|f| Self::mime_from_filename(f));
Self {
resource_type: ResourceType::Image,
filename,
mime_type,
data,
size,
width: None,
height: None,
alt_text: None,
}
}
pub fn extension(&self) -> Option<&str> {
self.filename.as_ref().and_then(|f| {
f.rsplit('.')
.next()
.filter(|ext| ext.len() <= 5 && ext.chars().all(|c| c.is_alphanumeric()))
})
}
pub fn mime_from_filename(filename: &str) -> Option<String> {
let ext = filename.rsplit('.').next()?.to_lowercase();
let mime = match ext.as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"bmp" => "image/bmp",
"tiff" | "tif" => "image/tiff",
"svg" => "image/svg+xml",
"wmf" => "image/x-wmf",
"emf" => "image/x-emf",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
"ogg" => "audio/ogg",
"m4a" => "audio/mp4",
"mp4" => "video/mp4",
"avi" => "video/x-msvideo",
"mov" => "video/quicktime",
"webm" => "video/webm",
_ => return None,
};
Some(mime.to_string())
}
pub fn suggested_filename(&self, id: &str) -> String {
if let Some(ref filename) = self.filename {
filename.clone()
} else {
let ext = match self.resource_type {
ResourceType::Image => self
.mime_type
.as_ref()
.and_then(|m| Self::extension_from_mime(m))
.unwrap_or("png"),
ResourceType::Audio => "mp3",
ResourceType::Video => "mp4",
ResourceType::Chart => "png",
ResourceType::Ole => "bin",
ResourceType::Other => "bin",
};
format!("{}.{}", id, ext)
}
}
fn extension_from_mime(mime: &str) -> Option<&'static str> {
match mime {
"image/png" => Some("png"),
"image/jpeg" => Some("jpg"),
"image/gif" => Some("gif"),
"image/bmp" => Some("bmp"),
"image/tiff" => Some("tiff"),
"image/svg+xml" => Some("svg"),
"image/x-wmf" => Some("wmf"),
"image/x-emf" => Some("emf"),
"audio/mpeg" => Some("mp3"),
"audio/wav" => Some("wav"),
"video/mp4" => Some("mp4"),
_ => None,
}
}
pub fn save_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::write(path, &self.data)
}
pub fn is_image(&self) -> bool {
matches!(
self.resource_type,
ResourceType::Image | ResourceType::Chart
)
}
pub fn is_media(&self) -> bool {
matches!(
self.resource_type,
ResourceType::Audio | ResourceType::Video
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_type_from_mime() {
assert_eq!(
ResourceType::from_mime_type("image/png"),
ResourceType::Image
);
assert_eq!(
ResourceType::from_mime_type("IMAGE/JPEG"),
ResourceType::Image
);
assert_eq!(
ResourceType::from_mime_type("audio/mpeg"),
ResourceType::Audio
);
assert_eq!(
ResourceType::from_mime_type("video/mp4"),
ResourceType::Video
);
assert_eq!(
ResourceType::from_mime_type("application/octet-stream"),
ResourceType::Other
);
}
#[test]
fn test_resource_type_from_extension() {
assert_eq!(ResourceType::from_extension("png"), ResourceType::Image);
assert_eq!(ResourceType::from_extension("JPG"), ResourceType::Image);
assert_eq!(ResourceType::from_extension("mp3"), ResourceType::Audio);
assert_eq!(ResourceType::from_extension("mp4"), ResourceType::Video);
assert_eq!(ResourceType::from_extension("xyz"), ResourceType::Other);
}
#[test]
fn test_resource_creation() {
let data = vec![0x89, 0x50, 0x4E, 0x47]; let resource = Resource::image(data.clone(), Some("test.png".to_string()));
assert_eq!(resource.resource_type, ResourceType::Image);
assert_eq!(resource.size, 4);
assert_eq!(resource.filename, Some("test.png".to_string()));
assert_eq!(resource.mime_type, Some("image/png".to_string()));
}
#[test]
fn test_resource_extension() {
let resource = Resource::image(vec![], Some("image.png".to_string()));
assert_eq!(resource.extension(), Some("png"));
let resource2 = Resource::image(vec![], Some("photo.JPEG".to_string()));
assert_eq!(resource2.extension(), Some("JPEG"));
}
#[test]
fn test_suggested_filename() {
let resource = Resource::image(vec![], Some("original.png".to_string()));
assert_eq!(resource.suggested_filename("img1"), "original.png");
let mut resource2 = Resource::new(ResourceType::Image, vec![]);
resource2.mime_type = Some("image/jpeg".to_string());
assert_eq!(resource2.suggested_filename("img2"), "img2.jpg");
}
#[test]
fn test_is_image() {
let image = Resource::new(ResourceType::Image, vec![]);
assert!(image.is_image());
let chart = Resource::new(ResourceType::Chart, vec![]);
assert!(chart.is_image());
let audio = Resource::new(ResourceType::Audio, vec![]);
assert!(!audio.is_image());
}
#[test]
fn test_is_media() {
let audio = Resource::new(ResourceType::Audio, vec![]);
assert!(audio.is_media());
let video = Resource::new(ResourceType::Video, vec![]);
assert!(video.is_media());
let image = Resource::new(ResourceType::Image, vec![]);
assert!(!image.is_media());
}
}