use super::*;
impl Workbook {
pub fn add_chart(
&mut self,
sheet: &str,
from_cell: &str,
to_cell: &str,
config: &ChartConfig,
) -> Result<()> {
self.hydrate_drawings();
let sheet_idx =
crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
Error::SheetNotFound {
name: sheet.to_string(),
}
})?;
let (from_col, from_row) = cell_name_to_coordinates(from_cell)?;
let (to_col, to_row) = cell_name_to_coordinates(to_cell)?;
let from_marker = MarkerType {
col: from_col - 1,
col_off: 0,
row: from_row - 1,
row_off: 0,
};
let to_marker = MarkerType {
col: to_col - 1,
col_off: 0,
row: to_row - 1,
row_off: 0,
};
let chart_num = self.charts.len() + 1;
let chart_path = format!("xl/charts/chart{}.xml", chart_num);
let chart_space = crate::chart::build_chart_xml(config);
self.charts.push((chart_path, chart_space));
let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
let chart_rid = self.next_drawing_rid(drawing_idx);
let chart_rel_target = format!("../charts/chart{}.xml", chart_num);
let dr_rels = self
.drawing_rels
.entry(drawing_idx)
.or_insert_with(|| Relationships {
xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
relationships: vec![],
});
dr_rels.relationships.push(Relationship {
id: chart_rid.clone(),
rel_type: rel_types::CHART.to_string(),
target: chart_rel_target,
target_mode: None,
});
let drawing = &mut self.drawings[drawing_idx].1;
let anchor = crate::chart::build_drawing_with_chart(&chart_rid, from_marker, to_marker);
drawing.two_cell_anchors.extend(anchor.two_cell_anchors);
self.content_types.overrides.push(ContentTypeOverride {
part_name: format!("/xl/charts/chart{}.xml", chart_num),
content_type: mime_types::CHART.to_string(),
});
Ok(())
}
pub fn add_shape(&mut self, sheet: &str, config: &crate::shape::ShapeConfig) -> Result<()> {
self.hydrate_drawings();
let sheet_idx =
crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
Error::SheetNotFound {
name: sheet.to_string(),
}
})?;
let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
let drawing = &mut self.drawings[drawing_idx].1;
let shape_id = (drawing.one_cell_anchors.len() + drawing.two_cell_anchors.len() + 2) as u32;
let anchor = crate::shape::build_shape_anchor(config, shape_id)?;
drawing.two_cell_anchors.push(anchor);
Ok(())
}
pub fn add_image(&mut self, sheet: &str, config: &ImageConfig) -> Result<()> {
self.hydrate_drawings();
crate::image::validate_image_config(config)?;
let sheet_idx =
crate::sheet::find_sheet_index(&self.worksheets, sheet).ok_or_else(|| {
Error::SheetNotFound {
name: sheet.to_string(),
}
})?;
let image_num = self.images.len() + 1;
let image_path = format!("xl/media/image{}.{}", image_num, config.format.extension());
self.images.push((image_path, config.data.clone()));
let ext = config.format.extension().to_string();
if !self
.content_types
.defaults
.iter()
.any(|d| d.extension == ext)
{
self.content_types.defaults.push(ContentTypeDefault {
extension: ext,
content_type: config.format.content_type().to_string(),
});
}
let drawing_idx = self.ensure_drawing_for_sheet(sheet_idx);
let image_rid = self.next_drawing_rid(drawing_idx);
let image_rel_target = format!("../media/image{}.{}", image_num, config.format.extension());
let dr_rels = self
.drawing_rels
.entry(drawing_idx)
.or_insert_with(|| Relationships {
xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
relationships: vec![],
});
dr_rels.relationships.push(Relationship {
id: image_rid.clone(),
rel_type: rel_types::IMAGE.to_string(),
target: image_rel_target,
target_mode: None,
});
let drawing = &mut self.drawings[drawing_idx].1;
let pic_id = (drawing.one_cell_anchors.len() + drawing.two_cell_anchors.len() + 2) as u32;
crate::image::add_image_to_drawing(drawing, &image_rid, config, pic_id)?;
Ok(())
}
pub fn delete_chart(&mut self, sheet: &str, cell: &str) -> Result<()> {
self.hydrate_drawings();
let sheet_idx = self.sheet_index(sheet)?;
let (col, row) = cell_name_to_coordinates(cell)?;
let target_col = col - 1;
let target_row = row - 1;
let &drawing_idx =
self.worksheet_drawings
.get(&sheet_idx)
.ok_or_else(|| Error::ChartNotFound {
sheet: sheet.to_string(),
cell: cell.to_string(),
})?;
let drawing = &self.drawings[drawing_idx].1;
let anchor_pos = drawing
.two_cell_anchors
.iter()
.position(|a| {
a.from.col == target_col && a.from.row == target_row && a.graphic_frame.is_some()
})
.ok_or_else(|| Error::ChartNotFound {
sheet: sheet.to_string(),
cell: cell.to_string(),
})?;
let anchor = &drawing.two_cell_anchors[anchor_pos];
let chart_rid = anchor
.graphic_frame
.as_ref()
.unwrap()
.graphic
.graphic_data
.chart
.r_id
.clone();
let chart_path = self
.drawing_rels
.get(&drawing_idx)
.and_then(|rels| {
rels.relationships
.iter()
.find(|r| r.id == chart_rid)
.map(|r| {
let drawing_path = &self.drawings[drawing_idx].0;
let base_dir = drawing_path
.rfind('/')
.map(|i| &drawing_path[..i])
.unwrap_or("");
if r.target.starts_with("../") {
let rel_target = r.target.trim_start_matches("../");
let parent = base_dir.rfind('/').map(|i| &base_dir[..i]).unwrap_or("");
if parent.is_empty() {
rel_target.to_string()
} else {
format!("{}/{}", parent, rel_target)
}
} else {
format!("{}/{}", base_dir, r.target)
}
})
})
.ok_or_else(|| Error::ChartNotFound {
sheet: sheet.to_string(),
cell: cell.to_string(),
})?;
self.charts.retain(|(path, _)| path != &chart_path);
self.raw_charts.retain(|(path, _)| path != &chart_path);
if let Some(rels) = self.drawing_rels.get_mut(&drawing_idx) {
rels.relationships.retain(|r| r.id != chart_rid);
}
self.drawings[drawing_idx]
.1
.two_cell_anchors
.remove(anchor_pos);
let ct_part_name = format!("/{}", chart_path);
self.content_types
.overrides
.retain(|o| o.part_name != ct_part_name);
Ok(())
}
pub fn delete_picture(&mut self, sheet: &str, cell: &str) -> Result<()> {
self.hydrate_drawings();
let sheet_idx = self.sheet_index(sheet)?;
let (col, row) = cell_name_to_coordinates(cell)?;
let target_col = col - 1;
let target_row = row - 1;
let &drawing_idx =
self.worksheet_drawings
.get(&sheet_idx)
.ok_or_else(|| Error::PictureNotFound {
sheet: sheet.to_string(),
cell: cell.to_string(),
})?;
let drawing = &self.drawings[drawing_idx].1;
if let Some(pos) = drawing
.one_cell_anchors
.iter()
.position(|a| a.from.col == target_col && a.from.row == target_row && a.pic.is_some())
{
let image_rid = drawing.one_cell_anchors[pos]
.pic
.as_ref()
.unwrap()
.blip_fill
.blip
.r_embed
.clone();
self.remove_picture_data(drawing_idx, &image_rid);
self.drawings[drawing_idx].1.one_cell_anchors.remove(pos);
return Ok(());
}
if let Some(pos) = drawing
.two_cell_anchors
.iter()
.position(|a| a.from.col == target_col && a.from.row == target_row && a.pic.is_some())
{
let image_rid = drawing.two_cell_anchors[pos]
.pic
.as_ref()
.unwrap()
.blip_fill
.blip
.r_embed
.clone();
self.remove_picture_data(drawing_idx, &image_rid);
self.drawings[drawing_idx].1.two_cell_anchors.remove(pos);
return Ok(());
}
Err(Error::PictureNotFound {
sheet: sheet.to_string(),
cell: cell.to_string(),
})
}
fn remove_picture_data(&mut self, drawing_idx: usize, image_rid: &str) {
let image_path = self.resolve_drawing_rel_target(drawing_idx, image_rid);
if let Some(rels) = self.drawing_rels.get_mut(&drawing_idx) {
rels.relationships.retain(|r| r.id != image_rid);
}
if let Some(path) = image_path {
if !self.any_drawing_rel_targets_path(&path) {
self.images.retain(|(p, _)| p != &path);
}
}
}
fn any_drawing_rel_targets_path(&self, target_path: &str) -> bool {
for (&di, rels) in &self.drawing_rels {
let drawing_path = match self.drawings.get(di) {
Some((p, _)) => p,
None => continue,
};
let base_dir = drawing_path
.rfind('/')
.map(|i| &drawing_path[..i])
.unwrap_or("");
for rel in &rels.relationships {
if rel.rel_type != rel_types::IMAGE {
continue;
}
let resolved = if rel.target.starts_with("../") {
let rel_target = rel.target.trim_start_matches("../");
let parent = base_dir.rfind('/').map(|i| &base_dir[..i]).unwrap_or("");
if parent.is_empty() {
rel_target.to_string()
} else {
format!("{}/{}", parent, rel_target)
}
} else {
format!("{}/{}", base_dir, rel.target)
};
if resolved == target_path {
return true;
}
}
}
false
}
fn resolve_drawing_rel_target(&self, drawing_idx: usize, rid: &str) -> Option<String> {
self.drawing_rels.get(&drawing_idx).and_then(|rels| {
rels.relationships
.iter()
.find(|r| r.id == rid)
.and_then(|r| {
let drawing_path = &self.drawings.get(drawing_idx)?.0;
let base_dir = drawing_path
.rfind('/')
.map(|i| &drawing_path[..i])
.unwrap_or("");
Some(if r.target.starts_with("../") {
let rel_target = r.target.trim_start_matches("../");
let parent = base_dir.rfind('/').map(|i| &base_dir[..i]).unwrap_or("");
if parent.is_empty() {
rel_target.to_string()
} else {
format!("{}/{}", parent, rel_target)
}
} else {
format!("{}/{}", base_dir, r.target)
})
})
})
}
pub fn get_pictures(&self, sheet: &str, cell: &str) -> Result<Vec<crate::image::PictureInfo>> {
let sheet_idx = self.sheet_index(sheet)?;
let (col, row) = cell_name_to_coordinates(cell)?;
let target_col = col - 1;
let target_row = row - 1;
let drawing_idx = match self.worksheet_drawings.get(&sheet_idx) {
Some(&idx) => idx,
None => return Ok(vec![]),
};
let drawing = match self.drawings.get(drawing_idx) {
Some((_, d)) => d,
None => return Ok(vec![]),
};
let mut results = Vec::new();
for anchor in &drawing.one_cell_anchors {
if anchor.from.col == target_col && anchor.from.row == target_row {
if let Some(pic) = &anchor.pic {
if let Some(info) = self.extract_picture_info(drawing_idx, pic, cell) {
results.push(info);
}
}
}
}
for anchor in &drawing.two_cell_anchors {
if anchor.from.col == target_col && anchor.from.row == target_row {
if let Some(pic) = &anchor.pic {
if let Some(info) = self.extract_picture_info(drawing_idx, pic, cell) {
results.push(info);
}
}
}
}
Ok(results)
}
fn extract_picture_info(
&self,
drawing_idx: usize,
pic: &sheetkit_xml::drawing::Picture,
cell: &str,
) -> Option<crate::image::PictureInfo> {
let rid = &pic.blip_fill.blip.r_embed;
let image_path = self.resolve_drawing_rel_target(drawing_idx, rid)?;
let (data, format) = self.find_image_with_format(&image_path)?;
let cx = pic.sp_pr.xfrm.ext.cx;
let cy = pic.sp_pr.xfrm.ext.cy;
let width_px = (cx / crate::image::EMU_PER_PIXEL) as u32;
let height_px = (cy / crate::image::EMU_PER_PIXEL) as u32;
Some(crate::image::PictureInfo {
data: data.clone(),
format,
cell: cell.to_string(),
width_px,
height_px,
})
}
fn find_image_with_format(
&self,
image_path: &str,
) -> Option<(&Vec<u8>, crate::image::ImageFormat)> {
self.images
.iter()
.find(|(p, _)| p == image_path)
.and_then(|(path, data)| {
let ext = path.rsplit('.').next()?;
let format = crate::image::ImageFormat::from_extension(ext).ok()?;
Some((data, format))
})
}
pub fn get_picture_cells(&self, sheet: &str) -> Result<Vec<String>> {
let sheet_idx = self.sheet_index(sheet)?;
let drawing_idx = match self.worksheet_drawings.get(&sheet_idx) {
Some(&idx) => idx,
None => return Ok(vec![]),
};
let drawing = match self.drawings.get(drawing_idx) {
Some((_, d)) => d,
None => return Ok(vec![]),
};
let mut cells = Vec::new();
for anchor in &drawing.one_cell_anchors {
if anchor.pic.is_some() {
if let Ok(name) = crate::utils::cell_ref::coordinates_to_cell_name(
anchor.from.col + 1,
anchor.from.row + 1,
) {
cells.push(name);
}
}
}
for anchor in &drawing.two_cell_anchors {
if anchor.pic.is_some() {
if let Ok(name) = crate::utils::cell_ref::coordinates_to_cell_name(
anchor.from.col + 1,
anchor.from.row + 1,
) {
cells.push(name);
}
}
}
Ok(cells)
}
}
#[cfg(test)]
#[allow(clippy::unnecessary_cast)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_add_chart_basic() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Col,
title: Some("Test Chart".to_string()),
series: vec![ChartSeries {
name: "Sales".to_string(),
categories: "Sheet1!$A$1:$A$5".to_string(),
values: "Sheet1!$B$1:$B$5".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
};
wb.add_chart("Sheet1", "E1", "L15", &config).unwrap();
assert_eq!(wb.charts.len(), 1);
assert_eq!(wb.drawings.len(), 1);
assert!(wb.worksheet_drawings.contains_key(&0));
assert!(wb.drawing_rels.contains_key(&0));
assert!(wb.worksheets[0].1.get().unwrap().drawing.is_some());
}
#[test]
fn test_add_chart_sheet_not_found() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Line,
title: None,
series: vec![ChartSeries {
name: String::new(),
categories: "Sheet1!$A$1:$A$5".to_string(),
values: "Sheet1!$B$1:$B$5".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
};
let result = wb.add_chart("NoSheet", "A1", "H10", &config);
assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
}
#[test]
fn test_add_multiple_charts_same_sheet() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config1 = ChartConfig {
chart_type: ChartType::Col,
title: Some("Chart 1".to_string()),
series: vec![ChartSeries {
name: "S1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
};
let config2 = ChartConfig {
chart_type: ChartType::Line,
title: Some("Chart 2".to_string()),
series: vec![ChartSeries {
name: "S2".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$C$1:$C$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
};
wb.add_chart("Sheet1", "A1", "F10", &config1).unwrap();
wb.add_chart("Sheet1", "A12", "F22", &config2).unwrap();
assert_eq!(wb.charts.len(), 2);
assert_eq!(wb.drawings.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
}
#[test]
fn test_add_charts_different_sheets() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
wb.new_sheet("Sheet2").unwrap();
let config = ChartConfig {
chart_type: ChartType::Pie,
title: None,
series: vec![ChartSeries {
name: String::new(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
};
wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
wb.add_chart("Sheet2", "A1", "F10", &config).unwrap();
assert_eq!(wb.charts.len(), 2);
assert_eq!(wb.drawings.len(), 2);
}
#[test]
fn test_save_with_chart() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let dir = TempDir::new().unwrap();
let path = dir.path().join("with_chart.xlsx");
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Bar,
title: Some("Bar Chart".to_string()),
series: vec![ChartSeries {
name: "Data".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
};
wb.add_chart("Sheet1", "E2", "L15", &config).unwrap();
wb.save(&path).unwrap();
let file = std::fs::File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/charts/chart1.xml").is_ok());
assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
assert!(archive
.by_name("xl/worksheets/_rels/sheet1.xml.rels")
.is_ok());
assert!(archive
.by_name("xl/drawings/_rels/drawing1.xml.rels")
.is_ok());
}
#[test]
fn test_add_image_basic() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 400,
height_px: 300,
};
wb.add_image("Sheet1", &config).unwrap();
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
assert!(wb.worksheet_drawings.contains_key(&0));
}
#[test]
fn test_add_image_sheet_not_found() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x89],
format: ImageFormat::Png,
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
};
let result = wb.add_image("NoSheet", &config);
assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
}
#[test]
fn test_add_image_invalid_config() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![],
format: ImageFormat::Png,
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
};
assert!(wb.add_image("Sheet1", &config).is_err());
let config = ImageConfig {
data: vec![1],
format: ImageFormat::Jpeg,
from_cell: "A1".to_string(),
width_px: 0,
height_px: 100,
};
assert!(wb.add_image("Sheet1", &config).is_err());
}
#[test]
fn test_save_with_image() {
use crate::image::{ImageConfig, ImageFormat};
let dir = TempDir::new().unwrap();
let path = dir.path().join("with_image.xlsx");
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
format: ImageFormat::Png,
from_cell: "C3".to_string(),
width_px: 200,
height_px: 150,
};
wb.add_image("Sheet1", &config).unwrap();
wb.save(&path).unwrap();
let file = std::fs::File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/media/image1.png").is_ok());
assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
assert!(archive
.by_name("xl/worksheets/_rels/sheet1.xml.rels")
.is_ok());
assert!(archive
.by_name("xl/drawings/_rels/drawing1.xml.rels")
.is_ok());
}
#[test]
fn test_save_with_jpeg_image() {
use crate::image::{ImageConfig, ImageFormat};
let dir = TempDir::new().unwrap();
let path = dir.path().join("with_jpeg.xlsx");
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0xFF, 0xD8, 0xFF, 0xE0],
format: ImageFormat::Jpeg,
from_cell: "A1".to_string(),
width_px: 640,
height_px: 480,
};
wb.add_image("Sheet1", &config).unwrap();
wb.save(&path).unwrap();
let file = std::fs::File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/media/image1.jpeg").is_ok());
}
#[test]
fn test_save_with_new_image_formats() {
use crate::image::{ImageConfig, ImageFormat};
let formats = [
(ImageFormat::Bmp, "bmp"),
(ImageFormat::Ico, "ico"),
(ImageFormat::Tiff, "tiff"),
(ImageFormat::Svg, "svg"),
(ImageFormat::Emf, "emf"),
(ImageFormat::Emz, "emz"),
(ImageFormat::Wmf, "wmf"),
(ImageFormat::Wmz, "wmz"),
];
for (format, ext) in &formats {
let dir = TempDir::new().unwrap();
let path = dir.path().join(format!("with_{ext}.xlsx"));
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x00, 0x01, 0x02, 0x03],
format: format.clone(),
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
};
wb.add_image("Sheet1", &config).unwrap();
wb.save(&path).unwrap();
let file = std::fs::File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
let media_path = format!("xl/media/image1.{ext}");
assert!(
archive.by_name(&media_path).is_ok(),
"expected {media_path} in archive for format {ext}"
);
}
}
#[test]
fn test_add_image_new_format_content_type_default() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x42, 0x4D],
format: ImageFormat::Bmp,
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
};
wb.add_image("Sheet1", &config).unwrap();
let has_bmp_default = wb
.content_types
.defaults
.iter()
.any(|d| d.extension == "bmp" && d.content_type == "image/bmp");
assert!(has_bmp_default, "content types should have bmp default");
}
#[test]
fn test_add_image_svg_content_type_default() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x3C, 0x73, 0x76, 0x67],
format: ImageFormat::Svg,
from_cell: "B3".to_string(),
width_px: 200,
height_px: 200,
};
wb.add_image("Sheet1", &config).unwrap();
let has_svg_default = wb
.content_types
.defaults
.iter()
.any(|d| d.extension == "svg" && d.content_type == "image/svg+xml");
assert!(has_svg_default, "content types should have svg default");
}
#[test]
fn test_add_image_emf_content_type_and_path() {
use crate::image::{ImageConfig, ImageFormat};
let dir = TempDir::new().unwrap();
let path = dir.path().join("with_emf.xlsx");
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x01, 0x00, 0x00, 0x00],
format: ImageFormat::Emf,
from_cell: "A1".to_string(),
width_px: 150,
height_px: 150,
};
wb.add_image("Sheet1", &config).unwrap();
let has_emf_default = wb
.content_types
.defaults
.iter()
.any(|d| d.extension == "emf" && d.content_type == "image/x-emf");
assert!(has_emf_default);
wb.save(&path).unwrap();
let file = std::fs::File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/media/image1.emf").is_ok());
}
#[test]
fn test_add_multiple_new_format_images() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x42, 0x4D],
format: ImageFormat::Bmp,
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x3C, 0x73],
format: ImageFormat::Svg,
from_cell: "C1".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x01, 0x00],
format: ImageFormat::Wmf,
from_cell: "E1".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
assert_eq!(wb.images.len(), 3);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 3);
let ext_defaults: Vec<&str> = wb
.content_types
.defaults
.iter()
.map(|d| d.extension.as_str())
.collect();
assert!(ext_defaults.contains(&"bmp"));
assert!(ext_defaults.contains(&"svg"));
assert!(ext_defaults.contains(&"wmf"));
}
#[test]
fn test_add_shape_basic() {
use crate::shape::{ShapeConfig, ShapeType};
let mut wb = Workbook::new();
let config = ShapeConfig {
shape_type: ShapeType::Rect,
from_cell: "B2".to_string(),
to_cell: "F10".to_string(),
text: Some("Test Shape".to_string()),
fill_color: Some("FF0000".to_string()),
line_color: None,
line_width: None,
};
wb.add_shape("Sheet1", &config).unwrap();
assert_eq!(wb.drawings.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
let anchor = &wb.drawings[0].1.two_cell_anchors[0];
assert!(anchor.shape.is_some());
assert!(anchor.graphic_frame.is_none());
assert!(anchor.pic.is_none());
}
#[test]
fn test_add_multiple_shapes_same_sheet() {
use crate::shape::{ShapeConfig, ShapeType};
let mut wb = Workbook::new();
wb.add_shape(
"Sheet1",
&ShapeConfig {
shape_type: ShapeType::Rect,
from_cell: "A1".to_string(),
to_cell: "C3".to_string(),
text: None,
fill_color: None,
line_color: None,
line_width: None,
},
)
.unwrap();
wb.add_shape(
"Sheet1",
&ShapeConfig {
shape_type: ShapeType::Ellipse,
from_cell: "E1".to_string(),
to_cell: "H5".to_string(),
text: Some("Circle".to_string()),
fill_color: Some("00FF00".to_string()),
line_color: None,
line_width: None,
},
)
.unwrap();
assert_eq!(wb.drawings.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
}
#[test]
fn test_add_shape_and_chart_same_sheet() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
use crate::shape::{ShapeConfig, ShapeType};
let mut wb = Workbook::new();
wb.add_chart(
"Sheet1",
"E1",
"L10",
&ChartConfig {
chart_type: ChartType::Col,
title: Some("Chart".to_string()),
series: vec![ChartSeries {
name: "S1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
},
)
.unwrap();
wb.add_shape(
"Sheet1",
&ShapeConfig {
shape_type: ShapeType::Rect,
from_cell: "A12".to_string(),
to_cell: "D18".to_string(),
text: Some("Label".to_string()),
fill_color: None,
line_color: None,
line_width: None,
},
)
.unwrap();
assert_eq!(wb.drawings.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
}
#[test]
fn test_add_chart_and_image_same_sheet() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let chart_config = ChartConfig {
chart_type: ChartType::Col,
title: Some("My Chart".to_string()),
series: vec![ChartSeries {
name: "Series 1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
};
wb.add_chart("Sheet1", "E1", "L10", &chart_config).unwrap();
let image_config = ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "E12".to_string(),
width_px: 300,
height_px: 200,
};
wb.add_image("Sheet1", &image_config).unwrap();
assert_eq!(wb.drawings.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
assert_eq!(wb.charts.len(), 1);
assert_eq!(wb.images.len(), 1);
}
#[test]
fn test_save_with_chart_roundtrip_drawing_ref() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let dir = TempDir::new().unwrap();
let path = dir.path().join("chart_drawref.xlsx");
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Col,
title: None,
series: vec![ChartSeries {
name: "Series 1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
};
wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
wb.save(&path).unwrap();
let wb2 = Workbook::open(&path).unwrap();
let ws = wb2.worksheet_ref("Sheet1").unwrap();
assert!(ws.drawing.is_some());
}
#[test]
fn test_open_save_preserves_existing_drawing_chart_and_image_parts() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
use crate::image::{ImageConfig, ImageFormat};
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("source_with_parts.xlsx");
let path2 = dir.path().join("resaved_with_parts.xlsx");
let mut wb = Workbook::new();
wb.add_chart(
"Sheet1",
"E1",
"L10",
&ChartConfig {
chart_type: ChartType::Col,
title: Some("Chart".to_string()),
series: vec![ChartSeries {
name: "Series 1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "E12".to_string(),
width_px: 120,
height_px: 80,
},
)
.unwrap();
wb.save(&path1).unwrap();
let opts = crate::workbook::open_options::OpenOptions::new()
.read_mode(crate::workbook::open_options::ReadMode::Eager)
.aux_parts(crate::workbook::open_options::AuxParts::EagerLoad);
let wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
assert_eq!(wb2.charts.len() + wb2.raw_charts.len(), 1);
assert_eq!(wb2.drawings.len(), 1);
assert_eq!(wb2.images.len(), 1);
assert_eq!(wb2.drawing_rels.len(), 1);
assert_eq!(wb2.worksheet_drawings.len(), 1);
wb2.save(&path2).unwrap();
let file = std::fs::File::open(&path2).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/charts/chart1.xml").is_ok());
assert!(archive.by_name("xl/drawings/drawing1.xml").is_ok());
assert!(archive.by_name("xl/media/image1.png").is_ok());
assert!(archive
.by_name("xl/worksheets/_rels/sheet1.xml.rels")
.is_ok());
assert!(archive
.by_name("xl/drawings/_rels/drawing1.xml.rels")
.is_ok());
}
#[test]
fn test_delete_chart_basic() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Col,
title: Some("Chart".to_string()),
series: vec![ChartSeries {
name: "S1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
};
wb.add_chart("Sheet1", "E1", "L10", &config).unwrap();
assert_eq!(wb.charts.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
wb.delete_chart("Sheet1", "E1").unwrap();
assert_eq!(wb.charts.len(), 0);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 0);
}
#[test]
fn test_delete_chart_not_found() {
let mut wb = Workbook::new();
let result = wb.delete_chart("Sheet1", "A1");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::ChartNotFound { .. }));
}
#[test]
fn test_delete_chart_wrong_cell() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Col,
title: None,
series: vec![ChartSeries {
name: "S".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
};
wb.add_chart("Sheet1", "E1", "L10", &config).unwrap();
let result = wb.delete_chart("Sheet1", "A1");
assert!(result.is_err());
assert_eq!(wb.charts.len(), 1);
}
#[test]
fn test_delete_chart_removes_content_type() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Col,
title: None,
series: vec![ChartSeries {
name: "S".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
};
wb.add_chart("Sheet1", "E1", "L10", &config).unwrap();
let has_chart_ct = wb
.content_types
.overrides
.iter()
.any(|o| o.part_name.contains("chart"));
assert!(has_chart_ct);
wb.delete_chart("Sheet1", "E1").unwrap();
let has_chart_ct = wb
.content_types
.overrides
.iter()
.any(|o| o.part_name.contains("chart"));
assert!(!has_chart_ct);
}
#[test]
fn test_delete_one_chart_keeps_others() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let mut wb = Workbook::new();
let config = ChartConfig {
chart_type: ChartType::Col,
title: None,
series: vec![ChartSeries {
name: "S".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
};
wb.add_chart("Sheet1", "A1", "F10", &config).unwrap();
wb.add_chart("Sheet1", "A12", "F22", &config).unwrap();
assert_eq!(wb.charts.len(), 2);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 2);
wb.delete_chart("Sheet1", "A1").unwrap();
assert_eq!(wb.charts.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors[0].from.row, 11);
}
#[test]
fn test_delete_picture_basic() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 200,
height_px: 150,
};
wb.add_image("Sheet1", &config).unwrap();
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
wb.delete_picture("Sheet1", "B2").unwrap();
assert_eq!(wb.images.len(), 0);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 0);
}
#[test]
fn test_delete_picture_not_found() {
let mut wb = Workbook::new();
let result = wb.delete_picture("Sheet1", "A1");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::PictureNotFound { .. }));
}
#[test]
fn test_delete_picture_wrong_cell() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let config = ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "C3".to_string(),
width_px: 100,
height_px: 100,
};
wb.add_image("Sheet1", &config).unwrap();
let result = wb.delete_picture("Sheet1", "A1");
assert!(result.is_err());
assert_eq!(wb.images.len(), 1);
}
#[test]
fn test_delete_one_picture_keeps_others() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0xFF, 0xD8, 0xFF, 0xE0],
format: ImageFormat::Jpeg,
from_cell: "C3".to_string(),
width_px: 200,
height_px: 200,
},
)
.unwrap();
assert_eq!(wb.images.len(), 2);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 2);
wb.delete_picture("Sheet1", "A1").unwrap();
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors[0].from.col, 2);
}
#[test]
fn test_get_picture_cells_empty() {
let wb = Workbook::new();
let cells = wb.get_picture_cells("Sheet1").unwrap();
assert!(cells.is_empty());
}
#[test]
fn test_get_picture_cells_returns_cells() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50],
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0xFF, 0xD8],
format: ImageFormat::Jpeg,
from_cell: "D5".to_string(),
width_px: 200,
height_px: 150,
},
)
.unwrap();
let cells = wb.get_picture_cells("Sheet1").unwrap();
assert_eq!(cells.len(), 2);
assert!(cells.contains(&"B2".to_string()));
assert!(cells.contains(&"D5".to_string()));
}
#[test]
fn test_get_pictures_returns_data() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
wb.add_image(
"Sheet1",
&ImageConfig {
data: image_data.clone(),
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 400,
height_px: 300,
},
)
.unwrap();
let pics = wb.get_pictures("Sheet1", "B2").unwrap();
assert_eq!(pics.len(), 1);
assert_eq!(pics[0].data, image_data);
assert_eq!(pics[0].format, ImageFormat::Png);
assert_eq!(pics[0].cell, "B2");
assert_eq!(pics[0].width_px, 400);
assert_eq!(pics[0].height_px, 300);
}
#[test]
fn test_get_pictures_empty_cell() {
let wb = Workbook::new();
let pics = wb.get_pictures("Sheet1", "A1").unwrap();
assert!(pics.is_empty());
}
#[test]
fn test_get_pictures_wrong_cell() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50],
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
let pics = wb.get_pictures("Sheet1", "A1").unwrap();
assert!(pics.is_empty());
}
#[test]
fn test_delete_chart_roundtrip() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("chart_delete_rt1.xlsx");
let path2 = dir.path().join("chart_delete_rt2.xlsx");
let mut wb = Workbook::new();
wb.add_chart(
"Sheet1",
"E1",
"L10",
&ChartConfig {
chart_type: ChartType::Col,
title: Some("Chart".to_string()),
series: vec![ChartSeries {
name: "S1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: true,
view_3d: None,
},
)
.unwrap();
wb.save(&path1).unwrap();
wb.delete_chart("Sheet1", "E1").unwrap();
wb.save(&path2).unwrap();
let file = std::fs::File::open(&path2).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/charts/chart1.xml").is_err());
}
#[test]
fn test_delete_picture_roundtrip() {
use crate::image::{ImageConfig, ImageFormat};
let dir = TempDir::new().unwrap();
let path1 = dir.path().join("pic_delete_rt1.xlsx");
let path2 = dir.path().join("pic_delete_rt2.xlsx");
let mut wb = Workbook::new();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 200,
height_px: 150,
},
)
.unwrap();
wb.save(&path1).unwrap();
wb.delete_picture("Sheet1", "B2").unwrap();
wb.save(&path2).unwrap();
let file = std::fs::File::open(&path2).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(archive.by_name("xl/media/image1.png").is_err());
}
#[test]
fn test_delete_chart_preserves_image() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_chart(
"Sheet1",
"E1",
"L10",
&ChartConfig {
chart_type: ChartType::Col,
title: None,
series: vec![ChartSeries {
name: "S1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "E12".to_string(),
width_px: 200,
height_px: 150,
},
)
.unwrap();
wb.delete_chart("Sheet1", "E1").unwrap();
assert_eq!(wb.charts.len(), 0);
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 0);
}
#[test]
fn test_delete_picture_preserves_chart() {
use crate::chart::{ChartConfig, ChartSeries, ChartType};
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_chart(
"Sheet1",
"E1",
"L10",
&ChartConfig {
chart_type: ChartType::Col,
title: None,
series: vec![ChartSeries {
name: "S1".to_string(),
categories: "Sheet1!$A$1:$A$3".to_string(),
values: "Sheet1!$B$1:$B$3".to_string(),
x_values: None,
bubble_sizes: None,
}],
show_legend: false,
view_3d: None,
},
)
.unwrap();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "E12".to_string(),
width_px: 200,
height_px: 150,
},
)
.unwrap();
wb.delete_picture("Sheet1", "E12").unwrap();
assert_eq!(wb.images.len(), 0);
assert_eq!(wb.charts.len(), 1);
assert_eq!(wb.drawings[0].1.two_cell_anchors.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 0);
}
fn add_shared_media_anchor(
wb: &mut Workbook,
drawing_idx: usize,
media_rel_target: &str,
from_col: u32,
from_row: u32,
pic_id: u32,
) -> String {
use sheetkit_xml::drawing::*;
use sheetkit_xml::relationships::Relationship;
let rid = wb.next_drawing_rid(drawing_idx);
let rels = wb
.drawing_rels
.entry(drawing_idx)
.or_insert_with(|| Relationships {
xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
relationships: vec![],
});
rels.relationships.push(Relationship {
id: rid.clone(),
rel_type: rel_types::IMAGE.to_string(),
target: media_rel_target.to_string(),
target_mode: None,
});
let pic = Picture {
nv_pic_pr: NvPicPr {
c_nv_pr: CNvPr {
id: pic_id,
name: format!("Picture {}", pic_id - 1),
},
c_nv_pic_pr: CNvPicPr {},
},
blip_fill: BlipFill {
blip: Blip {
r_embed: rid.clone(),
},
stretch: Stretch {
fill_rect: FillRect {},
},
},
sp_pr: SpPr {
xfrm: Xfrm {
off: Offset { x: 0, y: 0 },
ext: AExt {
cx: 100 * crate::image::EMU_PER_PIXEL as u64,
cy: 100 * crate::image::EMU_PER_PIXEL as u64,
},
},
prst_geom: PrstGeom {
prst: "rect".to_string(),
},
},
};
wb.drawings[drawing_idx]
.1
.one_cell_anchors
.push(OneCellAnchor {
from: MarkerType {
col: from_col,
col_off: 0,
row: from_row,
row_off: 0,
},
ext: Extent {
cx: 100 * crate::image::EMU_PER_PIXEL as u64,
cy: 100 * crate::image::EMU_PER_PIXEL as u64,
},
pic: Some(pic),
client_data: ClientData {},
});
rid
}
#[test]
fn test_delete_shared_media_keeps_other_picture() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A];
wb.add_image(
"Sheet1",
&ImageConfig {
data: image_data.clone(),
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 200,
height_px: 150,
},
)
.unwrap();
let drawing_idx = *wb.worksheet_drawings.get(&0).unwrap();
add_shared_media_anchor(&mut wb, drawing_idx, "../media/image1.png", 3, 3, 3);
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 2);
wb.delete_picture("Sheet1", "B2").unwrap();
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.images[0].0, "xl/media/image1.png");
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
let pics = wb.get_pictures("Sheet1", "D4").unwrap();
assert_eq!(pics.len(), 1);
assert_eq!(pics[0].data, image_data);
}
#[test]
fn test_delete_both_shared_media_pictures_cleans_up() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.add_image(
"Sheet1",
&ImageConfig {
data: vec![0x89, 0x50, 0x4E, 0x47],
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
let drawing_idx = *wb.worksheet_drawings.get(&0).unwrap();
add_shared_media_anchor(&mut wb, drawing_idx, "../media/image1.png", 3, 3, 3);
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 2);
wb.delete_picture("Sheet1", "B2").unwrap();
assert_eq!(wb.images.len(), 1);
wb.delete_picture("Sheet1", "D4").unwrap();
assert_eq!(wb.images.len(), 0);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 0);
}
#[test]
fn test_cross_sheet_shared_media_survives_single_delete() {
use crate::image::{ImageConfig, ImageFormat};
let mut wb = Workbook::new();
wb.new_sheet("Sheet2").unwrap();
let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D];
wb.add_image(
"Sheet1",
&ImageConfig {
data: image_data.clone(),
format: ImageFormat::Png,
from_cell: "A1".to_string(),
width_px: 100,
height_px: 100,
},
)
.unwrap();
let drawing_idx_s2 = wb.ensure_drawing_for_sheet(1);
add_shared_media_anchor(&mut wb, drawing_idx_s2, "../media/image1.png", 0, 0, 2);
assert_eq!(wb.images.len(), 1);
wb.delete_picture("Sheet1", "A1").unwrap();
assert_eq!(wb.images.len(), 1);
let pics = wb.get_pictures("Sheet2", "A1").unwrap();
assert_eq!(pics.len(), 1);
assert_eq!(pics[0].data, image_data);
}
#[test]
fn test_shared_media_save_preserves_image_data() {
use crate::image::{ImageConfig, ImageFormat};
let dir = TempDir::new().unwrap();
let path = dir.path().join("shared_media_save.xlsx");
let mut wb = Workbook::new();
let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
wb.add_image(
"Sheet1",
&ImageConfig {
data: image_data.clone(),
format: ImageFormat::Png,
from_cell: "B2".to_string(),
width_px: 200,
height_px: 150,
},
)
.unwrap();
let drawing_idx = *wb.worksheet_drawings.get(&0).unwrap();
add_shared_media_anchor(&mut wb, drawing_idx, "../media/image1.png", 3, 3, 3);
wb.delete_picture("Sheet1", "B2").unwrap();
assert_eq!(wb.images.len(), 1);
assert_eq!(wb.drawings[0].1.one_cell_anchors.len(), 1);
wb.save(&path).unwrap();
let file = std::fs::File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert!(
archive.by_name("xl/media/image1.png").is_ok(),
"shared media must survive in saved file"
);
}
}