diff --git a/calcchain.go b/calcchain.go index 671d144..8f5e277 100644 --- a/calcchain.go +++ b/calcchain.go @@ -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() diff --git a/calcchain_test.go b/calcchain_test.go index 4956f60..6144ed5 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -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() } diff --git a/cell.go b/cell.go index 5c34bb9..daff3d9 100644 --- a/cell.go +++ b/cell.go @@ -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) } diff --git a/cell_test.go b/cell_test.go index 4a78a06..03de73b 100644 --- a/cell_test.go +++ b/cell_test.go @@ -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()) +} diff --git a/docProps.go b/docProps.go index c8ab27c..271b370 100644 --- a/docProps.go +++ b/docProps.go @@ -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 diff --git a/docProps_test.go b/docProps_test.go index a5c35f7..97948c1 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -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") } diff --git a/errors.go b/errors.go index 4230c14..9460803 100644 --- a/errors.go +++ b/errors.go @@ -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 diff --git a/excelize.go b/excelize.go index c5778c8..25acd54 100644 --- a/excelize.go +++ b/excelize.go @@ -32,29 +32,30 @@ import ( // File define a populated spreadsheet file struct. type File struct { sync.Mutex - options *Options - xmlAttr map[string][]xml.Attr - checked map[string]bool - sheetMap map[string]string - streams map[string]*StreamWriter - tempFiles sync.Map - CalcChain *xlsxCalcChain - Comments map[string]*xlsxComments - ContentTypes *xlsxTypes - Drawings sync.Map - Path string - SharedStrings *xlsxSST - sharedStringsMap map[string]int - Sheet sync.Map - SheetCount int - Styles *xlsxStyleSheet - Theme *xlsxTheme - DecodeVMLDrawing map[string]*decodeVmlDrawing - VMLDrawing map[string]*vmlDrawing - WorkBook *xlsxWorkbook - Relationships sync.Map - Pkg sync.Map - CharsetReader charsetTranscoderFn + options *Options + xmlAttr map[string][]xml.Attr + checked map[string]bool + sheetMap map[string]string + streams map[string]*StreamWriter + tempFiles sync.Map + CalcChain *xlsxCalcChain + Comments map[string]*xlsxComments + ContentTypes *xlsxTypes + Drawings sync.Map + Path string + SharedStrings *xlsxSST + sharedStringsMap map[string]int + sharedStringItemMap *sync.Map + Sheet sync.Map + SheetCount int + Styles *xlsxStyleSheet + Theme *xlsxTheme + DecodeVMLDrawing map[string]*decodeVmlDrawing + VMLDrawing map[string]*vmlDrawing + WorkBook *xlsxWorkbook + Relationships sync.Map + Pkg sync.Map + CharsetReader charsetTranscoderFn } type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) @@ -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 + Password string + RawCellValue bool + UnzipSizeLimit 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) { diff --git a/excelize_test.go b/excelize_test.go index 4c136b6..9aaaae9 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -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() } diff --git a/file.go b/file.go index e2aeb4a..6c8bd93 100644 --- a/file.go +++ b/file.go @@ -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 { diff --git a/lib.go b/lib.go index 0efc180..8ec121b 100644 --- a/lib.go +++ b/lib.go @@ -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. diff --git a/rows.go b/rows.go index 7b2f52f..5071bb6 100644 --- a/rows.go +++ b/rows.go @@ -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 } diff --git a/rows_test.go b/rows_test.go index 0c154a4..63321ce 100644 --- a/rows_test.go +++ b/rows_test.go @@ -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()) diff --git a/sheet.go b/sheet.go index 99ae1a2..17f6693 100644 --- a/sheet.go +++ b/sheet.go @@ -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 diff --git a/stream.go b/stream.go index 65d6b72..4bd721e 100644 --- a/stream.go +++ b/stream.go @@ -112,7 +112,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { } f.streams[sheetPath] = sw - _, _ = sw.rawData.WriteString(XMLHeader + `\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 = `0Go Excelize` diff --git a/xmlDrawing.go b/xmlDrawing.go index 1690554..4ae6a29 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -37,21 +37,20 @@ var ( // Source relationship and namespace. const ( - SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" - SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" - SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" - SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" - SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" - SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" - SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" - SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" - SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" - 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" - + SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" + SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + 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"