2024-01-09 20:56:20 +08:00
|
|
|
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
2018-09-14 00:44:23 +08:00
|
|
|
// this source code is governed by a BSD-style license that can be found in
|
|
|
|
// the LICENSE file.
|
2018-12-05 00:27:19 +08:00
|
|
|
|
2022-02-17 00:09:11 +08:00
|
|
|
// Package excelize providing a set of functions that allow you to write to and
|
|
|
|
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
|
|
|
|
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
|
|
|
|
// Supports complex components by high compatibility, and provided streaming
|
|
|
|
// API for generating or reading data from a worksheet with huge amounts of
|
2024-01-18 15:31:43 +08:00
|
|
|
// data. This library needs Go version 1.18 or later.
|
2018-09-14 00:58:48 +08:00
|
|
|
//
|
|
|
|
// See https://xuri.me/excelize for more information about this package.
|
2016-08-30 11:51:31 +08:00
|
|
|
package excelize
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
2016-09-04 19:25:31 +08:00
|
|
|
"bytes"
|
2016-08-30 11:51:31 +08:00
|
|
|
"encoding/xml"
|
2017-02-17 02:25:55 +08:00
|
|
|
"io"
|
|
|
|
"os"
|
2021-08-15 00:06:40 +08:00
|
|
|
"path/filepath"
|
2016-08-30 11:51:31 +08:00
|
|
|
"strconv"
|
2019-07-21 12:56:36 +08:00
|
|
|
"strings"
|
2020-08-15 00:09:50 +08:00
|
|
|
"sync"
|
2019-12-20 00:30:48 +08:00
|
|
|
|
|
|
|
"golang.org/x/net/html/charset"
|
2016-08-30 11:51:31 +08:00
|
|
|
)
|
|
|
|
|
2020-05-21 22:57:58 +08:00
|
|
|
// File define a populated spreadsheet file struct.
|
2016-09-05 16:37:15 +08:00
|
|
|
type File struct {
|
2023-04-24 00:02:13 +08:00
|
|
|
mu sync.Mutex
|
2023-10-11 00:04:38 +08:00
|
|
|
checked sync.Map
|
2024-01-18 15:31:43 +08:00
|
|
|
formulaChecked bool
|
2023-11-02 00:15:41 +08:00
|
|
|
options *Options
|
|
|
|
sharedStringItem [][]uint
|
|
|
|
sharedStringsMap map[string]int
|
|
|
|
sharedStringTemp *os.File
|
2022-01-11 00:24:24 +08:00
|
|
|
sheetMap map[string]string
|
|
|
|
streams map[string]*StreamWriter
|
|
|
|
tempFiles sync.Map
|
2023-11-02 00:15:41 +08:00
|
|
|
xmlAttr sync.Map
|
2022-01-11 00:24:24 +08:00
|
|
|
CalcChain *xlsxCalcChain
|
2023-11-02 00:15:41 +08:00
|
|
|
CharsetReader charsetTranscoderFn
|
2022-01-11 00:24:24 +08:00
|
|
|
Comments map[string]*xlsxComments
|
|
|
|
ContentTypes *xlsxTypes
|
2023-11-02 00:15:41 +08:00
|
|
|
DecodeVMLDrawing map[string]*decodeVmlDrawing
|
2023-12-15 13:09:42 +08:00
|
|
|
DecodeCellImages *decodeCellImages
|
2022-01-11 00:24:24 +08:00
|
|
|
Drawings sync.Map
|
|
|
|
Path string
|
2023-11-02 00:15:41 +08:00
|
|
|
Pkg sync.Map
|
|
|
|
Relationships sync.Map
|
2022-01-11 00:24:24 +08:00
|
|
|
SharedStrings *xlsxSST
|
|
|
|
Sheet sync.Map
|
|
|
|
SheetCount int
|
|
|
|
Styles *xlsxStyleSheet
|
2023-10-20 00:04:31 +08:00
|
|
|
Theme *decodeTheme
|
2022-01-11 00:24:24 +08:00
|
|
|
VMLDrawing map[string]*vmlDrawing
|
2023-11-02 00:15:41 +08:00
|
|
|
VolatileDeps *xlsxVolTypes
|
2022-01-11 00:24:24 +08:00
|
|
|
WorkBook *xlsxWorkbook
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
|
|
|
|
2023-01-02 11:47:31 +08:00
|
|
|
// charsetTranscoderFn set user-defined codepage transcoder function for open
|
|
|
|
// the spreadsheet from non-UTF-8 encoding.
|
2019-12-20 00:30:48 +08:00
|
|
|
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
|
|
|
|
|
2023-05-30 00:14:44 +08:00
|
|
|
// Options define the options for opening and reading the spreadsheet.
|
2021-09-18 23:20:24 +08:00
|
|
|
//
|
2022-07-10 18:14:48 +08:00
|
|
|
// MaxCalcIterations specifies the maximum iterations for iterative
|
|
|
|
// calculation, the default value is 0.
|
|
|
|
//
|
2021-09-18 23:20:24 +08:00
|
|
|
// Password specifies the password of the spreadsheet in plain text.
|
|
|
|
//
|
|
|
|
// RawCellValue specifies if apply the number format for the cell value or get
|
|
|
|
// the raw value.
|
|
|
|
//
|
2023-05-30 00:14:44 +08:00
|
|
|
// UnzipSizeLimit specifies to unzip size limit in bytes on open the
|
2021-09-18 23:20:24 +08:00
|
|
|
// spreadsheet, this value should be greater than or equal to
|
2021-12-27 23:34:14 +08:00
|
|
|
// UnzipXMLSizeLimit, the default size limit is 16GB.
|
2021-09-18 23:20:24 +08:00
|
|
|
//
|
2021-12-27 23:34:14 +08:00
|
|
|
// 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.
|
2023-05-06 20:34:18 +08:00
|
|
|
//
|
2023-05-11 09:08:38 +08:00
|
|
|
// ShortDatePattern specifies the short date number format code. In the
|
2023-05-06 20:34:18 +08:00
|
|
|
// spreadsheet applications, date formats display date and time serial numbers
|
|
|
|
// as date values. Date formats that begin with an asterisk (*) respond to
|
|
|
|
// changes in regional date and time settings that are specified for the
|
|
|
|
// operating system. Formats without an asterisk are not affected by operating
|
2023-05-11 09:08:38 +08:00
|
|
|
// system settings. The ShortDatePattern used for specifies apply date formats
|
2023-05-06 20:34:18 +08:00
|
|
|
// that begin with an asterisk.
|
|
|
|
//
|
2023-05-11 09:08:38 +08:00
|
|
|
// LongDatePattern specifies the long date number format code.
|
2023-05-06 20:34:18 +08:00
|
|
|
//
|
2023-05-11 09:08:38 +08:00
|
|
|
// LongTimePattern specifies the long time number format code.
|
|
|
|
//
|
|
|
|
// CultureInfo specifies the country code for applying built-in language number
|
|
|
|
// format code these effect by the system's local language settings.
|
2020-09-01 00:40:56 +08:00
|
|
|
type Options struct {
|
2022-07-10 18:14:48 +08:00
|
|
|
MaxCalcIterations uint
|
2021-12-27 23:34:14 +08:00
|
|
|
Password string
|
|
|
|
RawCellValue bool
|
|
|
|
UnzipSizeLimit int64
|
|
|
|
UnzipXMLSizeLimit int64
|
2023-05-11 09:08:38 +08:00
|
|
|
ShortDatePattern string
|
|
|
|
LongDatePattern string
|
|
|
|
LongTimePattern string
|
|
|
|
CultureInfo CultureName
|
2020-09-01 00:40:56 +08:00
|
|
|
}
|
|
|
|
|
2023-05-30 00:14:44 +08:00
|
|
|
// OpenFile take the name of a spreadsheet file and returns a populated
|
2021-08-15 00:06:40 +08:00
|
|
|
// spreadsheet file struct for it. For example, open spreadsheet with
|
|
|
|
// password protection:
|
2020-09-01 00:40:56 +08:00
|
|
|
//
|
2022-08-13 11:21:59 +08:00
|
|
|
// f, err := excelize.OpenFile("Book1.xlsx", excelize.Options{Password: "password"})
|
2020-09-01 00:40:56 +08:00
|
|
|
//
|
2022-05-31 11:05:10 +08:00
|
|
|
// Close the file by Close function after opening the spreadsheet.
|
2022-09-18 00:07:15 +08:00
|
|
|
func OpenFile(filename string, opts ...Options) (*File, error) {
|
2021-08-15 00:06:40 +08:00
|
|
|
file, err := os.Open(filepath.Clean(filename))
|
2016-09-06 21:20:24 +08:00
|
|
|
if err != nil {
|
2017-02-17 02:25:55 +08:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-09-18 00:07:15 +08:00
|
|
|
f, err := OpenReader(file, opts...)
|
2017-02-17 02:38:57 +08:00
|
|
|
if err != nil {
|
2023-04-19 00:05:59 +08:00
|
|
|
if closeErr := file.Close(); closeErr != nil {
|
|
|
|
return f, closeErr
|
2022-03-24 00:19:30 +08:00
|
|
|
}
|
2023-04-19 00:05:59 +08:00
|
|
|
return f, err
|
2017-02-17 02:38:57 +08:00
|
|
|
}
|
|
|
|
f.Path = filename
|
2022-03-24 00:19:30 +08:00
|
|
|
return f, file.Close()
|
2017-02-17 02:30:05 +08:00
|
|
|
}
|
|
|
|
|
2019-12-20 22:22:56 +08:00
|
|
|
// newFile is object builder
|
2019-12-20 00:30:48 +08:00
|
|
|
func newFile() *File {
|
|
|
|
return &File{
|
2021-12-27 23:34:14 +08:00
|
|
|
options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize},
|
2023-10-11 00:04:38 +08:00
|
|
|
xmlAttr: sync.Map{},
|
|
|
|
checked: sync.Map{},
|
2019-12-20 00:30:48 +08:00
|
|
|
sheetMap: make(map[string]string),
|
2021-09-18 23:20:24 +08:00
|
|
|
tempFiles: sync.Map{},
|
2019-12-20 00:30:48 +08:00
|
|
|
Comments: make(map[string]*xlsxComments),
|
2021-07-04 12:13:06 +08:00
|
|
|
Drawings: sync.Map{},
|
2020-05-27 00:02:29 +08:00
|
|
|
sharedStringsMap: make(map[string]int),
|
2021-07-05 00:03:56 +08:00
|
|
|
Sheet: sync.Map{},
|
2019-12-20 00:30:48 +08:00
|
|
|
DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
|
|
|
|
VMLDrawing: make(map[string]*vmlDrawing),
|
2021-07-04 12:13:06 +08:00
|
|
|
Relationships: sync.Map{},
|
2019-12-20 00:30:48 +08:00
|
|
|
CharsetReader: charset.NewReaderLabel,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-13 00:05:46 +08:00
|
|
|
// checkOpenReaderOptions check and validate options field value for open
|
|
|
|
// reader.
|
|
|
|
func (f *File) checkOpenReaderOptions() error {
|
2021-09-05 11:59:50 +08:00
|
|
|
if f.options.UnzipSizeLimit == 0 {
|
|
|
|
f.options.UnzipSizeLimit = UnzipSizeLimit
|
2021-12-27 23:34:14 +08:00
|
|
|
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
|
|
|
|
f.options.UnzipSizeLimit = f.options.UnzipXMLSizeLimit
|
2021-09-18 23:20:24 +08:00
|
|
|
}
|
|
|
|
}
|
2021-12-27 23:34:14 +08:00
|
|
|
if f.options.UnzipXMLSizeLimit == 0 {
|
|
|
|
f.options.UnzipXMLSizeLimit = StreamChunkSize
|
|
|
|
if f.options.UnzipSizeLimit < f.options.UnzipXMLSizeLimit {
|
|
|
|
f.options.UnzipXMLSizeLimit = f.options.UnzipSizeLimit
|
2021-09-18 23:20:24 +08:00
|
|
|
}
|
|
|
|
}
|
2021-12-27 23:34:14 +08:00
|
|
|
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
|
2023-01-13 00:05:46 +08:00
|
|
|
return ErrOptionsUnzipSizeLimit
|
|
|
|
}
|
2023-05-11 09:08:38 +08:00
|
|
|
return f.checkDateTimePattern()
|
2023-01-13 00:05:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// OpenReader read data stream from io.Reader and return a populated
|
|
|
|
// spreadsheet file.
|
|
|
|
func OpenReader(r io.Reader, opts ...Options) (*File, error) {
|
|
|
|
b, err := io.ReadAll(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
f := newFile()
|
2024-02-15 13:30:07 +08:00
|
|
|
f.options = f.getOptions(opts...)
|
2023-01-13 00:05:46 +08:00
|
|
|
if err = f.checkOpenReaderOptions(); err != nil {
|
|
|
|
return nil, err
|
2021-08-15 00:06:40 +08:00
|
|
|
}
|
|
|
|
if bytes.Contains(b, oleIdentifier) {
|
2022-05-20 20:46:29 +08:00
|
|
|
if b, err = Decrypt(b, f.options); err != nil {
|
|
|
|
return nil, ErrWorkbookFileFormat
|
2019-06-27 21:58:14 +08:00
|
|
|
}
|
2020-09-01 00:40:56 +08:00
|
|
|
}
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
|
|
|
|
if err != nil {
|
2022-05-20 20:46:29 +08:00
|
|
|
if len(f.options.Password) > 0 {
|
|
|
|
return nil, ErrWorkbookPassword
|
|
|
|
}
|
2017-02-17 02:25:55 +08:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-09-18 23:20:24 +08:00
|
|
|
file, sheetCount, err := f.ReadZipReader(zr)
|
2017-02-17 02:25:55 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
2021-07-05 00:03:56 +08:00
|
|
|
f.SheetCount = sheetCount
|
|
|
|
for k, v := range file {
|
|
|
|
f.Pkg.Store(k, v)
|
|
|
|
}
|
2022-11-12 00:02:11 +08:00
|
|
|
if f.CalcChain, err = f.calcChainReader(); err != nil {
|
|
|
|
return f, err
|
|
|
|
}
|
2023-01-07 13:17:00 +08:00
|
|
|
if f.sheetMap, err = f.getSheetMap(); err != nil {
|
|
|
|
return f, err
|
|
|
|
}
|
2022-11-13 00:40:04 +08:00
|
|
|
if f.Styles, err = f.stylesReader(); err != nil {
|
|
|
|
return f, err
|
|
|
|
}
|
|
|
|
f.Theme, err = f.themeReader()
|
2022-11-12 00:02:11 +08:00
|
|
|
return f, err
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
|
|
|
|
2023-01-20 11:10:04 +08:00
|
|
|
// getOptions provides a function to parse the optional settings for open
|
2021-09-05 11:59:50 +08:00
|
|
|
// and reading spreadsheet.
|
2024-02-15 13:30:07 +08:00
|
|
|
func (f *File) getOptions(opts ...Options) *Options {
|
|
|
|
options := f.options
|
2022-09-18 00:07:15 +08:00
|
|
|
for _, opt := range opts {
|
|
|
|
options = &opt
|
2021-09-05 11:59:50 +08:00
|
|
|
}
|
2022-09-18 00:07:15 +08:00
|
|
|
return options
|
2021-09-05 11:59:50 +08:00
|
|
|
}
|
|
|
|
|
2019-12-20 22:22:56 +08:00
|
|
|
// CharsetTranscoder Set user defined codepage transcoder function for open
|
2024-05-14 12:06:10 +08:00
|
|
|
// workbook from non UTF-8 encoding.
|
2019-12-20 00:30:48 +08:00
|
|
|
func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f }
|
|
|
|
|
2019-12-20 22:22:56 +08:00
|
|
|
// Creates new XML decoder with charset reader.
|
2019-12-20 00:30:48 +08:00
|
|
|
func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
|
|
|
|
ret = xml.NewDecoder(rdr)
|
|
|
|
ret.CharsetReader = f.CharsetReader
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// setDefaultTimeStyle provides a function to set default numbers format for
|
2022-09-18 00:07:15 +08:00
|
|
|
// time.Time type cell value by given worksheet name, cell reference and
|
2018-01-05 09:39:31 +08:00
|
|
|
// number format code.
|
2022-09-18 00:07:15 +08:00
|
|
|
func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error {
|
2024-10-10 22:44:38 +08:00
|
|
|
styleIdx, err := f.GetCellStyle(sheet, cell)
|
2019-03-23 20:08:06 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-10-10 22:44:38 +08:00
|
|
|
if styleIdx == 0 {
|
|
|
|
styleIdx, _ = f.NewStyle(&Style{NumFmt: format})
|
|
|
|
} else {
|
|
|
|
style, _ := f.GetStyle(styleIdx)
|
|
|
|
style.NumFmt = format
|
|
|
|
styleIdx, _ = f.NewStyle(style)
|
2017-06-23 21:07:30 +08:00
|
|
|
}
|
2024-10-10 22:44:38 +08:00
|
|
|
return f.SetCellStyle(sheet, cell, cell, styleIdx)
|
2017-06-23 21:07:30 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// workSheetReader provides a function to get the pointer to the structure
|
|
|
|
// after deserialization by given worksheet name.
|
2020-11-10 23:48:09 +08:00
|
|
|
func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
|
2019-12-20 00:30:48 +08:00
|
|
|
var (
|
|
|
|
name string
|
|
|
|
ok bool
|
|
|
|
)
|
This closes #1425, breaking changes for sheet name (#1426)
- Checking and return error for invalid sheet name instead of trim invalid characters
- Add error return for the 4 functions: `DeleteSheet`, `GetSheetIndex`, `GetSheetVisible` and `SetSheetName`
- Export new error 4 constants: `ErrSheetNameBlank`, `ErrSheetNameInvalid`, `ErrSheetNameLength` and `ErrSheetNameSingleQuote`
- Rename exported error constant `ErrExistsWorksheet` to `ErrExistsSheet`
- Update unit tests for 90 functions: `AddChart`, `AddChartSheet`, `AddComment`, `AddDataValidation`, `AddPicture`, `AddPictureFromBytes`, `AddPivotTable`, `AddShape`, `AddSparkline`, `AddTable`, `AutoFilter`, `CalcCellValue`, `Cols`, `DeleteChart`, `DeleteComment`, `DeleteDataValidation`, `DeletePicture`, `DeleteSheet`, `DuplicateRow`, `DuplicateRowTo`, `GetCellFormula`, `GetCellHyperLink`, `GetCellRichText`, `GetCellStyle`, `GetCellType`, `GetCellValue`, `GetColOutlineLevel`, `GetCols`, `GetColStyle`, `GetColVisible`, `GetColWidth`, `GetConditionalFormats`, `GetDataValidations`, `GetMergeCells`, `GetPageLayout`, `GetPageMargins`, `GetPicture`, `GetRowHeight`, `GetRowOutlineLevel`, `GetRows`, `GetRowVisible`, `GetSheetIndex`, `GetSheetProps`, `GetSheetVisible`, `GroupSheets`, `InsertCol`, `InsertPageBreak`, `InsertRows`, `MergeCell`, `NewSheet`, `NewStreamWriter`, `ProtectSheet`, `RemoveCol`, `RemovePageBreak`, `RemoveRow`, `Rows`, `SearchSheet`, `SetCellBool`, `SetCellDefault`, `SetCellFloat`, `SetCellFormula`, `SetCellHyperLink`, `SetCellInt`, `SetCellRichText`, `SetCellStr`, `SetCellStyle`, `SetCellValue`, `SetColOutlineLevel`, `SetColStyle`, `SetColVisible`, `SetColWidth`, `SetConditionalFormat`, `SetHeaderFooter`, `SetPageLayout`, `SetPageMargins`, `SetPanes`, `SetRowHeight`, `SetRowOutlineLevel`, `SetRowStyle`, `SetRowVisible`, `SetSheetBackground`, `SetSheetBackgroundFromBytes`, `SetSheetCol`, `SetSheetName`, `SetSheetProps`, `SetSheetRow`, `SetSheetVisible`, `UnmergeCell`, `UnprotectSheet` and
`UnsetConditionalFormat`
- Update documentation of the set style functions
Co-authored-by: guoweikuang <weikuang.guo@shopee.com>
2022-12-23 00:54:40 +08:00
|
|
|
if err = checkSheetName(sheet); err != nil {
|
|
|
|
return
|
|
|
|
}
|
2022-07-18 00:21:34 +08:00
|
|
|
if name, ok = f.getSheetXMLPath(sheet); !ok {
|
2023-09-28 08:53:54 +08:00
|
|
|
err = ErrSheetNotExist{sheet}
|
2019-12-20 00:30:48 +08:00
|
|
|
return
|
2017-09-13 22:00:33 +08:00
|
|
|
}
|
2021-07-05 00:03:56 +08:00
|
|
|
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
|
|
|
|
ws = worksheet.(*xlsxWorksheet)
|
|
|
|
return
|
|
|
|
}
|
2022-06-21 20:08:47 +08:00
|
|
|
for _, sheetType := range []string{"xl/chartsheets", "xl/dialogsheet", "xl/macrosheet"} {
|
|
|
|
if strings.HasPrefix(name, sheetType) {
|
2022-08-19 23:24:13 +08:00
|
|
|
err = newNotWorksheetError(sheet)
|
2022-06-21 20:08:47 +08:00
|
|
|
return
|
|
|
|
}
|
2021-07-05 00:03:56 +08:00
|
|
|
}
|
|
|
|
ws = new(xlsxWorksheet)
|
2023-10-11 00:04:38 +08:00
|
|
|
if attrs, ok := f.xmlAttr.Load(name); !ok {
|
2021-09-18 23:20:24 +08:00
|
|
|
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name))))
|
2023-10-11 00:04:38 +08:00
|
|
|
if attrs == nil {
|
|
|
|
attrs = []xml.Attr{}
|
|
|
|
}
|
|
|
|
attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
|
|
|
|
f.xmlAttr.Store(name, attrs)
|
2021-07-05 00:03:56 +08:00
|
|
|
}
|
2021-09-18 23:20:24 +08:00
|
|
|
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))).
|
2021-07-05 00:03:56 +08:00
|
|
|
Decode(ws); err != nil && err != io.EOF {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = nil
|
2023-10-11 00:04:38 +08:00
|
|
|
if _, ok = f.checked.Load(name); !ok {
|
2023-04-24 00:02:13 +08:00
|
|
|
ws.checkSheet()
|
|
|
|
if err = ws.checkRow(); err != nil {
|
2019-12-20 00:30:48 +08:00
|
|
|
return
|
|
|
|
}
|
2023-10-11 00:04:38 +08:00
|
|
|
f.checked.Store(name, true)
|
2017-02-12 19:03:24 +08:00
|
|
|
}
|
2021-07-05 00:03:56 +08:00
|
|
|
f.Sheet.Store(name, ws)
|
2019-12-20 00:30:48 +08:00
|
|
|
return
|
2017-03-12 20:38:26 +08:00
|
|
|
}
|
2017-02-12 19:03:24 +08:00
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// checkSheet provides a function to fill each row element and make that is
|
2017-03-31 17:14:35 +08:00
|
|
|
// continuous in a worksheet of XML.
|
2023-04-24 00:02:13 +08:00
|
|
|
func (ws *xlsxWorksheet) checkSheet() {
|
2023-11-18 16:44:45 +08:00
|
|
|
var (
|
2024-03-15 11:36:34 +08:00
|
|
|
row int
|
|
|
|
r0Rows []xlsxRow
|
|
|
|
lastRowNum = func(r xlsxRow) int {
|
|
|
|
var num int
|
|
|
|
for _, cell := range r.C {
|
|
|
|
if _, row, err := CellNameToCoordinates(cell.R); err == nil {
|
|
|
|
if row > num {
|
|
|
|
num = row
|
|
|
|
}
|
2023-11-18 16:44:45 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return num
|
|
|
|
}
|
|
|
|
)
|
2024-03-15 11:36:34 +08:00
|
|
|
for i := 0; i < len(ws.SheetData.Row); i++ {
|
|
|
|
r := ws.SheetData.Row[i]
|
|
|
|
if r.R == 0 || r.R == row {
|
|
|
|
num := lastRowNum(r)
|
|
|
|
if num > row {
|
|
|
|
row = num
|
|
|
|
}
|
|
|
|
if num == 0 {
|
|
|
|
row++
|
2023-11-18 16:44:45 +08:00
|
|
|
}
|
2024-03-15 11:36:34 +08:00
|
|
|
r.R = row
|
|
|
|
r0Rows = append(r0Rows, r)
|
|
|
|
ws.SheetData.Row = append(ws.SheetData.Row[:i], ws.SheetData.Row[i+1:]...)
|
|
|
|
i--
|
2020-06-27 00:02:47 +08:00
|
|
|
continue
|
|
|
|
}
|
2024-03-15 11:36:34 +08:00
|
|
|
if r.R != 0 && r.R > row {
|
|
|
|
row = r.R
|
2023-11-18 16:44:45 +08:00
|
|
|
}
|
2016-12-20 14:40:36 +08:00
|
|
|
}
|
2024-03-15 11:36:34 +08:00
|
|
|
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
|
|
|
|
row = 0
|
|
|
|
for _, r := range ws.SheetData.Row {
|
|
|
|
if r.R != 0 {
|
|
|
|
sheetData.Row[r.R-1] = r
|
|
|
|
row = r.R
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, r0Row := range r0Rows {
|
|
|
|
sheetData.Row[r0Row.R-1].R = r0Row.R
|
|
|
|
ws.checkSheetR0(&sheetData, &r0Row, true)
|
|
|
|
}
|
|
|
|
for i := 1; i <= row; i++ {
|
|
|
|
sheetData.Row[i-1].R = i
|
|
|
|
ws.checkSheetR0(&sheetData, &sheetData.Row[i-1], false)
|
2016-12-20 14:40:36 +08:00
|
|
|
}
|
2021-08-23 00:15:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// checkSheetR0 handle the row element with r="0" attribute, cells in this row
|
|
|
|
// could be disorderly, the cell in this row can be used as the value of
|
|
|
|
// which cell is empty in the normal rows.
|
2024-03-15 11:36:34 +08:00
|
|
|
func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, rowData *xlsxRow, r0 bool) {
|
|
|
|
checkRow := func(col, row int, r0 bool, cell xlsxC) {
|
|
|
|
rowIdx := row - 1
|
|
|
|
columns, colIdx := len(sheetData.Row[rowIdx].C), col-1
|
|
|
|
for c := columns; c < col; c++ {
|
|
|
|
sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{})
|
|
|
|
}
|
|
|
|
if !sheetData.Row[rowIdx].C[colIdx].hasValue() {
|
|
|
|
sheetData.Row[rowIdx].C[colIdx] = cell
|
|
|
|
}
|
|
|
|
if r0 {
|
|
|
|
sheetData.Row[rowIdx].C[colIdx] = cell
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
for i, cell := range rowData.C {
|
|
|
|
col, row := i+1, rowData.R
|
|
|
|
if cell.R == "" {
|
|
|
|
checkRow(col, row, r0, cell)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if col, row, err = CellNameToCoordinates(cell.R); err == nil && r0 {
|
|
|
|
checkRow(col, row, r0, cell)
|
2021-08-23 00:15:43 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
ws.SheetData = *sheetData
|
2016-08-30 11:51:31 +08:00
|
|
|
}
|
|
|
|
|
2022-05-01 12:28:36 +08:00
|
|
|
// setRels provides a function to set relationships by given relationship ID,
|
|
|
|
// XML path, relationship type, target and target mode.
|
|
|
|
func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
|
2022-11-13 00:40:04 +08:00
|
|
|
rels, _ := f.relsReader(relPath)
|
2022-05-01 12:28:36 +08:00
|
|
|
if rels == nil || rID == "" {
|
|
|
|
return f.addRels(relPath, relType, target, targetMode)
|
|
|
|
}
|
2023-04-24 00:02:13 +08:00
|
|
|
rels.mu.Lock()
|
|
|
|
defer rels.mu.Unlock()
|
2022-05-01 12:28:36 +08:00
|
|
|
var ID int
|
|
|
|
for i, rel := range rels.Relationships {
|
|
|
|
if rel.ID == rID {
|
|
|
|
rels.Relationships[i].Type = relType
|
|
|
|
rels.Relationships[i].Target = target
|
|
|
|
rels.Relationships[i].TargetMode = targetMode
|
|
|
|
ID, _ = strconv.Atoi(strings.TrimPrefix(rID, "rId"))
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ID
|
|
|
|
}
|
|
|
|
|
2019-09-16 01:17:35 +08:00
|
|
|
// addRels provides a function to add relationships by given XML path,
|
|
|
|
// relationship type, target and target mode.
|
|
|
|
func (f *File) addRels(relPath, relType, target, targetMode string) int {
|
2022-01-23 00:32:34 +08:00
|
|
|
uniqPart := map[string]string{
|
2020-07-09 01:24:11 +08:00
|
|
|
SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
|
|
|
|
}
|
2022-11-13 00:40:04 +08:00
|
|
|
rels, _ := f.relsReader(relPath)
|
2019-09-16 01:17:35 +08:00
|
|
|
if rels == nil {
|
|
|
|
rels = &xlsxRelationships{}
|
|
|
|
}
|
2023-04-24 00:02:13 +08:00
|
|
|
rels.mu.Lock()
|
|
|
|
defer rels.mu.Unlock()
|
2019-12-23 00:07:40 +08:00
|
|
|
var rID int
|
2020-07-09 01:24:11 +08:00
|
|
|
for idx, rel := range rels.Relationships {
|
2019-12-23 00:07:40 +08:00
|
|
|
ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
|
|
|
|
if ID > rID {
|
|
|
|
rID = ID
|
|
|
|
}
|
2020-07-09 01:24:11 +08:00
|
|
|
if relType == rel.Type {
|
|
|
|
if partName, ok := uniqPart[rel.Type]; ok {
|
|
|
|
rels.Relationships[idx].Target = partName
|
|
|
|
return rID
|
|
|
|
}
|
|
|
|
}
|
2019-12-23 00:07:40 +08:00
|
|
|
}
|
|
|
|
rID++
|
2019-09-16 01:17:35 +08:00
|
|
|
var ID bytes.Buffer
|
|
|
|
ID.WriteString("rId")
|
|
|
|
ID.WriteString(strconv.Itoa(rID))
|
|
|
|
rels.Relationships = append(rels.Relationships, xlsxRelationship{
|
|
|
|
ID: ID.String(),
|
|
|
|
Type: relType,
|
|
|
|
Target: target,
|
|
|
|
TargetMode: targetMode,
|
|
|
|
})
|
2021-07-04 12:13:06 +08:00
|
|
|
f.Relationships.Store(relPath, rels)
|
2019-09-16 01:17:35 +08:00
|
|
|
return rID
|
|
|
|
}
|
|
|
|
|
2016-09-07 20:09:02 +08:00
|
|
|
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
|
2022-08-21 01:09:32 +08:00
|
|
|
// Office Excel application. This function will be remove value tag when met a
|
2017-01-18 16:05:01 +08:00
|
|
|
// cell have a linked value. Reference
|
2024-03-25 08:33:29 +08:00
|
|
|
// https://learn.microsoft.com/en-us/archive/msdn-technet-forums/e16bae1f-6a2c-4325-8013-e989a3479066
|
2016-09-06 21:20:24 +08:00
|
|
|
//
|
2022-08-21 01:09:32 +08:00
|
|
|
// Notice: after opening generated workbook, Excel will update the linked value
|
|
|
|
// and generate a new value and will prompt to save the file or not.
|
2016-09-06 21:20:24 +08:00
|
|
|
//
|
|
|
|
// For example:
|
|
|
|
//
|
2024-03-25 08:33:29 +08:00
|
|
|
// <row r="19">
|
2022-08-13 11:21:59 +08:00
|
|
|
// <c r="B19">
|
|
|
|
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
|
|
|
// <v>100</v>
|
|
|
|
// </c>
|
|
|
|
// </row>
|
2016-09-06 21:20:24 +08:00
|
|
|
//
|
|
|
|
// to
|
|
|
|
//
|
2024-03-25 08:33:29 +08:00
|
|
|
// <row r="19">
|
2022-08-13 11:21:59 +08:00
|
|
|
// <c r="B19">
|
|
|
|
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
|
|
|
// </c>
|
|
|
|
// </row>
|
2019-04-15 11:22:57 +08:00
|
|
|
func (f *File) UpdateLinkedValue() error {
|
2022-11-13 00:40:04 +08:00
|
|
|
wb, err := f.workbookReader()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-03-09 00:08:47 +08:00
|
|
|
// recalculate formulas
|
|
|
|
wb.CalcPr = nil
|
2020-04-23 02:01:14 +08:00
|
|
|
for _, name := range f.GetSheetList() {
|
2021-08-15 00:06:40 +08:00
|
|
|
ws, err := f.workSheetReader(name)
|
2019-04-15 11:22:57 +08:00
|
|
|
if err != nil {
|
2022-08-19 23:24:13 +08:00
|
|
|
if err.Error() == newNotWorksheetError(name).Error() {
|
2021-03-04 09:23:45 +08:00
|
|
|
continue
|
|
|
|
}
|
2019-04-15 11:22:57 +08:00
|
|
|
return err
|
|
|
|
}
|
2021-08-15 00:06:40 +08:00
|
|
|
for indexR := range ws.SheetData.Row {
|
|
|
|
for indexC, col := range ws.SheetData.Row[indexR].C {
|
2017-01-18 14:47:23 +08:00
|
|
|
if col.F != nil && col.V != "" {
|
2021-08-15 00:06:40 +08:00
|
|
|
ws.SheetData.Row[indexR].C[indexC].V = ""
|
|
|
|
ws.SheetData.Row[indexR].C[indexC].T = ""
|
2016-09-06 21:20:24 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-04-15 11:22:57 +08:00
|
|
|
return nil
|
2016-09-06 21:20:24 +08:00
|
|
|
}
|
2019-07-21 12:56:36 +08:00
|
|
|
|
|
|
|
// AddVBAProject provides the method to add vbaProject.bin file which contains
|
2023-03-25 13:30:13 +08:00
|
|
|
// functions and/or macros. The file extension should be XLSM or XLTM. For
|
|
|
|
// example:
|
2019-07-21 12:56:36 +08:00
|
|
|
//
|
2022-10-10 00:11:18 +08:00
|
|
|
// codeName := "Sheet1"
|
|
|
|
// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{
|
|
|
|
// CodeName: &codeName,
|
|
|
|
// }); err != nil {
|
|
|
|
// fmt.Println(err)
|
2023-03-25 13:30:13 +08:00
|
|
|
// return
|
2022-10-10 00:11:18 +08:00
|
|
|
// }
|
2023-03-25 13:30:13 +08:00
|
|
|
// file, err := os.ReadFile("vbaProject.bin")
|
|
|
|
// if err != nil {
|
2022-08-13 11:21:59 +08:00
|
|
|
// fmt.Println(err)
|
2023-03-25 13:30:13 +08:00
|
|
|
// return
|
|
|
|
// }
|
|
|
|
// if err := f.AddVBAProject(file); err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// return
|
2022-08-13 11:21:59 +08:00
|
|
|
// }
|
|
|
|
// if err := f.SaveAs("macros.xlsm"); err != nil {
|
|
|
|
// fmt.Println(err)
|
2023-03-25 13:30:13 +08:00
|
|
|
// return
|
2022-08-13 11:21:59 +08:00
|
|
|
// }
|
2023-03-25 13:30:13 +08:00
|
|
|
func (f *File) AddVBAProject(file []byte) error {
|
2019-07-21 12:56:36 +08:00
|
|
|
var err error
|
|
|
|
// Check vbaProject.bin exists first.
|
2023-03-25 13:30:13 +08:00
|
|
|
if !bytes.Contains(file, oleIdentifier) {
|
2021-05-10 00:09:24 +08:00
|
|
|
return ErrAddVBAProject
|
2019-07-21 12:56:36 +08:00
|
|
|
}
|
2022-11-13 00:40:04 +08:00
|
|
|
rels, err := f.relsReader(f.getWorkbookRelsPath())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-04-24 00:02:13 +08:00
|
|
|
rels.mu.Lock()
|
|
|
|
defer rels.mu.Unlock()
|
2019-07-21 12:56:36 +08:00
|
|
|
var rID int
|
|
|
|
var ok bool
|
2022-11-13 00:40:04 +08:00
|
|
|
for _, rel := range rels.Relationships {
|
2019-07-21 12:56:36 +08:00
|
|
|
if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject {
|
|
|
|
ok = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
t, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
|
|
|
|
if t > rID {
|
|
|
|
rID = t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rID++
|
|
|
|
if !ok {
|
2022-11-13 00:40:04 +08:00
|
|
|
rels.Relationships = append(rels.Relationships, xlsxRelationship{
|
2019-07-21 12:56:36 +08:00
|
|
|
ID: "rId" + strconv.Itoa(rID),
|
|
|
|
Target: "vbaProject.bin",
|
|
|
|
Type: SourceRelationshipVBAProject,
|
|
|
|
})
|
|
|
|
}
|
2021-07-05 00:03:56 +08:00
|
|
|
f.Pkg.Store("xl/vbaProject.bin", file)
|
2019-07-21 12:56:36 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-01-23 00:32:34 +08:00
|
|
|
// setContentTypePartProjectExtensions provides a function to set the content
|
|
|
|
// type for relationship parts and the main document part.
|
2022-11-13 00:40:04 +08:00
|
|
|
func (f *File) setContentTypePartProjectExtensions(contentType string) error {
|
2019-07-21 12:56:36 +08:00
|
|
|
var ok bool
|
2022-11-13 00:40:04 +08:00
|
|
|
content, err := f.contentTypesReader()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-04-24 00:02:13 +08:00
|
|
|
content.mu.Lock()
|
|
|
|
defer content.mu.Unlock()
|
2019-07-21 12:56:36 +08:00
|
|
|
for _, v := range content.Defaults {
|
|
|
|
if v.Extension == "bin" {
|
|
|
|
ok = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for idx, o := range content.Overrides {
|
|
|
|
if o.PartName == "/xl/workbook.xml" {
|
2022-01-23 00:32:34 +08:00
|
|
|
content.Overrides[idx].ContentType = contentType
|
2019-07-21 12:56:36 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
content.Defaults = append(content.Defaults, xlsxDefault{
|
|
|
|
Extension: "bin",
|
2020-03-28 23:47:26 +08:00
|
|
|
ContentType: ContentTypeVBA,
|
2019-07-21 12:56:36 +08:00
|
|
|
})
|
|
|
|
}
|
2022-11-13 00:40:04 +08:00
|
|
|
return err
|
2019-07-21 12:56:36 +08:00
|
|
|
}
|
2024-03-26 23:19:23 +08:00
|
|
|
|
|
|
|
// metadataReader provides a function to get the pointer to the structure
|
|
|
|
// after deserialization of xl/metadata.xml.
|
|
|
|
func (f *File) metadataReader() (*xlsxMetadata, error) {
|
|
|
|
var mataData xlsxMetadata
|
|
|
|
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLMetadata)))).
|
|
|
|
Decode(&mataData); err != nil && err != io.EOF {
|
|
|
|
return &mataData, err
|
|
|
|
}
|
|
|
|
return &mataData, nil
|
|
|
|
}
|
|
|
|
|
2024-03-28 16:37:35 +08:00
|
|
|
// richValueReader provides a function to get the pointer to the structure after
|
|
|
|
// deserialization of xl/richData/richvalue.xml.
|
|
|
|
func (f *File) richValueReader() (*xlsxRichValueData, error) {
|
|
|
|
var richValue xlsxRichValueData
|
2024-04-02 08:47:57 +08:00
|
|
|
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValuePart)))).
|
2024-03-28 16:37:35 +08:00
|
|
|
Decode(&richValue); err != nil && err != io.EOF {
|
|
|
|
return &richValue, err
|
|
|
|
}
|
|
|
|
return &richValue, nil
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:19:23 +08:00
|
|
|
// richValueRelReader provides a function to get the pointer to the structure
|
|
|
|
// after deserialization of xl/richData/richValueRel.xml.
|
|
|
|
func (f *File) richValueRelReader() (*xlsxRichValueRels, error) {
|
|
|
|
var richValueRels xlsxRichValueRels
|
2024-04-02 08:47:57 +08:00
|
|
|
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))).
|
2024-03-26 23:19:23 +08:00
|
|
|
Decode(&richValueRels); err != nil && err != io.EOF {
|
|
|
|
return &richValueRels, err
|
|
|
|
}
|
|
|
|
return &richValueRels, nil
|
|
|
|
}
|
|
|
|
|
2024-04-02 08:47:57 +08:00
|
|
|
// richValueWebImageReader provides a function to get the pointer to the
|
|
|
|
// structure after deserialization of xl/richData/rdRichValueWebImage.xml.
|
|
|
|
func (f *File) richValueWebImageReader() (*xlsxWebImagesSupportingRichData, error) {
|
|
|
|
var richValueWebImages xlsxWebImagesSupportingRichData
|
|
|
|
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueWebImagePart)))).
|
|
|
|
Decode(&richValueWebImages); err != nil && err != io.EOF {
|
|
|
|
return &richValueWebImages, err
|
|
|
|
}
|
|
|
|
return &richValueWebImages, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getRichDataRichValueRelRelationships provides a function to get relationships
|
|
|
|
// from xl/richData/_rels/richValueRel.xml.rels by given relationship ID.
|
2024-03-26 23:19:23 +08:00
|
|
|
func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship {
|
2024-04-02 08:47:57 +08:00
|
|
|
if rels, _ := f.relsReader(defaultXMLRdRichValueRelRels); rels != nil {
|
|
|
|
rels.mu.Lock()
|
|
|
|
defer rels.mu.Unlock()
|
|
|
|
for _, v := range rels.Relationships {
|
|
|
|
if v.ID == rID {
|
|
|
|
return &v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getRichValueWebImageRelationships provides a function to get relationships
|
|
|
|
// from xl/richData/_rels/rdRichValueWebImage.xml.rels by given relationship ID.
|
|
|
|
func (f *File) getRichValueWebImageRelationships(rID string) *xlsxRelationship {
|
|
|
|
if rels, _ := f.relsReader(defaultXMLRdRichValueWebImagePartRels); rels != nil {
|
2024-03-26 23:19:23 +08:00
|
|
|
rels.mu.Lock()
|
|
|
|
defer rels.mu.Unlock()
|
|
|
|
for _, v := range rels.Relationships {
|
|
|
|
if v.ID == rID {
|
|
|
|
return &v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|