1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{FileRef, Metadata};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct Image {
13 pub file_ref: FileRef,
15
16 pub width: Option<u32>,
18
19 pub height: Option<u32>,
21
22 pub alt_text: Option<String>,
24
25 pub caption: Option<String>,
27
28 pub format: Option<String>,
31
32 pub metadata: Vec<Metadata>,
34}
35
36impl Image {
37 pub fn new(file_ref: FileRef) -> Self {
39 Self {
40 file_ref,
41 width: None,
42 height: None,
43 alt_text: None,
44 caption: None,
45 format: None,
46 metadata: vec![],
47 }
48 }
49
50 pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
52 self.width = Some(width);
53 self.height = Some(height);
54 self
55 }
56
57 pub fn with_alt_text(mut self, alt_text: String) -> Self {
59 self.alt_text = Some(alt_text);
60 self
61 }
62
63 pub fn with_caption(mut self, caption: String) -> Self {
65 self.caption = Some(caption);
66 self
67 }
68
69 pub fn with_format(mut self, format: String) -> Self {
71 self.format = Some(format);
72 self
73 }
74
75 pub fn with_metadata(mut self, key: String, value: String) -> Self {
77 self.metadata.push(Metadata::Generic { key, value });
78 self
79 }
80
81 pub fn with_structured_metadata(mut self, metadata: Metadata) -> Self {
83 self.metadata.push(metadata);
84 self
85 }
86
87 pub fn generic_metadata(&self) -> BTreeMap<String, String> {
93 self.metadata
94 .iter()
95 .filter_map(|meta| match meta {
96 Metadata::Generic { key, value } => Some((key.clone(), value.clone())),
97 _ => None,
98 })
99 .collect()
100 }
101
102 pub fn aspect_ratio(&self) -> Option<f32> {
104 match (self.width, self.height) {
105 (Some(w), Some(h)) if h > 0 => Some(w as f32 / h as f32),
106 _ => None,
107 }
108 }
109
110 pub fn is_square(&self) -> Option<bool> {
112 match (self.width, self.height) {
113 (Some(w), Some(h)) => Some(w == h),
114 _ => None,
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 use zoe_encrypted_storage::ConvergentEncryptionInfo;
124
125 fn create_test_file_ref() -> FileRef {
126 FileRef::new(
127 "image_hash_123".to_string(),
128 ConvergentEncryptionInfo {
129 key: [1u8; 32],
130 was_compressed: false,
131 source_size: 2048,
132 },
133 Some("test_image.png".to_string()),
134 )
135 .with_content_type("image/png".to_string())
136 }
137
138 #[test]
139 fn test_image_new() {
140 let file_ref = create_test_file_ref();
141 let image = Image::new(file_ref.clone());
142
143 assert_eq!(image.file_ref, file_ref);
144 assert_eq!(image.width, None);
145 assert_eq!(image.height, None);
146 assert_eq!(image.alt_text, None);
147 assert_eq!(image.caption, None);
148 assert_eq!(image.format, None);
149 assert!(image.metadata.is_empty());
150 }
151
152 #[test]
153 fn test_image_with_dimensions() {
154 let image = Image::new(create_test_file_ref()).with_dimensions(1920, 1080);
155
156 assert_eq!(image.width, Some(1920));
157 assert_eq!(image.height, Some(1080));
158 }
159
160 #[test]
161 fn test_image_with_alt_text() {
162 let alt_text = "A beautiful sunset over the mountains".to_string();
163 let image = Image::new(create_test_file_ref()).with_alt_text(alt_text.clone());
164
165 assert_eq!(image.alt_text, Some(alt_text));
166 }
167
168 #[test]
169 fn test_image_with_caption() {
170 let caption = "Sunset at Mountain View, 2023".to_string();
171 let image = Image::new(create_test_file_ref()).with_caption(caption.clone());
172
173 assert_eq!(image.caption, Some(caption));
174 }
175
176 #[test]
177 fn test_image_with_format() {
178 let format = "PNG".to_string();
179 let image = Image::new(create_test_file_ref()).with_format(format.clone());
180
181 assert_eq!(image.format, Some(format));
182 }
183
184 #[test]
185 fn test_image_with_metadata() {
186 let image = Image::new(create_test_file_ref())
187 .with_metadata("camera".to_string(), "Canon EOS R5".to_string())
188 .with_metadata("iso".to_string(), "200".to_string())
189 .with_metadata("aperture".to_string(), "f/8".to_string());
190
191 let generic_meta = image.generic_metadata();
192 assert_eq!(
193 generic_meta.get("camera"),
194 Some(&"Canon EOS R5".to_string())
195 );
196 assert_eq!(generic_meta.get("iso"), Some(&"200".to_string()));
197 assert_eq!(generic_meta.get("aperture"), Some(&"f/8".to_string()));
198 assert_eq!(image.metadata.len(), 3);
199 }
200
201 #[test]
202 fn test_image_aspect_ratio() {
203 let image_16_9 = Image::new(create_test_file_ref()).with_dimensions(1920, 1080);
205 assert_eq!(image_16_9.aspect_ratio(), Some(1920.0 / 1080.0));
206
207 let image_square = Image::new(create_test_file_ref()).with_dimensions(500, 500);
209 assert_eq!(image_square.aspect_ratio(), Some(1.0));
210
211 let image_portrait = Image::new(create_test_file_ref()).with_dimensions(1080, 1920);
213 assert_eq!(image_portrait.aspect_ratio(), Some(1080.0 / 1920.0));
214
215 let image_no_dims = Image::new(create_test_file_ref());
217 assert_eq!(image_no_dims.aspect_ratio(), None);
218
219 let mut image_zero_height = Image::new(create_test_file_ref());
221 image_zero_height.width = Some(1920);
222 image_zero_height.height = Some(0);
223 assert_eq!(image_zero_height.aspect_ratio(), None);
224
225 let mut image_only_width = Image::new(create_test_file_ref());
227 image_only_width.width = Some(1920);
228 assert_eq!(image_only_width.aspect_ratio(), None);
229
230 let mut image_only_height = Image::new(create_test_file_ref());
232 image_only_height.height = Some(1080);
233 assert_eq!(image_only_height.aspect_ratio(), None);
234 }
235
236 #[test]
237 fn test_image_is_square() {
238 let image_square = Image::new(create_test_file_ref()).with_dimensions(500, 500);
240 assert_eq!(image_square.is_square(), Some(true));
241
242 let image_rectangle = Image::new(create_test_file_ref()).with_dimensions(1920, 1080);
244 assert_eq!(image_rectangle.is_square(), Some(false));
245
246 let image_no_dims = Image::new(create_test_file_ref());
248 assert_eq!(image_no_dims.is_square(), None);
249
250 let mut image_only_width = Image::new(create_test_file_ref());
252 image_only_width.width = Some(500);
253 assert_eq!(image_only_width.is_square(), None);
254
255 let mut image_only_height = Image::new(create_test_file_ref());
257 image_only_height.height = Some(500);
258 assert_eq!(image_only_height.is_square(), None);
259 }
260
261 #[test]
262 fn test_image_builder_pattern() {
263 let image = Image::new(create_test_file_ref())
264 .with_dimensions(1920, 1080)
265 .with_alt_text("Test image".to_string())
266 .with_caption("A test image for unit tests".to_string())
267 .with_format("PNG".to_string())
268 .with_metadata("created_by".to_string(), "test_suite".to_string());
269
270 assert_eq!(image.width, Some(1920));
271 assert_eq!(image.height, Some(1080));
272 assert_eq!(image.alt_text, Some("Test image".to_string()));
273 assert_eq!(
274 image.caption,
275 Some("A test image for unit tests".to_string())
276 );
277 assert_eq!(image.format, Some("PNG".to_string()));
278 let generic_meta = image.generic_metadata();
279 assert_eq!(
280 generic_meta.get("created_by"),
281 Some(&"test_suite".to_string())
282 );
283 }
284
285 #[test]
286 fn test_postcard_serialization_image() {
287 let image = Image::new(create_test_file_ref())
288 .with_dimensions(800, 600)
289 .with_alt_text("Serialization test".to_string())
290 .with_caption("Testing postcard serialization".to_string())
291 .with_format("JPEG".to_string())
292 .with_metadata("test".to_string(), "serialization".to_string());
293
294 let serialized = postcard::to_stdvec(&image).expect("Failed to serialize");
295 let deserialized: Image = postcard::from_bytes(&serialized).expect("Failed to deserialize");
296
297 assert_eq!(image, deserialized);
298 }
299
300 #[test]
301 fn test_image_with_complex_metadata() {
302 use crate::Metadata;
303
304 let complex_metadata = vec![
305 Metadata::Generic {
306 key: "exif_date".to_string(),
307 value: "2023-12-01T15:30:00Z".to_string(),
308 },
309 Metadata::Generic {
310 key: "location".to_string(),
311 value: "37.7749,-122.4194".to_string(),
312 },
313 Metadata::Generic {
314 key: "device".to_string(),
315 value: "iPhone 15 Pro".to_string(),
316 },
317 ];
318
319 let mut image = Image::new(create_test_file_ref());
320 image.metadata = complex_metadata;
321
322 let final_image = image
324 .with_metadata("edited".to_string(), "true".to_string())
325 .with_metadata("editor".to_string(), "Photoshop".to_string());
326
327 let generic_meta = final_image.generic_metadata();
329
330 assert_eq!(
332 generic_meta.get("exif_date"),
333 Some(&"2023-12-01T15:30:00Z".to_string())
334 );
335 assert_eq!(
336 generic_meta.get("location"),
337 Some(&"37.7749,-122.4194".to_string())
338 );
339 assert_eq!(
340 generic_meta.get("device"),
341 Some(&"iPhone 15 Pro".to_string())
342 );
343
344 assert_eq!(generic_meta.get("edited"), Some(&"true".to_string()));
346 assert_eq!(generic_meta.get("editor"), Some(&"Photoshop".to_string()));
347
348 assert_eq!(final_image.metadata.len(), 5);
349 }
350
351 #[test]
352 fn test_image_edge_cases() {
353 let large_image = Image::new(create_test_file_ref()).with_dimensions(u32::MAX, u32::MAX);
355
356 assert_eq!(large_image.width, Some(u32::MAX));
357 assert_eq!(large_image.height, Some(u32::MAX));
358 assert_eq!(large_image.is_square(), Some(true));
359 assert_eq!(large_image.aspect_ratio(), Some(1.0));
360
361 let tiny_image = Image::new(create_test_file_ref()).with_dimensions(1, 1);
363
364 assert_eq!(tiny_image.is_square(), Some(true));
365 assert_eq!(tiny_image.aspect_ratio(), Some(1.0));
366
367 let wide_image = Image::new(create_test_file_ref()).with_dimensions(10000, 1);
369
370 assert_eq!(wide_image.is_square(), Some(false));
371 assert_eq!(wide_image.aspect_ratio(), Some(10000.0));
372
373 let tall_image = Image::new(create_test_file_ref()).with_dimensions(1, 10000);
375
376 assert_eq!(tall_image.is_square(), Some(false));
377 assert_eq!(tall_image.aspect_ratio(), Some(0.0001));
378 }
379
380 #[test]
381 fn test_image_empty_strings() {
382 let image = Image::new(create_test_file_ref())
384 .with_alt_text("".to_string())
385 .with_caption("".to_string())
386 .with_format("".to_string())
387 .with_metadata("".to_string(), "".to_string());
388
389 assert_eq!(image.alt_text, Some("".to_string()));
390 assert_eq!(image.caption, Some("".to_string()));
391 assert_eq!(image.format, Some("".to_string()));
392 let generic_meta = image.generic_metadata();
393 assert_eq!(generic_meta.get(""), Some(&"".to_string()));
394 }
395}