The smokesignal.events web application
1use image::GenericImageView;
2use image::{ImageFormat, imageops::FilterType};
3
4use crate::image_errors::ImageError;
5
6/// Validate image data and ensure it's a valid image
7pub(crate) fn validate_image(data: &[u8], max_size: usize) -> Result<(), ImageError> {
8 if data.len() > max_size {
9 return Err(ImageError::SizeExceedsMaximum {
10 size: data.len(),
11 max: max_size,
12 });
13 }
14
15 // Try to load the image to ensure it's valid
16 image::load_from_memory(data).map_err(|e| ImageError::InvalidImageData(e.to_string()))?;
17
18 Ok(())
19}
20
21/// Process avatar image: validate 1:1 aspect ratio, resize to 400x400, convert to PNG
22pub(crate) fn process_avatar(data: &[u8]) -> Result<Vec<u8>, ImageError> {
23 // Load the image
24 let img =
25 image::load_from_memory(data).map_err(|e| ImageError::AvatarLoadFailed(e.to_string()))?;
26
27 let (width, height) = img.dimensions();
28
29 // Validate 1:1 aspect ratio (allow 5% deviation)
30 let aspect_ratio = width as f32 / height as f32;
31 if (aspect_ratio - 1.0).abs() > 0.05 {
32 return Err(ImageError::InvalidAvatarAspectRatio { width, height });
33 }
34
35 // Resize to 400x400 if needed
36 let resized = if width != 400 || height != 400 {
37 img.resize_exact(400, 400, FilterType::Lanczos3)
38 } else {
39 img
40 };
41
42 // Convert to PNG
43 let mut png_buffer = std::io::Cursor::new(Vec::new());
44 resized
45 .write_to(&mut png_buffer, ImageFormat::Png)
46 .map_err(|e| ImageError::AvatarEncodeFailed(e.to_string()))?;
47
48 Ok(png_buffer.into_inner())
49}
50
51/// Process banner image: validate 16:9 aspect ratio, resize to 1600x900, convert to PNG
52pub(crate) fn process_banner(data: &[u8]) -> Result<Vec<u8>, ImageError> {
53 // Load the image
54 let img =
55 image::load_from_memory(data).map_err(|e| ImageError::BannerLoadFailed(e.to_string()))?;
56
57 let (width, height) = img.dimensions();
58
59 // Validate 16:9 aspect ratio (allow 10% deviation)
60 let aspect_ratio = width as f32 / height as f32;
61 let expected_ratio = 16.0 / 9.0;
62 if (aspect_ratio - expected_ratio).abs() / expected_ratio > 0.10 {
63 return Err(ImageError::InvalidBannerAspectRatio { width, height });
64 }
65
66 // Resize to 1600x900 if needed
67 let resized = if width != 1600 || height != 900 {
68 img.resize_exact(1600, 900, FilterType::Lanczos3)
69 } else {
70 img
71 };
72
73 // Convert to PNG
74 let mut png_buffer = std::io::Cursor::new(Vec::new());
75 resized
76 .write_to(&mut png_buffer, ImageFormat::Png)
77 .map_err(|e| ImageError::BannerEncodeFailed(e.to_string()))?;
78
79 Ok(png_buffer.into_inner())
80}
81
82/// Process event header image: validate 3:1 aspect ratio, resize to 1500x500, convert to PNG
83pub(crate) fn process_event_header(data: &[u8]) -> Result<Vec<u8>, ImageError> {
84 // Load the image
85 let img = image::load_from_memory(data)
86 .map_err(|e| ImageError::EventHeaderLoadFailed(e.to_string()))?;
87
88 let (width, height) = img.dimensions();
89
90 // Validate 3:1 aspect ratio (allow 10% deviation)
91 let aspect_ratio = width as f32 / height as f32;
92 let expected_ratio = 3.0 / 1.0;
93 if (aspect_ratio - expected_ratio).abs() / expected_ratio > 0.10 {
94 return Err(ImageError::InvalidEventHeaderAspectRatio { width, height });
95 }
96
97 // Resize to 1500x500 if needed
98 let resized = if width != 1500 || height != 500 {
99 img.resize_exact(1500, 500, FilterType::Lanczos3)
100 } else {
101 img
102 };
103
104 // Convert to PNG
105 let mut png_buffer = std::io::Cursor::new(Vec::new());
106 resized
107 .write_to(&mut png_buffer, ImageFormat::Png)
108 .map_err(|e| ImageError::EventHeaderEncodeFailed(e.to_string()))?;
109
110 Ok(png_buffer.into_inner())
111}
112
113/// Process event thumbnail image: validate 1:1 aspect ratio, resize within 512x512 to 1024x1024, convert to PNG
114pub(crate) fn process_event_thumbnail(data: &[u8]) -> Result<Vec<u8>, ImageError> {
115 // Load the image
116 let img = image::load_from_memory(data)
117 .map_err(|e| ImageError::ThumbnailLoadFailed(e.to_string()))?;
118
119 let (width, height) = img.dimensions();
120
121 // Validate 1:1 aspect ratio (allow 5% deviation)
122 let aspect_ratio = width as f32 / height as f32;
123 if (aspect_ratio - 1.0).abs() > 0.05 {
124 return Err(ImageError::InvalidThumbnailAspectRatio { width, height });
125 }
126
127 // Validate minimum size of 512x512
128 if width < 512 || height < 512 {
129 return Err(ImageError::ThumbnailTooSmall { width, height });
130 }
131
132 // Resize to maximum 1024x1024 if larger, otherwise keep original size
133 let resized = if width > 1024 || height > 1024 {
134 img.resize_exact(1024, 1024, FilterType::Lanczos3)
135 } else {
136 // Keep original size if within bounds (512-1024)
137 img
138 };
139
140 // Convert to PNG
141 let mut png_buffer = std::io::Cursor::new(Vec::new());
142 resized
143 .write_to(&mut png_buffer, ImageFormat::Png)
144 .map_err(|e| ImageError::ThumbnailEncodeFailed(e.to_string()))?;
145
146 Ok(png_buffer.into_inner())
147}