This closes #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
This commit is contained in:
xuri 2021-12-27 23:34:14 +08:00
parent 6b1e592cbc
commit 89b85934f6
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
19 changed files with 260 additions and 144 deletions

View File

@ -25,7 +25,7 @@ func (f *File) calcChainReader() *xlsxCalcChain {
if f.CalcChain == nil {
f.CalcChain = new(xlsxCalcChain)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathCalcChain)))).
Decode(f.CalcChain); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
@ -39,7 +39,7 @@ func (f *File) calcChainReader() *xlsxCalcChain {
func (f *File) calcChainWriter() {
if f.CalcChain != nil && f.CalcChain.C != nil {
output, _ := xml.Marshal(f.CalcChain)
f.saveFileList("xl/calcChain.xml", output)
f.saveFileList(dafaultXMLPathCalcChain, output)
}
}
@ -54,7 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) {
}
if len(calc.C) == 0 {
f.CalcChain = nil
f.Pkg.Delete("xl/calcChain.xml")
f.Pkg.Delete(dafaultXMLPathCalcChain)
content := f.contentTypesReader()
content.Lock()
defer content.Unlock()

View File

@ -5,7 +5,7 @@ import "testing"
func TestCalcChainReader(t *testing.T) {
f := NewFile()
f.CalcChain = nil
f.Pkg.Store("xl/calcChain.xml", MacintoshCyrillicCharset)
f.Pkg.Store(dafaultXMLPathCalcChain, MacintoshCyrillicCharset)
f.calcChainReader()
}

90
cell.go
View File

@ -14,6 +14,7 @@ package excelize
import (
"encoding/xml"
"fmt"
"os"
"reflect"
"strconv"
"strings"
@ -348,28 +349,49 @@ func (f *File) SetCellStr(sheet, axis, value string) error {
ws.Lock()
defer ws.Unlock()
cellData.S = f.prepareCellStyle(ws, col, cellData.S)
cellData.T, cellData.V = f.setCellString(value)
cellData.T, cellData.V, err = f.setCellString(value)
return err
}
// setCellString provides a function to set string type to shared string
// table.
func (f *File) setCellString(value string) (t string, v string) {
func (f *File) setCellString(value string) (t, v string, err error) {
if len(value) > TotalCellChars {
value = value[:TotalCellChars]
}
t = "s"
v = strconv.Itoa(f.setSharedString(value))
var si int
if si, err = f.setSharedString(value); err != nil {
return
}
v = strconv.Itoa(si)
return
}
// sharedStringsLoader load shared string table from system temporary file to
// memory, and reset shared string table for reader.
func (f *File) sharedStringsLoader() (err error) {
f.Lock()
defer f.Unlock()
if path, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok {
f.Pkg.Store(dafaultXMLPathSharedStrings, f.readBytes(dafaultXMLPathSharedStrings))
f.tempFiles.Delete(dafaultXMLPathSharedStrings)
err = os.Remove(path.(string))
f.SharedStrings, f.sharedStringItemMap = nil, nil
}
return
}
// setSharedString provides a function to add string to the share string table.
func (f *File) setSharedString(val string) int {
func (f *File) setSharedString(val string) (int, error) {
if err := f.sharedStringsLoader(); err != nil {
return 0, err
}
sst := f.sharedStringsReader()
f.Lock()
defer f.Unlock()
if i, ok := f.sharedStringsMap[val]; ok {
return i
return i, nil
}
sst.Count++
sst.UniqueCount++
@ -377,7 +399,7 @@ func (f *File) setSharedString(val string) int {
_, val, t.Space = setCellStr(val)
sst.SI = append(sst.SI, xlsxSI{T: &t})
f.sharedStringsMap[val] = sst.UniqueCount - 1
return sst.UniqueCount - 1
return sst.UniqueCount - 1, nil
}
// 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
return
}
// newRpr create run properties for the rich text by given font format.
func newRpr(fnt *Font) *xlsxRPr {
rpr := xlsxRPr{}
trueVal := ""
if fnt.Bold {
rpr.B = &trueVal
}
if fnt.Italic {
rpr.I = &trueVal
}
if fnt.Strike {
rpr.Strike = &trueVal
}
if fnt.Underline != "" {
rpr.U = &attrValString{Val: &fnt.Underline}
}
if fnt.Family != "" {
rpr.RFont = &attrValString{Val: &fnt.Family}
}
if fnt.Size > 0.0 {
rpr.Sz = &attrValFloat{Val: &fnt.Size}
}
if fnt.Color != "" {
rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)}
}
return &rpr
}
// SetCellRichText provides a function to set cell with rich text by given
// worksheet. For example, set rich text on the A1 cell of the worksheet named
// Sheet1:
@ -875,6 +925,9 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
if err != nil {
return err
}
if err := f.sharedStringsLoader(); err != nil {
return err
}
cellData.S = f.prepareCellStyle(ws, col, cellData.S)
si := xlsxSI{}
sst := f.sharedStringsReader()
@ -889,30 +942,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
_, run.T.Val, run.T.Space = setCellStr(textRun.Text)
fnt := textRun.Font
if fnt != nil {
rpr := xlsxRPr{}
trueVal := ""
if fnt.Bold {
rpr.B = &trueVal
}
if fnt.Italic {
rpr.I = &trueVal
}
if fnt.Strike {
rpr.Strike = &trueVal
}
if fnt.Underline != "" {
rpr.U = &attrValString{Val: &fnt.Underline}
}
if fnt.Family != "" {
rpr.RFont = &attrValString{Val: &fnt.Family}
}
if fnt.Size > 0.0 {
rpr.Sz = &attrValFloat{Val: &fnt.Size}
}
if fnt.Color != "" {
rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)}
}
run.RPr = &rpr
run.RPr = newRpr(fnt)
}
textRuns = append(textRuns, run)
}

