use std::path::Path;
use crate::core::{Positioned, ElementSized, Dimension};
fn format_and_ext(format: &str) -> (String, String) {
let upper = format.to_uppercase();
let ext = match upper.as_str() {
"JPEG" => "jpg".to_string(),
_ => upper.to_lowercase(),
};
(upper, ext)
}
fn generate_image_filename(format: &str) -> (String, String) {
let (upper, ext) = format_and_ext(format);
let filename = format!("image_{}.{}", uuid::Uuid::new_v4(), ext);
(filename, upper)
}
#[derive(Clone, Debug)]
pub enum ImageSource {
File(String),
Base64(String),
Bytes(Vec<u8>),
#[cfg(feature = "web2ppt")]
Url(String),
}
#[derive(Clone, Debug, Default)]
pub struct Crop {
pub left: f64,
pub top: f64,
pub right: f64,
pub bottom: f64,
}
impl Crop {
pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
Self { left, top, right, bottom }
}
}
#[derive(Clone, Debug)]
pub enum ImageEffect {
Shadow,
Reflection,
Glow,
SoftEdges,
InnerShadow,
Blur,
}
#[derive(Clone, Debug)]
pub struct Image {
pub filename: String,
pub width: u32, pub height: u32, pub x: u32, pub y: u32, pub format: String, pub source: Option<ImageSource>,
pub crop: Option<Crop>,
pub effects: Vec<ImageEffect>,
}
impl Image {
pub fn new(filename: &str, width: u32, height: u32, format: &str) -> Self {
Image {
filename: filename.to_string(),
width,
height,
x: 0,
y: 0,
format: format.to_uppercase(),
source: Some(ImageSource::File(filename.to_string())),
crop: None,
effects: Vec::new(),
}
}
pub fn from_path<P: AsRef<Path>>(path: P) -> std::result::Result<Self, String> {
let path = path.as_ref();
let filename = path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "image.png".to_string());
let path_str = path.to_string_lossy().to_string();
let data = std::fs::read(path)
.map_err(|e| format!("Failed to open image: {e}"))?;
let (w, h, format) = read_image_dimensions(&data)
.ok_or_else(|| "Failed to detect image dimensions (unsupported format)".to_string())?;
let w_emu = w * 9525;
let h_emu = h * 9525;
Ok(Image {
filename,
width: w_emu,
height: h_emu,
x: 0,
y: 0,
format,
source: Some(ImageSource::File(path_str)),
crop: None,
effects: Vec::new(),
})
}
pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
let (filename, fmt) = generate_image_filename(format);
Self::with_source(filename, width, height, fmt, ImageSource::Base64(data.to_string()))
}
pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
let (filename, fmt) = generate_image_filename(format);
Self::with_source(filename, width, height, fmt, ImageSource::Bytes(data))
}
#[cfg(feature = "web2ppt")]
pub fn from_url(url: &str, width: u32, height: u32, format: &str) -> Self {
let (filename, fmt) = generate_image_filename(format);
Self::with_source(filename, width, height, fmt, ImageSource::Url(url.to_string()))
}
fn with_source(filename: String, width: u32, height: u32, format: String, source: ImageSource) -> Self {
Image {
filename,
width,
height,
x: 0,
y: 0,
format,
source: Some(source),
crop: None,
effects: Vec::new(),
}
}
pub fn get_bytes(&self) -> Option<Vec<u8>> {
match &self.source {
Some(ImageSource::Base64(data)) => {
base64_decode(data).ok()
}
Some(ImageSource::Bytes(data)) => Some(data.clone()),
Some(ImageSource::File(path)) => {
std::fs::read(path).ok()
}
#[cfg(feature = "web2ppt")]
Some(ImageSource::Url(url)) => {
let client = reqwest::blocking::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
.build()
.ok()?;
match client.get(url).send() {
Ok(resp) => {
if resp.status().is_success() {
resp.bytes().ok().map(|b| b.to_vec())
} else {
None
}
},
Err(_) => None,
}
}
None => None,
}
}
pub fn position(mut self, x: u32, y: u32) -> Self {
self.x = x;
self.y = y;
self
}
pub fn with_crop(mut self, left: f64, top: f64, right: f64, bottom: f64) -> Self {
self.crop = Some(Crop::new(left, top, right, bottom));
self
}
pub fn with_effect(mut self, effect: ImageEffect) -> Self {
self.effects.push(effect);
self
}
pub fn aspect_ratio(&self) -> f64 {
self.width as f64 / self.height as f64
}
pub fn scale_to_width(mut self, width: u32) -> Self {
let ratio = self.aspect_ratio();
self.width = width;
self.height = (width as f64 / ratio) as u32;
self
}
pub fn scale_to_height(mut self, height: u32) -> Self {
let ratio = self.aspect_ratio();
self.height = height;
self.width = (height as f64 * ratio) as u32;
self
}
pub fn extension(&self) -> String {
Path::new(&self.filename)
.extension()
.and_then(|ext| ext.to_str())
.map(|s| s.to_lowercase())
.unwrap_or_else(|| self.format.to_lowercase())
}
pub fn mime_type(&self) -> String {
match self.format.as_str() {
"PNG" => "image/png".to_string(),
"JPG" | "JPEG" => "image/jpeg".to_string(),
"GIF" => "image/gif".to_string(),
"BMP" => "image/bmp".to_string(),
"TIFF" => "image/tiff".to_string(),
"SVG" => "image/svg+xml".to_string(),
_ => "application/octet-stream".to_string(),
}
}
pub fn at(mut self, x: Dimension, y: Dimension) -> Self {
self.x = x.to_emu_x();
self.y = y.to_emu_y();
self
}
pub fn with_dimensions(mut self, width: Dimension, height: Dimension) -> Self {
self.width = width.to_emu_x();
self.height = height.to_emu_y();
self
}
}
impl Positioned for Image {
fn x(&self) -> u32 { self.x }
fn y(&self) -> u32 { self.y }
fn set_position(&mut self, x: u32, y: u32) {
self.x = x;
self.y = y;
}
}
impl ElementSized for Image {
fn width(&self) -> u32 { self.width }
fn height(&self) -> u32 { self.height }
fn set_size(&mut self, width: u32, height: u32) {
self.width = width;
self.height = height;
}
}
fn base64_decode(input: &str) -> Result<Vec<u8>, std::io::Error> {
const DECODE_TABLE: [i8; 128] = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
];
let input = input.trim().replace(['\n', '\r', ' '], "");
let mut output = Vec::with_capacity(input.len() * 3 / 4);
let bytes: Vec<u8> = input.bytes().collect();
let mut i = 0;
while i < bytes.len() {
let mut buf = [0u8; 4];
let mut pad = 0;
for j in 0..4 {
if i + j >= bytes.len() || bytes[i + j] == b'=' {
buf[j] = 0;
pad += 1;
} else if bytes[i + j] < 128 && DECODE_TABLE[bytes[i + j] as usize] >= 0 {
buf[j] = DECODE_TABLE[bytes[i + j] as usize] as u8;
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid base64 character",
));
}
}
output.push((buf[0] << 2) | (buf[1] >> 4));
if pad < 2 {
output.push((buf[1] << 4) | (buf[2] >> 2));
}
if pad < 1 {
output.push((buf[2] << 6) | buf[3]);
}
i += 4;
}
Ok(output)
}
pub struct ImageBuilder {
filename: String,
width: u32,
height: u32,
x: u32,
y: u32,
format: String,
source: Option<ImageSource>,
effects: Vec<ImageEffect>,
crop: Option<Crop>,
}
impl ImageBuilder {
pub fn new(filename: &str, width: u32, height: u32) -> Self {
let format = Path::new(filename)
.extension()
.and_then(|ext| ext.to_str())
.map(|s| s.to_uppercase())
.unwrap_or_else(|| "PNG".to_string());
ImageBuilder {
filename: filename.to_string(),
width,
height,
x: 0,
y: 0,
format,
source: Some(ImageSource::File(filename.to_string())),
effects: Vec::new(),
crop: None,
}
}
pub fn from_file(filename: &str) -> Self {
const DEFAULT_SIZE: u32 = 1828800; Self::new(filename, DEFAULT_SIZE, DEFAULT_SIZE)
}
pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
let (upper, ext) = format_and_ext(format);
ImageBuilder {
filename: format!("image.{}", ext),
width, height, x: 0, y: 0,
format: upper,
source: Some(ImageSource::Base64(data.to_string())),
effects: Vec::new(),
crop: None,
}
}
pub fn base64(data: &str, format: &str) -> Self {
const DEFAULT_SIZE: u32 = 1828800; Self::from_base64(data, DEFAULT_SIZE, DEFAULT_SIZE, format)
}
pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
let (upper, ext) = format_and_ext(format);
ImageBuilder {
filename: format!("image.{}", ext),
width, height, x: 0, y: 0,
format: upper,
source: Some(ImageSource::Bytes(data)),
effects: Vec::new(),
crop: None,
}
}
pub fn bytes(data: Vec<u8>, format: &str) -> Self {
const DEFAULT_SIZE: u32 = 1828800; Self::from_bytes(data, DEFAULT_SIZE, DEFAULT_SIZE, format)
}
pub fn auto(data: Vec<u8>) -> Self {
const DEFAULT_SIZE: u32 = 1828800;
let format = if data.len() >= 4 {
if &data[0..4] == b"\x89PNG" {
"PNG"
} else if data.len() >= 2 && &data[0..2] == b"\xFF\xD8" {
"JPEG"
} else if data.len() >= 6 && &data[0..6] == b"GIF89a" || &data[0..6] == b"GIF87a" {
"GIF"
} else {
"PNG" }
} else {
"PNG"
};
Self::from_bytes(data, DEFAULT_SIZE, DEFAULT_SIZE, format)
}
pub fn position(mut self, x: u32, y: u32) -> Self {
self.x = x;
self.y = y;
self
}
pub fn at(self, x: u32, y: u32) -> Self {
self.position(x, y)
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.width = width;
self.height = height;
self
}
pub fn format(mut self, format: &str) -> Self {
self.format = format.to_uppercase();
self
}
pub fn scale_to_width(mut self, width: u32) -> Self {
let ratio = self.width as f64 / self.height as f64;
self.width = width;
self.height = (width as f64 / ratio) as u32;
self
}
pub fn scale_to_height(mut self, height: u32) -> Self {
let ratio = self.width as f64 / self.height as f64;
self.height = height;
self.width = (height as f64 * ratio) as u32;
self
}
pub fn shadow(mut self) -> Self {
self.effects.push(ImageEffect::Shadow);
self
}
pub fn reflection(mut self) -> Self {
self.effects.push(ImageEffect::Reflection);
self
}
pub fn glow(mut self) -> Self {
self.effects.push(ImageEffect::Glow);
self
}
pub fn soft_edges(mut self) -> Self {
self.effects.push(ImageEffect::SoftEdges);
self
}
pub fn inner_shadow(mut self) -> Self {
self.effects.push(ImageEffect::InnerShadow);
self
}
pub fn blur(mut self) -> Self {
self.effects.push(ImageEffect::Blur);
self
}
pub fn crop(mut self, left: f64, top: f64, right: f64, bottom: f64) -> Self {
self.crop = Some(Crop::new(left, top, right, bottom));
self
}
pub fn build(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: self.crop,
effects: self.effects,
}
}
pub fn build_with_crop(self, left: f64, top: f64, right: f64, bottom: f64) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: Some(Crop::new(left, top, right, bottom)),
effects: Vec::new(),
}
}
pub fn build_with_shadow(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::Shadow],
}
}
pub fn build_with_reflection(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::Reflection],
}
}
pub fn build_with_effects(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::Shadow, ImageEffect::Reflection],
}
}
pub fn build_with_glow(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::Glow],
}
}
pub fn build_with_soft_edges(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::SoftEdges],
}
}
pub fn build_with_inner_shadow(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::InnerShadow],
}
}
pub fn build_with_blur(self) -> Image {
Image {
filename: self.filename,
width: self.width,
height: self.height,
x: self.x,
y: self.y,
format: self.format,
source: self.source,
crop: None,
effects: vec![ImageEffect::Blur],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_creation() {
let img = Image::new("test.png", 1920, 1080, "PNG");
assert_eq!(img.filename, "test.png");
assert_eq!(img.width, 1920);
assert_eq!(img.height, 1080);
}
#[test]
fn test_image_position() {
let img = Image::new("test.png", 1920, 1080, "PNG")
.position(500000, 1000000);
assert_eq!(img.x, 500000);
assert_eq!(img.y, 1000000);
}
#[test]
fn test_image_aspect_ratio() {
let img = Image::new("test.png", 1920, 1080, "PNG");
let ratio = img.aspect_ratio();
assert!((ratio - 1.777).abs() < 0.01);
}
#[test]
fn test_image_scale_to_width() {
let img = Image::new("test.png", 1920, 1080, "PNG")
.scale_to_width(960);
assert_eq!(img.width, 960);
assert_eq!(img.height, 540);
}
#[test]
fn test_image_scale_to_height() {
let img = Image::new("test.png", 1920, 1080, "PNG")
.scale_to_height(540);
assert_eq!(img.width, 960);
assert_eq!(img.height, 540);
}
#[test]
fn test_image_extension() {
let img = Image::new("photo.jpg", 1920, 1080, "JPEG");
assert_eq!(img.extension(), "jpg");
}
#[test]
fn test_image_mime_types() {
assert_eq!(
Image::new("test.png", 100, 100, "PNG").mime_type(),
"image/png"
);
assert_eq!(
Image::new("test.jpg", 100, 100, "JPG").mime_type(),
"image/jpeg"
);
assert_eq!(
Image::new("test.gif", 100, 100, "GIF").mime_type(),
"image/gif"
);
}
#[test]
fn test_image_builder() {
let img = ImageBuilder::new("photo.png", 1920, 1080)
.position(500000, 1000000)
.scale_to_width(960)
.build();
assert_eq!(img.filename, "photo.png");
assert_eq!(img.width, 960);
assert_eq!(img.height, 540);
assert_eq!(img.x, 500000);
assert_eq!(img.y, 1000000);
}
#[test]
fn test_image_builder_auto_format() {
let img = ImageBuilder::new("photo.jpg", 1920, 1080).build();
assert_eq!(img.format, "JPG");
}
#[test]
fn test_image_from_base64() {
let base64_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let img = Image::from_base64(base64_png, 100, 100, "PNG");
assert!(img.filename.ends_with(".png"));
assert_eq!(img.format, "PNG");
assert!(matches!(img.source, Some(ImageSource::Base64(_))));
}
#[test]
fn test_image_from_bytes() {
let data = vec![0x89, 0x50, 0x4E, 0x47]; let img = Image::from_bytes(data.clone(), 100, 100, "PNG");
assert_eq!(img.format, "PNG");
assert!(matches!(img.source, Some(ImageSource::Bytes(_))));
}
#[test]
fn test_base64_decode() {
let result = base64_decode("SGVsbG8=").unwrap();
assert_eq!(result, b"Hello");
let result = base64_decode("SGVsbG8gV29ybGQ=").unwrap();
assert_eq!(result, b"Hello World");
}
#[test]
fn test_image_get_bytes_base64() {
let base64_png = "SGVsbG8="; let img = Image::from_base64(base64_png, 100, 100, "PNG");
let bytes = img.get_bytes().unwrap();
assert_eq!(bytes, b"Hello");
}
#[test]
fn test_image_builder_from_base64() {
let base64_data = "SGVsbG8=";
let img = ImageBuilder::from_base64(base64_data, 200, 150, "JPEG")
.position(1000, 2000)
.build();
assert_eq!(img.width, 200);
assert_eq!(img.height, 150);
assert_eq!(img.x, 1000);
assert_eq!(img.y, 2000);
assert_eq!(img.format, "JPEG");
}
#[test]
fn test_read_png_dimensions() {
let png: Vec<u8> = vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, ];
let (w, h, fmt) = read_image_dimensions(&png).unwrap();
assert_eq!((w, h), (1, 1));
assert_eq!(fmt, "PNG");
}
#[test]
fn test_read_gif_dimensions() {
let gif: Vec<u8> = vec![
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x0A, 0x00, 0x14, 0x00, ];
let (w, h, fmt) = read_image_dimensions(&gif).unwrap();
assert_eq!((w, h), (10, 20));
assert_eq!(fmt, "GIF");
}
#[test]
fn test_read_bmp_dimensions() {
let mut bmp = vec![0u8; 26];
bmp[0] = 0x42; bmp[1] = 0x4D; bmp[18..22].copy_from_slice(&100u32.to_le_bytes()); bmp[22..26].copy_from_slice(&200u32.to_le_bytes()); let (w, h, fmt) = read_image_dimensions(&bmp).unwrap();
assert_eq!((w, h), (100, 200));
assert_eq!(fmt, "BMP");
}
}
fn read_image_dimensions(data: &[u8]) -> Option<(u32, u32, String)> {
if data.len() < 10 {
return None;
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) && data.len() >= 24 {
let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
return Some((w, h, "PNG".into()));
}
if data.starts_with(&[0xFF, 0xD8]) {
return read_jpeg_dimensions(data);
}
if data.starts_with(b"GIF8") && data.len() >= 10 {
let w = u16::from_le_bytes([data[6], data[7]]) as u32;
let h = u16::from_le_bytes([data[8], data[9]]) as u32;
return Some((w, h, "GIF".into()));
}
if data.starts_with(b"BM") && data.len() >= 26 {
let w = u32::from_le_bytes([data[18], data[19], data[20], data[21]]);
let h = u32::from_le_bytes([data[22], data[23], data[24], data[25]]);
return Some((w, h, "BMP".into()));
}
if data.len() >= 30 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
if &data[12..16] == b"VP8 " && data.len() >= 30 {
let w = u16::from_le_bytes([data[26], data[27]]) as u32 & 0x3FFF;
let h = u16::from_le_bytes([data[28], data[29]]) as u32 & 0x3FFF;
return Some((w, h, "WEBP".into()));
}
if &data[12..16] == b"VP8L" && data.len() >= 25 {
let b0 = data[21] as u32;
let b1 = data[22] as u32;
let b2 = data[23] as u32;
let b3 = data[24] as u32;
let bits = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
let w = (bits & 0x3FFF) + 1;
let h = ((bits >> 14) & 0x3FFF) + 1;
return Some((w, h, "WEBP".into()));
}
}
None
}
fn read_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32, String)> {
let mut i = 2;
while i + 1 < data.len() {
if data[i] != 0xFF {
i += 1;
continue;
}
let marker = data[i + 1];
i += 2;
if (marker == 0xC0 || marker == 0xC2) && i + 7 < data.len() {
let h = u16::from_be_bytes([data[i + 3], data[i + 4]]) as u32;
let w = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
return Some((w, h, "JPEG".into()));
}
if marker >= 0xC0 && marker != 0xD9 && marker != 0xDA && i + 1 < data.len() {
let len = u16::from_be_bytes([data[i], data[i + 1]]) as usize;
i += len;
}
}
None
}