Skip to content

Commit 89b8593

Browse files
committed
This closes qax-os#1096, memory usage optimization and another 4 changes
- Unzip shared string table to system temporary file when large inner XML, reduce memory usage about 70% - Remove unnecessary exported variable `XMLHeader`, we can using `encoding/xml` package's `xml.Header` instead of it - Using constant instead of inline text for default XML path - Rename exported option field `WorksheetUnzipMemLimit` to `UnzipXMLSizeLimit` - Unit test and documentation updated
1 parent 6b1e592 commit 89b8593

19 files changed

+260
-144
lines changed

calcchain.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (f *File) calcChainReader() *xlsxCalcChain {
2525

2626
if f.CalcChain == nil {
2727
f.CalcChain = new(xlsxCalcChain)
28-
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))).
28+
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathCalcChain)))).
2929
Decode(f.CalcChain); err != nil && err != io.EOF {
3030
log.Printf("xml decode error: %s", err)
3131
}
@@ -39,7 +39,7 @@ func (f *File) calcChainReader() *xlsxCalcChain {
3939
func (f *File) calcChainWriter() {
4040
if f.CalcChain != nil && f.CalcChain.C != nil {
4141
output, _ := xml.Marshal(f.CalcChain)
42-
f.saveFileList("xl/calcChain.xml", output)
42+
f.saveFileList(dafaultXMLPathCalcChain, output)
4343
}
4444
}
4545

@@ -54,7 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) {
5454
}
5555
if len(calc.C) == 0 {
5656
f.CalcChain = nil
57-
f.Pkg.Delete("xl/calcChain.xml")
57+
f.Pkg.Delete(dafaultXMLPathCalcChain)
5858
content := f.contentTypesReader()
5959
content.Lock()
6060
defer content.Unlock()

calcchain_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import "testing"
55
func TestCalcChainReader(t *testing.T) {
66
f := NewFile()
77
f.CalcChain = nil
8-
f.Pkg.Store("xl/calcChain.xml", MacintoshCyrillicCharset)
8+
f.Pkg.Store(dafaultXMLPathCalcChain, MacintoshCyrillicCharset)
99
f.calcChainReader()
1010
}
1111

cell.go

+60-30
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package excelize
1414
import (
1515
"encoding/xml"
1616
"fmt"
17+
"os"
1718
"reflect"
1819
"strconv"
1920
"strings"
@@ -348,36 +349,57 @@ func (f *File) SetCellStr(sheet, axis, value string) error {
348349
ws.Lock()
349350
defer ws.Unlock()
350351
cellData.S = f.prepareCellStyle(ws, col, cellData.S)
351-
cellData.T, cellData.V = f.setCellString(value)
352+
cellData.T, cellData.V, err = f.setCellString(value)
352353
return err
353354
}
354355

355356
// setCellString provides a function to set string type to shared string
356357
// table.
357-
func (f *File) setCellString(value string) (t string, v string) {
358+
func (f *File) setCellString(value string) (t, v string, err error) {
358359
if len(value) > TotalCellChars {
359360
value = value[:TotalCellChars]
360361
}
361362
t = "s"
362-
v = strconv.Itoa(f.setSharedString(value))
363+
var si int
364+
if si, err = f.setSharedString(value); err != nil {
365+
return
366+
}
367+
v = strconv.Itoa(si)
368+
return
369+
}
370+
371+
// sharedStringsLoader load shared string table from system temporary file to
372+
// memory, and reset shared string table for reader.
373+
func (f *File) sharedStringsLoader() (err error) {
374+
f.Lock()
375+
defer f.Unlock()
376+
if path, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok {
377+
f.Pkg.Store(dafaultXMLPathSharedStrings, f.readBytes(dafaultXMLPathSharedStrings))
378+
f.tempFiles.Delete(dafaultXMLPathSharedStrings)
379+
err = os.Remove(path.(string))
380+
f.SharedStrings, f.sharedStringItemMap = nil, nil
381+
}
363382
return
364383
}
365384

366385
// setSharedString provides a function to add string to the share string table.
367-
func (f *File) setSharedString(val string) int {
386+
func (f *File) setSharedString(val string) (int, error) {
387+
if err := f.sharedStringsLoader(); err != nil {
388+
return 0, err
389+
}
368390
sst := f.sharedStringsReader()
369391
f.Lock()
370392
defer f.Unlock()
371393
if i, ok := f.sharedStringsMap[val]; ok {
372-
return i
394+
return i, nil
373395
}
374396
sst.Count++
375397
sst.UniqueCount++
376398
t := xlsxT{Val: val}
377399
_, val, t.Space = setCellStr(val)
378400
sst.SI = append(sst.SI, xlsxSI{T: &t})
379401
f.sharedStringsMap[val] = sst.UniqueCount - 1
380-
return sst.UniqueCount - 1
402+
return sst.UniqueCount - 1, nil
381403
}
382404

383405
// setCellStr provides a function to set string type to cell.
@@ -762,6 +784,34 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro
762784
return
763785
}
764786

787+
// newRpr create run properties for the rich text by given font format.
788+
func newRpr(fnt *Font) *xlsxRPr {
789+
rpr := xlsxRPr{}
790+
trueVal := ""
791+
if fnt.Bold {
792+
rpr.B = &trueVal
793+
}
794+
if fnt.Italic {
795+
rpr.I = &trueVal
796+
}
797+
if fnt.Strike {
798+
rpr.Strike = &trueVal
799+
}
800+
if fnt.Underline != "" {
801+
rpr.U = &attrValString{Val: &fnt.Underline}
802+
}
803+
if fnt.Family != "" {
804+
rpr.RFont = &attrValString{Val: &fnt.Family}
805+
}
806+
if fnt.Size > 0.0 {
807+
rpr.Sz = &attrValFloat{Val: &fnt.Size}
808+
}
809+
if fnt.Color != "" {
810+
rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)}
811+
}
812+
return &rpr
813+
}
814+
765815
// SetCellRichText provides a function to set cell with rich text by given
766816
// worksheet. For example, set rich text on the A1 cell of the worksheet named
767817
// Sheet1:
@@ -875,6 +925,9 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
875925
if err != nil {
876926
return err
877927
}
928+
if err := f.sharedStringsLoader(); err != nil {
929+
return err
930+
}
878931
cellData.S = f.prepareCellStyle(ws, col, cellData.S)
879932
si := xlsxSI{}
880933
sst := f.sharedStringsReader()
@@ -889,30 +942,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
889942
_, run.T.Val, run.T.Space = setCellStr(textRun.Text)
890943
fnt := textRun.Font
891944
if fnt != nil {
892-
rpr := xlsxRPr{}
893-
trueVal := ""
894-
if fnt.Bold {
895-
rpr.B = &trueVal
896-
}
897-
if fnt.Italic {
898-
rpr.I = &trueVal
899-
}
900-
if fnt.Strike {
901-
rpr.Strike = &trueVal
902-
}
903-
if fnt.Underline != "" {
904-
rpr.U = &attrValString{Val: &fnt.Underline}
905-
}
906-
if fnt.Family != "" {
907-
rpr.RFont = &attrValString{Val: &fnt.Family}
908-
}
909-
if fnt.Size > 0.0 {
910-
rpr.Sz = &attrValFloat{Val: &fnt.Size}
911-
}
912-
if fnt.Color != "" {
913-
rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)}
914-
}
915-
run.RPr = &rpr
945+
run.RPr = newRpr(fnt)
916946
}
917947
textRuns = append(textRuns, run)
918948
}

