use crate::geometry::Rect;
#[derive(Debug, Clone)]
pub struct ImageContent {
pub bbox: Rect,
pub format: ImageFormat,
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
pub bits_per_component: u8,
pub color_space: ColorSpace,
pub reading_order: Option<usize>,
pub alt_text: Option<String>,
pub horizontal_dpi: Option<f32>,
pub vertical_dpi: Option<f32>,
pub soft_mask: Option<Vec<u8>>,
}
impl ImageContent {
pub fn new(bbox: Rect, format: ImageFormat, data: Vec<u8>, width: u32, height: u32) -> Self {
let mut image = Self {
bbox,
format,
data,
width,
height,
bits_per_component: 8,
color_space: ColorSpace::RGB,
reading_order: None,
alt_text: None,
horizontal_dpi: None,
vertical_dpi: None,
soft_mask: None,
};
image.calculate_dpi();
image
}
pub fn with_soft_mask(mut self, mask: Vec<u8>) -> Self {
self.soft_mask = Some(mask);
self
}
pub fn with_reading_order(mut self, order: usize) -> Self {
self.reading_order = Some(order);
self
}
pub fn with_alt_text(mut self, text: impl Into<String>) -> Self {
self.alt_text = Some(text.into());
self
}
pub fn aspect_ratio(&self) -> f32 {
if self.height == 0 {
1.0
} else {
self.width as f32 / self.height as f32
}
}
pub fn is_grayscale(&self) -> bool {
matches!(self.color_space, ColorSpace::Gray)
}
pub fn calculate_dpi(&mut self) {
let width_inches = self.bbox.width / 72.0;
let height_inches = self.bbox.height / 72.0;
if width_inches > 0.0 && self.width > 0 {
self.horizontal_dpi = Some(self.width as f32 / width_inches);
}
if height_inches > 0.0 && self.height > 0 {
self.vertical_dpi = Some(self.height as f32 / height_inches);
}
}
pub fn resolution(&self) -> Option<(f32, f32)> {
match (self.horizontal_dpi, self.vertical_dpi) {
(Some(h), Some(v)) => Some((h, v)),
_ => None,
}
}
pub fn get_horizontal_dpi(&self) -> Option<f32> {
self.horizontal_dpi
}
pub fn get_vertical_dpi(&self) -> Option<f32> {
self.vertical_dpi
}
pub fn is_high_resolution(&self) -> bool {
match self.resolution() {
Some((h, v)) => h >= 300.0 && v >= 300.0,
None => false,
}
}
pub fn is_low_resolution(&self) -> bool {
match self.resolution() {
Some((h, v)) => h < 150.0 || v < 150.0,
None => false,
}
}
pub fn is_medium_resolution(&self) -> bool {
match self.resolution() {
Some((h, v)) => {
let min_dpi = h.min(v);
(150.0..300.0).contains(&min_dpi)
},
None => false,
}
}
}
impl Default for ImageContent {
fn default() -> Self {
Self {
bbox: Rect::new(0.0, 0.0, 0.0, 0.0),
format: ImageFormat::Unknown,
data: Vec::new(),
width: 0,
height: 0,
bits_per_component: 8,
color_space: ColorSpace::RGB,
reading_order: None,
alt_text: None,
horizontal_dpi: None,
vertical_dpi: None,
soft_mask: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Jpeg,
Png,
Jpeg2000,
Jbig2,
Raw,
Unknown,
}
impl ImageFormat {
pub fn mime_type(&self) -> &'static str {
match self {
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::Png => "image/png",
ImageFormat::Jpeg2000 => "image/jp2",
ImageFormat::Jbig2 => "image/jbig2",
ImageFormat::Raw => "application/octet-stream",
ImageFormat::Unknown => "application/octet-stream",
}
}
pub fn extension(&self) -> &'static str {
match self {
ImageFormat::Jpeg => "jpg",
ImageFormat::Png => "png",
ImageFormat::Jpeg2000 => "jp2",
ImageFormat::Jbig2 => "jbig2",
ImageFormat::Raw => "raw",
ImageFormat::Unknown => "bin",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[allow(clippy::upper_case_acronyms)]
pub enum ColorSpace {
Gray,
#[default]
RGB,
CMYK,
Indexed,
Lab,
}
impl ColorSpace {
pub fn components(&self) -> u8 {
match self {
ColorSpace::Gray => 1,
ColorSpace::RGB => 3,
ColorSpace::CMYK => 4,
ColorSpace::Indexed => 1,
ColorSpace::Lab => 3,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_content_creation() {
let image = ImageContent::new(
Rect::new(0.0, 0.0, 100.0, 100.0),
ImageFormat::Jpeg,
vec![0u8; 1000],
800,
600,
);
assert_eq!(image.width, 800);
assert_eq!(image.height, 600);
assert_eq!(image.format, ImageFormat::Jpeg);
}
#[test]
fn test_aspect_ratio() {
let image = ImageContent::new(
Rect::new(0.0, 0.0, 100.0, 100.0),
ImageFormat::Png,
vec![],
1920,
1080,
);
let ratio = image.aspect_ratio();
assert!((ratio - (1920.0 / 1080.0)).abs() < 0.001);
}
#[test]
fn test_color_space_components() {
assert_eq!(ColorSpace::Gray.components(), 1);
assert_eq!(ColorSpace::RGB.components(), 3);
assert_eq!(ColorSpace::CMYK.components(), 4);
}
#[test]
fn test_image_format_extension() {
assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
assert_eq!(ImageFormat::Png.extension(), "png");
assert_eq!(ImageFormat::Jpeg2000.extension(), "jp2");
}
#[test]
fn test_dpi_calculation_high_res() {
let image = ImageContent::new(
Rect::new(0.0, 0.0, 144.0, 144.0), ImageFormat::Jpeg,
vec![],
600,
600,
);
let (h, v) = image.resolution().unwrap();
assert!((h - 300.0).abs() < 1.0);
assert!((v - 300.0).abs() < 1.0);
assert!(image.is_high_resolution());
assert!(!image.is_low_resolution());
}
#[test]
fn test_dpi_calculation_low_res() {
let image = ImageContent::new(
Rect::new(0.0, 0.0, 72.0, 72.0), ImageFormat::Png,
vec![],
100,
100,
);
let (h, v) = image.resolution().unwrap();
assert!((h - 100.0).abs() < 1.0);
assert!((v - 100.0).abs() < 1.0);
assert!(!image.is_high_resolution());
assert!(image.is_low_resolution());
}
#[test]
fn test_dpi_calculation_medium_res() {
let image =
ImageContent::new(Rect::new(0.0, 0.0, 72.0, 72.0), ImageFormat::Png, vec![], 200, 200);
let (h, v) = image.resolution().unwrap();
assert!((h - 200.0).abs() < 1.0);
assert!((v - 200.0).abs() < 1.0);
assert!(image.is_medium_resolution());
}
#[test]
fn test_dpi_asymmetric() {
let image =
ImageContent::new(Rect::new(0.0, 0.0, 72.0, 72.0), ImageFormat::Png, vec![], 300, 100);
let (h, v) = image.resolution().unwrap();
assert!((h - 300.0).abs() < 1.0);
assert!((v - 100.0).abs() < 1.0);
assert!(image.is_low_resolution());
assert!(!image.is_high_resolution());
}
#[test]
fn test_dpi_zero_dimensions() {
let image = ImageContent::default();
assert!(image.resolution().is_none());
}
#[test]
fn test_dpi_getters() {
let image =
ImageContent::new(Rect::new(0.0, 0.0, 72.0, 72.0), ImageFormat::Png, vec![], 300, 300);
assert!(image.get_horizontal_dpi().is_some());
assert!(image.get_vertical_dpi().is_some());
assert!((image.get_horizontal_dpi().unwrap() - 300.0).abs() < 1.0);
}
}