1use super::*;
2
3impl Workbook {
4 pub fn add_chart(
10 &mut self,
11 sheet: &str,
12 from_cell: &str,
13 to_cell: &str,
14 config: &ChartConfig,
15 ) -> Result<()> {
16 self.hydrate_drawings();
17 let sheet_idx =
18 crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
19 Error::SheetNotFound {
20 name: sheet.to_string(),
21 }
22 })?;
23
24 let (from_col, from_row) = cell_name_to_coordinates(from_cell)?;
26 let (to_col, to_row) = cell_name_to_coordinates(to_cell)?;
27
28 let from_marker = MarkerType {
29 col: from_col - 1,
30 col_off: 0,
31 row: from_row - 1,
32 row_off: 0,
33 };
34 let to_marker = MarkerType {
35 col: to_col - 1,
36 col_off: 0,
37 row: to_row - 1,
38 row_off: 0,
39 };
40
41 let chart_num = self.charts.len() + 1;
43 let chart_path = format!("xl/charts/chart{}.xml", chart_num);
44 let chart_space = crate::chart::build_chart_xml(config);
45 self.charts.push((chart_path, chart_space));
46
47 let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
49
50 let chart_rid = self.next_drawing_rid(drawing_idx);
52 let chart_rel_target = format!("../charts/chart{}.xml", chart_num);
53
54 let dr_rels = self
55 .drawing_rels
56 .entry(drawing_idx)
57 .or_insert_with(|| Relationships {
58 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
59 relationships: vec![],
60 });
61 dr_rels.relationships.push(Relationship {
62 id: chart_rid.clone(),
63 rel_type: rel_types::CHART.to_string(),
64 target: chart_rel_target,
65 target_mode: None,
66 });
67
68 let drawing = &mut self.drawings[drawing_idx].1;
70 let anchor = crate::chart::build_drawing_with_chart(&chart_rid, from_marker, to_marker);
71 drawing.two_cell_anchors.extend(anchor.two_cell_anchors);
72
73 self.content_types.overrides.push(ContentTypeOverride {
75 part_name: format!("/xl/charts/chart{}.xml", chart_num),
76 content_type: mime_types::CHART.to_string(),
77 });
78
79 Ok(())
80 }
81
82 pub fn add_shape(&mut self, sheet: &str, config: &crate::shape::ShapeConfig) -> Result<()> {
88 self.hydrate_drawings();
89 let sheet_idx =
90 crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
91 Error::SheetNotFound {
92 name: sheet.to_string(),
93 }
94 })?;
95
96 let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
97
98 let drawing = &mut self.drawings[drawing_idx].1;
99 let shape_id = (drawing.one_cell_anchors.len() + drawing.two_cell_anchors.len() + 2) as u32;
100
101 let anchor = crate::shape::build_shape_anchor(config, shape_id)?;
102 drawing.two_cell_anchors.push(anchor);
103
104 Ok(())
105 }
106
107 pub fn add_image(&mut self, sheet: &str, config: &ImageConfig) -> Result<()> {
113 self.hydrate_drawings();
114 crate::image::validate_image_config(config)?;
115
116 let sheet_idx =
117 crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
118 Error::SheetNotFound {
119 name: sheet.to_string(),
120 }
121 })?;
122
123 let image_num = self.images.len() + 1;
125 let image_path = format!("xl/media/image{}.{}", image_num, config.format.extension());
126 self.images.push((image_path, config.data.clone()));
127
128 let ext = config.format.extension().to_string();
130 if !self
131 .content_types
132 .defaults
133 .iter()
134 .any(|d| d.extension == ext)
135 {
136 self.content_types.defaults.push(ContentTypeDefault {
137 extension: ext,
138 content_type: config.format.content_type().to_string(),
139 });
140 }
141
142 let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
144
145 let image_rid = self.next_drawing_rid(drawing_idx);
147 let image_rel_target = format!("../media/image{}.{}", image_num, config.format.extension());
148
149 let dr_rels = self
150 .drawing_rels
151 .entry(drawing_idx)
152 .or_insert_with(|| Relationships {
153 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
154 relationships: vec![],
155 });
156 dr_rels.relationships.push(Relationship {
157 id: image_rid.clone(),
158 rel_type: rel_types::IMAGE.to_string(),
159 target: image_rel_target,
160 target_mode: None,
161 });
162
163 let drawing = &mut self.drawings[drawing_idx].1;
165 let pic_id = (drawing.one_cell_anchors.len() + drawing.two_cell_anchors.len() + 2) as u32;
166
167 crate::image::add_image_to_drawing(drawing, &image_rid, config, pic_id)?;
169
170 Ok(())
171 }
172
173 pub fn delete_chart(&mut self, sheet: &str, cell: &str) -> Result<()> {
178 self.hydrate_drawings();
179 let sheet_idx = self.sheet_index(sheet)?;
180 let (col, row) = cell_name_to_coordinates(cell)?;
181 let target_col = col - 1;
182 let target_row = row - 1;
183
184 let &drawing_idx =
185 self.worksheet_drawings
186 .get(&sheet_idx)
187 .ok_or_else(|| Error::ChartNotFound {
188 sheet: sheet.to_string(),
189 cell: cell.to_string(),
190 })?;
191
192 let drawing = &self.drawings[drawing_idx].1;
193 let anchor_pos = drawing
194 .two_cell_anchors
195 .iter()
196 .position(|a| {
197 a.from.col == target_col && a.from.row == target_row && a.graphic_frame.is_some()
198 })
199 .ok_or_else(|| Error::ChartNotFound {
200 sheet: sheet.to_string(),
201 cell: cell.to_string(),
202 })?;
203
204 let anchor = &drawing.two_cell_anchors[anchor_pos];
205 let chart_rid = anchor
206 .graphic_frame
207 .as_ref()
208 .unwrap()
209 .graphic
210 .graphic_data
211 .chart
212 .r_id
213 .clone();
214
215 let chart_path = self
216 .drawing_rels
217 .get(&drawing_idx)
218 .and_then(|rels| {
219 rels.relationships
220 .iter()
221 .find(|r| r.id == chart_rid)
222 .map(|r| {
223 let drawing_path = &self.drawings[drawing_idx].0;
224 let base_dir = drawing_path
225 .rfind('/')
226 .map(|i| &drawing_path[..i])
227 .unwrap_or("");
228 if r.target.starts_with("../") {
229 let rel_target = r.target.trim_start_matches("../");
230 let parent = base_dir.rfind('/').map(|i| &base_dir[..i]).unwrap_or("");
231 if parent.is_empty() {
232 rel_target.to_string()
233 } else {
234 format!("{}/{}", parent, rel_target)
235 }
236 } else {
237 format!("{}/{}", base_dir, r.target)
238 }
239 })
240 })
241 .ok_or_else(|| Error::ChartNotFound {
242 sheet: sheet.to_string(),
243 cell: cell.to_string(),
244 })?;
245
246 self.charts.retain(|(path, _)| path != &chart_path);
247 self.raw_charts.retain(|(path, _)| path != &chart_path);
248
249 if let Some(rels) = self.drawing_rels.get_mut(&drawing_idx) {
250 rels.relationships.retain(|r| r.id != chart_rid);
251 }
252
253 self.drawings[drawing_idx]
254 .1
255 .two_cell_anchors
256 .remove(anchor_pos);
257
258 let ct_part_name = format!("/{}", chart_path);
259 self.content_types
260 .overrides
261 .retain(|o| o.part_name != ct_part_name);
262
263 Ok(())
264 }
265
266 pub fn delete_picture(&mut self, sheet: &str, cell: &str) -> Result<()> {
272 self.hydrate_drawings();
273 let sheet_idx = self.sheet_index(sheet)?;
274 let (col, row) = cell_name_to_coordinates(cell)?;
275 let target_col = col - 1;
276 let target_row = row - 1;
277
278 let &drawing_idx =
279 self.worksheet_drawings
280 .get(&sheet_idx)
281 .ok_or_else(|| Error::PictureNotFound {
282 sheet: sheet.to_string(),
283 cell: cell.to_string(),
284 })?;
285
286 let drawing = &self.drawings[drawing_idx].1;
287
288 if let Some(pos) = drawing
290 .one_cell_anchors
291 .iter()
292 .position(|a| a.from.col == target_col && a.from.row == target_row && a.pic.is_some())
293 {
294 let image_rid = drawing.one_cell_anchors[pos]
295 .pic
296 .as_ref()
297 .unwrap()
298 .blip_fill
299 .blip
300 .r_embed
301 .clone();
302
303 self.remove_picture_data(drawing_idx, &image_rid);
304 self.drawings[drawing_idx].1.one_cell_anchors.remove(pos);
305 return Ok(());
306 }
307
308 if let Some(pos) = drawing
310 .two_cell_anchors
311 .iter()
312 .position(|a| a.from.col == target_col && a.from.row == target_row && a.pic.is_some())
313 {
314 let image_rid = drawing.two_cell_anchors[pos]
315 .pic
316 .as_ref()
317 .unwrap()
318 .blip_fill
319 .blip
320 .r_embed
321 .clone();
322
323 self.remove_picture_data(drawing_idx, &image_rid);
324 self.drawings[drawing_idx].1.two_cell_anchors.remove(pos);
325 return Ok(());
326 }
327
328 Err(Error::PictureNotFound {
329 sheet: sheet.to_string(),
330 cell: cell.to_string(),
331 })
332 }
333
334 fn remove_picture_data(&mut self, drawing_idx: usize, image_rid: &str) {
337 let image_path = self.resolve_drawing_rel_target(drawing_idx, image_rid);
338
339 if let Some(rels) = self.drawing_rels.get_mut(&drawing_idx) {
341 rels.relationships.retain(|r| r.id != image_rid);
342 }
343
344 if let Some(path) = image_path {
347 if !self.any_drawing_rel_targets_path(&path) {
348 self.images.retain(|(p, _)| p != &path);
349 }
350 }
351 }
352
353 fn any_drawing_rel_targets_path(&self, target_path: &str) -> bool {
356 for (&di, rels) in &self.drawing_rels {
357 let drawing_path = match self.drawings.get(di) {
358 Some((p, _)) => p,
359 None => continue,
360 };
361 let base_dir = drawing_path
362 .rfind('/')
363 .map(|i| &drawing_path[..i])
364 .unwrap_or("");
365
366 for rel in &rels.relationships {
367 if rel.rel_type != rel_types::IMAGE {
368 continue;
369 }
370 let resolved = if rel.target.starts_with("../") {
371 let rel_target = rel.target.trim_start_matches("../");
372 let parent = base_dir.rfind('/').map(|i| &base_dir[..i]).unwrap_or("");
373 if parent.is_empty() {
374 rel_target.to_string()
375 } else {
376 format!("{}/{}", parent, rel_target)
377 }
378 } else {
379 format!("{}/{}", base_dir, rel.target)
380 };
381 if resolved == target_path {
382 return true;
383 }
384 }
385 }
386 false
387 }
388
389 fn resolve_drawing_rel_target(&self, drawing_idx: usize, rid: &str) -> Option<String> {
391 self.drawing_rels.get(&drawing_idx).and_then(|rels| {
392 rels.relationships
393 .iter()
394 .find(|r| r.id == rid)
395 .and_then(|r| {
396 let drawing_path = &self.drawings.get(drawing_idx)?.0;
397 let base_dir = drawing_path
398 .rfind('/')
399 .map(|i| &drawing_path[..i])
400 .unwrap_or("");
401 Some(if r.target.starts_with("../") {
402 let rel_target = r.target.trim_start_matches("../");
403 let parent = base_dir.rfind('/').map(|i| &base_dir[..i]).unwrap_or("");
404 if parent.is_empty() {
405 rel_target.to_string()
406 } else {
407 format!("{}/{}", parent, rel_target)
408 }
409 } else {
410 format!("{}/{}", base_dir, r.target)
411 })
412 })
413 })
414 }
415
416 pub fn get_pictures(&self, sheet: &str, cell: &str) -> Result<Vec<crate::image::PictureInfo>> {
421 let sheet_idx = self.sheet_index(sheet)?;
422 let (col, row) = cell_name_to_coordinates(cell)?;
423 let target_col = col - 1;
424 let target_row = row - 1;
425
426 let drawing_idx = match self.worksheet_drawings.get(&sheet_idx) {
427 Some(&idx) => idx,
428 None => return Ok(vec![]),
429 };
430
431 let drawing = match self.drawings.get(drawing_idx) {
432 Some((_, d)) => d,
433 None => return Ok(vec![]),
434 };
435 let mut results = Vec::new();
436
437 for anchor in &drawing.one_cell_anchors {
438 if anchor.from.col == target_col && anchor.from.row == target_row {
439 if let Some(pic) = &anchor.pic {
440 if let Some(info) = self.extract_picture_info(drawing_idx, pic, cell) {
441 results.push(info);
442 }
443 }
444 }
445 }
446
447 for anchor in &drawing.two_cell_anchors {
448 if anchor.from.col == target_col && anchor.from.row == target_row {
449 if let Some(pic) = &anchor.pic {
450 if let Some(info) = self.extract_picture_info(drawing_idx, pic, cell) {
451 results.push(info);
452 }
453 }
454 }
455 }
456
457 Ok(results)
458 }
459
460 fn extract_picture_info(
462 &self,
463 drawing_idx: usize,
464 pic: &sheetkit_xml::drawing::Picture,
465 cell: &str,
466 ) -> Option<crate::image::PictureInfo> {
467 let rid = &pic.blip_fill.blip.r_embed;
468 let image_path = self.resolve_drawing_rel_target(drawing_idx, rid)?;
469 let (data, format) = self.find_image_with_format(&image_path)?;
470
471 let cx = pic.sp_pr.xfrm.ext.cx;
472 let cy = pic.sp_pr.xfrm.ext.cy;
473 let width_px = (cx / crate::image::EMU_PER_PIXEL) as u32;
474 let height_px = (cy / crate::image::EMU_PER_PIXEL) as u32;
475
476 Some(crate::image::PictureInfo {
477 data: data.clone(),
478 format,
479 cell: cell.to_string(),
480 width_px,
481 height_px,
482 })
483 }
484
485 fn find_image_with_format(
487 &self,
488 image_path: &str,
489 ) -> Option<(&Vec<u8>, crate::image::ImageFormat)> {
490 self.images
491 .iter()
492 .find(|(p, _)| p == image_path)
493 .and_then(|(path, data)| {
494 let ext = path.rsplit('.').next()?;
495 let format = crate::image::ImageFormat::from_extension(ext).ok()?;
496 Some((data, format))
497 })
498 }
499
500 pub fn get_picture_cells(&self, sheet: &str) -> Result<Vec<String>> {
502 let sheet_idx = self.sheet_index(sheet)?;
503
504 let drawing_idx = match self.worksheet_drawings.get(&sheet_idx) {
505 Some(&idx) => idx,
506 None => return Ok(vec![]),
507 };
508
509 let drawing = match self.drawings.get(drawing_idx) {
510 Some((_, d)) => d,
511 None => return Ok(vec![]),
512 };
513 let mut cells = Vec::new();
514
515 for anchor in &drawing.one_cell_anchors {
516 if anchor.pic.is_some() {
517 if let Ok(name) = crate::utils::cell_ref::coordinates_to_cell_name(
518 anchor.from.col + 1,
519 anchor.from.row + 1,
520 ) {
521 cells.push(name);
522 }
523 }
524 }
525
526 for anchor in &drawing.two_cell_anchors {
527 if anchor.pic.is_some() {
528 if let Ok(name) = crate::utils::cell_ref::coordinates_to_cell_name(
529 anchor.from.col + 1,
530 anchor.from.row + 1,
531 ) {
532 cells.push(name);
533 }
534 }
535 }
536
537 Ok(cells)
538 }
539}
540
541#[cfg(test)]
542#[allow(clippy::unnecessary_cast)]
543mod tests {
544 use super::*;
545 use tempfile::TempDir;
546
547 #[test]
548 fn test_add_chart_basic() {
549 use crate::chart::{ChartConfig, ChartSeries, ChartType};
550 let mut wb = Workbook::new();
551 let config = ChartConfig {
552 chart_type: ChartType::Col,
553 title: Some("Test Chart".to_string()),
554 series: vec![ChartSeries {
555 name: "Sales".to_string(),
556 categories: "Sheet1!$A$1:$A$5".to_string(),
557 values: "Sheet1!$B$1:$B$5".to_string(),
558 x_values: None,
559 bubble_sizes: None,
560 }],
561 show_legend: true,
562 view_3d: None,
563 };
564 wb.add_chart("Sheet1", "E1", "L15", &config).unwrap();
565
566 assert_eq!(wb.charts.len(), 1);
567 assert_eq!(wb.drawings.len(), 1);
568 assert!(wb.worksheet_drawings.contains_key(&0));
569 assert!(wb.drawing_rels.contains_key(&0));
570 assert!(wb.worksheets[0].1.get().unwrap().drawing.is_some());
571 }
572
573 #[test]
574 fn test_add_chart_sheet_not_found() {
575 use crate::chart::{ChartConfig, ChartSeries, ChartType};
576 let mut wb = Workbook::new();
577 let config = ChartConfig {
578 chart_type: ChartType::Line,
579 title: None,
580 series: vec![ChartSeries {
581 name: String::new(),
582 categories: "Sheet1!$A$1:$A$5".to_string(),
583 values: "Sheet1!$B$1:$B$5".to_string(),
584 x_values: None,
585 bubble_sizes: None,
586 }],
587 show_legend: false,
588 view_3d: None,
589 };
590 let result = wb.add_chart("NoSheet", "A1", "H10", &config);
591 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
592 }
593
594 #[test]
595 fn test_add_multiple_charts_same_sheet() {
596 use crate::chart::{ChartConfig, ChartSeries, ChartType};
597 let mut wb = Workbook::new();
598 let config1 = ChartConfig {
599 chart_type: ChartType::Col,
600 title: Some("Chart 1".to_string()),
601 series: vec![ChartSeries {
602 name: "S1".to_string(),
603 categories: "Sheet1!$A$1:$A$3".to_string(),
604 values: "Sheet1!$B$1:$B$3".to_string(),
605 x_values: None,
606 bubble_sizes: None,
607 }],
608 show_legend: true,
609 view_3d: None,
610 };
611 let config2 = ChartConfig {
612 chart_type: ChartType::Line,
613 title: Some("Chart 2".to_string()),
614 series: vec![ChartSeries {
615 name: "S2".to_string(),
616 categories: "Sheet1!$A$1:$A$3".to_string(),
617 values: "Sheet1!$C$1:$C$3".to_string(),
618 x_values: None,
619 bubble_sizes: None,
620 }],
621 show_legend: false,
622 view_3d: None,
623 };
624 wb.add_chart("Sheet1", "A1", "F10", &config1).unwrap();
625 wb.add_chart("Sheet1", "A12", "F22", &config2).unwrap();
626
627 assert_eq!(wb.charts.len(), 2);
628 assert_eq!(wb.drawings.len(), 1);
629 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
630 }
631
632 #[test]
633 fn test_add_charts_different_sheets() {
634 use crate::chart::{ChartConfig, ChartSeries, ChartType};
635 let mut wb = Workbook::new();
636 wb.new_sheet("Sheet2").unwrap();
637
638 let config = ChartConfig {
639 chart_type: ChartType::Pie,
640 title: None,
641 series: vec![ChartSeries {
642 name: String::new(),
643 categories: "Sheet1!$A$1:$A$3".to_string(),
644 values: "Sheet1!$B$1:$B$3".to_string(),
645 x_values: None,
646 bubble_sizes: None,
647 }],
648 show_legend: true,
649 view_3d: None,
650 };
651 wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
652 wb.add_chart("Sheet2", "A1", "F10", &config).unwrap();
653
654 assert_eq!(wb.charts.len(), 2);
655 assert_eq!(wb.drawings.len(), 2);
656 }
657
658 #[test]
659 fn test_save_with_chart() {
660 use crate::chart::{ChartConfig, ChartSeries, ChartType};
661 let dir = TempDir::new().unwrap();
662 let path = dir.path().join("with_chart.xlsx");
663
664 let mut wb = Workbook::new();
665 let config = ChartConfig {
666 chart_type: ChartType::Bar,
667 title: Some("Bar Chart".to_string()),
668 series: vec![ChartSeries {
669 name: "Data".to_string(),
670 categories: "Sheet1!$A$1:$A$3".to_string(),
671 values: "Sheet1!$B$1:$B$3".to_string(),
672 x_values: None,
673 bubble_sizes: None,
674 }],
675 show_legend: true,
676 view_3d: None,
677 };
678 wb.add_chart("Sheet1", "E2", "L15", &config).unwrap();
679 wb.save(&path).unwrap();
680
681 let file = std::fs::File::open(&path).unwrap();
682 let mut archive = zip::ZipArchive::new(file).unwrap();
683
684 assert!(archive.by_name("xl/charts/chart1.xml").is_ok());
685 assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
686 assert!(archive
687 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
688 .is_ok());
689 assert!(archive
690 .by_name("xl/drawings/_rels/drawing1.xml.rels")
691 .is_ok());
692 }
693
694 #[test]
695 fn test_add_image_basic() {
696 use crate::image::{ImageConfig, ImageFormat};
697 let mut wb = Workbook::new();
698 let config = ImageConfig {
699 data: vec![0x89, 0x50, 0x4E, 0x47],
700 format: ImageFormat::Png,
701 from_cell: "B2".to_string(),
702 width_px: 400,
703 height_px: 300,
704 };
705 wb.add_image("Sheet1", &config).unwrap();
706
707 assert_eq!(wb.images.len(), 1);
708 assert_eq!(wb.drawings.len(), 1);
709 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
710 assert!(wb.worksheet_drawings.contains_key(&0));
711 }
712
713 #[test]
714 fn test_add_image_sheet_not_found() {
715 use crate::image::{ImageConfig, ImageFormat};
716 let mut wb = Workbook::new();
717 let config = ImageConfig {
718 data: vec![0x89],
719 format: ImageFormat::Png,
720 from_cell: "A1".to_string(),
721 width_px: 100,
722 height_px: 100,
723 };
724 let result = wb.add_image("NoSheet", &config);
725 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
726 }
727
728 #[test]
729 fn test_add_image_invalid_config() {
730 use crate::image::{ImageConfig, ImageFormat};
731 let mut wb = Workbook::new();
732 let config = ImageConfig {
733 data: vec![],
734 format: ImageFormat::Png,
735 from_cell: "A1".to_string(),
736 width_px: 100,
737 height_px: 100,
738 };
739 assert!(wb.add_image("Sheet1", &config).is_err());
740
741 let config = ImageConfig {
742 data: vec![1],
743 format: ImageFormat::Jpeg,
744 from_cell: "A1".to_string(),
745 width_px: 0,
746 height_px: 100,
747 };
748 assert!(wb.add_image("Sheet1", &config).is_err());
749 }
750
751 #[test]
752 fn test_save_with_image() {
753 use crate::image::{ImageConfig, ImageFormat};
754 let dir = TempDir::new().unwrap();
755 let path = dir.path().join("with_image.xlsx");
756
757 let mut wb = Workbook::new();
758 let config = ImageConfig {
759 data: vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
760 format: ImageFormat::Png,
761 from_cell: "C3".to_string(),
762 width_px: 200,
763 height_px: 150,
764 };
765 wb.add_image("Sheet1", &config).unwrap();
766 wb.save(&path).unwrap();
767
768 let file = std::fs::File::open(&path).unwrap();
769 let mut archive = zip::ZipArchive::new(file).unwrap();
770
771 assert!(archive.by_name("xl/media/image1.png").is_ok());
772 assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
773 assert!(archive
774 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
775 .is_ok());
776 assert!(archive
777 .by_name("xl/drawings/_rels/drawing1.xml.rels")
778 .is_ok());
779 }
780
781 #[test]
782 fn test_save_with_jpeg_image() {
783 use crate::image::{ImageConfig, ImageFormat};
784 let dir = TempDir::new().unwrap();
785 let path = dir.path().join("with_jpeg.xlsx");
786
787 let mut wb = Workbook::new();
788 let config = ImageConfig {
789 data: vec![0xFF, 0xD8, 0xFF, 0xE0],
790 format: ImageFormat::Jpeg,
791 from_cell: "A1".to_string(),
792 width_px: 640,
793 height_px: 480,
794 };
795 wb.add_image("Sheet1", &config).unwrap();
796 wb.save(&path).unwrap();
797
798 let file = std::fs::File::open(&path).unwrap();
799 let mut archive = zip::ZipArchive::new(file).unwrap();
800 assert!(archive.by_name("xl/media/image1.jpeg").is_ok());
801 }
802
803 #[test]
804 fn test_save_with_new_image_formats() {
805 use crate::image::{ImageConfig, ImageFormat};
806
807 let formats = [
808 (ImageFormat::Bmp, "bmp"),
809 (ImageFormat::Ico, "ico"),
810 (ImageFormat::Tiff, "tiff"),
811 (ImageFormat::Svg, "svg"),
812 (ImageFormat::Emf, "emf"),
813 (ImageFormat::Emz, "emz"),
814 (ImageFormat::Wmf, "wmf"),
815 (ImageFormat::Wmz, "wmz"),
816 ];
817
818 for (format, ext) in &formats {
819 let dir = TempDir::new().unwrap();
820 let path = dir.path().join(format!("with_{ext}.xlsx"));
821
822 let mut wb = Workbook::new();
823 let config = ImageConfig {
824 data: vec![0x00, 0x01, 0x02, 0x03],
825 format: format.clone(),
826 from_cell: "A1".to_string(),
827 width_px: 100,
828 height_px: 100,
829 };
830 wb.add_image("Sheet1", &config).unwrap();
831 wb.save(&path).unwrap();
832
833 let file = std::fs::File::open(&path).unwrap();
834 let mut archive = zip::ZipArchive::new(file).unwrap();
835 let media_path = format!("xl/media/image1.{ext}");
836 assert!(
837 archive.by_name(&media_path).is_ok(),
838 "expected {media_path} in archive for format {ext}"
839 );
840 }
841 }
842
843 #[test]
844 fn test_add_image_new_format_content_type_default() {
845 use crate::image::{ImageConfig, ImageFormat};
846 let mut wb = Workbook::new();
847 let config = ImageConfig {
848 data: vec![0x42, 0x4D],
849 format: ImageFormat::Bmp,
850 from_cell: "A1".to_string(),
851 width_px: 100,
852 height_px: 100,
853 };
854 wb.add_image("Sheet1", &config).unwrap();
855
856 let has_bmp_default = wb
857 .content_types
858 .defaults
859 .iter()
860 .any(|d| d.extension == "bmp" && d.content_type == "image/bmp");
861 assert!(has_bmp_default, "content types should have bmp default");
862 }
863
864 #[test]
865 fn test_add_image_svg_content_type_default() {
866 use crate::image::{ImageConfig, ImageFormat};
867 let mut wb = Workbook::new();
868 let config = ImageConfig {
869 data: vec![0x3C, 0x73, 0x76, 0x67],
870 format: ImageFormat::Svg,
871 from_cell: "B3".to_string(),
872 width_px: 200,
873 height_px: 200,
874 };
875 wb.add_image("Sheet1", &config).unwrap();
876
877 let has_svg_default = wb
878 .content_types
879 .defaults
880 .iter()
881 .any(|d| d.extension == "svg" && d.content_type == "image/svg+xml");
882 assert!(has_svg_default, "content types should have svg default");
883 }
884
885 #[test]
886 fn test_add_image_emf_content_type_and_path() {
887 use crate::image::{ImageConfig, ImageFormat};
888 let dir = TempDir::new().unwrap();
889 let path = dir.path().join("with_emf.xlsx");
890
891 let mut wb = Workbook::new();
892 let config = ImageConfig {
893 data: vec![0x01, 0x00, 0x00, 0x00],
894 format: ImageFormat::Emf,
895 from_cell: "A1".to_string(),
896 width_px: 150,
897 height_px: 150,
898 };
899 wb.add_image("Sheet1", &config).unwrap();
900
901 let has_emf_default = wb
902 .content_types
903 .defaults
904 .iter()
905 .any(|d| d.extension == "emf" && d.content_type == "image/x-emf");
906 assert!(has_emf_default);
907
908 wb.save(&path).unwrap();
909 let file = std::fs::File::open(&path).unwrap();
910 let mut archive = zip::ZipArchive::new(file).unwrap();
911 assert!(archive.by_name("xl/media/image1.emf").is_ok());
912 }
913
914 #[test]
915 fn test_add_multiple_new_format_images() {
916 use crate::image::{ImageConfig, ImageFormat};
917 let mut wb = Workbook::new();
918
919 wb.add_image(
920 "Sheet1",
921 &ImageConfig {
922 data: vec![0x42, 0x4D],
923 format: ImageFormat::Bmp,
924 from_cell: "A1".to_string(),
925 width_px: 100,
926 height_px: 100,
927 },
928 )
929 .unwrap();
930
931 wb.add_image(
932 "Sheet1",
933 &ImageConfig {
934 data: vec![0x3C, 0x73],
935 format: ImageFormat::Svg,
936 from_cell: "C1".to_string(),
937 width_px: 100,
938 height_px: 100,
939 },
940 )
941 .unwrap();
942
943 wb.add_image(
944 "Sheet1",
945 &ImageConfig {
946 data: vec![0x01, 0x00],
947 format: ImageFormat::Wmf,
948 from_cell: "E1".to_string(),
949 width_px: 100,
950 height_px: 100,
951 },
952 )
953 .unwrap();
954
955 assert_eq!(wb.images.len(), 3);
956 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 3);
957
958 let ext_defaults: Vec<&str> = wb
959 .content_types
960 .defaults
961 .iter()
962 .map(|d| d.extension.as_str())
963 .collect();
964 assert!(ext_defaults.contains(&"bmp"));
965 assert!(ext_defaults.contains(&"svg"));
966 assert!(ext_defaults.contains(&"wmf"));
967 }
968
969 #[test]
970 fn test_add_shape_basic() {
971 use crate::shape::{ShapeConfig, ShapeType};
972 let mut wb = Workbook::new();
973 let config = ShapeConfig {
974 shape_type: ShapeType::Rect,
975 from_cell: "B2".to_string(),
976 to_cell: "F10".to_string(),
977 text: Some("Test Shape".to_string()),
978 fill_color: Some("FF0000".to_string()),
979 line_color: None,
980 line_width: None,
981 };
982 wb.add_shape("Sheet1", &config).unwrap();
983
984 assert_eq!(wb.drawings.len(), 1);
985 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
986 let anchor = &wb.drawings[0].1.two_cell_anchors[0];
987 assert!(anchor.shape.is_some());
988 assert!(anchor.graphic_frame.is_none());
989 assert!(anchor.pic.is_none());
990 }
991
992 #[test]
993 fn test_add_multiple_shapes_same_sheet() {
994 use crate::shape::{ShapeConfig, ShapeType};
995 let mut wb = Workbook::new();
996 wb.add_shape(
997 "Sheet1",
998 &ShapeConfig {
999 shape_type: ShapeType::Rect,
1000 from_cell: "A1".to_string(),
1001 to_cell: "C3".to_string(),
1002 text: None,
1003 fill_color: None,
1004 line_color: None,
1005 line_width: None,
1006 },
1007 )
1008 .unwrap();
1009 wb.add_shape(
1010 "Sheet1",
1011 &ShapeConfig {
1012 shape_type: ShapeType::Ellipse,
1013 from_cell: "E1".to_string(),
1014 to_cell: "H5".to_string(),
1015 text: Some("Circle".to_string()),
1016 fill_color: Some("00FF00".to_string()),
1017 line_color: None,
1018 line_width: None,
1019 },
1020 )
1021 .unwrap();
1022
1023 assert_eq!(wb.drawings.len(), 1);
1024 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
1025 }
1026
1027 #[test]
1028 fn test_add_shape_and_chart_same_sheet() {
1029 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1030 use crate::shape::{ShapeConfig, ShapeType};
1031
1032 let mut wb = Workbook::new();
1033 wb.add_chart(
1034 "Sheet1",
1035 "E1",
1036 "L10",
1037 &ChartConfig {
1038 chart_type: ChartType::Col,
1039 title: Some("Chart".to_string()),
1040 series: vec![ChartSeries {
1041 name: "S1".to_string(),
1042 categories: "Sheet1!$A$1:$A$3".to_string(),
1043 values: "Sheet1!$B$1:$B$3".to_string(),
1044 x_values: None,
1045 bubble_sizes: None,
1046 }],
1047 show_legend: true,
1048 view_3d: None,
1049 },
1050 )
1051 .unwrap();
1052 wb.add_shape(
1053 "Sheet1",
1054 &ShapeConfig {
1055 shape_type: ShapeType::Rect,
1056 from_cell: "A12".to_string(),
1057 to_cell: "D18".to_string(),
1058 text: Some("Label".to_string()),
1059 fill_color: None,
1060 line_color: None,
1061 line_width: None,
1062 },
1063 )
1064 .unwrap();
1065
1066 assert_eq!(wb.drawings.len(), 1);
1067 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
1068 }
1069
1070 #[test]
1071 fn test_add_chart_and_image_same_sheet() {
1072 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1073 use crate::image::{ImageConfig, ImageFormat};
1074
1075 let mut wb = Workbook::new();
1076
1077 let chart_config = ChartConfig {
1078 chart_type: ChartType::Col,
1079 title: Some("My Chart".to_string()),
1080 series: vec![ChartSeries {
1081 name: "Series 1".to_string(),
1082 categories: "Sheet1!$A$1:$A$3".to_string(),
1083 values: "Sheet1!$B$1:$B$3".to_string(),
1084 x_values: None,
1085 bubble_sizes: None,
1086 }],
1087 show_legend: true,
1088 view_3d: None,
1089 };
1090 wb.add_chart("Sheet1", "E1", "L10", &chart_config).unwrap();
1091
1092 let image_config = ImageConfig {
1093 data: vec![0x89, 0x50, 0x4E, 0x47],
1094 format: ImageFormat::Png,
1095 from_cell: "E12".to_string(),
1096 width_px: 300,
1097 height_px: 200,
1098 };
1099 wb.add_image("Sheet1", &image_config).unwrap();
1100
1101 assert_eq!(wb.drawings.len(), 1);
1102 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
1103 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
1104 assert_eq!(wb.charts.len(), 1);
1105 assert_eq!(wb.images.len(), 1);
1106 }
1107
1108 #[test]
1109 fn test_save_with_chart_roundtrip_drawing_ref() {
1110 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1111 let dir = TempDir::new().unwrap();
1112 let path = dir.path().join("chart_drawref.xlsx");
1113
1114 let mut wb = Workbook::new();
1115 let config = ChartConfig {
1116 chart_type: ChartType::Col,
1117 title: None,
1118 series: vec![ChartSeries {
1119 name: "Series 1".to_string(),
1120 categories: "Sheet1!$A$1:$A$3".to_string(),
1121 values: "Sheet1!$B$1:$B$3".to_string(),
1122 x_values: None,
1123 bubble_sizes: None,
1124 }],
1125 show_legend: false,
1126 view_3d: None,
1127 };
1128 wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
1129 wb.save(&path).unwrap();
1130
1131 let wb2 = Workbook::open(&path).unwrap();
1132 let ws = wb2.worksheet_ref("Sheet1").unwrap();
1133 assert!(ws.drawing.is_some());
1134 }
1135
1136 #[test]
1137 fn test_open_save_preserves_existing_drawing_chart_and_image_parts() {
1138 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1139 use crate::image::{ImageConfig, ImageFormat};
1140 let dir = TempDir::new().unwrap();
1141 let path1 = dir.path().join("source_with_parts.xlsx");
1142 let path2 = dir.path().join("resaved_with_parts.xlsx");
1143
1144 let mut wb = Workbook::new();
1145 wb.add_chart(
1146 "Sheet1",
1147 "E1",
1148 "L10",
1149 &ChartConfig {
1150 chart_type: ChartType::Col,
1151 title: Some("Chart".to_string()),
1152 series: vec![ChartSeries {
1153 name: "Series 1".to_string(),
1154 categories: "Sheet1!$A$1:$A$3".to_string(),
1155 values: "Sheet1!$B$1:$B$3".to_string(),
1156 x_values: None,
1157 bubble_sizes: None,
1158 }],
1159 show_legend: true,
1160 view_3d: None,
1161 },
1162 )
1163 .unwrap();
1164 wb.add_image(
1165 "Sheet1",
1166 &ImageConfig {
1167 data: vec![0x89, 0x50, 0x4E, 0x47],
1168 format: ImageFormat::Png,
1169 from_cell: "E12".to_string(),
1170 width_px: 120,
1171 height_px: 80,
1172 },
1173 )
1174 .unwrap();
1175 wb.save(&path1).unwrap();
1176
1177 let opts = crate::workbook::open_options::OpenOptions::new()
1178 .read_mode(crate::workbook::open_options::ReadMode::Eager)
1179 .aux_parts(crate::workbook::open_options::AuxParts::EagerLoad);
1180 let wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
1181 assert_eq!(wb2.charts.len() + wb2.raw_charts.len(), 1);
1182 assert_eq!(wb2.drawings.len(), 1);
1183 assert_eq!(wb2.images.len(), 1);
1184 assert_eq!(wb2.drawing_rels.len(), 1);
1185 assert_eq!(wb2.worksheet_drawings.len(), 1);
1186
1187 wb2.save(&path2).unwrap();
1188
1189 let file = std::fs::File::open(&path2).unwrap();
1190 let mut archive = zip::ZipArchive::new(file).unwrap();
1191 assert!(archive.by_name("xl/charts/chart1.xml").is_ok());
1192 assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
1193 assert!(archive.by_name("xl/media/image1.png").is_ok());
1194 assert!(archive
1195 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
1196 .is_ok());
1197 assert!(archive
1198 .by_name("xl/drawings/_rels/drawing1.xml.rels")
1199 .is_ok());
1200 }
1201
1202 #[test]
1203 fn test_delete_chart_basic() {
1204 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1205 let mut wb = Workbook::new();
1206 let config = ChartConfig {
1207 chart_type: ChartType::Col,
1208 title: Some("Chart".to_string()),
1209 series: vec![ChartSeries {
1210 name: "S1".to_string(),
1211 categories: "Sheet1!$A$1:$A$3".to_string(),
1212 values: "Sheet1!$B$1:$B$3".to_string(),
1213 x_values: None,
1214 bubble_sizes: None,
1215 }],
1216 show_legend: true,
1217 view_3d: None,
1218 };
1219 wb.add_chart("Sheet1", "E1", "L10", &config).unwrap();
1220 assert_eq!(wb.charts.len(), 1);
1221 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
1222
1223 wb.delete_chart("Sheet1", "E1").unwrap();
1224 assert_eq!(wb.charts.len(), 0);
1225 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 0);
1226 }
1227
1228 #[test]
1229 fn test_delete_chart_not_found() {
1230 let mut wb = Workbook::new();
1231 let result = wb.delete_chart("Sheet1", "A1");
1232 assert!(result.is_err());
1233 assert!(matches!(result.unwrap_err(), Error::ChartNotFound { .. }));
1234 }
1235
1236 #[test]
1237 fn test_delete_chart_wrong_cell() {
1238 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1239 let mut wb = Workbook::new();
1240 let config = ChartConfig {
1241 chart_type: ChartType::Col,
1242 title: None,
1243 series: vec![ChartSeries {
1244 name: "S".to_string(),
1245 categories: "Sheet1!$A$1:$A$3".to_string(),
1246 values: "Sheet1!$B$1:$B$3".to_string(),
1247 x_values: None,
1248 bubble_sizes: None,
1249 }],
1250 show_legend: false,
1251 view_3d: None,
1252 };
1253 wb.add_chart("Sheet1", "E1", "L10", &config).unwrap();
1254
1255 let result = wb.delete_chart("Sheet1", "A1");
1256 assert!(result.is_err());
1257 assert_eq!(wb.charts.len(), 1);
1258 }
1259
1260 #[test]
1261 fn test_delete_chart_removes_content_type() {
1262 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1263 let mut wb = Workbook::new();
1264 let config = ChartConfig {
1265 chart_type: ChartType::Col,
1266 title: None,
1267 series: vec![ChartSeries {
1268 name: "S".to_string(),
1269 categories: "Sheet1!$A$1:$A$3".to_string(),
1270 values: "Sheet1!$B$1:$B$3".to_string(),
1271 x_values: None,
1272 bubble_sizes: None,
1273 }],
1274 show_legend: false,
1275 view_3d: None,
1276 };
1277 wb.add_chart("Sheet1", "E1", "L10", &config).unwrap();
1278 let has_chart_ct = wb
1279 .content_types
1280 .overrides
1281 .iter()
1282 .any(|o| o.part_name.contains("chart"));
1283 assert!(has_chart_ct);
1284
1285 wb.delete_chart("Sheet1", "E1").unwrap();
1286 let has_chart_ct = wb
1287 .content_types
1288 .overrides
1289 .iter()
1290 .any(|o| o.part_name.contains("chart"));
1291 assert!(!has_chart_ct);
1292 }
1293
1294 #[test]
1295 fn test_delete_one_chart_keeps_others() {
1296 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1297 let mut wb = Workbook::new();
1298 let config = ChartConfig {
1299 chart_type: ChartType::Col,
1300 title: None,
1301 series: vec![ChartSeries {
1302 name: "S".to_string(),
1303 categories: "Sheet1!$A$1:$A$3".to_string(),
1304 values: "Sheet1!$B$1:$B$3".to_string(),
1305 x_values: None,
1306 bubble_sizes: None,
1307 }],
1308 show_legend: false,
1309 view_3d: None,
1310 };
1311 wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
1312 wb.add_chart("Sheet1", "A12", "F22", &config).unwrap();
1313 assert_eq!(wb.charts.len(), 2);
1314 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
1315
1316 wb.delete_chart("Sheet1", "A1").unwrap();
1317 assert_eq!(wb.charts.len(), 1);
1318 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
1319 assert_eq!(wb.drawings[0].1.two_cell_anchors[0].from.row, 11);
1320 }
1321
1322 #[test]
1323 fn test_delete_picture_basic() {
1324 use crate::image::{ImageConfig, ImageFormat};
1325 let mut wb = Workbook::new();
1326 let config = ImageConfig {
1327 data: vec![0x89, 0x50, 0x4E, 0x47],
1328 format: ImageFormat::Png,
1329 from_cell: "B2".to_string(),
1330 width_px: 200,
1331 height_px: 150,
1332 };
1333 wb.add_image("Sheet1", &config).unwrap();
1334 assert_eq!(wb.images.len(), 1);
1335 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
1336
1337 wb.delete_picture("Sheet1", "B2").unwrap();
1338 assert_eq!(wb.images.len(), 0);
1339 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 0);
1340 }
1341
1342 #[test]
1343 fn test_delete_picture_not_found() {
1344 let mut wb = Workbook::new();
1345 let result = wb.delete_picture("Sheet1", "A1");
1346 assert!(result.is_err());
1347 assert!(matches!(result.unwrap_err(), Error::PictureNotFound { .. }));
1348 }
1349
1350 #[test]
1351 fn test_delete_picture_wrong_cell() {
1352 use crate::image::{ImageConfig, ImageFormat};
1353 let mut wb = Workbook::new();
1354 let config = ImageConfig {
1355 data: vec![0x89, 0x50, 0x4E, 0x47],
1356 format: ImageFormat::Png,
1357 from_cell: "C3".to_string(),
1358 width_px: 100,
1359 height_px: 100,
1360 };
1361 wb.add_image("Sheet1", &config).unwrap();
1362
1363 let result = wb.delete_picture("Sheet1", "A1");
1364 assert!(result.is_err());
1365 assert_eq!(wb.images.len(), 1);
1366 }
1367
1368 #[test]
1369 fn test_delete_one_picture_keeps_others() {
1370 use crate::image::{ImageConfig, ImageFormat};
1371 let mut wb = Workbook::new();
1372 wb.add_image(
1373 "Sheet1",
1374 &ImageConfig {
1375 data: vec![0x89, 0x50, 0x4E, 0x47],
1376 format: ImageFormat::Png,
1377 from_cell: "A1".to_string(),
1378 width_px: 100,
1379 height_px: 100,
1380 },
1381 )
1382 .unwrap();
1383 wb.add_image(
1384 "Sheet1",
1385 &ImageConfig {
1386 data: vec![0xFF, 0xD8, 0xFF, 0xE0],
1387 format: ImageFormat::Jpeg,
1388 from_cell: "C3".to_string(),
1389 width_px: 200,
1390 height_px: 200,
1391 },
1392 )
1393 .unwrap();
1394 assert_eq!(wb.images.len(), 2);
1395 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 2);
1396
1397 wb.delete_picture("Sheet1", "A1").unwrap();
1398 assert_eq!(wb.images.len(), 1);
1399 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
1400 assert_eq!(wb.drawings[0].1.one_cell_anchors[0].from.col, 2);
1401 }
1402
1403 #[test]
1404 fn test_get_picture_cells_empty() {
1405 let wb = Workbook::new();
1406 let cells = wb.get_picture_cells("Sheet1").unwrap();
1407 assert!(cells.is_empty());
1408 }
1409
1410 #[test]
1411 fn test_get_picture_cells_returns_cells() {
1412 use crate::image::{ImageConfig, ImageFormat};
1413 let mut wb = Workbook::new();
1414 wb.add_image(
1415 "Sheet1",
1416 &ImageConfig {
1417 data: vec![0x89, 0x50],
1418 format: ImageFormat::Png,
1419 from_cell: "B2".to_string(),
1420 width_px: 100,
1421 height_px: 100,
1422 },
1423 )
1424 .unwrap();
1425 wb.add_image(
1426 "Sheet1",
1427 &ImageConfig {
1428 data: vec![0xFF, 0xD8],
1429 format: ImageFormat::Jpeg,
1430 from_cell: "D5".to_string(),
1431 width_px: 200,
1432 height_px: 150,
1433 },
1434 )
1435 .unwrap();
1436
1437 let cells = wb.get_picture_cells("Sheet1").unwrap();
1438 assert_eq!(cells.len(), 2);
1439 assert!(cells.contains(&"B2".to_string()));
1440 assert!(cells.contains(&"D5".to_string()));
1441 }
1442
1443 #[test]
1444 fn test_get_pictures_returns_data() {
1445 use crate::image::{ImageConfig, ImageFormat};
1446 let mut wb = Workbook::new();
1447 let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
1448 wb.add_image(
1449 "Sheet1",
1450 &ImageConfig {
1451 data: image_data.clone(),
1452 format: ImageFormat::Png,
1453 from_cell: "B2".to_string(),
1454 width_px: 400,
1455 height_px: 300,
1456 },
1457 )
1458 .unwrap();
1459
1460 let pics = wb.get_pictures("Sheet1", "B2").unwrap();
1461 assert_eq!(pics.len(), 1);
1462 assert_eq!(pics[0].data, image_data);
1463 assert_eq!(pics[0].format, ImageFormat::Png);
1464 assert_eq!(pics[0].cell, "B2");
1465 assert_eq!(pics[0].width_px, 400);
1466 assert_eq!(pics[0].height_px, 300);
1467 }
1468
1469 #[test]
1470 fn test_get_pictures_empty_cell() {
1471 let wb = Workbook::new();
1472 let pics = wb.get_pictures("Sheet1", "A1").unwrap();
1473 assert!(pics.is_empty());
1474 }
1475
1476 #[test]
1477 fn test_get_pictures_wrong_cell() {
1478 use crate::image::{ImageConfig, ImageFormat};
1479 let mut wb = Workbook::new();
1480 wb.add_image(
1481 "Sheet1",
1482 &ImageConfig {
1483 data: vec![0x89, 0x50],
1484 format: ImageFormat::Png,
1485 from_cell: "B2".to_string(),
1486 width_px: 100,
1487 height_px: 100,
1488 },
1489 )
1490 .unwrap();
1491
1492 let pics = wb.get_pictures("Sheet1", "A1").unwrap();
1493 assert!(pics.is_empty());
1494 }
1495
1496 #[test]
1497 fn test_delete_chart_roundtrip() {
1498 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1499 let dir = TempDir::new().unwrap();
1500 let path1 = dir.path().join("chart_delete_rt1.xlsx");
1501 let path2 = dir.path().join("chart_delete_rt2.xlsx");
1502
1503 let mut wb = Workbook::new();
1504 wb.add_chart(
1505 "Sheet1",
1506 "E1",
1507 "L10",
1508 &ChartConfig {
1509 chart_type: ChartType::Col,
1510 title: Some("Chart".to_string()),
1511 series: vec![ChartSeries {
1512 name: "S1".to_string(),
1513 categories: "Sheet1!$A$1:$A$3".to_string(),
1514 values: "Sheet1!$B$1:$B$3".to_string(),
1515 x_values: None,
1516 bubble_sizes: None,
1517 }],
1518 show_legend: true,
1519 view_3d: None,
1520 },
1521 )
1522 .unwrap();
1523 wb.save(&path1).unwrap();
1524
1525 wb.delete_chart("Sheet1", "E1").unwrap();
1527 wb.save(&path2).unwrap();
1528
1529 let file = std::fs::File::open(&path2).unwrap();
1530 let mut archive = zip::ZipArchive::new(file).unwrap();
1531 assert!(archive.by_name("xl/charts/chart1.xml").is_err());
1532 }
1533
1534 #[test]
1535 fn test_delete_picture_roundtrip() {
1536 use crate::image::{ImageConfig, ImageFormat};
1537 let dir = TempDir::new().unwrap();
1538 let path1 = dir.path().join("pic_delete_rt1.xlsx");
1539 let path2 = dir.path().join("pic_delete_rt2.xlsx");
1540
1541 let mut wb = Workbook::new();
1542 wb.add_image(
1543 "Sheet1",
1544 &ImageConfig {
1545 data: vec![0x89, 0x50, 0x4E, 0x47],
1546 format: ImageFormat::Png,
1547 from_cell: "B2".to_string(),
1548 width_px: 200,
1549 height_px: 150,
1550 },
1551 )
1552 .unwrap();
1553 wb.save(&path1).unwrap();
1554
1555 wb.delete_picture("Sheet1", "B2").unwrap();
1557 wb.save(&path2).unwrap();
1558
1559 let file = std::fs::File::open(&path2).unwrap();
1560 let mut archive = zip::ZipArchive::new(file).unwrap();
1561 assert!(archive.by_name("xl/media/image1.png").is_err());
1562 }
1563
1564 #[test]
1565 fn test_delete_chart_preserves_image() {
1566 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1567 use crate::image::{ImageConfig, ImageFormat};
1568
1569 let mut wb = Workbook::new();
1570 wb.add_chart(
1571 "Sheet1",
1572 "E1",
1573 "L10",
1574 &ChartConfig {
1575 chart_type: ChartType::Col,
1576 title: None,
1577 series: vec![ChartSeries {
1578 name: "S1".to_string(),
1579 categories: "Sheet1!$A$1:$A$3".to_string(),
1580 values: "Sheet1!$B$1:$B$3".to_string(),
1581 x_values: None,
1582 bubble_sizes: None,
1583 }],
1584 show_legend: false,
1585 view_3d: None,
1586 },
1587 )
1588 .unwrap();
1589 wb.add_image(
1590 "Sheet1",
1591 &ImageConfig {
1592 data: vec![0x89, 0x50, 0x4E, 0x47],
1593 format: ImageFormat::Png,
1594 from_cell: "E12".to_string(),
1595 width_px: 200,
1596 height_px: 150,
1597 },
1598 )
1599 .unwrap();
1600
1601 wb.delete_chart("Sheet1", "E1").unwrap();
1602 assert_eq!(wb.charts.len(), 0);
1603 assert_eq!(wb.images.len(), 1);
1604 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
1605 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 0);
1606 }
1607
1608 #[test]
1609 fn test_delete_picture_preserves_chart() {
1610 use crate::chart::{ChartConfig, ChartSeries, ChartType};
1611 use crate::image::{ImageConfig, ImageFormat};
1612
1613 let mut wb = Workbook::new();
1614 wb.add_chart(
1615 "Sheet1",
1616 "E1",
1617 "L10",
1618 &ChartConfig {
1619 chart_type: ChartType::Col,
1620 title: None,
1621 series: vec![ChartSeries {
1622 name: "S1".to_string(),
1623 categories: "Sheet1!$A$1:$A$3".to_string(),
1624 values: "Sheet1!$B$1:$B$3".to_string(),
1625 x_values: None,
1626 bubble_sizes: None,
1627 }],
1628 show_legend: false,
1629 view_3d: None,
1630 },
1631 )
1632 .unwrap();
1633 wb.add_image(
1634 "Sheet1",
1635 &ImageConfig {
1636 data: vec![0x89, 0x50, 0x4E, 0x47],
1637 format: ImageFormat::Png,
1638 from_cell: "E12".to_string(),
1639 width_px: 200,
1640 height_px: 150,
1641 },
1642 )
1643 .unwrap();
1644
1645 wb.delete_picture("Sheet1", "E12").unwrap();
1646 assert_eq!(wb.images.len(), 0);
1647 assert_eq!(wb.charts.len(), 1);
1648 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
1649 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 0);
1650 }
1651
1652 fn add_shared_media_anchor(
1655 wb: &mut Workbook,
1656 drawing_idx: usize,
1657 media_rel_target: &str,
1658 from_col: u32,
1659 from_row: u32,
1660 pic_id: u32,
1661 ) -> String {
1662 use sheetkit_xml::drawing::*;
1663 use sheetkit_xml::relationships::Relationship;
1664
1665 let rid = wb.next_drawing_rid(drawing_idx);
1666
1667 let rels = wb
1668 .drawing_rels
1669 .entry(drawing_idx)
1670 .or_insert_with(|| Relationships {
1671 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
1672 relationships: vec![],
1673 });
1674 rels.relationships.push(Relationship {
1675 id: rid.clone(),
1676 rel_type: rel_types::IMAGE.to_string(),
1677 target: media_rel_target.to_string(),
1678 target_mode: None,
1679 });
1680
1681 let pic = Picture {
1682 nv_pic_pr: NvPicPr {
1683 c_nv_pr: CNvPr {
1684 id: pic_id,
1685 name: format!("Picture {}", pic_id - 1),
1686 },
1687 c_nv_pic_pr: CNvPicPr {},
1688 },
1689 blip_fill: BlipFill {
1690 blip: Blip {
1691 r_embed: rid.clone(),
1692 },
1693 stretch: Stretch {
1694 fill_rect: FillRect {},
1695 },
1696 },
1697 sp_pr: SpPr {
1698 xfrm: Xfrm {
1699 off: Offset { x: 0, y: 0 },
1700 ext: AExt {
1701 cx: 100 * crate::image::EMU_PER_PIXEL as u64,
1702 cy: 100 * crate::image::EMU_PER_PIXEL as u64,
1703 },
1704 },
1705 prst_geom: PrstGeom {
1706 prst: "rect".to_string(),
1707 },
1708 },
1709 };
1710
1711 wb.drawings[drawing_idx]
1712 .1
1713 .one_cell_anchors
1714 .push(OneCellAnchor {
1715 from: MarkerType {
1716 col: from_col,
1717 col_off: 0,
1718 row: from_row,
1719 row_off: 0,
1720 },
1721 ext: Extent {
1722 cx: 100 * crate::image::EMU_PER_PIXEL as u64,
1723 cy: 100 * crate::image::EMU_PER_PIXEL as u64,
1724 },
1725 pic: Some(pic),
1726 client_data: ClientData {},
1727 });
1728
1729 rid
1730 }
1731
1732 #[test]
1733 fn test_delete_shared_media_keeps_other_picture() {
1734 use crate::image::{ImageConfig, ImageFormat};
1735
1736 let mut wb = Workbook::new();
1737 let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A];
1738 wb.add_image(
1739 "Sheet1",
1740 &ImageConfig {
1741 data: image_data.clone(),
1742 format: ImageFormat::Png,
1743 from_cell: "B2".to_string(),
1744 width_px: 200,
1745 height_px: 150,
1746 },
1747 )
1748 .unwrap();
1749
1750 let drawing_idx = *wb.worksheet_drawings.get(&0).unwrap();
1752 add_shared_media_anchor(&mut wb, drawing_idx, "../media/image1.png", 3, 3, 3);
1753
1754 assert_eq!(wb.images.len(), 1);
1755 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 2);
1756
1757 wb.delete_picture("Sheet1", "B2").unwrap();
1759
1760 assert_eq!(wb.images.len(), 1);
1762 assert_eq!(wb.images[0].0, "xl/media/image1.png");
1763 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
1764
1765 let pics = wb.get_pictures("Sheet1", "D4").unwrap();
1767 assert_eq!(pics.len(), 1);
1768 assert_eq!(pics[0].data, image_data);
1769 }
1770
1771 #[test]
1772 fn test_delete_both_shared_media_pictures_cleans_up() {
1773 use crate::image::{ImageConfig, ImageFormat};
1774
1775 let mut wb = Workbook::new();
1776 wb.add_image(
1777 "Sheet1",
1778 &ImageConfig {
1779 data: vec![0x89, 0x50, 0x4E, 0x47],
1780 format: ImageFormat::Png,
1781 from_cell: "B2".to_string(),
1782 width_px: 100,
1783 height_px: 100,
1784 },
1785 )
1786 .unwrap();
1787
1788 let drawing_idx = *wb.worksheet_drawings.get(&0).unwrap();
1789 add_shared_media_anchor(&mut wb, drawing_idx, "../media/image1.png", 3, 3, 3);
1790
1791 assert_eq!(wb.images.len(), 1);
1792 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 2);
1793
1794 wb.delete_picture("Sheet1", "B2").unwrap();
1796 assert_eq!(wb.images.len(), 1);
1797
1798 wb.delete_picture("Sheet1", "D4").unwrap();
1800 assert_eq!(wb.images.len(), 0);
1801 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 0);
1802 }
1803
1804 #[test]
1805 fn test_cross_sheet_shared_media_survives_single_delete() {
1806 use crate::image::{ImageConfig, ImageFormat};
1807
1808 let mut wb = Workbook::new();
1809 wb.new_sheet("Sheet2").unwrap();
1810
1811 let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D];
1812 wb.add_image(
1813 "Sheet1",
1814 &ImageConfig {
1815 data: image_data.clone(),
1816 format: ImageFormat::Png,
1817 from_cell: "A1".to_string(),
1818 width_px: 100,
1819 height_px: 100,
1820 },
1821 )
1822 .unwrap();
1823
1824 let drawing_idx_s2 = wb.ensure_drawing_for_sheet(1);
1826 add_shared_media_anchor(&mut wb, drawing_idx_s2, "../media/image1.png", 0, 0, 2);
1827
1828 assert_eq!(wb.images.len(), 1);
1829
1830 wb.delete_picture("Sheet1", "A1").unwrap();
1832
1833 assert_eq!(wb.images.len(), 1);
1835
1836 let pics = wb.get_pictures("Sheet2", "A1").unwrap();
1838 assert_eq!(pics.len(), 1);
1839 assert_eq!(pics[0].data, image_data);
1840 }
1841
1842 #[test]
1843 fn test_shared_media_save_preserves_image_data() {
1844 use crate::image::{ImageConfig, ImageFormat};
1845
1846 let dir = TempDir::new().unwrap();
1847 let path = dir.path().join("shared_media_save.xlsx");
1848
1849 let mut wb = Workbook::new();
1850 let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
1851 wb.add_image(
1852 "Sheet1",
1853 &ImageConfig {
1854 data: image_data.clone(),
1855 format: ImageFormat::Png,
1856 from_cell: "B2".to_string(),
1857 width_px: 200,
1858 height_px: 150,
1859 },
1860 )
1861 .unwrap();
1862
1863 let drawing_idx = *wb.worksheet_drawings.get(&0).unwrap();
1864 add_shared_media_anchor(&mut wb, drawing_idx, "../media/image1.png", 3, 3, 3);
1865
1866 wb.delete_picture("Sheet1", "B2").unwrap();
1868 assert_eq!(wb.images.len(), 1);
1869 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
1870
1871 wb.save(&path).unwrap();
1873 let file = std::fs::File::open(&path).unwrap();
1874 let mut archive = zip::ZipArchive::new(file).unwrap();
1875 assert!(
1876 archive.by_name("xl/media/image1.png").is_ok(),
1877 "shared media must survive in saved file"
1878 );
1879 }
1880}