cell_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,20 @@ func TestFormattedValue2(t *testing.T) {
649649
v = f.formattedValue(1, "43528", false)
650650
assert.Equal(t, "43528", v)
651651
}
652+
653+
func TestSharedStringsError(t *testing.T) {
654+
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
655+
assert.NoError(t, err)
656+
f.tempFiles.Store(dafaultXMLPathSharedStrings, "")
657+
assert.Equal(t, "1", f.getFromStringItemMap(1))
658+
659+
// Test reload the file error on set cell cell and rich text. The error message was different between macOS and Windows.
660+
err = f.SetCellValue("Sheet1", "A19", "A19")
661+
assert.Error(t, err)
662+
663+
f.tempFiles.Store(dafaultXMLPathSharedStrings, "")
664+
err = f.SetCellRichText("Sheet1", "A19", []RichTextRun{})
665+
assert.Error(t, err)
666+
667+
assert.NoError(t, f.Close())
668+
}

docProps.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import (
2727
// Application | The name of the application that created this document.
2828
// |
2929
// ScaleCrop | Indicates the display mode of the document thumbnail. Set this element
30-
// | to TRUE to enable scaling of the document thumbnail to the display. Set
31-
// | this element to FALSE to enable cropping of the document thumbnail to
30+
// | to 'true' to enable scaling of the document thumbnail to the display. Set
31+
// | this element to 'false' to enable cropping of the document thumbnail to
3232
// | show only sections that will fit the display.
3333
// |
3434
// DocSecurity | Security level of a document as a numeric value. Document security is
@@ -41,8 +41,8 @@ import (
4141
// Company | The name of a company associated with the document.
4242
// |
4343
// LinksUpToDate | Indicates whether hyperlinks in a document are up-to-date. Set this
44-
// | element to TRUE to indicate that hyperlinks are updated. Set this
45-
// | element to FALSE to indicate that hyperlinks are outdated.
44+
// | element to 'true' to indicate that hyperlinks are updated. Set this
45+
// | element to 'false' to indicate that hyperlinks are outdated.
4646
// |
4747
// HyperlinksChanged | Specifies that one or more hyperlinks in this part were updated
4848
// | exclusively in this part by a producer. The next producer to open this
@@ -75,7 +75,7 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) {
7575
field string
7676
)
7777
app = new(xlsxProperties)
78-
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))).
78+
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))).
7979
Decode(app); err != nil && err != io.EOF {
8080
err = fmt.Errorf("xml decode error: %s", err)
8181
return
@@ -95,14 +95,14 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) {
9595
}
9696
app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value
9797
output, err = xml.Marshal(app)
98-
f.saveFileList("docProps/app.xml", output)
98+
f.saveFileList(dafaultXMLPathDocPropsApp, output)
9999
return
100100
}
101101