View File

@ -649,3 +649,20 @@ func TestFormattedValue2(t *testing.T) {
v = f.formattedValue(1, "43528", false)
assert.Equal(t, "43528", v)
}
func TestSharedStringsError(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
assert.NoError(t, err)
f.tempFiles.Store(dafaultXMLPathSharedStrings, "")
assert.Equal(t, "1", f.getFromStringItemMap(1))
// Test reload the file error on set cell cell and rich text. The error message was different between macOS and Windows.
err = f.SetCellValue("Sheet1", "A19", "A19")
assert.Error(t, err)
f.tempFiles.Store(dafaultXMLPathSharedStrings, "")
err = f.SetCellRichText("Sheet1", "A19", []RichTextRun{})
assert.Error(t, err)
assert.NoError(t, f.Close())
}

View File

@ -27,8 +27,8 @@ import (
// Application | The name of the application that created this document.
// |
// ScaleCrop | Indicates the display mode of the document thumbnail. Set this element
// | to TRUE to enable scaling of the document thumbnail to the display. Set
// | this element to FALSE to enable cropping of the document thumbnail to
// | to 'true' to enable scaling of the document thumbnail to the display. Set
// | this element to 'false' to enable cropping of the document thumbnail to
// | show only sections that will fit the display.
// |
// DocSecurity | Security level of a document as a numeric value. Document security is
@ -41,8 +41,8 @@ import (
// Company | The name of a company associated with the document.
// |
// LinksUpToDate | Indicates whether hyperlinks in a document are up-to-date. Set this
// | element to TRUE to indicate that hyperlinks are updated. Set this
// | element to FALSE to indicate that hyperlinks are outdated.
// | element to 'true' to indicate that hyperlinks are updated. Set this
// | element to 'false' to indicate that hyperlinks are outdated.
// |
// HyperlinksChanged | Specifies that one or more hyperlinks in this part were updated
// | 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) {
field string
)
app = new(xlsxProperties)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))).
Decode(app); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
@ -95,14 +95,14 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) {
}
app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value
output, err = xml.Marshal(app)
f.saveFileList("docProps/app.xml", output)
f.saveFileList(dafaultXMLPathDocPropsApp, output)
return
}
// GetAppProps provides a function to get document application properties.
func (f *File) GetAppProps() (ret *AppProperties, err error) {
var app = new(xlsxProperties)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))).
Decode(app); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
@ -181,7 +181,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
)
core = new(decodeCoreProperties)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))).
Decode(core); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
@ -223,7 +223,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
newProps.Modified.Text = docProperties.Modified
}
output, err = xml.Marshal(newProps)
f.saveFileList("docProps/core.xml", output)
f.saveFileList(dafaultXMLPathDocPropsCore, output)
return
}
@ -232,7 +232,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
func (f *File) GetDocProps() (ret *DocProperties, err error) {
var core = new(decodeCoreProperties)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))).
Decode(core); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return

View File

