Skip to main content

sheetkit_core/workbook/
drawing.rs

1use super::*;
2
3impl Workbook {
4    /// Add a chart to a sheet, anchored between two cells.
5    ///
6    /// The chart spans from `from_cell` (e.g., `"B2"`) to `to_cell`
7    /// (e.g., `"J15"`). The `config` specifies the chart type, series data,
8    /// title, and legend visibility.
9    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        // Parse cell references to marker coordinates (0-based).
24        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        // Allocate chart part.
41        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        // Get or create drawing for this sheet.
47        let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
48
49        // Add chart reference to the drawing's relationships.
50        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        // Build the chart anchor and add it to the drawing.
68        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        // Add content type for the chart.
73        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    /// Add a shape to a sheet, anchored between two cells.
82    ///
83    /// The shape spans from `config.from_cell` to `config.to_cell`. Unlike
84    /// charts and images, shapes do not reference external parts and therefore
85    /// do not need a relationship entry.
86    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    /// Add an image to a sheet from bytes.
106    ///
107    /// The image is anchored to the cell specified in `config.from_cell`.
108    /// Dimensions are specified in pixels via `config.width_px` and
109    /// `config.height_px`.
110    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        // Allocate image media part.
121        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        // Ensure the image extension has a default content type.
126        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        // Get or create drawing for this sheet.
140        let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
141
142        // Add image reference to the drawing's relationships.
143        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        // Count existing objects in the drawing to assign a unique ID.
161        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        // Add image anchor to the drawing.
165        crate::image::add_image_to_drawing(drawing, &image_rid, config, pic_id)?;
166
167        Ok(())
168    }
169
170    /// Delete a chart anchored at the given cell.
171    ///
172    /// Removes the drawing anchor, chart data, relationship entry, and content
173    /// type override for the chart at `cell` on `sheet`.
174    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    /// Delete a picture anchored at the given cell.
263    ///
264    /// Removes the drawing anchor, image data, relationship entry, and content
265    /// type for the picture at `cell` on `sheet`. Searches both one-cell and
266    /// two-cell anchors.
267    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        // Check one-cell anchors first
284        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        // Check two-cell anchors
304        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    /// Remove the relationship for a picture and clean up the image data only
330    /// when no other relationship across any drawing still references it.
331    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        // Remove the relationship entry for this specific picture.
335        if let Some(rels) = self.drawing_rels.get_mut(&drawing_idx) {
336            rels.relationships.retain(|r| r.id != image_rid);
337        }
338
339        // Only remove the actual image bytes when no remaining relationship
340        // across ALL drawings still targets the same media path.
341        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    /// Check whether any relationship in any drawing targets the given
349    /// resolved media path.
350    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    /// Resolve a relationship target to a full zip path.
385    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    /// Get all pictures anchored at the given cell.
412    ///
413    /// Returns picture data, format, anchor cell, and dimensions for each
414    /// picture found at the specified cell.
415    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    /// Extract picture info from a Picture element by resolving its relationship.
456    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    /// Find image data and determine format from the zip path extension.
481    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    /// Get all cells that have pictures anchored to them on the given sheet.
496    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        // Delete the chart from the in-memory workbook before save
1517        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        // Delete the picture from the in-memory workbook before save
1547        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    // Helper: add a second anchor that references an existing media path,
1644    // simulating shared media as found in real .xlsx files.
1645    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        // Add a second anchor sharing the same media (simulates opened .xlsx).
1742        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        // Delete the first picture (B2 = col 1, row 1 in 0-based).
1749        wb.delete_picture("Sheet1", "B2").unwrap();
1750
1751        // The media must survive because the second anchor still references it.
1752        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        // The surviving picture should still be retrievable.
1757        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        // Delete first picture.
1786        wb.delete_picture("Sheet1", "B2").unwrap();
1787        assert_eq!(wb.images.len(), 1);
1788
1789        // Delete second picture -- now the media should be removed.
1790        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        // Add an image to Sheet2 that shares the same media path.
1816        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        // Delete the picture from Sheet1.
1822        wb.delete_picture("Sheet1", "A1").unwrap();
1823
1824        // The media must survive because Sheet2 still references it.
1825        assert_eq!(wb.images.len(), 1);
1826
1827        // Sheet2's picture should still be retrievable.
1828        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        // Delete picture at B2 -- shared media must survive.
1858        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        // Save and verify the media file is in the zip archive.
1863        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}