diff --git a/docProps.go b/docProps.go
index 975f51b0a2..49cbdaad61 100644
--- a/docProps.go
+++ b/docProps.go
@@ -16,6 +16,8 @@ import (
"encoding/xml"
"io"
"reflect"
+ "slices"
+ "time"
)
// SetAppProps provides a function to set document application properties. The
@@ -236,3 +238,117 @@ func (f *File) GetDocProps() (ret *DocProperties, err error) {
}
return
}
+
+// SetCustomProps provides a function to set custom file properties by given
+// property name and value. If the property name already exists, it will be
+// updated, otherwise a new property will be added. The value can be of type
+// int32, float64, bool, string, time.Time or nil. The property will be delete
+// if the value is nil. The function returns an error if the property value is
+// not of the correct type.
+func (f *File) SetCustomProps(prop CustomProperty) error {
+ if prop.Name == "" {
+ return ErrParameterInvalid
+ }
+ props := new(decodeCustomProperties)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
+ Decode(props); err != nil && err != io.EOF {
+ return err
+ }
+ customProps := xlsxCustomProperties{Vt: NameSpaceDocumentPropertiesVariantTypes.Value}
+ idx, pID := -1, 1
+ for i := range props.Property {
+ p := new(xlsxProperty)
+ setPtrFields(reflect.ValueOf(&props.Property[i]).Elem(), reflect.ValueOf(p).Elem())
+ if pID < props.Property[i].PID {
+ pID = props.Property[i].PID
+ }
+ if props.Property[i].Name == prop.Name {
+ idx = i
+ }
+ customProps.Property = append(customProps.Property, *p)
+ }
+ if idx != -1 && prop.Value == nil {
+ customProps.Property = slices.Delete(customProps.Property, idx, idx+1)
+ }
+ if prop.Value != nil {
+ property := xlsxProperty{Name: prop.Name, FmtID: EXtURICustomPropertyFmtID}
+ if err := property.setCustomProps(prop.Value); err != nil {
+ return err
+ }
+ if idx != -1 {
+ property.PID = customProps.Property[idx].PID
+ customProps.Property[idx] = property
+ } else {
+ property.PID = pID + 1
+ customProps.Property = append(customProps.Property, property)
+ }
+ }
+ _ = f.addRels(defaultXMLPathRels, SourceRelationshipCustomProperties, defaultXMLPathDocPropsCustom, "")
+ if err := f.addContentTypePart(0, "customProperties"); err != nil {
+ return err
+ }
+ output, err := xml.Marshal(customProps)
+ f.saveFileList(defaultXMLPathDocPropsCustom, output)
+ return err
+}
+
+// setCustomProps sets the custom property value based on its type.
+func (prop *xlsxProperty) setCustomProps(value interface{}) error {
+ switch v := value.(type) {
+ case int32:
+ prop.I4 = &v
+ case float64:
+ prop.R8 = float64Ptr(v)
+ case bool:
+ prop.Bool = boolPtr(v)
+ case string:
+ prop.Lpwstr = stringPtr(value.(string))
+ case time.Time:
+ prop.FileTime = stringPtr(value.(time.Time).Format(time.RFC3339))
+ default:
+ return ErrParameterInvalid
+ }
+ return nil
+}
+
+// GetCustomProps provides a function to get custom file properties.
+func (f *File) GetCustomProps() ([]CustomProperty, error) {
+ var customProps []CustomProperty
+ props := new(decodeCustomProperties)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
+ Decode(props); err != nil && err != io.EOF {
+ return customProps, err
+ }
+ for _, p := range props.Property {
+ prop := CustomProperty{Name: p.Name}
+ var err error
+ if prop.Value, err = p.getCustomProps(); err != nil {
+ return customProps, err
+ }
+ customProps = append(customProps, prop)
+ }
+ return customProps, nil
+}
+
+// getCustomProps gets the custom property value based on its type.
+func (p *decodeProperty) getCustomProps() (interface{}, error) {
+ s := reflect.ValueOf(p).Elem()
+ for i := range s.NumField() {
+ if 11 <= i && i <= 20 && !s.Field(i).IsNil() {
+ return int32(s.Field(i).Elem().Int()), nil // Field vt:i1 to vt:uint
+ }
+ if 21 <= i && i <= 22 && !s.Field(i).IsNil() {
+ return s.Field(i).Elem().Float(), nil // Field vt:r4 to vt:r8
+ }
+ if p.Bool != nil {
+ return *p.Bool, nil
+ }
+ if p.Lpwstr != nil {
+ return *p.Lpwstr, nil
+ }
+ if p.FileTime != nil {
+ return time.Parse(time.RFC3339, *p.FileTime)
+ }
+ }
+ return nil, nil
+}
diff --git a/docProps_test.go b/docProps_test.go
index dc26e2b285..014b3778c8 100644
--- a/docProps_test.go
+++ b/docProps_test.go
@@ -12,8 +12,11 @@
package excelize
import (
+ "fmt"
"path/filepath"
+ "slices"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
)
@@ -116,3 +119,70 @@ func TestGetDocProps(t *testing.T) {
_, err = f.GetDocProps()
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
+
+func TestCustomProps(t *testing.T) {
+ f := NewFile()
+ expected := []CustomProperty{
+ {Name: "Text Prop", Value: "text"},
+ {Name: "Boolean Prop 1", Value: true},
+ {Name: "Boolean Prop 2", Value: false},
+ {Name: "Number Prop 1", Value: -123.456},
+ {Name: "Number Prop 2", Value: int32(1)},
+ {Name: "Date Prop", Value: time.Date(2021, time.September, 11, 0, 0, 0, 0, time.UTC)},
+ }
+ for _, prop := range expected {
+ assert.NoError(t, f.SetCustomProps(prop))
+ }
+ props, err := f.GetCustomProps()
+ assert.NoError(t, err)
+ assert.Equal(t, expected, props)
+
+ // Test delete custom property
+ assert.NoError(t, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 1", Value: nil}))
+ props, err = f.GetCustomProps()
+ assert.NoError(t, err)
+ expected = slices.Delete(expected, 1, 2)
+ assert.Equal(t, expected, props)
+
+ // Test change custom property value data type
+ assert.NoError(t, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 2", Value: "true"}))
+ props, err = f.GetCustomProps()
+ assert.NoError(t, err)
+ assert.Equal(t, props[1].Value, "true")
+
+ // Test set custom property with unsupported value data type
+ assert.Equal(t, ErrParameterInvalid, f.SetCustomProps(CustomProperty{Name: "Boolean Prop 2", Value: 1}))
+
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCustomProps.xlsx")))
+ assert.NoError(t, f.Close())
+
+ // Test set custom property without property name
+ f = NewFile()
+ assert.Equal(t, ErrParameterInvalid, f.SetCustomProps(CustomProperty{}))
+
+ // Test set custom property with unsupported charset
+ f.Pkg.Store(defaultXMLPathDocPropsCustom, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCustomProps(CustomProperty{Name: "Prop"}), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test get custom property with unsupported charset
+ _, err = f.GetCustomProps()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ // Test set custom property with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCustomProps(CustomProperty{Name: "Prop"}), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test get custom property with unsupported charset
+ f.Pkg.Store(defaultXMLPathDocPropsCustom, []byte(fmt.Sprintf(`x`, NameSpaceDocumentPropertiesVariantTypes, EXtURICustomPropertyFmtID)))
+ _, err = f.GetCustomProps()
+ assert.EqualError(t, err, "parsing time \"x\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"x\" as \"2006\"")
+
+ // Test get custom property with unsupported value data type
+ f.Pkg.Store(defaultXMLPathDocPropsCustom, []byte(fmt.Sprintf(``, NameSpaceDocumentPropertiesVariantTypes, EXtURICustomPropertyFmtID)))
+ props, err = f.GetCustomProps()
+ assert.Equal(t, []CustomProperty{{Name: "Prop"}}, props)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+}
diff --git a/excelize.go b/excelize.go
index 61bb6d3489..52b7fa33fb 100644
--- a/excelize.go
+++ b/excelize.go
@@ -419,7 +419,8 @@ func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
// relationship type, target and target mode.
func (f *File) addRels(relPath, relType, target, targetMode string) int {
uniqPart := map[string]string{
- SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
+ SourceRelationshipCustomProperties: "/docProps/custom.xml",
+ SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
}
rels, _ := f.relsReader(relPath)
if rels == nil {
diff --git a/file.go b/file.go
index 1ef8a8a5fb..03036c9163 100644
--- a/file.go
+++ b/file.go
@@ -31,7 +31,7 @@ import (
// f := NewFile()
func NewFile(opts ...Options) *File {
f := newFile()
- f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
+ f.Pkg.Store(defaultXMLPathRels, []byte(xml.Header+templateRels))
f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
f.Pkg.Store(defaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore))
f.Pkg.Store(defaultXMLPathWorkbookRels, []byte(xml.Header+templateWorkbookRels))
diff --git a/lib.go b/lib.go
index 4cd0552ae1..7eeb813a19 100644
--- a/lib.go
+++ b/lib.go
@@ -892,6 +892,19 @@ func assignFieldValue(field string, immutable, mutable reflect.Value) {
}
}
+// setPtrFields assigns the fields of the immutable struct to the mutable
+// struct. The fields name of the immutable struct must match the field names of
+// the mutable struct.
+func setPtrFields(immutable, mutable reflect.Value) {
+ for i := range immutable.NumField() {
+ srcField := immutable.Type().Field(i)
+ dstField := mutable.FieldByName(srcField.Name)
+ if dstField.IsValid() && dstField.CanSet() && dstField.Type() == immutable.Field(i).Type() {
+ dstField.Set(immutable.Field(i))
+ }
+ }
+}
+
// setNoPtrFieldsVal assigns values from the pointer or no-pointer structs
// fields (immutable) value to no-pointer struct field.
func setNoPtrFieldsVal(fields []string, immutable, mutable reflect.Value) {
diff --git a/sheet_test.go b/sheet_test.go
index d5cc4cfa7b..dc82346b8c 100644
--- a/sheet_test.go
+++ b/sheet_test.go
@@ -524,14 +524,14 @@ func TestWorksheetWriter(t *testing.T) {
func TestGetWorkbookPath(t *testing.T) {
f := NewFile()
- f.Pkg.Delete("_rels/.rels")
+ f.Pkg.Delete(defaultXMLPathRels)
assert.Empty(t, f.getWorkbookPath())
}
func TestGetWorkbookRelsPath(t *testing.T) {
f := NewFile()
f.Pkg.Delete("xl/_rels/.rels")
- f.Pkg.Store("_rels/.rels", []byte(xml.Header+``))
+ f.Pkg.Store(defaultXMLPathRels, []byte(xml.Header+``))
assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath())
}
diff --git a/templates.go b/templates.go
index b8cf159131..bdcb375cec 100644
--- a/templates.go
+++ b/templates.go
@@ -43,6 +43,7 @@ var (
// Source relationship and namespace.
const (
ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
+ ContentTypeCustomProperties = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
@@ -71,6 +72,7 @@ const (
SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+ SourceRelationshipCustomProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
@@ -102,6 +104,7 @@ const (
ExtURICalcFeatures = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"
ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
+ EXtURICustomPropertyFmtID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
ExtURIDataField = "{E15A36E0-9728-4E99-A89B-3F7291B0FE68}"
ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}"
ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
@@ -278,6 +281,8 @@ const (
defaultXMLPathContentTypes = "[Content_Types].xml"
defaultXMLPathDocPropsApp = "docProps/app.xml"
defaultXMLPathDocPropsCore = "docProps/core.xml"
+ defaultXMLPathDocPropsCustom = "docProps/custom.xml"
+ defaultXMLPathRels = "_rels/.rels"
defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
defaultXMLPathStyles = "xl/styles.xml"
defaultXMLPathTheme = "xl/theme/theme1.xml"
diff --git a/workbook.go b/workbook.go
index d1cb1da45b..d05acba918 100644
--- a/workbook.go
+++ b/workbook.go
@@ -198,7 +198,7 @@ func (f *File) setWorkbook(name string, sheetID, rid int) {
// getWorkbookPath provides a function to get the path of the workbook.xml in
// the spreadsheet.
func (f *File) getWorkbookPath() (path string) {
- if rels, _ := f.relsReader("_rels/.rels"); rels != nil {
+ if rels, _ := f.relsReader(defaultXMLPathRels); rels != nil {
rels.mu.Lock()
defer rels.mu.Unlock()
for _, rel := range rels.Relationships {
@@ -364,28 +364,30 @@ func (f *File) addContentTypePart(index int, contentType string) error {
"drawings": f.setContentTypePartImageExtensions,
}
partNames := map[string]string{
- "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
- "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
- "comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
- "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
- "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
- "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
- "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
- "sharedStrings": "/xl/sharedStrings.xml",
- "slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
- "slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
+ "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
+ "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
+ "comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
+ "customProperties": "/docProps/custom.xml",
+ "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
+ "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
+ "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
+ "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
+ "sharedStrings": "/xl/sharedStrings.xml",
+ "slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
+ "slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
}
contentTypes := map[string]string{
- "chart": ContentTypeDrawingML,
- "chartsheet": ContentTypeSpreadSheetMLChartsheet,
- "comments": ContentTypeSpreadSheetMLComments,
- "drawings": ContentTypeDrawing,
- "table": ContentTypeSpreadSheetMLTable,
- "pivotTable": ContentTypeSpreadSheetMLPivotTable,
- "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
- "sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
- "slicer": ContentTypeSlicer,
- "slicerCache": ContentTypeSlicerCache,
+ "chart": ContentTypeDrawingML,
+ "chartsheet": ContentTypeSpreadSheetMLChartsheet,
+ "comments": ContentTypeSpreadSheetMLComments,
+ "customProperties": ContentTypeCustomProperties,
+ "drawings": ContentTypeDrawing,
+ "table": ContentTypeSpreadSheetMLTable,
+ "pivotTable": ContentTypeSpreadSheetMLPivotTable,
+ "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
+ "sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
+ "slicer": ContentTypeSlicer,
+ "slicerCache": ContentTypeSlicerCache,
}
s, ok := setContentType[contentType]
if ok {
diff --git a/xmlCustom.go b/xmlCustom.go
new file mode 100644
index 0000000000..c0c1c5211d
--- /dev/null
+++ b/xmlCustom.go
@@ -0,0 +1,127 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excelâ„¢ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import "encoding/xml"
+
+// xlsxCustomProperties directly maps the element for the custom file properties
+// part, that represents additional information. The information can be used as
+// metadata for XML.
+type xlsxCustomProperties struct {
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties Properties"`
+ Vt string `xml:"xmlns:vt,attr"`
+ Property []xlsxProperty `xml:"property"`
+}
+
+// xlsxProperty directly maps the element specifies a single custom file
+// property. Custom file property type is defined through child elements in the
+// File Properties Variant Type namespace. Custom file property value can be set
+// by setting the appropriate Variant Type child element value.
+type xlsxProperty struct {
+ XMLName xml.Name `xml:"property"`
+ FmtID string `xml:"fmtid,attr"`
+ PID int `xml:"pid,attr"`
+ Name string `xml:"name,attr,omitempty"`
+ LinkTarget string `xml:"linkTarget,attr,omitempty"`
+ Vector *string `xml:"vt:vector"`
+ Array *string `xml:"vt:array"`
+ Blob *string `xml:"vt:blob"`
+ Oblob *string `xml:"vt:oblob"`
+ Empty *string `xml:"vt:empty"`
+ Null *string `xml:"vt:null"`
+ I1 *int8 `xml:"vt:i1"`
+ I2 *int16 `xml:"vt:i2"`
+ I4 *int32 `xml:"vt:i4"`
+ I8 *int64 `xml:"vt:i8"`
+ Int *int `xml:"vt:int"`
+ Ui1 *uint8 `xml:"vt:ui1"`
+ Ui2 *uint16 `xml:"vt:ui2"`
+ Ui4 *uint32 `xml:"vt:ui4"`
+ Ui8 *uint64 `xml:"vt:ui8"`
+ Uint *uint `xml:"vt:uint"`
+ R4 *float32 `xml:"vt:r4"`
+ R8 *float64 `xml:"vt:r8"`
+ Decimal *string `xml:"vt:decimal"`
+ Lpstr *string `xml:"vt:lpstr"`
+ Lpwstr *string `xml:"vt:lpwstr"`
+ Bstr *string `xml:"vt:bstr"`
+ Date *string `xml:"vt:date"`
+ FileTime *string `xml:"vt:filetime"`
+ Bool *bool `xml:"vt:bool"`
+ Cy *string `xml:"vt:cy"`
+ Error *string `xml:"vt:error"`
+ Stream *string `xml:"vt:stream"`
+ Ostream *string `xml:"vt:ostream"`
+ Storage *string `xml:"vt:storage"`
+ Ostorage *string `xml:"vt:ostorage"`
+ Vstream *string `xml:"vt:vstream"`
+ ClsID *string `xml:"vt:clsid"`
+}
+
+// decodeCustomProperties specifies to an OOXML document custom properties.
+// decodeCustomProperties just for deserialization.
+type decodeCustomProperties struct {
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties Properties"`
+ Vt string `xml:"xmlns:vt,attr"`
+ Property []decodeProperty `xml:"property"`
+}
+
+// decodeProperty specifies to an OOXML document custom property. decodeProperty
+// just for deserialization.
+type decodeProperty struct {
+ XMLName xml.Name `xml:"property"`
+ FmtID string `xml:"fmtid,attr"`
+ PID int `xml:"pid,attr"`
+ Name string `xml:"name,attr,omitempty"`
+ LinkTarget string `xml:"linkTarget,attr,omitempty"`
+ Vector *string `xml:"vector"`
+ Array *string `xml:"array"`
+ Blob *string `xml:"blob"`
+ Oblob *string `xml:"oblob"`
+ Empty *string `xml:"empty"`
+ Null *string `xml:"null"`
+ I1 *int8 `xml:"i1"`
+ I2 *int16 `xml:"i2"`
+ I4 *int32 `xml:"i4"`
+ I8 *int64 `xml:"i8"`
+ Int *int `xml:"int"`
+ Ui1 *uint8 `xml:"ui1"`
+ Ui2 *uint16 `xml:"ui2"`
+ Ui4 *uint32 `xml:"ui4"`
+ Ui8 *uint64 `xml:"ui8"`
+ Uint *uint `xml:"uint"`
+ R4 *float32 `xml:"r4"`
+ R8 *float64 `xml:"r8"`
+ Decimal *string `xml:"decimal"`
+ Lpstr *string `xml:"lpstr"`
+ Lpwstr *string `xml:"lpwstr"`
+ Bstr *string `xml:"bstr"`
+ Date *string `xml:"date"`
+ FileTime *string `xml:"filetime"`
+ Bool *bool `xml:"bool"`
+ Cy *string `xml:"cy"`
+ Error *string `xml:"error"`
+ Stream *string `xml:"stream"`
+ Ostream *string `xml:"ostream"`
+ Storage *string `xml:"storage"`
+ Ostorage *string `xml:"ostorage"`
+ Vstream *string `xml:"vstream"`
+ ClsID *string `xml:"clsid"`
+}
+
+// CustomProperty directly maps the custom property of the workbook. The value
+// date type may be one of the following: int32, float64, string, bool,
+// time.Time, or nil.
+type CustomProperty struct {
+ Name string
+ Value interface{}
+}