Initial support for adding macros to workbooks.

Initial support for adding macros extracted from existing xlsm
files to new xlsm files.

Issue #29 add_vba_project()
This commit is contained in:
John McNamara 2019-06-16 15:46:09 +01:00
parent 7d372b91c7
commit cb87c73a95
16 changed files with 278 additions and 6 deletions

View File

@ -17,6 +17,7 @@
#define LXW_APP_PACKAGE "application/vnd.openxmlformats-package."
#define LXW_APP_DOCUMENT "application/vnd.openxmlformats-officedocument."
#define LXW_APP_MSEXCEL "application/vnd.ms-excel."
/*
* Struct to represent a content_types.

View File

@ -285,6 +285,9 @@ typedef struct lxw_workbook {
lxw_hash_table *used_xf_formats;
char *vba_project;
char *vba_codename;
} lxw_workbook;
@ -846,6 +849,47 @@ lxw_chartsheet *workbook_get_chartsheet_by_name(lxw_workbook *workbook,
lxw_error workbook_validate_sheet_name(lxw_workbook *workbook,
const char *sheetname);
/**
* @brief Add a vbaProject binary to the Excel workbook.
*
* @param workbook Pointer to a lxw_workbook instance.
* @param filename The path/filename of the vbaProject.bin file.
*
* The `%workbook_add_vba_project()` function can be used to add macros or
* functions to a workbook using a binary VBA project file that has been
* extracted from an existing Excel xlsm file:
*
* @code
* workbook_add_vba_project(workbook, "vbaProject.bin");
* @endcode
*
* Only one `vbaProject.bin file` can be added per workbook.
*
* @return A #lxw_error.
*/
lxw_error workbook_add_vba_project(lxw_workbook *workbook,
const char *filename);
/**
* @brief Set the VBA name for the workbook.
*
* @param workbook Pointer to a lxw_workbook instance.
* @param name Name of the workbook used by VBA.
*
* The `workbook_set_vba_name()` function can be used to set the VBA name for
* the workbook. This is sometimes required when a vbaProject macro included
* via `workbook_add_vba_project()` refers to the workbook.
*
* @code
* workbook_set_vba_name(workbook, "MyWorkbook");
* @endcode
*
* The most common Excel VBA name for a workbook is `ThisWorkbook`.
*
* @return A #lxw_error.
*/
lxw_error workbook_set_vba_name(lxw_workbook *workbook, const char *name);
void lxw_workbook_free(lxw_workbook *workbook);
void lxw_workbook_assemble_xml_file(lxw_workbook *workbook);
void lxw_workbook_set_default_xf_indices(lxw_workbook *workbook);

View File

