1use super::*;
2
3impl Workbook {
4 pub fn add_chart(
10 &mut self,
11 sheet: &str,
12 from_cell: &str,
13 to_cell: &str,
14 config: &ChartConfig,
15 ) -> Result<()> {
16 let sheet_idx =
17 crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
18 Error::SheetNotFound {
19 name: sheet.to_string(),
20 }
21 })?;
22
23 let (from_col, from_row) = cell_name_to_coordinates(from_cell)?;
25 let (to_col, to_row) = cell_name_to_coordinates(to_cell)?;
26
27 let from_marker = MarkerType {
28 col: from_col - 1,
29 col_off: 0,
30 row: from_row - 1,
31 row_off: 0,
32 };
33 let to_marker = MarkerType {
34 col: to_col - 1,
35 col_off: 0,
36 row: to_row - 1,
37 row_off: 0,
38 };
39
40 let chart_num = self.charts.len() + 1;
42 let chart_path = format!("xl/charts/chart{}.xml", chart_num);
43 let chart_space = crate::chart::build_chart_xml(config);
44 self.charts.push((chart_path, chart_space));
45
46 let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
48
49 let chart_rid = self.next_drawing_rid(drawing_idx);
51 let chart_rel_target = format!("../charts/chart{}.xml", chart_num);
52
53 let dr_rels = self
54 .drawing_rels
55 .entry(drawing_idx)
56 .or_insert_with(|| Relationships {
57 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
58 relationships: vec![],
59 });
60 dr_rels.relationships.push(Relationship {
61 id: chart_rid.clone(),
62 rel_type: rel_types::CHART.to_string(),
63 target: chart_rel_target,
64 target_mode: None,
65 });
66
67 let drawing = &mut self.drawings[drawing_idx].1;
69 let anchor = crate::chart::build_drawing_with_chart(&chart_rid, from_marker, to_marker);
70 drawing.two_cell_anchors.extend(anchor.two_cell_anchors);
71
72 self.content_types.overrides.push(ContentTypeOverride {
74 part_name: format!("/xl/charts/chart{}.xml", chart_num),
75 content_type: mime_types::CHART.to_string(),
76 });
77
78 Ok(())
79 }
80
81 pub fn add_image(&mut self, sheet: &str, config: &ImageConfig) -> Result<()> {
87 crate::image::validate_image_config(config)?;
88
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 image_num = self.images.len() + 1;
98 let image_path = format!("xl/media/image{}.{}", image_num, config.format.extension());
99 self.images.push((image_path, config.data.clone()));
100
101 let ext = config.format.extension().to_string();
103 if !self
104 .content_types
105 .defaults
106 .iter()
107 .any(|d| d.extension == ext)
108 {
109 self.content_types.defaults.push(ContentTypeDefault {
110 extension: ext,
111 content_type: config.format.content_type().to_string(),
112 });
113 }
114
115 let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
117
118 let image_rid = self.next_drawing_rid(drawing_idx);
120 let image_rel_target = format!("../media/image{}.{}", image_num, config.format.extension());
121
122 let dr_rels = self
123 .drawing_rels
124 .entry(drawing_idx)
125 .or_insert_with(|| Relationships {
126 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
127 relationships: vec![],
128 });
129 dr_rels.relationships.push(Relationship {
130 id: image_rid.clone(),
131 rel_type: rel_types::IMAGE.to_string(),
132 target: image_rel_target,
133 target_mode: None,
134 });
135
136 let drawing = &mut self.drawings[drawing_idx].1;
138 let pic_id = (drawing.one_cell_anchors.len() + drawing.two_cell_anchors.len() + 2) as u32;
139
140 crate::image::add_image_to_drawing(drawing, &image_rid, config, pic_id)?;
142
143 Ok(())
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use tempfile::TempDir;
151
152 #[test]
153 fn test_add_chart_basic() {
154 use crate::chart::{ChartConfig, ChartSeries, ChartType};
155 let mut wb = Workbook::new();
156 let config = ChartConfig {
157 chart_type: ChartType::Col,
158 title: Some("Test Chart".to_string()),
159 series: vec![ChartSeries {
160 name: "Sales".to_string(),
161 categories: "Sheet1!$A$1:$A$5".to_string(),
162 values: "Sheet1!$B$1:$B$5".to_string(),
163 x_values: None,
164 bubble_sizes: None,
165 }],
166 show_legend: true,
167 view_3d: None,
168 };
169 wb.add_chart("Sheet1", "E1", "L15", &config).unwrap();
170
171 assert_eq!(wb.charts.len(), 1);
172 assert_eq!(wb.drawings.len(), 1);
173 assert!(wb.worksheet_drawings.contains_key(&0));
174 assert!(wb.drawing_rels.contains_key(&0));
175 assert!(wb.worksheets[0].1.drawing.is_some());
176 }
177
178 #[test]
179 fn test_add_chart_sheet_not_found() {
180 use crate::chart::{ChartConfig, ChartSeries, ChartType};
181 let mut wb = Workbook::new();
182 let config = ChartConfig {
183 chart_type: ChartType::Line,
184 title: None,
185 series: vec![ChartSeries {
186 name: String::new(),
187 categories: "Sheet1!$A$1:$A$5".to_string(),
188 values: "Sheet1!$B$1:$B$5".to_string(),
189 x_values: None,
190 bubble_sizes: None,
191 }],
192 show_legend: false,
193 view_3d: None,
194 };
195 let result = wb.add_chart("NoSheet", "A1", "H10", &config);
196 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
197 }
198
199 #[test]
200 fn test_add_multiple_charts_same_sheet() {
201 use crate::chart::{ChartConfig, ChartSeries, ChartType};
202 let mut wb = Workbook::new();
203 let config1 = ChartConfig {
204 chart_type: ChartType::Col,
205 title: Some("Chart 1".to_string()),
206 series: vec![ChartSeries {
207 name: "S1".to_string(),
208 categories: "Sheet1!$A$1:$A$3".to_string(),
209 values: "Sheet1!$B$1:$B$3".to_string(),
210 x_values: None,
211 bubble_sizes: None,
212 }],
213 show_legend: true,
214 view_3d: None,
215 };
216 let config2 = ChartConfig {
217 chart_type: ChartType::Line,
218 title: Some("Chart 2".to_string()),
219 series: vec![ChartSeries {
220 name: "S2".to_string(),
221 categories: "Sheet1!$A$1:$A$3".to_string(),
222 values: "Sheet1!$C$1:$C$3".to_string(),
223 x_values: None,
224 bubble_sizes: None,
225 }],
226 show_legend: false,
227 view_3d: None,
228 };
229 wb.add_chart("Sheet1", "A1", "F10", &config1).unwrap();
230 wb.add_chart("Sheet1", "A12", "F22", &config2).unwrap();
231
232 assert_eq!(wb.charts.len(), 2);
233 assert_eq!(wb.drawings.len(), 1);
234 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
235 }
236
237 #[test]
238 fn test_add_charts_different_sheets() {
239 use crate::chart::{ChartConfig, ChartSeries, ChartType};
240 let mut wb = Workbook::new();
241 wb.new_sheet("Sheet2").unwrap();
242
243 let config = ChartConfig {
244 chart_type: ChartType::Pie,
245 title: None,
246 series: vec![ChartSeries {
247 name: String::new(),
248 categories: "Sheet1!$A$1:$A$3".to_string(),
249 values: "Sheet1!$B$1:$B$3".to_string(),
250 x_values: None,
251 bubble_sizes: None,
252 }],
253 show_legend: true,
254 view_3d: None,
255 };
256 wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
257 wb.add_chart("Sheet2", "A1", "F10", &config).unwrap();
258
259 assert_eq!(wb.charts.len(), 2);
260 assert_eq!(wb.drawings.len(), 2);
261 }
262
263 #[test]
264 fn test_save_with_chart() {
265 use crate::chart::{ChartConfig, ChartSeries, ChartType};
266 let dir = TempDir::new().unwrap();
267 let path = dir.path().join("with_chart.xlsx");
268
269 let mut wb = Workbook::new();
270 let config = ChartConfig {
271 chart_type: ChartType::Bar,
272 title: Some("Bar Chart".to_string()),
273 series: vec![ChartSeries {
274 name: "Data".to_string(),
275 categories: "Sheet1!$A$1:$A$3".to_string(),
276 values: "Sheet1!$B$1:$B$3".to_string(),
277 x_values: None,
278 bubble_sizes: None,
279 }],
280 show_legend: true,
281 view_3d: None,
282 };
283 wb.add_chart("Sheet1", "E2", "L15", &config).unwrap();
284 wb.save(&path).unwrap();
285
286 let file = std::fs::File::open(&path).unwrap();
287 let mut archive = zip::ZipArchive::new(file).unwrap();
288
289 assert!(archive.by_name("xl/charts/chart1.xml").is_ok());
290 assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
291 assert!(archive
292 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
293 .is_ok());
294 assert!(archive
295 .by_name("xl/drawings/_rels/drawing1.xml.rels")
296 .is_ok());
297 }
298
299 #[test]
300 fn test_add_image_basic() {
301 use crate::image::{ImageConfig, ImageFormat};
302 let mut wb = Workbook::new();
303 let config = ImageConfig {
304 data: vec![0x89, 0x50, 0x4E, 0x47],
305 format: ImageFormat::Png,
306 from_cell: "B2".to_string(),
307 width_px: 400,
308 height_px: 300,
309 };
310 wb.add_image("Sheet1", &config).unwrap();
311
312 assert_eq!(wb.images.len(), 1);
313 assert_eq!(wb.drawings.len(), 1);
314 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
315 assert!(wb.worksheet_drawings.contains_key(&0));
316 }
317
318 #[test]
319 fn test_add_image_sheet_not_found() {
320 use crate::image::{ImageConfig, ImageFormat};
321 let mut wb = Workbook::new();
322 let config = ImageConfig {
323 data: vec![0x89],
324 format: ImageFormat::Png,
325 from_cell: "A1".to_string(),
326 width_px: 100,
327 height_px: 100,
328 };
329 let result = wb.add_image("NoSheet", &config);
330 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
331 }
332
333 #[test]
334 fn test_add_image_invalid_config() {
335 use crate::image::{ImageConfig, ImageFormat};
336 let mut wb = Workbook::new();
337 let config = ImageConfig {
338 data: vec![],
339 format: ImageFormat::Png,
340 from_cell: "A1".to_string(),
341 width_px: 100,
342 height_px: 100,
343 };
344 assert!(wb.add_image("Sheet1", &config).is_err());
345
346 let config = ImageConfig {
347 data: vec![1],
348 format: ImageFormat::Jpeg,
349 from_cell: "A1".to_string(),
350 width_px: 0,
351 height_px: 100,
352 };
353 assert!(wb.add_image("Sheet1", &config).is_err());
354 }
355
356 #[test]
357 fn test_save_with_image() {
358 use crate::image::{ImageConfig, ImageFormat};
359 let dir = TempDir::new().unwrap();
360 let path = dir.path().join("with_image.xlsx");
361
362 let mut wb = Workbook::new();
363 let config = ImageConfig {
364 data: vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
365 format: ImageFormat::Png,
366 from_cell: "C3".to_string(),
367 width_px: 200,
368 height_px: 150,
369 };
370 wb.add_image("Sheet1", &config).unwrap();
371 wb.save(&path).unwrap();
372
373 let file = std::fs::File::open(&path).unwrap();
374 let mut archive = zip::ZipArchive::new(file).unwrap();
375
376 assert!(archive.by_name("xl/media/image1.png").is_ok());
377 assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
378 assert!(archive
379 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
380 .is_ok());
381 assert!(archive
382 .by_name("xl/drawings/_rels/drawing1.xml.rels")
383 .is_ok());
384 }
385
386 #[test]
387 fn test_save_with_jpeg_image() {
388 use crate::image::{ImageConfig, ImageFormat};
389 let dir = TempDir::new().unwrap();
390 let path = dir.path().join("with_jpeg.xlsx");
391
392 let mut wb = Workbook::new();
393 let config = ImageConfig {
394 data: vec![0xFF, 0xD8, 0xFF, 0xE0],
395 format: ImageFormat::Jpeg,
396 from_cell: "A1".to_string(),
397 width_px: 640,
398 height_px: 480,
399 };
400 wb.add_image("Sheet1", &config).unwrap();
401 wb.save(&path).unwrap();
402
403 let file = std::fs::File::open(&path).unwrap();
404 let mut archive = zip::ZipArchive::new(file).unwrap();
405 assert!(archive.by_name("xl/media/image1.jpeg").is_ok());
406 }
407
408 #[test]
409 fn test_save_with_new_image_formats() {
410 use crate::image::{ImageConfig, ImageFormat};
411
412 let formats = [
413 (ImageFormat::Bmp, "bmp"),
414 (ImageFormat::Ico, "ico"),
415 (ImageFormat::Tiff, "tiff"),
416 (ImageFormat::Svg, "svg"),
417 (ImageFormat::Emf, "emf"),
418 (ImageFormat::Emz, "emz"),
419 (ImageFormat::Wmf, "wmf"),
420 (ImageFormat::Wmz, "wmz"),
421 ];
422
423 for (format, ext) in &formats {
424 let dir = TempDir::new().unwrap();
425 let path = dir.path().join(format!("with_{ext}.xlsx"));
426
427 let mut wb = Workbook::new();
428 let config = ImageConfig {
429 data: vec![0x00, 0x01, 0x02, 0x03],
430 format: format.clone(),
431 from_cell: "A1".to_string(),
432 width_px: 100,
433 height_px: 100,
434 };
435 wb.add_image("Sheet1", &config).unwrap();
436 wb.save(&path).unwrap();
437
438 let file = std::fs::File::open(&path).unwrap();
439 let mut archive = zip::ZipArchive::new(file).unwrap();
440 let media_path = format!("xl/media/image1.{ext}");
441 assert!(
442 archive.by_name(&media_path).is_ok(),
443 "expected {media_path} in archive for format {ext}"
444 );
445 }
446 }
447
448 #[test]
449 fn test_add_image_new_format_content_type_default() {
450 use crate::image::{ImageConfig, ImageFormat};
451 let mut wb = Workbook::new();
452 let config = ImageConfig {
453 data: vec![0x42, 0x4D],
454 format: ImageFormat::Bmp,
455 from_cell: "A1".to_string(),
456 width_px: 100,
457 height_px: 100,
458 };
459 wb.add_image("Sheet1", &config).unwrap();
460
461 let has_bmp_default = wb
462 .content_types
463 .defaults
464 .iter()
465 .any(|d| d.extension == "bmp" && d.content_type == "image/bmp");
466 assert!(has_bmp_default, "content types should have bmp default");
467 }
468
469 #[test]
470 fn test_add_image_svg_content_type_default() {
471 use crate::image::{ImageConfig, ImageFormat};
472 let mut wb = Workbook::new();
473 let config = ImageConfig {
474 data: vec![0x3C, 0x73, 0x76, 0x67],
475 format: ImageFormat::Svg,
476 from_cell: "B3".to_string(),
477 width_px: 200,
478 height_px: 200,
479 };
480 wb.add_image("Sheet1", &config).unwrap();
481
482 let has_svg_default = wb
483 .content_types
484 .defaults
485 .iter()
486 .any(|d| d.extension == "svg" && d.content_type == "image/svg+xml");
487 assert!(has_svg_default, "content types should have svg default");
488 }
489
490 #[test]
491 fn test_add_image_emf_content_type_and_path() {
492 use crate::image::{ImageConfig, ImageFormat};
493 let dir = TempDir::new().unwrap();
494 let path = dir.path().join("with_emf.xlsx");
495
496 let mut wb = Workbook::new();
497 let config = ImageConfig {
498 data: vec![0x01, 0x00, 0x00, 0x00],
499 format: ImageFormat::Emf,
500 from_cell: "A1".to_string(),
501 width_px: 150,
502 height_px: 150,
503 };
504 wb.add_image("Sheet1", &config).unwrap();
505
506 let has_emf_default = wb
507 .content_types
508 .defaults
509 .iter()
510 .any(|d| d.extension == "emf" && d.content_type == "image/x-emf");
511 assert!(has_emf_default);
512
513 wb.save(&path).unwrap();
514 let file = std::fs::File::open(&path).unwrap();
515 let mut archive = zip::ZipArchive::new(file).unwrap();
516 assert!(archive.by_name("xl/media/image1.emf").is_ok());
517 }
518
519 #[test]
520 fn test_add_multiple_new_format_images() {
521 use crate::image::{ImageConfig, ImageFormat};
522 let mut wb = Workbook::new();
523
524 wb.add_image(
525 "Sheet1",
526 &ImageConfig {
527 data: vec![0x42, 0x4D],
528 format: ImageFormat::Bmp,
529 from_cell: "A1".to_string(),
530 width_px: 100,
531 height_px: 100,
532 },
533 )
534 .unwrap();
535
536 wb.add_image(
537 "Sheet1",
538 &ImageConfig {
539 data: vec![0x3C, 0x73],
540 format: ImageFormat::Svg,
541 from_cell: "C1".to_string(),
542 width_px: 100,
543 height_px: 100,
544 },
545 )
546 .unwrap();
547
548 wb.add_image(
549 "Sheet1",
550 &ImageConfig {
551 data: vec![0x01, 0x00],
552 format: ImageFormat::Wmf,
553 from_cell: "E1".to_string(),
554 width_px: 100,
555 height_px: 100,
556 },
557 )
558 .unwrap();
559
560 assert_eq!(wb.images.len(), 3);
561 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 3);
562
563 let ext_defaults: Vec<&str> = wb
564 .content_types
565 .defaults
566 .iter()
567 .map(|d| d.extension.as_str())
568 .collect();
569 assert!(ext_defaults.contains(&"bmp"));
570 assert!(ext_defaults.contains(&"svg"));
571 assert!(ext_defaults.contains(&"wmf"));
572 }
573
574 #[test]
575 fn test_add_chart_and_image_same_sheet() {
576 use crate::chart::{ChartConfig, ChartSeries, ChartType};
577 use crate::image::{ImageConfig, ImageFormat};
578
579 let mut wb = Workbook::new();
580
581 let chart_config = ChartConfig {
582 chart_type: ChartType::Col,
583 title: Some("My Chart".to_string()),
584 series: vec![ChartSeries {
585 name: "Series 1".to_string(),
586 categories: "Sheet1!$A$1:$A$3".to_string(),
587 values: "Sheet1!$B$1:$B$3".to_string(),
588 x_values: None,
589 bubble_sizes: None,
590 }],
591 show_legend: true,
592 view_3d: None,
593 };
594 wb.add_chart("Sheet1", "E1", "L10", &chart_config).unwrap();
595
596 let image_config = ImageConfig {
597 data: vec![0x89, 0x50, 0x4E, 0x47],
598 format: ImageFormat::Png,
599 from_cell: "E12".to_string(),
600 width_px: 300,
601 height_px: 200,
602 };
603 wb.add_image("Sheet1", &image_config).unwrap();
604
605 assert_eq!(wb.drawings.len(), 1);
606 assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
607 assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
608 assert_eq!(wb.charts.len(), 1);
609 assert_eq!(wb.images.len(), 1);
610 }
611
612 #[test]
613 fn test_save_with_chart_roundtrip_drawing_ref() {
614 use crate::chart::{ChartConfig, ChartSeries, ChartType};
615 let dir = TempDir::new().unwrap();
616 let path = dir.path().join("chart_drawref.xlsx");
617
618 let mut wb = Workbook::new();
619 let config = ChartConfig {
620 chart_type: ChartType::Col,
621 title: None,
622 series: vec![ChartSeries {
623 name: "Series 1".to_string(),
624 categories: "Sheet1!$A$1:$A$3".to_string(),
625 values: "Sheet1!$B$1:$B$3".to_string(),
626 x_values: None,
627 bubble_sizes: None,
628 }],
629 show_legend: false,
630 view_3d: None,
631 };
632 wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
633 wb.save(&path).unwrap();
634
635 let wb2 = Workbook::open(&path).unwrap();
636 let ws = wb2.worksheet_ref("Sheet1").unwrap();
637 assert!(ws.drawing.is_some());
638 }
639
640 #[test]
641 fn test_open_save_preserves_existing_drawing_chart_and_image_parts() {
642 use crate::chart::{ChartConfig, ChartSeries, ChartType};
643 use crate::image::{ImageConfig, ImageFormat};
644 let dir = TempDir::new().unwrap();
645 let path1 = dir.path().join("source_with_parts.xlsx");
646 let path2 = dir.path().join("resaved_with_parts.xlsx");
647
648 let mut wb = Workbook::new();
649 wb.add_chart(
650 "Sheet1",
651 "E1",
652 "L10",
653 &ChartConfig {
654 chart_type: ChartType::Col,
655 title: Some("Chart".to_string()),
656 series: vec![ChartSeries {
657 name: "Series 1".to_string(),
658 categories: "Sheet1!$A$1:$A$3".to_string(),
659 values: "Sheet1!$B$1:$B$3".to_string(),
660 x_values: None,
661 bubble_sizes: None,
662 }],
663 show_legend: true,
664 view_3d: None,
665 },
666 )
667 .unwrap();
668 wb.add_image(
669 "Sheet1",
670 &ImageConfig {
671 data: vec![0x89, 0x50, 0x4E, 0x47],
672 format: ImageFormat::Png,
673 from_cell: "E12".to_string(),
674 width_px: 120,
675 height_px: 80,
676 },
677 )
678 .unwrap();
679 wb.save(&path1).unwrap();
680
681 let wb2 = Workbook::open(&path1).unwrap();
682 assert_eq!(wb2.charts.len() + wb2.raw_charts.len(), 1);
683 assert_eq!(wb2.drawings.len(), 1);
684 assert_eq!(wb2.images.len(), 1);
685 assert_eq!(wb2.drawing_rels.len(), 1);
686 assert_eq!(wb2.worksheet_drawings.len(), 1);
687
688 wb2.save(&path2).unwrap();
689
690 let file = std::fs::File::open(&path2).unwrap();
691 let mut archive = zip::ZipArchive::new(file).unwrap();
692 assert!(archive.by_name("xl/charts/chart1.xml").is_ok());
693 assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
694 assert!(archive.by_name("xl/media/image1.png").is_ok());
695 assert!(archive
696 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
697 .is_ok());
698 assert!(archive
699 .by_name("xl/drawings/_rels/drawing1.xml.rels")
700 .is_ok());
701 }
702}