Skip to content

Commit

Permalink
image: add initial support for background images
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcnamara committed Feb 4, 2025
1 parent 819a948 commit cc5a8e1
Show file tree
Hide file tree
Showing 20 changed files with 382 additions and 1 deletion.
18 changes: 18 additions & 0 deletions src/packager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ impl<W: Write + Seek + Send> Packager<W> {
rels.add_document_relationship(&relationship.0, &relationship.1, &relationship.2);
}

for relationship in &worksheet.background_relationships {
rels.add_document_relationship(&relationship.0, &relationship.1, &relationship.2);
}

for relationship in &worksheet.table_relationships {
rels.add_document_relationship(&relationship.0, &relationship.1, &relationship.2);
}
Expand Down Expand Up @@ -960,6 +964,7 @@ impl<W: Write + Seek + Send> Packager<W> {
let mut index = 1;
let mut unique_worksheet_images = HashSet::new();
let mut unique_header_footer_images = HashSet::new();
let mut unique_background_images = HashSet::new();

for image in &workbook.embedded_images {
let filename = format!("xl/media/image{index}.{}", image.image_type.extension());
Expand All @@ -971,6 +976,19 @@ impl<W: Write + Seek + Send> Packager<W> {
}

for worksheet in &mut workbook.worksheets {
if let Some(image) = &worksheet.background_image {
if !unique_background_images.contains(&image.hash) {
let filename =
format!("xl/media/image{index}.{}", image.image_type.extension());
self.zip
.start_file(filename, self.zip_options_for_binary_files)?;

self.zip.write_all(&image.data)?;
unique_background_images.insert(image.hash.clone());
index += 1;
}
}

for image in worksheet.images.values() {
if !unique_worksheet_images.contains(&image.hash) {
let filename =
Expand Down
19 changes: 18 additions & 1 deletion src/workbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2197,8 +2197,24 @@ impl Workbook {
// These are the image ids for each unique image file.
let mut worksheet_image_ids: HashMap<String, u32> = HashMap::new();
let mut header_footer_image_ids: HashMap<String, u32> = HashMap::new();
let mut background_image_ids: HashMap<String, u32> = HashMap::new();

for worksheet in &mut self.worksheets {
if let Some(image) = &worksheet.background_image {
let image = image.clone();

let background_image_id = match background_image_ids.get(&image.hash) {
Some(image_id) => *image_id,
None => {
image_id += 1;
background_image_ids.insert(image.hash.clone(), image_id);
image_id
}
};

worksheet.prepare_background_image(background_image_id, &image);
}

if !worksheet.images.is_empty() {
worksheet.prepare_worksheet_images(
&mut worksheet_image_ids,
Expand Down Expand Up @@ -2227,7 +2243,8 @@ impl Workbook {

if worksheet.has_header_footer_images() {
// The header/footer images are counted from the last worksheet id.
let base_image_id = worksheet_image_ids.len() as u32;
let base_image_id =
background_image_ids.len() as u32 + worksheet_image_ids.len() as u32;

worksheet.prepare_header_footer_images(&mut header_footer_image_ids, base_image_id);
}
Expand Down
35 changes: 35 additions & 0 deletions src/worksheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,7 @@ pub struct Worksheet {

pub(crate) has_workbook_global_xfs: bool,
pub(crate) has_workbook_global_sst: bool,
pub(crate) background_image: Option<Image>,

// These collections need to be reset on resave.
drawing_rel_ids: HashMap<String, u32>,
Expand All @@ -1543,6 +1544,7 @@ pub struct Worksheet {
pub(crate) hyperlink_relationships: Vec<(String, String, String)>,
pub(crate) table_relationships: Vec<(String, String, String)>,
pub(crate) vml_drawing_relationships: Vec<(String, String, String)>,
pub(crate) background_relationships: Vec<(String, String, String)>,

data_table: BTreeMap<RowNum, BTreeMap<ColNum, CellType>>,
is_writing_ahead: bool,
Expand Down Expand Up @@ -1848,6 +1850,7 @@ impl Worksheet {
max_outline_col_level: 0,
outline_symbols_above: false,
outline_symbols_left: false,
background_image: None,

// These collections need to be reset on resave.
comment_relationships: vec![],
Expand All @@ -1858,6 +1861,7 @@ impl Worksheet {
hyperlink_relationships: vec![],
table_relationships: vec![],
vml_drawing_relationships: vec![],
background_relationships: vec![],
is_chartsheet: false,
use_constant_memory: false,
use_inline_strings: false,
Expand Down Expand Up @@ -5421,6 +5425,12 @@ impl Worksheet {
Ok(self)
}

/// Todo
pub fn insert_background_image(&mut self, image: &Image) -> &mut Worksheet {
self.background_image = Some(image.clone());
self
}

/// Add a chart to a worksheet.
///
/// Add a [`Chart`] to a worksheet at a cell location.
Expand Down Expand Up @@ -16128,6 +16138,16 @@ impl Worksheet {
self.has_drawing_object_linkage = true;
}

// Set the relationship for the background image.
pub(crate) fn prepare_background_image(&mut self, image_id: u32, image: &Image) {
let image_name = format!("../media/image{image_id}.{}", image.image_type.extension());

self.image_types[image.image_type.clone() as usize] = true;

self.background_relationships
.push(("image".to_string(), image_name, String::new()));
}

// Convert the shape dimensions into drawing dimensions and add them to
// the Drawing object. Also set the rel linkages between the files.
pub(crate) fn prepare_worksheet_shapes(&mut self, shape_id: u32, drawing_id: u32) {
Expand Down Expand Up @@ -16679,13 +16699,15 @@ impl Worksheet {
self.hyperlink_relationships.clear();
self.table_relationships.clear();
self.vml_drawing_relationships.clear();
self.background_relationships.clear();
}

// Check if any external relationships are required.
pub(crate) fn has_relationships(&self) -> bool {
!self.hyperlink_relationships.is_empty()
|| !self.drawing_object_relationships.is_empty()
|| !self.table_relationships.is_empty()
|| !self.background_relationships.is_empty()
}

// Check if there is a header image.
Expand Down Expand Up @@ -17329,6 +17351,11 @@ impl Worksheet {
self.write_legacy_drawing_hf();
}

// Write the picture element.
if self.background_image.is_some() {
self.write_picture();
}

// Write the tableParts element.
if !self.tables.is_empty() {
self.write_table_parts();
Expand Down Expand Up @@ -19319,6 +19346,14 @@ impl Worksheet {
xml_empty_tag(&mut self.writer, "legacyDrawingHF", &attributes);
}

// Write the <picture> element.
fn write_picture(&mut self) {
self.rel_count += 1;
let attributes = [("r:id", format!("rId{}", self.rel_count))];

xml_empty_tag(&mut self.writer, "picture", &attributes);
}

// Write the <tableParts> element.
fn write_table_parts(&mut self) {
let num_tables = self.tables.len();
Expand Down
Binary file added tests/input/background01.xlsx
Binary file not shown.
Binary file added tests/input/background02.xlsx
Binary file not shown.
Binary file added tests/input/background03.xlsx
Binary file not shown.
Binary file added tests/input/background04.xlsx
Binary file not shown.
Binary file added tests/input/background05.xlsx
Binary file not shown.
Binary file added tests/input/background06.xlsx
Binary file not shown.
Binary file added tests/input/background07.xlsx
Binary file not shown.
Binary file added tests/input/table30.xlsx
Binary file not shown.
34 changes: 34 additions & 0 deletions tests/integration/background01.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Test case that compares a file generated by rust_xlsxwriter with a file
// created by Excel.
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2022-2025, John McNamara, [email protected]

use crate::common;
use rust_xlsxwriter::{Image, Workbook, XlsxError};

// Create rust_xlsxwriter file to compare against Excel file.
fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();

let image = Image::new("tests/input/images/logo.jpg")?.set_alt_text("logo.jpg");

worksheet.insert_image(8, 4, &image)?;

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_background01() {
let test_runner = common::TestRunner::new()
.set_name("background01")
.set_function(create_new_xlsx_file)
.initialize();

test_runner.assert_eq();
test_runner.cleanup();
}
33 changes: 33 additions & 0 deletions tests/integration/background02.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Test case that compares a file generated by rust_xlsxwriter with a file
// created by Excel.
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2022-2025, John McNamara, [email protected]

use crate::common;
use rust_xlsxwriter::{Image, Workbook, XlsxError};

// Create rust_xlsxwriter file to compare against Excel file.
fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();

let image = Image::new("tests/input/images/logo.jpg")?;
worksheet.insert_background_image(&image);

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_background02() {
let test_runner = common::TestRunner::new()
.set_name("background02")
.set_function(create_new_xlsx_file)
.initialize();

test_runner.assert_eq();
test_runner.cleanup();
}
35 changes: 35 additions & 0 deletions tests/integration/background03.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Test case that compares a file generated by rust_xlsxwriter with a file
// created by Excel.
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2022-2025, John McNamara, [email protected]

use crate::common;
use rust_xlsxwriter::{Image, Workbook, XlsxError};

// Create rust_xlsxwriter file to compare against Excel file.
fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();

let image = Image::new("tests/input/images/logo.jpg")?.set_alt_text("logo.jpg");

worksheet.insert_image(8, 4, &image)?;
worksheet.insert_background_image(&image);

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_background03() {
let test_runner = common::TestRunner::new()
.set_name("background03")
.set_function(create_new_xlsx_file)
.initialize();

test_runner.assert_eq();
test_runner.cleanup();
}
37 changes: 37 additions & 0 deletions tests/integration/background04.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Test case that compares a file generated by rust_xlsxwriter with a file
// created by Excel.
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2022-2025, John McNamara, [email protected]

use crate::common;
use rust_xlsxwriter::{Image, Workbook, XlsxError};

// Create rust_xlsxwriter file to compare against Excel file.
fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();

let image = Image::new("tests/input/images/logo.jpg")?;

let worksheet = workbook.add_worksheet();
worksheet.insert_background_image(&image);

let worksheet = workbook.add_worksheet();
worksheet.insert_background_image(&image);

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_background04() {
let test_runner = common::TestRunner::new()
.set_name("background04")
.set_function(create_new_xlsx_file)
.initialize();

test_runner.assert_eq();
test_runner.cleanup();
}
37 changes: 37 additions & 0 deletions tests/integration/background05.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Test case that compares a file generated by rust_xlsxwriter with a file
// created by Excel.
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2022-2025, John McNamara, [email protected]

use crate::common;
use rust_xlsxwriter::{Image, Workbook, XlsxError};

// Create rust_xlsxwriter file to compare against Excel file.
fn create_new_xlsx_file(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();

let worksheet = workbook.add_worksheet();
let image = Image::new("tests/input/images/logo.jpg")?;
worksheet.insert_background_image(&image);

let worksheet = workbook.add_worksheet();
let image = Image::new("tests/input/images/red.jpg")?;
worksheet.insert_background_image(&image);

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_background05() {
let test_runner = common::TestRunner::new()
.set_name("background05")
.set_function(create_new_xlsx_file)
.initialize();

test_runner.assert_eq();
test_runner.cleanup();
}
Loading

0 comments on commit cc5a8e1

Please sign in to comment.