@ -741,10 +741,10 @@ typedef struct lxw_worksheet {
uint8_t right_to_left;
uint8_t screen_gridlines;
uint8_t show_zeros;
uint8_t vba_codename;
uint8_t vcenter;
uint8_t zoom_scale_normal;
uint8_t num_validations;
char *vba_codename;
lxw_color_t tab_color;
@ -3235,6 +3235,29 @@ void worksheet_outline_settings(lxw_worksheet *worksheet, uint8_t visible,
void worksheet_set_default_row(lxw_worksheet *worksheet, double height,
uint8_t hide_unused_rows);
/**
* @brief Set the VBA name for the worksheet.
*
* @param worksheet Pointer to a lxw_worksheet instance.
* @param name Name of the worksheet used by VBA.
*
* The `worksheet_set_vba_name()` function can be used to set the VBA name for
* the worksheet. This is sometimes required when a vbaProject macro included
* via `workbook_add_vba_project()` refers to the worksheet.
*
* @code
* workbook_set_vba_name (workbook, "MyWorkbook");
* worksheet_set_vba_name(worksheet, "MySheet1");
* @endcode
*
* In general Excel uses the worksheet name such as "Sheet1" as the VBA name.
* However, this can be changed in the VBA environment or if the the macro was
* extracted from a foreign language version of Excel.
*
* @return A #lxw_error.
*/
lxw_error worksheet_set_vba_name(lxw_worksheet *worksheet, const char *name);
lxw_worksheet *lxw_worksheet_new(lxw_worksheet_init_data *init_data);
void lxw_worksheet_free(lxw_worksheet *worksheet);
void lxw_worksheet_assemble_xml_file(lxw_worksheet *worksheet);

View File

@ -50,8 +50,6 @@ lxw_content_types_new(void)
LXW_APP_DOCUMENT "spreadsheetml.styles+xml");
lxw_ct_add_override(content_types, "/xl/theme/theme1.xml",
LXW_APP_DOCUMENT "theme+xml");
lxw_ct_add_override(content_types, "/xl/workbook.xml",
LXW_APP_DOCUMENT "spreadsheetml.sheet.main+xml");
return content_types;

View File

@ -294,6 +294,35 @@ _write_image_files(lxw_packager *self)
return LXW_NO_ERROR;
}
/*
* Write the xl/vbaProject.bin file.
*/
STATIC lxw_error
_add_vba_project(lxw_packager *self)
{
lxw_workbook *workbook = self->workbook;
lxw_error err;
FILE *image_stream;
if (!workbook->vba_project)
return LXW_NO_ERROR;
/* Check that the image file exists and can be opened. */
image_stream = fopen(workbook->vba_project, "rb");
if (!image_stream) {
LXW_WARN_FORMAT1("Error adding vbaProject.bin to xlsx file: "
"file doesn't exist or can't be opened: %s.",
workbook->vba_project);
return LXW_ERROR_CREATING_TMPFILE;
}
err = _add_file_to_zip(self, image_stream, "xl/vbaProject.bin");
fclose(image_stream);
RETURN_ON_ERROR(err);
return LXW_NO_ERROR;
}
/*
* Write the chart files.
*/
@ -722,6 +751,17 @@ _write_content_types_file(lxw_packager *self)
if (workbook->has_bmp)
lxw_ct_add_default(content_types, "bmp", "image/bmp");
if (workbook->vba_project)
lxw_ct_add_default(content_types, "bin",
"application/vnd.ms-office.vbaProject");
if (workbook->vba_project)
lxw_ct_add_override(content_types, "/xl/workbook.xml",
LXW_APP_MSEXCEL "sheet.macroEnabled.main+xml");
else
lxw_ct_add_override(content_types, "/xl/workbook.xml",
LXW_APP_DOCUMENT "spreadsheetml.sheet.main+xml");
STAILQ_FOREACH(sheet, workbook->sheets, list_pointers) {
if (sheet->is_chartsheet) {
lxw_snprintf(filename, LXW_FILENAME_LENGTH,
@ -812,6 +852,10 @@ _write_workbook_rels_file(lxw_packager *self)
lxw_add_document_relationship(rels, "/sharedStrings",
"sharedStrings.xml");
if (workbook->vba_project)
lxw_add_ms_package_relationship(rels, "/vbaProject",
"vbaProject.bin");
lxw_relationships_assemble_xml_file(rels);
err = _add_file_to_zip(self, rels->file, "xl/_rels/workbook.xml.rels");
@ -1203,6 +1247,9 @@ lxw_create_package(lxw_packager *self)
error = _write_image_files(self);
RETURN_ON_ERROR(error);
error = _add_vba_project(self);
RETURN_ON_ERROR(error);
error = _write_core_file(self);
RETURN_ON_ERROR(error);

View File

@ -208,6 +208,8 @@ lxw_workbook_free(lxw_workbook *workbook)
lxw_sst_free(workbook->sst);
free(workbook->options.tmpdir);
free(workbook->ordered_charts);
free(workbook->vba_project);
free(workbook->vba_codename);
free(workbook);
}
@ -1135,6 +1137,10 @@ _write_file_version(lxw_workbook *self)
LXW_PUSH_ATTRIBUTES_STR("lowestEdited", "4");
LXW_PUSH_ATTRIBUTES_STR("rupBuild", "4505");
if (self->vba_project)
LXW_PUSH_ATTRIBUTES_STR("codeName",
"{37E998C4-C9E5-D4B9-71C8-EB1FF731991C}");
lxw_xml_empty_tag(self->file, "fileVersion", &attributes);
LXW_FREE_ATTRIBUTES();
@ -1150,6 +1156,10 @@ _write_workbook_pr(lxw_workbook *self)
struct xml_attribute *attribute;
LXW_INIT_ATTRIBUTES();
if (self->vba_codename)
LXW_PUSH_ATTRIBUTES_STR("codeName", self->vba_codename);
LXW_PUSH_ATTRIBUTES_STR("defaultThemeVersion", "124226");
lxw_xml_empty_tag(self->file, "workbookPr", &attributes);
@ -1718,6 +1728,22 @@ workbook_close(lxw_workbook *self)
worksheet->active = 1;
}
/* Set workbook and worksheet VBA codenames if a macro has been added. */
if (self->vba_project) {
if (!self->vba_codename)
workbook_set_vba_name(self, "ThisWorkbook");
STAILQ_FOREACH(sheet, self->sheets, list_pointers) {
if (sheet->is_chartsheet)
continue;
else
worksheet = sheet->u.worksheet;
if (!worksheet->vba_codename)
worksheet_set_vba_name(worksheet, worksheet->name);
}
}
/* Set the defined names for the worksheets such as Print Titles. */
_prepare_defined_names(self);
@ -2156,3 +2182,48 @@ workbook_validate_sheet_name(lxw_workbook *self, const char *sheetname)
return LXW_NO_ERROR;
}
/*
* Add a vbaProject binary to the Excel workbook.
*/
lxw_error
workbook_add_vba_project(lxw_workbook *self, const char *filename)
{
FILE *filehandle;
if (!filename) {
LXW_WARN("workbook_add_vba_project(): "
"filename must be specified.");
return LXW_ERROR_NULL_PARAMETER_IGNORED;
}
/* Check that the vbaProject file exists and can be opened. */
filehandle = fopen(filename, "rb");
if (!filehandle) {
LXW_WARN_FORMAT1("workbook_add_vba_project(): "
"file doesn't exist or can't be opened: %s.",
filename);
return LXW_ERROR_PARAMETER_VALIDATION;
}
fclose(filehandle);
self->vba_project = lxw_strdup(filename);
return LXW_NO_ERROR;
}
/*
* Set the VBA name for the workbook.
*/
lxw_error
workbook_set_vba_name(lxw_workbook *self, const char *name)
{
if (!name) {
LXW_WARN("workbook_set_vba_name(): " "name must be specified.");
return LXW_ERROR_NULL_PARAMETER_IGNORED;
}
self->vba_codename = lxw_strdup(name);
return LXW_NO_ERROR;
}

