use std::path::Path;
use crate::error::{PptxError, PptxResult};
use crate::units::Emu;
use super::{compute_sha1, content_type_to_ext, detect_format_from_bytes, ext_to_content_type};
const MAX_IMAGE_SIZE: u64 = 200 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct Image {
blob: Vec<u8>,
content_type: String,
ext: String,
sha1: String,
filename: Option<String>,
}
impl Image {
pub fn from_file(path: impl AsRef<Path>) -> PptxResult<Self> {
let path = path.as_ref();
let metadata = std::fs::metadata(path).map_err(PptxError::Io)?;
if metadata.len() > MAX_IMAGE_SIZE {
return Err(PptxError::ResourceLimit {
message: format!(
"image file size {} bytes exceeds the limit of {} bytes",
metadata.len(),
MAX_IMAGE_SIZE
),
});
}
let blob = std::fs::read(path).map_err(PptxError::Io)?;
let (ext, content_type) = detect_format_from_bytes(&blob)
.map(|(e, ct)| (e.to_string(), ct.to_string()))
.or_else(|| {
let file_ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_lowercase)?;
ext_to_content_type(&file_ext).map(|ct| (file_ext, ct.to_string()))
})
.ok_or_else(|| {
PptxError::InvalidXml(format!(
"unsupported or unrecognized image format: {}",
path.display()
))
})?;
let sha1 = compute_sha1(&blob);
let filename = path
.file_name()
.and_then(|n| n.to_str())
.map(std::string::ToString::to_string);
Ok(Self {
blob,
content_type,
ext,
sha1,
filename,
})
}
#[must_use]
pub fn from_bytes(data: Vec<u8>, content_type: &str) -> Self {
let ext = content_type_to_ext(content_type)
.unwrap_or("bin")
.to_string();
let sha1 = compute_sha1(&data);
Self {
blob: data,
content_type: content_type.to_string(),
ext,
sha1,
filename: None,
}
}
#[must_use]
pub fn filename(&self) -> Option<&str> {
self.filename.as_deref()
}
#[must_use]
pub fn blob(&self) -> &[u8] {
&self.blob
}
#[must_use]
pub fn content_type(&self) -> &str {
&self.content_type
}
#[must_use]
pub fn ext(&self) -> &str {
&self.ext
}
#[must_use]
pub fn sha1(&self) -> &str {
&self.sha1
}
#[must_use]
pub fn width_px(&self) -> Option<u32> {
self.dimensions().map(|(w, _)| w)
}
#[must_use]
pub fn height_px(&self) -> Option<u32> {
self.dimensions().map(|(_, h)| h)
}
#[must_use]
pub const fn dpi(&self) -> (u32, u32) {
(72, 72)
}
#[must_use]
pub fn native_size(&self) -> Option<(Emu, Emu)> {
let (w, h) = self.dimensions()?;
let (dpi_x, dpi_y) = self.dpi();
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let emu_x = (f64::from(w) * 914_400.0 / f64::from(dpi_x)) as i64;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let emu_y = (f64::from(h) * 914_400.0 / f64::from(dpi_y)) as i64;
Some((Emu(emu_x), Emu(emu_y)))
}
#[must_use]
pub fn scale_to_fit(&self, max_width: Emu, max_height: Emu) -> Option<(Emu, Emu)> {
let (native_w, native_h) = self.native_size()?;
if native_w.0 <= 0 || native_h.0 <= 0 {
return None;
}
#[allow(clippy::cast_precision_loss)] let scale_x = max_width.0 as f64 / native_w.0 as f64;
#[allow(clippy::cast_precision_loss)] let scale_y = max_height.0 as f64 / native_h.0 as f64;
let scale = scale_x.min(scale_y).min(1.0);
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let scaled_w = (native_w.0 as f64 * scale) as i64;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let scaled_h = (native_h.0 as f64 * scale) as i64;
Some((Emu(scaled_w), Emu(scaled_h)))
}
fn dimensions(&self) -> Option<(u32, u32)> {
if self.content_type == "image/svg+xml" {
return None;
}
dimensions_from_bytes(&self.blob)
}
}
fn dimensions_from_bytes(data: &[u8]) -> Option<(u32, u32)> {
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) && 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));
}
if data.starts_with(b"GIF8") && data.len() >= 10 {
let w = u32::from(u16::from_le_bytes([data[6], data[7]]));
let h = u32::from(u16::from_le_bytes([data[8], data[9]]));
return Some((w, h));
}
if data.starts_with(b"BM") && data.len() >= 26 {
let w = i32::from_le_bytes([data[18], data[19], data[20], data[21]]);
let h = i32::from_le_bytes([data[22], data[23], data[24], data[25]]);
return Some((w.unsigned_abs(), h.unsigned_abs()));
}
if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
return jpeg_dimensions(data);
}
None
}
fn jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
let mut i = 2; while i + 1 < data.len() {
if data[i] != 0xFF {
return None;
}
while i + 1 < data.len() && data[i + 1] == 0xFF {
i += 1;
}
if i + 1 >= data.len() {
return None;
}
let marker = data[i + 1];
i += 2;
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xCC {
if i + 7 > data.len() {
return None;
}
let h = u32::from(u16::from_be_bytes([data[i + 3], data[i + 4]]));
let w = u32::from(u16::from_be_bytes([data[i + 5], data[i + 6]]));
return Some((w, h));
}
if i + 1 >= data.len() {
return None;
}
let seg_len = u16::from_be_bytes([data[i], data[i + 1]]) as usize;
if seg_len < 2 {
return None;
}
i += seg_len;
}
None
}