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