View File

@ -448,6 +448,7 @@ lxw_worksheet_free(lxw_worksheet *worksheet)
free(worksheet->vbreaks);
free(worksheet->name);
free(worksheet->quoted_name);
free(worksheet->vba_codename);
free(worksheet);
worksheet = NULL;
@ -3147,7 +3148,7 @@ _worksheet_write_sheet_pr(lxw_worksheet *self)
LXW_INIT_ATTRIBUTES();
if (self->vba_codename)
LXW_PUSH_ATTRIBUTES_INT("codeName", self->vba_codename);
LXW_PUSH_ATTRIBUTES_STR("codeName", self->vba_codename);
if (self->filter_on)
LXW_PUSH_ATTRIBUTES_STR("filterMode", "1");
@ -6069,3 +6070,19 @@ worksheet_data_validation_cell(lxw_worksheet *self, lxw_row_t row,
return worksheet_data_validation_range(self, row, col,
row, col, validation);
}
/*
* Set the VBA name for the worksheet.
*/
lxw_error
worksheet_set_vba_name(lxw_worksheet *self, const char *name)
{
if (!name) {
LXW_WARN("worksheet_set_vba_name(): " "name must be specified.");
return LXW_ERROR_NULL_PARAMETER_IGNORED;
}
self->vba_codename = lxw_strdup(name);
return LXW_NO_ERROR;
}

View File

@ -32,8 +32,11 @@ class XLSXBaseTest(unittest.TestCase):
got = os.system("cd test/functional/src; ./%s" % exe_name)
self.assertEqual(got, self.no_system_error)
# Create the path/file names for the xlsx files to compare.
got_filename = exe_name.replace('test_', '') + '.xlsx'
# Create the path/file names for the xlsx/xlsm files to compare.
if exp_filename and exp_filename.endswith('.xlsm'):
got_filename = exe_name.replace('test_', '') + '.xlsm'
else:
got_filename = exe_name.replace('test_', '') + '.xlsx'
if not exp_filename:
exp_filename = got_filename

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
/*****************************************************************************
* Test cases for libxlsxwriter.
*
* Test to compare output against Excel files.
*
* Copyright 2014-2019, John McNamara, jmcnamara@cpan.org
*
*/
#include "xlsxwriter.h"
int main() {
lxw_workbook *workbook = new_workbook("test_macro01.xlsm");
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
workbook_add_vba_project(workbook, "images/vbaProject01.bin");
worksheet_write_number(worksheet, CELL("A1"), 123 , NULL);
return workbook_close(workbook);
}

View File

@ -0,0 +1,24 @@
/*****************************************************************************
* Test cases for libxlsxwriter.
*
* Test to compare output against Excel files.
*
* Copyright 2014-2019, John McNamara, jmcnamara@cpan.org
*
*/
#include "xlsxwriter.h"
int main() {
lxw_workbook *workbook = new_workbook("test_macro02.xlsm");
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
workbook_add_vba_project(workbook, "images/vbaProject03.bin");
workbook_set_vba_name(workbook, "MyWorkbook");
worksheet_set_vba_name(worksheet, "MySheet1");
worksheet_write_number(worksheet, CELL("A1"), 123 , NULL);
return workbook_close(workbook);
}

View File

@ -0,0 +1,20 @@
###############################################################################
#
# Tests for libxlsxwriter.
#
# Copyright 2014-2019, John McNamara, jmcnamara@cpan.org
#
import base_test_class
class TestCompareXLSXFiles(base_test_class.XLSXBaseTest):
"""
Test file created with libxlsxwriter against a file created by Excel.
"""
def test_macro01(self):
self.run_exe_test('test_macro01', 'macro01.xlsm')
def test_macro02(self):
self.run_exe_test('test_macro02', 'macro02.xlsm')

Binary file not shown.

Binary file not shown.

View File

@ -37,6 +37,8 @@ CTEST(content_types, content_types01) {
lxw_content_types *content_types = lxw_content_types_new();
content_types->file = testfile;
lxw_ct_add_override(content_types, "/xl/workbook.xml",
LXW_APP_DOCUMENT "spreadsheetml.sheet.main+xml");
lxw_ct_add_worksheet_name(content_types, "/xl/worksheets/sheet1.xml");
lxw_ct_add_default(content_types, "jpeg", "image/jpeg");
lxw_ct_add_shared_strings(content_types);