@ -35,13 +35,13 @@ func TestSetAppProps(t *testing.T) {
AppVersion: "16.0000",
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetAppProps.xlsx")))
f.Pkg.Store("docProps/app.xml", nil)
f.Pkg.Store(dafaultXMLPathDocPropsApp, nil)
assert.NoError(t, f.SetAppProps(&AppProperties{}))
assert.NoError(t, f.Close())
// Test unsupported charset
f = NewFile()
f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset)
f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset)
assert.EqualError(t, f.SetAppProps(&AppProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
}
@ -53,14 +53,14 @@ func TestGetAppProps(t *testing.T) {
props, err := f.GetAppProps()
assert.NoError(t, err)
assert.Equal(t, props.Application, "Microsoft Macintosh Excel")
f.Pkg.Store("docProps/app.xml", nil)
f.Pkg.Store(dafaultXMLPathDocPropsApp, nil)
_, err = f.GetAppProps()
assert.NoError(t, err)
assert.NoError(t, f.Close())
// Test unsupported charset
f = NewFile()
f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset)
f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset)
_, err = f.GetAppProps()
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
}
@ -87,13 +87,13 @@ func TestSetDocProps(t *testing.T) {
Version: "1.0.0",
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx")))
f.Pkg.Store("docProps/core.xml", nil)
f.Pkg.Store(dafaultXMLPathDocPropsCore, nil)
assert.NoError(t, f.SetDocProps(&DocProperties{}))
assert.NoError(t, f.Close())
// Test unsupported charset
f = NewFile()
f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset)
f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset)
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
}
@ -105,14 +105,14 @@ func TestGetDocProps(t *testing.T) {
props, err := f.GetDocProps()
assert.NoError(t, err)
assert.Equal(t, props.Creator, "Microsoft Office User")
f.Pkg.Store("docProps/core.xml", nil)
f.Pkg.Store(dafaultXMLPathDocPropsCore, nil)
_, err = f.GetDocProps()
assert.NoError(t, err)
assert.NoError(t, f.Close())
// Test unsupported charset
f = NewFile()
f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset)
f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset)
_, err = f.GetDocProps()
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
}

View File

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

View File

