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