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 00000000..78cceb61 Binary files /dev/null and b/fastexcel-writer/src/test/resources/org/dhatim/fastexcel/HelloWorldMacro.xlsm differ