102102
// GetAppProps provides a function to get document application properties.
103103
func (f *File) GetAppProps() (ret *AppProperties, err error) {
104104
var app = new(xlsxProperties)
105-
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))).
105+
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))).
106106
Decode(app); err != nil && err != io.EOF {
107107
err = fmt.Errorf("xml decode error: %s", err)
108108
return
@@ -181,7 +181,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
181181
)
182182

183183
core = new(decodeCoreProperties)
184-
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
184+
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))).
185185
Decode(core); err != nil && err != io.EOF {
186186
err = fmt.Errorf("xml decode error: %s", err)
187187
return
@@ -223,7 +223,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
223223
newProps.Modified.Text = docProperties.Modified
224224
}
225225
output, err = xml.Marshal(newProps)
226-
f.saveFileList("docProps/core.xml", output)
226+
f.saveFileList(dafaultXMLPathDocPropsCore, output)
227227

228228
return
229229
}
@@ -232,7 +232,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
232232
func (f *File) GetDocProps() (ret *DocProperties, err error) {
233233
var core = new(decodeCoreProperties)
234234

235-
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
235+
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))).
236236
Decode(core); err != nil && err != io.EOF {
237237
err = fmt.Errorf("xml decode error: %s", err)
238238
return

docProps_test.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ func TestSetAppProps(t *testing.T) {
3535
AppVersion: "16.0000",
3636
}))
3737
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetAppProps.xlsx")))
38-
f.Pkg.Store("docProps/app.xml", nil)
38+
f.Pkg.Store(dafaultXMLPathDocPropsApp, nil)
3939
assert.NoError(t, f.SetAppProps(&AppProperties{}))
4040
assert.NoError(t, f.Close())
4141

4242
// Test unsupported charset
4343
f = NewFile()
44-
f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset)
44+
f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset)
4545
assert.EqualError(t, f.SetAppProps(&AppProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
4646
}
4747

@@ -53,14 +53,14 @@ func TestGetAppProps(t *testing.T) {
5353
props, err := f.GetAppProps()
5454
assert.NoError(t, err)
5555
assert.Equal(t, props.Application, "Microsoft Macintosh Excel")
56-
f.Pkg.Store("docProps/app.xml", nil)
56+
f.Pkg.Store(dafaultXMLPathDocPropsApp, nil)
5757
_, err = f.GetAppProps()
5858
assert.NoError(t, err)
5959
assert.NoError(t, f.Close())
6060

6161
// Test unsupported charset
6262
f = NewFile()
63-
f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset)
63+
f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset)
6464
_, err = f.GetAppProps()
6565
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
6666
}
@@ -87,13 +87,13 @@ func TestSetDocProps(t *testing.T) {
8787
Version: "1.0.0",
8888
}))
8989
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx")))
90-
f.Pkg.Store("docProps/core.xml", nil)
90+
f.Pkg.Store(dafaultXMLPathDocPropsCore, nil)
9191
assert.NoError(t, f.SetDocProps(&DocProperties{}))
9292
assert.NoError(t, f.Close())
9393

9494
// Test unsupported charset
9595
f = NewFile()
96-
f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset)
96+
f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset)
9797
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
9898
}
9999

@@ -105,14 +105,14 @@ func TestGetDocProps(t *testing.T) {
105105
props, err := f.GetDocProps()
106106
assert.NoError(t, err)
107107
assert.Equal(t, props.Creator, "Microsoft Office User")
108-
f.Pkg.Store("docProps/core.xml", nil)
108+
f.Pkg.Store(dafaultXMLPathDocPropsCore, nil)
109109
_, err = f.GetDocProps()
110110
assert.NoError(t, err)
111111
assert.NoError(t, f.Close())
112112

113113
// Test unsupported charset
114114
f = NewFile()
115-
f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset)
115+
f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset)
116116
_, err = f.GetDocProps()
117117
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
118118
}

errors.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ var (
143143
// characters length that exceeds the limit.
144144
ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
145145
// ErrOptionsUnzipSizeLimit defined the error message for receiving
146-
// invalid UnzipSizeLimit and WorksheetUnzipMemLimit.
147-
ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to WorksheetUnzipMemLimit")
146+
// invalid UnzipSizeLimit and UnzipXMLSizeLimit.
147+
ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit")
148148
// ErrSave defined the error message for saving file.
149149
ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write")
150150
// ErrAttrValBool defined the error message on marshal and unmarshal

0 commit comments

Comments
 (0)