megacommerce_shared/models/
images.rs1use std::io::Cursor;
2
3use base64::{engine::general_purpose, Engine as _};
4use image::{ImageFormat, ImageReader};
5
6#[derive(Debug, Clone)]
7pub struct ImageValidationConfig {
8 pub max_size_bytes: usize,
9 pub allowed_formats: Vec<ImageFormat>,
10 pub max_width: u32,
11 pub max_height: u32,
12 pub min_width: u32,
13 pub min_height: u32,
14}
15
16#[derive(Debug, Clone)]
17pub struct ImageValidationResult {
18 pub format: ImageFormat,
19 pub dimensions: (u32, u32),
20 pub size_bytes: usize,
21 pub is_valid: bool,
22 pub decoded_data: Vec<u8>,
23}
24
25impl Default for ImageValidationConfig {
26 fn default() -> Self {
27 Self {
28 max_size_bytes: 1024 * 1024, allowed_formats: vec![ImageFormat::Png, ImageFormat::Jpeg, ImageFormat::WebP],
30 max_width: 4000,
31 max_height: 4000,
32 min_width: 100,
33 min_height: 100,
34 }
35 }
36}
37
38#[derive(Debug)]
39pub enum ImageValidationError {
40 LargeImage(String),
41 InvalidBase64(String),
42 UnknownFormat(String),
43 NotAllowedFormat(String),
44 SmallDimensions(String),
45 LargeDimensions(String),
46 UnknownDimensions(String),
47}
48
49pub fn validate_base64_image(
50 data: &str,
51 config: &ImageValidationConfig,
52) -> Result<ImageValidationResult, ImageValidationError> {
53 let clean_data = data.split(',').last().unwrap_or(data);
54
55 let estimated_size = (clean_data.len() * 3) / 4;
61 if estimated_size > config.max_size_bytes {
62 return Err(ImageValidationError::LargeImage(format!(
63 "Estimated size: {} is bigger than allowed size: {}",
64 estimated_size, config.max_size_bytes
65 )));
66 }
67
68 let decoded_data = general_purpose::STANDARD
69 .decode(clean_data)
70 .map_err(|err| ImageValidationError::InvalidBase64(format!("Invalid base64 data: {}", err)))?;
71
72 if decoded_data.len() > config.max_size_bytes {
74 return Err(ImageValidationError::LargeImage(format!(
75 "Actual image size: {} is bigger than allowed size: {}",
76 decoded_data.len(),
77 config.max_size_bytes
78 )));
79 }
80
81 let cursor = Cursor::new(&decoded_data);
82 let reader = ImageReader::new(cursor).with_guessed_format().map_err(|err| {
83 ImageValidationError::UnknownFormat(format!("Unable to determine image format: {}", err))
84 })?;
85
86 let format = reader.format().ok_or_else(|| {
87 ImageValidationError::UnknownFormat("Unable to determine image format".to_string())
88 })?;
89
90 if !config.allowed_formats.contains(&format) {
91 return Err(ImageValidationError::NotAllowedFormat(format!(
92 "Image format {} is not allowed",
93 format.extensions_str().first().unwrap_or(&"unknown")
94 )));
95 }
96
97 let dimensions = reader.into_dimensions().map_err(|err| {
98 ImageValidationError::UnknownDimensions(format!("Unable to get dimensions: {}", err))
99 })?;
100
101 if dimensions.0 < config.min_width || dimensions.1 < config.min_height {
102 return Err(ImageValidationError::SmallDimensions(format!(
103 "Image dimensions {}x{} are too small, minimum is {}x{}",
104 dimensions.0, dimensions.1, config.min_width, config.min_height
105 )));
106 }
107
108 if dimensions.0 > config.max_width || dimensions.1 > config.max_height {
109 return Err(ImageValidationError::LargeDimensions(format!(
110 "Image dimensions {}x{} are too large, maximum is {}x{}",
111 dimensions.0, dimensions.1, config.max_width, config.max_height
112 )));
113 }
114
115 Ok(ImageValidationResult {
116 format,
117 dimensions,
118 size_bytes: decoded_data.len(),
119 is_valid: true,
120 decoded_data,
121 })
122}