Skip to content

Commit 9d519ce

Browse files
author
mengzhongyuan
committed
This close #2146, support setting excel custom properties (properties in DocProps/custom.xml)
- support save some biz k:v data to excel custom properties to do such that save excel biz version etc. Change-Id: I4a524b494f09fbfca321d515b5029f4f7361311f Signed-off-by: mengzhongyuan <mengzhongyuan@bytedance.com>
1 parent 5bd9647 commit 9d519ce

File tree

8 files changed

+256
-2
lines changed

8 files changed

+256
-2
lines changed

docProps.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"encoding/xml"
1717
"io"
1818
"reflect"
19+
"strconv"
20+
"time"
1921
)
2022

2123
// SetAppProps provides a function to set document application properties. The
@@ -206,6 +208,123 @@ func (f *File) SetDocProps(docProperties *DocProperties) error {
206208
return err
207209
}
208210

211+
// SetDocCustomProps provides a function to set excel custom properties, support string, bool, float64,
212+
// time.DateTime four types.
213+
func (f *File) SetDocCustomProps(name string, value interface{}) error {
214+
customProps := new(xlsxCustomProperties)
215+
216+
// find existing custom properties
217+
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
218+
Decode(customProps); err != nil && err != io.EOF {
219+
return err
220+
}
221+
222+
props := customProps.Props
223+
existingPropertyMap := make(map[string]xlsxCustomProperty)
224+
maxPID := 1 // pid from 2
225+
for _, prop := range props {
226+
pid, err := strconv.Atoi(prop.PID)
227+
if err == nil && pid > maxPID {
228+
maxPID = pid
229+
}
230+
231+
existingPropertyMap[prop.Name] = prop
232+
}
233+
234+
// different custom property value type setter function
235+
var setValueFunc func(*xlsxCustomProperty) error
236+
switch v := value.(type) {
237+
case float64:
238+
setValueFunc = func(property *xlsxCustomProperty) error {
239+
property.Number = &NumberValue{Number: v}
240+
return nil
241+
}
242+
case bool:
243+
setValueFunc = func(property *xlsxCustomProperty) error {
244+
property.Bool = &BoolValue{Bool: v}
245+
return nil
246+
}
247+
case string:
248+
setValueFunc = func(property *xlsxCustomProperty) error {
249+
property.Text = &TextValue{Text: v}
250+
return nil
251+
}
252+
case time.Time:
253+
setValueFunc = func(property *xlsxCustomProperty) error {
254+
property.DateTime = &FileTimeValue{
255+
DateTime: v.Format(time.RFC3339),
256+
}
257+
return nil
258+
}
259+
default:
260+
setValueFunc = func(_ *xlsxCustomProperty) error {
261+
return ErrUnsupportedCustomPropertyDataType
262+
}
263+
}
264+
265+
// update existing custom properties
266+
if existingProperty, ok := existingPropertyMap[name]; ok {
267+
if err := setValueFunc(&existingProperty); err != nil {
268+
return err
269+
}
270+
} else {
271+
// add new custom property
272+
newProperty := xlsxCustomProperty{
273+
FmtID: CustomPropertiesFMTID,
274+
PID: strconv.FormatInt(int64(maxPID+1), 10), // max pid plus 1 to create a new unique pid
275+
Name: name,
276+
}
277+
278+
if err := setValueFunc(&newProperty); err != nil {
279+
return err
280+
}
281+
282+
props = append(props, newProperty)
283+
}
284+
285+
newCustomProps := &xlsxCustomProperties{
286+
Vt: NameSpaceDocumentPropertiesVariantTypes.Value,
287+
Props: props,
288+
}
289+
290+
output, err := xml.Marshal(newCustomProps)
291+
f.saveFileList(defaultXMLPathDocPropsCustom, output)
292+
293+
// set custom properties if necessary
294+
_ = f.addRels(defaultXMLRels, SourceRelationshipCustomProperties, defaultXMLPathDocPropsCustom, "")
295+
296+
// set content type if necessary
297+
_ = f.setContentTypes("/"+defaultXMLPathDocPropsCustom, ContentTypeCustomProperties)
298+
299+
return err
300+
}
301+
302+
// GetDocCustomProps provides a function to get document custom properties, supported string, bool, float64,
303+
// time.DateTime four types. If the custom property is not set, it will return an empty map, and if the custom property
304+
// value is invalid format, it returns as if the custom property does not exist.
305+
func (f *File) GetDocCustomProps() (kv map[string]interface{}, err error) {
306+
custom := new(xlsxCustomProperties)
307+
308+
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCustom)))).
309+
Decode(custom); err != nil && err != io.EOF {
310+
return
311+
}
312+
313+
kv = make(map[string]interface{})
314+
if custom == nil || len(custom.Props) == 0 {
315+
return kv, nil
316+
}
317+
318+
for _, prop := range custom.Props {
319+
propertyValue := prop.getPropertyValue()
320+
if propertyValue != nil {
321+
kv[prop.Name] = propertyValue
322+
}
323+
}
324+
325+
return kv, nil
326+
}
327+
209328
// GetDocProps provides a function to get document core properties.
210329
func (f *File) GetDocProps() (ret *DocProperties, err error) {
211330
core := new(decodeCoreProperties)

docProps_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package excelize
1414
import (
1515
"path/filepath"
1616
"testing"
17+
"time"
1718

1819
"github.com/stretchr/testify/assert"
1920
)
@@ -97,6 +98,28 @@ func TestSetDocProps(t *testing.T) {
9798
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "XML syntax error on line 1: invalid UTF-8")
9899
}
99100

