use super::{ShapeId, ShapeStyle, ShapeTrait};
use kurbo::{Affine, BezPath, Point, Rect, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImageFormat {
Png,
Jpeg,
WebP,
}
impl ImageFormat {
pub fn mime_type(&self) -> &'static str {
match self {
ImageFormat::Png => "image/png",
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::WebP => "image/webp",
}
}
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"png" => Some(ImageFormat::Png),
"jpg" | "jpeg" => Some(ImageFormat::Jpeg),
"webp" => Some(ImageFormat::WebP),
_ => None,
}
}
pub fn from_magic_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return Some(ImageFormat::Png);
}
if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some(ImageFormat::Jpeg);
}
if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
return Some(ImageFormat::WebP);
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Image {
pub(crate) id: ShapeId,
pub position: Point,
pub width: f64,
pub height: f64,
pub source_width: u32,
pub source_height: u32,
pub format: ImageFormat,
pub data_base64: String,
pub style: ShapeStyle,
}
impl Image {
pub fn new(
position: Point,
data: &[u8],
source_width: u32,
source_height: u32,
format: ImageFormat,
) -> Self {
use base64::{Engine, engine::general_purpose::STANDARD};
Self {
id: Uuid::new_v4(),
position,
width: source_width as f64,
height: source_height as f64,
source_width,
source_height,
format,
data_base64: STANDARD.encode(data),
style: ShapeStyle::default(),
}
}
pub fn with_size(mut self, width: f64, height: f64) -> Self {
self.width = width;
self.height = height;
self
}
pub fn fit_within(mut self, max_width: f64, max_height: f64) -> Self {
let aspect = self.source_width as f64 / self.source_height as f64;
let target_aspect = max_width / max_height;
if aspect > target_aspect {
self.width = max_width;
self.height = max_width / aspect;
} else {
self.height = max_height;
self.width = max_height * aspect;
}
self
}
pub fn data(&self) -> Option<Vec<u8>> {
use base64::{Engine, engine::general_purpose::STANDARD};
STANDARD.decode(&self.data_base64).ok()
}
pub fn as_rect(&self) -> Rect {
Rect::new(
self.position.x,
self.position.y,
self.position.x + self.width,
self.position.y + self.height,
)
}
pub fn data_size(&self) -> usize {
self.data_base64.len() * 3 / 4
}
}
impl ShapeTrait for Image {
fn id(&self) -> ShapeId {
self.id
}
fn bounds(&self) -> Rect {
self.as_rect()
}
fn hit_test(&self, point: Point, tolerance: f64) -> bool {
let rect = self.as_rect().inflate(tolerance, tolerance);
rect.contains(point)
}
fn to_path(&self) -> BezPath {
self.as_rect().to_path(0.1)
}
fn style(&self) -> &ShapeStyle {
&self.style
}
fn style_mut(&mut self) -> &mut ShapeStyle {
&mut self.style
}
fn transform(&mut self, affine: Affine) {
self.position = affine * self.position;
let scale = affine.as_coeffs();
self.width *= scale[0].abs();
self.height *= scale[3].abs();
}
fn clone_box(&self) -> Box<dyn ShapeTrait + Send + Sync> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_creation() {
let png_data = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
];
let format = ImageFormat::from_magic_bytes(&png_data);
assert_eq!(format, Some(ImageFormat::Png));
}
#[test]
fn test_format_detection() {
assert_eq!(ImageFormat::from_extension("png"), Some(ImageFormat::Png));
assert_eq!(ImageFormat::from_extension("PNG"), Some(ImageFormat::Png));
assert_eq!(ImageFormat::from_extension("jpg"), Some(ImageFormat::Jpeg));
assert_eq!(ImageFormat::from_extension("jpeg"), Some(ImageFormat::Jpeg));
assert_eq!(ImageFormat::from_extension("webp"), Some(ImageFormat::WebP));
assert_eq!(ImageFormat::from_extension("gif"), None);
}
#[test]
fn test_fit_within() {
let data = vec![0u8; 10];
let img = Image::new(Point::ZERO, &data, 1000, 500, ImageFormat::Png);
let fitted = img.fit_within(400.0, 400.0);
assert!((fitted.width - 400.0).abs() < 0.01);
assert!((fitted.height - 200.0).abs() < 0.01);
}
#[test]
fn test_bounds() {
let data = vec![0u8; 10];
let img = Image::new(Point::new(10.0, 20.0), &data, 100, 50, ImageFormat::Png);
let bounds = img.bounds();
assert!((bounds.x0 - 10.0).abs() < f64::EPSILON);
assert!((bounds.y0 - 20.0).abs() < f64::EPSILON);
assert!((bounds.x1 - 110.0).abs() < f64::EPSILON);
assert!((bounds.y1 - 70.0).abs() < f64::EPSILON);
}
}