1use sheetkit_xml::drawing::{
7 AExt, Blip, BlipFill, CNvPicPr, CNvPr, ClientData, Extent, FillRect, MarkerType, NvPicPr,
8 Offset, OneCellAnchor, Picture, PrstGeom, SpPr, Stretch, WsDr, Xfrm,
9};
10
11use crate::error::{Error, Result};
12use crate::utils::cell_ref::cell_name_to_coordinates;
13
14pub const EMU_PER_PIXEL: u64 = 9525;
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum ImageFormat {
21 Png,
23 Jpeg,
25 Gif,
27 Bmp,
29 Ico,
31 Tiff,
33 Svg,
35 Emf,
37 Emz,
39 Wmf,
41 Wmz,
43}
44
45impl ImageFormat {
46 pub fn from_extension(ext: &str) -> Result<Self> {
51 match ext.to_ascii_lowercase().as_str() {
52 "png" => Ok(ImageFormat::Png),
53 "jpeg" | "jpg" => Ok(ImageFormat::Jpeg),
54 "gif" => Ok(ImageFormat::Gif),
55 "bmp" => Ok(ImageFormat::Bmp),
56 "ico" => Ok(ImageFormat::Ico),
57 "tiff" | "tif" => Ok(ImageFormat::Tiff),
58 "svg" => Ok(ImageFormat::Svg),
59 "emf" => Ok(ImageFormat::Emf),
60 "emz" => Ok(ImageFormat::Emz),
61 "wmf" => Ok(ImageFormat::Wmf),
62 "wmz" => Ok(ImageFormat::Wmz),
63 _ => Err(Error::UnsupportedImageFormat {
64 format: ext.to_string(),
65 }),
66 }
67 }
68
69 pub fn content_type(&self) -> &str {
71 match self {
72 ImageFormat::Png => "image/png",
73 ImageFormat::Jpeg => "image/jpeg",
74 ImageFormat::Gif => "image/gif",
75 ImageFormat::Bmp => "image/bmp",
76 ImageFormat::Ico => "image/x-icon",
77 ImageFormat::Tiff => "image/tiff",
78 ImageFormat::Svg => "image/svg+xml",
79 ImageFormat::Emf => "image/x-emf",
80 ImageFormat::Emz => "image/x-emz",
81 ImageFormat::Wmf => "image/x-wmf",
82 ImageFormat::Wmz => "image/x-wmz",
83 }
84 }
85
86 pub fn extension(&self) -> &str {
88 match self {
89 ImageFormat::Png => "png",
90 ImageFormat::Jpeg => "jpeg",
91 ImageFormat::Gif => "gif",
92 ImageFormat::Bmp => "bmp",
93 ImageFormat::Ico => "ico",
94 ImageFormat::Tiff => "tiff",
95 ImageFormat::Svg => "svg",
96 ImageFormat::Emf => "emf",
97 ImageFormat::Emz => "emz",
98 ImageFormat::Wmf => "wmf",
99 ImageFormat::Wmz => "wmz",
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct ImageConfig {
107 pub data: Vec<u8>,
109 pub format: ImageFormat,
111 pub from_cell: String,
113 pub width_px: u32,
115 pub height_px: u32,
117}
118
119pub fn pixels_to_emu(px: u32) -> u64 {
121 px as u64 * EMU_PER_PIXEL
122}
123
124pub fn build_drawing_with_image(image_ref_id: &str, config: &ImageConfig) -> Result<WsDr> {
129 let (col, row) = cell_name_to_coordinates(&config.from_cell)?;
130 let from = MarkerType {
132 col: col - 1,
133 col_off: 0,
134 row: row - 1,
135 row_off: 0,
136 };
137
138 let cx = pixels_to_emu(config.width_px);
139 let cy = pixels_to_emu(config.height_px);
140
141 let pic = Picture {
142 nv_pic_pr: NvPicPr {
143 c_nv_pr: CNvPr {
144 id: 2,
145 name: "Picture 1".to_string(),
146 },
147 c_nv_pic_pr: CNvPicPr {},
148 },
149 blip_fill: BlipFill {
150 blip: Blip {
151 r_embed: image_ref_id.to_string(),
152 },
153 stretch: Stretch {
154 fill_rect: FillRect {},
155 },
156 },
157 sp_pr: SpPr {
158 xfrm: Xfrm {
159 off: Offset { x: 0, y: 0 },
160 ext: AExt { cx, cy },
161 },
162 prst_geom: PrstGeom {
163 prst: "rect".to_string(),
164 },
165 },
166 };
167
168 let anchor = OneCellAnchor {
169 from,
170 ext: Extent { cx, cy },
171 pic: Some(pic),
172 client_data: ClientData {},
173 };
174
175 Ok(WsDr {
176 one_cell_anchors: vec![anchor],
177 ..WsDr::default()
178 })
179}
180
181pub fn add_image_to_drawing(
186 drawing: &mut WsDr,
187 image_ref_id: &str,
188 config: &ImageConfig,
189 pic_id: u32,
190) -> Result<()> {
191 let (col, row) = cell_name_to_coordinates(&config.from_cell)?;
192 let from = MarkerType {
193 col: col - 1,
194 col_off: 0,
195 row: row - 1,
196 row_off: 0,
197 };
198
199 let cx = pixels_to_emu(config.width_px);
200 let cy = pixels_to_emu(config.height_px);
201
202 let pic = Picture {
203 nv_pic_pr: NvPicPr {
204 c_nv_pr: CNvPr {
205 id: pic_id,
206 name: format!("Picture {}", pic_id - 1),
207 },
208 c_nv_pic_pr: CNvPicPr {},
209 },
210 blip_fill: BlipFill {
211 blip: Blip {
212 r_embed: image_ref_id.to_string(),
213 },
214 stretch: Stretch {
215 fill_rect: FillRect {},
216 },
217 },
218 sp_pr: SpPr {
219 xfrm: Xfrm {
220 off: Offset { x: 0, y: 0 },
221 ext: AExt { cx, cy },
222 },
223 prst_geom: PrstGeom {
224 prst: "rect".to_string(),
225 },
226 },
227 };
228
229 drawing.one_cell_anchors.push(OneCellAnchor {
230 from,
231 ext: Extent { cx, cy },
232 pic: Some(pic),
233 client_data: ClientData {},
234 });
235
236 Ok(())
237}
238
239pub fn validate_image_config(config: &ImageConfig) -> Result<()> {
241 if config.data.is_empty() {
242 return Err(Error::Internal("image data is empty".to_string()));
243 }
244 if config.width_px == 0 || config.height_px == 0 {
245 return Err(Error::Internal(
246 "image dimensions must be non-zero".to_string(),
247 ));
248 }
249 cell_name_to_coordinates(&config.from_cell)?;
251 Ok(())
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_emu_per_pixel_constant() {
260 assert_eq!(EMU_PER_PIXEL, 9525);
261 }
262
263 #[test]
264 fn test_pixels_to_emu() {
265 assert_eq!(pixels_to_emu(1), 9525);
266 assert_eq!(pixels_to_emu(100), 952500);
267 assert_eq!(pixels_to_emu(1000), 9525000);
268 assert_eq!(pixels_to_emu(0), 0);
269 }
270
271 #[test]
272 fn test_image_format_content_type_original() {
273 assert_eq!(ImageFormat::Png.content_type(), "image/png");
274 assert_eq!(ImageFormat::Jpeg.content_type(), "image/jpeg");
275 assert_eq!(ImageFormat::Gif.content_type(), "image/gif");
276 }
277
278 #[test]
279 fn test_image_format_content_type_new_formats() {
280 assert_eq!(ImageFormat::Bmp.content_type(), "image/bmp");
281 assert_eq!(ImageFormat::Ico.content_type(), "image/x-icon");
282 assert_eq!(ImageFormat::Tiff.content_type(), "image/tiff");
283 assert_eq!(ImageFormat::Svg.content_type(), "image/svg+xml");
284 assert_eq!(ImageFormat::Emf.content_type(), "image/x-emf");
285 assert_eq!(ImageFormat::Emz.content_type(), "image/x-emz");
286 assert_eq!(ImageFormat::Wmf.content_type(), "image/x-wmf");
287 assert_eq!(ImageFormat::Wmz.content_type(), "image/x-wmz");
288 }
289
290 #[test]
291 fn test_image_format_extension_original() {
292 assert_eq!(ImageFormat::Png.extension(), "png");
293 assert_eq!(ImageFormat::Jpeg.extension(), "jpeg");
294 assert_eq!(ImageFormat::Gif.extension(), "gif");
295 }
296
297 #[test]
298 fn test_image_format_extension_new_formats() {
299 assert_eq!(ImageFormat::Bmp.extension(), "bmp");
300 assert_eq!(ImageFormat::Ico.extension(), "ico");
301 assert_eq!(ImageFormat::Tiff.extension(), "tiff");
302 assert_eq!(ImageFormat::Svg.extension(), "svg");
303 assert_eq!(ImageFormat::Emf.extension(), "emf");
304 assert_eq!(ImageFormat::Emz.extension(), "emz");
305 assert_eq!(ImageFormat::Wmf.extension(), "wmf");
306 assert_eq!(ImageFormat::Wmz.extension(), "wmz");
307 }
308
309 #[test]
310 fn test_from_extension_original_formats() {
311 assert_eq!(
312 ImageFormat::from_extension("png").unwrap(),
313 ImageFormat::Png
314 );
315 assert_eq!(
316 ImageFormat::from_extension("jpeg").unwrap(),
317 ImageFormat::Jpeg
318 );
319 assert_eq!(
320 ImageFormat::from_extension("jpg").unwrap(),
321 ImageFormat::Jpeg
322 );
323 assert_eq!(
324 ImageFormat::from_extension("gif").unwrap(),
325 ImageFormat::Gif
326 );
327 }
328
329 #[test]
330 fn test_from_extension_new_formats() {
331 assert_eq!(
332 ImageFormat::from_extension("bmp").unwrap(),
333 ImageFormat::Bmp
334 );
335 assert_eq!(
336 ImageFormat::from_extension("ico").unwrap(),
337 ImageFormat::Ico
338 );
339 assert_eq!(
340 ImageFormat::from_extension("tiff").unwrap(),
341 ImageFormat::Tiff
342 );
343 assert_eq!(
344 ImageFormat::from_extension("tif").unwrap(),
345 ImageFormat::Tiff
346 );
347 assert_eq!(
348 ImageFormat::from_extension("svg").unwrap(),
349 ImageFormat::Svg
350 );
351 assert_eq!(
352 ImageFormat::from_extension("emf").unwrap(),
353 ImageFormat::Emf
354 );
355 assert_eq!(
356 ImageFormat::from_extension("emz").unwrap(),
357 ImageFormat::Emz
358 );
359 assert_eq!(
360 ImageFormat::from_extension("wmf").unwrap(),
361 ImageFormat::Wmf
362 );
363 assert_eq!(
364 ImageFormat::from_extension("wmz").unwrap(),
365 ImageFormat::Wmz
366 );
367 }
368
369 #[test]
370 fn test_from_extension_case_insensitive() {
371 assert_eq!(
372 ImageFormat::from_extension("PNG").unwrap(),
373 ImageFormat::Png
374 );
375 assert_eq!(
376 ImageFormat::from_extension("Jpeg").unwrap(),
377 ImageFormat::Jpeg
378 );
379 assert_eq!(
380 ImageFormat::from_extension("TIFF").unwrap(),
381 ImageFormat::Tiff
382 );
383 assert_eq!(
384 ImageFormat::from_extension("SVG").unwrap(),
385 ImageFormat::Svg
386 );
387 assert_eq!(
388 ImageFormat::from_extension("Emf").unwrap(),
389 ImageFormat::Emf
390 );
391 }
392
393 #[test]
394 fn test_from_extension_unknown_returns_error() {
395 let result = ImageFormat::from_extension("webp");
396 assert!(result.is_err());
397 let err = result.unwrap_err();
398 assert!(matches!(err, Error::UnsupportedImageFormat { .. }));
399 assert!(err.to_string().contains("webp"));
400 }
401
402 #[test]
403 fn test_from_extension_empty_returns_error() {
404 let result = ImageFormat::from_extension("");
405 assert!(result.is_err());
406 assert!(matches!(
407 result.unwrap_err(),
408 Error::UnsupportedImageFormat { .. }
409 ));
410 }
411
412 #[test]
413 fn test_from_extension_roundtrip() {
414 let formats = [
415 ImageFormat::Png,
416 ImageFormat::Jpeg,
417 ImageFormat::Gif,
418 ImageFormat::Bmp,
419 ImageFormat::Ico,
420 ImageFormat::Tiff,
421 ImageFormat::Svg,
422 ImageFormat::Emf,
423 ImageFormat::Emz,
424 ImageFormat::Wmf,
425 ImageFormat::Wmz,
426 ];
427 for fmt in &formats {
428 let ext = fmt.extension();
429 let parsed = ImageFormat::from_extension(ext).unwrap();
430 assert_eq!(&parsed, fmt);
431 }
432 }
433
434 #[test]
435 fn test_build_drawing_with_image() {
436 let config = ImageConfig {
437 data: vec![0x89, 0x50, 0x4E, 0x47],
438 format: ImageFormat::Png,
439 from_cell: "B2".to_string(),
440 width_px: 400,
441 height_px: 300,
442 };
443
444 let dr = build_drawing_with_image("rId1", &config).unwrap();
445
446 assert!(dr.two_cell_anchors.is_empty());
447 assert_eq!(dr.one_cell_anchors.len(), 1);
448
449 let anchor = &dr.one_cell_anchors[0];
450 assert_eq!(anchor.from.col, 1);
451 assert_eq!(anchor.from.row, 1);
452 assert_eq!(anchor.ext.cx, 400 * 9525);
453 assert_eq!(anchor.ext.cy, 300 * 9525);
454
455 let pic = anchor.pic.as_ref().unwrap();
456 assert_eq!(pic.blip_fill.blip.r_embed, "rId1");
457 assert_eq!(pic.sp_pr.prst_geom.prst, "rect");
458 }
459
460 #[test]
461 fn test_build_drawing_with_image_a1() {
462 let config = ImageConfig {
463 data: vec![0xFF, 0xD8],
464 format: ImageFormat::Jpeg,
465 from_cell: "A1".to_string(),
466 width_px: 200,
467 height_px: 100,
468 };
469
470 let dr = build_drawing_with_image("rId2", &config).unwrap();
471 let anchor = &dr.one_cell_anchors[0];
472 assert_eq!(anchor.from.col, 0);
473 assert_eq!(anchor.from.row, 0);
474 }
475
476 #[test]
477 fn test_build_drawing_with_image_invalid_cell() {
478 let config = ImageConfig {
479 data: vec![0x89],
480 format: ImageFormat::Png,
481 from_cell: "INVALID".to_string(),
482 width_px: 100,
483 height_px: 100,
484 };
485
486 let result = build_drawing_with_image("rId1", &config);
487 assert!(result.is_err());
488 }
489
490 #[test]
491 fn test_build_drawing_with_new_format() {
492 let config = ImageConfig {
493 data: vec![0x42, 0x4D],
494 format: ImageFormat::Bmp,
495 from_cell: "D4".to_string(),
496 width_px: 320,
497 height_px: 240,
498 };
499
500 let dr = build_drawing_with_image("rId1", &config).unwrap();
501 assert_eq!(dr.one_cell_anchors.len(), 1);
502 let anchor = &dr.one_cell_anchors[0];
503 assert_eq!(anchor.from.col, 3);
504 assert_eq!(anchor.from.row, 3);
505 assert_eq!(anchor.ext.cx, 320 * 9525);
506 assert_eq!(anchor.ext.cy, 240 * 9525);
507 }
508
509 #[test]
510 fn test_validate_image_config_ok() {
511 let config = ImageConfig {
512 data: vec![1, 2, 3],
513 format: ImageFormat::Png,
514 from_cell: "A1".to_string(),
515 width_px: 100,
516 height_px: 100,
517 };
518 assert!(validate_image_config(&config).is_ok());
519 }
520
521 #[test]
522 fn test_validate_image_config_new_format_ok() {
523 let config = ImageConfig {
524 data: vec![1, 2, 3],
525 format: ImageFormat::Svg,
526 from_cell: "A1".to_string(),
527 width_px: 100,
528 height_px: 100,
529 };
530 assert!(validate_image_config(&config).is_ok());
531 }
532
533 #[test]
534 fn test_validate_image_config_empty_data() {
535 let config = ImageConfig {
536 data: vec![],
537 format: ImageFormat::Png,
538 from_cell: "A1".to_string(),
539 width_px: 100,
540 height_px: 100,
541 };
542 assert!(validate_image_config(&config).is_err());
543 }
544
545 #[test]
546 fn test_validate_image_config_zero_width() {
547 let config = ImageConfig {
548 data: vec![1],
549 format: ImageFormat::Png,
550 from_cell: "A1".to_string(),
551 width_px: 0,
552 height_px: 100,
553 };
554 assert!(validate_image_config(&config).is_err());
555 }
556
557 #[test]
558 fn test_validate_image_config_zero_height() {
559 let config = ImageConfig {
560 data: vec![1],
561 format: ImageFormat::Png,
562 from_cell: "A1".to_string(),
563 width_px: 100,
564 height_px: 0,
565 };
566 assert!(validate_image_config(&config).is_err());
567 }
568
569 #[test]
570 fn test_validate_image_config_invalid_cell() {
571 let config = ImageConfig {
572 data: vec![1],
573 format: ImageFormat::Png,
574 from_cell: "ZZZZZ0".to_string(),
575 width_px: 100,
576 height_px: 100,
577 };
578 assert!(validate_image_config(&config).is_err());
579 }
580
581 #[test]
582 fn test_add_image_to_existing_drawing() {
583 let mut dr = WsDr::default();
584
585 let config = ImageConfig {
586 data: vec![1, 2, 3],
587 format: ImageFormat::Png,
588 from_cell: "C5".to_string(),
589 width_px: 200,
590 height_px: 150,
591 };
592
593 add_image_to_drawing(&mut dr, "rId3", &config, 3).unwrap();
594
595 assert_eq!(dr.one_cell_anchors.len(), 1);
596 let anchor = &dr.one_cell_anchors[0];
597 assert_eq!(anchor.from.col, 2);
598 assert_eq!(anchor.from.row, 4);
599 assert_eq!(
600 anchor.pic.as_ref().unwrap().nv_pic_pr.c_nv_pr.name,
601 "Picture 2"
602 );
603 }
604
605 #[test]
606 fn test_emu_calculation_accuracy() {
607 assert_eq!(pixels_to_emu(96), 914400);
608 }
609}