@ -45,6 +45,7 @@ type File struct {
Path string
SharedStrings *xlsxSST
sharedStringsMap map[string]int
sharedStringItemMap *sync.Map
Sheet sync.Map
SheetCount int
Styles *xlsxStyleSheet
@ -68,17 +69,18 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e
//
// UnzipSizeLimit specifies the unzip size limit in bytes on open the
// spreadsheet, this value should be greater than or equal to
// WorksheetUnzipMemLimit, the default size limit is 16GB.
// UnzipXMLSizeLimit, the default size limit is 16GB.
//
// WorksheetUnzipMemLimit specifies the memory limit on unzipping worksheet in
// bytes, worksheet XML will be extracted to system temporary directory when
// the file size is over this value, this value should be less than or equal
// to UnzipSizeLimit, the default value is 16MB.
// UnzipXMLSizeLimit specifies the memory limit on unzipping worksheet and
// shared string table in bytes, worksheet XML will be extracted to system
// temporary directory when the file size is over this value, this value
// should be less than or equal to UnzipSizeLimit, the default value is
// 16MB.
type Options struct {
Password string
RawCellValue bool
UnzipSizeLimit int64
WorksheetUnzipMemLimit int64
UnzipXMLSizeLimit int64
}
// OpenFile take the name of an spreadsheet file and returns a populated
@ -111,7 +113,7 @@ func OpenFile(filename string, opt ...Options) (*File, error) {
// newFile is object builder
func newFile() *File {
return &File{
options: &Options{UnzipSizeLimit: UnzipSizeLimit, WorksheetUnzipMemLimit: StreamChunkSize},
options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize},
xmlAttr: make(map[string][]xml.Attr),
checked: make(map[string]bool),
sheetMap: make(map[string]string),
@ -138,17 +140,17 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
f.options = parseOptions(opt...)
if f.options.UnzipSizeLimit == 0 {
f.options.UnzipSizeLimit = UnzipSizeLimit
if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit {
f.options.UnzipSizeLimit = f.options.WorksheetUnzipMemLimit
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
f.options.UnzipSizeLimit = f.options.UnzipXMLSizeLimit
}
}
if f.options.WorksheetUnzipMemLimit == 0 {
f.options.WorksheetUnzipMemLimit = StreamChunkSize
if f.options.UnzipSizeLimit < f.options.WorksheetUnzipMemLimit {
f.options.WorksheetUnzipMemLimit = f.options.UnzipSizeLimit
if f.options.UnzipXMLSizeLimit == 0 {
f.options.UnzipXMLSizeLimit = StreamChunkSize
if f.options.UnzipSizeLimit < f.options.UnzipXMLSizeLimit {
f.options.UnzipXMLSizeLimit = f.options.UnzipSizeLimit
}
}
if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit {
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
return nil, ErrOptionsUnzipSizeLimit
}
if bytes.Contains(b, oleIdentifier) {

View File

@ -201,7 +201,7 @@ func TestCharsetTranscoder(t *testing.T) {
func TestOpenReader(t *testing.T) {
_, err := OpenReader(strings.NewReader(""))
assert.EqualError(t, err, "zip: not a valid zip file")
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", WorksheetUnzipMemLimit: UnzipSizeLimit + 1})
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1})
assert.EqualError(t, err, "decrypted file failed")
// Test open spreadsheet with unzip size limit.
@ -225,7 +225,7 @@ func TestOpenReader(t *testing.T) {
assert.NoError(t, f.Close())
// Test open spreadsheet with invalid optioins.
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, WorksheetUnzipMemLimit: 2})
_, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, UnzipXMLSizeLimit: 2})
assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error())
// Test unexpected EOF.
@ -1208,7 +1208,7 @@ func TestContentTypesReader(t *testing.T) {
// Test unsupported charset.
f := NewFile()
f.ContentTypes = nil
f.Pkg.Store("[Content_Types].xml", MacintoshCyrillicCharset)
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
f.contentTypesReader()
}
@ -1216,7 +1216,7 @@ func TestWorkbookReader(t *testing.T) {
// Test unsupported charset.
f := NewFile()
f.WorkBook = nil
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
f.workbookReader()
}

23
file.go
View File

@ -14,6 +14,7 @@ package excelize
import (
"archive/zip"
"bytes"
"encoding/xml"
"io"
"os"
"path/filepath"
@ -27,15 +28,15 @@ import (
//
func NewFile() *File {
f := newFile()
f.Pkg.Store("_rels/.rels", []byte(XMLHeader+templateRels))
f.Pkg.Store("docProps/app.xml", []byte(XMLHeader+templateDocpropsApp))
f.Pkg.Store("docProps/core.xml", []byte(XMLHeader+templateDocpropsCore))
f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(XMLHeader+templateWorkbookRels))
f.Pkg.Store("xl/theme/theme1.xml", []byte(XMLHeader+templateTheme))
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(XMLHeader+templateSheet))
f.Pkg.Store("xl/styles.xml", []byte(XMLHeader+templateStyles))
f.Pkg.Store("xl/workbook.xml", []byte(XMLHeader+templateWorkbook))
f.Pkg.Store("[Content_Types].xml", []byte(XMLHeader+templateContentTypes))
f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
f.Pkg.Store(dafaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
f.Pkg.Store(dafaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore))
f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(xml.Header+templateWorkbookRels))
f.Pkg.Store("xl/theme/theme1.xml", []byte(xml.Header+templateTheme))
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+templateSheet))
f.Pkg.Store(defaultXMLPathStyles, []byte(xml.Header+templateStyles))
f.Pkg.Store(defaultXMLPathWorkbook, []byte(xml.Header+templateWorkbook))
f.Pkg.Store(defaultXMLPathContentTypes, []byte(xml.Header+templateContentTypes))
f.SheetCount = 1
f.CalcChain = f.calcChainReader()
f.Comments = make(map[string]*xlsxComments)
@ -159,6 +160,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
f.workBookWriter()
f.workSheetWriter()
f.relsWriter()
f.sharedStringsLoader()
f.sharedStringsWriter()
f.styleSheetWriter()
@ -196,6 +198,9 @@ func (f *File) writeToZip(zw *zip.Writer) error {
return true
})
f.tempFiles.Range(func(path, content interface{}) bool {
if _, ok := f.Pkg.Load(path); ok {
return true
}
var fi io.Writer
fi, err = zw.Create(path.(string))
if err != nil {

14
lib.go
View File

@ -30,8 +30,8 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
var (
err error
docPart = map[string]string{
"[content_types].xml": "[Content_Types].xml",
"xl/sharedstrings.xml": "xl/sharedStrings.xml",
"[content_types].xml": defaultXMLPathContentTypes,
"xl/sharedstrings.xml": dafaultXMLPathSharedStrings,
}
fileList = make(map[string][]byte, len(r.File))
worksheets int
@ -47,9 +47,15 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
if partName, ok := docPart[strings.ToLower(fileName)]; ok {
fileName = partName
}
if strings.EqualFold(fileName, dafaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit {
if tempFile, err := f.unzipToTemp(v); err == nil {
f.tempFiles.Store(fileName, tempFile)
continue
}
}
if strings.HasPrefix(fileName, "xl/worksheets/sheet") {
worksheets++
if fileSize > f.options.WorksheetUnzipMemLimit && !v.FileInfo().IsDir() {
if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() {
if tempFile, err := f.unzipToTemp(v); err == nil {
f.tempFiles.Store(fileName, tempFile)
continue
@ -120,7 +126,7 @@ func (f *File) readTemp(name string) (file *os.File, err error) {
// saveFileList provides a function to update given file content in file list
// of spreadsheet.
func (f *File) saveFileList(name string, content []byte) {
f.Pkg.Store(name, append([]byte(XMLHeader), content...))
f.Pkg.Store(name, append([]byte(xml.Header), content...))
}
// Read file content as string in a archive file.

51
rows.go
View File

@ -21,6 +21,7 @@ import (
"math/big"
"os"
"strconv"
"sync"
"github.com/mohae/deepcopy"
)
@ -244,7 +245,7 @@ func (f *File) Rows(sheet string) (*Rows, error) {
decoder *xml.Decoder
tempFile *os.File
)
if needClose, decoder, tempFile, err = f.sheetDecoder(name); needClose && err == nil {
if needClose, decoder, tempFile, err = f.xmlDecoder(name); needClose && err == nil {
defer tempFile.Close()
}
for {
@ -271,7 +272,7 @@ func (f *File) Rows(sheet string) (*Rows, error) {
if xmlElement.Name.Local == "sheetData" {
rows.f = f
rows.sheet = name
_, rows.decoder, rows.tempFile, err = f.sheetDecoder(name)
_, rows.decoder, rows.tempFile, err = f.xmlDecoder(name)
return &rows, err
}
}
@ -279,9 +280,46 @@ func (f *File) Rows(sheet string) (*Rows, error) {
return &rows, nil
}
// sheetDecoder creates XML decoder by given path in the zip from memory data
// getFromStringItemMap build shared string item map from system temporary
// file at one time, and return value by given to string index.
func (f *File) getFromStringItemMap(index int) string {
if f.sharedStringItemMap != nil {
if value, ok := f.sharedStringItemMap.Load(index); ok {
return value.(string)
}
return strconv.Itoa(index)
}
f.sharedStringItemMap = &sync.Map{}
needClose, decoder, tempFile, err := f.xmlDecoder(dafaultXMLPathSharedStrings)
if needClose && err == nil {
defer tempFile.Close()
}
var (
inElement string
i int
)
for {
token, _ := decoder.Token()
if token == nil {
break
}
switch xmlElement := token.(type) {
case xml.StartElement:
inElement = xmlElement.Name.Local
if inElement == "si" {
si := xlsxSI{}
_ = decoder.DecodeElement(&si, &xmlElement)
f.sharedStringItemMap.Store(i, si.String())
i++
}
}
}
return f.getFromStringItemMap(index)
}
// xmlDecoder creates XML decoder by given path in the zip from memory data
// or system temporary file.
func (f *File) sheetDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
var (
content []byte
err error
@ -373,7 +411,7 @@ func (f *File) sharedStringsReader() *xlsxSST {
relPath := f.getWorkbookRelsPath()
if f.SharedStrings == nil {
var sharedStrings xlsxSST
ss := f.readXML("xl/sharedStrings.xml")
ss := f.readXML(dafaultXMLPathSharedStrings)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))).
Decode(&sharedStrings); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
@ -415,6 +453,9 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
if c.V != "" {
xlsxSI := 0
xlsxSI, _ = strconv.Atoi(c.V)
if _, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok {
return f.formattedValue(c.S, f.getFromStringItemMap(xlsxSI), raw), nil
}
if len(d.SI) > xlsxSI {
return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil
}

View File

@ -56,11 +56,18 @@ func TestRows(t *testing.T) {
assert.NoError(t, err)
// Test reload the file to memory from system temporary directory.
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{WorksheetUnzipMemLimit: 1024})
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
assert.NoError(t, err)
value, err := f.GetCellValue("Sheet1", "A19")
assert.NoError(t, err)
assert.Equal(t, "Total:", value)
// Test load shared string table to memory
err = f.SetCellValue("Sheet1", "A19", "A19")
assert.NoError(t, err)
value, err = f.GetCellValue("Sheet1", "A19")
assert.NoError(t, err)
assert.Equal(t, "A19", value)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRow.xlsx")))
assert.NoError(t, f.Close())
}
@ -200,7 +207,7 @@ func TestColumns(t *testing.T) {
func TestSharedStringsReader(t *testing.T) {
f := NewFile()
f.Pkg.Store("xl/sharedStrings.xml", MacintoshCyrillicCharset)
f.Pkg.Store(dafaultXMLPathSharedStrings, MacintoshCyrillicCharset)
f.sharedStringsReader()
si := xlsxSI{}
assert.EqualValues(t, "", si.String())

View File

@ -76,7 +76,7 @@ func (f *File) contentTypesReader() *xlsxTypes {
f.ContentTypes = new(xlsxTypes)
f.ContentTypes.Lock()
defer f.ContentTypes.Unlock()
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))).
Decode(f.ContentTypes); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
@ -89,7 +89,7 @@ func (f *File) contentTypesReader() *xlsxTypes {
func (f *File) contentTypesWriter() {
if f.ContentTypes != nil {
output, _ := xml.Marshal(f.ContentTypes)
f.saveFileList("[Content_Types].xml", output)
f.saveFileList(defaultXMLPathContentTypes, output)
}
}
@ -304,7 +304,7 @@ func (f *File) relsWriter() {
// setAppXML update docProps/app.xml file of XML.
func (f *File) setAppXML() {
f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp))
f.saveFileList(dafaultXMLPathDocPropsApp, []byte(templateDocpropsApp))
}
// replaceRelationshipsBytes; Some tools that read spreadsheet files have very

View File

@ -112,7 +112,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
}
f.streams[sheetPath] = sw
_, _ = sw.rawData.WriteString(XMLHeader + `<worksheet` + templateNamespaceIDMap)
_, _ = sw.rawData.WriteString(xml.Header + `<worksheet` + templateNamespaceIDMap)
bulkAppendFields(&sw.rawData, sw.worksheet, 2, 5)
return sw, err
}

View File

@ -1078,7 +1078,7 @@ func (f *File) stylesReader() *xlsxStyleSheet {
if f.Styles == nil {
f.Styles = new(xlsxStyleSheet)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/styles.xml")))).
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathStyles)))).
Decode(f.Styles); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
@ -1092,7 +1092,7 @@ func (f *File) stylesReader() *xlsxStyleSheet {
func (f *File) styleSheetWriter() {
if f.Styles != nil {
output, _ := xml.Marshal(f.Styles)
f.saveFileList("xl/styles.xml", f.replaceNameSpaceBytes("xl/styles.xml", output))
f.saveFileList(defaultXMLPathStyles, f.replaceNameSpaceBytes(defaultXMLPathStyles, output))
}
}
@ -1101,7 +1101,7 @@ func (f *File) styleSheetWriter() {
func (f *File) sharedStringsWriter() {
if f.SharedStrings != nil {
output, _ := xml.Marshal(f.SharedStrings)
f.saveFileList("xl/sharedStrings.xml", f.replaceNameSpaceBytes("xl/sharedStrings.xml", output))
f.saveFileList(dafaultXMLPathSharedStrings, f.replaceNameSpaceBytes(dafaultXMLPathSharedStrings, output))
}
}

View File

@ -300,7 +300,7 @@ func TestStylesReader(t *testing.T) {
f := NewFile()
// Test read styles with unsupported charset.
f.Styles = nil
f.Pkg.Store("xl/styles.xml", MacintoshCyrillicCharset)
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader())
}

View File

@ -14,13 +14,22 @@
package excelize
// XMLHeader define an XML declaration can also contain a standalone declaration.
const XMLHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
import "encoding/xml"
var (
// XMLHeaderByte define an XML declaration can also contain a standalone
// declaration.
XMLHeaderByte = []byte(XMLHeader)
XMLHeaderByte = []byte(xml.Header)
)
const (
defaultXMLPathContentTypes = "[Content_Types].xml"
dafaultXMLPathDocPropsApp = "docProps/app.xml"
dafaultXMLPathDocPropsCore = "docProps/core.xml"
dafaultXMLPathCalcChain = "xl/calcChain.xml"
dafaultXMLPathSharedStrings = "xl/sharedStrings.xml"
defaultXMLPathStyles = "xl/styles.xml"
defaultXMLPathWorkbook = "xl/workbook.xml"
)
const templateDocpropsApp = `<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime><Application>Go Excelize</Application></Properties>`

View File

@ -51,7 +51,6 @@ const (
SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"