Skip to content

Commit

Permalink
checkbox: add table format support
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcnamara committed Jan 28, 2025
1 parent b566618 commit 1b2dc61
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 28 deletions.
38 changes: 36 additions & 2 deletions src/feature_property_bag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
//
// Copyright 2022-2025, John McNamara, [email protected]

use std::io::Cursor;
use std::{collections::HashSet, io::Cursor};

use crate::xmlwriter::{
xml_data_element, xml_declaration, xml_empty_tag, xml_end_tag, xml_start_tag,
};

pub struct FeaturePropertyBag {
pub(crate) writer: Cursor<Vec<u8>>,
pub(crate) feature_property_bags: HashSet<FeaturePropertyBagTypes>,
}

impl FeaturePropertyBag {
Expand All @@ -24,7 +25,10 @@ impl FeaturePropertyBag {
pub(crate) fn new() -> FeaturePropertyBag {
let writer = Cursor::new(Vec::with_capacity(2048));

FeaturePropertyBag { writer }
FeaturePropertyBag {
writer,
feature_property_bags: HashSet::new(),
}
}

// -----------------------------------------------------------------------
Expand All @@ -50,6 +54,14 @@ impl FeaturePropertyBag {
// Write the XFComplements <bag> element.
self.write_xf_compliments_bag();

// Write the DXFComplements <bag> element.
if self
.feature_property_bags
.contains(&FeaturePropertyBagTypes::DXFComplements)
{
self.write_dxf_compliments_bag();
}

// Close the feature_property_bagProperties tag.
xml_end_tag(&mut self.writer, "FeaturePropertyBags");
}
Expand Down Expand Up @@ -111,6 +123,22 @@ impl FeaturePropertyBag {
xml_end_tag(&mut self.writer, "bag");
}

// Write the DXFComplements <bag> element.
fn write_dxf_compliments_bag(&mut self) {
let attributes = [
("type", "DXFComplements"),
("extRef", "DXFComplementsMapperExtRef"),
];

xml_start_tag(&mut self.writer, "bag", &attributes);
xml_start_tag(&mut self.writer, "a", &[("k", "MappedFeaturePropertyBags")]);

self.write_bag_id("", "2");

xml_end_tag(&mut self.writer, "a");
xml_end_tag(&mut self.writer, "bag");
}

// Write the <bagId> element.
fn write_bag_id(&mut self, key: &str, id: &str) {
let mut attributes = vec![];
Expand All @@ -122,3 +150,9 @@ impl FeaturePropertyBag {
xml_data_element(&mut self.writer, "bagId", id, &attributes);
}
}

#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub(crate) enum FeaturePropertyBagTypes {
XFComplements,
DXFComplements,
}
22 changes: 14 additions & 8 deletions src/packager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ use crate::content_types::ContentTypes;
use crate::core::Core;
use crate::custom::Custom;
use crate::error::XlsxError;
use crate::feature_property_bag::FeaturePropertyBag;
use crate::feature_property_bag::{FeaturePropertyBag, FeaturePropertyBagTypes};
use crate::metadata::Metadata;
use crate::relationship::Relationship;
use crate::rich_value::RichValue;
Expand Down Expand Up @@ -211,8 +211,8 @@ impl<W: Write + Seek + Send> Packager<W> {
self.write_rich_value_files(workbook, options)?;
}

if options.has_checkboxes {
self.write_feature_property_bag()?;
if !options.feature_property_bags.is_empty() {
self.write_feature_property_bag(&options.feature_property_bags)?;
}

// Close the zip file.
Expand Down Expand Up @@ -286,7 +286,7 @@ impl<W: Write + Seek + Send> Packager<W> {
content_types.add_rich_value();
}

if options.has_checkboxes {
if !options.feature_property_bags.is_empty() {
content_types.add_feature_bag_property();
}

Expand Down Expand Up @@ -417,7 +417,7 @@ impl<W: Write + Seek + Send> Packager<W> {
);
}

if options.has_checkboxes {
if !options.feature_property_bags.is_empty() {
rels.add_office_relationship(
"2022/11",
"FeaturePropertyBag",
Expand Down Expand Up @@ -1068,8 +1068,14 @@ impl<W: Write + Seek + Send> Packager<W> {
}

// Write the vba project file.
fn write_feature_property_bag(&mut self) -> Result<(), XlsxError> {
fn write_feature_property_bag(
&mut self,
feature_property_bags: &HashSet<FeaturePropertyBagTypes>,
) -> Result<(), XlsxError> {
let mut property_bag = FeaturePropertyBag::new();
property_bag
.feature_property_bags
.clone_from(feature_property_bags);

self.zip.start_file(
"xl/featurePropertyBag/featurePropertyBag.xml",
Expand Down Expand Up @@ -1105,7 +1111,7 @@ pub(crate) struct PackagerOptions {
pub(crate) properties: DocProperties,
pub(crate) num_embedded_images: u32,
pub(crate) has_embedded_image_descriptions: bool,
pub(crate) has_checkboxes: bool,
pub(crate) feature_property_bags: HashSet<FeaturePropertyBagTypes>,
}

impl PackagerOptions {
Expand All @@ -1132,7 +1138,7 @@ impl PackagerOptions {
properties: DocProperties::new(),
num_embedded_images: 0,
has_embedded_image_descriptions: false,
has_checkboxes: false,
feature_property_bags: HashSet::new(),
}
}
}
47 changes: 35 additions & 12 deletions src/styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ impl<'a> Styles<'a> {

if has_checkbox {
// Write the checkbox extLst element.
self.write_checkbox_ext();
self.write_xf_format_extensions();
}

xml_end_tag(&mut self.writer, "xf");
Expand Down Expand Up @@ -803,23 +803,27 @@ impl<'a> Styles<'a> {
} else {
xml_start_tag(&mut self.writer, "dxfs", &attributes);

for xf_format in self.dxf_formats {
for dxf_format in self.dxf_formats {
xml_start_tag_only(&mut self.writer, "dxf");

if xf_format.has_dxf_font() {
self.write_font(&xf_format.font, true);
if dxf_format.has_dxf_font() {
self.write_font(&dxf_format.font, true);
}

if xf_format.num_format_index > 0 {
self.write_num_fmt(xf_format.num_format_index, &xf_format.num_format);
if dxf_format.num_format_index > 0 {
self.write_num_fmt(dxf_format.num_format_index, &dxf_format.num_format);
}

if xf_format.has_dxf_fill() {
self.write_fill(&xf_format.fill, true);
if dxf_format.has_dxf_fill() {
self.write_fill(&dxf_format.fill, true);
}

if xf_format.has_border {
self.write_border(&xf_format.borders, true);
if dxf_format.has_border {
self.write_border(&dxf_format.borders, true);
}

if dxf_format.has_checkbox() {
self.write_dxf_format_extensions();
}

xml_end_tag(&mut self.writer, "dxf");
Expand Down Expand Up @@ -867,8 +871,8 @@ impl<'a> Styles<'a> {
xml_empty_tag(&mut self.writer, "numFmt", &attributes);
}

// Write the Checkbox <extLst> element.
fn write_checkbox_ext(&mut self) {
// Write the xfComplement <extLst> elements.
fn write_xf_format_extensions(&mut self) {
let attributes = [
("uri", "{C7286773-470A-42A8-94C5-96B5CB345126}"),
(
Expand All @@ -885,4 +889,23 @@ impl<'a> Styles<'a> {
xml_end_tag(&mut self.writer, "ext");
xml_end_tag(&mut self.writer, "extLst");
}

// Write the DXFfComplement <extLst> elements.
fn write_dxf_format_extensions(&mut self) {
let attributes = [
("uri", "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"),
(
"xmlns:xfpb",
"http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag",
),
];

xml_start_tag_only(&mut self.writer, "extLst");
xml_start_tag(&mut self.writer, "ext", &attributes);

xml_empty_tag(&mut self.writer, "xfpb:DXFComplement", &[("i", "0")]);

xml_end_tag(&mut self.writer, "ext");
xml_end_tag(&mut self.writer, "extLst");
}
}
31 changes: 27 additions & 4 deletions src/workbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};

use crate::error::XlsxError;
use crate::feature_property_bag::FeaturePropertyBagTypes;
use crate::format::Format;
use crate::packager::Packager;
use crate::packager::PackagerOptions;
Expand Down Expand Up @@ -328,6 +329,7 @@ pub struct Workbook {
pub(crate) is_xlsm_file: bool,
pub(crate) has_comments: bool,
pub(crate) string_table: Arc<Mutex<SharedStringsTable>>,
pub(crate) feature_property_bags: HashSet<FeaturePropertyBagTypes>,

xf_indices: Arc<RwLock<HashMap<Format, u32>>>,
dxf_indices: HashMap<Format, u32>,
Expand Down Expand Up @@ -418,6 +420,7 @@ impl Workbook {
num_worksheets: 0,
num_chartsheets: 0,
use_large_file: false,
feature_property_bags: HashSet::new(),
}
}

Expand Down Expand Up @@ -2443,6 +2446,9 @@ impl Workbook {

// Set the number format index for the format objects.
self.prepare_num_formats();

// Check for any format properties that require a feature bag.
self.prepare_feature_property_bags();
}

// Set the font index for the format objects. This only needs to be done for
Expand Down Expand Up @@ -2583,9 +2589,24 @@ impl Workbook {
}
}

// Check if any of the formats has a checkbox property.
fn has_checkboxes(&mut self) -> bool {
self.xf_formats.iter().any(Format::has_checkbox)
// Check for any format properties that require a feature bag. Currently
// this only applies to checkboxes.
fn prepare_feature_property_bags(&mut self) {
for xf_format in &self.xf_formats {
if xf_format.has_checkbox() {
self.feature_property_bags
.insert(FeaturePropertyBagTypes::XFComplements);
break;
}
}

for dxf_format in &self.dxf_formats {
if dxf_format.has_checkbox() {
self.feature_property_bags
.insert(FeaturePropertyBagTypes::DXFComplements);
break;
}
}
}

// Collect some workbook level metadata to help generate the xlsx
Expand All @@ -2602,7 +2623,9 @@ impl Workbook {

package_options.is_xlsm_file = self.is_xlsm_file;
package_options.has_vba_signature = !self.vba_signature.is_empty();
package_options.has_checkboxes = self.has_checkboxes();
package_options
.feature_property_bags
.clone_from(&self.feature_property_bags);

// Iterate over the worksheets to capture workbook and update the
// package options metadata.
Expand Down
2 changes: 1 addition & 1 deletion src/worksheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7637,7 +7637,7 @@ impl Worksheet {
self.set_writing_ahead(true);

// Get a copy of the column format or use the default format. This
// is mainly to workaround constant memory cases which can't use the
// is mainly to work around constant memory cases which can't use the
// update_cell_format() approach below.
let col_format = match &column.format {
Some(format) => format.clone(),
Expand Down
Binary file added tests/input/checkbox06.xlsx
Binary file not shown.
Binary file added tests/input/checkbox07.xlsx
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/integration/checkbox03.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2032-2035, John McNamara, [email protected]
// Copyright 2022-2025, John McNamara, [email protected]

use crate::common;
use rust_xlsxwriter::{Format, Workbook, XlsxError};
Expand Down
78 changes: 78 additions & 0 deletions tests/integration/checkbox06.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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::{Format, Workbook, XlsxError};

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

worksheet.write(0, 0, "Col1")?;
worksheet.write(1, 0, 1)?;
worksheet.write(2, 0, 2)?;
worksheet.write(3, 0, 3)?;
worksheet.write(4, 0, 4)?;

worksheet.write(0, 1, "Col2")?;
worksheet.insert_checkbox(1, 1, true)?;
worksheet.insert_checkbox(2, 1, false)?;
worksheet.insert_checkbox(3, 1, false)?;
worksheet.insert_checkbox(4, 1, true)?;

workbook.save(filename)?;

Ok(())
}

// Test with standard boolean value and format.
fn create_new_xlsx_file_2(filename: &str) -> Result<(), XlsxError> {
let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet();
let format = Format::new().set_checkbox();

worksheet.write(0, 0, "Col1")?;
worksheet.write(1, 0, 1)?;
worksheet.write(2, 0, 2)?;
worksheet.write(3, 0, 3)?;
worksheet.write(4, 0, 4)?;

worksheet.write(0, 1, "Col2")?;
worksheet.write_boolean_with_format(1, 1, true, &format)?;
worksheet.write_boolean_with_format(2, 1, false, &format)?;
worksheet.write_boolean_with_format(3, 1, false, &format)?;
worksheet.write_boolean_with_format(4, 1, true, &format)?;

workbook.save(filename)?;

Ok(())
}

#[test]
fn test_checkbox06_1() {
let test_runner = common::TestRunner::new()
.set_name("checkbox06")
.set_function(create_new_xlsx_file_1)
.unique("1")
.initialize();

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

#[test]
fn test_checkbox06_2() {
let test_runner = common::TestRunner::new()
.set_name("checkbox06")
.set_function(create_new_xlsx_file_2)
.unique("2")
.initialize();

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

0 comments on commit 1b2dc61

Please sign in to comment.