101+
func TestSetDocCustomProps(t *testing.T) {
102+
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
103+
if !assert.NoError(t, err) {
104+
t.FailNow()
105+
}
106+
107+
assert.NoError(t, f.SetDocCustomProps("string", "v1.0.0"))
108+
assert.NoError(t, f.SetDocCustomProps("bool", true))
109+
assert.EqualError(t, f.SetDocCustomProps("int64", int64(1)), ErrUnsupportedCustomPropertyDataType.Error())
110+
assert.NoError(t, f.SetDocCustomProps("float64", 1.0))
111+
assert.NoError(t, f.SetDocCustomProps("date", time.Now()))
112+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocCustomProps.xlsx")))
113+
f.Pkg.Store(defaultXMLPathDocPropsCustom, nil)
114+
assert.NoError(t, f.SetDocCustomProps("version", ""))
115+
assert.NoError(t, f.Close())
116+
117+
// Test unsupported charset
118+
f = NewFile()
119+
f.Pkg.Store(defaultXMLPathDocPropsCustom, MacintoshCyrillicCharset)
120+
assert.EqualError(t, f.SetDocCustomProps("version", ""), "XML syntax error on line 1: invalid UTF-8")
121+
}
122+
100123
func TestGetDocProps(t *testing.T) {
101124
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
102125
if !assert.NoError(t, err) {
@@ -116,3 +139,34 @@ func TestGetDocProps(t *testing.T) {
116139
_, err = f.GetDocProps()
117140
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
118141
}
142+
143+
func TestFile_GetDocCustomProps(t *testing.T) {
144+
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
145+
if !assert.NoError(t, err) {
146+
t.FailNow()
147+
}
148+
149+
// now no custom properties in f
150+
props, err := f.GetDocCustomProps()
151+
assert.NoError(t, err)
152+
assert.Empty(t, props)
153+
154+
assert.NoError(t, f.SetDocCustomProps("string", "v1.0.0"))
155+
assert.NoError(t, f.SetDocCustomProps("bool", true))
156+
assert.NoError(t, f.SetDocCustomProps("float64", 1.0))
157+
dateValue := time.Date(2006, 01, 02, 15, 04, 05, 0, time.Local)
158+
assert.NoError(t, f.SetDocCustomProps("date", dateValue))
159+
160+
props, err = f.GetDocCustomProps()
161+
assert.NoError(t, err)
162+
assert.Equal(t, "v1.0.0", props["string"])
163+
assert.Equal(t, true, props["bool"])
164+
assert.Equal(t, 1.0, props["float64"])
165+
assert.Equal(t, dateValue, props["date"])
166+
167+
// Test get workbook properties with unsupported charset
168+
f = NewFile()
169+
f.Pkg.Store(defaultXMLPathDocPropsCustom, MacintoshCyrillicCharset)
170+
_, err = f.GetDocCustomProps()
171+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
172+
}

errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ var (
178178
// ErrWorkbookPassword defined the error message on receiving the incorrect
179179
// workbook password.
180180
ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
181+
// ErrUnsupportedCustomPropertyDataType defined the error message on unsupported custom property value data type.
182+
ErrUnsupportedCustomPropertyDataType = errors.New("unsupported custom property value data type")
181183
)
182184

183185
// ErrSheetNotExist defined an error of sheet that does not exist.

excelize.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,8 @@ func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
419419
// relationship type, target and target mode.
420420
func (f *File) addRels(relPath, relType, target, targetMode string) int {
421421
uniqPart := map[string]string{
422-
SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
422+
SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
423+
SourceRelationshipCustomProperties: "/docProps/custom.xml",
423424
}
424425
rels, _ := f.relsReader(relPath)
425426
if rels == nil {

file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131
// f := NewFile()
3232
func NewFile(opts ...Options) *File {
3333
f := newFile()
34-
f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
34+
f.Pkg.Store(defaultXMLRels, []byte(xml.Header+templateRels))
3535
f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
3636
f.Pkg.Store(defaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore))
3737
f.Pkg.Store(defaultXMLPathWorkbookRels, []byte(xml.Header+templateWorkbookRels))

sheet.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ func (f *File) setContentTypes(partName, contentType string) error {
239239
}
240240
content.mu.Lock()
241241
defer content.mu.Unlock()
242+
243+
// if target partName exists, update it
244+
for i, v := range content.Overrides {
245+
if v.PartName == partName {
246+
content.Overrides[i].ContentType = contentType
247+
return nil
248+
}
249+
}
250+
242251
content.Overrides = append(content.Overrides, xlsxOverride{
243252
PartName: partName,
244253
ContentType: contentType,

templates.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const (
5959
ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
6060
ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
6161
ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
62+
ContentTypeCustomProperties = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
6263
ContentTypeVBA = "application/vnd.ms-office.vbaProject"
6364
ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
6465
NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
@@ -86,6 +87,7 @@ const (
8687
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
8788
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
8889
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
90+
SourceRelationshipCustomProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
8991
StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
9092
StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
9193
StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
@@ -132,6 +134,9 @@ const (
132134
ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
133135
ExtURIWorkbookPrX14 = "{79F54976-1DA5-4618-B147-ACDE4B953A38}"
134136
ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}"
137+
138+
// CustomPropertiesFMTID is the format ID for custom properties items.
139+
CustomPropertiesFMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
135140
)
136141

137142
// workbookExtURIPriority is the priority of URI in the workbook extension lists.
@@ -270,13 +275,15 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp
270275
}
271276

272277
const (
278+
defaultXMLRels = "_rels/.rels"
273279
defaultTempFileSST = "sharedStrings"
274280
defaultXMLMetadata = "xl/metadata.xml"
275281
defaultXMLPathCalcChain = "xl/calcChain.xml"
276282
defaultXMLPathCellImages = "xl/cellimages.xml"
277283
defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels"
278284
defaultXMLPathContentTypes = "[Content_Types].xml"
279285
defaultXMLPathDocPropsApp = "docProps/app.xml"
286+
defaultXMLPathDocPropsCustom = "docProps/custom.xml"
280287
defaultXMLPathDocPropsCore = "docProps/core.xml"
281288
defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
282289
defaultXMLPathStyles = "xl/styles.xml"

xmlCustom.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package excelize
2+
3+
import (
4+
"encoding/xml"
5+
"time"
6+
)
7+
8+
// xlsxCustomProperties is the root element of the Custom File Properties part
9+
type xlsxCustomProperties struct {
10+
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/custom-properties Properties"`
11+
Vt string `xml:"xmlns:vt,attr"`
12+
Props []xlsxCustomProperty `xml:"property"`
13+
}
14+
15+
type xlsxCustomProperty struct {
16+
FmtID string `xml:"fmtid,attr"`
17+
PID string `xml:"pid,attr"`
18+
Name string `xml:"name,attr"`
19+
Text *TextValue `xml:"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes lpwstr,omitempty"`
20+
Bool *BoolValue `xml:"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes bool,omitempty"`
21+
Number *NumberValue `xml:"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes r8,omitempty"`
22+
DateTime *FileTimeValue `xml:"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes filetime,omitempty"`
23+
}
24+
25+
func (p *xlsxCustomProperty) getPropertyValue() interface{} {
26+
if p.Text != nil {
27+
return p.Text.Text
28+
}
29+
if p.Bool != nil {
30+
return p.Bool.Bool
31+
}
32+
if p.Number != nil {
33+
return p.Number.Number
34+
}
35+
if p.DateTime != nil {
36+
// parse date time raw str to time.Time
37+
timeStr := p.DateTime.DateTime
38+
parsedTime, err := time.Parse(time.RFC3339, timeStr)
39+
if err != nil {
40+
return nil
41+
}
42+
return parsedTime
43+
}
44+
45+
return nil
46+
}
47+
48+
type TextValue struct {
49+
Text string `xml:",chardata"`
50+
}
51+
52+
type BoolValue struct {
53+
Bool bool `xml:",chardata"`
54+
}
55+
56+
type NumberValue struct {
57+
Number float64 `xml:",chardata"`
58+
}
59+
60+
type FileTimeValue struct {
61+
DateTime string `xml:",chardata"`
62+
}

0 commit comments

Comments
 (0)