Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ abstract class GenericStyleSetter<STYLE_SETTER extends GenericStyleSetter<STYLE_
* Border.
*/
private Border border;
/**
* Whether the cell is rendered as a checkbox.
*/
private boolean checkbox;

/**
* Protection options.
Expand Down Expand Up @@ -424,6 +428,19 @@ public STYLE_SETTER borderColor(BorderSide side, String borderColor) {
return borderElement(side, border.elements.get(side).updateColor(borderColor));
}

/**
* Set the checkbox property. If it set to true, client applications supporting it (Excel 2024+) will render
* the cell as a checkbox.
*
* @param checkbox Whether a checkbox should be rendered in the cell.
* @return This style setter.
*/
public STYLE_SETTER checkbox(boolean checkbox) {
this.checkbox = checkbox;
worksheet.getWorkbook().addFeaturePropertyBag();
return getThis();
}

/**
* Set cell diagonal property.
*
Expand Down Expand Up @@ -499,7 +516,7 @@ protected void setStyle(boolean shadingEnabled, Set<Integer> currentStyles,
}

// Compute a map giving new styles for current styles
Map<Integer, Integer> newStyles = currentStyles.stream().collect(Collectors.toMap(Function.identity(), s -> worksheet.getWorkbook().mergeAndCacheStyle(s, valueFormatting, font, fill, border, alignment, protection)));
Map<Integer, Integer> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
25 changes: 21 additions & 4 deletions fastexcel-writer/src/main/java/org/dhatim/fastexcel/Style.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -116,6 +125,14 @@ void write(Writer w) throws IOException {
}

w.append('>');
if (checkbox) {
w
.append("<extLst>")
.append("<ext xmlns:xfpb=\"http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag\" uri=\"{C7286773-470A-42A8-94C5-96B5CB345126}\">")
.append("<xfpb:xfComplement i=\"0\"/>")
.append("</ext>")
.append("</extLst>");
}
if (alignment != null) {
alignment.write(w);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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());
}

Expand Down
124 changes: 116 additions & 8 deletions fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +33,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;


/**
Expand All @@ -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<Worksheet> worksheets = new ArrayList<>();
Expand Down Expand Up @@ -186,7 +194,13 @@ public void finish() throws IOException {
w.append("<Default Extension=\"").append(imageType.getExtension())
.append("\" ContentType=\"").append(imageType.getContentType()).append("\"/>");
}
w.append("<Override PartName=\"/xl/sharedStrings.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\"/><Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/><Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>");
w.append("<Override PartName=\"/xl/sharedStrings.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\"/><Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>");
if (hasMacros()) {
w.append("<Override PartName=\"/xl/vbaProject.bin\" ContentType=\"application/vnd.ms-office.vbaProject\"/>");
w.append("<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.ms-excel.sheet.macroEnabled.main+xml\"/>");
} else {
w.append("<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>");
}
for (Worksheet ws : worksheets) {
int index = getIndex(ws);
w.append("<Override PartName=\"/xl/worksheets/sheet").append(index).append(".xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>");
Expand All @@ -210,6 +224,9 @@ public void finish() throws IOException {
if (properties.hasCustomProperties()) {
w.append("<Override PartName=\"/docProps/custom.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.custom-properties+xml\"/>");
}
if (featurePropertyBag) {
w.append("<Override PartName=\"/xl/featurePropertyBag/featurePropertyBag.xml\" ContentType=\"application/vnd.ms-excel.featurepropertybag+xml\"/>");
}
w.append("</Types>");
});
writeProperties();
Expand All @@ -220,26 +237,46 @@ public void finish() throws IOException {
writeFile("_rels/.rels", w -> {
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
w.append("<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">");
w.append("<Relationship Id=\"rId3\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\" Target=\"docProps/app.xml\"/>");
w.append("<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\" Target=\"docProps/core.xml\"/>");
w.append("<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>");
if (properties.hasCustomProperties()) {
w.append("<Relationship Id=\"rId4\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties\" Target=\"docProps/custom.xml\"/>");
}
w.append("<Relationship Id=\"rId3\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\" Target=\"docProps/app.xml\"/>");
w.append("<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\" Target=\"docProps/core.xml\"/>");
w.append("<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>");
w.append("</Relationships>");
});

if (featurePropertyBag) {
writeFile("xl/featurePropertyBag/featurePropertyBag.xml", w -> {
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
w.append("<FeaturePropertyBags xmlns=\"http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag\">");
w.append("<bag type=\"Checkbox\"/><bag type=\"XFControls\"><bagId k=\"CellControl\">0</bagId></bag><bag type=\"XFComplement\"><bagId k=\"XFControls\">1</bagId></bag><bag type=\"XFComplements\" extRef=\"XFComplementsMapperExtRef\"><a k=\"MappedFeaturePropertyBags\"><bagId>2</bagId></a></bag>");
w.append("</FeaturePropertyBags>");
});
}

writeWorkbookFile();

writeFile("xl/_rels/workbook.xml.rels", w -> {
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Target=\"sharedStrings.xml\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\"/><Relationship Id=\"rId2\" Target=\"styles.xml\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\"/>");
int rels = 3;
for (Worksheet ws : worksheets) {
w.append("<Relationship Id=\"rId").append(getIndex(ws) + 2).append("\" Target=\"worksheets/sheet").append(getIndex(ws)).append(".xml\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\"/>");
w.append("<Relationship Id=\"rId").append(rels++).append("\" Target=\"worksheets/sheet").append(getIndex(ws)).append(".xml\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\"/>");
}
if (featurePropertyBag) {
w.append("<Relationship Id=\"rId").append(rels).append("\" Type=\"http://schemas.microsoft.com/office/2022/11/relationships/FeaturePropertyBag\" Target=\"featurePropertyBag/featurePropertyBag.xml\"/>");
}
if (hasMacros()) {
w.append("<Relationship Id=\"rId").append(rels).append("\" Target=\"vbaProject.bin\" Type=\"http://schemas.microsoft.com/office/2006/relationships/vbaProject\"/>");
}
w.append("</Relationships>");
});
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;
}
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -353,7 +397,7 @@ private void writeWorkbookFile() throws IOException {
"<workbook " +
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
"<workbookPr date1904=\"false\"/>");
"<workbookPr date1904=\"false\" codeName=\"" + codeName + "\"/>");

if (workbookPasswordHash != null) {
w.append("<workbookProtection workbookPassword=\"")
Expand Down Expand Up @@ -506,8 +550,8 @@ CachedString cacheString(String s) {
* @param alignment Alignment attributes.
* @return Cached style index.
*/
int mergeAndCacheStyle(int currentStyle, String numberingFormat, Font font, Fill fill, Border border, Alignment alignment, Protection protection) {
return styleCache.mergeAndCacheStyle(currentStyle, numberingFormat, font, fill, border, alignment, protection);
int mergeAndCacheStyle(int currentStyle, String numberingFormat, Font font, Fill fill, Border border, boolean checkbox, Alignment alignment, Protection protection) {
return styleCache.mergeAndCacheStyle(currentStyle, numberingFormat, font, fill, border, checkbox, alignment, protection);
}

/**
Expand Down Expand Up @@ -569,7 +613,71 @@ public Worksheet newWorksheet(String name) {
}
}

/**
* Embed a macro into this workbook
* @param vbaProject the byte array containing the vbaProject.bin file
*/
private void embedMacro(byte[] vbaProject) {
this.vbaProject = Objects.requireNonNull(vbaProject);
}

/**
*
* @param input the input stream to copy macros from
* @throws IllegalArgumentException thrown if file is not a valid XLSX file
* @throws NullPointerException if file is null
*/
public void copyMacrosFromInputStream(InputStream input) throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(input);
try (ZipInputStream zis = new ZipInputStream(input)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if ("xl/vbaProject.bin".equals(entry.getName())) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = zis.read(buffer)) > 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -1112,7 +1122,11 @@ public void flush() throws IOException {
writer = workbook.beginFile("xl/worksheets/sheet" + index + ".xml");
writer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
writer.append("<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">");
writer.append("<sheetPr filterMode=\"" + "false" + "\">");
if (codeName == null) {
writer.append("<sheetPr filterMode=\"" + "false" + "\">");
} else {
writer.append("<sheetPr filterMode=\"" + "false" + "\" codeName=\"" + codeName + "\">");
}
if (tabColor != null) {
writer.append("<tabColor rgb=\"" + tabColor + "\"/>");
}
Expand Down
Loading
Loading