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