excelize/lib.go

464 lines
13 KiB
Go
Raw Normal View History

2019-12-29 16:02:31 +08:00
// Copyright 2016 - 2020 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.
//
// Package excelize providing a set of functions that allow you to write to
2020-06-22 00:14:56 +08:00
// and read from XLSX / XLSM / XLTM files. Supports reading and writing
// spreadsheet documents generated by Microsoft Exce™ 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 data. This
// library needs Go version 1.10 or later.
2018-09-14 00:58:48 +08:00
2016-08-30 11:51:31 +08:00
package excelize
import (
"archive/zip"
"bytes"
"container/list"
"encoding/xml"
"fmt"
2016-08-30 11:51:31 +08:00
"io"
"strconv"
"strings"
2016-08-30 11:51:31 +08:00
)
// ReadZipReader can be used to read the spreadsheet in memory without touching the
// filesystem.
2018-05-07 16:12:51 +08:00
func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
var err error
var docPart = map[string]string{
"[content_types].xml": "[Content_Types].xml",
"xl/sharedstrings.xml": "xl/sharedStrings.xml",
}
fileList := make(map[string][]byte, len(r.File))
worksheets := 0
2016-08-30 11:51:31 +08:00
for _, v := range r.File {
fileName := v.Name
if partName, ok := docPart[strings.ToLower(v.Name)]; ok {
fileName = partName
}
if fileList[fileName], err = readFile(v); err != nil {
return nil, 0, err
}
if strings.HasPrefix(v.Name, "xl/worksheets/sheet") {
worksheets++
}
2016-08-30 11:51:31 +08:00
}
return fileList, worksheets, nil
2016-08-30 11:51:31 +08:00
}
2018-08-06 10:21:24 +08:00
// readXML provides a function to read XML content as string.
2018-05-07 16:12:51 +08:00
func (f *File) readXML(name string) []byte {
if content, ok := f.XLSX[name]; ok {
return content
2016-08-30 11:51:31 +08:00
}
2018-05-07 16:12:51 +08:00
return []byte{}
2016-08-30 11:51:31 +08:00
}
2018-08-06 10:21:24 +08:00
// saveFileList provides a function to update given file content in file list
// of XLSX.
2018-05-07 16:12:51 +08:00
func (f *File) saveFileList(name string, content []byte) {
newContent := make([]byte, 0, len(XMLHeader)+len(content))
newContent = append(newContent, []byte(XMLHeader)...)
newContent = append(newContent, content...)
f.XLSX[name] = newContent
2016-08-30 11:51:31 +08:00
}
2016-10-19 20:39:44 +08:00
// Read file content as string in a archive file.
func readFile(file *zip.File) ([]byte, error) {
2016-08-30 11:51:31 +08:00
rc, err := file.Open()
if err != nil {
return nil, err
2016-08-30 11:51:31 +08:00
}
dat := make([]byte, 0, file.FileInfo().Size())
buff := bytes.NewBuffer(dat)
_, _ = io.Copy(buff, rc)
2016-08-30 11:51:31 +08:00
rc.Close()
return buff.Bytes(), nil
2016-08-30 11:51:31 +08:00
}
// SplitCellName splits cell name to column name and row number.
//
// Example:
//
// excelize.SplitCellName("AK74") // return "AK", 74, nil
//
func SplitCellName(cell string) (string, int, error) {
alpha := func(r rune) bool {
return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
}
if strings.IndexFunc(cell, alpha) == 0 {
i := strings.LastIndexFunc(cell, alpha)
if i >= 0 && i < len(cell)-1 {
col, rowstr := cell[:i+1], cell[i+1:]
if row, err := strconv.Atoi(rowstr); err == nil && row > 0 {
return col, row, nil
}
}
}
return "", -1, newInvalidCellNameError(cell)
}
2019-04-20 14:57:50 +08:00
// JoinCellName joins cell name from column name and row number.
func JoinCellName(col string, row int) (string, error) {
normCol := strings.Map(func(rune rune) rune {
switch {
case 'A' <= rune && rune <= 'Z':
return rune
case 'a' <= rune && rune <= 'z':
return rune - 32
}
return -1
}, col)
if len(col) == 0 || len(col) != len(normCol) {
return "", newInvalidColumnNameError(col)
2016-08-30 11:51:31 +08:00
}
if row < 1 {
return "", newInvalidRowNumberError(row)
2016-08-30 11:51:31 +08:00
}
2020-04-02 00:41:14 +08:00
return normCol + strconv.Itoa(row), nil
2016-08-30 11:51:31 +08:00
}
2019-04-20 14:57:50 +08:00
// ColumnNameToNumber provides a function to convert Excel sheet column name
// to int. Column name case insensitive. The function returns an error if
// column name incorrect.
//
// Example:
//
// excelize.ColumnNameToNumber("AK") // returns 37, nil
//
func ColumnNameToNumber(name string) (int, error) {
if len(name) == 0 {
return -1, newInvalidColumnNameError(name)
}
col := 0
multi := 1
for i := len(name) - 1; i >= 0; i-- {
r := name[i]
if r >= 'A' && r <= 'Z' {
col += int(r-'A'+1) * multi
} else if r >= 'a' && r <= 'z' {
col += int(r-'a'+1) * multi
} else {
return -1, newInvalidColumnNameError(name)
2017-09-05 18:06:38 +08:00
}
multi *= 26
2016-08-30 11:51:31 +08:00
}
if col > TotalColumns {
return -1, fmt.Errorf("column number exceeds maximum limit")
}
return col, nil
2016-08-30 11:51:31 +08:00
}
2019-04-20 14:57:50 +08:00
// ColumnNumberToName provides a function to convert the integer to Excel
// sheet column title.
//
// Example:
//
2019-03-20 15:13:41 +08:00
// excelize.ColumnNumberToName(37) // returns "AK", nil
//
func ColumnNumberToName(num int) (string, error) {
if num < 1 {
return "", fmt.Errorf("incorrect column number %d", num)
2016-08-30 11:51:31 +08:00
}
if num > TotalColumns {
return "", fmt.Errorf("column number exceeds maximum limit")
}
var col string
for num > 0 {
col = string(rune((num-1)%26+65)) + col
num = (num - 1) / 26
}
return col, nil
2016-08-30 11:51:31 +08:00
}
2019-04-20 14:57:50 +08:00
// CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates
// or returns an error.
//
// Example:
//
// excelize.CellNameToCoordinates("A1") // returns 1, 1, nil
// excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil
//
func CellNameToCoordinates(cell string) (int, int, error) {
const msg = "cannot convert cell %q to coordinates: %v"
colname, row, err := SplitCellName(cell)
if err != nil {
return -1, -1, fmt.Errorf(msg, cell, err)
}
if row > TotalRows {
return -1, -1, fmt.Errorf("row number exceeds maximum limit")
}
col, err := ColumnNameToNumber(colname)
2020-05-23 13:29:51 +08:00
return col, row, err
}
// CoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell
// name or returns an error.
//
// Example:
//
// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil
//
func CoordinatesToCellName(col, row int) (string, error) {
if col < 1 || row < 1 {
return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row)
}
colname, err := ColumnNumberToName(col)
2020-05-23 13:29:51 +08:00
return fmt.Sprintf("%s%d", colname, row), err
}
// boolPtr returns a pointer to a bool with the given value.
func boolPtr(b bool) *bool { return &b }
// intPtr returns a pointer to a int with the given value.
func intPtr(i int) *int { return &i }
// float64Ptr returns a pofloat64er to a float64 with the given value.
func float64Ptr(f float64) *float64 { return &f }
// stringPtr returns a pointer to a string with the given value.
func stringPtr(s string) *string { return &s }
// defaultTrue returns true if b is nil, or the pointed value.
func defaultTrue(b *bool) bool {
if b == nil {
return true
}
return *b
}
2018-07-13 17:40:47 +08:00
// parseFormatSet provides a method to convert format string to []byte and
// handle empty string.
func parseFormatSet(formatSet string) []byte {
if formatSet != "" {
return []byte(formatSet)
}
return []byte("{}")
}
// namespaceStrictToTransitional provides a method to convert Strict and
// Transitional namespaces.
func namespaceStrictToTransitional(content []byte) []byte {
var namespaceTranslationDic = map[string]string{
StrictSourceRelationship: SourceRelationship.Value,
StrictSourceRelationshipChart: SourceRelationshipChart,
StrictSourceRelationshipComments: SourceRelationshipComments,
StrictSourceRelationshipImage: SourceRelationshipImage,
StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value,
}
for s, n := range namespaceTranslationDic {
content = bytesReplace(content, []byte(s), []byte(n), -1)
}
return content
}
2020-04-02 00:41:14 +08:00
// bytesReplace replace old bytes with given new.
func bytesReplace(s, old, new []byte, n int) []byte {
if n == 0 {
return s
}
if len(old) < len(new) {
return bytes.Replace(s, old, new, n)
}
if n < 0 {
n = len(s)
}
var wid, i, j, w int
for i, j = 0, 0; i < len(s) && j < n; j++ {
wid = bytes.Index(s[i:], old)
if wid < 0 {
break
}
w += copy(s[w:], s[i:i+wid])
w += copy(s[w:], new)
i += wid + len(old)
}
w += copy(s[w:], s[i:])
return s[0:w]
}
// genSheetPasswd provides a method to generate password for worksheet
// protection by given plaintext. When an Excel sheet is being protected with
// a password, a 16-bit (two byte) long hash is generated. To verify a
// password, it is compared to the hash. Obviously, if the input data volume
// is great, numerous passwords will match the same hash. Here is the
// algorithm to create the hash value:
//
// take the ASCII values of all characters shift left the first character 1 bit,
// the second 2 bits and so on (use only the lower 15 bits and rotate all higher bits,
// the highest bit of the 16-bit value is always 0 [signed short])
// XOR all these values
// XOR the count of characters
// XOR the constant 0xCE4B
func genSheetPasswd(plaintext string) string {
var password int64 = 0x0000
var charPos uint = 1
for _, v := range plaintext {
value := int64(v) << charPos
charPos++
rotatedBits := value >> 15 // rotated bits beyond bit 15
value &= 0x7fff // first 15 bits
password ^= (value | rotatedBits)
}
password ^= int64(len(plaintext))
password ^= 0xCE4B
return strings.ToUpper(strconv.FormatInt(password, 16))
}
// getRootElement extract root element attributes by given XML decoder.
func getRootElement(d *xml.Decoder) []xml.Attr {
tokenIdx := 0
for {
token, _ := d.Token()
if token == nil {
break
}
switch startElement := token.(type) {
case xml.StartElement:
tokenIdx++
if tokenIdx == 1 {
return startElement.Attr
}
}
}
return nil
}
// genXMLNamespace generate serialized XML attributes with a multi namespace
// by given element attributes.
func genXMLNamespace(attr []xml.Attr) string {
var rootElement string
for _, v := range attr {
if lastSpace := getXMLNamespace(v.Name.Space, attr); lastSpace != "" {
rootElement += fmt.Sprintf("%s:%s=\"%s\" ", lastSpace, v.Name.Local, v.Value)
continue
}
rootElement += fmt.Sprintf("%s=\"%s\" ", v.Name.Local, v.Value)
}
return strings.TrimSpace(rootElement) + ">"
}
// getXMLNamespace extract XML namespace from specified element name and attributes.
func getXMLNamespace(space string, attr []xml.Attr) string {
for _, attribute := range attr {
if attribute.Value == space {
return attribute.Name.Local
}
}
return space
}
// replaceNameSpaceBytes provides a function to replace the XML root element
// attribute by the given component part path and XML content.
func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte {
var oldXmlns = []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
var newXmlns = []byte(templateNamespaceIDMap)
if attr, ok := f.xmlAttr[path]; ok {
newXmlns = []byte(genXMLNamespace(attr))
}
return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1)
}
// addNameSpaces provides a function to add a XML attribute by the given
// component part path.
func (f *File) addNameSpaces(path string, ns xml.Attr) {
exist := false
mc := false
ignore := -1
if attr, ok := f.xmlAttr[path]; ok {
for i, attribute := range attr {
if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space {
exist = true
}
if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" {
ignore = i
}
if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" {
mc = true
}
}
}
if !exist {
f.xmlAttr[path] = append(f.xmlAttr[path], ns)
if !mc {
2020-07-19 00:10:42 +08:00
f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility)
}
if ignore == -1 {
f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{
Name: xml.Name{Local: "Ignorable", Space: "mc"},
Value: ns.Name.Local,
})
return
}
f.setIgnorableNameSpace(path, ignore, ns)
}
}
// setIgnorableNameSpace provides a function to set XML namespace as ignorable by the given
// attribute.
func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) {
ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"}
if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 {
f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local))
}
}
// addSheetNameSpace add XML attribute for worksheet.
func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) {
name, _ := f.sheetMap[trimSheetName(sheet)]
f.addNameSpaces(name, ns)
}
// Stack defined an abstract data type that serves as a collection of elements.
type Stack struct {
list *list.List
}
// NewStack create a new stack.
func NewStack() *Stack {
list := list.New()
return &Stack{list}
}
// Push a value onto the top of the stack.
func (stack *Stack) Push(value interface{}) {
stack.list.PushBack(value)
}
// Pop the top item of the stack and return it.
func (stack *Stack) Pop() interface{} {
e := stack.list.Back()
if e != nil {
stack.list.Remove(e)
return e.Value
}
return nil
}
// Peek view the top item on the stack.
func (stack *Stack) Peek() interface{} {
e := stack.list.Back()
if e != nil {
return e.Value
}
return nil
}
// Len return the number of items in the stack.
func (stack *Stack) Len() int {
return stack.list.Len()
}
// Empty the stack.
func (stack *Stack) Empty() bool {
return stack.list.Len() == 0
}