From a956fc028ec3918756421adacb2e5d881f471100 Mon Sep 17 00:00:00 2001 From: "Marton-Levente Szigeti (ext.)" Date: Mon, 15 Jun 2026 23:14:29 +0200 Subject: [PATCH] Add macro copying and support for Excel 2024 FeaturePropertyBag type checkbox inputs --- .../dhatim/fastexcel/GenericStyleSetter.java | 19 ++- .../org/dhatim/fastexcel/Relationships.java | 1 + .../main/java/org/dhatim/fastexcel/Style.java | 25 +++- .../java/org/dhatim/fastexcel/StyleCache.java | 6 +- .../java/org/dhatim/fastexcel/Workbook.java | 124 ++++++++++++++++-- .../java/org/dhatim/fastexcel/Worksheet.java | 16 ++- .../fastexcel/CheckboxWorksheetTest.java | 24 ++++ .../org/dhatim/fastexcel/MacroCopyTest.java | 42 ++++++ .../fastexcel/StyleCacheBeforeAfterTest.java | 2 + .../org/dhatim/fastexcel/HelloWorldMacro.xlsm | Bin 0 -> 13173 bytes 10 files changed, 242 insertions(+), 17 deletions(-) create mode 100644 fastexcel-writer/src/test/java/org/dhatim/fastexcel/CheckboxWorksheetTest.java create mode 100644 fastexcel-writer/src/test/java/org/dhatim/fastexcel/MacroCopyTest.java create mode 100644 fastexcel-writer/src/test/resources/org/dhatim/fastexcel/HelloWorldMacro.xlsm diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java index 6cb0776d..a0ac1393 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/GenericStyleSetter.java @@ -115,6 +115,10 @@ abstract class GenericStyleSetter currentStyles, } // Compute a map giving new styles for current styles - Map newStyles = currentStyles.stream().collect(Collectors.toMap(Function.identity(), s -> worksheet.getWorkbook().mergeAndCacheStyle(s, valueFormatting, font, fill, border, alignment, protection))); + Map newStyles = currentStyles.stream().collect(Collectors.toMap(Function.identity(), s -> worksheet.getWorkbook().mergeAndCacheStyle(s, valueFormatting, font, fill, border, checkbox, alignment, protection))); // Apply styles stylesFunction.applyStyles(newStyles); diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java index 4b71ad55..6fb90b2d 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Relationships.java @@ -13,6 +13,7 @@ public class Relationships { private static final String TYPE_OF_COMMENTS= "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; private static final String TYPE_OF_VMLDRAWING= "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"; private static final String TYPE_OF_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"; + private static final String TYPE_OF_VBAPROJECT = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"; private final AtomicInteger maxIndex = new AtomicInteger(1); diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Style.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Style.java index 92460489..98d7ad63 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Style.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Style.java @@ -50,6 +50,11 @@ class Style { */ private final Protection protection; + /** + * Whether it is displayed as an Excel 2004 FeaturePropertyBag checkbox + */ + private final boolean checkbox; + /** * Constructor. * @@ -58,21 +63,24 @@ class Style { * @param valueFormatting Index of cached value formatting. Zero if not set. * @param font Index of cached font. Zero if not set. * @param fill Index of cached fill pattern. Zero if not set. + * @param checkbox Whether this style should be renderes as checkbox. * @param border Index of cached border. Zero if not set. * @param alignment Alignment. {@code null} if not set. + * @param protection The cell protection applied to this style. */ - Style(Style original, int valueFormatting, int font, int fill, int border, Alignment alignment, Protection protection) { + Style(Style original, int valueFormatting, int font, int fill, int border, boolean checkbox, Alignment alignment, Protection protection) { this.valueFormatting = (valueFormatting == 0 && original != null) ? original.valueFormatting : valueFormatting; this.font = (font == 0 && original != null) ? original.font : font; this.fill = (fill == 0 && original != null) ? original.fill : fill; this.border = (border == 0 && original != null) ? original.border : border; + this.checkbox = checkbox; this.alignment = (alignment == null && original != null) ? original.alignment : alignment; this.protection = (protection == null && original != null) ? original.protection : protection; } @Override public int hashCode() { - return Objects.hash(valueFormatting, font, fill, border, alignment, protection); + return Objects.hash(valueFormatting, font, fill, border, checkbox, alignment, protection); } @Override @@ -85,7 +93,8 @@ public boolean equals(Object obj) { && Objects.equals(fill, other.fill) && Objects.equals(border, other.border) && Objects.equals(alignment, other.alignment) - && Objects.equals(protection, other.protection); + && Objects.equals(protection, other.protection) + && checkbox == other.checkbox; } else { result = false; } @@ -104,7 +113,7 @@ void write(Writer w) throws IOException { w.append(" applyBorder=\"1\""); } - if (alignment == null && protection == null) { + if (alignment == null && protection == null && !checkbox) { w.append("/>"); return; } @@ -116,6 +125,14 @@ void write(Writer w) throws IOException { } w.append('>'); + if (checkbox) { + w + .append("") + .append("") + .append("") + .append("") + .append(""); + } if (alignment != null) { alignment.write(w); } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java index c200de1e..0ada5c82 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/StyleCache.java @@ -43,7 +43,7 @@ final class StyleCache { * Default constructor. Pre-cache Excel-reserved stuff. */ StyleCache() { - mergeAndCacheStyle(0, null, Font.DEFAULT, Fill.NONE, Border.NONE, null, null); + mergeAndCacheStyle(0, null, Font.DEFAULT, Fill.NONE, Border.NONE, false, null, null); cacheFill(Fill.GRAY125); } @@ -139,9 +139,9 @@ int cacheDxf(DifferentialFormat f) { return cacheStuff(dxfs, f); } - int mergeAndCacheStyle(int currentStyle, String numberingFormat, Font font, Fill fill, Border border, Alignment alignment, Protection protection) { + int mergeAndCacheStyle(int currentStyle, String numberingFormat, Font font, Fill fill, Border border, boolean checkbox, Alignment alignment, Protection protection) { Style original = styleIndexToStyle.get(currentStyle); - Style s = new Style(original, cacheValueFormatting(numberingFormat), cacheFont(font), cacheFill(fill), cacheBorder(border), alignment, protection); + Style s = new Style(original, cacheValueFormatting(numberingFormat), cacheFont(font), cacheFill(fill), cacheBorder(border), checkbox, alignment, protection); return cacheStyle(s, k -> styles.size()); } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java index d79f39d7..681b79f7 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java @@ -17,8 +17,12 @@ import com.github.rzymek.opczip.OpcOutputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.math.BigDecimal; import java.time.Instant; @@ -29,6 +33,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** @@ -39,6 +44,9 @@ public class Workbook implements Closeable { private int activeTab = 0; private boolean finished = false; private String workbookPasswordHash; + private String codeName = "ThisWorkbook"; + private byte[] vbaProject = null; + private boolean featurePropertyBag = false; private final String applicationName; private final String applicationVersion; private final List worksheets = new ArrayList<>(); @@ -186,7 +194,13 @@ public void finish() throws IOException { w.append(""); } - w.append(""); + w.append(""); + if (hasMacros()) { + w.append(""); + w.append(""); + } else { + w.append(""); + } for (Worksheet ws : worksheets) { int index = getIndex(ws); w.append(""); @@ -210,6 +224,9 @@ public void finish() throws IOException { if (properties.hasCustomProperties()) { w.append(""); } + if (featurePropertyBag) { + w.append(""); + } w.append(""); }); writeProperties(); @@ -220,26 +237,46 @@ public void finish() throws IOException { writeFile("_rels/.rels", w -> { w.append(""); w.append(""); - w.append(""); - w.append(""); - w.append(""); if (properties.hasCustomProperties()) { w.append(""); } + w.append(""); + w.append(""); + w.append(""); w.append(""); }); + if (featurePropertyBag) { + writeFile("xl/featurePropertyBag/featurePropertyBag.xml", w -> { + w.append(""); + w.append(""); + w.append("012"); + w.append(""); + }); + } + writeWorkbookFile(); writeFile("xl/_rels/workbook.xml.rels", w -> { w.append(""); + int rels = 3; for (Worksheet ws : worksheets) { - w.append(""); + w.append(""); + } + if (featurePropertyBag) { + w.append(""); + } + if (hasMacros()) { + w.append(""); } w.append(""); }); writeFile("xl/sharedStrings.xml", stringCache::write); writeFile("xl/styles.xml", styleCache::write); + if (hasMacros()) { + writeBinaryFile("xl/vbaProject.bin", vbaProject); + vbaProject = null; + } this.os.finish(); finished = true; } @@ -317,6 +354,13 @@ private void writeProperties() throws IOException { }); } + /** + * @return true when macros have been injected + */ + private boolean hasMacros() { + return vbaProject != null; + } + /** * @return true when any sheet has any comments */ @@ -353,7 +397,7 @@ private void writeWorkbookFile() throws IOException { "" + - ""); + ""); if (workbookPasswordHash != null) { w.append(" 0) { + bos.write(buffer, 0, len); + } + embedMacro(bos.toByteArray()); + return; + } + } + throw new IllegalArgumentException("File did not contains vbaProject.bin file"); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * + * @param file the file to copy macros from + * @throws IllegalArgumentException thrown if file is not a valid XLSX file + * @throws NullPointerException if file is null + */ + public void copyMacrosFromFile(File file) throws IllegalArgumentException, NullPointerException { + Objects.requireNonNull(file); + try (FileInputStream fis = new FileInputStream(file)) { + copyMacrosFromInputStream(fis); + + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Sets the code name for this Workbook to be references in macros + * @param codeName the code name of this workbook + */ + public void setCodeName(String codeName) { + this.codeName = Objects.requireNonNull(codeName); + } + int nextTableIndex() { return maxTableIndex.getAndIncrement(); } + + public void addFeaturePropertyBag() { + this.featurePropertyBag = true; + } } diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java index bad3a6a2..f4c49c9f 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java @@ -58,6 +58,8 @@ public class Worksheet implements Closeable { private final Workbook workbook; private final String name; + private String codeName; + /** * List of rows. A row is an array of cells. * Flushed rows are null. @@ -310,6 +312,14 @@ public String getName() { return name; } + /** + * Set the worksheet code name for referencing in macros + * @param codeName the new code name to set + */ + public void setCodeName(String codeName) { + this.codeName = codeName; + } + /** * Get repeating rows defined for the print setup. * @@ -1112,7 +1122,11 @@ public void flush() throws IOException { writer = workbook.beginFile("xl/worksheets/sheet" + index + ".xml"); writer.append(""); writer.append(""); - writer.append(""); + if (codeName == null) { + writer.append(""); + } else { + writer.append(""); + } if (tabColor != null) { writer.append(""); } diff --git a/fastexcel-writer/src/test/java/org/dhatim/fastexcel/CheckboxWorksheetTest.java b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/CheckboxWorksheetTest.java new file mode 100644 index 00000000..20716a59 --- /dev/null +++ b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/CheckboxWorksheetTest.java @@ -0,0 +1,24 @@ +package org.dhatim.fastexcel; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +public class CheckboxWorksheetTest { + + @Test + public void testCheckboxWorksheet() throws IOException { + try (ByteArrayOutputStream fos = new ByteArrayOutputStream(); Workbook wb = new Workbook(fos, "FastExcel", "1.0")) { + Worksheet sheet = wb.newWorksheet("Test"); + sheet.value(1, 1, true); + sheet.value(1, 2, false); + sheet.value(2, 1, true); + sheet.value(2, 2, false); + sheet.style(2, 1).checkbox(true).set(); + sheet.style(2, 2).checkbox(true).set(); + sheet.close(); + } + } +} diff --git a/fastexcel-writer/src/test/java/org/dhatim/fastexcel/MacroCopyTest.java b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/MacroCopyTest.java new file mode 100644 index 00000000..8e2b2bf2 --- /dev/null +++ b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/MacroCopyTest.java @@ -0,0 +1,42 @@ +package org.dhatim.fastexcel; + +import com.github.rzymek.opczip.reader.skipping.ZipStreamReader; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MacroCopyTest { + + @Test + void testCopyMacro() throws IOException { + byte[] document; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (Workbook workbook = new Workbook(out, "FastExcel", "1.0")) { + Worksheet sheet = workbook.newWorksheet("Hello"); + sheet.value(1, 1, "Hello World"); + workbook.copyMacrosFromInputStream(MacroCopyTest.class.getResourceAsStream("HelloWorldMacro.xlsm")); + } + out.flush(); + document = out.toByteArray(); + } + + boolean hasVbaProjectEmbedded = false; + try (ZipStreamReader zipInputStream = new ZipStreamReader(new ByteArrayInputStream(document))) { + ZipEntry entry; + while ((entry = zipInputStream.nextEntry()) != null) { + if (entry.getName().equals("xl/vbaProject.bin")) { + hasVbaProjectEmbedded = true; + break; + } + zipInputStream.skipStream(); + zipInputStream.skipStream(); + } + } + assertTrue(hasVbaProjectEmbedded); + } +} diff --git a/fastexcel-writer/src/test/java/org/dhatim/fastexcel/StyleCacheBeforeAfterTest.java b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/StyleCacheBeforeAfterTest.java index 227566e0..5fdfc704 100644 --- a/fastexcel-writer/src/test/java/org/dhatim/fastexcel/StyleCacheBeforeAfterTest.java +++ b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/StyleCacheBeforeAfterTest.java @@ -29,6 +29,7 @@ int mergeAndCacheStyleOldWay(int currentStyle, String numberingFormat, Font font helper.cacheFont(font), helper.cacheFill(fill), helper.cacheBorder(border), + false, alignment, protection); return styles.computeIfAbsent(s, k -> styles.size()); @@ -49,6 +50,7 @@ int mergeAndCacheStyleNewWay(int currentStyle, String numberingFormat, Font font helper.cacheFont(font), helper.cacheFill(fill), helper.cacheBorder(border), + false, alignment, protection); Integer index = styles.computeIfAbsent(s, k -> styles.size()); diff --git a/fastexcel-writer/src/test/resources/org/dhatim/fastexcel/HelloWorldMacro.xlsm b/fastexcel-writer/src/test/resources/org/dhatim/fastexcel/HelloWorldMacro.xlsm new file mode 100644 index 0000000000000000000000000000000000000000..78cceb611920d603052af318f7900af3e93f8792 GIT binary patch literal 13173 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 literal 0 HcmV?d00001