Compare commits
191 Commits
Author | SHA1 | Date |
---|---|---|
Eng Zer Jun | c93618856a | |
xuri | 5f446f25f0 | |
Ilia Mirkin | 30d3561d0e | |
Ilia Mirkin | d2be5cdf8e | |
Ilia Mirkin | b7375bc6d4 | |
xuri | 0d5d1c53b2 | |
xuri | af190c7fdc | |
wushiling50 | d1937a0cde | |
xuri | f1d1a5dc2b | |
Jian Yu, Chen | b23e5a26df | |
liuwangchao | bebb802069 | |
xuri | 41c7dd30ce | |
ArcholSevier | 02189fb016 | |
xuri | ad8541790d | |
xuri | 8c0ef7f90d | |
Ben Smith | aca04ecf57 | |
Zhang Zhipeng | 0447cb22c8 | |
xuri | 29366fd126 | |
centurion-hub | 8f87131608 | |
wanghaochen2024 | 9c460ffe6c | |
zhangyimingdatiancai | c805be1f6f | |
xuri | 9a38657515 | |
xuri | d21b598235 | |
xuri | 30c4cd70e0 | |
xuri | d81b4c8661 | |
xuri | 4dd34477f7 | |
pjh591029530 | 68a1704900 | |
xuri | 9c278365f2 | |
wxy | 307e533061 | |
Patrick Wang | 431c31029e | |
xuri | 53b65150ce | |
ShowerBandV | 7999a492a4 | |
Aybek | b18b48099b | |
Vovka Morkovka | 4e6457accd | |
wangsongyan | f04aa8dd31 | |
联盟少侠 | 1a99dd4a23 | |
xuri | c349313850 | |
xiaokui | 08d25006f9 | |
xuri | 0c3dfb1605 | |
xuri | 42ad4d6959 | |
nna | 5f583549f4 | |
xuri | a64efca31f | |
barlevd | 781c38481d | |
xuri | 7715c1462a | |
xuri | 055349d8a6 | |
jianxinhou | f8487a68a8 | |
Nima | 3e636ae7b2 | |
xuri | 5f8a5b8690 | |
xuri | 5dc22e874b | |
xuri | 9999221450 | |
yunkeweb | ffad7aecb5 | |
yangyile-yyle88 | 5e500f5e5d | |
Matthew Sackman | 838232fd27 | |
xuri | 703b73779c | |
realzuojianxiang | 5975d87f7e | |
vic | 9e884c798b | |
hu5ky | 4eb088cf73 | |
yeahyear | 585ebff5b7 | |
Evan lu | 4ed493819a | |
xuri | f20bbd1f1d | |
Paolo Barbolini | 963a058535 | |
陈王 | 9d4c2e60f6 | |
岳晨旭 | 7b4da3906d | |
helloWorld | bb603b37d0 | |
xuri | 688808b2b4 | |
Ed | 02b84a906c | |
Vivek Kairi | ee2ef152d9 | |
zhukewen | 9cbe3b6bd0 | |
coolbit | bba155e06d | |
funa12 | a258e3d858 | |
xxxwang1983 | 99e91e19ef | |
cherry | 9a6855375e | |
Jerry | e4497c494d | |
L4nn15ter | 9b078980df | |
MELF晓宇 | 5399572353 | |
327674413 | 4eb3486682 | |
3zmx | 50e23df865 | |
xuri | 792656552b | |
xuri | f4e395137d | |
xuri | bb8e5dacd2 | |
xuri | 8831afc558 | |
xuri | e998c374ac | |
xuri | 37e2d946be | |
li | 00d62590f4 | |
yuegu520 | dfaf418f34 | |
Xuesong | 284345e471 | |
cui fliter | 7b3dd03947 | |
xuri | 866f3086cd | |
cui fliter | 77ece87e32 | |
天爱有情 | 18a160c5be | |
user65536 | a16182e004 | |
Bram Vanbilsen | 866e7fd9e1 | |
zcgly | bce2789c11 | |
xuri | 41259b474f | |
ZX | 6251d493b3 | |
Tian | 55e4d4b2c3 | |
lujin | 6220a798fd | |
Tajang | 57faaf253a | |
15535382838 | 3bdc2c5fc7 | |
Yang Li | 5e247de805 | |
xuri | 2499bf6b5b | |
xuri | c7acf4fafe | |
ByteFlyCoding | e014a8bb23 | |
Nick | 134865d9d2 | |
xuri | a0252bd05a | |
Marko Krstic | 6cc1a547ab | |
Anton Petrov | fe639faa45 | |
magicrabbit | f753e560fa | |
xuri | 4e936dafdd | |
xuri | 7291e787bd | |
rjtee | b41a6cc3cd | |
xuri | 5bba8f9805 | |
rjtee | cf3e0164d9 | |
rjtee | a8cbcfa39b | |
xuri | 05689d6ade | |
xuri | b52db71d61 | |
壹次心 | 27f1056929 | |
Eng Zer Jun | f752f2ddf4 | |
xuri | d9a0da7b48 | |
xuri | d133dc12d7 | |
xuri | 87a00e4f7e | |
xuri | 99df1a7343 | |
Abdelaziz-Ouhammou | 07f2c6831a | |
xuri | 95fc35f46c | |
xuri | df032fcae7 | |
dependabot[bot] | ecb4f62b77 | |
xuri | 0861faf2f2 | |
xuri | 1c7c417c70 | |
xuri | f85770f4c9 | |
xuri | 1c23dc3507 | |
xuri | c62d23e0a1 | |
xuri | 9c079e5eec | |
xuri | 744236b4b8 | |
xuri | e3b7dad69a | |
xuri | 5a039f3045 | |
xuri | 49706c9018 | |
Matthias Endler | a0a7d5cdbb | |
xuri | ae64bcaabe | |
Francis Nickels III | ff5657ba87 | |
xuri | 3b2b8ca8d6 | |
fsfsx | 4957ee9abc | |
xuri | 15614badfc | |
cnmlgbgithub | db22452398 | |
fsfsx | cb5a8e2d1e | |
xuri | 1b63d098a7 | |
xuri | c63ae6d262 | |
xuri | a1810aa056 | |
xuri | ae17fa87d5 | |
xuri | eb175906e7 | |
xuri | aa3c79a811 | |
xuri | 5fe30eb456 | |
xuri | a07c8cd0b4 | |
xuri | 2e9c2904f2 | |
xuri | 7f3d663628 | |
xuri | 8d996ca138 | |
xuri | b667987084 | |
David | 2c8dc5c150 | |
xuri | 8418bd7afd | |
xuri | f5fe6d3fc9 | |
xuri | fb72e56667 | |
lidp20 | e2c7416292 | |
xuri | 700af6a529 | |
xuri | dcb26b2cb8 | |
xuri | f8aa3adf7e | |
chengxinyao | 9bc3fd7e9f | |
xuri | 8e891b52c6 | |
vb6iscool | 78c974d855 | |
xuri | 661c0eade9 | |
xuri | 121ac17ca0 | |
壹次心 | e3fb2d7bad | |
joehan109 | 16efeae5b1 | |
xuri | 76cd0992b0 | |
Eng Zer Jun | c232748400 | |
xuri | a246db6a40 | |
xuri | 08ba2723fe | |
xuri | ef3e81de8e | |
xuri | 1088302331 | |
xuri | 49234fb95e | |
fudali | dfdd97c0a7 | |
xuri | bbdb83abf0 | |
xuri | 7c221cf295 | |
xuri | 65fc25e7a6 | |
xuri | 612f6f104c | |
xuri | 93c72b4d55 | |
Chen Zhidong | 787453c6f0 | |
xuri | 63d8a09082 | |
xuri | fb6ce60bd5 | |
xuri | d0ad0f39ec | |
xuri | 17c029494a | |
Valery Ozarnichuk | 635ec33576 | |
dependabot[bot] | 4196348f9f |
|
@ -1,5 +1,6 @@
|
|||
patreon: xuri
|
||||
github: xuri
|
||||
open_collective: excelize
|
||||
patreon: xuri
|
||||
ko_fi: xurime
|
||||
liberapay: xuri
|
||||
issuehunt: xuri
|
||||
|
|
|
@ -20,16 +20,16 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
|
@ -5,8 +5,8 @@ jobs:
|
|||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x, 1.23.x]
|
||||
os: [ubuntu-latest, macos-13, windows-latest]
|
||||
targetplatform: [x86, x64]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
@ -14,12 +14,13 @@ jobs:
|
|||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
|
@ -28,10 +29,12 @@ jobs:
|
|||
run: go build -v .
|
||||
|
||||
- name: Test
|
||||
run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile='coverage.txt' -covermode=atomic
|
||||
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
file: coverage.txt
|
||||
flags: unittests
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2016-2023 The excelize Authors.
|
||||
Copyright (c) 2016-2024 The excelize Authors.
|
||||
Copyright (c) 2011-2017 Geoffrey J. Teale
|
||||
All rights reserved.
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
<!--- see how your change affects other areas of the code, etc. -->
|
||||
<!--- See how your change affects other areas of the code, etc. -->
|
||||
|
||||
## Types of changes
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
Excelize is a library written in pure Go 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 data. This library needs Go version 1.16 or later. The full docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/).
|
||||
Excelize is a library written in pure Go 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 data. This library needs Go version 1.18 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. The full docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
|
@ -165,8 +165,10 @@ func main() {
|
|||
Categories: "Sheet1!$B$1:$D$1",
|
||||
Values: "Sheet1!$B$4:$D$4",
|
||||
}},
|
||||
Title: excelize.ChartTitle{
|
||||
Name: "Fruit 3D Clustered Column Chart",
|
||||
Title: []excelize.RichTextRun{
|
||||
{
|
||||
Text: "Fruit 3D Clustered Column Chart",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
fmt.Println(err)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
## 简介
|
||||
|
||||
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.16 或更高版本,完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。
|
||||
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.18 或更高版本,请注意,Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。
|
||||
|
||||
## 快速上手
|
||||
|
||||
|
@ -165,8 +165,10 @@ func main() {
|
|||
Categories: "Sheet1!$B$1:$D$1",
|
||||
Values: "Sheet1!$B$4:$D$4",
|
||||
}},
|
||||
Title: excelize.ChartTitle{
|
||||
Name: "Fruit 3D Clustered Column Chart",
|
||||
Title: []excelize.RichTextRun{
|
||||
{
|
||||
Text: "Fruit 3D Clustered Column Chart",
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
fmt.Println(err)
|
||||
|
|
901
adjust_test.go
901
adjust_test.go
File diff suppressed because it is too large
Load Diff
845
calc_test.go
845
calc_test.go
File diff suppressed because it is too large
Load Diff
44
calcchain.go
44
calcchain.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -58,8 +58,8 @@ func (f *File) deleteCalcChain(index int, cell string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
for k, v := range content.Overrides {
|
||||
if v.PartName == "/xl/calcChain.xml" {
|
||||
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
|
||||
|
@ -81,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa
|
|||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// volatileDepsReader provides a function to get the pointer to the structure
|
||||
// after deserialization of xl/volatileDependencies.xml.
|
||||
func (f *File) volatileDepsReader() (*xlsxVolTypes, error) {
|
||||
if f.VolatileDeps == nil {
|
||||
volatileDeps, ok := f.Pkg.Load(defaultXMLPathVolatileDeps)
|
||||
if !ok {
|
||||
return f.VolatileDeps, nil
|
||||
}
|
||||
f.VolatileDeps = new(xlsxVolTypes)
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(volatileDeps.([]byte)))).
|
||||
Decode(f.VolatileDeps); err != nil && err != io.EOF {
|
||||
return f.VolatileDeps, err
|
||||
}
|
||||
}
|
||||
return f.VolatileDeps, nil
|
||||
}
|
||||
|
||||
// volatileDepsWriter provides a function to save xl/volatileDependencies.xml
|
||||
// after serialize structure.
|
||||
func (f *File) volatileDepsWriter() {
|
||||
if f.VolatileDeps != nil {
|
||||
output, _ := xml.Marshal(f.VolatileDeps)
|
||||
f.saveFileList(defaultXMLPathVolatileDeps, output)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteVolTopicRef provides a function to remove cell reference on the
|
||||
// volatile dependencies topic.
|
||||
func (vt *xlsxVolTypes) deleteVolTopicRef(i1, i2, i3, i4 int) {
|
||||
for i := range vt.VolType[i1].Main[i2].Tp[i3].Tr {
|
||||
if i == i4 {
|
||||
vt.VolType[i1].Main[i2].Tp[i3].Tr = append(vt.VolType[i1].Main[i2].Tp[i3].Tr[:i], vt.VolType[i1].Main[i2].Tp[i3].Tr[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
430
cell.go
430
cell.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -15,11 +15,13 @@ import (
|
|||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// CellType is the type of cell value type.
|
||||
|
@ -71,7 +73,7 @@ func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error)
|
|||
if err != nil {
|
||||
return "", true, err
|
||||
}
|
||||
val, err := c.getValueFrom(f, sst, getOptions(opts...).RawCellValue)
|
||||
val, err := c.getValueFrom(f, sst, f.getOptions(opts...).RawCellValue)
|
||||
return val, true, err
|
||||
})
|
||||
}
|
||||
|
@ -142,7 +144,7 @@ func (f *File) SetCellValue(sheet, cell string, value interface{}) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = f.setDefaultTimeStyle(sheet, cell, 21)
|
||||
err = f.setDefaultTimeStyle(sheet, cell, getDurationNumFmt(v))
|
||||
case time.Time:
|
||||
err = f.setCellTimeFunc(sheet, cell, v)
|
||||
case bool:
|
||||
|
@ -215,15 +217,15 @@ func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error {
|
|||
case int64:
|
||||
err = f.SetCellInt(sheet, cell, int(v))
|
||||
case uint:
|
||||
err = f.SetCellInt(sheet, cell, int(v))
|
||||
err = f.SetCellUint(sheet, cell, uint64(v))
|
||||
case uint8:
|
||||
err = f.SetCellInt(sheet, cell, int(v))
|
||||
err = f.SetCellUint(sheet, cell, uint64(v))
|
||||
case uint16:
|
||||
err = f.SetCellInt(sheet, cell, int(v))
|
||||
err = f.SetCellUint(sheet, cell, uint64(v))
|
||||
case uint32:
|
||||
err = f.SetCellInt(sheet, cell, int(v))
|
||||
err = f.SetCellUint(sheet, cell, uint64(v))
|
||||
case uint64:
|
||||
err = f.SetCellInt(sheet, cell, int(v))
|
||||
err = f.SetCellUint(sheet, cell, v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -235,13 +237,13 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
ws.Unlock()
|
||||
ws.mu.Lock()
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
ws.mu.Unlock()
|
||||
var date1904, isNum bool
|
||||
wb, err := f.workbookReader()
|
||||
if err != nil {
|
||||
|
@ -254,7 +256,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error {
|
|||
return err
|
||||
}
|
||||
if isNum {
|
||||
_ = f.setDefaultTimeStyle(sheet, cell, 22)
|
||||
_ = f.setDefaultTimeStyle(sheet, cell, getTimeNumFmt(value))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -287,50 +289,84 @@ func setCellDuration(value time.Duration) (t string, v string) {
|
|||
// SetCellInt provides a function to set int type value of a cell by given
|
||||
// worksheet name, cell reference and cell value.
|
||||
func (f *File) SetCellInt(sheet, cell string, value int) error {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
c.T, c.V = setCellInt(value)
|
||||
c.IS = nil
|
||||
return f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
|
||||
// setCellInt prepares cell type and string type cell value by a given
|
||||
// integer.
|
||||
// setCellInt prepares cell type and string type cell value by a given integer.
|
||||
func setCellInt(value int) (t string, v string) {
|
||||
v = strconv.Itoa(value)
|
||||
return
|
||||
}
|
||||
|
||||
// SetCellUint provides a function to set uint type value of a cell by given
|
||||
// worksheet name, cell reference and cell value.
|
||||
func (f *File) SetCellUint(sheet, cell string, value uint64) error {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
c.T, c.V = setCellUint(value)
|
||||
c.IS = nil
|
||||
return f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
|
||||
// setCellUint prepares cell type and string type cell value by a given unsigned
|
||||
// integer.
|
||||
func setCellUint(value uint64) (t string, v string) {
|
||||
v = strconv.FormatUint(value, 10)
|
||||
return
|
||||
}
|
||||
|
||||
// SetCellBool provides a function to set bool type value of a cell by given
|
||||
// worksheet name, cell reference and cell value.
|
||||
func (f *File) SetCellBool(sheet, cell string, value bool) error {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
c.T, c.V = setCellBool(value)
|
||||
c.IS = nil
|
||||
return f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
|
||||
// setCellBool prepares cell type and string type cell value by a given
|
||||
// boolean value.
|
||||
// setCellBool prepares cell type and string type cell value by a given boolean
|
||||
// value.
|
||||
func setCellBool(value bool) (t string, v string) {
|
||||
t = "b"
|
||||
if value {
|
||||
|
@ -350,43 +386,55 @@ func setCellBool(value bool) (t string, v string) {
|
|||
// var x float32 = 1.325
|
||||
// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32)
|
||||
func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error {
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return f.SetCellStr(sheet, cell, fmt.Sprint(value))
|
||||
}
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
c.T, c.V = setCellFloat(value, precision, bitSize)
|
||||
c.IS = nil
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
c.setCellFloat(value, precision, bitSize)
|
||||
return f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
|
||||
// setCellFloat prepares cell type and string type cell value by a given
|
||||
// float value.
|
||||
func setCellFloat(value float64, precision, bitSize int) (t string, v string) {
|
||||
v = strconv.FormatFloat(value, 'f', precision, bitSize)
|
||||
// setCellFloat prepares cell type and string type cell value by a given float
|
||||
// value.
|
||||
func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) {
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
c.setInlineStr(fmt.Sprint(value))
|
||||
return
|
||||
}
|
||||
c.T, c.V = "", strconv.FormatFloat(value, 'f', precision, bitSize)
|
||||
c.IS = nil
|
||||
}
|
||||
|
||||
// SetCellStr provides a function to set string type value of a cell. Total
|
||||
// number of characters that a cell can contain 32767 characters.
|
||||
func (f *File) SetCellStr(sheet, cell, value string) error {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
if c.T, c.V, err = f.setCellString(value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -394,11 +442,10 @@ func (f *File) SetCellStr(sheet, cell, value string) error {
|
|||
return f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
|
||||
// setCellString provides a function to set string type to shared string
|
||||
// table.
|
||||
// setCellString provides a function to set string type to shared string table.
|
||||
func (f *File) setCellString(value string) (t, v string, err error) {
|
||||
if len(value) > TotalCellChars {
|
||||
value = value[:TotalCellChars]
|
||||
if utf8.RuneCountInString(value) > TotalCellChars {
|
||||
value = string([]rune(value)[:TotalCellChars])
|
||||
}
|
||||
t = "s"
|
||||
var si int
|
||||
|
@ -412,8 +459,8 @@ func (f *File) setCellString(value string) (t, v string, err error) {
|
|||
// 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()
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
|
||||
f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings))
|
||||
f.tempFiles.Delete(defaultXMLPathSharedStrings)
|
||||
|
@ -442,24 +489,33 @@ func (f *File) setSharedString(val string) (int, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if i, ok := f.sharedStringsMap[val]; ok {
|
||||
return i, nil
|
||||
}
|
||||
sst.Count++
|
||||
sst.UniqueCount++
|
||||
sst.mu.Lock()
|
||||
defer sst.mu.Unlock()
|
||||
t := xlsxT{Val: val}
|
||||
val, t.Space = trimCellValue(val)
|
||||
val, t.Space = trimCellValue(val, false)
|
||||
sst.SI = append(sst.SI, xlsxSI{T: &t})
|
||||
sst.Count = len(sst.SI)
|
||||
sst.UniqueCount = sst.Count
|
||||
f.sharedStringsMap[val] = sst.UniqueCount - 1
|
||||
return sst.UniqueCount - 1, nil
|
||||
}
|
||||
|
||||
// trimCellValue provides a function to set string type to cell.
|
||||
func trimCellValue(value string) (v string, ns xml.Attr) {
|
||||
if len(value) > TotalCellChars {
|
||||
value = value[:TotalCellChars]
|
||||
func trimCellValue(value string, escape bool) (v string, ns xml.Attr) {
|
||||
if utf8.RuneCountInString(value) > TotalCellChars {
|
||||
value = string([]rune(value)[:TotalCellChars])
|
||||
}
|
||||
if escape {
|
||||
var buf bytes.Buffer
|
||||
enc := xml.NewEncoder(&buf)
|
||||
_ = enc.EncodeToken(xml.CharData(value))
|
||||
enc.Flush()
|
||||
value = buf.String()
|
||||
}
|
||||
if len(value) > 0 {
|
||||
prefix, suffix := value[0], value[len(value)-1]
|
||||
|
@ -491,18 +547,16 @@ func (c *xlsxC) setCellValue(val string) {
|
|||
// string.
|
||||
func (c *xlsxC) setInlineStr(val string) {
|
||||
c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}}
|
||||
buf := &bytes.Buffer{}
|
||||
_ = xml.EscapeText(buf, []byte(val))
|
||||
c.IS.T.Val, c.IS.T.Space = trimCellValue(buf.String())
|
||||
c.IS.T.Val, c.IS.T.Space = trimCellValue(val, true)
|
||||
}
|
||||
|
||||
// setStr set cell data type and value which containing a formula string.
|
||||
func (c *xlsxC) setStr(val string) {
|
||||
c.T, c.IS = "str", nil
|
||||
c.V, c.XMLSpace = trimCellValue(val)
|
||||
c.V, c.XMLSpace = trimCellValue(val, false)
|
||||
}
|
||||
|
||||
// getCellDate parse cell value which containing a boolean.
|
||||
// getCellBool parse cell value which containing a boolean.
|
||||
func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) {
|
||||
if !raw {
|
||||
if c.V == "1" {
|
||||
|
@ -512,7 +566,7 @@ func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) {
|
|||
return "FALSE", nil
|
||||
}
|
||||
}
|
||||
return f.formattedValue(c.S, c.V, raw)
|
||||
return f.formattedValue(c, raw, CellTypeBool)
|
||||
}
|
||||
|
||||
// setCellDefault prepares cell type and string type cell value by a given
|
||||
|
@ -547,15 +601,13 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) {
|
|||
c.V = strconv.FormatFloat(excelTime, 'G', 15, 64)
|
||||
}
|
||||
}
|
||||
return f.formattedValue(c.S, c.V, raw)
|
||||
return f.formattedValue(c, raw, CellTypeDate)
|
||||
}
|
||||
|
||||
// getValueFrom return a value from a column/row cell, this function is
|
||||
// intended to be used with for range on rows an argument with the spreadsheet
|
||||
// opened file.
|
||||
func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
switch c.T {
|
||||
case "b":
|
||||
return c.getCellBool(f, raw)
|
||||
|
@ -563,21 +615,24 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
|
|||
return c.getCellDate(f, raw)
|
||||
case "s":
|
||||
if c.V != "" {
|
||||
xlsxSI := 0
|
||||
xlsxSI, _ = strconv.Atoi(strings.TrimSpace(c.V))
|
||||
xlsxSI, _ := strconv.Atoi(strings.TrimSpace(c.V))
|
||||
if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
|
||||
return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw)
|
||||
return f.formattedValue(&xlsxC{S: c.S, V: f.getFromStringItem(xlsxSI)}, raw, CellTypeSharedString)
|
||||
}
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if len(d.SI) > xlsxSI {
|
||||
return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw)
|
||||
return f.formattedValue(&xlsxC{S: c.S, V: d.SI[xlsxSI].String()}, raw, CellTypeSharedString)
|
||||
}
|
||||
}
|
||||
return f.formattedValue(c.S, c.V, raw)
|
||||
return f.formattedValue(c, raw, CellTypeSharedString)
|
||||
case "str":
|
||||
return c.V, nil
|
||||
case "inlineStr":
|
||||
if c.IS != nil {
|
||||
return f.formattedValue(c.S, c.IS.String(), raw)
|
||||
return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString)
|
||||
}
|
||||
return f.formattedValue(c.S, c.V, raw)
|
||||
return f.formattedValue(c, raw, CellTypeInlineString)
|
||||
default:
|
||||
if isNum, precision, decimal := isNumeric(c.V); isNum && !raw {
|
||||
if precision > 15 {
|
||||
|
@ -586,24 +641,27 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
|
|||
c.V = strconv.FormatFloat(decimal, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
return f.formattedValue(c.S, c.V, raw)
|
||||
return f.formattedValue(c, raw, CellTypeNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// SetCellDefault provides a function to set string type value of a cell as
|
||||
// default format without escaping the cell.
|
||||
func (f *File) SetCellDefault(sheet, cell, value string) error {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
c.setCellDefault(value)
|
||||
return f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
|
@ -611,7 +669,22 @@ func (f *File) SetCellDefault(sheet, cell, value string) error {
|
|||
// GetCellFormula provides a function to get formula from cell by given
|
||||
// worksheet name and cell reference in spreadsheet.
|
||||
func (f *File) GetCellFormula(sheet, cell string) (string, error) {
|
||||
return f.getCellFormula(sheet, cell, false)
|
||||
}
|
||||
|
||||
// getCellFormula provides a function to get transformed formula from cell by
|
||||
// given worksheet name and cell reference in spreadsheet.
|
||||
func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, error) {
|
||||
return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
|
||||
if transformed && !f.formulaChecked {
|
||||
if err := f.setArrayFormulaCells(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
f.formulaChecked = true
|
||||
}
|
||||
if transformed && c.f != "" {
|
||||
return c.f, true, nil
|
||||
}
|
||||
if c.F == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
|
@ -715,7 +788,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, _, _, err := f.prepareCell(ws, cell)
|
||||
c, _, _, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -736,6 +809,11 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
|
|||
return err
|
||||
}
|
||||
c.F.T = *opt.Type
|
||||
if c.F.T == STCellFormulaTypeArray && opt.Ref != nil {
|
||||
if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.F.T == STCellFormulaTypeShared {
|
||||
if err = ws.setSharedFormula(*opt.Ref); err != nil {
|
||||
return err
|
||||
|
@ -750,6 +828,67 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
|
|||
return err
|
||||
}
|
||||
|
||||
// setArrayFormula transform the array formula in an array formula range to the
|
||||
// normal formula and set cells in this range to the formula as the normal
|
||||
// formula.
|
||||
func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName) error {
|
||||
if len(strings.Split(formula.Ref, ":")) < 2 {
|
||||
return nil
|
||||
}
|
||||
coordinates, err := rangeRefToCoordinates(formula.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = sortCoordinates(coordinates)
|
||||
tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
topLeftCol, topLeftRow := coordinates[0], coordinates[1]
|
||||
for c := coordinates[0]; c <= coordinates[2]; c++ {
|
||||
for r := coordinates[1]; r <= coordinates[3]; r++ {
|
||||
colOffset, rowOffset := c-topLeftCol, r-topLeftRow
|
||||
for i, af := range arrayFormulaOperandTokens {
|
||||
colNum, rowNum := af.topLeftCol+colOffset, af.topLeftRow+rowOffset
|
||||
if colNum <= af.bottomRightCol && rowNum <= af.bottomRightRow {
|
||||
arrayFormulaOperandTokens[i].targetCellRef, _ = CoordinatesToCellName(colNum, rowNum)
|
||||
}
|
||||
}
|
||||
ws.prepareSheetXML(c, r)
|
||||
if cell := &ws.SheetData.Row[r-1].C[c-1]; cell.f == "" {
|
||||
cell.f = transformArrayFormula(tokens, arrayFormulaOperandTokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// setArrayFormulaCells transform the array formula in all worksheets to the
|
||||
// normal formula and set cells in the array formula reference range to the
|
||||
// formula as the normal formula.
|
||||
func (f *File) setArrayFormulaCells() error {
|
||||
definedNames := f.GetDefinedName()
|
||||
for _, sheetN := range f.GetSheetList() {
|
||||
ws, err := f.workSheetReader(sheetN)
|
||||
if err != nil {
|
||||
if err.Error() == newNotWorksheetError(sheetN).Error() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, row := range ws.SheetData.Row {
|
||||
for _, cell := range row.C {
|
||||
if cell.F != nil && cell.F.T == STCellFormulaTypeArray {
|
||||
if err = ws.setArrayFormula(sheetN, cell.F, definedNames); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSharedFormula set shared formula for the cells.
|
||||
func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
|
||||
coordinates, err := rangeRefToCoordinates(ref)
|
||||
|
@ -760,7 +899,7 @@ func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
|
|||
cnt := ws.countSharedFormula()
|
||||
for c := coordinates[0]; c <= coordinates[2]; c++ {
|
||||
for r := coordinates[1]; r <= coordinates[3]; r++ {
|
||||
prepareSheetXML(ws, c, r)
|
||||
ws.prepareSheetXML(c, r)
|
||||
cell := &ws.SheetData.Row[r-1].C[c-1]
|
||||
if cell.F == nil {
|
||||
cell.F = &xlsxF{}
|
||||
|
@ -825,14 +964,36 @@ type HyperlinkOpts struct {
|
|||
Tooltip *string
|
||||
}
|
||||
|
||||
// removeHyperLink remove hyperlink for worksheet and delete relationships for
|
||||
// the worksheet by given sheet name and cell reference. Note that if the cell
|
||||
// in a range reference, the whole hyperlinks will be deleted.
|
||||
func (f *File) removeHyperLink(ws *xlsxWorksheet, sheet, cell string) error {
|
||||
for idx := 0; idx < len(ws.Hyperlinks.Hyperlink); idx++ {
|
||||
link := ws.Hyperlinks.Hyperlink[idx]
|
||||
ok, err := f.checkCellInRangeRef(cell, link.Ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if link.Ref == cell || ok {
|
||||
ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink[:idx], ws.Hyperlinks.Hyperlink[idx+1:]...)
|
||||
idx--
|
||||
f.deleteSheetRelationships(sheet, link.RID)
|
||||
}
|
||||
}
|
||||
if len(ws.Hyperlinks.Hyperlink) == 0 {
|
||||
ws.Hyperlinks = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCellHyperLink provides a function to set cell hyperlink by given
|
||||
// worksheet name and link URL address. LinkType defines two types of
|
||||
// worksheet name and link URL address. LinkType defines three types of
|
||||
// hyperlink "External" for website or "Location" for moving to one of cell in
|
||||
// this workbook. Maximum limit hyperlinks in a worksheet is 65530. This
|
||||
// function is only used to set the hyperlink of the cell and doesn't affect
|
||||
// the value of the cell. If you need to set the value of the cell, please use
|
||||
// the other functions such as `SetCellStyle` or `SetSheetRow`. The below is
|
||||
// example for external link.
|
||||
// this workbook or "None" for remove hyperlink. Maximum limit hyperlinks in a
|
||||
// worksheet is 65530. This function is only used to set the hyperlink of the
|
||||
// cell and doesn't affect the value of the cell. If you need to set the value
|
||||
// of the cell, please use the other functions such as `SetCellStyle` or
|
||||
// `SetSheetRow`. The below is example for external link.
|
||||
//
|
||||
// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub"
|
||||
// if err := f.SetCellHyperLink("Sheet1", "A3",
|
||||
|
@ -864,7 +1025,7 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cell, err = f.mergeCellsParser(ws, cell); err != nil {
|
||||
if cell, err = ws.mergeCellsParser(cell); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -900,8 +1061,10 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
|
|||
Ref: cell,
|
||||
Location: link,
|
||||
}
|
||||
case "None":
|
||||
return f.removeHyperLink(ws, sheet, cell)
|
||||
default:
|
||||
return fmt.Errorf("invalid link type %q", linkType)
|
||||
return newInvalidLinkTypeError(linkType)
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
|
@ -922,6 +1085,9 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
|
|||
|
||||
// getCellRichText returns rich text of cell by given string item.
|
||||
func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
|
||||
if si.T != nil {
|
||||
runs = append(runs, RichTextRun{Text: si.T.Val})
|
||||
}
|
||||
for _, v := range si.R {
|
||||
run := RichTextRun{
|
||||
Text: v.T.Val,
|
||||
|
@ -941,12 +1107,19 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, _, _, err := f.prepareCell(ws, cell)
|
||||
c, _, _, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.T == "inlineStr" && c.IS != nil {
|
||||
runs = getCellRichText(c.IS)
|
||||
return
|
||||
}
|
||||
if c.T != "s" || c.V == "" {
|
||||
return
|
||||
}
|
||||
siIdx, err := strconv.Atoi(c.V)
|
||||
if err != nil || c.T != "s" {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sst, err := f.sharedStringsReader()
|
||||
|
@ -1030,7 +1203,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) {
|
|||
return textRuns, ErrCellCharsLength
|
||||
}
|
||||
run := xlsxR{T: &xlsxT{}}
|
||||
run.T.Val, run.T.Space = trimCellValue(textRun.Text)
|
||||
run.T.Val, run.T.Space = trimCellValue(textRun.Text, false)
|
||||
fnt := textRun.Font
|
||||
if fnt != nil {
|
||||
run.RPr = newRpr(fnt)
|
||||
|
@ -1168,14 +1341,14 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, col, row, err := f.prepareCell(ws, cell)
|
||||
c, col, row, err := ws.prepareCell(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.sharedStringsLoader(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.S = f.prepareCellStyle(ws, col, row, c.S)
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
si := xlsxSI{}
|
||||
sst, err := f.sharedStringsReader()
|
||||
if err != nil {
|
||||
|
@ -1249,9 +1422,9 @@ func (f *File) setSheetCells(sheet, cell string, slice interface{}, dir adjustDi
|
|||
}
|
||||
|
||||
// getCellInfo does common preparation for all set cell value functions.
|
||||
func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, error) {
|
||||
func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) {
|
||||
var err error
|
||||
cell, err = f.mergeCellsParser(ws, cell)
|
||||
cell, err = ws.mergeCellsParser(cell)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -1260,9 +1433,7 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
prepareSheetXML(ws, col, row)
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
ws.prepareSheetXML(col, row)
|
||||
return &ws.SheetData.Row[row-1].C[col-1], col, row, err
|
||||
}
|
||||
|
||||
|
@ -1270,11 +1441,16 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er
|
|||
// value function. Passed function implements specific part of required
|
||||
// logic.
|
||||
func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return "", err
|
||||
}
|
||||
cell, err = f.mergeCellsParser(ws, cell)
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
cell, err = ws.mergeCellsParser(cell)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -1282,10 +1458,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
|
||||
lastRowNum := 0
|
||||
if l := len(ws.SheetData.Row); l > 0 {
|
||||
lastRowNum = ws.SheetData.Row[l-1].R
|
||||
|
@ -1321,52 +1493,60 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
|
|||
// formattedValue provides a function to returns a value after formatted. If
|
||||
// it is possible to apply a format to the cell value, it will do so, if not
|
||||
// then an error will be returned, along with the raw value of the cell.
|
||||
func (f *File) formattedValue(s int, v string, raw bool) (string, error) {
|
||||
if raw {
|
||||
return v, nil
|
||||
}
|
||||
if s == 0 {
|
||||
return v, nil
|
||||
func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, error) {
|
||||
if raw || c.S == 0 {
|
||||
return c.V, nil
|
||||
}
|
||||
styleSheet, err := f.stylesReader()
|
||||
if err != nil {
|
||||
return v, err
|
||||
return c.V, err
|
||||
}
|
||||
if styleSheet.CellXfs == nil {
|
||||
return v, err
|
||||
return c.V, err
|
||||
}
|
||||
if s >= len(styleSheet.CellXfs.Xf) || s < 0 {
|
||||
return v, err
|
||||
if c.S >= len(styleSheet.CellXfs.Xf) || c.S < 0 {
|
||||
return c.V, err
|
||||
}
|
||||
var numFmtID int
|
||||
if styleSheet.CellXfs.Xf[s].NumFmtID != nil {
|
||||
numFmtID = *styleSheet.CellXfs.Xf[s].NumFmtID
|
||||
if styleSheet.CellXfs.Xf[c.S].NumFmtID != nil {
|
||||
numFmtID = *styleSheet.CellXfs.Xf[c.S].NumFmtID
|
||||
}
|
||||
date1904 := false
|
||||
wb, err := f.workbookReader()
|
||||
if err != nil {
|
||||
return v, err
|
||||
return c.V, err
|
||||
}
|
||||
if wb != nil && wb.WorkbookPr != nil {
|
||||
date1904 = wb.WorkbookPr.Date1904
|
||||
}
|
||||
if ok := builtInNumFmtFunc[numFmtID]; ok != nil {
|
||||
return ok(v, builtInNumFmt[numFmtID], date1904), err
|
||||
if fmtCode, ok := styleSheet.getCustomNumFmtCode(numFmtID); ok {
|
||||
return format(c.V, fmtCode, date1904, cellType, f.options), err
|
||||
}
|
||||
if styleSheet.NumFmts == nil {
|
||||
return v, err
|
||||
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
|
||||
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
|
||||
}
|
||||
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
|
||||
return c.V, err
|
||||
}
|
||||
|
||||
// getCustomNumFmtCode provides a function to returns custom number format code.
|
||||
func (ss *xlsxStyleSheet) getCustomNumFmtCode(numFmtID int) (string, bool) {
|
||||
if ss.NumFmts == nil {
|
||||
return "", false
|
||||
}
|
||||
for _, xlsxFmt := range ss.NumFmts.NumFmt {
|
||||
if xlsxFmt.NumFmtID == numFmtID {
|
||||
return format(v, xlsxFmt.FormatCode, date1904), err
|
||||
if xlsxFmt.FormatCode16 != "" {
|
||||
return xlsxFmt.FormatCode16, true
|
||||
}
|
||||
return xlsxFmt.FormatCode, true
|
||||
}
|
||||
}
|
||||
return v, err
|
||||
return "", false
|
||||
}
|
||||
|
||||
// prepareCellStyle provides a function to prepare style index of cell in
|
||||
// worksheet by given column index and style index.
|
||||
func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int {
|
||||
func (ws *xlsxWorksheet) prepareCellStyle(col, row, style int) int {
|
||||
if style != 0 {
|
||||
return style
|
||||
}
|
||||
|
@ -1387,7 +1567,7 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int {
|
|||
|
||||
// mergeCellsParser provides a function to check merged cells in worksheet by
|
||||
// given cell reference.
|
||||
func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) {
|
||||
func (ws *xlsxWorksheet) mergeCellsParser(cell string) (string, error) {
|
||||
cell = strings.ToUpper(cell)
|
||||
col, row, err := CellNameToCoordinates(cell)
|
||||
if err != nil {
|
||||
|
@ -1508,8 +1688,10 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) {
|
|||
// Note that this function not validate ref tag to check the cell whether in
|
||||
// allow range reference, and always return origin shared formula.
|
||||
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string {
|
||||
for _, r := range ws.SheetData.Row {
|
||||
for _, c := range r.C {
|
||||
for row := 0; row < len(ws.SheetData.Row); row++ {
|
||||
r := &ws.SheetData.Row[row]
|
||||
for column := 0; column < len(r.C); column++ {
|
||||
c := &r.C[column]
|
||||
if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si {
|
||||
col, row, _ := CellNameToCoordinates(cell)
|
||||
sharedCol, sharedRow, _ := CellNameToCoordinates(c.R)
|
||||
|
|
304
cell_test.go
304
cell_test.go
|
@ -3,6 +3,7 @@ package excelize
|
|||
import (
|
||||
"fmt"
|
||||
_ "image/jpeg"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
@ -41,6 +42,9 @@ func TestConcurrency(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
// Concurrency set cell style
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style))
|
||||
// Concurrency get cell style
|
||||
_, err = f.GetCellStyle("Sheet1", "A3")
|
||||
assert.NoError(t, err)
|
||||
// Concurrency add picture
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
|
||||
&GraphicOptions{
|
||||
|
@ -65,7 +69,7 @@ func TestConcurrency(t *testing.T) {
|
|||
// Concurrency iterate columns
|
||||
cols, err := f.Cols("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
for rows.Next() {
|
||||
for cols.Next() {
|
||||
_, err := cols.Rows()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -87,6 +91,14 @@ func TestConcurrency(t *testing.T) {
|
|||
visible, err := f.GetColVisible("Sheet1", "A")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, visible)
|
||||
// Concurrency add data validation
|
||||
dv := NewDataValidation(true)
|
||||
dv.Sqref = fmt.Sprintf("A%d:B%d", val, val)
|
||||
assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
|
||||
dv.SetInput(fmt.Sprintf("title:%d", val), strconv.Itoa(val))
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
// Concurrency delete data validation with reference sequence
|
||||
assert.NoError(t, f.DeleteDataValidation("Sheet1", dv.Sqref))
|
||||
wg.Done()
|
||||
}(i, t)
|
||||
}
|
||||
|
@ -96,6 +108,10 @@ func TestConcurrency(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, "1", val)
|
||||
// Test the length of data validation
|
||||
dataValidations, err := f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, dataValidations, 0)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
@ -133,11 +149,11 @@ func TestCheckCellInRangeRef(t *testing.T) {
|
|||
}
|
||||
|
||||
ok, err := f.checkCellInRangeRef("A1", "A:B")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
assert.False(t, ok)
|
||||
|
||||
ok, err = f.checkCellInRangeRef("AA0", "Z0:AB1")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")), err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
|
@ -171,15 +187,68 @@ func TestSetCellFloat(t *testing.T) {
|
|||
assert.Equal(t, "123.42", val, "A1 should be 123.42")
|
||||
})
|
||||
f := NewFile()
|
||||
assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellFloat(sheet, "A", 123.42, -1, 64))
|
||||
// Test set cell float data type value with invalid sheet name
|
||||
assert.EqualError(t, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64))
|
||||
}
|
||||
|
||||
func TestSetCellUint(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint8(math.MaxUint8)))
|
||||
result, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.Equal(t, "255", result)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint16)))
|
||||
result, err = f.GetCellValue("Sheet1", "A1")
|
||||
assert.Equal(t, "65535", result)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint32)))
|
||||
result, err = f.GetCellValue("Sheet1", "A1")
|
||||
assert.Equal(t, "4294967295", result)
|
||||
assert.NoError(t, err)
|
||||
// Test uint cell value not exists worksheet
|
||||
assert.EqualError(t, f.SetCellUint("SheetN", "A1", 1), "sheet SheetN does not exist")
|
||||
// Test uint cell value with illegal cell reference
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellUint("Sheet1", "A", 1))
|
||||
}
|
||||
|
||||
func TestSetCellValuesMultiByte(t *testing.T) {
|
||||
f := NewFile()
|
||||
row := []interface{}{
|
||||
// Test set cell value with multi byte characters value
|
||||
strings.Repeat("\u4E00", TotalCellChars+1),
|
||||
// Test set cell value with XML escape characters
|
||||
strings.Repeat("<>", TotalCellChars/2),
|
||||
strings.Repeat(">", TotalCellChars-1),
|
||||
strings.Repeat(">", TotalCellChars+1),
|
||||
}
|
||||
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &row))
|
||||
// Test set cell value with XML escape characters in stream writer
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
streamWriter, err := f.NewStreamWriter("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, streamWriter.SetRow("A1", row))
|
||||
assert.NoError(t, streamWriter.Flush())
|
||||
for _, sheetName := range []string{"Sheet1", "Sheet2"} {
|
||||
for cell, expected := range map[string]int{
|
||||
"A1": TotalCellChars,
|
||||
"B1": TotalCellChars - 1,
|
||||
"C1": TotalCellChars - 1,
|
||||
"D1": TotalCellChars,
|
||||
} {
|
||||
result, err := f.GetCellValue(sheetName, cell)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, []rune(result), expected)
|
||||
}
|
||||
}
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellValuesMultiByte.xlsx")))
|
||||
}
|
||||
|
||||
func TestSetCellValue(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Now().UTC()))
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Duration(1e13)))
|
||||
// Test set cell value with column and row style inherit
|
||||
style1, err := f.NewStyle(&Style{NumFmt: 2})
|
||||
assert.NoError(t, err)
|
||||
|
@ -197,7 +266,7 @@ func TestSetCellValue(t *testing.T) {
|
|||
assert.Equal(t, "0.50", B2)
|
||||
|
||||
// Test set cell value with invalid sheet name
|
||||
assert.EqualError(t, f.SetCellValue("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetCellValue("Sheet:1", "A1", "A1"))
|
||||
// Test set cell value with unsupported charset shared strings table
|
||||
f.SharedStrings = nil
|
||||
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
|
||||
|
@ -206,6 +275,59 @@ func TestSetCellValue(t *testing.T) {
|
|||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.SetCellValue("Sheet1", "A1", time.Now().UTC()), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test set cell value with the shared string table's count not equal with unique count
|
||||
f = NewFile()
|
||||
f.SharedStrings = nil
|
||||
f.Pkg.Store(defaultXMLPathSharedStrings, []byte(fmt.Sprintf(`<sst xmlns="%s" count="2" uniqueCount="1"><si><t>a</t></si><si><t>a</t></si></sst>`, NameSpaceSpreadSheet.Value)))
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
|
||||
SheetData: xlsxSheetData{Row: []xlsxRow{
|
||||
{R: 1, C: []xlsxC{{R: "A1", T: "str", V: "1"}}},
|
||||
}},
|
||||
})
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "b"))
|
||||
val, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "b", val)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "B1", "b"))
|
||||
val, err = f.GetCellValue("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "b", val)
|
||||
|
||||
f = NewFile()
|
||||
// Test set cell value with an IEEE 754 "not-a-number" value or infinity
|
||||
for num, expected := range map[float64]string{
|
||||
math.NaN(): "NaN",
|
||||
math.Inf(0): "+Inf",
|
||||
math.Inf(-1): "-Inf",
|
||||
} {
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", num))
|
||||
val, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, val)
|
||||
}
|
||||
// Test set cell value with time duration
|
||||
for val, expected := range map[time.Duration]string{
|
||||
time.Hour*21 + time.Minute*51 + time.Second*44: "21:51:44",
|
||||
time.Hour*21 + time.Minute*50: "21:50",
|
||||
time.Hour*24 + time.Minute*51 + time.Second*44: "24:51:44",
|
||||
time.Hour*24 + time.Minute*50: "24:50:00",
|
||||
} {
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", val))
|
||||
val, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, val)
|
||||
}
|
||||
// Test set cell value with time
|
||||
for val, expected := range map[time.Time]string{
|
||||
time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC): "Oct-24",
|
||||
time.Date(2024, time.October, 10, 0, 0, 0, 0, time.UTC): "10-10-24",
|
||||
time.Date(2024, time.October, 10, 12, 0, 0, 0, time.UTC): "10/10/24 12:00",
|
||||
} {
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", val))
|
||||
val, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCellValues(t *testing.T) {
|
||||
|
@ -215,7 +337,7 @@ func TestSetCellValues(t *testing.T) {
|
|||
|
||||
v, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v, "12/31/10 00:00")
|
||||
assert.Equal(t, v, "12-31-10")
|
||||
|
||||
// Test date value lower than min date supported by Excel
|
||||
err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC))
|
||||
|
@ -228,9 +350,9 @@ func TestSetCellValues(t *testing.T) {
|
|||
|
||||
func TestSetCellBool(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellBool("Sheet1", "A", true))
|
||||
// Test set cell boolean data type value with invalid sheet name
|
||||
assert.EqualError(t, f.SetCellBool("Sheet:1", "A1", true), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetCellBool("Sheet:1", "A1", true))
|
||||
}
|
||||
|
||||
func TestSetCellTime(t *testing.T) {
|
||||
|
@ -259,7 +381,7 @@ func TestGetCellValue(t *testing.T) {
|
|||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="3"><c t="inlineStr"><is><t>A3</t></is></c></row><row><c t="inlineStr"><is><t>A4</t></is></c><c t="inlineStr"><is><t>B4</t></is></c></row><row r="7"><c t="inlineStr"><is><t>A7</t></is></c><c t="inlineStr"><is><t>B7</t></is></c></row><row><c t="inlineStr"><is><t>A8</t></is></c><c t="inlineStr"><is><t>B8</t></is></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
cells := []string{"A3", "A4", "B4", "A7", "B7"}
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
|
||||
|
@ -275,35 +397,35 @@ func TestGetCellValue(t *testing.T) {
|
|||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="inlineStr"><is><t>A2</t></is></c></row><row r="2"><c r="B2" t="inlineStr"><is><t>B2</t></is></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
cell, err := f.GetCellValue("Sheet1", "A2")
|
||||
assert.Equal(t, "A2", cell)
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="inlineStr"><is><t>A2</t></is></c></row><row r="2"><c r="B2" t="inlineStr"><is><t>B2</t></is></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
rows, err = f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows)
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="1"><c r="A1" t="inlineStr"><is><t>A1</t></is></c></row><row r="1"><c r="B1" t="inlineStr"><is><t>B1</t></is></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
rows, err = f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{{"A1", "B1"}}, rows)
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row><c t="inlineStr"><is><t>A3</t></is></c></row><row><c t="inlineStr"><is><t>A4</t></is></c><c t="inlineStr"><is><t>B4</t></is></c></row><row r="7"><c t="inlineStr"><is><t>A7</t></is></c><c t="inlineStr"><is><t>B7</t></is></c></row><row><c t="inlineStr"><is><t>A8</t></is></c><c t="inlineStr"><is><t>B8</t></is></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
rows, err = f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="0"><c r="H6" t="inlineStr"><is><t>H6</t></is></c><c r="A1" t="inlineStr"><is><t>r0A6</t></is></c><c r="F4" t="inlineStr"><is><t>F4</t></is></c></row><row><c r="A1" t="inlineStr"><is><t>A6</t></is></c><c r="B1" t="inlineStr"><is><t>B6</t></is></c><c r="C1" t="inlineStr"><is><t>C6</t></is></c></row><row r="3"><c r="A3"><v>100</v></c><c r="B3" t="inlineStr"><is><t>B3</t></is></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
cell, err = f.GetCellValue("Sheet1", "H6")
|
||||
assert.Equal(t, "H6", cell)
|
||||
assert.NoError(t, err)
|
||||
|
@ -318,6 +440,16 @@ func TestGetCellValue(t *testing.T) {
|
|||
}, rows)
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row><c r="A1" t="inlineStr"><is><t>A1</t></is></c></row><row></row><row><c r="A3" t="inlineStr"><is><t>A3</t></is></c></row>`)))
|
||||
f.checked = sync.Map{}
|
||||
rows, err = f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{{"A1"}, nil, {"A3"}}, rows)
|
||||
assert.NoError(t, err)
|
||||
cell, err = f.GetCellValue("Sheet1", "A3")
|
||||
assert.Equal(t, "A3", cell)
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `
|
||||
<row r="1"><c r="A1"><v>2422.3000000000002</v></c></row>
|
||||
|
@ -356,7 +488,7 @@ func TestGetCellValue(t *testing.T) {
|
|||
<row r="34"><c r="A34" t="d"><v>20221022T150529Z</v></c></row>
|
||||
<row r="35"><c r="A35" t="d"><v>2022-10-22T15:05:29Z</v></c></row>
|
||||
<row r="36"><c r="A36" t="d"><v>2020-07-10 15:00:00.000</v></c></row>`)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
rows, err = f.GetCols("Sheet1")
|
||||
assert.Equal(t, []string{
|
||||
"2422.3",
|
||||
|
@ -405,7 +537,7 @@ func TestGetCellValue(t *testing.T) {
|
|||
assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get cell value with invalid sheet name
|
||||
_, err = f.GetCellValue("Sheet:1", "A1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
||||
func TestGetCellType(t *testing.T) {
|
||||
|
@ -418,10 +550,10 @@ func TestGetCellType(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, CellTypeSharedString, cellType)
|
||||
_, err = f.GetCellType("Sheet1", "A")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
// Test get cell type with invalid sheet name
|
||||
_, err = f.GetCellType("Sheet:1", "A1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
||||
func TestGetValueFrom(t *testing.T) {
|
||||
|
@ -447,7 +579,7 @@ func TestGetCellFormula(t *testing.T) {
|
|||
|
||||
// Test get cell formula with invalid sheet name
|
||||
_, err = f.GetCellFormula("Sheet:1", "A1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
|
||||
// Test get cell formula on no formula cell
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", true))
|
||||
|
@ -475,6 +607,25 @@ func TestGetCellFormula(t *testing.T) {
|
|||
formula, err := f.GetCellFormula("Sheet1", "B2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", formula)
|
||||
|
||||
// Test get array formula with invalid cell range reference
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
formulaType, ref := STCellFormulaTypeArray, "B1:B2"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet2", "B1", "A1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet3.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[1].F.Ref = ":"
|
||||
_, err = f.getCellFormula("Sheet2", "A1", true)
|
||||
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err)
|
||||
|
||||
// Test set formula for the cells in array formula range with unsupported charset
|
||||
f = NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.setArrayFormulaCells(), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func ExampleFile_SetCellFloat() {
|
||||
|
@ -533,10 +684,10 @@ func TestSetCellFormula(t *testing.T) {
|
|||
assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)"))
|
||||
|
||||
// Test set cell formula with invalid sheet name
|
||||
assert.EqualError(t, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"))
|
||||
|
||||
// Test set cell formula with illegal rows number
|
||||
assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), newCellNameToCoordinatesError("C", newInvalidCellNameError("C")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("C", newInvalidCellNameError("C")), f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"))
|
||||
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
|
@ -568,7 +719,7 @@ func TestSetCellFormula(t *testing.T) {
|
|||
ref = "D1:D5"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
ref = ""
|
||||
assert.EqualError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}), ErrParameterInvalid.Error())
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula5.xlsx")))
|
||||
|
||||
// Test set table formula for the cells
|
||||
|
@ -580,6 +731,14 @@ func TestSetCellFormula(t *testing.T) {
|
|||
formulaType = STCellFormulaTypeDataTable
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula6.xlsx")))
|
||||
|
||||
// Test set array formula with invalid cell range reference
|
||||
formulaType, ref = STCellFormulaTypeArray, ":"
|
||||
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
|
||||
// Test set array formula with invalid cell reference
|
||||
formulaType, ref = STCellFormulaTypeArray, "A1:A2"
|
||||
assert.Equal(t, ErrColumnNumber, f.SetCellFormula("Sheet1", "A1", "SUM(XFE1:XFE2)", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
}
|
||||
|
||||
func TestGetCellRichText(t *testing.T) {
|
||||
|
@ -621,32 +780,54 @@ func TestGetCellRichText(t *testing.T) {
|
|||
runsSource[1].Font.Color = strings.ToUpper(runsSource[1].Font.Color)
|
||||
assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font")
|
||||
|
||||
// Test get cell rich text when string item index overflow
|
||||
// Test get cell rich text with inlineStr
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "2"
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{
|
||||
T: "inlineStr",
|
||||
IS: &xlsxSI{
|
||||
T: &xlsxT{Val: "A"},
|
||||
R: []xlsxR{{T: &xlsxT{Val: "1"}}},
|
||||
},
|
||||
}
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []RichTextRun{{Text: "A"}, {Text: "1"}}, runs)
|
||||
|
||||
// Test get cell rich text when string item index overflow
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "2", IS: &xlsxSI{}}
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(runs))
|
||||
// Test get cell rich text when string item index is negative
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "-1"
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "-1", IS: &xlsxSI{}}
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(runs))
|
||||
// Test get cell rich text when string item index is invalid
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "A", IS: &xlsxSI{}}
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
|
||||
assert.Equal(t, 0, len(runs))
|
||||
// Test get cell rich text on invalid string item index
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "x"
|
||||
_, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax")
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "x"}
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(runs))
|
||||
// Test set cell rich text on not exists worksheet
|
||||
_, err = f.GetCellRichText("SheetN", "A1")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test set cell rich text with illegal cell reference
|
||||
_, err = f.GetCellRichText("Sheet1", "A")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
// Test set rich text color theme without tint
|
||||
assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}}))
|
||||
// Test set rich text color tint without theme
|
||||
|
@ -663,7 +844,7 @@ func TestGetCellRichText(t *testing.T) {
|
|||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get cell rich text with invalid sheet name
|
||||
_, err = f.GetCellRichText("Sheet:1", "A1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
||||
func TestSetCellRichText(t *testing.T) {
|
||||
|
@ -762,7 +943,7 @@ func TestSetCellRichText(t *testing.T) {
|
|||
// Test set cell rich text with invalid sheet name
|
||||
assert.EqualError(t, f.SetCellRichText("Sheet:1", "A1", richTextRun), ErrSheetNameInvalid.Error())
|
||||
// Test set cell rich text with illegal cell reference
|
||||
assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellRichText("Sheet1", "A", richTextRun))
|
||||
richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}}
|
||||
// Test set cell rich text with characters over the maximum limit
|
||||
assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error())
|
||||
|
@ -770,21 +951,21 @@ func TestSetCellRichText(t *testing.T) {
|
|||
|
||||
func TestFormattedValue(t *testing.T) {
|
||||
f := NewFile()
|
||||
result, err := f.formattedValue(0, "43528", false)
|
||||
result, err := f.formattedValue(&xlsxC{S: 0, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
// S is too large
|
||||
result, err = f.formattedValue(15, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: 15, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
// S is too small
|
||||
result, err = f.formattedValue(-15, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: -15, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
result, err = f.formattedValue(1, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
customNumFmt := "[$-409]MM/DD/YYYY"
|
||||
|
@ -792,7 +973,7 @@ func TestFormattedValue(t *testing.T) {
|
|||
CustomNumFmt: &customNumFmt,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
result, err = f.formattedValue(1, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "03/04/2019", result)
|
||||
|
||||
|
@ -801,7 +982,7 @@ func TestFormattedValue(t *testing.T) {
|
|||
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
|
||||
NumFmtID: &numFmtID,
|
||||
})
|
||||
result, err = f.formattedValue(2, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: 2, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
|
@ -809,7 +990,7 @@ func TestFormattedValue(t *testing.T) {
|
|||
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
|
||||
NumFmtID: nil,
|
||||
})
|
||||
result, err = f.formattedValue(3, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
|
@ -818,7 +999,16 @@ func TestFormattedValue(t *testing.T) {
|
|||
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
|
||||
NumFmtID: &numFmtID,
|
||||
})
|
||||
result, err = f.formattedValue(1, "43528", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
// Test format numeric value with shared string data type
|
||||
f.Styles.NumFmts, numFmtID = nil, 11
|
||||
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
|
||||
NumFmtID: &numFmtID,
|
||||
})
|
||||
result, err = f.formattedValue(&xlsxC{S: 5, V: "43528"}, false, CellTypeSharedString)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
|
||||
|
@ -827,32 +1017,32 @@ func TestFormattedValue(t *testing.T) {
|
|||
NumFmt: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
result, err = f.formattedValue(styleID, "310.56", false)
|
||||
result, err = f.formattedValue(&xlsxC{S: styleID, V: "310.56"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "311", result)
|
||||
|
||||
for _, fn := range builtInNumFmtFunc {
|
||||
assert.Equal(t, "0_0", fn("0_0", "", false))
|
||||
}
|
||||
assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber, nil))
|
||||
|
||||
// Test format value with unsupported charset workbook
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
_, err = f.formattedValue(1, "43528", false)
|
||||
_, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test format value with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
_, err = f.formattedValue(1, "43528", false)
|
||||
_, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
assert.Equal(t, "text", format("text", "0", false, CellTypeNumber, nil))
|
||||
}
|
||||
|
||||
func TestFormattedValueNilXfs(t *testing.T) {
|
||||
// Set the CellXfs to nil and verify that the formattedValue function does not crash
|
||||
f := NewFile()
|
||||
f.Styles.CellXfs = nil
|
||||
result, err := f.formattedValue(3, "43528", false)
|
||||
result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
}
|
||||
|
@ -861,7 +1051,7 @@ func TestFormattedValueNilNumFmts(t *testing.T) {
|
|||
// Set the NumFmts value to nil and verify that the formattedValue function does not crash
|
||||
f := NewFile()
|
||||
f.Styles.NumFmts = nil
|
||||
result, err := f.formattedValue(3, "43528", false)
|
||||
result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
}
|
||||
|
@ -870,7 +1060,7 @@ func TestFormattedValueNilWorkbook(t *testing.T) {
|
|||
// Set the Workbook value to nil and verify that the formattedValue function does not crash
|
||||
f := NewFile()
|
||||
f.WorkBook = nil
|
||||
result, err := f.formattedValue(3, "43528", false)
|
||||
result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
}
|
||||
|
@ -880,11 +1070,21 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) {
|
|||
// crash.
|
||||
f := NewFile()
|
||||
f.WorkBook.WorkbookPr = nil
|
||||
result, err := f.formattedValue(3, "43528", false)
|
||||
result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "43528", result)
|
||||
}
|
||||
|
||||
func TestGetCustomNumFmtCode(t *testing.T) {
|
||||
expected := "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"
|
||||
styleSheet := &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{
|
||||
{NumFmtID: 164, FormatCode16: expected},
|
||||
}}}
|
||||
numFmtCode, ok := styleSheet.getCustomNumFmtCode(164)
|
||||
assert.Equal(t, expected, numFmtCode)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestSharedStringsError(t *testing.T) {
|
||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
|
||||
assert.NoError(t, err)
|
||||
|
|
160
chart.go
160
chart.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -80,6 +80,30 @@ const (
|
|||
Bubble3D
|
||||
)
|
||||
|
||||
// ChartLineType is the type of supported chart line types.
|
||||
type ChartLineType byte
|
||||
|
||||
// This section defines the currently supported chart line types enumeration.
|
||||
const (
|
||||
ChartLineUnset ChartLineType = iota
|
||||
ChartLineSolid
|
||||
ChartLineNone
|
||||
ChartLineAutomatic
|
||||
)
|
||||
|
||||
// ChartTickLabelPositionType is the type of supported chart tick label position
|
||||
// types.
|
||||
type ChartTickLabelPositionType byte
|
||||
|
||||
// This section defines the supported chart tick label position types
|
||||
// enumeration.
|
||||
const (
|
||||
ChartTickLabelNextToAxis ChartTickLabelPositionType = iota
|
||||
ChartTickLabelHigh
|
||||
ChartTickLabelLow
|
||||
ChartTickLabelNone
|
||||
)
|
||||
|
||||
// This section defines the default value of chart properties.
|
||||
var (
|
||||
chartView3DRotX = map[ChartType]int{
|
||||
|
@ -474,7 +498,13 @@ var (
|
|||
true: "r",
|
||||
false: "l",
|
||||
}
|
||||
valTickLblPos = map[ChartType]string{
|
||||
tickLblPosVal = map[ChartTickLabelPositionType]string{
|
||||
ChartTickLabelNextToAxis: "nextTo",
|
||||
ChartTickLabelHigh: "high",
|
||||
ChartTickLabelLow: "low",
|
||||
ChartTickLabelNone: "none",
|
||||
}
|
||||
tickLblPosNone = map[ChartType]string{
|
||||
Contour: "none",
|
||||
WireframeContour: "none",
|
||||
}
|
||||
|
@ -499,26 +529,42 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
opts.Format.Locked = boolPtr(false)
|
||||
}
|
||||
if opts.Format.ScaleX == 0 {
|
||||
opts.Format.ScaleX = defaultPictureScale
|
||||
opts.Format.ScaleX = defaultDrawingScale
|
||||
}
|
||||
if opts.Format.ScaleY == 0 {
|
||||
opts.Format.ScaleY = defaultPictureScale
|
||||
opts.Format.ScaleY = defaultDrawingScale
|
||||
}
|
||||
if opts.Legend.Position == "" {
|
||||
opts.Legend.Position = defaultChartLegendPosition
|
||||
}
|
||||
if opts.Title.Name == "" {
|
||||
opts.Title.Name = " "
|
||||
}
|
||||
opts.parseTitle()
|
||||
if opts.VaryColors == nil {
|
||||
opts.VaryColors = boolPtr(true)
|
||||
}
|
||||
if opts.Border.Width == 0 {
|
||||
opts.Border.Width = 0.75
|
||||
}
|
||||
if opts.ShowBlanksAs == "" {
|
||||
opts.ShowBlanksAs = defaultChartShowBlanksAs
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// parseTitle parse the title settings of the chart with default value.
|
||||
func (opts *Chart) parseTitle() {
|
||||
for i := range opts.Title {
|
||||
if opts.Title[i].Font == nil {
|
||||
opts.Title[i].Font = &Font{}
|
||||
}
|
||||
if opts.Title[i].Font.Color == "" {
|
||||
opts.Title[i].Font.Color = "595959"
|
||||
}
|
||||
if opts.Title[i].Font.Size == 0 {
|
||||
opts.Title[i].Font.Size = 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddChart provides the method to add chart in a sheet by given chart format
|
||||
// set (such as offset, scale, aspect ratio setting and print settings) and
|
||||
// properties set. For example, create 3D clustered column chart with data
|
||||
|
@ -569,8 +615,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// Values: "Sheet1!$B$4:$D$4",
|
||||
// },
|
||||
// },
|
||||
// Title: excelize.ChartTitle{
|
||||
// Name: "Fruit 3D Clustered Column Chart",
|
||||
// Title: []excelize.RichTextRun{
|
||||
// {
|
||||
// Text: "Fruit 3D Clustered Column Chart",
|
||||
// },
|
||||
// },
|
||||
// Legend: excelize.ChartLegend{
|
||||
// ShowLegendKey: false,
|
||||
|
@ -660,11 +708,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
//
|
||||
// Name
|
||||
// Categories
|
||||
// Sizes
|
||||
// Values
|
||||
// Fill
|
||||
// Line
|
||||
// Marker
|
||||
// DataLabelPosition
|
||||
//
|
||||
// Name: Set the name for the series. The name is displayed in the chart legend
|
||||
// and in the formula bar. The 'Name' property is optional and if it isn't
|
||||
|
@ -675,13 +723,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// the same as the X axis. In most chart types the 'Categories' property is
|
||||
// optional and the chart will just assume a sequential series from 1..n.
|
||||
//
|
||||
// Sizes: This sets the bubble size in a data series.
|
||||
//
|
||||
// Values: This is the most important property of a series and is the only
|
||||
// mandatory option for every chart object. This option links the chart with
|
||||
// the worksheet data that it displays.
|
||||
//
|
||||
// Fill: This set the format for the data series fill.
|
||||
// Sizes: This sets the bubble size in a data series. The 'Sizes' property is
|
||||
// optional and the default value was same with 'Values'.
|
||||
//
|
||||
// Fill: This set the format for the data series fill. The 'Fill' property is
|
||||
// optional
|
||||
//
|
||||
// Line: This sets the line format of the line chart. The 'Line' property is
|
||||
// optional and if it isn't supplied it will default style. The options that
|
||||
|
@ -705,6 +755,8 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// x
|
||||
// auto
|
||||
//
|
||||
// DataLabelPosition: This sets the position of the chart series data label.
|
||||
//
|
||||
// Set properties of the chart legend. The options that can be set are:
|
||||
//
|
||||
// Position
|
||||
|
@ -727,7 +779,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
//
|
||||
// Title
|
||||
//
|
||||
// Name: Set the name (title) for the chart. The name is displayed above the
|
||||
// Title: Set the name (title) for the chart. The name is displayed above the
|
||||
// chart. The name can also be a formula such as Sheet1!$A$1 or a list with a
|
||||
// sheet name. The name property is optional. The default is to have no chart
|
||||
// title.
|
||||
|
@ -748,11 +800,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// Specifies that each data marker in the series has a different color by
|
||||
// 'VaryColors'. The default value is true.
|
||||
//
|
||||
// Set chart offset, scale, aspect ratio setting and print settings by format,
|
||||
// Set chart offset, scale, aspect ratio setting and print settings by 'Format',
|
||||
// same as function 'AddPicture'.
|
||||
//
|
||||
// Set the position of the chart plot area by PlotArea. The properties that can
|
||||
// be set are:
|
||||
// Set the position of the chart plot area by 'PlotArea'. The properties that
|
||||
// can be set are:
|
||||
//
|
||||
// SecondPlotValues
|
||||
// ShowBubbleSize
|
||||
|
@ -761,6 +813,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// ShowPercent
|
||||
// ShowSerName
|
||||
// ShowVal
|
||||
// NumFmt
|
||||
//
|
||||
// SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and
|
||||
// 'barOfPie' chart.
|
||||
|
@ -783,6 +836,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// ShowVal: Specifies that the value shall be shown in a data label.
|
||||
// The 'ShowVal' property is optional. The default value is false.
|
||||
//
|
||||
// NumFmt: Specifies that if linked to source and set custom number format code
|
||||
// for data labels. The 'NumFmt' property is optional. The default format code
|
||||
// is 'General'.
|
||||
//
|
||||
// Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'.
|
||||
// The properties of 'XAxis' that can be set are:
|
||||
//
|
||||
|
@ -793,7 +850,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// ReverseOrder
|
||||
// Maximum
|
||||
// Minimum
|
||||
// Alignment
|
||||
// Font
|
||||
// NumFmt
|
||||
// Title
|
||||
//
|
||||
// The properties of 'YAxis' that can be set are:
|
||||
//
|
||||
|
@ -801,10 +861,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// MajorGridLines
|
||||
// MinorGridLines
|
||||
// MajorUnit
|
||||
// Secondary
|
||||
// ReverseOrder
|
||||
// Maximum
|
||||
// Minimum
|
||||
// Alignment
|
||||
// Font
|
||||
// LogBase
|
||||
// NumFmt
|
||||
// Title
|
||||
//
|
||||
// None: Disable axes.
|
||||
//
|
||||
|
@ -813,14 +878,18 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// MinorGridLines: Specifies minor grid lines.
|
||||
//
|
||||
// MajorUnit: Specifies the distance between major ticks. Shall contain a
|
||||
// positive floating-point number. The MajorUnit property is optional. The
|
||||
// positive floating-point number. The 'MajorUnit' property is optional. The
|
||||
// default value is auto.
|
||||
//
|
||||
// Secondary: Specifies the current series vertical axis as the secondary axis,
|
||||
// this only works for the second and later chart in the combo chart. The
|
||||
// default value is false.
|
||||
//
|
||||
// TickLabelSkip: Specifies how many tick labels to skip between label that is
|
||||
// drawn. The 'TickLabelSkip' property is optional. The default value is auto.
|
||||
//
|
||||
// ReverseOrder: Specifies that the categories or values on reverse order
|
||||
// (orientation of the chart). The ReverseOrder property is optional. The
|
||||
// (orientation of the chart). The 'ReverseOrder' property is optional. The
|
||||
// default value is false.
|
||||
//
|
||||
// Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property
|
||||
|
@ -829,6 +898,24 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property
|
||||
// is optional. The default value is auto.
|
||||
//
|
||||
// Alignment: Specifies that the alignment of the horizontal and vertical axis.
|
||||
// The properties of font that can be set are:
|
||||
//
|
||||
// TextRotation
|
||||
// Vertical
|
||||
//
|
||||
// The value of 'TextRotation' that can be set from -90 to 90:
|
||||
//
|
||||
// The value of 'Vertical' that can be set are:
|
||||
//
|
||||
// horz
|
||||
// vert
|
||||
// vert270
|
||||
// wordArtVert
|
||||
// eaVert
|
||||
// mongolianVert
|
||||
// wordArtVertRtl
|
||||
//
|
||||
// Font: Specifies that the font of the horizontal and vertical axis. The
|
||||
// properties of font that can be set are:
|
||||
//
|
||||
|
@ -841,8 +928,26 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// Color
|
||||
// VertAlign
|
||||
//
|
||||
// LogBase: Specifies logarithmic scale base number of the vertical axis.
|
||||
//
|
||||
// NumFmt: Specifies that if linked to source and set custom number format code
|
||||
// for axis. The 'NumFmt' property is optional. The default format code is
|
||||
// 'General'.
|
||||
//
|
||||
// Title: Specifies that the primary horizontal or vertical axis title and
|
||||
// resize chart. The 'Title' property is optional.
|
||||
//
|
||||
// Set chart size by 'Dimension' property. The 'Dimension' property is optional.
|
||||
// The default width is 480, and height is 290.
|
||||
// The default width is 480, and height is 260.
|
||||
//
|
||||
// Set the bubble size in all data series for the bubble chart or 3D bubble
|
||||
// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. The
|
||||
// default width is 100, and the value should be great than 0 and less or equal
|
||||
// than 300.
|
||||
//
|
||||
// Set the doughnut hole size in all data series for the doughnut chart by
|
||||
// 'HoleSize' property. The 'HoleSize' property is optional. The default width
|
||||
// is 75, and the value should be great than 0 and less or equal than 90.
|
||||
//
|
||||
// combo: Specifies the create a chart that combines two or more chart types in
|
||||
// a single chart. For example, create a clustered column - line chart with
|
||||
|
@ -876,7 +981,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// }
|
||||
// enable, disable := true, false
|
||||
// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
|
||||
// Type: "col",
|
||||
// Type: excelize.Col,
|
||||
// Series: []excelize.ChartSeries{
|
||||
// {
|
||||
// Name: "Sheet1!$A$2",
|
||||
|
@ -893,8 +998,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// LockAspectRatio: false,
|
||||
// Locked: &disable,
|
||||
// },
|
||||
// Title: excelize.ChartTitle{
|
||||
// Name: "Clustered Column - Line Chart",
|
||||
// Title: []excelize.RichTextRun{
|
||||
// {
|
||||
// Text: "Clustered Column - Line Chart",
|
||||
// },
|
||||
// },
|
||||
// Legend: excelize.ChartLegend{
|
||||
// Position: "left",
|
||||
|
@ -908,7 +1015,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// ShowVal: true,
|
||||
// },
|
||||
// }, &excelize.Chart{
|
||||
// Type: "line",
|
||||
// Type: excelize.Line,
|
||||
// Series: []excelize.ChartSeries{
|
||||
// {
|
||||
// Name: "Sheet1!$A$4",
|
||||
|
@ -1078,7 +1185,8 @@ func (f *File) DeleteChart(sheet, cell string) error {
|
|||
return err
|
||||
}
|
||||
drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl")
|
||||
return f.deleteDrawing(col, row, drawingXML, "Chart")
|
||||
_, err = f.deleteDrawing(col, row, drawingXML, "Chart")
|
||||
return err
|
||||
}
|
||||
|
||||
// countCharts provides a function to get chart files count storage in the
|
||||
|
|
208
chart_test.go
208
chart_test.go
|
@ -52,7 +52,7 @@ func TestChartSize(t *testing.T) {
|
|||
{Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"},
|
||||
{Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"},
|
||||
},
|
||||
Title: ChartTitle{Name: "3D Clustered Column Chart"},
|
||||
Title: []RichTextRun{{Text: "3D Clustered Column Chart"}},
|
||||
}))
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
@ -70,7 +70,7 @@ func TestChartSize(t *testing.T) {
|
|||
|
||||
var (
|
||||
workdir decodeWsDr
|
||||
anchor decodeTwoCellAnchor
|
||||
anchor decodeCellAnchor
|
||||
)
|
||||
|
||||
content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml")
|
||||
|
@ -81,8 +81,8 @@ func TestChartSize(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
err = xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+
|
||||
workdir.TwoCellAnchor[0].Content+"</decodeTwoCellAnchor>"), &anchor)
|
||||
err = xml.Unmarshal([]byte("<decodeCellAnchor>"+
|
||||
workdir.TwoCellAnchor[0].Content+"</decodeCellAnchor>"), &anchor)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ func TestChartSize(t *testing.T) {
|
|||
}
|
||||
|
||||
if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") ||
|
||||
!assert.Equal(t, 27, anchor.To.Row, "Expected 'to' row 27") {
|
||||
!assert.Equal(t, 29, anchor.To.Row, "Expected 'to' row 29") {
|
||||
|
||||
t.FailNow()
|
||||
}
|
||||
|
@ -120,7 +120,15 @@ func TestDeleteDrawing(t *testing.T) {
|
|||
f := NewFile()
|
||||
path := "xl/drawings/drawing1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteDrawing(0, 0, path, "Chart"), "XML syntax error on line 1: invalid UTF-8")
|
||||
_, err := f.deleteDrawing(0, 0, path, "Chart")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
f.Drawings.Store(path, &xlsxWsDr{TwoCellAnchor: []*xdrCellAnchor{{
|
||||
GraphicFrame: string(MacintoshCyrillicCharset),
|
||||
}}})
|
||||
_, err = f.deleteDrawing(0, 0, path, "Chart")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestAddChart(t *testing.T) {
|
||||
|
@ -150,7 +158,12 @@ func TestAddChart(t *testing.T) {
|
|||
{Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"},
|
||||
{Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"},
|
||||
{Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"},
|
||||
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"},
|
||||
{
|
||||
Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37",
|
||||
Marker: ChartMarker{
|
||||
Fill: Fill{Type: "pattern", Color: []string{"FFFF00"}, Pattern: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
series2 := []ChartSeries{
|
||||
{
|
||||
|
@ -168,18 +181,18 @@ func TestAddChart(t *testing.T) {
|
|||
}
|
||||
series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}
|
||||
series4 := []ChartSeries{
|
||||
{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30"},
|
||||
{Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31"},
|
||||
{Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32"},
|
||||
{Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33"},
|
||||
{Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34"},
|
||||
{Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35"},
|
||||
{Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36"},
|
||||
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37"},
|
||||
{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30", DataLabelPosition: ChartDataLabelsPositionAbove},
|
||||
{Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31", DataLabelPosition: ChartDataLabelsPositionLeft},
|
||||
{Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32", DataLabelPosition: ChartDataLabelsPositionBestFit},
|
||||
{Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33", DataLabelPosition: ChartDataLabelsPositionCenter},
|
||||
{Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34", DataLabelPosition: ChartDataLabelsPositionInsideBase},
|
||||
{Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35", DataLabelPosition: ChartDataLabelsPositionInsideEnd},
|
||||
{Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36", DataLabelPosition: ChartDataLabelsPositionOutsideEnd},
|
||||
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37", DataLabelPosition: ChartDataLabelsPositionRight},
|
||||
}
|
||||
format := GraphicOptions{
|
||||
ScaleX: defaultPictureScale,
|
||||
ScaleY: defaultPictureScale,
|
||||
ScaleX: defaultDrawingScale,
|
||||
ScaleY: defaultDrawingScale,
|
||||
OffsetX: 15,
|
||||
OffsetY: 10,
|
||||
PrintObject: boolPtr(true),
|
||||
|
@ -195,74 +208,75 @@ func TestAddChart(t *testing.T) {
|
|||
ShowPercent: true,
|
||||
ShowSerName: true,
|
||||
ShowVal: true,
|
||||
Fill: Fill{Type: "pattern", Pattern: 1},
|
||||
}
|
||||
for _, c := range []struct {
|
||||
sheetName, cell string
|
||||
opts *Chart
|
||||
}{
|
||||
{sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}},
|
||||
{sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}},
|
||||
{sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: Col3DCone, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: Col3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: Col3DPyramid, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}},
|
||||
{sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}},
|
||||
{sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}},
|
||||
{sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}},
|
||||
{sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, Color: "000000"}, Title: []RichTextRun{{Text: "Primary Horizontal Axis Title"}}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}, Title: []RichTextRun{{Text: "Primary Vertical Axis Title", Font: &Font{Color: "777777", Bold: true, Italic: true, Size: 12}}}}}},
|
||||
{sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}},
|
||||
{sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert", TextRotation: 0}}}},
|
||||
{sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}},
|
||||
{sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: Col3DCone, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: Col3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: Col3DPyramid, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert270", TextRotation: 0}}}},
|
||||
{sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}},
|
||||
{sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Scatter Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Doughnut Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}},
|
||||
{sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, TickLabelPosition: ChartTickLabelLow}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}},
|
||||
{sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Pie Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}},
|
||||
// bar series chart
|
||||
{sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}},
|
||||
{sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}},
|
||||
{sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked 100% Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}},
|
||||
{sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}},
|
||||
// area series chart
|
||||
{sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
// cylinder series chart
|
||||
{sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: Bar3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: Bar3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
// cone series chart
|
||||
{sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: Bar3DConeStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: Bar3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: Bar3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: Bar3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
// surface series chart
|
||||
{sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: Surface3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: WireframeSurface3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Wireframe Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Wireframe Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: Surface3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Surface Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: WireframeSurface3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Wireframe Surface Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Wireframe Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
// bubble chart
|
||||
{sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", BubbleSize: 75}},
|
||||
{sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
// pie of pie chart
|
||||
{sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: PieOfPie, Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Pie of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: PieOfPie, Series: series3, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Pie of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
// bar of pie chart
|
||||
{sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: BarOfPie, Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
{sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: BarOfPie, Series: series3, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
|
||||
} {
|
||||
assert.NoError(t, f.AddChart(c.sheetName, c.cell, c.opts))
|
||||
}
|
||||
|
@ -274,32 +288,32 @@ func TestAddChart(t *testing.T) {
|
|||
{"I1", Doughnut, "Clustered Column - Doughnut Chart"},
|
||||
}
|
||||
for _, props := range clusteredColumnCombo {
|
||||
assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}))
|
||||
assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[2].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}, YAxis: ChartAxis{Secondary: true}}))
|
||||
}
|
||||
stackedAreaCombo := map[string][]interface{}{
|
||||
"A16": {Line, "Stacked Area - Line Chart"},
|
||||
"I16": {Doughnut, "Stacked Area - Doughnut Chart"},
|
||||
}
|
||||
for axis, props := range stackedAreaCombo {
|
||||
assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[1].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}))
|
||||
assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[1].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}))
|
||||
}
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx")))
|
||||
// Test with invalid sheet name
|
||||
assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: Col, Series: series[:1]}), ErrSheetNameInvalid.Error())
|
||||
// Test with illegal cell reference
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
// Test with unsupported chart type
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error())
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error())
|
||||
// Test add combo chart with invalid format set
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error())
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error())
|
||||
// Test add combo chart with unsupported chart type
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error())
|
||||
assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error())
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add chart with unsupported charset content types.
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestAddChartSheet(t *testing.T) {
|
||||
|
@ -317,7 +331,7 @@ func TestAddChartSheet(t *testing.T) {
|
|||
{Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"},
|
||||
{Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"},
|
||||
}
|
||||
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}))
|
||||
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}))
|
||||
// Test set the chartsheet as active sheet
|
||||
var sheetIdx int
|
||||
for idx, sheetName := range f.GetSheetList() {
|
||||
|
@ -332,11 +346,11 @@ func TestAddChartSheet(t *testing.T) {
|
|||
assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet")
|
||||
// Test add chartsheet on already existing name sheet
|
||||
|
||||
assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrExistsSheet.Error())
|
||||
assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrExistsSheet.Error())
|
||||
// Test add chartsheet with invalid sheet name
|
||||
assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrSheetNameInvalid.Error())
|
||||
assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrSheetNameInvalid.Error())
|
||||
// Test with unsupported chart type
|
||||
assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), newUnsupportedChartType(0x37).Error())
|
||||
assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), newUnsupportedChartType(0x37).Error())
|
||||
|
||||
assert.NoError(t, f.UpdateLinkedValue())
|
||||
|
||||
|
@ -345,7 +359,7 @@ func TestAddChartSheet(t *testing.T) {
|
|||
f = NewFile()
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestDeleteChart(t *testing.T) {
|
||||
|
@ -363,8 +377,8 @@ func TestDeleteChart(t *testing.T) {
|
|||
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"},
|
||||
}
|
||||
format := GraphicOptions{
|
||||
ScaleX: defaultPictureScale,
|
||||
ScaleY: defaultPictureScale,
|
||||
ScaleX: defaultDrawingScale,
|
||||
ScaleY: defaultDrawingScale,
|
||||
OffsetX: 15,
|
||||
OffsetY: 10,
|
||||
PrintObject: boolPtr(true),
|
||||
|
@ -380,7 +394,7 @@ func TestDeleteChart(t *testing.T) {
|
|||
ShowSerName: true,
|
||||
ShowVal: true,
|
||||
}
|
||||
assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}))
|
||||
assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}))
|
||||
assert.NoError(t, f.DeleteChart("Sheet1", "P1"))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx")))
|
||||
// Test delete chart with invalid sheet name
|
||||
|
@ -395,7 +409,7 @@ func TestDeleteChart(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChartWithLogarithmicBase(t *testing.T) {
|
||||
// Create test XLSX file with data
|
||||
// Create test workbook with data
|
||||
f := NewFile()
|
||||
sheet1 := f.GetSheetName(0)
|
||||
categories := map[string]float64{
|
||||
|
@ -429,25 +443,25 @@ func TestChartWithLogarithmicBase(t *testing.T) {
|
|||
cell string
|
||||
opts *Chart
|
||||
}{
|
||||
{cell: "C1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}},
|
||||
{cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}},
|
||||
{cell: "A25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}},
|
||||
{cell: "F25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}},
|
||||
{cell: "K25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}},
|
||||
{cell: "P25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}},
|
||||
{cell: "C1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "Line chart without log scaling"}}}},
|
||||
{cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 10.5 scaling"}}, YAxis: ChartAxis{LogBase: 10.5}}},
|
||||
{cell: "A25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1.9 scaling"}}, YAxis: ChartAxis{LogBase: 1.9}}},
|
||||
{cell: "F25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 2 scaling"}}, YAxis: ChartAxis{LogBase: 2}}},
|
||||
{cell: "K25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1000.1 scaling"}}, YAxis: ChartAxis{LogBase: 1000.1}}},
|
||||
{cell: "P25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1000 scaling"}}, YAxis: ChartAxis{LogBase: 1000}}},
|
||||
} {
|
||||
// Add two chart, one without and one with log scaling
|
||||
assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts))
|
||||
}
|
||||
|
||||
// Export XLSX file for human confirmation
|
||||
// Export workbook for human confirmation
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx")))
|
||||
|
||||
// Write the XLSX file to a buffer
|
||||
// Write the workbook to a buffer
|
||||
var buffer bytes.Buffer
|
||||
assert.NoError(t, f.Write(&buffer))
|
||||
|
||||
// Read back the XLSX file from the buffer
|
||||
// Read back the workbook from the buffer
|
||||
newFile, err := OpenReader(&buffer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
143
col.go
143
col.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/tiendc/go-deepcopy"
|
||||
)
|
||||
|
||||
// Define the default cell size and EMU unit of measurement.
|
||||
|
@ -92,7 +92,7 @@ func (cols *Cols) Rows(opts ...Options) ([]string, error) {
|
|||
if cols.stashCol >= cols.curCol {
|
||||
return rowIterator.cells, rowIterator.err
|
||||
}
|
||||
cols.rawCellValue = getOptions(opts...).RawCellValue
|
||||
cols.rawCellValue = cols.f.getOptions(opts...).RawCellValue
|
||||
if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil {
|
||||
return rowIterator.cells, rowIterator.err
|
||||
}
|
||||
|
@ -215,11 +215,11 @@ func (f *File) Cols(sheet string) (*Cols, error) {
|
|||
if !ok {
|
||||
return nil, ErrSheetNotExist{sheet}
|
||||
}
|
||||
if ws, ok := f.Sheet.Load(name); ok && ws != nil {
|
||||
worksheet := ws.(*xlsxWorksheet)
|
||||
worksheet.Lock()
|
||||
defer worksheet.Unlock()
|
||||
output, _ := xml.Marshal(worksheet)
|
||||
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
|
||||
ws := worksheet.(*xlsxWorksheet)
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
output, _ := xml.Marshal(ws)
|
||||
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
|
||||
}
|
||||
var colIterator columnXMLIterator
|
||||
|
@ -257,12 +257,15 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
|
|||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return false, err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
if ws.Cols == nil {
|
||||
return true, err
|
||||
}
|
||||
|
@ -287,7 +290,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
|
|||
//
|
||||
// err := f.SetColVisible("Sheet1", "D:F", false)
|
||||
func (f *File) SetColVisible(sheet, columns string, visible bool) error {
|
||||
min, max, err := f.parseColRange(columns)
|
||||
minVal, maxVal, err := f.parseColRange(columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -295,11 +298,11 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
colData := xlsxCol{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Min: minVal,
|
||||
Max: maxVal,
|
||||
Width: float64Ptr(defaultColWidth),
|
||||
Hidden: !visible,
|
||||
CustomWidth: true,
|
||||
|
@ -351,20 +354,20 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) {
|
|||
}
|
||||
|
||||
// parseColRange parse and convert column range with column name to the column number.
|
||||
func (f *File) parseColRange(columns string) (min, max int, err error) {
|
||||
func (f *File) parseColRange(columns string) (minVal, maxVal int, err error) {
|
||||
colsTab := strings.Split(columns, ":")
|
||||
min, err = ColumnNameToNumber(colsTab[0])
|
||||
minVal, err = ColumnNameToNumber(colsTab[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
max = min
|
||||
maxVal = minVal
|
||||
if len(colsTab) == 2 {
|
||||
if max, err = ColumnNameToNumber(colsTab[1]); err != nil {
|
||||
if maxVal, err = ColumnNameToNumber(colsTab[1]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if max < min {
|
||||
min, max = max, min
|
||||
if maxVal < minVal {
|
||||
minVal, maxVal = maxVal, minVal
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -424,32 +427,40 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
|
|||
//
|
||||
// err = f.SetColStyle("Sheet1", "C:F", style)
|
||||
func (f *File) SetColStyle(sheet, columns string, styleID int) error {
|
||||
min, max, err := f.parseColRange(columns)
|
||||
minVal, maxVal, err := f.parseColRange(columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.mu.Lock()
|
||||
s, err := f.stylesReader()
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
s.Lock()
|
||||
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
|
||||
s.Unlock()
|
||||
return newInvalidStyleID(styleID)
|
||||
}
|
||||
s.Unlock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
f.mu.Unlock()
|
||||
s.mu.Lock()
|
||||
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
|
||||
s.mu.Unlock()
|
||||
return newInvalidStyleID(styleID)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
if ws.Cols == nil {
|
||||
ws.Cols = &xlsxCols{}
|
||||
}
|
||||
width := defaultColWidth
|
||||
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
|
||||
width = ws.SheetFormatPr.DefaultColWidth
|
||||
}
|
||||
ws.Cols.Col = flatCols(xlsxCol{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Width: float64Ptr(defaultColWidth),
|
||||
Min: minVal,
|
||||
Max: maxVal,
|
||||
Width: float64Ptr(width),
|
||||
Style: styleID,
|
||||
}, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
|
||||
fc.BestFit = c.BestFit
|
||||
|
@ -461,9 +472,9 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
|
|||
fc.Width = c.Width
|
||||
return fc
|
||||
})
|
||||
ws.Unlock()
|
||||
ws.mu.Unlock()
|
||||
if rows := len(ws.SheetData.Row); rows > 0 {
|
||||
for col := min; col <= max; col++ {
|
||||
for col := minVal; col <= maxVal; col++ {
|
||||
from, _ := CoordinatesToCellName(col, 1)
|
||||
to, _ := CoordinatesToCellName(col, rows)
|
||||
err = f.SetCellStyle(sheet, from, to, styleID)
|
||||
|
@ -477,22 +488,25 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
|
|||
//
|
||||
// err := f.SetColWidth("Sheet1", "A", "H", 20)
|
||||
func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error {
|
||||
min, max, err := f.parseColRange(startCol + ":" + endCol)
|
||||
minVal, maxVal, err := f.parseColRange(startCol + ":" + endCol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if width > MaxColumnWidth {
|
||||
return ErrColumnWidth
|
||||
}
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
col := xlsxCol{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Min: minVal,
|
||||
Max: maxVal,
|
||||
Width: float64Ptr(width),
|
||||
CustomWidth: true,
|
||||
}
|
||||
|
@ -519,7 +533,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error
|
|||
func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol {
|
||||
var fc []xlsxCol
|
||||
for i := col.Min; i <= col.Max; i++ {
|
||||
c := deepcopy.Copy(col).(xlsxCol)
|
||||
var c xlsxCol
|
||||
deepcopy.Copy(&c, col)
|
||||
c.Min, c.Max = i, i
|
||||
fc = append(fc, c)
|
||||
}
|
||||
|
@ -537,7 +552,8 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
|
|||
fc[idx] = replacer(fc[idx], column)
|
||||
continue
|
||||
}
|
||||
c := deepcopy.Copy(column).(xlsxCol)
|
||||
var c xlsxCol
|
||||
deepcopy.Copy(&c, column)
|
||||
c.Min, c.Max = i, i
|
||||
fc = append(fc, c)
|
||||
}
|
||||
|
@ -594,20 +610,21 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
|
|||
// width # Width of object frame.
|
||||
// height # Height of object frame.
|
||||
func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) {
|
||||
colIdx, rowIdx := col-1, row-1
|
||||
// Adjust start column for offsets that are greater than the col width.
|
||||
for x1 >= f.getColWidth(sheet, col) {
|
||||
x1 -= f.getColWidth(sheet, col)
|
||||
col++
|
||||
for x1 >= f.getColWidth(sheet, colIdx+1) {
|
||||
colIdx++
|
||||
x1 -= f.getColWidth(sheet, colIdx)
|
||||
}
|
||||
|
||||
// Adjust start row for offsets that are greater than the row height.
|
||||
for y1 >= f.getRowHeight(sheet, row) {
|
||||
y1 -= f.getRowHeight(sheet, row)
|
||||
row++
|
||||
for y1 >= f.getRowHeight(sheet, rowIdx+1) {
|
||||
rowIdx++
|
||||
y1 -= f.getRowHeight(sheet, rowIdx)
|
||||
}
|
||||
|
||||
// Initialized end cell to the same as the start cell.
|
||||
colEnd, rowEnd := col, row
|
||||
colEnd, rowEnd := colIdx, rowIdx
|
||||
|
||||
width += x1
|
||||
height += y1
|
||||
|
@ -625,17 +642,15 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh
|
|||
}
|
||||
|
||||
// The end vertices are whatever is left from the width and height.
|
||||
x2 := width
|
||||
y2 := height
|
||||
return col, row, colEnd, rowEnd, x2, y2
|
||||
return colIdx, rowIdx, colEnd, rowEnd, width, height
|
||||
}
|
||||
|
||||
// getColWidth provides a function to get column width in pixels by given
|
||||
// sheet name and column number.
|
||||
func (f *File) getColWidth(sheet string, col int) int {
|
||||
ws, _ := f.workSheetReader(sheet)
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
if ws.Cols != nil {
|
||||
var width float64
|
||||
for _, v := range ws.Cols.Col {
|
||||
|
@ -647,6 +662,9 @@ func (f *File) getColWidth(sheet string, col int) int {
|
|||
return int(convertColWidthToPixels(width))
|
||||
}
|
||||
}
|
||||
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
|
||||
return int(convertColWidthToPixels(ws.SheetFormatPr.DefaultColWidth))
|
||||
}
|
||||
// Optimization for when the column widths haven't changed.
|
||||
return int(defaultColWidthPixels)
|
||||
}
|
||||
|
@ -659,12 +677,15 @@ func (f *File) GetColStyle(sheet, col string) (int, error) {
|
|||
if err != nil {
|
||||
return styleID, err
|
||||
}
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return styleID, err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
if ws.Cols != nil {
|
||||
for _, v := range ws.Cols.Col {
|
||||
if v.Min <= colNum && colNum <= v.Max {
|
||||
|
@ -682,12 +703,15 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) {
|
|||
if err != nil {
|
||||
return defaultColWidth, err
|
||||
}
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return defaultColWidth, err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
if ws.Cols != nil {
|
||||
var width float64
|
||||
for _, v := range ws.Cols.Col {
|
||||
|
@ -699,6 +723,9 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) {
|
|||
return width, err
|
||||
}
|
||||
}
|
||||
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
|
||||
return ws.SheetFormatPr.DefaultColWidth, err
|
||||
}
|
||||
// Optimization for when the column widths haven't changed.
|
||||
return defaultColWidth, err
|
||||
}
|
||||
|
|
46
col_test.go
46
col_test.go
|
@ -1,7 +1,9 @@
|
|||
package excelize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -71,6 +73,17 @@ func TestCols(t *testing.T) {
|
|||
cols.Next()
|
||||
_, err = cols.Rows()
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
f = NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`))
|
||||
f.checked = sync.Map{}
|
||||
_, err = f.Cols("Sheet1")
|
||||
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
|
||||
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`))
|
||||
_, err = f.Cols("Sheet1")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
}
|
||||
|
||||
func TestColumnsIterator(t *testing.T) {
|
||||
|
@ -124,12 +137,12 @@ func TestGetColsError(t *testing.T) {
|
|||
|
||||
f = NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`))
|
||||
f.checked = nil
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value)))
|
||||
f.checked = sync.Map{}
|
||||
_, err = f.GetCols("Sheet1")
|
||||
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
|
||||
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`))
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value)))
|
||||
_, err = f.GetCols("Sheet1")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
|
||||
|
@ -139,7 +152,7 @@ func TestGetColsError(t *testing.T) {
|
|||
cols.totalRows = 2
|
||||
cols.totalCols = 2
|
||||
cols.curCol = 1
|
||||
cols.sheetXML = []byte(`<worksheet><sheetData><row r="1"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`)
|
||||
cols.sheetXML = []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="1"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value))
|
||||
_, err = cols.Rows()
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
|
||||
|
@ -217,7 +230,7 @@ func TestColumnVisibility(t *testing.T) {
|
|||
assert.Equal(t, true, visible)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test get column visible on an inexistent worksheet
|
||||
// Test get column visible on not exists worksheet
|
||||
_, err = f.GetColVisible("SheetN", "F")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test get column visible with invalid sheet name
|
||||
|
@ -353,6 +366,16 @@ func TestSetColStyle(t *testing.T) {
|
|||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.SetColStyle("Sheet1", "C:F", styleID), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test set column style with worksheet properties columns default width settings
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{DefaultColWidth: float64Ptr(20)}))
|
||||
style, err = f.NewStyle(&Style{Alignment: &Alignment{Vertical: "center"}})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetColStyle("Sheet1", "A:Z", style))
|
||||
width, err := f.GetColWidth("Sheet1", "B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20.0, width)
|
||||
}
|
||||
|
||||
func TestColWidth(t *testing.T) {
|
||||
|
@ -366,6 +389,15 @@ func TestColWidth(t *testing.T) {
|
|||
assert.Equal(t, defaultColWidth, width)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetFormatPr = &xlsxSheetFormatPr{DefaultColWidth: 10}
|
||||
ws.(*xlsxWorksheet).Cols = nil
|
||||
width, err = f.GetColWidth("Sheet1", "A")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10.0, width)
|
||||
assert.Equal(t, 76, f.getColWidth("Sheet1", 1))
|
||||
|
||||
// Test set and get column width with illegal cell reference
|
||||
width, err = f.GetColWidth("Sheet1", "*")
|
||||
assert.Equal(t, defaultColWidth, width)
|
||||
|
@ -407,7 +439,7 @@ func TestInsertCols(t *testing.T) {
|
|||
f := NewFile()
|
||||
sheet1 := f.GetSheetName(0)
|
||||
|
||||
fillCells(f, sheet1, 10, 10)
|
||||
assert.NoError(t, fillCells(f, sheet1, 10, 10))
|
||||
|
||||
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
|
||||
assert.NoError(t, f.MergeCell(sheet1, "A1", "C3"))
|
||||
|
@ -430,7 +462,7 @@ func TestRemoveCol(t *testing.T) {
|
|||
f := NewFile()
|
||||
sheet1 := f.GetSheetName(0)
|
||||
|
||||
fillCells(f, sheet1, 10, 15)
|
||||
assert.NoError(t, fillCells(f, sheet1, 10, 15))
|
||||
|
||||
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
|
||||
assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External"))
|
||||
|
|
426
comment.go
426
comment.go
|
@ -1,426 +0,0 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// 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 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetComments retrieves all comments in a worksheet by given worksheet name.
|
||||
func (f *File) GetComments(sheet string) ([]Comment, error) {
|
||||
var comments []Comment
|
||||
sheetXMLPath, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
return comments, newNoExistSheetError(sheet)
|
||||
}
|
||||
commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath))
|
||||
if !strings.HasPrefix(commentsXML, "/") {
|
||||
commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..")
|
||||
}
|
||||
commentsXML = strings.TrimPrefix(commentsXML, "/")
|
||||
cmts, err := f.commentsReader(commentsXML)
|
||||
if err != nil {
|
||||
return comments, err
|
||||
}
|
||||
if cmts != nil {
|
||||
for _, cmt := range cmts.CommentList.Comment {
|
||||
comment := Comment{}
|
||||
if cmt.AuthorID < len(cmts.Authors.Author) {
|
||||
comment.Author = cmts.Authors.Author[cmt.AuthorID]
|
||||
}
|
||||
comment.Cell = cmt.Ref
|
||||
comment.AuthorID = cmt.AuthorID
|
||||
if cmt.Text.T != nil {
|
||||
comment.Text += *cmt.Text.T
|
||||
}
|
||||
for _, text := range cmt.Text.R {
|
||||
if text.T != nil {
|
||||
run := RichTextRun{Text: text.T.Val}
|
||||
if text.RPr != nil {
|
||||
run.Font = newFont(text.RPr)
|
||||
}
|
||||
comment.Runs = append(comment.Runs, run)
|
||||
}
|
||||
}
|
||||
comments = append(comments, comment)
|
||||
}
|
||||
}
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
// getSheetComments provides the method to get the target comment reference by
|
||||
// given worksheet file path.
|
||||
func (f *File) getSheetComments(sheetFile string) string {
|
||||
rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels")
|
||||
if sheetRels := rels; sheetRels != nil {
|
||||
sheetRels.Lock()
|
||||
defer sheetRels.Unlock()
|
||||
for _, v := range sheetRels.Relationships {
|
||||
if v.Type == SourceRelationshipComments {
|
||||
return v.Target
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddComment provides the method to add comment in a sheet by given worksheet
|
||||
// index, cell and format set (such as author and text). Note that the max
|
||||
// author length is 255 and the max text length is 32512. For example, add a
|
||||
// comment in Sheet1!$A$30:
|
||||
//
|
||||
// err := f.AddComment("Sheet1", excelize.Comment{
|
||||
// Cell: "A12",
|
||||
// Author: "Excelize",
|
||||
// Runs: []excelize.RichTextRun{
|
||||
// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}},
|
||||
// {Text: "This is a comment."},
|
||||
// },
|
||||
// })
|
||||
func (f *File) AddComment(sheet string, comment Comment) error {
|
||||
// Read sheet data.
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commentID := f.countComments() + 1
|
||||
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
|
||||
sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml"
|
||||
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
|
||||
if ws.LegacyDrawing != nil {
|
||||
// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
|
||||
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
|
||||
commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
|
||||
drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
|
||||
} else {
|
||||
// Add first comment for given sheet.
|
||||
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
|
||||
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
|
||||
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
|
||||
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
|
||||
f.addSheetNameSpace(sheet, SourceRelationship)
|
||||
f.addSheetLegacyDrawing(sheet, rID)
|
||||
}
|
||||
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
|
||||
var rows, cols int
|
||||
for _, runs := range comment.Runs {
|
||||
for _, subStr := range strings.Split(runs.Text, "\n") {
|
||||
rows++
|
||||
if chars := len(subStr); chars > cols {
|
||||
cols = chars
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.addComment(commentsXML, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.addContentTypePart(commentID, "comments")
|
||||
}
|
||||
|
||||
// DeleteComment provides the method to delete comment in a sheet by given
|
||||
// worksheet name. For example, delete the comment in Sheet1!$A$30:
|
||||
//
|
||||
// err := f.DeleteComment("Sheet1", "A30")
|
||||
func (f *File) DeleteComment(sheet, cell string) error {
|
||||
if err := checkSheetName(sheet); err != nil {
|
||||
return err
|
||||
}
|
||||
sheetXMLPath, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
return newNoExistSheetError(sheet)
|
||||
}
|
||||
commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath))
|
||||
if !strings.HasPrefix(commentsXML, "/") {
|
||||
commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..")
|
||||
}
|
||||
commentsXML = strings.TrimPrefix(commentsXML, "/")
|
||||
cmts, err := f.commentsReader(commentsXML)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmts != nil {
|
||||
for i := 0; i < len(cmts.CommentList.Comment); i++ {
|
||||
cmt := cmts.CommentList.Comment[i]
|
||||
if cmt.Ref != cell {
|
||||
continue
|
||||
}
|
||||
if len(cmts.CommentList.Comment) > 1 {
|
||||
cmts.CommentList.Comment = append(
|
||||
cmts.CommentList.Comment[:i],
|
||||
cmts.CommentList.Comment[i+1:]...,
|
||||
)
|
||||
i--
|
||||
continue
|
||||
}
|
||||
cmts.CommentList.Comment = nil
|
||||
}
|
||||
f.Comments[commentsXML] = cmts
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// addDrawingVML provides a function to create comment as
|
||||
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
|
||||
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
|
||||
col, row, err := CellNameToCoordinates(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
yAxis := col - 1
|
||||
xAxis := row - 1
|
||||
vml := f.VMLDrawing[drawingVML]
|
||||
if vml == nil {
|
||||
vml = &vmlDrawing{
|
||||
XMLNSv: "urn:schemas-microsoft-com:vml",
|
||||
XMLNSo: "urn:schemas-microsoft-com:office:office",
|
||||
XMLNSx: "urn:schemas-microsoft-com:office:excel",
|
||||
XMLNSmv: "http://macVmlSchemaUri",
|
||||
Shapelayout: &xlsxShapelayout{
|
||||
Ext: "edit",
|
||||
IDmap: &xlsxIDmap{
|
||||
Ext: "edit",
|
||||
Data: commentID,
|
||||
},
|
||||
},
|
||||
Shapetype: &xlsxShapetype{
|
||||
ID: "_x0000_t202",
|
||||
Coordsize: "21600,21600",
|
||||
Spt: 202,
|
||||
Path: "m0,0l0,21600,21600,21600,21600,0xe",
|
||||
Stroke: &xlsxStroke{
|
||||
Joinstyle: "miter",
|
||||
},
|
||||
VPath: &vPath{
|
||||
Gradientshapeok: "t",
|
||||
Connecttype: "rect",
|
||||
},
|
||||
},
|
||||
}
|
||||
// load exist comment shapes from xl/drawings/vmlDrawing%d.vml
|
||||
d, err := f.decodeVMLDrawingReader(drawingVML)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d != nil {
|
||||
for _, v := range d.Shape {
|
||||
s := xlsxShape{
|
||||
ID: "_x0000_s1025",
|
||||
Type: "#_x0000_t202",
|
||||
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
|
||||
Fillcolor: "#FBF6D6",
|
||||
Strokecolor: "#EDEAA1",
|
||||
Val: v.Val,
|
||||
}
|
||||
vml.Shape = append(vml.Shape, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
sp := encodeShape{
|
||||
Fill: &vFill{
|
||||
Color2: "#FBFE82",
|
||||
Angle: -180,
|
||||
Type: "gradient",
|
||||
Fill: &oFill{
|
||||
Ext: "view",
|
||||
Type: "gradientUnscaled",
|
||||
},
|
||||
},
|
||||
Shadow: &vShadow{
|
||||
On: "t",
|
||||
Color: "black",
|
||||
Obscured: "t",
|
||||
},
|
||||
Path: &vPath{
|
||||
Connecttype: "none",
|
||||
},
|
||||
Textbox: &vTextbox{
|
||||
Style: "mso-direction-alt:auto",
|
||||
Div: &xlsxDiv{
|
||||
Style: "text-align:left",
|
||||
},
|
||||
},
|
||||
ClientData: &xClientData{
|
||||
ObjectType: "Note",
|
||||
Anchor: fmt.Sprintf(
|
||||
"%d, 23, %d, 0, %d, %d, %d, 5",
|
||||
1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
|
||||
AutoFill: "True",
|
||||
Row: xAxis,
|
||||
Column: yAxis,
|
||||
},
|
||||
}
|
||||
s, _ := xml.Marshal(sp)
|
||||
shape := xlsxShape{
|
||||
ID: "_x0000_s1025",
|
||||
Type: "#_x0000_t202",
|
||||
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
|
||||
Fillcolor: "#FBF6D6",
|
||||
Strokecolor: "#EDEAA1",
|
||||
Val: string(s[13 : len(s)-14]),
|
||||
}
|
||||
vml.Shape = append(vml.Shape, shape)
|
||||
f.VMLDrawing[drawingVML] = vml
|
||||
return err
|
||||
}
|
||||
|
||||
// addComment provides a function to create chart as xl/comments%d.xml by
|
||||
// given cell and format sets.
|
||||
func (f *File) addComment(commentsXML string, comment Comment) error {
|
||||
if comment.Author == "" {
|
||||
comment.Author = "Author"
|
||||
}
|
||||
if len(comment.Author) > MaxFieldLength {
|
||||
comment.Author = comment.Author[:MaxFieldLength]
|
||||
}
|
||||
cmts, err := f.commentsReader(commentsXML)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var authorID int
|
||||
if cmts == nil {
|
||||
cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}}
|
||||
}
|
||||
if inStrSlice(cmts.Authors.Author, comment.Author, true) == -1 {
|
||||
cmts.Authors.Author = append(cmts.Authors.Author, comment.Author)
|
||||
authorID = len(cmts.Authors.Author) - 1
|
||||
}
|
||||
defaultFont, err := f.GetDefaultFont()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chars, cmt := 0, xlsxComment{
|
||||
Ref: comment.Cell,
|
||||
AuthorID: authorID,
|
||||
Text: xlsxText{R: []xlsxR{}},
|
||||
}
|
||||
if comment.Text != "" {
|
||||
if len(comment.Text) > TotalCellChars {
|
||||
comment.Text = comment.Text[:TotalCellChars]
|
||||
}
|
||||
cmt.Text.T = stringPtr(comment.Text)
|
||||
chars += len(comment.Text)
|
||||
}
|
||||
for _, run := range comment.Runs {
|
||||
if chars == TotalCellChars {
|
||||
break
|
||||
}
|
||||
if chars+len(run.Text) > TotalCellChars {
|
||||
run.Text = run.Text[:TotalCellChars-chars]
|
||||
}
|
||||
chars += len(run.Text)
|
||||
r := xlsxR{
|
||||
RPr: &xlsxRPr{
|
||||
Sz: &attrValFloat{Val: float64Ptr(9)},
|
||||
Color: &xlsxColor{
|
||||
Indexed: 81,
|
||||
},
|
||||
RFont: &attrValString{Val: stringPtr(defaultFont)},
|
||||
Family: &attrValInt{Val: intPtr(2)},
|
||||
},
|
||||
T: &xlsxT{Val: run.Text, Space: xml.Attr{
|
||||
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
|
||||
Value: "preserve",
|
||||
}},
|
||||
}
|
||||
if run.Font != nil {
|
||||
r.RPr = newRpr(run.Font)
|
||||
}
|
||||
cmt.Text.R = append(cmt.Text.R, r)
|
||||
}
|
||||
cmts.CommentList.Comment = append(cmts.CommentList.Comment, cmt)
|
||||
f.Comments[commentsXML] = cmts
|
||||
return err
|
||||
}
|
||||
|
||||
// countComments provides a function to get comments files count storage in
|
||||
// the folder xl.
|
||||
func (f *File) countComments() int {
|
||||
c1, c2 := 0, 0
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/comments") {
|
||||
c1++
|
||||
}
|
||||
return true
|
||||
})
|
||||
for rel := range f.Comments {
|
||||
if strings.Contains(rel, "xl/comments") {
|
||||
c2++
|
||||
}
|
||||
}
|
||||
if c1 < c2 {
|
||||
return c2
|
||||
}
|
||||
return c1
|
||||
}
|
||||
|
||||
// decodeVMLDrawingReader provides a function to get the pointer to the
|
||||
// structure after deserialization of xl/drawings/vmlDrawing%d.xml.
|
||||
func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) {
|
||||
if f.DecodeVMLDrawing[path] == nil {
|
||||
c, ok := f.Pkg.Load(path)
|
||||
if ok && c != nil {
|
||||
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))).
|
||||
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return f.DecodeVMLDrawing[path], nil
|
||||
}
|
||||
|
||||
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
|
||||
// after serialize structure.
|
||||
func (f *File) vmlDrawingWriter() {
|
||||
for path, vml := range f.VMLDrawing {
|
||||
if vml != nil {
|
||||
v, _ := xml.Marshal(vml)
|
||||
f.Pkg.Store(path, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// commentsReader provides a function to get the pointer to the structure
|
||||
// after deserialization of xl/comments%d.xml.
|
||||
func (f *File) commentsReader(path string) (*xlsxComments, error) {
|
||||
if f.Comments[path] == nil {
|
||||
content, ok := f.Pkg.Load(path)
|
||||
if ok && content != nil {
|
||||
f.Comments[path] = new(xlsxComments)
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
|
||||
Decode(f.Comments[path]); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return f.Comments[path], nil
|
||||
}
|
||||
|
||||
// commentsWriter provides a function to save xl/comments%d.xml after
|
||||
// serialize structure.
|
||||
func (f *File) commentsWriter() {
|
||||
for path, c := range f.Comments {
|
||||
if c != nil {
|
||||
v, _ := xml.Marshal(c)
|
||||
f.saveFileList(path, v)
|
||||
}
|
||||
}
|
||||
}
|
146
comment_test.go
146
comment_test.go
|
@ -1,146 +0,0 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// 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 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddComment(t *testing.T) {
|
||||
f, err := prepareTestBook1()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
s := strings.Repeat("c", TotalCellChars+1)
|
||||
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Runs: []RichTextRun{{Text: s}, {Text: s}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
|
||||
|
||||
// Test add comment on not exists worksheet
|
||||
assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist")
|
||||
// Test add comment on with illegal cell reference
|
||||
assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
comments, err := f.GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 2)
|
||||
comments, err = f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 1)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx")))
|
||||
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><authors><author>Excelize: </author></authors><commentList><comment ref="B7" authorId="0"><text><t>Excelize: </t></text></comment></commentList></comments>`))
|
||||
comments, err = f.GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 2)
|
||||
comments, err = f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 1)
|
||||
comments, err = NewFile().GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 0)
|
||||
|
||||
// Test add comments with invalid sheet name
|
||||
assert.EqualError(t, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}), ErrSheetNameInvalid.Error())
|
||||
|
||||
// Test add comments with unsupported charset
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetComments("Sheet2")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test add comments with unsupported charset
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test add comments with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test get comments on not exists worksheet
|
||||
comments, err = f.GetComments("SheetN")
|
||||
assert.Len(t, comments, 0)
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
}
|
||||
|
||||
func TestDeleteComment(t *testing.T) {
|
||||
f, err := prepareTestBook1()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
|
||||
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
|
||||
|
||||
comments, err := f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 5)
|
||||
|
||||
comments, err = NewFile().GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 0)
|
||||
|
||||
// Test delete comment with invalid sheet name
|
||||
assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error())
|
||||
// Test delete all comments in a worksheet
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "A41"))
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "C41"))
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "C42"))
|
||||
comments, err = f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, len(comments))
|
||||
// Test delete comment on not exists worksheet
|
||||
assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist")
|
||||
// Test delete comment with worksheet part
|
||||
f.Pkg.Delete("xl/worksheets/sheet1.xml")
|
||||
assert.NoError(t, f.DeleteComment("Sheet1", "A22"))
|
||||
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeleteComment("Sheet2", "A41"), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestDecodeVMLDrawingReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
path := "xl/drawings/vmlDrawing1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err := f.decodeVMLDrawingReader(path)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestCommentsReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test read comments with unsupported charset
|
||||
path := "xl/comments1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err := f.commentsReader(path)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestCountComments(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.Comments["xl/comments1.xml"] = nil
|
||||
assert.Equal(t, f.countComments(), 1)
|
||||
}
|
6
crypt.go
6
crypt.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -676,7 +676,7 @@ func (c *cfb) writeUint64(value int) {
|
|||
c.writeBytes(buf)
|
||||
}
|
||||
|
||||
// writeBytes write strings in the stream by a given value with an offset.
|
||||
// writeStrings write strings in the stream by a given value with an offset.
|
||||
func (c *cfb) writeStrings(value string) {
|
||||
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
buffer, err := encoder.Bytes([]byte(value))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,16 +7,19 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/richardlehane/mscfb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -36,7 +39,7 @@ func TestEncrypt(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
raw[2050] = 3
|
||||
_, err = Decrypt(raw, &Options{Password: "password"})
|
||||
assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error())
|
||||
assert.Equal(t, ErrUnsupportedEncryptMechanism, err)
|
||||
|
||||
// Test encrypt spreadsheet with invalid password
|
||||
assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error())
|
||||
|
@ -51,6 +54,25 @@ func TestEncrypt(t *testing.T) {
|
|||
// Test remove password by save workbook with options
|
||||
assert.NoError(t, f.Save(Options{Password: ""}))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
doc, err := mscfb.New(bytes.NewReader(raw))
|
||||
assert.NoError(t, err)
|
||||
encryptionInfoBuf, encryptedPackageBuf := extractPart(doc)
|
||||
binary.LittleEndian.PutUint64(encryptionInfoBuf[20:32], uint64(0))
|
||||
_, err = standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, &Options{Password: "password"})
|
||||
assert.NoError(t, err)
|
||||
_, err = decrypt(nil, nil, nil)
|
||||
assert.EqualError(t, err, "crypto/aes: invalid key size 0")
|
||||
_, err = agileDecrypt(encryptionInfoBuf, MacintoshCyrillicCharset, &Options{Password: "password"})
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid character entity &0 (no semicolon)")
|
||||
_, err = convertPasswdToKey("password", nil, Encryption{
|
||||
KeyEncryptors: KeyEncryptors{KeyEncryptor: []KeyEncryptor{
|
||||
{EncryptedKey: EncryptedKey{KeyData: KeyData{SaltValue: "=="}}},
|
||||
}},
|
||||
})
|
||||
assert.EqualError(t, err, "illegal base64 data at input byte 0")
|
||||
_, err = createIV([]byte{0}, Encryption{KeyData: KeyData{SaltValue: "=="}})
|
||||
assert.EqualError(t, err, "illegal base64 data at input byte 0")
|
||||
}
|
||||
|
||||
func TestEncryptionMechanism(t *testing.T) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,12 +7,13 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
@ -24,14 +25,13 @@ type DataValidationType int
|
|||
// Data validation types.
|
||||
const (
|
||||
_ DataValidationType = iota
|
||||
typeNone // inline use
|
||||
DataValidationTypeNone
|
||||
DataValidationTypeCustom
|
||||
DataValidationTypeDate
|
||||
DataValidationTypeDecimal
|
||||
typeList // inline use
|
||||
DataValidationTypeList
|
||||
DataValidationTypeTextLength
|
||||
DataValidationTypeTime
|
||||
// DataValidationTypeWhole Integer
|
||||
DataValidationTypeWhole
|
||||
)
|
||||
|
||||
|
@ -69,13 +69,41 @@ const (
|
|||
DataValidationOperatorNotEqual
|
||||
)
|
||||
|
||||
var (
|
||||
// formulaEscaper mimics the Excel escaping rules for data validation,
|
||||
// which converts `"` to `""` instead of `"`.
|
||||
var formulaEscaper = strings.NewReplacer(
|
||||
formulaEscaper = strings.NewReplacer(
|
||||
`&`, `&`,
|
||||
`<`, `<`,
|
||||
`>`, `>`,
|
||||
`"`, `""`,
|
||||
)
|
||||
formulaUnescaper = strings.NewReplacer(
|
||||
`&`, `&`,
|
||||
`<`, `<`,
|
||||
`>`, `>`,
|
||||
)
|
||||
// dataValidationTypeMap defined supported data validation types.
|
||||
dataValidationTypeMap = map[DataValidationType]string{
|
||||
DataValidationTypeNone: "none",
|
||||
DataValidationTypeCustom: "custom",
|
||||
DataValidationTypeDate: "date",
|
||||
DataValidationTypeDecimal: "decimal",
|
||||
DataValidationTypeList: "list",
|
||||
DataValidationTypeTextLength: "textLength",
|
||||
DataValidationTypeTime: "time",
|
||||
DataValidationTypeWhole: "whole",
|
||||
}
|
||||
// dataValidationOperatorMap defined supported data validation operators.
|
||||
dataValidationOperatorMap = map[DataValidationOperator]string{
|
||||
DataValidationOperatorBetween: "between",
|
||||
DataValidationOperatorEqual: "equal",
|
||||
DataValidationOperatorGreaterThan: "greaterThan",
|
||||
DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
|
||||
DataValidationOperatorLessThan: "lessThan",
|
||||
DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
|
||||
DataValidationOperatorNotBetween: "notBetween",
|
||||
DataValidationOperatorNotEqual: "notEqual",
|
||||
}
|
||||
)
|
||||
|
||||
// NewDataValidation return data validation struct.
|
||||
|
@ -112,113 +140,92 @@ func (dv *DataValidation) SetInput(title, msg string) {
|
|||
dv.Prompt = &msg
|
||||
}
|
||||
|
||||
// SetDropList data validation list.
|
||||
// SetDropList data validation list. If you type the items into the data
|
||||
// validation dialog box (a delimited list), the limit is 255 characters,
|
||||
// including the separators. If your data validation list source formula is
|
||||
// over the maximum length limit, please set the allowed values in the
|
||||
// worksheet cells, and use the SetSqrefDropList function to set the reference
|
||||
// for their cells.
|
||||
func (dv *DataValidation) SetDropList(keys []string) error {
|
||||
formula := strings.Join(keys, ",")
|
||||
if MaxFieldLength < len(utf16.Encode([]rune(formula))) {
|
||||
return ErrDataValidationFormulaLength
|
||||
}
|
||||
dv.Formula1 = fmt.Sprintf(`<formula1>"%s"</formula1>`, formulaEscaper.Replace(formula))
|
||||
dv.Type = convDataValidationType(typeList)
|
||||
dv.Type = dataValidationTypeMap[DataValidationTypeList]
|
||||
if strings.HasPrefix(formula, "=") {
|
||||
dv.Formula1 = formulaEscaper.Replace(formula)
|
||||
return nil
|
||||
}
|
||||
dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRange provides function to set data validation range in drop list, only
|
||||
// accepts int, float64, or string data type formula argument.
|
||||
// accepts int, float64, string or []string data type formula argument.
|
||||
func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error {
|
||||
var formula1, formula2 string
|
||||
switch v := f1.(type) {
|
||||
genFormula := func(val interface{}) (string, error) {
|
||||
var formula string
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
formula1 = fmt.Sprintf("<formula1>%d</formula1>", v)
|
||||
formula = fmt.Sprintf("%d", v)
|
||||
case float64:
|
||||
if math.Abs(v) > math.MaxFloat32 {
|
||||
return ErrDataValidationRange
|
||||
return formula, ErrDataValidationRange
|
||||
}
|
||||
formula1 = fmt.Sprintf("<formula1>%.17g</formula1>", v)
|
||||
formula = fmt.Sprintf("%.17g", v)
|
||||
case string:
|
||||
formula1 = fmt.Sprintf("<formula1>%s</formula1>", v)
|
||||
formula = v
|
||||
default:
|
||||
return ErrParameterInvalid
|
||||
return formula, ErrParameterInvalid
|
||||
}
|
||||
switch v := f2.(type) {
|
||||
case int:
|
||||
formula2 = fmt.Sprintf("<formula2>%d</formula2>", v)
|
||||
case float64:
|
||||
if math.Abs(v) > math.MaxFloat32 {
|
||||
return ErrDataValidationRange
|
||||
return formula, nil
|
||||
}
|
||||
formula2 = fmt.Sprintf("<formula2>%.17g</formula2>", v)
|
||||
case string:
|
||||
formula2 = fmt.Sprintf("<formula2>%s</formula2>", v)
|
||||
default:
|
||||
return ErrParameterInvalid
|
||||
formula1, err := genFormula(f1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formula2, err := genFormula(f2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dv.Formula1, dv.Formula2 = formula1, formula2
|
||||
dv.Type = convDataValidationType(t)
|
||||
dv.Operator = convDataValidationOperator(o)
|
||||
return nil
|
||||
dv.Type = dataValidationTypeMap[t]
|
||||
dv.Operator = dataValidationOperatorMap[o]
|
||||
return err
|
||||
}
|
||||
|
||||
// SetSqrefDropList provides set data validation on a range with source
|
||||
// reference range of the worksheet by given data validation object and
|
||||
// worksheet name. The data validation object can be created by
|
||||
// NewDataValidation function. For example, set data validation on
|
||||
// Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create
|
||||
// in-cell dropdown by allowing list source:
|
||||
// NewDataValidation function. There are limits to the number of items that
|
||||
// will show in a data validation drop down list: The list can show up to show
|
||||
// 32768 items from a list on the worksheet. If you need more items than that,
|
||||
// you could create a dependent drop down list, broken down by category. For
|
||||
// example, set data validation on Sheet1!A7:B8 with validation criteria source
|
||||
// Sheet1!E1:E3 settings, create in-cell dropdown by allowing list source:
|
||||
//
|
||||
// dv := excelize.NewDataValidation(true)
|
||||
// dv.Sqref = "A7:B8"
|
||||
// dv.SetSqrefDropList("$E$1:$E$3")
|
||||
// err := f.AddDataValidation("Sheet1", dv)
|
||||
func (dv *DataValidation) SetSqrefDropList(sqref string) {
|
||||
dv.Formula1 = fmt.Sprintf("<formula1>%s</formula1>", sqref)
|
||||
dv.Type = convDataValidationType(typeList)
|
||||
dv.Formula1 = sqref
|
||||
dv.Type = dataValidationTypeMap[DataValidationTypeList]
|
||||
}
|
||||
|
||||
// SetSqref provides function to set data validation range in drop list.
|
||||
func (dv *DataValidation) SetSqref(sqref string) {
|
||||
if dv.Sqref == "" {
|
||||
dv.Sqref = sqref
|
||||
} else {
|
||||
return
|
||||
}
|
||||
dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref)
|
||||
}
|
||||
}
|
||||
|
||||
// convDataValidationType get excel data validation type.
|
||||
func convDataValidationType(t DataValidationType) string {
|
||||
typeMap := map[DataValidationType]string{
|
||||
typeNone: "none",
|
||||
DataValidationTypeCustom: "custom",
|
||||
DataValidationTypeDate: "date",
|
||||
DataValidationTypeDecimal: "decimal",
|
||||
typeList: "list",
|
||||
DataValidationTypeTextLength: "textLength",
|
||||
DataValidationTypeTime: "time",
|
||||
DataValidationTypeWhole: "whole",
|
||||
}
|
||||
|
||||
return typeMap[t]
|
||||
}
|
||||
|
||||
// convDataValidationOperator get excel data validation operator.
|
||||
func convDataValidationOperator(o DataValidationOperator) string {
|
||||
typeMap := map[DataValidationOperator]string{
|
||||
DataValidationOperatorBetween: "between",
|
||||
DataValidationOperatorEqual: "equal",
|
||||
DataValidationOperatorGreaterThan: "greaterThan",
|
||||
DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
|
||||
DataValidationOperatorLessThan: "lessThan",
|
||||
DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
|
||||
DataValidationOperatorNotBetween: "notBetween",
|
||||
DataValidationOperatorNotEqual: "notEqual",
|
||||
}
|
||||
|
||||
return typeMap[o]
|
||||
}
|
||||
|
||||
// AddDataValidation provides set data validation on a range of the worksheet
|
||||
// by given data validation object and worksheet name. The data validation
|
||||
// object can be created by NewDataValidation function.
|
||||
// by given data validation object and worksheet name. This function is
|
||||
// concurrency safe. The data validation object can be created by
|
||||
// NewDataValidation function.
|
||||
//
|
||||
// Example 1, set data validation on Sheet1!A1:B2 with validation criteria
|
||||
// settings, show error alert after invalid data is entered with "Stop" style
|
||||
|
@ -251,10 +258,32 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
if nil == ws.DataValidations {
|
||||
ws.DataValidations = new(xlsxDataValidations)
|
||||
}
|
||||
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv)
|
||||
dataValidation := &xlsxDataValidation{
|
||||
AllowBlank: dv.AllowBlank,
|
||||
Error: dv.Error,
|
||||
ErrorStyle: dv.ErrorStyle,
|
||||
ErrorTitle: dv.ErrorTitle,
|
||||
Operator: dv.Operator,
|
||||
Prompt: dv.Prompt,
|
||||
PromptTitle: dv.PromptTitle,
|
||||
ShowDropDown: dv.ShowDropDown,
|
||||
ShowErrorMessage: dv.ShowErrorMessage,
|
||||
ShowInputMessage: dv.ShowInputMessage,
|
||||
Sqref: dv.Sqref,
|
||||
Type: dv.Type,
|
||||
}
|
||||
if dv.Formula1 != "" {
|
||||
dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1}
|
||||
}
|
||||
if dv.Formula2 != "" {
|
||||
dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2}
|
||||
}
|
||||
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation)
|
||||
ws.DataValidations.Count = len(ws.DataValidations.DataValidation)
|
||||
return err
|
||||
}
|
||||
|
@ -265,20 +294,83 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 {
|
||||
return nil, err
|
||||
var (
|
||||
dataValidations []*DataValidation
|
||||
decodeExtLst = new(decodeExtLst)
|
||||
decodeDataValidations *xlsxDataValidations
|
||||
ext *xlsxExt
|
||||
)
|
||||
if ws.DataValidations != nil {
|
||||
dataValidations = append(dataValidations, getDataValidations(ws.DataValidations)...)
|
||||
}
|
||||
return ws.DataValidations.DataValidation, err
|
||||
if ws.ExtLst != nil {
|
||||
if err = f.xmlNewDecoder(strings.NewReader("<extLst>" + ws.ExtLst.Ext + "</extLst>")).
|
||||
Decode(decodeExtLst); err != nil && err != io.EOF {
|
||||
return dataValidations, err
|
||||
}
|
||||
for _, ext = range decodeExtLst.Ext {
|
||||
if ext.URI == ExtURIDataValidations {
|
||||
decodeDataValidations = new(xlsxDataValidations)
|
||||
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeDataValidations)
|
||||
dataValidations = append(dataValidations, getDataValidations(decodeDataValidations)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dataValidations, err
|
||||
}
|
||||
|
||||
// getDataValidations returns data validations list by given worksheet data
|
||||
// validations.
|
||||
func getDataValidations(dvs *xlsxDataValidations) []*DataValidation {
|
||||
if dvs == nil {
|
||||
return nil
|
||||
}
|
||||
var dataValidations []*DataValidation
|
||||
for _, dv := range dvs.DataValidation {
|
||||
if dv == nil {
|
||||
continue
|
||||
}
|
||||
dataValidation := &DataValidation{
|
||||
AllowBlank: dv.AllowBlank,
|
||||
Error: dv.Error,
|
||||
ErrorStyle: dv.ErrorStyle,
|
||||
ErrorTitle: dv.ErrorTitle,
|
||||
Operator: dv.Operator,
|
||||
Prompt: dv.Prompt,
|
||||
PromptTitle: dv.PromptTitle,
|
||||
ShowDropDown: dv.ShowDropDown,
|
||||
ShowErrorMessage: dv.ShowErrorMessage,
|
||||
ShowInputMessage: dv.ShowInputMessage,
|
||||
Sqref: dv.Sqref,
|
||||
Type: dv.Type,
|
||||
}
|
||||
if dv.Formula1 != nil {
|
||||
dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content)
|
||||
}
|
||||
if dv.Formula2 != nil {
|
||||
dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content)
|
||||
}
|
||||
if dv.XMSqref != "" {
|
||||
dataValidation.Sqref = dv.XMSqref
|
||||
dataValidation.Formula1 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula1, "<xm:f>"), "</xm:f>")
|
||||
dataValidation.Formula2 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula2, "<xm:f>"), "</xm:f>")
|
||||
}
|
||||
dataValidations = append(dataValidations, dataValidation)
|
||||
}
|
||||
return dataValidations
|
||||
}
|
||||
|
||||
// DeleteDataValidation delete data validation by given worksheet name and
|
||||
// reference sequence. All data validations in the worksheet will be deleted
|
||||
// reference sequence. This function is concurrency safe.
|
||||
// All data validations in the worksheet will be deleted
|
||||
// if not specify reference sequence parameter.
|
||||
func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
if ws.DataValidations == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -286,14 +378,14 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
|
|||
ws.DataValidations = nil
|
||||
return nil
|
||||
}
|
||||
delCells, err := f.flatSqref(sqref[0])
|
||||
delCells, err := flatSqref(sqref[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dv := ws.DataValidations
|
||||
for i := 0; i < len(dv.DataValidation); i++ {
|
||||
var applySqref []string
|
||||
colCells, err := f.flatSqref(dv.DataValidation[i].Sqref)
|
||||
colCells, err := flatSqref(dv.DataValidation[i].Sqref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -306,7 +398,7 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
|
|||
}
|
||||
}
|
||||
for _, col := range colCells {
|
||||
applySqref = append(applySqref, f.squashSqref(col)...)
|
||||
applySqref = append(applySqref, squashSqref(col)...)
|
||||
}
|
||||
dv.DataValidation[i].Sqref = strings.Join(applySqref, " ")
|
||||
if len(applySqref) == 0 {
|
||||
|
@ -322,30 +414,43 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
|
|||
}
|
||||
|
||||
// squashSqref generates cell reference sequence by given cells coordinates list.
|
||||
func (f *File) squashSqref(cells [][]int) []string {
|
||||
func squashSqref(cells [][]int) []string {
|
||||
if len(cells) == 1 {
|
||||
cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1])
|
||||
return []string{cell}
|
||||
} else if len(cells) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
var res []string
|
||||
var refs []string
|
||||
l, r := 0, 0
|
||||
for i := 1; i < len(cells); i++ {
|
||||
if cells[i][0] == cells[r][0] && cells[i][1]-cells[r][1] > 1 {
|
||||
curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...))
|
||||
ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...))
|
||||
if l == r {
|
||||
curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
|
||||
ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
|
||||
}
|
||||
res = append(res, curr)
|
||||
refs = append(refs, ref)
|
||||
l, r = i, i
|
||||
} else {
|
||||
r++
|
||||
}
|
||||
}
|
||||
curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...))
|
||||
ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...))
|
||||
if l == r {
|
||||
curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
|
||||
ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
|
||||
}
|
||||
return append(res, curr)
|
||||
return append(refs, ref)
|
||||
}
|
||||
|
||||
// isFormulaDataValidation returns whether the data validation rule is a formula.
|
||||
func (dv *xlsxInnerXML) isFormula() bool {
|
||||
return dv != nil && !(strings.HasPrefix(dv.Content, """) && strings.HasSuffix(dv.Content, """))
|
||||
}
|
||||
|
||||
// unescapeDataValidationFormula returns unescaped data validation formula.
|
||||
func unescapeDataValidationFormula(val string) string {
|
||||
if strings.HasPrefix(val, "\"") { // Text detection
|
||||
return strings.NewReplacer(`""`, `"`).Replace(formulaUnescaper.Replace(val))
|
||||
}
|
||||
return formulaUnescaper.Replace(val)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,11 +7,12 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -71,6 +72,7 @@ func TestDataValidation(t *testing.T) {
|
|||
dv.Sqref = "A5:B6"
|
||||
for _, listValid := range [][]string{
|
||||
{"1", "2", "3"},
|
||||
{"=A1"},
|
||||
{strings.Repeat("&", MaxFieldLength)},
|
||||
{strings.Repeat("\u4E00", MaxFieldLength)},
|
||||
{strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
|
||||
|
@ -82,7 +84,7 @@ func TestDataValidation(t *testing.T) {
|
|||
assert.NotEqual(t, "", dv.Formula1,
|
||||
"Formula1 should not be empty for valid input %v", listValid)
|
||||
}
|
||||
assert.Equal(t, `<formula1>"A<,B>,C"",D ,E',F"</formula1>`, dv.Formula1)
|
||||
assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1)
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
|
||||
dataValidations, err = f.GetDataValidations("Sheet1")
|
||||
|
@ -103,6 +105,31 @@ func TestDataValidation(t *testing.T) {
|
|||
dataValidations, err = f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*DataValidation(nil), dataValidations)
|
||||
|
||||
// Test get data validations which storage in the extension lists
|
||||
f = NewFile()
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s" xmlns:x14="%s"><x14:dataValidations><x14:dataValidation type="list" allowBlank="1"><x14:formula1><xm:f>Sheet1!$B$1:$B$5</xm:f></x14:formula1><xm:sqref>A7:B8</xm:sqref></x14:dataValidation></x14:dataValidations></ext>`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
|
||||
dataValidations, err = f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*DataValidation{
|
||||
{
|
||||
AllowBlank: true,
|
||||
Type: "list",
|
||||
Formula1: "Sheet1!$B$1:$B$5",
|
||||
Sqref: "A7:B8",
|
||||
},
|
||||
}, dataValidations)
|
||||
|
||||
// Test get data validations with invalid extension list characters
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s" xmlns:x14="%s"><x14:dataValidations></x14:dataValidation></x14:dataValidations></ext>`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
|
||||
_, err = f.GetDataValidations("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: element <dataValidations> closed by </dataValidation>")
|
||||
|
||||
// Test get validations without validations
|
||||
assert.Nil(t, getDataValidations(nil))
|
||||
assert.Nil(t, getDataValidations(&xlsxDataValidations{DataValidation: []*xlsxDataValidation{nil}}))
|
||||
}
|
||||
|
||||
func TestDataValidationError(t *testing.T) {
|
||||
|
|
36
date.go
36
date.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -114,7 +114,7 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time {
|
|||
// "Communications of the ACM" in 1968 (published in CACM, volume 11, number
|
||||
// 10, October 1968, p.657). None of those programmers seems to have found it
|
||||
// necessary to explain the constants or variable names set out by Henry F.
|
||||
// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that jounal and
|
||||
// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that journal and
|
||||
// expand an explanation here - that day is not today.
|
||||
func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
|
||||
l := jd + 68569
|
||||
|
@ -163,7 +163,7 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time {
|
|||
return date.Truncate(time.Second)
|
||||
}
|
||||
|
||||
// ExcelDateToTime converts a float-based excel date representation to a time.Time.
|
||||
// ExcelDateToTime converts a float-based Excel date representation to a time.Time.
|
||||
func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) {
|
||||
if excelDate < 0 {
|
||||
return time.Time{}, newInvalidExcelDateError(excelDate)
|
||||
|
@ -214,3 +214,31 @@ func formatYear(y int) int {
|
|||
}
|
||||
return y
|
||||
}
|
||||
|
||||
// getDurationNumFmt returns most simplify numbers format code for time
|
||||
// duration type cell value by given worksheet name, cell reference and number.
|
||||
func getDurationNumFmt(d time.Duration) int {
|
||||
if d >= time.Hour*24 {
|
||||
return 46
|
||||
}
|
||||
// Whole minutes
|
||||
if d.Minutes() == float64(int(d.Minutes())) {
|
||||
return 20
|
||||
}
|
||||
return 21
|
||||
}
|
||||
|
||||
// getTimeNumFmt returns most simplify numbers format code for time type cell
|
||||
// value by given worksheet name, cell reference and number.
|
||||
func getTimeNumFmt(t time.Time) int {
|
||||
nextMonth := t.AddDate(0, 1, 0)
|
||||
// Whole months
|
||||
if t.Day() == 1 && nextMonth.Day() == 1 {
|
||||
return 17
|
||||
}
|
||||
// Whole days
|
||||
if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 {
|
||||
return 14
|
||||
}
|
||||
return 22
|
||||
}
|
||||
|
|
|
@ -68,22 +68,22 @@ func TestTimeFromExcelTime(t *testing.T) {
|
|||
})
|
||||
}
|
||||
for hour := 0; hour < 24; hour++ {
|
||||
for min := 0; min < 60; min++ {
|
||||
for minVal := 0; minVal < 60; minVal++ {
|
||||
for sec := 0; sec < 60; sec++ {
|
||||
date := time.Date(2021, time.December, 30, hour, min, sec, 0, time.UTC)
|
||||
date := time.Date(2021, time.December, 30, hour, minVal, sec, 0, time.UTC)
|
||||
// Test use 1900 date system
|
||||
excel1900Time, err := timeToExcelTime(date, false)
|
||||
assert.NoError(t, err)
|
||||
date1900Out := timeFromExcelTime(excel1900Time, false)
|
||||
assert.EqualValues(t, hour, date1900Out.Hour())
|
||||
assert.EqualValues(t, min, date1900Out.Minute())
|
||||
assert.EqualValues(t, minVal, date1900Out.Minute())
|
||||
assert.EqualValues(t, sec, date1900Out.Second())
|
||||
// Test use 1904 date system
|
||||
excel1904Time, err := timeToExcelTime(date, true)
|
||||
assert.NoError(t, err)
|
||||
date1904Out := timeFromExcelTime(excel1904Time, true)
|
||||
assert.EqualValues(t, hour, date1904Out.Hour())
|
||||
assert.EqualValues(t, min, date1904Out.Minute())
|
||||
assert.EqualValues(t, minVal, date1904Out.Minute())
|
||||
assert.EqualValues(t, sec, date1904Out.Second())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
|
727
drawing.go
727
drawing.go
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -38,3 +38,12 @@ func TestDrawingParser(t *testing.T) {
|
|||
_, _, err = f.drawingParser("wsDr")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteDrawingRels(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test delete drawing relationships with unsupported charset
|
||||
rels := "xl/drawings/_rels/drawing1.xml.rels"
|
||||
f.Relationships.Delete(rels)
|
||||
f.Pkg.Store(rels, MacintoshCyrillicCharset)
|
||||
f.deleteDrawingRels(rels, "")
|
||||
}
|
||||
|
|
483
errors.go
483
errors.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -16,16 +16,210 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// newInvalidColumnNameError defined the error message on receiving the
|
||||
// invalid column name.
|
||||
func newInvalidColumnNameError(col string) error {
|
||||
return fmt.Errorf("invalid column name %q", col)
|
||||
var (
|
||||
// ErrAddVBAProject defined the error message on add the VBA project in
|
||||
// the workbook.
|
||||
ErrAddVBAProject = errors.New("unsupported VBA project")
|
||||
// ErrAttrValBool defined the error message on marshal and unmarshal
|
||||
// boolean type XML attribute.
|
||||
ErrAttrValBool = errors.New("unexpected child of attrValBool")
|
||||
// ErrCellCharsLength defined the error message for receiving a cell
|
||||
// characters length that exceeds the limit.
|
||||
ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
|
||||
// ErrCellStyles defined the error message on cell styles exceeds the limit.
|
||||
ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles)
|
||||
// ErrColumnNumber defined the error message on receive an invalid column
|
||||
// number.
|
||||
ErrColumnNumber = fmt.Errorf("the column number must be greater than or equal to %d and less than or equal to %d", MinColumns, MaxColumns)
|
||||
// ErrColumnWidth defined the error message on receive an invalid column
|
||||
// width.
|
||||
ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth)
|
||||
// ErrCoordinates defined the error message on invalid coordinates tuples
|
||||
// length.
|
||||
ErrCoordinates = errors.New("coordinates length must be 4")
|
||||
// ErrCustomNumFmt defined the error message on receive the empty custom number format.
|
||||
ErrCustomNumFmt = errors.New("custom number format can not be empty")
|
||||
// ErrDataValidationFormulaLength defined the error message for receiving a
|
||||
// data validation formula length that exceeds the limit.
|
||||
ErrDataValidationFormulaLength = fmt.Errorf("data validation must be 0-%d characters", MaxFieldLength)
|
||||
// ErrDataValidationRange defined the error message on set decimal range
|
||||
// exceeds limit.
|
||||
ErrDataValidationRange = errors.New("data validation range exceeds limit")
|
||||
// ErrDefinedNameDuplicate defined the error message on the same name
|
||||
// already exists on the scope.
|
||||
ErrDefinedNameDuplicate = errors.New("the same name already exists on the scope")
|
||||
// ErrDefinedNameScope defined the error message on not found defined name
|
||||
// in the given scope.
|
||||
ErrDefinedNameScope = errors.New("no defined name on the scope")
|
||||
// ErrExistsSheet defined the error message on given sheet already exists.
|
||||
ErrExistsSheet = errors.New("the same name sheet already exists")
|
||||
// ErrExistsTableName defined the error message on given table already exists.
|
||||
ErrExistsTableName = errors.New("the same name table already exists")
|
||||
// ErrFontLength defined the error message on the length of the font
|
||||
// family name overflow.
|
||||
ErrFontLength = fmt.Errorf("the length of the font family name must be less than or equal to %d", MaxFontFamilyLength)
|
||||
// ErrFontSize defined the error message on the size of the font is invalid.
|
||||
ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize)
|
||||
// ErrFormControlValue defined the error message for receiving a scroll
|
||||
// value exceeds limit.
|
||||
ErrFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue)
|
||||
// ErrGroupSheets defined the error message on group sheets.
|
||||
ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
|
||||
// ErrImgExt defined the error message on receive an unsupported image
|
||||
// extension.
|
||||
ErrImgExt = errors.New("unsupported image extension")
|
||||
// ErrInvalidFormula defined the error message on receive an invalid
|
||||
// formula.
|
||||
ErrInvalidFormula = errors.New("formula not valid")
|
||||
// ErrMaxFilePathLength defined the error message on receive the file path
|
||||
// length overflow.
|
||||
ErrMaxFilePathLength = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength)
|
||||
// ErrMaxRowHeight defined the error message on receive an invalid row
|
||||
// height.
|
||||
ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight)
|
||||
// ErrMaxRows defined the error message on receive a row number exceeds maximum limit.
|
||||
ErrMaxRows = errors.New("row number exceeds maximum limit")
|
||||
// ErrNameLength defined the error message on receiving the defined name or
|
||||
// table name length exceeds the limit.
|
||||
ErrNameLength = fmt.Errorf("the name length exceeds the %d characters limit", MaxFieldLength)
|
||||
// ErrOptionsUnzipSizeLimit defined the error message for receiving
|
||||
// invalid UnzipSizeLimit and UnzipXMLSizeLimit.
|
||||
ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit")
|
||||
// ErrOutlineLevel defined the error message on receive an invalid outline
|
||||
// level number.
|
||||
ErrOutlineLevel = errors.New("invalid outline level")
|
||||
// ErrPageSetupAdjustTo defined the error message for receiving a page setup
|
||||
// adjust to value exceeds limit.
|
||||
ErrPageSetupAdjustTo = errors.New("adjust to value must be between 10 and 400")
|
||||
// ErrParameterInvalid defined the error message on receive the invalid
|
||||
// parameter.
|
||||
ErrParameterInvalid = errors.New("parameter is invalid")
|
||||
// ErrParameterRequired defined the error message on receive the empty
|
||||
// parameter.
|
||||
ErrParameterRequired = errors.New("parameter is required")
|
||||
// ErrPasswordLengthInvalid defined the error message on invalid password
|
||||
// length.
|
||||
ErrPasswordLengthInvalid = errors.New("password length invalid")
|
||||
// ErrPivotTableClassicLayout defined the error message on enable
|
||||
// ClassicLayout and CompactData in the same time.
|
||||
ErrPivotTableClassicLayout = errors.New("cannot enable ClassicLayout and CompactData in the same time")
|
||||
// ErrSave defined the error message for saving file.
|
||||
ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write")
|
||||
// ErrSheetIdx defined the error message on receive the invalid worksheet
|
||||
// index.
|
||||
ErrSheetIdx = errors.New("invalid worksheet index")
|
||||
// ErrSheetNameBlank defined the error message on receive the blank sheet
|
||||
// name.
|
||||
ErrSheetNameBlank = errors.New("the sheet name can not be blank")
|
||||
// ErrSheetNameInvalid defined the error message on receive the sheet name
|
||||
// contains invalid characters.
|
||||
ErrSheetNameInvalid = errors.New("the sheet can not contain any of the characters :\\/?*[or]")
|
||||
// ErrSheetNameLength defined the error message on receiving the sheet
|
||||
// name length exceeds the limit.
|
||||
ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength)
|
||||
// ErrSheetNameSingleQuote defined the error message on the first or last
|
||||
// character of the sheet name was a single quote.
|
||||
ErrSheetNameSingleQuote = errors.New("the first or last character of the sheet name can not be a single quote")
|
||||
// ErrSparkline defined the error message on receive the invalid sparkline
|
||||
// parameters.
|
||||
ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters")
|
||||
// ErrSparklineLocation defined the error message on missing Location
|
||||
// parameters
|
||||
ErrSparklineLocation = errors.New("parameter 'Location' is required")
|
||||
// ErrSparklineRange defined the error message on missing sparkline Range
|
||||
// parameters
|
||||
ErrSparklineRange = errors.New("parameter 'Range' is required")
|
||||
// ErrSparklineStyle defined the error message on receive the invalid
|
||||
// sparkline Style parameters.
|
||||
ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35")
|
||||
// ErrSparklineType defined the error message on receive the invalid
|
||||
// sparkline Type parameters.
|
||||
ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'")
|
||||
// ErrStreamSetColWidth defined the error message on set column width in
|
||||
// stream writing mode.
|
||||
ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function")
|
||||
// ErrStreamSetPanes defined the error message on set panes in stream
|
||||
// writing mode.
|
||||
ErrStreamSetPanes = errors.New("must call the SetPanes function before the SetRow function")
|
||||
// ErrTotalSheetHyperlinks defined the error message on hyperlinks count
|
||||
// overflow.
|
||||
ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet")
|
||||
// ErrUnknownEncryptMechanism defined the error message on unsupported
|
||||
// encryption mechanism.
|
||||
ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism")
|
||||
// ErrUnprotectSheet defined the error message on worksheet has set no
|
||||
// protection.
|
||||
ErrUnprotectSheet = errors.New("worksheet has set no protect")
|
||||
// ErrUnprotectSheetPassword defined the error message on remove sheet
|
||||
// protection with password verification failed.
|
||||
ErrUnprotectSheetPassword = errors.New("worksheet protect password not match")
|
||||
// ErrUnprotectWorkbook defined the error message on workbook has set no
|
||||
// protection.
|
||||
ErrUnprotectWorkbook = errors.New("workbook has set no protect")
|
||||
// ErrUnprotectWorkbookPassword defined the error message on remove workbook
|
||||
// protection with password verification failed.
|
||||
ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
|
||||
// ErrUnsupportedEncryptMechanism defined the error message on unsupported
|
||||
// encryption mechanism.
|
||||
ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism")
|
||||
// ErrUnsupportedHashAlgorithm defined the error message on unsupported
|
||||
// hash algorithm.
|
||||
ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")
|
||||
// ErrUnsupportedNumberFormat defined the error message on unsupported number format
|
||||
// expression.
|
||||
ErrUnsupportedNumberFormat = errors.New("unsupported number format token")
|
||||
// ErrWorkbookFileFormat defined the error message on receive an
|
||||
// unsupported workbook file format.
|
||||
ErrWorkbookFileFormat = errors.New("unsupported workbook file format")
|
||||
// ErrWorkbookPassword defined the error message on receiving the incorrect
|
||||
// workbook password.
|
||||
ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
|
||||
)
|
||||
|
||||
// ErrSheetNotExist defined an error of sheet that does not exist.
|
||||
type ErrSheetNotExist struct {
|
||||
SheetName string
|
||||
}
|
||||
|
||||
// newInvalidRowNumberError defined the error message on receiving the invalid
|
||||
// row number.
|
||||
func newInvalidRowNumberError(row int) error {
|
||||
return fmt.Errorf("invalid row number %d", row)
|
||||
// Error returns the error message on receiving the non existing sheet name.
|
||||
func (err ErrSheetNotExist) Error() string {
|
||||
return fmt.Sprintf("sheet %s does not exist", err.SheetName)
|
||||
}
|
||||
|
||||
// newCellNameToCoordinatesError defined the error message on converts
|
||||
// alphanumeric cell name to coordinates.
|
||||
func newCellNameToCoordinatesError(cell string, err error) error {
|
||||
return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err)
|
||||
}
|
||||
|
||||
// newCoordinatesToCellNameError defined the error message on converts [X, Y]
|
||||
// coordinates to alpha-numeric cell name.
|
||||
func newCoordinatesToCellNameError(col, row int) error {
|
||||
return fmt.Errorf("invalid cell reference [%d, %d]", col, row)
|
||||
}
|
||||
|
||||
// newFieldLengthError defined the error message on receiving the field length
|
||||
// overflow.
|
||||
func newFieldLengthError(name string) error {
|
||||
return fmt.Errorf("field %s must be less than or equal to 255 characters", name)
|
||||
}
|
||||
|
||||
// newInvalidAutoFilterColumnError defined the error message on receiving the
|
||||
// incorrect index of column.
|
||||
func newInvalidAutoFilterColumnError(col string) error {
|
||||
return fmt.Errorf("incorrect index of column %q", col)
|
||||
}
|
||||
|
||||
// newInvalidAutoFilterExpError defined the error message on receiving the
|
||||
// incorrect number of tokens in criteria expression.
|
||||
func newInvalidAutoFilterExpError(exp string) error {
|
||||
return fmt.Errorf("incorrect number of tokens in criteria %q", exp)
|
||||
}
|
||||
|
||||
// newInvalidAutoFilterOperatorError defined the error message on receiving the
|
||||
// incorrect expression operator.
|
||||
func newInvalidAutoFilterOperatorError(op, exp string) error {
|
||||
return fmt.Errorf("the operator %q in expression %q is not valid in relation to Blanks/NonBlanks", op, exp)
|
||||
}
|
||||
|
||||
// newInvalidCellNameError defined the error message on receiving the invalid
|
||||
|
@ -34,16 +228,94 @@ func newInvalidCellNameError(cell string) error {
|
|||
return fmt.Errorf("invalid cell name %q", cell)
|
||||
}
|
||||
|
||||
// newInvalidColumnNameError defined the error message on receiving the
|
||||
// invalid column name.
|
||||
func newInvalidColumnNameError(col string) error {
|
||||
return fmt.Errorf("invalid column name %q", col)
|
||||
}
|
||||
|
||||
// newInvalidExcelDateError defined the error message on receiving the data
|
||||
// with negative values.
|
||||
func newInvalidExcelDateError(dateValue float64) error {
|
||||
return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue)
|
||||
}
|
||||
|
||||
// newInvalidTableNameError defined the error message on receiving the invalid
|
||||
// newInvalidLinkTypeError defined the error message on receiving the invalid
|
||||
// hyper link type.
|
||||
func newInvalidLinkTypeError(linkType string) error {
|
||||
return fmt.Errorf("invalid link type %q", linkType)
|
||||
}
|
||||
|
||||
// newInvalidNameError defined the error message on receiving the invalid
|
||||
// defined name or table name.
|
||||
func newInvalidNameError(name string) error {
|
||||
return fmt.Errorf("invalid name %q, the name should be starts with a letter or underscore, can not include a space or character, and can not conflict with an existing name in the workbook", name)
|
||||
}
|
||||
|
||||
// newInvalidPageLayoutValueError defined the error message on receiving the invalid
|
||||
// page layout options value.
|
||||
func newInvalidPageLayoutValueError(name, value, msg string) error {
|
||||
return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, msg)
|
||||
}
|
||||
|
||||
// newInvalidRowNumberError defined the error message on receiving the invalid
|
||||
// row number.
|
||||
func newInvalidRowNumberError(row int) error {
|
||||
return fmt.Errorf("invalid row number %d", row)
|
||||
}
|
||||
|
||||
// newInvalidSlicerNameError defined the error message on receiving the invalid
|
||||
// slicer name.
|
||||
func newInvalidSlicerNameError(name string) error {
|
||||
return fmt.Errorf("invalid slicer name %q", name)
|
||||
}
|
||||
|
||||
// newInvalidStyleID defined the error message on receiving the invalid style
|
||||
// ID.
|
||||
func newInvalidStyleID(styleID int) error {
|
||||
return fmt.Errorf("invalid style ID %d", styleID)
|
||||
}
|
||||
|
||||
// newNoExistSlicerError defined the error message on receiving the non existing
|
||||
// slicer name.
|
||||
func newNoExistSlicerError(name string) error {
|
||||
return fmt.Errorf("slicer %s does not exist", name)
|
||||
}
|
||||
|
||||
// newNoExistTableError defined the error message on receiving the non existing
|
||||
// table name.
|
||||
func newInvalidTableNameError(name string) error {
|
||||
return fmt.Errorf("invalid table name %q", name)
|
||||
func newNoExistTableError(name string) error {
|
||||
return fmt.Errorf("table %s does not exist", name)
|
||||
}
|
||||
|
||||
// newNotWorksheetError defined the error message on receiving a sheet which
|
||||
// not a worksheet.
|
||||
func newNotWorksheetError(name string) error {
|
||||
return fmt.Errorf("sheet %s is not a worksheet", name)
|
||||
}
|
||||
|
||||
// newPivotTableDataRangeError defined the error message on receiving the
|
||||
// invalid pivot table data range.
|
||||
func newPivotTableDataRangeError(msg string) error {
|
||||
return fmt.Errorf("parameter 'DataRange' parsing error: %s", msg)
|
||||
}
|
||||
|
||||
// newPivotTableRangeError defined the error message on receiving the invalid
|
||||
// pivot table range.
|
||||
func newPivotTableRangeError(msg string) error {
|
||||
return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", msg)
|
||||
}
|
||||
|
||||
// newStreamSetRowError defined the error message on the stream writer
|
||||
// receiving the non-ascending row number.
|
||||
func newStreamSetRowError(row int) error {
|
||||
return fmt.Errorf("row %d has already been written", row)
|
||||
}
|
||||
|
||||
// newUnknownFilterTokenError defined the error message on receiving a unknown
|
||||
// filter operator token.
|
||||
func newUnknownFilterTokenError(token string) error {
|
||||
return fmt.Errorf("unknown operator: %s", token)
|
||||
}
|
||||
|
||||
// newUnsupportedChartType defined the error message on receiving the chart
|
||||
|
@ -58,193 +330,8 @@ func newUnzipSizeLimitError(unzipSizeLimit int64) error {
|
|||
return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit)
|
||||
}
|
||||
|
||||
// newInvalidStyleID defined the error message on receiving the invalid style
|
||||
// ID.
|
||||
func newInvalidStyleID(styleID int) error {
|
||||
return fmt.Errorf("invalid style ID %d", styleID)
|
||||
}
|
||||
|
||||
// newFieldLengthError defined the error message on receiving the field length
|
||||
// overflow.
|
||||
func newFieldLengthError(name string) error {
|
||||
return fmt.Errorf("field %s must be less than or equal to 255 characters", name)
|
||||
}
|
||||
|
||||
// newCellNameToCoordinatesError defined the error message on converts
|
||||
// alphanumeric cell name to coordinates.
|
||||
func newCellNameToCoordinatesError(cell string, err error) error {
|
||||
return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err)
|
||||
}
|
||||
|
||||
// newNoExistSheetError defined the error message on receiving the non existing
|
||||
// sheet name.
|
||||
func newNoExistSheetError(name string) error {
|
||||
return fmt.Errorf("sheet %s does not exist", name)
|
||||
}
|
||||
|
||||
// newNotWorksheetError defined the error message on receiving a sheet which
|
||||
// not a worksheet.
|
||||
func newNotWorksheetError(name string) error {
|
||||
return fmt.Errorf("sheet %s is not a worksheet", name)
|
||||
}
|
||||
|
||||
// newStreamSetRowError defined the error message on the stream writer
|
||||
// receiving the non-ascending row number.
|
||||
func newStreamSetRowError(row int) error {
|
||||
return fmt.Errorf("row %d has already been written", row)
|
||||
}
|
||||
|
||||
// newViewIdxError defined the error message on receiving a invalid sheet view
|
||||
// index.
|
||||
func newViewIdxError(viewIndex int) error {
|
||||
return fmt.Errorf("view index %d out of range", viewIndex)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrStreamSetColWidth defined the error message on set column width in
|
||||
// stream writing mode.
|
||||
ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function")
|
||||
// ErrStreamSetPanes defined the error message on set panes in stream
|
||||
// writing mode.
|
||||
ErrStreamSetPanes = errors.New("must call the SetPanes function before the SetRow function")
|
||||
// ErrColumnNumber defined the error message on receive an invalid column
|
||||
// number.
|
||||
ErrColumnNumber = fmt.Errorf(`the column number must be greater than or equal to %d and less than or equal to %d`, MinColumns, MaxColumns)
|
||||
// ErrColumnWidth defined the error message on receive an invalid column
|
||||
// width.
|
||||
ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth)
|
||||
// ErrOutlineLevel defined the error message on receive an invalid outline
|
||||
// level number.
|
||||
ErrOutlineLevel = errors.New("invalid outline level")
|
||||
// ErrCoordinates defined the error message on invalid coordinates tuples
|
||||
// length.
|
||||
ErrCoordinates = errors.New("coordinates length must be 4")
|
||||
// ErrExistsSheet defined the error message on given sheet already exists.
|
||||
ErrExistsSheet = errors.New("the same name sheet already exists")
|
||||
// ErrTotalSheetHyperlinks defined the error message on hyperlinks count
|
||||
// overflow.
|
||||
ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet")
|
||||
// ErrInvalidFormula defined the error message on receive an invalid
|
||||
// formula.
|
||||
ErrInvalidFormula = errors.New("formula not valid")
|
||||
// ErrAddVBAProject defined the error message on add the VBA project in
|
||||
// the workbook.
|
||||
ErrAddVBAProject = errors.New("unsupported VBA project")
|
||||
// ErrMaxRows defined the error message on receive a row number exceeds maximum limit.
|
||||
ErrMaxRows = errors.New("row number exceeds maximum limit")
|
||||
// ErrMaxRowHeight defined the error message on receive an invalid row
|
||||
// height.
|
||||
ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight)
|
||||
// ErrImgExt defined the error message on receive an unsupported image
|
||||
// extension.
|
||||
ErrImgExt = errors.New("unsupported image extension")
|
||||
// ErrWorkbookFileFormat defined the error message on receive an
|
||||
// unsupported workbook file format.
|
||||
ErrWorkbookFileFormat = errors.New("unsupported workbook file format")
|
||||
// ErrMaxFilePathLength defined the error message on receive the file path
|
||||
// length overflow.
|
||||
ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit")
|
||||
// ErrUnknownEncryptMechanism defined the error message on unsupported
|
||||
// encryption mechanism.
|
||||
ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism")
|
||||
// ErrUnsupportedEncryptMechanism defined the error message on unsupported
|
||||
// encryption mechanism.
|
||||
ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism")
|
||||
// ErrUnsupportedHashAlgorithm defined the error message on unsupported
|
||||
// hash algorithm.
|
||||
ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")
|
||||
// ErrUnsupportedNumberFormat defined the error message on unsupported number format
|
||||
// expression.
|
||||
ErrUnsupportedNumberFormat = errors.New("unsupported number format token")
|
||||
// ErrPasswordLengthInvalid defined the error message on invalid password
|
||||
// length.
|
||||
ErrPasswordLengthInvalid = errors.New("password length invalid")
|
||||
// ErrParameterRequired defined the error message on receive the empty
|
||||
// parameter.
|
||||
ErrParameterRequired = errors.New("parameter is required")
|
||||
// ErrParameterInvalid defined the error message on receive the invalid
|
||||
// parameter.
|
||||
ErrParameterInvalid = errors.New("parameter is invalid")
|
||||
// ErrDefinedNameScope defined the error message on not found defined name
|
||||
// in the given scope.
|
||||
ErrDefinedNameScope = errors.New("no defined name on the scope")
|
||||
// ErrDefinedNameDuplicate defined the error message on the same name
|
||||
// already exists on the scope.
|
||||
ErrDefinedNameDuplicate = errors.New("the same name already exists on the scope")
|
||||
// ErrCustomNumFmt defined the error message on receive the empty custom number format.
|
||||
ErrCustomNumFmt = errors.New("custom number format can not be empty")
|
||||
// ErrFontLength defined the error message on the length of the font
|
||||
// family name overflow.
|
||||
ErrFontLength = fmt.Errorf("the length of the font family name must be less than or equal to %d", MaxFontFamilyLength)
|
||||
// ErrFontSize defined the error message on the size of the font is invalid.
|
||||
ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize)
|
||||
// ErrSheetIdx defined the error message on receive the invalid worksheet
|
||||
// index.
|
||||
ErrSheetIdx = errors.New("invalid worksheet index")
|
||||
// ErrUnprotectSheet defined the error message on worksheet has set no
|
||||
// protection.
|
||||
ErrUnprotectSheet = errors.New("worksheet has set no protect")
|
||||
// ErrUnprotectSheetPassword defined the error message on remove sheet
|
||||
// protection with password verification failed.
|
||||
ErrUnprotectSheetPassword = errors.New("worksheet protect password not match")
|
||||
// ErrGroupSheets defined the error message on group sheets.
|
||||
ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
|
||||
// ErrDataValidationFormulaLength defined the error message for receiving a
|
||||
// data validation formula length that exceeds the limit.
|
||||
ErrDataValidationFormulaLength = fmt.Errorf("data validation must be 0-%d characters", MaxFieldLength)
|
||||
// ErrDataValidationRange defined the error message on set decimal range
|
||||
// exceeds limit.
|
||||
ErrDataValidationRange = errors.New("data validation range exceeds limit")
|
||||
// ErrCellCharsLength defined the error message for receiving a cell
|
||||
// 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 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
|
||||
// boolean type XML attribute.
|
||||
ErrAttrValBool = errors.New("unexpected child of attrValBool")
|
||||
// ErrSparklineType defined the error message on receive the invalid
|
||||
// sparkline Type parameters.
|
||||
ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'")
|
||||
// ErrSparklineLocation defined the error message on missing Location
|
||||
// parameters
|
||||
ErrSparklineLocation = errors.New("parameter 'Location' is required")
|
||||
// ErrSparklineRange defined the error message on missing sparkline Range
|
||||
// parameters
|
||||
ErrSparklineRange = errors.New("parameter 'Range' is required")
|
||||
// ErrSparkline defined the error message on receive the invalid sparkline
|
||||
// parameters.
|
||||
ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters")
|
||||
// ErrSparklineStyle defined the error message on receive the invalid
|
||||
// sparkline Style parameters.
|
||||
ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35")
|
||||
// ErrWorkbookPassword defined the error message on receiving the incorrect
|
||||
// workbook password.
|
||||
ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
|
||||
// ErrSheetNameInvalid defined the error message on receive the sheet name
|
||||
// contains invalid characters.
|
||||
ErrSheetNameInvalid = errors.New("the sheet can not contain any of the characters :\\/?*[or]")
|
||||
// ErrSheetNameSingleQuote defined the error message on the first or last
|
||||
// character of the sheet name was a single quote.
|
||||
ErrSheetNameSingleQuote = errors.New("the first or last character of the sheet name can not be a single quote")
|
||||
// ErrSheetNameBlank defined the error message on receive the blank sheet
|
||||
// name.
|
||||
ErrSheetNameBlank = errors.New("the sheet name can not be blank")
|
||||
// ErrSheetNameLength defined the error message on receiving the sheet
|
||||
// name length exceeds the limit.
|
||||
ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength)
|
||||
// ErrTableNameLength defined the error message on receiving the table name
|
||||
// length exceeds the limit.
|
||||
ErrTableNameLength = fmt.Errorf("the table name length exceeds the %d characters limit", MaxFieldLength)
|
||||
// ErrCellStyles defined the error message on cell styles exceeds the limit.
|
||||
ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles)
|
||||
// ErrUnprotectWorkbook defined the error message on workbook has set no
|
||||
// protection.
|
||||
ErrUnprotectWorkbook = errors.New("workbook has set no protect")
|
||||
// ErrUnprotectWorkbookPassword defined the error message on remove workbook
|
||||
// protection with password verification failed.
|
||||
ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
|
||||
)
|
||||
|
|
285
excelize.go
285
excelize.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
//
|
||||
// See https://xuri.me/excelize for more information about this package.
|
||||
package excelize
|
||||
|
@ -28,39 +28,42 @@ import (
|
|||
|
||||
// File define a populated spreadsheet file struct.
|
||||
type File struct {
|
||||
sync.Mutex
|
||||
mu sync.Mutex
|
||||
checked sync.Map
|
||||
formulaChecked bool
|
||||
options *Options
|
||||
xmlAttr map[string][]xml.Attr
|
||||
checked map[string]bool
|
||||
sharedStringItem [][]uint
|
||||
sharedStringsMap map[string]int
|
||||
sharedStringTemp *os.File
|
||||
sheetMap map[string]string
|
||||
streams map[string]*StreamWriter
|
||||
tempFiles sync.Map
|
||||
sharedStringsMap map[string]int
|
||||
sharedStringItem [][]uint
|
||||
sharedStringTemp *os.File
|
||||
xmlAttr sync.Map
|
||||
CalcChain *xlsxCalcChain
|
||||
CharsetReader charsetTranscoderFn
|
||||
Comments map[string]*xlsxComments
|
||||
ContentTypes *xlsxTypes
|
||||
DecodeVMLDrawing map[string]*decodeVmlDrawing
|
||||
DecodeCellImages *decodeCellImages
|
||||
Drawings sync.Map
|
||||
Path string
|
||||
Pkg sync.Map
|
||||
Relationships sync.Map
|
||||
SharedStrings *xlsxSST
|
||||
Sheet sync.Map
|
||||
SheetCount int
|
||||
Styles *xlsxStyleSheet
|
||||
Theme *xlsxTheme
|
||||
DecodeVMLDrawing map[string]*decodeVmlDrawing
|
||||
Theme *decodeTheme
|
||||
VMLDrawing map[string]*vmlDrawing
|
||||
VolatileDeps *xlsxVolTypes
|
||||
WorkBook *xlsxWorkbook
|
||||
Relationships sync.Map
|
||||
Pkg sync.Map
|
||||
CharsetReader charsetTranscoderFn
|
||||
}
|
||||
|
||||
// charsetTranscoderFn set user-defined codepage transcoder function for open
|
||||
// the spreadsheet from non-UTF-8 encoding.
|
||||
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
|
||||
|
||||
// Options define the options for open and reading spreadsheet.
|
||||
// Options define the options for opening and reading the spreadsheet.
|
||||
//
|
||||
// MaxCalcIterations specifies the maximum iterations for iterative
|
||||
// calculation, the default value is 0.
|
||||
|
@ -70,7 +73,7 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e
|
|||
// RawCellValue specifies if apply the number format for the cell value or get
|
||||
// the raw value.
|
||||
//
|
||||
// UnzipSizeLimit specifies the unzip size limit in bytes on open the
|
||||
// UnzipSizeLimit specifies to unzip size limit in bytes on open the
|
||||
// spreadsheet, this value should be greater than or equal to
|
||||
// UnzipXMLSizeLimit, the default size limit is 16GB.
|
||||
//
|
||||
|
@ -79,15 +82,34 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e
|
|||
// 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.
|
||||
//
|
||||
// ShortDatePattern specifies the short date number format code. In the
|
||||
// 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
|
||||
// system settings. The ShortDatePattern used for specifies apply date formats
|
||||
// that begin with an asterisk.
|
||||
//
|
||||
// LongDatePattern specifies the long date number format code.
|
||||
//
|
||||
// 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.
|
||||
type Options struct {
|
||||
MaxCalcIterations uint
|
||||
Password string
|
||||
RawCellValue bool
|
||||
UnzipSizeLimit int64
|
||||
UnzipXMLSizeLimit int64
|
||||
ShortDatePattern string
|
||||
LongDatePattern string
|
||||
LongTimePattern string
|
||||
CultureInfo CultureName
|
||||
}
|
||||
|
||||
// OpenFile take the name of an spreadsheet file and returns a populated
|
||||
// OpenFile take the name of a spreadsheet file and returns a populated
|
||||
// spreadsheet file struct for it. For example, open spreadsheet with
|
||||
// password protection:
|
||||
//
|
||||
|
@ -101,12 +123,11 @@ func OpenFile(filename string, opts ...Options) (*File, error) {
|
|||
}
|
||||
f, err := OpenReader(file, opts...)
|
||||
if err != nil {
|
||||
closeErr := file.Close()
|
||||
if closeErr == nil {
|
||||
return f, err
|
||||
}
|
||||
if closeErr := file.Close(); closeErr != nil {
|
||||
return f, closeErr
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
f.Path = filename
|
||||
return f, file.Close()
|
||||
}
|
||||
|
@ -115,8 +136,8 @@ func OpenFile(filename string, opts ...Options) (*File, error) {
|
|||
func newFile() *File {
|
||||
return &File{
|
||||
options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize},
|
||||
xmlAttr: make(map[string][]xml.Attr),
|
||||
checked: make(map[string]bool),
|
||||
xmlAttr: sync.Map{},
|
||||
checked: sync.Map{},
|
||||
sheetMap: make(map[string]string),
|
||||
tempFiles: sync.Map{},
|
||||
Comments: make(map[string]*xlsxComments),
|
||||
|
@ -148,7 +169,7 @@ func (f *File) checkOpenReaderOptions() error {
|
|||
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
|
||||
return ErrOptionsUnzipSizeLimit
|
||||
}
|
||||
return nil
|
||||
return f.checkDateTimePattern()
|
||||
}
|
||||
|
||||
// OpenReader read data stream from io.Reader and return a populated
|
||||
|
@ -159,7 +180,7 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
|
|||
return nil, err
|
||||
}
|
||||
f := newFile()
|
||||
f.options = getOptions(opts...)
|
||||
f.options = f.getOptions(opts...)
|
||||
if err = f.checkOpenReaderOptions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -198,8 +219,8 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
|
|||
|
||||
// getOptions provides a function to parse the optional settings for open
|
||||
// and reading spreadsheet.
|
||||
func getOptions(opts ...Options) *Options {
|
||||
options := &Options{}
|
||||
func (f *File) getOptions(opts ...Options) *Options {
|
||||
options := f.options
|
||||
for _, opt := range opts {
|
||||
options = &opt
|
||||
}
|
||||
|
@ -207,7 +228,7 @@ func getOptions(opts ...Options) *Options {
|
|||
}
|
||||
|
||||
// CharsetTranscoder Set user defined codepage transcoder function for open
|
||||
// XLSX from non UTF-8 encoding.
|
||||
// workbook from non UTF-8 encoding.
|
||||
func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f }
|
||||
|
||||
// Creates new XML decoder with charset reader.
|
||||
|
@ -221,22 +242,23 @@ func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
|
|||
// time.Time type cell value by given worksheet name, cell reference and
|
||||
// number format code.
|
||||
func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error {
|
||||
s, err := f.GetCellStyle(sheet, cell)
|
||||
styleIdx, err := f.GetCellStyle(sheet, cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s == 0 {
|
||||
style, _ := f.NewStyle(&Style{NumFmt: format})
|
||||
err = f.SetCellStyle(sheet, cell, cell, style)
|
||||
if styleIdx == 0 {
|
||||
styleIdx, _ = f.NewStyle(&Style{NumFmt: format})
|
||||
} else {
|
||||
style, _ := f.GetStyle(styleIdx)
|
||||
style.NumFmt = format
|
||||
styleIdx, _ = f.NewStyle(style)
|
||||
}
|
||||
return err
|
||||
return f.SetCellStyle(sheet, cell, cell, styleIdx)
|
||||
}
|
||||
|
||||
// workSheetReader provides a function to get the pointer to the structure
|
||||
// after deserialization by given worksheet name.
|
||||
func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
var (
|
||||
name string
|
||||
ok bool
|
||||
|
@ -245,7 +267,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
|
|||
return
|
||||
}
|
||||
if name, ok = f.getSheetXMLPath(sheet); !ok {
|
||||
err = newNoExistSheetError(sheet)
|
||||
err = ErrSheetNotExist{sheet}
|
||||
return
|
||||
}
|
||||
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
|
||||
|
@ -259,24 +281,25 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
|
|||
}
|
||||
}
|
||||
ws = new(xlsxWorksheet)
|
||||
if _, ok := f.xmlAttr[name]; !ok {
|
||||
if attrs, ok := f.xmlAttr.Load(name); !ok {
|
||||
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name))))
|
||||
f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...)
|
||||
if attrs == nil {
|
||||
attrs = []xml.Attr{}
|
||||
}
|
||||
attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
|
||||
f.xmlAttr.Store(name, attrs)
|
||||
}
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))).
|
||||
Decode(ws); err != nil && err != io.EOF {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
if f.checked == nil {
|
||||
f.checked = make(map[string]bool)
|
||||
}
|
||||
if ok = f.checked[name]; !ok {
|
||||
checkSheet(ws)
|
||||
if err = checkRow(ws); err != nil {
|
||||
if _, ok = f.checked.Load(name); !ok {
|
||||
ws.checkSheet()
|
||||
if err = ws.checkRow(); err != nil {
|
||||
return
|
||||
}
|
||||
f.checked[name] = true
|
||||
f.checked.Store(name, true)
|
||||
}
|
||||
f.Sheet.Store(name, ws)
|
||||
return
|
||||
|
@ -284,55 +307,66 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
|
|||
|
||||
// checkSheet provides a function to fill each row element and make that is
|
||||
// continuous in a worksheet of XML.
|
||||
func checkSheet(ws *xlsxWorksheet) {
|
||||
var row int
|
||||
var r0 xlsxRow
|
||||
for i, r := range ws.SheetData.Row {
|
||||
if i == 0 && r.R == 0 {
|
||||
r0 = r
|
||||
ws.SheetData.Row = ws.SheetData.Row[1:]
|
||||
func (ws *xlsxWorksheet) checkSheet() {
|
||||
var (
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return num
|
||||
}
|
||||
)
|
||||
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++
|
||||
}
|
||||
r.R = row
|
||||
r0Rows = append(r0Rows, r)
|
||||
ws.SheetData.Row = append(ws.SheetData.Row[:i], ws.SheetData.Row[i+1:]...)
|
||||
i--
|
||||
continue
|
||||
}
|
||||
if r.R != 0 && r.R > row {
|
||||
row = r.R
|
||||
continue
|
||||
}
|
||||
if r.R != row {
|
||||
row++
|
||||
}
|
||||
}
|
||||
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
|
||||
row = 0
|
||||
for _, r := range ws.SheetData.Row {
|
||||
if r.R == row && row > 0 {
|
||||
sheetData.Row[r.R-1].C = append(sheetData.Row[r.R-1].C, r.C...)
|
||||
continue
|
||||
}
|
||||
if r.R != 0 {
|
||||
sheetData.Row[r.R-1] = r
|
||||
row = r.R
|
||||
continue
|
||||
}
|
||||
row++
|
||||
r.R = row
|
||||
sheetData.Row[row-1] = 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)
|
||||
}
|
||||
checkSheetR0(ws, &sheetData, &r0)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) {
|
||||
for _, cell := range r0.C {
|
||||
if col, row, err := CellNameToCoordinates(cell.R); err == nil {
|
||||
rows, rowIdx := len(sheetData.Row), row-1
|
||||
for r := rows; r < row; r++ {
|
||||
sheetData.Row = append(sheetData.Row, xlsxRow{R: r + 1})
|
||||
}
|
||||
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{})
|
||||
|
@ -340,6 +374,19 @@ func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
ws.SheetData = *sheetData
|
||||
|
@ -352,8 +399,8 @@ func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
|
|||
if rels == nil || rID == "" {
|
||||
return f.addRels(relPath, relType, target, targetMode)
|
||||
}
|
||||
rels.Lock()
|
||||
defer rels.Unlock()
|
||||
rels.mu.Lock()
|
||||
defer rels.mu.Unlock()
|
||||
var ID int
|
||||
for i, rel := range rels.Relationships {
|
||||
if rel.ID == rID {
|
||||
|
@ -377,8 +424,8 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
|
|||
if rels == nil {
|
||||
rels = &xlsxRelationships{}
|
||||
}
|
||||
rels.Lock()
|
||||
defer rels.Unlock()
|
||||
rels.mu.Lock()
|
||||
defer rels.mu.Unlock()
|
||||
var rID int
|
||||
for idx, rel := range rels.Relationships {
|
||||
ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
|
||||
|
@ -409,14 +456,14 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
|
|||
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
|
||||
// Office Excel application. This function will be remove value tag when met a
|
||||
// cell have a linked value. Reference
|
||||
// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating
|
||||
// https://learn.microsoft.com/en-us/archive/msdn-technet-forums/e16bae1f-6a2c-4325-8013-e989a3479066
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// <row r="19" spans="2:2">
|
||||
// <row r="19">
|
||||
// <c r="B19">
|
||||
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
||||
// <v>100</v>
|
||||
|
@ -425,7 +472,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
|
|||
//
|
||||
// to
|
||||
//
|
||||
// <row r="19" spans="2:2">
|
||||
// <row r="19">
|
||||
// <c r="B19">
|
||||
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
|
||||
// </c>
|
||||
|
@ -491,8 +538,8 @@ func (f *File) AddVBAProject(file []byte) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rels.Lock()
|
||||
defer rels.Unlock()
|
||||
rels.mu.Lock()
|
||||
defer rels.mu.Unlock()
|
||||
var rID int
|
||||
var ok bool
|
||||
for _, rel := range rels.Relationships {
|
||||
|
@ -525,8 +572,8 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
for _, v := range content.Defaults {
|
||||
if v.Extension == "bin" {
|
||||
ok = true
|
||||
|
@ -545,3 +592,77 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValuePart)))).
|
||||
Decode(&richValue); err != nil && err != io.EOF {
|
||||
return &richValue, err
|
||||
}
|
||||
return &richValue, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))).
|
||||
Decode(&richValueRels); err != nil && err != io.EOF {
|
||||
return &richValueRels, err
|
||||
}
|
||||
return &richValueRels, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship {
|
||||
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 {
|
||||
rels.mu.Lock()
|
||||
defer rels.mu.Unlock()
|
||||
for _, v := range rels.Relationships {
|
||||
if v.ID == rID {
|
||||
return &v
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
222
excelize_test.go
222
excelize_test.go
|
@ -16,6 +16,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -207,6 +208,30 @@ func TestSaveFile(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.Save())
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
t.Run("for_save_multiple_times", func(t *testing.T) {
|
||||
{
|
||||
f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A20", 20))
|
||||
assert.NoError(t, f.Save())
|
||||
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A21", 21))
|
||||
assert.NoError(t, f.Save())
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
{
|
||||
f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
val, err := f.GetCellValue("Sheet1", "A20")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "20", val)
|
||||
val, err = f.GetCellValue("Sheet1", "A21")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "21", val)
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSaveAsWrongPath(t *testing.T) {
|
||||
|
@ -364,11 +389,11 @@ func TestNewFile(t *testing.T) {
|
|||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
_, err = f.NewSheet("XLSXSheet2")
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
_, err = f.NewSheet("XLSXSheet3")
|
||||
_, err = f.NewSheet("Sheet3")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellInt("XLSXSheet2", "A23", 56))
|
||||
assert.NoError(t, f.SetCellInt("Sheet2", "A23", 56))
|
||||
assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42"))
|
||||
f.SetActiveSheet(0)
|
||||
|
||||
|
@ -383,15 +408,6 @@ func TestNewFile(t *testing.T) {
|
|||
assert.NoError(t, f.Save())
|
||||
}
|
||||
|
||||
func TestAddDrawingVML(t *testing.T) {
|
||||
// Test addDrawingVML with illegal cell reference
|
||||
f := NewFile()
|
||||
assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error())
|
||||
|
||||
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", "A1", 0, 0), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestSetCellHyperLink(t *testing.T) {
|
||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
|
@ -408,8 +424,8 @@ func TestSetCellHyperLink(t *testing.T) {
|
|||
Tooltip: &tooltip,
|
||||
}))
|
||||
// Test set cell hyperlink with invalid sheet name
|
||||
assert.EqualError(t, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"), ErrSheetNameInvalid.Error())
|
||||
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`)
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"))
|
||||
assert.Equal(t, newInvalidLinkTypeError(""), f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""))
|
||||
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
|
@ -439,6 +455,18 @@ func TestSetCellHyperLink(t *testing.T) {
|
|||
assert.Equal(t, link, true)
|
||||
assert.Equal(t, "https://github.com/xuri/excelize", target)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test remove hyperlink for a cell
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location"))
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A1:D4"
|
||||
assert.NoError(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"))
|
||||
// Test remove hyperlink for a cell with invalid cell reference
|
||||
assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location"))
|
||||
ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A:A"
|
||||
assert.Error(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
|
||||
}
|
||||
|
||||
func TestGetCellHyperLink(t *testing.T) {
|
||||
|
@ -747,33 +775,33 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
|
|||
|
||||
// Test only set fill and number format for a cell
|
||||
col := []string{"L", "M", "N", "O", "P"}
|
||||
data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}
|
||||
idxTbl := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}
|
||||
value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"}
|
||||
expected := [][]string{
|
||||
{"37947.7500001", "37948", "37947.75", "37,948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"},
|
||||
{"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"},
|
||||
{"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"},
|
||||
{"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"},
|
||||
{"37947.75", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947 3/4", "37947 3/4", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", " 37,948 ", " $37,948 ", " 37,947.75 ", " $37,947.75 ", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"},
|
||||
{"-37947.75", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947 3/4", "-37947 3/4", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", " (37,948)", " $(37,948)", " (37,947.75)", " $(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"},
|
||||
{"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0 ", "0 ", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", " 0 ", " $0 ", " 0.01 ", " $0.01 ", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"},
|
||||
{"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2 1/9", "2 1/10", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", " 2 ", " $2 ", " 2.10 ", " $2.10 ", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"},
|
||||
{"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", " String ", " String ", " String ", " String ", "String", "String", "String", "String", "String"},
|
||||
}
|
||||
|
||||
for i, v := range value {
|
||||
for k, d := range data {
|
||||
c := col[i] + strconv.Itoa(k+1)
|
||||
for c, v := range value {
|
||||
for r, idx := range idxTbl {
|
||||
cell := col[c] + strconv.Itoa(r+1)
|
||||
var val float64
|
||||
val, err = strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", c, v))
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", cell, v))
|
||||
} else {
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", c, val))
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", cell, val))
|
||||
}
|
||||
style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: d})
|
||||
style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: idx})
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style))
|
||||
cellValue, err := f.GetCellValue("Sheet2", c)
|
||||
assert.Equal(t, expected[i][k], cellValue, "Sheet2!"+c, i, k)
|
||||
assert.NoError(t, f.SetCellStyle("Sheet2", cell, cell, style))
|
||||
cellValue, err := f.GetCellValue("Sheet2", cell)
|
||||
assert.Equal(t, expected[c][r], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %s c: %d r: %d", cell, value[c], builtInNumFmt[idx], c, r))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
@ -783,6 +811,16 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
|
|||
assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style))
|
||||
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx")))
|
||||
|
||||
// Test get cell value with built-in number format code 22 with custom short date pattern
|
||||
f = NewFile(Options{ShortDatePattern: "yyyy-m-dd"})
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 45074.625694444447))
|
||||
style, err = f.NewStyle(&Style{NumFmt: 22})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
|
||||
cellValue, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2023-5-28 15:01", cellValue)
|
||||
}
|
||||
|
||||
func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
|
||||
|
@ -793,11 +831,11 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
|
|||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3))
|
||||
var style int
|
||||
style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: -1})
|
||||
style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(-1)})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
|
||||
style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: 31, NegRed: true})
|
||||
style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(31), NegRed: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
|
||||
|
@ -811,19 +849,19 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
|
|||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5))
|
||||
|
||||
_, err = f.NewStyle(&Style{NumFmt: 26, Lang: "zh-tw"})
|
||||
_, err = f.NewStyle(&Style{NumFmt: 26})
|
||||
assert.NoError(t, err)
|
||||
|
||||
style, err := f.NewStyle(&Style{NumFmt: 27})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
|
||||
style, err = f.NewStyle(&Style{NumFmt: 31, Lang: "ko-kr"})
|
||||
style, err = f.NewStyle(&Style{NumFmt: 31})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
|
||||
|
||||
style, err = f.NewStyle(&Style{NumFmt: 71, Lang: "th-th"})
|
||||
style, err = f.NewStyle(&Style{NumFmt: 71})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
|
||||
|
||||
|
@ -831,6 +869,50 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSetCellStyleLangNumberFormat(t *testing.T) {
|
||||
rawCellValues := make([][]string, 42)
|
||||
for i := 0; i < 42; i++ {
|
||||
rawCellValues[i] = []string{"45162"}
|
||||
}
|
||||
for lang, expected := range map[CultureName][][]string{
|
||||
CultureNameUnknown: rawCellValues,
|
||||
CultureNameEnUS: {{"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"45162"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0時00分"}, {"0時00分00秒"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"08-24-56"}, {"4356년 08월 24일"}, {"0시 00분"}, {"0시 00분 00초"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0时00分"}, {"0时00分00秒"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"00時00分"}, {"00時00分00秒"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
} {
|
||||
f, err := prepareTestBook5(Options{CultureInfo: lang})
|
||||
assert.NoError(t, err)
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, rows)
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
// Test apply language number format code with date and time pattern
|
||||
for lang, expected := range map[CultureName][][]string{
|
||||
CultureNameEnUS: {{"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"45162"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"4356-8-24"}, {"4356년 08월 24일"}, {"00:00:00"}, {"00:00:00"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
|
||||
} {
|
||||
f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"})
|
||||
assert.NoError(t, err)
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, rows)
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
// Test open workbook with invalid date and time pattern options
|
||||
_, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongDatePattern: "0.00"})
|
||||
assert.Equal(t, ErrUnsupportedNumberFormat, err)
|
||||
_, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongTimePattern: "0.00"})
|
||||
assert.Equal(t, ErrUnsupportedNumberFormat, err)
|
||||
_, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{ShortDatePattern: "0.00"})
|
||||
assert.Equal(t, ErrUnsupportedNumberFormat, err)
|
||||
}
|
||||
|
||||
func TestSetCellStyleCustomNumberFormat(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
|
||||
|
@ -925,7 +1007,7 @@ func TestSetDeleteSheet(t *testing.T) {
|
|||
f, err := prepareTestBook3()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, f.DeleteSheet("XLSXSheet3"))
|
||||
assert.NoError(t, f.DeleteSheet("Sheet3"))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx")))
|
||||
})
|
||||
|
||||
|
@ -933,7 +1015,7 @@ func TestSetDeleteSheet(t *testing.T) {
|
|||
f, err := prepareTestBook4()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DeleteSheet("Sheet1"))
|
||||
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx")))
|
||||
})
|
||||
}
|
||||
|
@ -997,7 +1079,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
f := NewFile()
|
||||
sheet1 := f.GetSheetName(0)
|
||||
|
||||
fillCells(f, sheet1, 10, 15)
|
||||
assert.NoError(t, fillCells(f, sheet1, 10, 15))
|
||||
|
||||
var format1, format2, format3, format4 int
|
||||
var err error
|
||||
|
@ -1051,7 +1133,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "cell",
|
||||
Criteria: "between",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
MinValue: "6",
|
||||
MaxValue: "8",
|
||||
},
|
||||
|
@ -1063,7 +1145,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "cell",
|
||||
Criteria: ">",
|
||||
Format: format3,
|
||||
Format: &format3,
|
||||
Value: "6",
|
||||
},
|
||||
},
|
||||
|
@ -1074,7 +1156,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "top",
|
||||
Criteria: "=",
|
||||
Format: format3,
|
||||
Format: &format3,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1084,7 +1166,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "unique",
|
||||
Criteria: "=",
|
||||
Format: format2,
|
||||
Format: &format2,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1094,7 +1176,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "duplicate",
|
||||
Criteria: "=",
|
||||
Format: format2,
|
||||
Format: &format2,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1104,7 +1186,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "top",
|
||||
Criteria: "=",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
Value: "6",
|
||||
Percent: true,
|
||||
},
|
||||
|
@ -1116,7 +1198,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "average",
|
||||
Criteria: "=",
|
||||
Format: format3,
|
||||
Format: &format3,
|
||||
AboveAverage: true,
|
||||
},
|
||||
},
|
||||
|
@ -1127,7 +1209,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "average",
|
||||
Criteria: "=",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
AboveAverage: false,
|
||||
},
|
||||
},
|
||||
|
@ -1150,7 +1232,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "formula",
|
||||
Criteria: "L2<3",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1160,21 +1242,23 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "cell",
|
||||
Criteria: ">",
|
||||
Format: format4,
|
||||
Format: &format4,
|
||||
Value: "0",
|
||||
},
|
||||
},
|
||||
))
|
||||
// Test set conditional format with invalid cell reference
|
||||
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.SetConditionalFormat("Sheet1", "A1:-", nil))
|
||||
// Test set conditional format on not exists worksheet
|
||||
assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", nil), "sheet SheetN does not exist")
|
||||
// Test set conditional format with invalid sheet name
|
||||
assert.EqualError(t, f.SetConditionalFormat("Sheet:1", "L1:L10", nil), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetConditionalFormat("Sheet:1", "L1:L10", nil))
|
||||
|
||||
err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Set conditional format with illegal valid type
|
||||
assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10",
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10",
|
||||
[]ConditionalFormatOptions{
|
||||
{
|
||||
Type: "",
|
||||
|
@ -1186,7 +1270,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
},
|
||||
))
|
||||
// Set conditional format with illegal criteria type
|
||||
assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10",
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10",
|
||||
[]ConditionalFormatOptions{
|
||||
{
|
||||
Type: "data_bar",
|
||||
|
@ -1200,7 +1284,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
// Test create conditional format with invalid custom number format
|
||||
var exp string
|
||||
_, err = f.NewConditionalStyle(&Style{CustomNumFmt: &exp})
|
||||
assert.EqualError(t, err, ErrCustomNumFmt.Error())
|
||||
assert.Equal(t, ErrCustomNumFmt, err)
|
||||
|
||||
// Set conditional format with file without dxfs element should not return error
|
||||
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
|
@ -1495,7 +1579,7 @@ func TestWorkSheetReader(t *testing.T) {
|
|||
f = NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
_, err = f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -1571,13 +1655,13 @@ func prepareTestBook1() (*File, error) {
|
|||
|
||||
func prepareTestBook3() (*File, error) {
|
||||
f := NewFile()
|
||||
if _, err := f.NewSheet("XLSXSheet2"); err != nil {
|
||||
if _, err := f.NewSheet("Sheet2"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := f.NewSheet("XLSXSheet3"); err != nil {
|
||||
if _, err := f.NewSheet("Sheet3"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil {
|
||||
if err := f.SetCellInt("Sheet2", "A23", 56); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil {
|
||||
|
@ -1612,15 +1696,41 @@ func prepareTestBook4() (*File, error) {
|
|||
return f, nil
|
||||
}
|
||||
|
||||
func fillCells(f *File, sheet string, colCount, rowCount int) {
|
||||
func prepareTestBook5(opts Options) (*File, error) {
|
||||
f := NewFile(opts)
|
||||
var rowNum int
|
||||
for _, idxRange := range [][]int{{27, 36}, {50, 81}} {
|
||||
for numFmtIdx := idxRange[0]; numFmtIdx <= idxRange[1]; numFmtIdx++ {
|
||||
rowNum++
|
||||
styleID, err := f.NewStyle(&Style{NumFmt: numFmtIdx})
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
cell, err := CoordinatesToCellName(1, rowNum)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
if err := f.SetCellValue("Sheet1", cell, 45162); err != nil {
|
||||
return f, err
|
||||
}
|
||||
if err := f.SetCellStyle("Sheet1", cell, cell, styleID); err != nil {
|
||||
return f, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func fillCells(f *File, sheet string, colCount, rowCount int) error {
|
||||
for col := 1; col <= colCount; col++ {
|
||||
for row := 1; row <= rowCount; row++ {
|
||||
cell, _ := CoordinatesToCellName(col, row)
|
||||
if err := f.SetCellStr(sheet, cell, cell); err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BenchmarkOpenFile(b *testing.B) {
|
||||
|
|
54
file.go
54
file.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -18,6 +18,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
@ -26,7 +27,7 @@ import (
|
|||
// For example:
|
||||
//
|
||||
// f := NewFile()
|
||||
func NewFile() *File {
|
||||
func NewFile(opts ...Options) *File {
|
||||
f := newFile()
|
||||
f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
|
||||
f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
|
||||
|
@ -49,6 +50,7 @@ func NewFile() *File {
|
|||
ws, _ := f.workSheetReader("Sheet1")
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
|
||||
f.Theme, _ = f.themeReader()
|
||||
f.options = f.getOptions(opts...)
|
||||
return f
|
||||
}
|
||||
|
||||
|
@ -175,6 +177,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
|
|||
f.commentsWriter()
|
||||
f.contentTypesWriter()
|
||||
f.drawingsWriter()
|
||||
f.volatileDepsWriter()
|
||||
f.vmlDrawingWriter()
|
||||
f.workBookWriter()
|
||||
f.workSheetWriter()
|
||||
|
@ -190,43 +193,48 @@ func (f *File) writeToZip(zw *zip.Writer) error {
|
|||
return err
|
||||
}
|
||||
var from io.Reader
|
||||
from, err = stream.rawData.Reader()
|
||||
if err != nil {
|
||||
if from, err = stream.rawData.Reader(); err != nil {
|
||||
_ = stream.rawData.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(fi, from)
|
||||
if err != nil {
|
||||
if _, err = io.Copy(fi, from); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var err error
|
||||
var (
|
||||
err error
|
||||
files, tempFiles []string
|
||||
)
|
||||
f.Pkg.Range(func(path, content interface{}) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, ok := f.streams[path.(string)]; ok {
|
||||
return true
|
||||
}
|
||||
var fi io.Writer
|
||||
fi, err = zw.Create(path.(string))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = fi.Write(content.([]byte))
|
||||
files = append(files, path.(string))
|
||||
return true
|
||||
})
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(files)))
|
||||
for _, path := range files {
|
||||
var fi io.Writer
|
||||
if fi, err = zw.Create(path); err != nil {
|
||||
break
|
||||
}
|
||||
content, _ := f.Pkg.Load(path)
|
||||
_, err = fi.Write(content.([]byte))
|
||||
}
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
_, err = fi.Write(f.readBytes(path.(string)))
|
||||
tempFiles = append(tempFiles, path.(string))
|
||||
return true
|
||||
})
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(tempFiles)))
|
||||
for _, path := range tempFiles {
|
||||
var fi io.Writer
|
||||
if fi, err = zw.Create(path); err != nil {
|
||||
break
|
||||
}
|
||||
_, err = fi.Write(f.readBytes(path))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
25
go.mod
25
go.mod
|
@ -1,17 +1,22 @@
|
|||
module github.com/xuri/excelize/v2
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/richardlehane/mscfb v1.0.4
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470
|
||||
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/image v0.5.0
|
||||
golang.org/x/net v0.9.0
|
||||
golang.org/x/text v0.9.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tiendc/go-deepcopy v1.1.0
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/text v0.20.0
|
||||
)
|
||||
|
||||
require github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
73
go.sum
73
go.sum
|
@ -1,66 +1,29 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
|
||||
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
|
||||
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
|
||||
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tiendc/go-deepcopy v1.1.0 h1:rBHhm5vg7WYnGLwktbQouodWjBXDoStOL4S7v/K8S4A=
|
||||
github.com/tiendc/go-deepcopy v1.1.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
20
hsl.go
20
hsl.go
|
@ -60,26 +60,26 @@ func hslModel(c color.Color) color.Color {
|
|||
return HSL{h, s, l}
|
||||
}
|
||||
|
||||
// RGBToHSL converts an RGB triple to a HSL triple.
|
||||
// RGBToHSL converts an RGB triple to an HSL triple.
|
||||
func RGBToHSL(r, g, b uint8) (h, s, l float64) {
|
||||
fR := float64(r) / 255
|
||||
fG := float64(g) / 255
|
||||
fB := float64(b) / 255
|
||||
max := math.Max(math.Max(fR, fG), fB)
|
||||
min := math.Min(math.Min(fR, fG), fB)
|
||||
l = (max + min) / 2
|
||||
if max == min {
|
||||
maxVal := math.Max(math.Max(fR, fG), fB)
|
||||
minVal := math.Min(math.Min(fR, fG), fB)
|
||||
l = (maxVal + minVal) / 2
|
||||
if maxVal == minVal {
|
||||
// Achromatic.
|
||||
h, s = 0, 0
|
||||
} else {
|
||||
// Chromatic.
|
||||
d := max - min
|
||||
d := maxVal - minVal
|
||||
if l > 0.5 {
|
||||
s = d / (2.0 - max - min)
|
||||
s = d / (2.0 - maxVal - minVal)
|
||||
} else {
|
||||
s = d / (max + min)
|
||||
s = d / (maxVal + minVal)
|
||||
}
|
||||
switch max {
|
||||
switch maxVal {
|
||||
case fR:
|
||||
h = (fG - fB) / d
|
||||
if fG < fB {
|
||||
|
@ -95,7 +95,7 @@ func RGBToHSL(r, g, b uint8) (h, s, l float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// HSLToRGB converts an HSL triple to a RGB triple.
|
||||
// HSLToRGB converts an HSL triple to an RGB triple.
|
||||
func HSLToRGB(h, s, l float64) (r, g, b uint8) {
|
||||
var fR, fG, fB float64
|
||||
if s == 0 {
|
||||
|
|
163
lib.go
163
lib.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -18,6 +18,7 @@ import (
|
|||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"regexp"
|
||||
|
@ -48,16 +49,22 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
|
|||
fileName = partName
|
||||
}
|
||||
if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit {
|
||||
if tempFile, err := f.unzipToTemp(v); err == nil {
|
||||
tempFile, err := f.unzipToTemp(v)
|
||||
if tempFile != "" {
|
||||
f.tempFiles.Store(fileName, tempFile)
|
||||
}
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(fileName, "xl/worksheets/sheet") {
|
||||
if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") {
|
||||
worksheets++
|
||||
if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() {
|
||||
if tempFile, err := f.unzipToTemp(v); err == nil {
|
||||
tempFile, err := f.unzipToTemp(v)
|
||||
if tempFile != "" {
|
||||
f.tempFiles.Store(fileName, tempFile)
|
||||
}
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -225,12 +232,18 @@ func ColumnNumberToName(num int) (string, error) {
|
|||
if num < MinColumns || num > MaxColumns {
|
||||
return "", ErrColumnNumber
|
||||
}
|
||||
var col string
|
||||
estimatedLength := 0
|
||||
for n := num; n > 0; n = (n - 1) / 26 {
|
||||
estimatedLength++
|
||||
}
|
||||
|
||||
result := make([]byte, estimatedLength)
|
||||
for num > 0 {
|
||||
col = string(rune((num-1)%26+65)) + col
|
||||
estimatedLength--
|
||||
result[estimatedLength] = byte((num-1)%26 + 'A')
|
||||
num = (num - 1) / 26
|
||||
}
|
||||
return col, nil
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates
|
||||
|
@ -261,7 +274,10 @@ func CellNameToCoordinates(cell string) (int, int, error) {
|
|||
// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil
|
||||
func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
|
||||
if col < 1 || row < 1 {
|
||||
return "", fmt.Errorf("invalid cell reference [%d, %d]", col, row)
|
||||
return "", newCoordinatesToCellNameError(col, row)
|
||||
}
|
||||
if row > TotalRows {
|
||||
return "", ErrMaxRows
|
||||
}
|
||||
sign := ""
|
||||
for _, a := range abs {
|
||||
|
@ -313,7 +329,7 @@ func sortCoordinates(coordinates []int) error {
|
|||
|
||||
// coordinatesToRangeRef provides a function to convert a pair of coordinates
|
||||
// to range reference.
|
||||
func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) {
|
||||
func coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) {
|
||||
if len(coordinates) != 4 {
|
||||
return "", ErrCoordinates
|
||||
}
|
||||
|
@ -329,7 +345,7 @@ func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, er
|
|||
}
|
||||
|
||||
// getDefinedNameRefTo convert defined name to reference range.
|
||||
func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) {
|
||||
func (f *File) getDefinedNameRefTo(definedNameName, currentSheet string) (refTo string) {
|
||||
var workbookRefTo, worksheetRefTo string
|
||||
for _, definedName := range f.GetDefinedName() {
|
||||
if definedName.Name == definedNameName {
|
||||
|
@ -350,7 +366,7 @@ func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string)
|
|||
}
|
||||
|
||||
// flatSqref convert reference sequence to cell reference list.
|
||||
func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) {
|
||||
func flatSqref(sqref string) (cells map[int][][]int, err error) {
|
||||
var coordinates []int
|
||||
cells = make(map[int][][]int)
|
||||
for _, ref := range strings.Fields(sqref) {
|
||||
|
@ -421,8 +437,8 @@ func boolPtr(b bool) *bool { return &b }
|
|||
// intPtr returns a pointer to an int with the given value.
|
||||
func intPtr(i int) *int { return &i }
|
||||
|
||||
// uintPtr returns a pointer to an int with the given value.
|
||||
func uintPtr(i uint) *uint { return &i }
|
||||
// uintPtr returns a pointer to an unsigned integer with the given value.
|
||||
func uintPtr(u uint) *uint { return &u }
|
||||
|
||||
// float64Ptr returns a pointer to a float64 with the given value.
|
||||
func float64Ptr(f float64) *float64 { return &f }
|
||||
|
@ -430,6 +446,30 @@ 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 }
|
||||
|
||||
// Value extracts string data type text from a attribute value.
|
||||
func (avb *attrValString) Value() string {
|
||||
if avb != nil && avb.Val != nil {
|
||||
return *avb.Val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Value extracts boolean data type value from a attribute value.
|
||||
func (avb *attrValBool) Value() bool {
|
||||
if avb != nil && avb.Val != nil {
|
||||
return *avb.Val
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Value extracts float64 data type numeric from a attribute value.
|
||||
func (attr *attrValFloat) Value() float64 {
|
||||
if attr != nil && attr.Val != nil {
|
||||
return *attr.Val
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// MarshalXML convert the boolean data type to literal values 0 or 1 on
|
||||
// serialization.
|
||||
func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
|
@ -493,6 +533,34 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML encodes ext element with specified namespace attributes on
|
||||
// serialization.
|
||||
func (ext xlsxExt) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
start.Attr = ext.xmlns
|
||||
return e.EncodeElement(decodeExt{URI: ext.URI, Content: ext.Content}, start)
|
||||
}
|
||||
|
||||
// UnmarshalXML extracts ext element attributes namespace by giving XML decoder
|
||||
// on deserialization.
|
||||
func (ext *xlsxExt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "uri" {
|
||||
continue
|
||||
}
|
||||
if attr.Name.Space == "xmlns" {
|
||||
attr.Name.Space = ""
|
||||
attr.Name.Local = "xmlns:" + attr.Name.Local
|
||||
}
|
||||
ext.xmlns = append(ext.xmlns, attr)
|
||||
}
|
||||
e := &decodeExt{}
|
||||
if err := d.DecodeElement(&e, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
ext.URI, ext.Content = e.URI, e.Content
|
||||
return nil
|
||||
}
|
||||
|
||||
// namespaceStrictToTransitional provides a method to convert Strict and
|
||||
// Transitional namespaces.
|
||||
func namespaceStrictToTransitional(content []byte) []byte {
|
||||
|
@ -584,6 +652,16 @@ func getRootElement(d *xml.Decoder) []xml.Attr {
|
|||
case xml.StartElement:
|
||||
tokenIdx++
|
||||
if tokenIdx == 1 {
|
||||
var ns bool
|
||||
for i := 0; i < len(startElement.Attr); i++ {
|
||||
if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value &&
|
||||
startElement.Attr[i].Name == NameSpaceSpreadSheet.Name {
|
||||
ns = true
|
||||
}
|
||||
}
|
||||
if !ns {
|
||||
startElement.Attr = append(startElement.Attr, NameSpaceSpreadSheet)
|
||||
}
|
||||
return startElement.Attr
|
||||
}
|
||||
}
|
||||
|
@ -623,8 +701,8 @@ func getXMLNamespace(space string, attr []xml.Attr) string {
|
|||
func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte {
|
||||
sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
|
||||
targetXmlns := []byte(templateNamespaceIDMap)
|
||||
if attr, ok := f.xmlAttr[path]; ok {
|
||||
targetXmlns = []byte(genXMLNamespace(attr))
|
||||
if attrs, ok := f.xmlAttr.Load(path); ok {
|
||||
targetXmlns = []byte(genXMLNamespace(attrs.([]xml.Attr)))
|
||||
}
|
||||
return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1)
|
||||
}
|
||||
|
@ -635,29 +713,36 @@ 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 {
|
||||
if attrs, ok := f.xmlAttr.Load(path); ok {
|
||||
for i, attr := range attrs.([]xml.Attr) {
|
||||
if attr.Name.Local == ns.Name.Local && attr.Name.Space == ns.Name.Space {
|
||||
exist = true
|
||||
}
|
||||
if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" {
|
||||
if attr.Name.Local == "Ignorable" && getXMLNamespace(attr.Name.Space, attrs.([]xml.Attr)) == "mc" {
|
||||
ignore = i
|
||||
}
|
||||
if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" {
|
||||
if attr.Name.Local == "mc" && attr.Name.Space == "xmlns" {
|
||||
mc = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !exist {
|
||||
f.xmlAttr[path] = append(f.xmlAttr[path], ns)
|
||||
attrs, _ := f.xmlAttr.Load(path)
|
||||
if attrs == nil {
|
||||
attrs = []xml.Attr{}
|
||||
}
|
||||
attrs = append(attrs.([]xml.Attr), ns)
|
||||
f.xmlAttr.Store(path, attrs)
|
||||
if !mc {
|
||||
f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility)
|
||||
attrs = append(attrs.([]xml.Attr), SourceRelationshipCompatibility)
|
||||
f.xmlAttr.Store(path, attrs)
|
||||
}
|
||||
if ignore == -1 {
|
||||
f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{
|
||||
attrs = append(attrs.([]xml.Attr), xml.Attr{
|
||||
Name: xml.Name{Local: "Ignorable", Space: "mc"},
|
||||
Value: ns.Name.Local,
|
||||
})
|
||||
f.xmlAttr.Store(path, attrs)
|
||||
return
|
||||
}
|
||||
f.setIgnorableNameSpace(path, ignore, ns)
|
||||
|
@ -668,8 +753,10 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) {
|
|||
// 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, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 {
|
||||
f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local))
|
||||
xmlAttrs, _ := f.xmlAttr.Load(path)
|
||||
if inStrSlice(strings.Fields(xmlAttrs.([]xml.Attr)[index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 {
|
||||
xmlAttrs.([]xml.Attr)[index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", xmlAttrs.([]xml.Attr)[index].Value, ns.Name.Local))
|
||||
f.xmlAttr.Store(path, xmlAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,6 +854,30 @@ func bstrMarshal(s string) (result string) {
|
|||
return result
|
||||
}
|
||||
|
||||
// newRat converts decimals to rational fractions with the required precision.
|
||||
func newRat(n float64, iterations int64, prec float64) *big.Rat {
|
||||
x := int64(math.Floor(n))
|
||||
y := n - float64(x)
|
||||
rat := continuedFraction(y, 1, iterations, prec)
|
||||
return rat.Add(rat, new(big.Rat).SetInt64(x))
|
||||
}
|
||||
|
||||
// continuedFraction returns rational from decimal with the continued fraction
|
||||
// algorithm.
|
||||
func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat {
|
||||
if i >= limit || n <= prec {
|
||||
return big.NewRat(0, 1)
|
||||
}
|
||||
inverted := 1 / n
|
||||
y := int64(math.Floor(inverted))
|
||||
x := inverted - float64(y)
|
||||
ratY := new(big.Rat).SetInt64(y)
|
||||
ratNext := continuedFraction(x, i+1, limit, prec)
|
||||
res := ratY.Add(ratY, ratNext)
|
||||
res = res.Inv(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// Stack defined an abstract data type that serves as a collection of elements.
|
||||
type Stack struct {
|
||||
list *list.List
|
||||
|
|
42
lib_test.go
42
lib_test.go
|
@ -218,14 +218,13 @@ func TestCoordinatesToCellName_Error(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCoordinatesToRangeRef(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.coordinatesToRangeRef([]int{})
|
||||
_, err := coordinatesToRangeRef([]int{})
|
||||
assert.EqualError(t, err, ErrCoordinates.Error())
|
||||
_, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1})
|
||||
assert.EqualError(t, err, "invalid cell reference [1, -1]")
|
||||
_, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1})
|
||||
assert.EqualError(t, err, "invalid cell reference [1, -1]")
|
||||
ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1})
|
||||
_, err = coordinatesToRangeRef([]int{1, -1, 1, 1})
|
||||
assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
|
||||
_, err = coordinatesToRangeRef([]int{1, 1, 1, -1})
|
||||
assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
|
||||
ref, err := coordinatesToRangeRef([]int{1, 1, 1, 1})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, ref, "A1:A1")
|
||||
}
|
||||
|
@ -238,6 +237,12 @@ func TestInStrSlice(t *testing.T) {
|
|||
assert.EqualValues(t, -1, inStrSlice([]string{}, "", true))
|
||||
}
|
||||
|
||||
func TestAttrValue(t *testing.T) {
|
||||
assert.Empty(t, (&attrValString{}).Value())
|
||||
assert.False(t, (&attrValBool{}).Value())
|
||||
assert.Zero(t, (&attrValFloat{}).Value())
|
||||
}
|
||||
|
||||
func TestBoolValMarshal(t *testing.T) {
|
||||
bold := true
|
||||
node := &xlsxFont{B: &attrValBool{Val: &bold}}
|
||||
|
@ -268,6 +273,15 @@ func TestBoolValUnmarshalXML(t *testing.T) {
|
|||
assert.EqualError(t, attr.UnmarshalXML(xml.NewDecoder(strings.NewReader("")), xml.StartElement{}), io.EOF.Error())
|
||||
}
|
||||
|
||||
func TestExtUnmarshalXML(t *testing.T) {
|
||||
f, extLst := NewFile(), decodeExtLst{}
|
||||
expected := fmt.Sprintf(`<extLst><ext uri="%s" xmlns:x14="%s"/></extLst>`,
|
||||
ExtURISlicerCachesX14, NameSpaceSpreadSheetX14.Value)
|
||||
assert.NoError(t, f.xmlNewDecoder(strings.NewReader(expected)).Decode(&extLst))
|
||||
assert.Len(t, extLst.Ext, 1)
|
||||
assert.Equal(t, extLst.Ext[0].URI, ExtURISlicerCachesX14)
|
||||
}
|
||||
|
||||
func TestBytesReplace(t *testing.T) {
|
||||
s := []byte{0x01}
|
||||
assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0))
|
||||
|
@ -275,13 +289,19 @@ func TestBytesReplace(t *testing.T) {
|
|||
|
||||
func TestGetRootElement(t *testing.T) {
|
||||
assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0)
|
||||
// Test get workbook root element which all workbook XML namespace has prefix
|
||||
f := NewFile()
|
||||
d := f.xmlNewDecoder(bytes.NewReader([]byte(`<x:workbook xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"></x:workbook>`)))
|
||||
assert.Len(t, getRootElement(d), 3)
|
||||
}
|
||||
|
||||
func TestSetIgnorableNameSpace(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.xmlAttr["xml_path"] = []xml.Attr{{}}
|
||||
f.xmlAttr.Store("xml_path", []xml.Attr{{}})
|
||||
f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}})
|
||||
assert.EqualValues(t, "c14", f.xmlAttr["xml_path"][0].Value)
|
||||
attrs, ok := f.xmlAttr.Load("xml_path")
|
||||
assert.EqualValues(t, "c14", attrs.([]xml.Attr)[0].Value)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
|
@ -342,11 +362,9 @@ func TestReadBytes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUnzipToTemp(t *testing.T) {
|
||||
for _, v := range []string{"go1.19", "go1.20"} {
|
||||
if strings.HasPrefix(runtime.Version(), v) {
|
||||
if ver := runtime.Version(); strings.HasPrefix(ver, "go1.19") || strings.HasPrefix(ver, "go1.2") {
|
||||
t.Skip()
|
||||
}
|
||||
}
|
||||
os.Setenv("TMPDIR", "test")
|
||||
defer os.Unsetenv("TMPDIR")
|
||||
assert.NoError(t, os.Chmod(os.TempDir(), 0o444))
|
||||
|
|
53
merge.go
53
merge.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -49,24 +49,35 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) {
|
|||
// | |
|
||||
// |A8(x3,y4) C8(x4,y4)|
|
||||
// +------------------------+
|
||||
func (f *File) MergeCell(sheet, hCell, vCell string) error {
|
||||
rect, err := rangeRefToCoordinates(hCell + ":" + vCell)
|
||||
func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error {
|
||||
rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Correct the range reference, such correct C1:B3 to B1:C3.
|
||||
_ = sortCoordinates(rect)
|
||||
|
||||
hCell, _ = CoordinatesToCellName(rect[0], rect[1])
|
||||
vCell, _ = CoordinatesToCellName(rect[2], rect[3])
|
||||
topLeftCell, _ = CoordinatesToCellName(rect[0], rect[1])
|
||||
bottomRightCell, _ = CoordinatesToCellName(rect[2], rect[3])
|
||||
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
ref := hCell + ":" + vCell
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
for col := rect[0]; col <= rect[2]; col++ {
|
||||
for row := rect[1]; row <= rect[3]; row++ {
|
||||
if col == rect[0] && row == rect[1] {
|
||||
continue
|
||||
}
|
||||
ws.prepareSheetXML(col, row)
|
||||
c := &ws.SheetData.Row[row-1].C[col-1]
|
||||
c.setCellDefault("")
|
||||
_ = f.removeFormula(c, ws, sheet)
|
||||
}
|
||||
}
|
||||
ref := topLeftCell + ":" + bottomRightCell
|
||||
if ws.MergeCells != nil {
|
||||
ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect})
|
||||
} else {
|
||||
|
@ -82,14 +93,14 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
|
|||
// err := f.UnmergeCell("Sheet1", "D3", "E9")
|
||||
//
|
||||
// Attention: overlapped range will also be unmerged.
|
||||
func (f *File) UnmergeCell(sheet, hCell, vCell string) error {
|
||||
func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
rect1, err := rangeRefToCoordinates(hCell + ":" + vCell)
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
rect1, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -128,8 +139,8 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetMergeCells provides a function to get all merged cells from a worksheet
|
||||
// currently.
|
||||
// GetMergeCells provides a function to get all merged cells from a specific
|
||||
// worksheet.
|
||||
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
|
||||
var mergeCells []MergeCell
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
|
@ -265,9 +276,9 @@ func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell {
|
|||
if rect1[3] < rect2[3] {
|
||||
rect1[3], rect2[3] = rect2[3], rect1[3]
|
||||
}
|
||||
hCell, _ := CoordinatesToCellName(rect1[0], rect1[1])
|
||||
vCell, _ := CoordinatesToCellName(rect1[2], rect1[3])
|
||||
return &xlsxMergeCell{rect: rect1, Ref: hCell + ":" + vCell}
|
||||
topLeftCell, _ := CoordinatesToCellName(rect1[0], rect1[1])
|
||||
bottomRightCell, _ := CoordinatesToCellName(rect1[2], rect1[3])
|
||||
return &xlsxMergeCell{rect: rect1, Ref: topLeftCell + ":" + bottomRightCell}
|
||||
}
|
||||
|
||||
// MergeCell define a merged cell data.
|
||||
|
@ -289,5 +300,9 @@ func (m *MergeCell) GetStartAxis() string {
|
|||
// GetEndAxis returns the bottom right cell reference of merged range, for
|
||||
// example: "D4".
|
||||
func (m *MergeCell) GetEndAxis() string {
|
||||
return strings.Split((*m)[0], ":")[1]
|
||||
coordinates := strings.Split((*m)[0], ":")
|
||||
if len(coordinates) == 2 {
|
||||
return coordinates[1]
|
||||
}
|
||||
return coordinates[0]
|
||||
}
|
||||
|
|
|
@ -13,14 +13,18 @@ func TestMergeCell(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12"))
|
||||
for _, cells := range [][]string{
|
||||
{"D9", "D9"},
|
||||
{"D9", "E9"},
|
||||
{"H14", "G13"},
|
||||
{"C9", "D8"},
|
||||
{"F11", "G13"},
|
||||
{"H7", "B15"},
|
||||
{"D11", "F13"},
|
||||
{"G10", "K12"},
|
||||
} {
|
||||
assert.NoError(t, f.MergeCell("Sheet1", cells[0], cells[1]))
|
||||
}
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell"))
|
||||
assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "I11", 0.5))
|
||||
|
@ -39,32 +43,29 @@ func TestMergeCell(t *testing.T) {
|
|||
|
||||
_, err = f.NewSheet("Sheet3")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12"))
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12"))
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10"))
|
||||
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13"))
|
||||
assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11"))
|
||||
for _, cells := range [][]string{
|
||||
{"D11", "F13"},
|
||||
{"G10", "K12"},
|
||||
{"B1", "D5"}, // B1:D5
|
||||
{"E1", "F5"}, // E1:F5
|
||||
{"H2", "I5"},
|
||||
{"I4", "J6"}, // H2:J6
|
||||
{"M2", "N5"},
|
||||
{"L4", "M6"}, // L2:N6
|
||||
{"P4", "Q7"},
|
||||
{"O2", "P5"}, // O2:Q7
|
||||
{"A9", "B12"},
|
||||
{"B7", "C9"}, // A7:C12
|
||||
{"E9", "F10"},
|
||||
{"D8", "G12"},
|
||||
{"I8", "I12"},
|
||||
{"I10", "K10"},
|
||||
{"M8", "Q13"},
|
||||
{"N10", "O11"},
|
||||
} {
|
||||
assert.NoError(t, f.MergeCell("Sheet3", cells[0], cells[1]))
|
||||
}
|
||||
|
||||
// Test merge cells on not exists worksheet
|
||||
assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist")
|
||||
|
@ -79,6 +80,13 @@ func TestMergeCell(t *testing.T) {
|
|||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
|
||||
// Test getting merged cells with the same start and end axis
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
|
||||
mergedCells, err := f.GetMergeCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A1", mergedCells[0].GetStartAxis())
|
||||
assert.Equal(t, "A1", mergedCells[0].GetEndAxis())
|
||||
assert.Empty(t, mergedCells[0].GetCellValue())
|
||||
}
|
||||
|
||||
func TestMergeCellOverlap(t *testing.T) {
|
||||
|
@ -207,7 +215,7 @@ func TestFlatMergedCells(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeCellsParser(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.mergeCellsParser(&xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}, "A1")
|
||||
ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}
|
||||
_, err := ws.mergeCellsParser("A1")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
3210
numfmt_test.go
3210
numfmt_test.go
File diff suppressed because it is too large
Load Diff
753
picture.go
753
picture.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -23,6 +23,18 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// PictureInsertType defines the type of the picture has been inserted into the
|
||||
// worksheet.
|
||||
type PictureInsertType int
|
||||
|
||||
// Insert picture types.
|
||||
const (
|
||||
PictureInsertTypePlaceOverCells PictureInsertType = iota
|
||||
PictureInsertTypePlaceInCell
|
||||
PictureInsertTypeIMAGE
|
||||
PictureInsertTypeDISPIMG
|
||||
)
|
||||
|
||||
// parseGraphicOptions provides a function to parse the format settings of
|
||||
// the picture with default value.
|
||||
func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
|
||||
|
@ -30,8 +42,8 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
|
|||
return &GraphicOptions{
|
||||
PrintObject: boolPtr(true),
|
||||
Locked: boolPtr(true),
|
||||
ScaleX: defaultPictureScale,
|
||||
ScaleY: defaultPictureScale,
|
||||
ScaleX: defaultDrawingScale,
|
||||
ScaleY: defaultDrawingScale,
|
||||
}
|
||||
}
|
||||
if opts.PrintObject == nil {
|
||||
|
@ -41,10 +53,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
|
|||
opts.Locked = boolPtr(true)
|
||||
}
|
||||
if opts.ScaleX == 0 {
|
||||
opts.ScaleX = defaultPictureScale
|
||||
opts.ScaleX = defaultDrawingScale
|
||||
}
|
||||
if opts.ScaleY == 0 {
|
||||
opts.ScaleY = defaultPictureScale
|
||||
opts.ScaleY = defaultDrawingScale
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
@ -52,7 +64,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
|
|||
// AddPicture provides the method to add picture in a sheet by given picture
|
||||
// format set (such as offset, scale, aspect ratio setting and print settings)
|
||||
// and file path, supported image types: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG,
|
||||
// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency safe. For example:
|
||||
// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency-safe. Note that
|
||||
// this function only supports adding pictures placed over the cells currently,
|
||||
// and doesn't support adding pictures placed in cells or creating the Kingsoft
|
||||
// WPS Office embedded image cells. For example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
|
@ -110,41 +125,50 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
|
|||
// }
|
||||
// }
|
||||
//
|
||||
// The optional parameter "AutoFit" specifies if you make image size auto-fits the
|
||||
// cell, the default value of that is 'false'.
|
||||
// The optional parameter "AltText" is used to add alternative text to a graph
|
||||
// object.
|
||||
//
|
||||
// The optional parameter "Hyperlink" specifies the hyperlink of the image.
|
||||
// The optional parameter "PrintObject" indicates whether the graph object is
|
||||
// printed when the worksheet is printed, the default value of that is 'true'.
|
||||
//
|
||||
// The optional parameter "Locked" indicates whether lock the graph object.
|
||||
// Locking an object has no effect unless the sheet is protected.
|
||||
//
|
||||
// The optional parameter "LockAspectRatio" indicates whether lock aspect ratio
|
||||
// for the graph object, the default value of that is 'false'.
|
||||
//
|
||||
// The optional parameter "AutoFit" specifies if you make graph object size
|
||||
// auto-fits the cell, the default value of that is 'false'.
|
||||
//
|
||||
// The optional parameter "AutoFitIgnoreAspect" specifies if fill the cell with
|
||||
// the image and ignore its aspect ratio, the default value of that is 'false'.
|
||||
// This option only works when the "AutoFit" is enabled.
|
||||
//
|
||||
// The optional parameter "OffsetX" specifies the horizontal offset of the graph
|
||||
// object with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "OffsetY" specifies the vertical offset of the graph
|
||||
// object with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "ScaleX" specifies the horizontal scale of graph
|
||||
// object, the default value of that is 1.0 which presents 100%.
|
||||
//
|
||||
// The optional parameter "ScaleY" specifies the vertical scale of graph object,
|
||||
// the default value of that is 1.0 which presents 100%.
|
||||
//
|
||||
// The optional parameter "Hyperlink" specifies the hyperlink of the graph
|
||||
// object.
|
||||
//
|
||||
// The optional parameter "HyperlinkType" defines two types of
|
||||
// hyperlink "External" for website or "Location" for moving to one of the
|
||||
// cells in this workbook. When the "HyperlinkType" is "Location",
|
||||
// coordinates need to start with "#".
|
||||
//
|
||||
// The optional parameter "Positioning" defines two types of the position of an
|
||||
// image in an Excel spreadsheet, "oneCell" (Move but don't size with
|
||||
// cells) or "absolute" (Don't move or size with cells). If you don't set this
|
||||
// parameter, the default positioning is move and size with cells.
|
||||
//
|
||||
// The optional parameter "PrintObject" indicates whether the image is printed
|
||||
// when the worksheet is printed, the default value of that is 'true'.
|
||||
//
|
||||
// The optional parameter "LockAspectRatio" indicates whether lock aspect
|
||||
// ratio for the image, the default value of that is 'false'.
|
||||
//
|
||||
// The optional parameter "Locked" indicates whether lock the image. Locking
|
||||
// an object has no effect unless the sheet is protected.
|
||||
//
|
||||
// The optional parameter "OffsetX" specifies the horizontal offset of the
|
||||
// image with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "ScaleX" specifies the horizontal scale of images,
|
||||
// the default value of that is 1.0 which presents 100%.
|
||||
//
|
||||
// The optional parameter "OffsetY" specifies the vertical offset of the
|
||||
// image with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "ScaleY" specifies the vertical scale of images,
|
||||
// the default value of that is 1.0 which presents 100%.
|
||||
// The optional parameter "Positioning" defines 3 types of the position of a
|
||||
// graph object in a spreadsheet: "oneCell" (Move but don't size with
|
||||
// cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or
|
||||
// size with cells). If you don't set this parameter, the default positioning
|
||||
// is to move and size with cells.
|
||||
func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error {
|
||||
var err error
|
||||
// Check picture exists first.
|
||||
|
@ -162,8 +186,10 @@ func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error
|
|||
// AddPictureFromBytes provides the method to add picture in a sheet by given
|
||||
// picture format set (such as offset, scale, aspect ratio setting and print
|
||||
// settings), file base name, extension name and file bytes, supported image
|
||||
// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. For
|
||||
// example:
|
||||
// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. Note that
|
||||
// this function only supports adding pictures placed over the cells currently,
|
||||
// and doesn't support adding pictures placed in cells or creating the Kingsoft
|
||||
// WPS Office embedded image cells. For example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
|
@ -206,24 +232,41 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
|
|||
if !ok {
|
||||
return ErrImgExt
|
||||
}
|
||||
if pic.InsertType != PictureInsertTypePlaceOverCells {
|
||||
return ErrParameterInvalid
|
||||
}
|
||||
options := parseGraphicOptions(pic.Format)
|
||||
img, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Read sheet data.
|
||||
// Read sheet data
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
// Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
|
||||
drawingID := f.countDrawings() + 1
|
||||
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
|
||||
drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
|
||||
drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels"
|
||||
mediaStr := ".." + strings.TrimPrefix(f.addMedia(pic.File, ext), "xl")
|
||||
drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType)
|
||||
var drawingRID int
|
||||
if rels, _ := f.relsReader(drawingRels); rels != nil {
|
||||
for _, rel := range rels.Relationships {
|
||||
if rel.Type == SourceRelationshipImage && rel.Target == mediaStr {
|
||||
drawingRID, _ = strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if drawingRID == 0 {
|
||||
drawingRID = f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType)
|
||||
}
|
||||
// Add picture with hyperlink.
|
||||
if options.Hyperlink != "" && options.HyperlinkType != "" {
|
||||
if options.HyperlinkType == "External" {
|
||||
|
@ -231,7 +274,7 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
|
|||
}
|
||||
drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType)
|
||||
}
|
||||
ws.Unlock()
|
||||
ws.mu.Unlock()
|
||||
err = f.addDrawingPicture(sheet, drawingXML, cell, ext, drawingRID, drawingHyperlinkRID, img, options)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -243,29 +286,6 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// deleteSheetRelationships provides a function to delete relationships in
|
||||
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
||||
// relationship index.
|
||||
func (f *File) deleteSheetRelationships(sheet, rID string) {
|
||||
name, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
sheetRels, _ := f.relsReader(rels)
|
||||
if sheetRels == nil {
|
||||
sheetRels = &xlsxRelationships{}
|
||||
}
|
||||
sheetRels.Lock()
|
||||
defer sheetRels.Unlock()
|
||||
for k, v := range sheetRels.Relationships {
|
||||
if v.ID == rID {
|
||||
sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
|
||||
}
|
||||
}
|
||||
f.Relationships.Store(rels, sheetRels)
|
||||
}
|
||||
|
||||
// addSheetLegacyDrawing provides a function to add legacy drawing element to
|
||||
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
||||
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
|
||||
|
@ -275,6 +295,16 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
|
|||
}
|
||||
}
|
||||
|
||||
// addSheetLegacyDrawingHF provides a function to add legacy drawing
|
||||
// header/footer element to xl/worksheets/sheet%d.xml by given
|
||||
// worksheet name and relationship index.
|
||||
func (f *File) addSheetLegacyDrawingHF(sheet string, rID int) {
|
||||
ws, _ := f.workSheetReader(sheet)
|
||||
ws.LegacyDrawingHF = &xlsxLegacyDrawingHF{
|
||||
RID: "rId" + strconv.Itoa(rID),
|
||||
}
|
||||
}
|
||||
|
||||
// addSheetDrawing provides a function to add drawing element to
|
||||
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
||||
func (f *File) addSheetDrawing(sheet string, rID int) {
|
||||
|
@ -300,23 +330,20 @@ func (f *File) addSheetPicture(sheet string, rID int) error {
|
|||
// countDrawings provides a function to get drawing files count storage in the
|
||||
// folder xl/drawings.
|
||||
func (f *File) countDrawings() int {
|
||||
var c1, c2 int
|
||||
drawings := map[string]struct{}{}
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/drawings/drawing") {
|
||||
c1++
|
||||
drawings[k.(string)] = struct{}{}
|
||||
}
|
||||
return true
|
||||
})
|
||||
f.Drawings.Range(func(rel, value interface{}) bool {
|
||||
if strings.Contains(rel.(string), "xl/drawings/drawing") {
|
||||
c2++
|
||||
drawings[rel.(string)] = struct{}{}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if c1 < c2 {
|
||||
return c2
|
||||
}
|
||||
return c1
|
||||
return len(drawings)
|
||||
}
|
||||
|
||||
// addDrawingPicture provides a function to add picture by given sheet,
|
||||
|
@ -327,18 +354,18 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 {
|
||||
return ErrParameterInvalid
|
||||
}
|
||||
width, height := img.Width, img.Height
|
||||
if opts.AutoFit {
|
||||
width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts)
|
||||
if err != nil {
|
||||
if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
width = int(float64(width) * opts.ScaleX)
|
||||
height = int(float64(height) * opts.ScaleY)
|
||||
}
|
||||
col--
|
||||
row--
|
||||
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
|
||||
content, cNvPrID, err := f.drawingParser(drawingXML)
|
||||
if err != nil {
|
||||
|
@ -391,8 +418,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
|
|||
FLocksWithSheet: *opts.Locked,
|
||||
FPrintsWithSheet: *opts.PrintObject,
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
|
||||
f.Drawings.Store(drawingXML, content)
|
||||
return err
|
||||
|
@ -435,130 +462,6 @@ func (f *File) addMedia(file []byte, ext string) string {
|
|||
return media
|
||||
}
|
||||
|
||||
// setContentTypePartImageExtensions provides a function to set the content
|
||||
// type for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartImageExtensions() error {
|
||||
imageTypes := map[string]string{
|
||||
"bmp": "image/", "jpeg": "image/", "png": "image/", "gif": "image/",
|
||||
"svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-",
|
||||
"emz": "image/x-", "wmz": "image/x-",
|
||||
}
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
for _, file := range content.Defaults {
|
||||
delete(imageTypes, file.Extension)
|
||||
}
|
||||
for extension, prefix := range imageTypes {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: extension,
|
||||
ContentType: prefix + extension,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// setContentTypePartVMLExtensions provides a function to set the content type
|
||||
// for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartVMLExtensions() error {
|
||||
var vml bool
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
for _, v := range content.Defaults {
|
||||
if v.Extension == "vml" {
|
||||
vml = true
|
||||
}
|
||||
}
|
||||
if !vml {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: "vml",
|
||||
ContentType: ContentTypeVML,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// addContentTypePart provides a function to add content type part
|
||||
// relationships in the file [Content_Types].xml by given index.
|
||||
func (f *File) addContentTypePart(index int, contentType string) error {
|
||||
setContentType := map[string]func() error{
|
||||
"comments": f.setContentTypePartVMLExtensions,
|
||||
"drawings": f.setContentTypePartImageExtensions,
|
||||
}
|
||||
partNames := map[string]string{
|
||||
"chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
|
||||
"chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
|
||||
"comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
|
||||
"drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
|
||||
"table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
|
||||
"pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
|
||||
"pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
|
||||
"sharedStrings": "/xl/sharedStrings.xml",
|
||||
}
|
||||
contentTypes := map[string]string{
|
||||
"chart": ContentTypeDrawingML,
|
||||
"chartsheet": ContentTypeSpreadSheetMLChartsheet,
|
||||
"comments": ContentTypeSpreadSheetMLComments,
|
||||
"drawings": ContentTypeDrawing,
|
||||
"table": ContentTypeSpreadSheetMLTable,
|
||||
"pivotTable": ContentTypeSpreadSheetMLPivotTable,
|
||||
"pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
|
||||
"sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
|
||||
}
|
||||
s, ok := setContentType[contentType]
|
||||
if ok {
|
||||
if err := s(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
for _, v := range content.Overrides {
|
||||
if v.PartName == partNames[contentType] {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content.Overrides = append(content.Overrides, xlsxOverride{
|
||||
PartName: partNames[contentType],
|
||||
ContentType: contentTypes[contentType],
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// getSheetRelationshipsTargetByID provides a function to get Target attribute
|
||||
// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
||||
// relationship index.
|
||||
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
|
||||
name, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
sheetRels, _ := f.relsReader(rels)
|
||||
if sheetRels == nil {
|
||||
sheetRels = &xlsxRelationships{}
|
||||
}
|
||||
sheetRels.Lock()
|
||||
defer sheetRels.Unlock()
|
||||
for _, v := range sheetRels.Relationships {
|
||||
if v.ID == rID {
|
||||
return v.Target
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPictures provides a function to get picture meta info and raw content
|
||||
// embed in spreadsheet by given worksheet and cell name. This function
|
||||
// returns the image contents as []byte data types. This function is
|
||||
|
@ -591,24 +494,62 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) {
|
|||
}
|
||||
col--
|
||||
row--
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
f.mu.Unlock()
|
||||
if ws.Drawing == nil {
|
||||
return f.getCellImages(sheet, cell)
|
||||
}
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
|
||||
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
|
||||
drawingRelationships := strings.ReplaceAll(
|
||||
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
|
||||
imgs, err := f.getCellImages(sheet, cell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ws.Drawing == nil {
|
||||
pics, err := f.getPicture(row, col, drawingXML, drawingRelationships)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
|
||||
drawingXML := strings.ReplaceAll(target, "..", "xl")
|
||||
drawingRelationships := strings.ReplaceAll(
|
||||
strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
|
||||
return append(imgs, pics...), err
|
||||
}
|
||||
|
||||
return f.getPicture(row, col, drawingXML, drawingRelationships)
|
||||
// GetPictureCells returns all picture cell references in a worksheet by a
|
||||
// specific worksheet name.
|
||||
func (f *File) GetPictureCells(sheet string) ([]string, error) {
|
||||
f.mu.Lock()
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
f.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
f.mu.Unlock()
|
||||
if ws.Drawing == nil {
|
||||
return f.getImageCells(sheet)
|
||||
}
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
|
||||
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
|
||||
drawingRelationships := strings.ReplaceAll(
|
||||
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
|
||||
|
||||
embeddedImageCells, err := f.getImageCells(sheet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageCells, err := f.getPictureCells(drawingXML, drawingRelationships)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(embeddedImageCells, imageCells...), err
|
||||
}
|
||||
|
||||
// DeletePicture provides a function to delete all pictures in a cell by given
|
||||
// worksheet name and cell reference. Note that the image file won't be deleted
|
||||
// from the document currently.
|
||||
// worksheet name and cell reference.
|
||||
func (f *File) DeletePicture(sheet, cell string) error {
|
||||
col, row, err := CellNameToCoordinates(cell)
|
||||
if err != nil {
|
||||
|
@ -624,94 +565,136 @@ func (f *File) DeletePicture(sheet, cell string) error {
|
|||
return err
|
||||
}
|
||||
drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl")
|
||||
return f.deleteDrawing(col, row, drawingXML, "Pic")
|
||||
drawingRels := "xl/drawings/_rels/" + filepath.Base(drawingXML) + ".rels"
|
||||
rID, err := f.deleteDrawing(col, row, drawingXML, "Pic")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rels := f.getDrawingRelationships(drawingRels, rID)
|
||||
if rels == nil {
|
||||
return err
|
||||
}
|
||||
var used bool
|
||||
checkPicRef := func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/drawings/_rels/drawing") {
|
||||
r, err := f.relsReader(k.(string))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
for _, rel := range r.Relationships {
|
||||
if rel.ID != rels.ID && rel.Type == SourceRelationshipImage &&
|
||||
filepath.Base(rel.Target) == filepath.Base(rels.Target) {
|
||||
used = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
f.Relationships.Range(checkPicRef)
|
||||
f.Pkg.Range(checkPicRef)
|
||||
if !used {
|
||||
f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1))
|
||||
}
|
||||
f.deleteDrawingRels(drawingRels, rID)
|
||||
return err
|
||||
}
|
||||
|
||||
// getPicture provides a function to get picture base name and raw content
|
||||
// embed in spreadsheet by given coordinates and drawing relationships.
|
||||
func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) {
|
||||
var (
|
||||
wsDr *xlsxWsDr
|
||||
ok bool
|
||||
deWsDr *decodeWsDr
|
||||
drawRel *xlsxRelationship
|
||||
deTwoCellAnchor *decodeTwoCellAnchor
|
||||
)
|
||||
|
||||
var wsDr *xlsxWsDr
|
||||
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
|
||||
return
|
||||
}
|
||||
if pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr); len(pics) > 0 {
|
||||
return
|
||||
}
|
||||
deWsDr = new(decodeWsDr)
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))).
|
||||
Decode(deWsDr); err != nil && err != io.EOF {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
for _, anchor := range deWsDr.TwoCellAnchor {
|
||||
deTwoCellAnchor = new(decodeTwoCellAnchor)
|
||||
if err = f.xmlNewDecoder(strings.NewReader("<decodeTwoCellAnchor>" + anchor.Content + "</decodeTwoCellAnchor>")).
|
||||
Decode(deTwoCellAnchor); err != nil && err != io.EOF {
|
||||
return
|
||||
}
|
||||
if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil {
|
||||
if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row {
|
||||
drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed)
|
||||
if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
|
||||
pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}}
|
||||
if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil {
|
||||
wsDr.mu.Lock()
|
||||
defer wsDr.mu.Unlock()
|
||||
cond := func(from *xlsxFrom) bool { return from.Col == col && from.Row == row }
|
||||
cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row }
|
||||
cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
|
||||
pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
|
||||
if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil {
|
||||
pic.File = buffer.([]byte)
|
||||
pic.Format.AltText = deTwoCellAnchor.Pic.NvPicPr.CNvPr.Descr
|
||||
pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
|
||||
pics = append(pics, pic)
|
||||
}
|
||||
return
|
||||
}
|
||||
cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) {
|
||||
var target string
|
||||
if strings.HasPrefix(r.Target, "/") {
|
||||
target = strings.TrimPrefix(r.Target, "/")
|
||||
} else {
|
||||
target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))
|
||||
}
|
||||
|
||||
pic := Picture{Extension: filepath.Ext(target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
|
||||
if buffer, _ := f.Pkg.Load(target); buffer != nil {
|
||||
pic.File = buffer.([]byte)
|
||||
pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
|
||||
pics = append(pics, pic)
|
||||
}
|
||||
}
|
||||
for _, anchor := range wsDr.TwoCellAnchor {
|
||||
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
|
||||
}
|
||||
for _, anchor := range wsDr.OneCellAnchor {
|
||||
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getPicturesFromWsDr provides a function to get picture base name and raw
|
||||
// content in worksheet drawing by given coordinates and drawing
|
||||
// relationships.
|
||||
func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (pics []Picture) {
|
||||
var (
|
||||
ok bool
|
||||
anchor *xdrCellAnchor
|
||||
drawRel *xlsxRelationship
|
||||
)
|
||||
wsDr.Lock()
|
||||
defer wsDr.Unlock()
|
||||
for _, anchor = range wsDr.TwoCellAnchor {
|
||||
// extractCellAnchor extract drawing object from cell anchor by giving drawing
|
||||
// cell anchor, drawing relationships part path, conditional and callback
|
||||
// function.
|
||||
func (f *File) extractCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
|
||||
cond func(from *xlsxFrom) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship),
|
||||
cond2 func(from *decodeFrom) bool, cb2 func(anchor *decodeCellAnchor, rels *xlsxRelationship),
|
||||
) {
|
||||
var drawRel *xlsxRelationship
|
||||
if anchor.GraphicFrame == "" {
|
||||
if anchor.From != nil && anchor.Pic != nil {
|
||||
if anchor.From.Col == col && anchor.From.Row == row {
|
||||
if cond(anchor.From) {
|
||||
if drawRel = f.getDrawingRelationships(drawingRelationships,
|
||||
anchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
|
||||
if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
|
||||
pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}}
|
||||
if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil {
|
||||
pic.File = buffer.([]byte)
|
||||
pic.Format.AltText = anchor.Pic.NvPicPr.CNvPr.Descr
|
||||
pics = append(pics, pic)
|
||||
}
|
||||
}
|
||||
if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
|
||||
cb(anchor, drawRel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
f.extractDecodeCellAnchor(anchor, drawingRelationships, cond2, cb2)
|
||||
}
|
||||
|
||||
// extractDecodeCellAnchor extract drawing object from cell anchor by giving
|
||||
// decoded drawing cell anchor, drawing relationships part path, conditional and
|
||||
// callback function.
|
||||
func (f *File) extractDecodeCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
|
||||
cond func(from *decodeFrom) bool, cb func(anchor *decodeCellAnchor, rels *xlsxRelationship),
|
||||
) {
|
||||
var (
|
||||
drawRel *xlsxRelationship
|
||||
deCellAnchor = new(decodeCellAnchor)
|
||||
)
|
||||
_ = f.xmlNewDecoder(strings.NewReader("<decodeCellAnchor>" + anchor.GraphicFrame + "</decodeCellAnchor>")).Decode(&deCellAnchor)
|
||||
if deCellAnchor.From != nil && deCellAnchor.Pic != nil {
|
||||
if cond(deCellAnchor.From) {
|
||||
if drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
|
||||
if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
|
||||
cb(deCellAnchor, drawRel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getDrawingRelationships provides a function to get drawing relationships
|
||||
// from xl/drawings/_rels/drawing%s.xml.rels by given file name and
|
||||
// relationship ID.
|
||||
func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship {
|
||||
if drawingRels, _ := f.relsReader(rels); drawingRels != nil {
|
||||
drawingRels.Lock()
|
||||
defer drawingRels.Unlock()
|
||||
drawingRels.mu.Lock()
|
||||
defer drawingRels.mu.Unlock()
|
||||
for _, v := range drawingRels.Relationships {
|
||||
if v.ID == rID {
|
||||
return &v
|
||||
|
@ -750,10 +733,7 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
|
|||
if inMergeCell {
|
||||
continue
|
||||
}
|
||||
if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err != nil {
|
||||
return
|
||||
}
|
||||
if inMergeCell {
|
||||
if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err == nil {
|
||||
rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis())
|
||||
_ = sortCoordinates(rng)
|
||||
}
|
||||
|
@ -776,7 +756,244 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
|
|||
asp := float64(cellHeight) / height
|
||||
height, width = float64(cellHeight), width*asp
|
||||
}
|
||||
if opts.AutoFitIgnoreAspect {
|
||||
width, height = float64(cellWidth), float64(cellHeight)
|
||||
}
|
||||
width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY)
|
||||
w, h = int(width*opts.ScaleX), int(height*opts.ScaleY)
|
||||
return
|
||||
}
|
||||
|
||||
// getPictureCells provides a function to get all picture cell references in a
|
||||
// worksheet by given drawing part path and drawing relationships path.
|
||||
func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]string, error) {
|
||||
var (
|
||||
cells []string
|
||||
err error
|
||||
wsDr *xlsxWsDr
|
||||
)
|
||||
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
|
||||
return cells, err
|
||||
}
|
||||
wsDr.mu.Lock()
|
||||
defer wsDr.mu.Unlock()
|
||||
cond := func(from *xlsxFrom) bool { return true }
|
||||
cond2 := func(from *decodeFrom) bool { return true }
|
||||
cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
|
||||
if _, ok := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); ok {
|
||||
if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 {
|
||||
cells = append(cells, cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) {
|
||||
var target string
|
||||
if strings.HasPrefix(r.Target, "/") {
|
||||
target = strings.TrimPrefix(r.Target, "/")
|
||||
} else {
|
||||
target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))
|
||||
}
|
||||
|
||||
if _, ok := f.Pkg.Load(target); ok {
|
||||
if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 {
|
||||
cells = append(cells, cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, anchor := range wsDr.TwoCellAnchor {
|
||||
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
|
||||
}
|
||||
for _, anchor := range wsDr.OneCellAnchor {
|
||||
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
|
||||
}
|
||||
return cells, err
|
||||
}
|
||||
|
||||
// cellImagesReader provides a function to get the pointer to the structure
|
||||
// after deserialization of xl/cellimages.xml.
|
||||
func (f *File) cellImagesReader() (*decodeCellImages, error) {
|
||||
if f.DecodeCellImages == nil {
|
||||
f.DecodeCellImages = new(decodeCellImages)
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCellImages)))).
|
||||
Decode(f.DecodeCellImages); err != nil && err != io.EOF {
|
||||
return f.DecodeCellImages, err
|
||||
}
|
||||
}
|
||||
return f.DecodeCellImages, nil
|
||||
}
|
||||
|
||||
// getImageCells returns all the cell images and the Kingsoft WPS
|
||||
// Office embedded image cells reference by given worksheet name.
|
||||
func (f *File) getImageCells(sheet string) ([]string, error) {
|
||||
var (
|
||||
err error
|
||||
cells []string
|
||||
)
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return cells, err
|
||||
}
|
||||
for _, row := range ws.SheetData.Row {
|
||||
for _, c := range row.C {
|
||||
if c.F != nil && c.F.Content != "" &&
|
||||
strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(c.F.Content, "="), "_xlfn."), "DISPIMG") {
|
||||
if _, err = f.CalcCellValue(sheet, c.R); err != nil {
|
||||
return cells, err
|
||||
}
|
||||
cells = append(cells, c.R)
|
||||
}
|
||||
r, err := f.getImageCellRel(&c, &Picture{})
|
||||
if err != nil {
|
||||
return cells, err
|
||||
}
|
||||
if r != nil {
|
||||
cells = append(cells, c.R)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return cells, err
|
||||
}
|
||||
|
||||
// getRichDataRichValueRel returns relationship of the cell image by given meta
|
||||
// blocks value.
|
||||
func (f *File) getRichDataRichValueRel(val string) (*xlsxRelationship, error) {
|
||||
var r *xlsxRelationship
|
||||
idx, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
richValueRel, err := f.richValueRelReader()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if idx >= len(richValueRel.Rels) {
|
||||
return r, err
|
||||
}
|
||||
rID := richValueRel.Rels[idx].ID
|
||||
if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
|
||||
return nil, err
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
// getRichDataWebImagesRel returns relationship of a web image by given meta
|
||||
// blocks value.
|
||||
func (f *File) getRichDataWebImagesRel(val string) (*xlsxRelationship, error) {
|
||||
var r *xlsxRelationship
|
||||
idx, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
richValueWebImages, err := f.richValueWebImageReader()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if idx >= len(richValueWebImages.WebImageSrd) {
|
||||
return r, err
|
||||
}
|
||||
rID := richValueWebImages.WebImageSrd[idx].Blip.RID
|
||||
if r = f.getRichValueWebImageRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
|
||||
return nil, err
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
// getImageCellRel returns the cell image relationship.
|
||||
func (f *File) getImageCellRel(c *xlsxC, pic *Picture) (*xlsxRelationship, error) {
|
||||
var r *xlsxRelationship
|
||||
if c.Vm == nil || c.V != formulaErrorVALUE {
|
||||
return r, nil
|
||||
}
|
||||
metaData, err := f.metadataReader()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
vmd := metaData.ValueMetadata
|
||||
if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 {
|
||||
return r, err
|
||||
}
|
||||
richValueIdx := vmd.Bk[*c.Vm-1].Rc[0].V
|
||||
richValue, err := f.richValueReader()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if richValueIdx >= len(richValue.Rv) {
|
||||
return r, err
|
||||
}
|
||||
rv := richValue.Rv[richValueIdx].V
|
||||
if len(rv) == 2 && rv[1] == "5" {
|
||||
pic.InsertType = PictureInsertTypePlaceInCell
|
||||
return f.getRichDataRichValueRel(rv[0])
|
||||
}
|
||||
// cell image inserted by IMAGE formula function
|
||||
if len(rv) > 3 && rv[1]+rv[2] == "10" {
|
||||
pic.InsertType = PictureInsertTypeIMAGE
|
||||
return f.getRichDataWebImagesRel(rv[0])
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
// getCellImages provides a function to get the cell images and
|
||||
// the Kingsoft WPS Office embedded cell images by given worksheet name and cell
|
||||
// reference.
|
||||
func (f *File) getCellImages(sheet, cell string) ([]Picture, error) {
|
||||
pics, err := f.getDispImages(sheet, cell)
|
||||
if err != nil {
|
||||
return pics, err
|
||||
}
|
||||
_, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
|
||||
pic := Picture{Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell}
|
||||
r, err := f.getImageCellRel(c, &pic)
|
||||
if err != nil || r == nil {
|
||||
return "", true, err
|
||||
}
|
||||
pic.Extension = filepath.Ext(r.Target)
|
||||
if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil {
|
||||
pic.File = buffer.([]byte)
|
||||
pics = append(pics, pic)
|
||||
}
|
||||
return "", true, nil
|
||||
})
|
||||
return pics, err
|
||||
}
|
||||
|
||||
// getDispImages provides a function to get the Kingsoft WPS Office embedded
|
||||
// cell images by given worksheet name and cell reference.
|
||||
func (f *File) getDispImages(sheet, cell string) ([]Picture, error) {
|
||||
formula, err := f.GetCellFormula(sheet, cell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(formula, "="), "_xlfn."), "DISPIMG") {
|
||||
return nil, err
|
||||
}
|
||||
imgID, err := f.CalcCellValue(sheet, cell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cellImages, err := f.cellImagesReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rels, err := f.relsReader(defaultXMLPathCellImagesRels)
|
||||
if rels == nil {
|
||||
return nil, err
|
||||
}
|
||||
var pics []Picture
|
||||
for _, cellImg := range cellImages.CellImage {
|
||||
if cellImg.Pic.NvPicPr.CNvPr.Name == imgID {
|
||||
for _, r := range rels.Relationships {
|
||||
if r.ID == cellImg.Pic.BlipFill.Blip.Embed {
|
||||
pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypeDISPIMG}
|
||||
if buffer, _ := f.Pkg.Load("xl/" + r.Target); buffer != nil {
|
||||
pic.File = buffer.([]byte)
|
||||
pic.Format.AltText = cellImg.Pic.NvPicPr.CNvPr.Descr
|
||||
pics = append(pics, pic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pics, err
|
||||
}
|
||||
|
|
365
picture_test.go
365
picture_test.go
|
@ -12,10 +12,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkAddPictureFromBytes(b *testing.B) {
|
||||
|
@ -49,6 +48,7 @@ func TestAddPicture(t *testing.T) {
|
|||
// Test add picture to worksheet with autofit
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{OffsetX: 10, OffsetY: 10, AutoFit: true}))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "C30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true, AutoFitIgnoreAspect: true}))
|
||||
_, err = f.NewSheet("AddPicture")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30))
|
||||
|
@ -59,18 +59,55 @@ func TestAddPicture(t *testing.T) {
|
|||
|
||||
// Test add picture to worksheet from bytes
|
||||
assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}))
|
||||
// Test add picture to worksheet from bytes with unsupported insert type
|
||||
assert.Equal(t, ErrParameterInvalid, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}, InsertType: PictureInsertTypePlaceInCell}))
|
||||
// Test add picture to worksheet from bytes with illegal cell reference
|
||||
assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}))
|
||||
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q28", filepath.Join("test", "images", "excel.bmp"), nil))
|
||||
for _, preset := range [][]string{{"Q8", "gif"}, {"Q15", "jpg"}, {"Q22", "tif"}, {"Q28", "bmp"}} {
|
||||
assert.NoError(t, f.AddPicture("Sheet1", preset[0], filepath.Join("test", "images", fmt.Sprintf("excel.%s", preset[1])), nil))
|
||||
}
|
||||
|
||||
// Test write file to given path
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test get pictures after inserting a new picture from a workbook which contains existing pictures
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
pics, err := f.GetPictures("Sheet1", "A30")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pics, 2)
|
||||
|
||||
// Test get picture cells
|
||||
cells, err := f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
path := "xl/drawings/drawing1.xml"
|
||||
f.Drawings.Delete(path)
|
||||
cells, err = f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells)
|
||||
// Test get picture cells with unsupported charset
|
||||
f.Drawings.Delete(path)
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictureCells("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
// Test get picture cells with unsupported charset
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictureCells("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add picture with unsupported charset content types
|
||||
f = NewFile()
|
||||
f.ContentTypes = nil
|
||||
|
@ -99,16 +136,11 @@ func TestAddPictureErrors(t *testing.T) {
|
|||
// Test add picture with custom image decoder and encoder
|
||||
decode := func(r io.Reader) (image.Image, error) { return nil, nil }
|
||||
decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil }
|
||||
image.RegisterFormat("emf", "", decode, decodeConfig)
|
||||
image.RegisterFormat("wmf", "", decode, decodeConfig)
|
||||
image.RegisterFormat("emz", "", decode, decodeConfig)
|
||||
image.RegisterFormat("wmz", "", decode, decodeConfig)
|
||||
image.RegisterFormat("svg", "", decode, decodeConfig)
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.1}))
|
||||
for cell, ext := range map[string]string{"Q1": "emf", "Q7": "wmf", "Q13": "emz", "Q19": "wmz"} {
|
||||
image.RegisterFormat(ext, "", decode, decodeConfig)
|
||||
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), nil))
|
||||
}
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.8}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
@ -120,6 +152,7 @@ func TestGetPicture(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Len(t, pics[0].File, 13233)
|
||||
assert.Empty(t, pics[0].Format.AltText)
|
||||
assert.Equal(t, PictureInsertTypePlaceOverCells, pics[0].InsertType)
|
||||
|
||||
f, err = prepareTestBook1()
|
||||
if !assert.NoError(t, err) {
|
||||
|
@ -135,7 +168,7 @@ func TestGetPicture(t *testing.T) {
|
|||
|
||||
// Try to get picture from a worksheet with illegal cell reference
|
||||
_, err = f.GetPictures("Sheet1", "A")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
|
||||
// Try to get picture from a worksheet that doesn't contain any images
|
||||
pics, err = f.GetPictures("Sheet3", "I9")
|
||||
|
@ -175,6 +208,32 @@ func TestGetPicture(t *testing.T) {
|
|||
assert.Len(t, pics, 0)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Try to get picture with one cell anchor
|
||||
f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/drawing2.xml", []byte(`<xdr:wsDr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><xdr:oneCellAnchor><xdr:from><xdr:col>10</xdr:col><xdr:row>15</xdr:row></xdr:from><xdr:to><xdr:col>13</xdr:col><xdr:row>22</xdr:row></xdr:to><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="2"></xdr:cNvPr></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"></a:blip></xdr:blipFill></xdr:pic></xdr:oneCellAnchor></xdr:wsDr>`))
|
||||
pics, err = f.GetPictures("Sheet2", "K16")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pics, 1)
|
||||
// Try to get picture cells with one cell anchor
|
||||
cells, err := f.GetPictureCells("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"K16"}, cells)
|
||||
|
||||
// Try to get picture cells with absolute target path in the drawing relationship
|
||||
rels, err := f.relsReader("xl/drawings/_rels/drawing2.xml.rels")
|
||||
assert.NoError(t, err)
|
||||
rels.Relationships[0].Target = "/xl/media/image2.jpeg"
|
||||
cells, err = f.GetPictureCells("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"K16"}, cells)
|
||||
// Try to get pictures with absolute target path in the drawing relationship
|
||||
pics, err = f.GetPictures("Sheet2", "K16")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pics, 1)
|
||||
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test get picture from none drawing worksheet
|
||||
f = NewFile()
|
||||
pics, err = f.GetPictures("Sheet1", "F22")
|
||||
|
@ -185,12 +244,44 @@ func TestGetPicture(t *testing.T) {
|
|||
|
||||
// Test get pictures with unsupported charset
|
||||
path := "xl/drawings/drawing1.xml"
|
||||
f.Drawings.Delete(path)
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictures("Sheet1", "F21")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
_, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
f.Drawings.Delete(path)
|
||||
_, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test get embedded cell pictures
|
||||
f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "F21", "=_xlfn.DISPIMG(\"ID_********************************\",1)"))
|
||||
f.Pkg.Store(defaultXMLPathCellImages, []byte(`<etc:cellImages xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData"><etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="ID_********************************" descr="CellImage1"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/></xdr:blipFill></xdr:pic></etc:cellImage></etc:cellImages>`))
|
||||
f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="media/image1.jpeg"/></Relationships>`, SourceRelationshipImage)))
|
||||
pics, err = f.GetPictures("Sheet1", "F21")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pics, 2)
|
||||
assert.Equal(t, "CellImage1", pics[0].Format.AltText)
|
||||
assert.Equal(t, PictureInsertTypeDISPIMG, pics[0].InsertType)
|
||||
|
||||
// Test get embedded cell pictures with invalid formula
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=_xlfn.DISPIMG()"))
|
||||
_, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments")
|
||||
|
||||
// Test get embedded cell pictures with unsupported charset
|
||||
f.Relationships.Delete(defaultXMLPathCellImagesRels)
|
||||
f.Pkg.Store(defaultXMLPathCellImagesRels, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictures("Sheet1", "F21")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
f.Pkg.Store(defaultXMLPathCellImages, MacintoshCyrillicCharset)
|
||||
f.DecodeCellImages = nil
|
||||
_, err = f.GetPictures("Sheet1", "F21")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddDrawingPicture(t *testing.T) {
|
||||
|
@ -198,6 +289,8 @@ func TestAddDrawingPicture(t *testing.T) {
|
|||
f := NewFile()
|
||||
opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}
|
||||
assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
// Test addDrawingPicture with invalid positioning types
|
||||
assert.Equal(t, f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}), ErrParameterInvalid)
|
||||
|
||||
path := "xl/drawings/drawing1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
|
@ -227,19 +320,60 @@ func TestAddPictureFromBytes(t *testing.T) {
|
|||
func TestDeletePicture(t *testing.T) {
|
||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
// Test delete picture on a worksheet which does not contains any pictures
|
||||
assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.NoError(t, f.DeletePicture("Sheet1", "P1"))
|
||||
// Add same pictures on different worksheets
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "F20", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "I20", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet2", "F1", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
// Test delete picture on a worksheet, the images should be preserved
|
||||
assert.NoError(t, f.DeletePicture("Sheet1", "F20"))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
// Test delete same picture on different worksheet, the images should be removed
|
||||
assert.NoError(t, f.DeletePicture("Sheet1", "F10"))
|
||||
assert.NoError(t, f.DeletePicture("Sheet2", "F1"))
|
||||
assert.NoError(t, f.DeletePicture("Sheet1", "I20"))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture2.xlsx")))
|
||||
|
||||
// Test delete picture on not exists worksheet
|
||||
assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist")
|
||||
// Test delete picture with invalid sheet name
|
||||
assert.EqualError(t, f.DeletePicture("Sheet:1", "A1"), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.DeletePicture("Sheet:1", "A1"))
|
||||
// Test delete picture with invalid coordinates
|
||||
assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", ""))
|
||||
assert.NoError(t, f.Close())
|
||||
// Test delete picture on no chart worksheet
|
||||
assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1"))
|
||||
|
||||
f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
// Test delete picture with unsupported charset drawing
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeletePicture("Sheet1", "F10"), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
// Test delete picture with unsupported charset drawing relationships
|
||||
f.Relationships.Delete("xl/drawings/_rels/drawing1.xml.rels")
|
||||
f.Pkg.Store("xl/drawings/_rels/drawing1.xml.rels", MacintoshCyrillicCharset)
|
||||
assert.NoError(t, f.DeletePicture("Sheet2", "F1"))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "G1", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml")
|
||||
assert.True(t, ok)
|
||||
// Made two picture reference the same drawing relationship ID
|
||||
drawing.(*xlsxWsDr).TwoCellAnchor[1].Pic.BlipFill.Blip.Embed = "rId1"
|
||||
assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestDrawingResize(t *testing.T) {
|
||||
|
@ -249,11 +383,22 @@ func TestDrawingResize(t *testing.T) {
|
|||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test calculate drawing resize with invalid coordinates
|
||||
_, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil)
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err)
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
|
||||
}
|
||||
|
||||
func TestSetContentTypePartRelsExtensions(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.ContentTypes = &xlsxTypes{}
|
||||
assert.NoError(t, f.setContentTypePartRelsExtensions())
|
||||
|
||||
// Test set content type part relationships extensions with unsupported charset content types
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.setContentTypePartRelsExtensions(), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestSetContentTypePartImageExtensions(t *testing.T) {
|
||||
|
@ -279,3 +424,175 @@ func TestAddContentTypePart(t *testing.T) {
|
|||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestGetPictureCells(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test get picture cells on a worksheet which not contains any pictures
|
||||
cells, err := f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, cells)
|
||||
// Test get picture cells on not exists worksheet
|
||||
_, err = f.GetPictureCells("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test get embedded picture cells
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG(\"ID_********************************\",1)"))
|
||||
cells, err = f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"A2", "A1"}, cells)
|
||||
|
||||
// Test get embedded cell pictures with invalid formula
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG()"))
|
||||
_, err = f.GetPictureCells("Sheet1")
|
||||
assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestExtractDecodeCellAnchor(t *testing.T) {
|
||||
f := NewFile()
|
||||
cond := func(a *decodeFrom) bool { return true }
|
||||
cb := func(a *decodeCellAnchor, r *xlsxRelationship) {}
|
||||
f.extractDecodeCellAnchor(&xdrCellAnchor{GraphicFrame: string(MacintoshCyrillicCharset)}, "", cond, cb)
|
||||
}
|
||||
|
||||
func TestGetCellImages(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
_, err := f.getCellImages("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test get the cell images
|
||||
prepareWorkbook := func() *File {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
|
||||
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
|
||||
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v>0</v><v>5</v></rv></rvData>`))
|
||||
f.Pkg.Store(defaultXMLRdRichValueRel, []byte(`<richValueRels><rel r:id="rId1"/></richValueRels>`))
|
||||
f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipImage)))
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
|
||||
SheetData: xlsxSheetData{Row: []xlsxRow{
|
||||
{R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}},
|
||||
}},
|
||||
})
|
||||
return f
|
||||
}
|
||||
f = prepareWorkbook()
|
||||
pics, err := f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(pics))
|
||||
assert.Equal(t, PictureInsertTypePlaceInCell, pics[0].InsertType)
|
||||
cells, err := f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"A1"}, cells)
|
||||
|
||||
// Test get the cell images without image relationships parts
|
||||
f.Relationships.Delete(defaultXMLRdRichValueRelRels)
|
||||
f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink)))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images with unsupported charset rich data rich value relationships
|
||||
f.Relationships.Delete(defaultXMLRdRichValueRelRels)
|
||||
f.Pkg.Store(defaultXMLRdRichValueRelRels, MacintoshCyrillicCharset)
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images with unsupported charset rich data rich value
|
||||
f.Pkg.Store(defaultXMLRdRichValueRel, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get the image cells without block of metadata records
|
||||
cells, err = f.GetPictureCells("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.Empty(t, cells)
|
||||
// Test get the cell images with rich data rich value relationships
|
||||
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
|
||||
f.Pkg.Store(defaultXMLRdRichValueRel, []byte(`<richValueRels/>`))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images with unsupported charset meta data
|
||||
f.Pkg.Store(defaultXMLMetadata, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get the cell images without block of metadata records
|
||||
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata/></metadata>`))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
|
||||
f = prepareWorkbook()
|
||||
// Test get the cell images with empty image cell rich value
|
||||
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v></v><v>5</v></rv></rvData>`))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images without image cell rich value
|
||||
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v>0</v><v>1</v></rv></rvData>`))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images with unsupported charset rich value
|
||||
f.Pkg.Store(defaultXMLRdRichValuePart, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
f = prepareWorkbook()
|
||||
// Test get the cell images with invalid rich value index
|
||||
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="1"/></bk></valueMetadata></metadata>`))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
|
||||
f = prepareWorkbook()
|
||||
// Test get the cell images inserted by IMAGE formula function
|
||||
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="1"><v>0</v><v>1</v><v>0</v><v>0</v></rv></rvData>`))
|
||||
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`<webImagesSrd xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><webImageSrd><address r:id="rId1"/><blip r:id="rId2"/></webImageSrd>
|
||||
</webImagesSrd>`))
|
||||
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="https://github.com/xuri/excelize" TargetMode="External"/><Relationship Id="rId2" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink, SourceRelationshipImage)))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(pics))
|
||||
assert.Equal(t, PictureInsertTypeIMAGE, pics[0].InsertType)
|
||||
|
||||
// Test get the cell images inserted by IMAGE formula function with unsupported charset web images relationships
|
||||
f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels)
|
||||
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, MacintoshCyrillicCharset)
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
|
||||
// Test get the cell images inserted by IMAGE formula function without image part
|
||||
f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels)
|
||||
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="https://github.com/xuri/excelize" TargetMode="External"/><Relationship Id="rId2" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink, SourceRelationshipHyperLink)))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images inserted by IMAGE formula function with unsupported charset web images part
|
||||
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, MacintoshCyrillicCharset)
|
||||
_, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get the cell images inserted by IMAGE formula function with empty charset web images part
|
||||
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`<webImagesSrd xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />`))
|
||||
pics, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, pics)
|
||||
// Test get the cell images inserted by IMAGE formula function with invalid rich value index
|
||||
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="1"><v></v><v>1</v><v>0</v><v>0</v></rv></rvData>`))
|
||||
_, err = f.GetPictures("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
|
||||
}
|
||||
|
||||
func TestGetImageCells(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
_, err := f.getImageCells("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
|
551
pivotTable.go
551
pivotTable.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,15 +7,22 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// PivotTableOptions directly maps the format settings of the pivot table.
|
||||
|
@ -26,9 +33,14 @@ import (
|
|||
// PivotStyleMedium1 - PivotStyleMedium28
|
||||
// PivotStyleDark1 - PivotStyleDark28
|
||||
type PivotTableOptions struct {
|
||||
pivotTableSheetName string
|
||||
pivotTableXML string
|
||||
pivotCacheXML string
|
||||
pivotSheetName string
|
||||
pivotDataRange string
|
||||
namedDataRange bool
|
||||
DataRange string
|
||||
PivotTableRange string
|
||||
Name string
|
||||
Rows []PivotTableField
|
||||
Columns []PivotTableField
|
||||
Data []PivotTableField
|
||||
|
@ -39,6 +51,7 @@ type PivotTableOptions struct {
|
|||
UseAutoFormatting bool
|
||||
PageOverThenDown bool
|
||||
MergeItem bool
|
||||
ClassicLayout bool
|
||||
CompactData bool
|
||||
ShowError bool
|
||||
ShowRowHeaders bool
|
||||
|
@ -46,10 +59,16 @@ type PivotTableOptions struct {
|
|||
ShowRowStripes bool
|
||||
ShowColStripes bool
|
||||
ShowLastColumn bool
|
||||
FieldPrintTitles bool
|
||||
ItemPrintTitles bool
|
||||
PivotTableStyleName string
|
||||
}
|
||||
|
||||
// PivotTableField directly maps the field settings of the pivot table.
|
||||
//
|
||||
// Name specifies the name of the data field. Maximum 255 characters
|
||||
// are allowed in data field name, excess characters will be truncated.
|
||||
//
|
||||
// Subtotal specifies the aggregation function that applies to this data
|
||||
// field. The default value is sum. The possible values for this attribute
|
||||
// are:
|
||||
|
@ -66,24 +85,28 @@ type PivotTableOptions struct {
|
|||
// Var
|
||||
// Varp
|
||||
//
|
||||
// Name specifies the name of the data field. Maximum 255 characters
|
||||
// are allowed in data field name, excess characters will be truncated.
|
||||
// NumFmt specifies the number format ID of the data field, this filed only
|
||||
// accepts built-in number format ID and does not support custom number format
|
||||
// expression currently.
|
||||
type PivotTableField struct {
|
||||
Compact bool
|
||||
Data string
|
||||
Name string
|
||||
Outline bool
|
||||
ShowAll bool
|
||||
InsertBlankRow bool
|
||||
Subtotal string
|
||||
DefaultSubtotal bool
|
||||
NumFmt int
|
||||
}
|
||||
|
||||
// AddPivotTable provides the method to add pivot table by given pivot table
|
||||
// options. Note that the same fields can not in Columns, Rows and Filter
|
||||
// fields at the same time.
|
||||
//
|
||||
// For example, create a pivot table on the range reference Sheet1!$G$2:$M$34
|
||||
// with the range reference Sheet1!$A$1:$E$31 as the data source, summarize by
|
||||
// sum for sales:
|
||||
// For example, create a pivot table on the range reference Sheet1!G2:M34 with
|
||||
// the range reference Sheet1!A1:E31 as the data source, summarize by sum for
|
||||
// sales:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
|
@ -115,8 +138,8 @@ type PivotTableField struct {
|
|||
// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])
|
||||
// }
|
||||
// if err := f.AddPivotTable(&excelize.PivotTableOptions{
|
||||
// DataRange: "Sheet1!$A$1:$E$31",
|
||||
// PivotTableRange: "Sheet1!$G$2:$M$34",
|
||||
// DataRange: "Sheet1!A1:E31",
|
||||
// PivotTableRange: "Sheet1!G2:M34",
|
||||
// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
// Filter: []excelize.PivotTableField{{Data: "Region"}},
|
||||
// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
|
@ -145,22 +168,20 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
|
|||
pivotCacheID := f.countPivotCache() + 1
|
||||
|
||||
sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml"
|
||||
pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
|
||||
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
|
||||
err = f.addPivotCache(pivotCacheXML, opts)
|
||||
if err != nil {
|
||||
opts.pivotTableXML = strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
|
||||
opts.pivotCacheXML = "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
|
||||
if err = f.addPivotCache(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// workbook pivot cache
|
||||
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
|
||||
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(opts.pivotCacheXML, "xl/"), "")
|
||||
cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID)
|
||||
|
||||
pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels"
|
||||
// rId not used
|
||||
_ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
|
||||
err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts)
|
||||
if err != nil {
|
||||
if err = f.addPivotTable(cacheID, pivotTableID, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels"
|
||||
|
@ -179,16 +200,18 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
|
|||
}
|
||||
pivotTableSheetName, _, err := f.adjustRange(opts.PivotTableRange)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
|
||||
return nil, "", newPivotTableRangeError(err.Error())
|
||||
}
|
||||
opts.pivotTableSheetName = pivotTableSheetName
|
||||
dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName)
|
||||
if dataRange == "" {
|
||||
dataRange = opts.DataRange
|
||||
if len(opts.Name) > MaxFieldLength {
|
||||
return nil, "", ErrNameLength
|
||||
}
|
||||
dataSheetName, _, err := f.adjustRange(dataRange)
|
||||
opts.pivotSheetName = pivotTableSheetName
|
||||
if err = f.getPivotTableDataRange(opts); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
dataSheetName, _, err := f.adjustRange(opts.pivotDataRange)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
|
||||
return nil, "", newPivotTableDataRangeError(err.Error())
|
||||
}
|
||||
dataSheet, err := f.workSheetReader(dataSheetName)
|
||||
if err != nil {
|
||||
|
@ -196,7 +219,10 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
|
|||
}
|
||||
pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName)
|
||||
if !ok {
|
||||
return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s does not exist", pivotTableSheetName)
|
||||
return dataSheet, pivotTableSheetPath, ErrSheetNotExist{pivotTableSheetName}
|
||||
}
|
||||
if opts.CompactData && opts.ClassicLayout {
|
||||
return nil, "", ErrPivotTableClassicLayout
|
||||
}
|
||||
return dataSheet, pivotTableSheetPath, err
|
||||
}
|
||||
|
@ -231,17 +257,16 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
|
|||
return rng[0], []int{x1, y1, x2, y2}, nil
|
||||
}
|
||||
|
||||
// getPivotFieldsOrder provides a function to get order list of pivot table
|
||||
// getTableFieldsOrder provides a function to get order list of pivot table
|
||||
// fields.
|
||||
func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) {
|
||||
func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) {
|
||||
var order []string
|
||||
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
|
||||
if dataRange == "" {
|
||||
dataRange = opts.DataRange
|
||||
if err := f.getPivotTableDataRange(opts); err != nil {
|
||||
return order, err
|
||||
}
|
||||
dataSheet, coordinates, err := f.adjustRange(dataRange)
|
||||
dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
|
||||
if err != nil {
|
||||
return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
|
||||
return order, newPivotTableDataRangeError(err.Error())
|
||||
}
|
||||
for col := coordinates[0]; col <= coordinates[2]; col++ {
|
||||
coordinate, _ := CoordinatesToCellName(col, coordinates[1])
|
||||
|
@ -249,83 +274,68 @@ func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) {
|
|||
if err != nil {
|
||||
return order, err
|
||||
}
|
||||
if name == "" {
|
||||
return order, ErrParameterInvalid
|
||||
}
|
||||
order = append(order, name)
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// addPivotCache provides a function to create a pivot cache by given properties.
|
||||
func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error {
|
||||
func (f *File) addPivotCache(opts *PivotTableOptions) error {
|
||||
// validate data range
|
||||
definedNameRef := true
|
||||
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
|
||||
if dataRange == "" {
|
||||
definedNameRef = false
|
||||
dataRange = opts.DataRange
|
||||
}
|
||||
dataSheet, coordinates, err := f.adjustRange(dataRange)
|
||||
dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
|
||||
return newPivotTableDataRangeError(err.Error())
|
||||
}
|
||||
// data range has been checked
|
||||
order, _ := f.getPivotFieldsOrder(opts)
|
||||
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
|
||||
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
|
||||
order, err := f.getTableFieldsOrder(opts)
|
||||
if err != nil {
|
||||
return newPivotTableDataRangeError(err.Error())
|
||||
}
|
||||
topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
|
||||
bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
|
||||
pc := xlsxPivotCacheDefinition{
|
||||
SaveData: false,
|
||||
RefreshOnLoad: true,
|
||||
CreatedVersion: pivotTableVersion,
|
||||
RefreshedVersion: pivotTableVersion,
|
||||
RefreshedVersion: pivotTableRefreshedVersion,
|
||||
MinRefreshableVersion: pivotTableVersion,
|
||||
CacheSource: &xlsxCacheSource{
|
||||
Type: "worksheet",
|
||||
WorksheetSource: &xlsxWorksheetSource{
|
||||
Ref: hCell + ":" + vCell,
|
||||
Ref: topLeftCell + ":" + bottomRightCell,
|
||||
Sheet: dataSheet,
|
||||
},
|
||||
},
|
||||
CacheFields: &xlsxCacheFields{},
|
||||
}
|
||||
if definedNameRef {
|
||||
if opts.namedDataRange {
|
||||
pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange}
|
||||
}
|
||||
for _, name := range order {
|
||||
rowOptions, rowOk := f.getPivotTableFieldOptions(name, opts.Rows)
|
||||
columnOptions, colOk := f.getPivotTableFieldOptions(name, opts.Columns)
|
||||
sharedItems := xlsxSharedItems{
|
||||
Count: 0,
|
||||
}
|
||||
s := xlsxString{}
|
||||
if (rowOk && !rowOptions.DefaultSubtotal) || (colOk && !columnOptions.DefaultSubtotal) {
|
||||
s = xlsxString{
|
||||
V: "",
|
||||
}
|
||||
sharedItems.Count++
|
||||
sharedItems.S = &s
|
||||
}
|
||||
|
||||
pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{
|
||||
Name: name,
|
||||
SharedItems: &sharedItems,
|
||||
SharedItems: &xlsxSharedItems{ContainsBlank: true, M: []xlsxMissing{{}}},
|
||||
})
|
||||
}
|
||||
pc.CacheFields.Count = len(pc.CacheFields.CacheField)
|
||||
pivotCache, err := xml.Marshal(pc)
|
||||
f.saveFileList(pivotCacheXML, pivotCache)
|
||||
f.saveFileList(opts.pivotCacheXML, pivotCache)
|
||||
return err
|
||||
}
|
||||
|
||||
// addPivotTable provides a function to create a pivot table by given pivot
|
||||
// table ID and properties.
|
||||
func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opts *PivotTableOptions) error {
|
||||
func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) error {
|
||||
// validate pivot table range
|
||||
_, coordinates, err := f.adjustRange(opts.PivotTableRange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
|
||||
return newPivotTableRangeError(err.Error())
|
||||
}
|
||||
|
||||
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
|
||||
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
|
||||
topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
|
||||
bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
|
||||
|
||||
pivotTableStyle := func() string {
|
||||
if opts.PivotTableStyleName == "" {
|
||||
|
@ -334,11 +344,11 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
|
|||
return opts.PivotTableStyleName
|
||||
}
|
||||
pt := xlsxPivotTableDefinition{
|
||||
Name: fmt.Sprintf("Pivot Table%d", pivotTableID),
|
||||
Name: opts.Name,
|
||||
CacheID: cacheID,
|
||||
RowGrandTotals: &opts.RowGrandTotals,
|
||||
ColGrandTotals: &opts.ColGrandTotals,
|
||||
UpdatedVersion: pivotTableVersion,
|
||||
UpdatedVersion: pivotTableRefreshedVersion,
|
||||
MinRefreshableVersion: pivotTableVersion,
|
||||
ShowDrill: &opts.ShowDrill,
|
||||
UseAutoFormatting: &opts.UseAutoFormatting,
|
||||
|
@ -346,10 +356,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
|
|||
MergeItem: &opts.MergeItem,
|
||||
CreatedVersion: pivotTableVersion,
|
||||
CompactData: &opts.CompactData,
|
||||
GridDropZones: opts.ClassicLayout,
|
||||
ShowError: &opts.ShowError,
|
||||
FieldPrintTitles: opts.FieldPrintTitles,
|
||||
ItemPrintTitles: opts.ItemPrintTitles,
|
||||
DataCaption: "Values",
|
||||
Location: &xlsxLocation{
|
||||
Ref: hCell + ":" + vCell,
|
||||
Ref: topLeftCell + ":" + bottomRightCell,
|
||||
FirstDataCol: 1,
|
||||
FirstDataRow: 1,
|
||||
FirstHeaderRow: 1,
|
||||
|
@ -376,6 +389,14 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
|
|||
ShowLastColumn: opts.ShowLastColumn,
|
||||
},
|
||||
}
|
||||
if pt.Name == "" {
|
||||
pt.Name = fmt.Sprintf("PivotTable%d", pivotTableID)
|
||||
}
|
||||
|
||||
// set classic layout
|
||||
if opts.ClassicLayout {
|
||||
pt.Compact, pt.CompactData = boolPtr(false), boolPtr(false)
|
||||
}
|
||||
|
||||
// pivot fields
|
||||
_ = f.addPivotFields(&pt, opts)
|
||||
|
@ -390,7 +411,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
|
|||
_ = f.addPivotDataFields(&pt, opts)
|
||||
|
||||
pivotTable, err := xml.Marshal(pt)
|
||||
f.saveFileList(pivotTableXML, pivotTable)
|
||||
f.saveFileList(opts.pivotTableXML, pivotTable)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -454,6 +475,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
|
|||
}
|
||||
dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data)
|
||||
dataFieldsName := f.getPivotTableFieldsName(opts.Data)
|
||||
dataFieldsNumFmtID := f.getPivotTableFieldsNumFmtID(opts.Data)
|
||||
for idx, dataField := range dataFieldsIndex {
|
||||
if pt.DataFields == nil {
|
||||
pt.DataFields = &xlsxDataFields{}
|
||||
|
@ -462,6 +484,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
|
|||
Name: dataFieldsName[idx],
|
||||
Fld: dataField,
|
||||
Subtotal: dataFieldsSubtotals[idx],
|
||||
NumFmtID: dataFieldsNumFmtID[idx],
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -525,10 +548,18 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableO
|
|||
return err
|
||||
}
|
||||
|
||||
// setClassicLayout provides a method to set classic layout for pivot table by
|
||||
// setting Compact and Outline to false.
|
||||
func (fld *xlsxPivotField) setClassicLayout(classicLayout bool) {
|
||||
if classicLayout {
|
||||
fld.Compact, fld.Outline = boolPtr(false), boolPtr(false)
|
||||
}
|
||||
}
|
||||
|
||||
// addPivotFields create pivot fields based on the column order of the first
|
||||
// row in the data region by given pivot table definition and option.
|
||||
func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
|
||||
order, err := f.getPivotFieldsOrder(opts)
|
||||
order, err := f.getTableFieldsOrder(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -542,23 +573,26 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
|
|||
} else {
|
||||
items = append(items, &xlsxItem{T: "default"})
|
||||
}
|
||||
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
|
||||
fld := &xlsxPivotField{
|
||||
Name: f.getPivotTableFieldName(name, opts.Rows),
|
||||
Axis: "axisRow",
|
||||
DataField: inPivotTableField(opts.Data, name) != -1,
|
||||
Compact: &rowOptions.Compact,
|
||||
Outline: &rowOptions.Outline,
|
||||
ShowAll: rowOptions.ShowAll,
|
||||
InsertBlankRow: rowOptions.InsertBlankRow,
|
||||
DefaultSubtotal: &rowOptions.DefaultSubtotal,
|
||||
Items: &xlsxItems{
|
||||
Count: len(items),
|
||||
Item: items,
|
||||
},
|
||||
})
|
||||
}
|
||||
fld.setClassicLayout(opts.ClassicLayout)
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
|
||||
continue
|
||||
}
|
||||
if inPivotTableField(opts.Filter, name) != -1 {
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
|
||||
fld := &xlsxPivotField{
|
||||
Axis: "axisPage",
|
||||
DataField: inPivotTableField(opts.Data, name) != -1,
|
||||
Name: f.getPivotTableFieldName(name, opts.Columns),
|
||||
|
@ -568,7 +602,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
|
|||
{T: "default"},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
fld.setClassicLayout(opts.ClassicLayout)
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
|
||||
continue
|
||||
}
|
||||
if inPivotTableField(opts.Columns, name) != -1 {
|
||||
|
@ -579,33 +615,41 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
|
|||
} else {
|
||||
items = append(items, &xlsxItem{T: "default"})
|
||||
}
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
|
||||
fld := &xlsxPivotField{
|
||||
Name: f.getPivotTableFieldName(name, opts.Columns),
|
||||
Axis: "axisCol",
|
||||
DataField: inPivotTableField(opts.Data, name) != -1,
|
||||
Compact: &columnOptions.Compact,
|
||||
Outline: &columnOptions.Outline,
|
||||
ShowAll: columnOptions.ShowAll,
|
||||
InsertBlankRow: columnOptions.InsertBlankRow,
|
||||
DefaultSubtotal: &columnOptions.DefaultSubtotal,
|
||||
Items: &xlsxItems{
|
||||
Count: len(items),
|
||||
Item: items,
|
||||
},
|
||||
})
|
||||
}
|
||||
fld.setClassicLayout(opts.ClassicLayout)
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
|
||||
continue
|
||||
}
|
||||
if inPivotTableField(opts.Data, name) != -1 {
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
|
||||
fld := &xlsxPivotField{
|
||||
DataField: true,
|
||||
})
|
||||
}
|
||||
fld.setClassicLayout(opts.ClassicLayout)
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
|
||||
continue
|
||||
}
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{})
|
||||
fld := &xlsxPivotField{}
|
||||
fld.setClassicLayout(opts.ClassicLayout)
|
||||
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// countPivotTables provides a function to get drawing files count storage in
|
||||
// the folder xl/pivotTables.
|
||||
// countPivotTables provides a function to get pivot table files count storage
|
||||
// in the folder xl/pivotTables.
|
||||
func (f *File) countPivotTables() int {
|
||||
count := 0
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
|
@ -617,8 +661,8 @@ func (f *File) countPivotTables() int {
|
|||
return count
|
||||
}
|
||||
|
||||
// countPivotCache provides a function to get drawing files count storage in
|
||||
// the folder xl/pivotCache.
|
||||
// countPivotCache provides a function to get pivot table cache definition files
|
||||
// count storage in the folder xl/pivotCache.
|
||||
func (f *File) countPivotCache() int {
|
||||
count := 0
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
|
@ -634,7 +678,7 @@ func (f *File) countPivotCache() int {
|
|||
// to a sequential index by given fields and pivot option.
|
||||
func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) {
|
||||
var pivotFieldsIndex []int
|
||||
orders, err := f.getPivotFieldsOrder(opts)
|
||||
orders, err := f.getTableFieldsOrder(opts)
|
||||
if err != nil {
|
||||
return pivotFieldsIndex, err
|
||||
}
|
||||
|
@ -689,6 +733,22 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str
|
|||
return ""
|
||||
}
|
||||
|
||||
// getPivotTableFieldsNumFmtID prepare fields number format ID by given pivot
|
||||
// table fields.
|
||||
func (f *File) getPivotTableFieldsNumFmtID(fields []PivotTableField) []int {
|
||||
field := make([]int, len(fields))
|
||||
for idx, fld := range fields {
|
||||
if _, ok := builtInNumFmt[fld.NumFmt]; ok {
|
||||
field[idx] = fld.NumFmt
|
||||
continue
|
||||
}
|
||||
if (27 <= fld.NumFmt && fld.NumFmt <= 36) || (50 <= fld.NumFmt && fld.NumFmt <= 81) {
|
||||
field[idx] = fld.NumFmt
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// getPivotTableFieldOptions return options for specific field by given field name.
|
||||
func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) {
|
||||
for _, field := range fields {
|
||||
|
@ -719,3 +779,318 @@ func (f *File) addWorkbookPivotCache(RID int) int {
|
|||
})
|
||||
return cacheID
|
||||
}
|
||||
|
||||
// GetPivotTables returns all pivot table definitions in a worksheet by given
|
||||
// worksheet name.
|
||||
func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) {
|
||||
var pivotTables []PivotTableOptions
|
||||
name, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
return pivotTables, ErrSheetNotExist{sheet}
|
||||
}
|
||||
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
sheetRels, err := f.relsReader(rels)
|
||||
if err != nil {
|
||||
return pivotTables, err
|
||||
}
|
||||
if sheetRels == nil {
|
||||
sheetRels = &xlsxRelationships{}
|
||||
}
|
||||
for _, v := range sheetRels.Relationships {
|
||||
if v.Type == SourceRelationshipPivotTable {
|
||||
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
|
||||
pivotCacheRels := "xl/pivotTables/_rels/" + filepath.Base(v.Target) + ".rels"
|
||||
pivotTable, err := f.getPivotTable(sheet, pivotTableXML, pivotCacheRels)
|
||||
if err != nil {
|
||||
return pivotTables, err
|
||||
}
|
||||
pivotTables = append(pivotTables, pivotTable)
|
||||
}
|
||||
}
|
||||
return pivotTables, nil
|
||||
}
|
||||
|
||||
// getPivotTableDataRange checking given if data range is a cell reference or
|
||||
// named reference (defined name or table name), and set pivot table data range.
|
||||
func (f *File) getPivotTableDataRange(opts *PivotTableOptions) error {
|
||||
if opts.DataRange == "" {
|
||||
return newPivotTableDataRangeError(ErrParameterRequired.Error())
|
||||
}
|
||||
if opts.pivotDataRange != "" {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(opts.DataRange, "!") {
|
||||
opts.pivotDataRange = opts.DataRange
|
||||
return nil
|
||||
}
|
||||
tbls, err := f.getTables()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for sheetName, tables := range tbls {
|
||||
for _, table := range tables {
|
||||
if table.Name == opts.DataRange {
|
||||
opts.pivotDataRange, opts.namedDataRange = fmt.Sprintf("%s!%s", sheetName, table.Range), true
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !opts.namedDataRange {
|
||||
opts.pivotDataRange = f.getDefinedNameRefTo(opts.DataRange, opts.pivotSheetName)
|
||||
if opts.pivotDataRange != "" {
|
||||
opts.namedDataRange = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return newPivotTableDataRangeError(ErrParameterInvalid.Error())
|
||||
}
|
||||
|
||||
// getPivotTable provides a function to get a pivot table definition by given
|
||||
// worksheet name, pivot table XML path and pivot cache relationship XML path.
|
||||
func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) {
|
||||
var opts PivotTableOptions
|
||||
rels, err := f.relsReader(pivotCacheRels)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
var pivotCacheXML string
|
||||
for _, v := range rels.Relationships {
|
||||
if v.Type == SourceRelationshipPivotCache {
|
||||
pivotCacheXML = strings.ReplaceAll(v.Target, "..", "xl")
|
||||
break
|
||||
}
|
||||
}
|
||||
pc, err := f.pivotCacheReader(pivotCacheXML)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
pt, err := f.pivotTableReader(pivotTableXML)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
opts = PivotTableOptions{
|
||||
pivotTableXML: pivotTableXML,
|
||||
pivotCacheXML: pivotCacheXML,
|
||||
pivotSheetName: sheet,
|
||||
DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref),
|
||||
PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref),
|
||||
Name: pt.Name,
|
||||
ClassicLayout: pt.GridDropZones,
|
||||
FieldPrintTitles: pt.FieldPrintTitles,
|
||||
ItemPrintTitles: pt.ItemPrintTitles,
|
||||
}
|
||||
if pc.CacheSource.WorksheetSource.Name != "" {
|
||||
opts.DataRange = pc.CacheSource.WorksheetSource.Name
|
||||
_ = f.getPivotTableDataRange(&opts)
|
||||
}
|
||||
fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"}
|
||||
immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem()
|
||||
for _, field := range fields {
|
||||
immutableField := immutable.FieldByName(field)
|
||||
if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
|
||||
mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
|
||||
}
|
||||
}
|
||||
if si := pt.PivotTableStyleInfo; si != nil {
|
||||
opts.ShowRowHeaders = si.ShowRowHeaders
|
||||
opts.ShowColHeaders = si.ShowColHeaders
|
||||
opts.ShowRowStripes = si.ShowRowStripes
|
||||
opts.ShowColStripes = si.ShowColStripes
|
||||
opts.ShowLastColumn = si.ShowLastColumn
|
||||
opts.PivotTableStyleName = si.Name
|
||||
}
|
||||
order, err := f.getTableFieldsOrder(&opts)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
f.extractPivotTableFields(order, pt, &opts)
|
||||
return opts, err
|
||||
}
|
||||
|
||||
// pivotTableReader provides a function to get the pointer to the structure
|
||||
// after deserialization of xl/pivotTables/pivotTable%d.xml.
|
||||
func (f *File) pivotTableReader(path string) (*xlsxPivotTableDefinition, error) {
|
||||
content, ok := f.Pkg.Load(path)
|
||||
pivotTable := &xlsxPivotTableDefinition{}
|
||||
if ok && content != nil {
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
|
||||
Decode(pivotTable); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return pivotTable, nil
|
||||
}
|
||||
|
||||
// pivotCacheReader provides a function to get the pointer to the structure
|
||||
// after deserialization of xl/pivotCache/pivotCacheDefinition%d.xml.
|
||||
func (f *File) pivotCacheReader(path string) (*xlsxPivotCacheDefinition, error) {
|
||||
content, ok := f.Pkg.Load(path)
|
||||
pivotCache := &xlsxPivotCacheDefinition{}
|
||||
if ok && content != nil {
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
|
||||
Decode(pivotCache); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return pivotCache, nil
|
||||
}
|
||||
|
||||
// extractPivotTableFields provides a function to extract all pivot table fields
|
||||
// settings by given pivot table fields.
|
||||
func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinition, opts *PivotTableOptions) {
|
||||
for fieldIdx, field := range pt.PivotFields.PivotField {
|
||||
if field.Axis == "axisRow" {
|
||||
opts.Rows = append(opts.Rows, extractPivotTableField(order[fieldIdx], field))
|
||||
}
|
||||
if field.Axis == "axisCol" {
|
||||
opts.Columns = append(opts.Columns, extractPivotTableField(order[fieldIdx], field))
|
||||
}
|
||||
if field.Axis == "axisPage" {
|
||||
opts.Filter = append(opts.Filter, extractPivotTableField(order[fieldIdx], field))
|
||||
}
|
||||
}
|
||||
if pt.DataFields != nil {
|
||||
for _, field := range pt.DataFields.DataField {
|
||||
opts.Data = append(opts.Data, PivotTableField{
|
||||
Data: order[field.Fld],
|
||||
Name: field.Name,
|
||||
Subtotal: cases.Title(language.English).String(field.Subtotal),
|
||||
NumFmt: field.NumFmtID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractPivotTableField provides a function to extract pivot table field
|
||||
// settings by given pivot table fields.
|
||||
func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField {
|
||||
pivotTableField := PivotTableField{
|
||||
Data: data,
|
||||
ShowAll: fld.ShowAll,
|
||||
InsertBlankRow: fld.InsertBlankRow,
|
||||
}
|
||||
fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"}
|
||||
immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem()
|
||||
for _, field := range fields {
|
||||
immutableField := immutable.FieldByName(field)
|
||||
if immutableField.Kind() == reflect.String {
|
||||
mutable.FieldByName(field).SetString(immutableField.String())
|
||||
}
|
||||
if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
|
||||
mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
|
||||
}
|
||||
}
|
||||
return pivotTableField
|
||||
}
|
||||
|
||||
// genPivotCacheDefinitionID generates a unique pivot table cache definition ID.
|
||||
func (f *File) genPivotCacheDefinitionID() int {
|
||||
var (
|
||||
ID int
|
||||
decodeExtLst = new(decodeExtLst)
|
||||
decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition)
|
||||
)
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") {
|
||||
pc, err := f.pivotCacheReader(k.(string))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if pc.ExtLst != nil {
|
||||
_ = f.xmlNewDecoder(strings.NewReader("<extLst>" + pc.ExtLst.Ext + "</extLst>")).Decode(decodeExtLst)
|
||||
for _, ext := range decodeExtLst.Ext {
|
||||
if ext.URI == ExtURIPivotCacheDefinition {
|
||||
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition)
|
||||
if ID < decodeX14PivotCacheDefinition.PivotCacheID {
|
||||
ID = decodeX14PivotCacheDefinition.PivotCacheID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return ID + 1
|
||||
}
|
||||
|
||||
// deleteWorkbookPivotCache remove workbook pivot cache and pivot cache
|
||||
// relationships.
|
||||
func (f *File) deleteWorkbookPivotCache(opt PivotTableOptions) error {
|
||||
rID, err := f.deleteWorkbookRels(SourceRelationshipPivotCache, strings.TrimPrefix(strings.TrimPrefix(opt.pivotCacheXML, "/"), "xl/"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wb, err := f.workbookReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wb.PivotCaches != nil {
|
||||
for i, pivotCache := range wb.PivotCaches.PivotCache {
|
||||
if pivotCache.RID == rID {
|
||||
wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache[:i], wb.PivotCaches.PivotCache[i+1:]...)
|
||||
}
|
||||
}
|
||||
if len(wb.PivotCaches.PivotCache) == 0 {
|
||||
wb.PivotCaches = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePivotTable delete a pivot table by giving the worksheet name and pivot
|
||||
// table name. Note that this function does not clean cell values in the pivot
|
||||
// table range.
|
||||
func (f *File) DeletePivotTable(sheet, name string) error {
|
||||
sheetXML, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
return ErrSheetNotExist{sheet}
|
||||
}
|
||||
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXML, "xl/worksheets/") + ".rels"
|
||||
sheetRels, err := f.relsReader(rels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sheetRels == nil {
|
||||
sheetRels = &xlsxRelationships{}
|
||||
}
|
||||
opts, err := f.GetPivotTables(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pivotTableCaches := map[string]int{}
|
||||
pivotTables, _ := f.getPivotTables()
|
||||
for _, sheetPivotTables := range pivotTables {
|
||||
for _, sheetPivotTable := range sheetPivotTables {
|
||||
pivotTableCaches[sheetPivotTable.pivotCacheXML]++
|
||||
}
|
||||
}
|
||||
for _, v := range sheetRels.Relationships {
|
||||
for _, opt := range opts {
|
||||
if v.Type == SourceRelationshipPivotTable {
|
||||
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
|
||||
if opt.Name == name && opt.pivotTableXML == pivotTableXML {
|
||||
if pivotTableCaches[opt.pivotCacheXML] == 1 {
|
||||
err = f.deleteWorkbookPivotCache(opt)
|
||||
}
|
||||
f.deleteSheetRelationships(sheet, v.ID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return newNoExistTableError(name)
|
||||
}
|
||||
|
||||
// getPivotTables provides a function to get all pivot tables in a workbook.
|
||||
func (f *File) getPivotTables() (map[string][]PivotTableOptions, error) {
|
||||
pivotTables := map[string][]PivotTableOptions{}
|
||||
for _, sheetName := range f.GetSheetList() {
|
||||
pts, err := f.GetPivotTables(sheetName)
|
||||
e := ErrSheetNotExist{sheetName}
|
||||
if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
|
||||
return pivotTables, err
|
||||
}
|
||||
pivotTables[sheetName] = append(pivotTables[sheetName], pts...)
|
||||
}
|
||||
return pivotTables, nil
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddPivotTable(t *testing.T) {
|
||||
func TestPivotTable(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Create some data in a sheet
|
||||
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
|
@ -25,25 +25,38 @@ func TestAddPivotTable(t *testing.T) {
|
|||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
|
||||
}
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$G$2:$M$34",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
expected := &PivotTableOptions{
|
||||
pivotTableXML: "xl/pivotTables/pivotTable1.xml",
|
||||
pivotCacheXML: "xl/pivotCache/pivotCacheDefinition1.xml",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!G2:M34",
|
||||
Name: "PivotTable1",
|
||||
Rows: []PivotTableField{{Data: "Month", ShowAll: true, DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Filter: []PivotTableField{{Data: "Region"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
|
||||
Columns: []PivotTableField{{Data: "Type", ShowAll: true, InsertBlankRow: true, DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum", NumFmt: 38}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
ClassicLayout: true,
|
||||
ShowError: true,
|
||||
ShowRowHeaders: true,
|
||||
ShowColHeaders: true,
|
||||
ShowLastColumn: true,
|
||||
ShowError: true,
|
||||
}))
|
||||
FieldPrintTitles: true,
|
||||
ItemPrintTitles: true,
|
||||
PivotTableStyleName: "PivotStyleLight16",
|
||||
}
|
||||
assert.NoError(t, f.AddPivotTable(expected))
|
||||
// Test get pivot table
|
||||
pivotTables, err := f.GetPivotTables("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pivotTables, 1)
|
||||
assert.Equal(t, *expected, pivotTables[0])
|
||||
// Use different order of coordinate tests
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
|
||||
|
@ -54,10 +67,15 @@ func TestAddPivotTable(t *testing.T) {
|
|||
ShowColHeaders: true,
|
||||
ShowLastColumn: true,
|
||||
}))
|
||||
// Test get pivot table with default style name
|
||||
pivotTables, err = f.GetPivotTables("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pivotTables, 2)
|
||||
assert.Equal(t, "PivotStyleLight16", pivotTables[1].PivotTableStyleName)
|
||||
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$W$2:$AC$34",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!W2:AC34",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Region"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}},
|
||||
|
@ -69,8 +87,8 @@ func TestAddPivotTable(t *testing.T) {
|
|||
ShowLastColumn: true,
|
||||
}))
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$G$42:$W$55",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!G42:W55",
|
||||
Rows: []PivotTableField{{Data: "Month"}},
|
||||
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}},
|
||||
|
@ -82,8 +100,8 @@ func TestAddPivotTable(t *testing.T) {
|
|||
ShowLastColumn: true,
|
||||
}))
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$AE$2:$AG$33",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!AE2:AG33",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
|
||||
RowGrandTotals: true,
|
||||
|
@ -95,8 +113,8 @@ func TestAddPivotTable(t *testing.T) {
|
|||
}))
|
||||
// Create pivot table with empty subtotal field name and specified style
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$AJ$2:$AP1$35",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!AJ2:AP135",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Filter: []PivotTableField{{Data: "Region"}},
|
||||
Columns: []PivotTableField{},
|
||||
|
@ -109,14 +127,14 @@ func TestAddPivotTable(t *testing.T) {
|
|||
ShowLastColumn: true,
|
||||
PivotTableStyleName: "PivotStyleLight19",
|
||||
}))
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet2!$A$1:$AR$15",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet2!A1:AN17",
|
||||
Rows: []PivotTableField{{Data: "Month"}},
|
||||
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min", NumFmt: 32}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
|
@ -124,12 +142,19 @@ func TestAddPivotTable(t *testing.T) {
|
|||
ShowColHeaders: true,
|
||||
ShowLastColumn: true,
|
||||
}))
|
||||
|
||||
// Test get pivot table with across worksheet data range
|
||||
pivotTables, err = f.GetPivotTables("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pivotTables, 1)
|
||||
assert.Equal(t, "Sheet1!A1:E31", pivotTables[0].DataRange)
|
||||
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet2!$A$18:$AR$54",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet2!A20:AR60",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}},
|
||||
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product", NumFmt: 32}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
|
@ -140,16 +165,16 @@ func TestAddPivotTable(t *testing.T) {
|
|||
// Create pivot table with many data, many rows, many cols and defined name
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "dataRange",
|
||||
RefersTo: "Sheet1!$A$1:$E$31",
|
||||
RefersTo: "Sheet1!A1:E31",
|
||||
Comment: "Pivot Table Data Range",
|
||||
Scope: "Sheet2",
|
||||
}))
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "dataRange",
|
||||
PivotTableRange: "Sheet2!$A$57:$AJ$91",
|
||||
PivotTableRange: "Sheet2!A65:AJ100",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales", NumFmt: -1}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales", NumFmt: 38}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
|
@ -159,106 +184,123 @@ func TestAddPivotTable(t *testing.T) {
|
|||
}))
|
||||
|
||||
// Test empty pivot table options
|
||||
assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error())
|
||||
assert.Equal(t, ErrParameterRequired, f.AddPivotTable(nil))
|
||||
// Test add pivot table with custom name which exceeds the max characters limit
|
||||
assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "dataRange",
|
||||
PivotTableRange: "Sheet2!A65:AJ100",
|
||||
Name: strings.Repeat("c", MaxFieldLength+1),
|
||||
}))
|
||||
// Test invalid data range
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$A$1",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!A1:A1",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), `parameter 'DataRange' parsing error: parameter is invalid`)
|
||||
}))
|
||||
// Test the data range of the worksheet that is not declared
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "A1:E31",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), `parameter 'DataRange' parsing error: parameter is invalid`)
|
||||
}))
|
||||
// Test the worksheet declared in the data range does not exist
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "SheetN!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "SheetN!A1:E31",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), "sheet SheetN does not exist")
|
||||
}))
|
||||
// Test the pivot table range of the worksheet that is not declared
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "$U$34:$O$2",
|
||||
assert.Equal(t, newPivotTableRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), `parameter 'PivotTableRange' parsing error: parameter is invalid`)
|
||||
}))
|
||||
// Test the worksheet declared in the pivot table range does not exist
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "SheetN!$U$34:$O$2",
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "SheetN!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), "sheet SheetN does not exist")
|
||||
}))
|
||||
// Test not exists worksheet in data range
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "SheetN!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "SheetN!A1:E31",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), "sheet SheetN does not exist")
|
||||
}))
|
||||
// Test invalid row number in data range
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$0:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
assert.Equal(t, newPivotTableDataRangeError(newCellNameToCoordinatesError("A0", newInvalidCellNameError("A0")).Error()), f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!A0:E31",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`)
|
||||
}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx")))
|
||||
// Test with field names that exceed the length limit and invalid subtotal
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$G$2:$M$34",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!G2:M34",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
|
||||
}))
|
||||
// Test delete pivot table
|
||||
pivotTables, err = f.GetPivotTables("Sheet1")
|
||||
assert.Len(t, pivotTables, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
|
||||
pivotTables, err = f.GetPivotTables("Sheet1")
|
||||
assert.Len(t, pivotTables, 6)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test add pivot table with invalid sheet name
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet:1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet:1!$G$2:$M$34",
|
||||
assert.Error(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet:1!A1:E31",
|
||||
PivotTableRange: "Sheet:1!G2:M34",
|
||||
Rows: []PivotTableField{{Data: "Year"}},
|
||||
}), ErrSheetNameInvalid.Error())
|
||||
}), ErrSheetNameInvalid)
|
||||
// Test add pivot table with enable ClassicLayout and CompactData in the same time
|
||||
assert.Error(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!G2:M34",
|
||||
CompactData: true,
|
||||
ClassicLayout: true,
|
||||
}), ErrPivotTableClassicLayout)
|
||||
// Test delete pivot table with not exists worksheet
|
||||
assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist")
|
||||
// Test delete pivot table with not exists pivot table name
|
||||
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist")
|
||||
// Test adjust range with invalid range
|
||||
_, _, err = f.adjustRange("")
|
||||
assert.EqualError(t, err, ErrParameterRequired.Error())
|
||||
assert.Error(t, err, ErrParameterRequired)
|
||||
// Test adjust range with incorrect range
|
||||
_, _, err = f.adjustRange("sheet1!")
|
||||
assert.EqualError(t, err, "parameter is invalid")
|
||||
// Test get pivot fields order with empty data range
|
||||
_, err = f.getPivotFieldsOrder(&PivotTableOptions{})
|
||||
// Test get table fields order with empty data range
|
||||
_, err = f.getTableFieldsOrder(&PivotTableOptions{})
|
||||
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
|
||||
// Test add pivot cache with empty data range
|
||||
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
|
||||
// Test add pivot cache with invalid data range
|
||||
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{
|
||||
DataRange: "$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
}), "parameter 'DataRange' parsing error: parameter is invalid")
|
||||
assert.EqualError(t, f.addPivotCache(&PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
|
||||
// Test add pivot table with empty options
|
||||
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
|
||||
assert.EqualError(t, f.addPivotTable(0, 0, &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
|
||||
// Test add pivot table with invalid data range
|
||||
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
|
||||
assert.EqualError(t, f.addPivotTable(0, 0, &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
|
||||
// Test add pivot fields with empty data range
|
||||
assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{
|
||||
DataRange: "$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$U$34:$O$2",
|
||||
DataRange: "A1:E31",
|
||||
PivotTableRange: "Sheet1!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales"}},
|
||||
|
@ -268,20 +310,145 @@ func TestAddPivotTable(t *testing.T) {
|
|||
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
|
||||
// Test add pivot table with unsupported charset content types.
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$E$31",
|
||||
PivotTableRange: "Sheet1!$G$2:$M$34",
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!G2:M34",
|
||||
Rows: []PivotTableField{{Data: "Year"}},
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test get pivot table without pivot table
|
||||
f = NewFile()
|
||||
pivotTables, err = f.GetPivotTables("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pivotTables, 0)
|
||||
// Test get pivot table with not exists worksheet
|
||||
_, err = f.GetPivotTables("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test get pivot table with unsupported charset worksheet relationships
|
||||
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
|
||||
_, err = f.GetPivotTables("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
// Test get pivot table with unsupported charset pivot cache definition
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetPivotTables("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
// Test get pivot table with unsupported charset pivot table relationships
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/pivotTables/_rels/pivotTable1.xml.rels", MacintoshCyrillicCharset)
|
||||
_, err = f.GetPivotTables("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
// Test get pivot table with unsupported charset pivot table
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetPivotTables("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
_, err = f.getPivotTables()
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestPivotTableDataRange(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Create table in a worksheet
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
for row := 2; row < 6; row++ {
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), rand.Intn(10)))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), rand.Intn(10)))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), rand.Intn(10)))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(10)))
|
||||
}
|
||||
// Test add pivot table with table data range
|
||||
opts := PivotTableOptions{
|
||||
DataRange: "Table1",
|
||||
PivotTableRange: "Sheet1!G2:K7",
|
||||
Rows: []PivotTableField{{Data: "Column1"}},
|
||||
Columns: []PivotTableField{{Data: "Column2"}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
ShowRowHeaders: true,
|
||||
ShowColHeaders: true,
|
||||
ShowLastColumn: true,
|
||||
ShowError: true,
|
||||
PivotTableStyleName: "PivotStyleLight16",
|
||||
}
|
||||
assert.NoError(t, f.AddPivotTable(&opts))
|
||||
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable2.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
assert.NoError(t, f.AddPivotTable(&opts))
|
||||
|
||||
// Test delete pivot table with unsupported table relationships charset
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test delete pivot table with unsupported worksheet relationships charset
|
||||
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
|
||||
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test delete pivot table without worksheet relationships
|
||||
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
|
||||
f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels")
|
||||
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist")
|
||||
|
||||
t.Run("data_range_with_empty_column", func(t *testing.T) {
|
||||
// Test add pivot table with data range doesn't organized as a list with labeled columns
|
||||
f := NewFile()
|
||||
// Create some data in a sheet
|
||||
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
|
||||
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "", "Type"}))
|
||||
for row := 2; row < 32; row++ {
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
|
||||
}
|
||||
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet1!A1:E31",
|
||||
PivotTableRange: "Sheet1!G2:M34",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Type"}},
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseFormatPivotTableSet(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Create table in a worksheet
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
// Test parse format pivot table options with unsupported table relationships charset
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
_, _, err := f.parseFormatPivotTableSet(&PivotTableOptions{
|
||||
DataRange: "Table1",
|
||||
PivotTableRange: "Sheet1!G2:K7",
|
||||
Rows: []PivotTableField{{Data: "Column1"}},
|
||||
})
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestAddPivotRowFields(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test invalid data range
|
||||
assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$A$1",
|
||||
DataRange: "Sheet1!A1:A1",
|
||||
}), `parameter 'DataRange' parsing error: parameter is invalid`)
|
||||
}
|
||||
|
||||
|
@ -289,7 +456,7 @@ func TestAddPivotPageFields(t *testing.T) {
|
|||
f := NewFile()
|
||||
// Test invalid data range
|
||||
assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$A$1",
|
||||
DataRange: "Sheet1!A1:A1",
|
||||
}), `parameter 'DataRange' parsing error: parameter is invalid`)
|
||||
}
|
||||
|
||||
|
@ -297,7 +464,7 @@ func TestAddPivotDataFields(t *testing.T) {
|
|||
f := NewFile()
|
||||
// Test invalid data range
|
||||
assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$A$1",
|
||||
DataRange: "Sheet1!A1:A1",
|
||||
}), `parameter 'DataRange' parsing error: parameter is invalid`)
|
||||
}
|
||||
|
||||
|
@ -305,19 +472,55 @@ func TestAddPivotColFields(t *testing.T) {
|
|||
f := NewFile()
|
||||
// Test invalid data range
|
||||
assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
|
||||
DataRange: "Sheet1!$A$1:$A$1",
|
||||
DataRange: "Sheet1!A1:A1",
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
}), `parameter 'DataRange' parsing error: parameter is invalid`)
|
||||
}
|
||||
|
||||
func TestGetPivotFieldsOrder(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test get pivot fields order with not exist worksheet
|
||||
_, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"})
|
||||
// Test get table fields order with not exist worksheet
|
||||
_, err := f.getTableFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"})
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Create table in a worksheet
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
// Test get table fields order with unsupported table relationships charset
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.getTableFieldsOrder(&PivotTableOptions{DataRange: "Table"})
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestGetPivotTableFieldName(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.getPivotTableFieldName("-", []PivotTableField{})
|
||||
assert.Empty(t, f.getPivotTableFieldName("-", []PivotTableField{}))
|
||||
}
|
||||
|
||||
func TestGetPivotTableFieldOptions(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, ok := f.getPivotTableFieldOptions("-", []PivotTableField{})
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestGenPivotCacheDefinitionID(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test generate pivot table cache definition ID with unsupported charset
|
||||
f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
|
||||
assert.Equal(t, 1, f.genPivotCacheDefinitionID())
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestDeleteWorkbookPivotCache(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test delete workbook pivot table cache with unsupported workbook charset
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test delete workbook pivot table cache with unsupported workbook relationships charset
|
||||
f.Relationships.Delete("xl/_rels/workbook.xml.rels")
|
||||
f.Pkg.Store("xl/_rels/workbook.xml.rels", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
|
214
rows.go
214
rows.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,22 +7,35 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/tiendc/go-deepcopy"
|
||||
)
|
||||
|
||||
// duplicateHelperFunc defines functions to duplicate helper.
|
||||
var duplicateHelperFunc = [3]func(*File, *xlsxWorksheet, string, int, int) error{
|
||||
func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
|
||||
return f.duplicateConditionalFormat(ws, sheet, row, row2)
|
||||
},
|
||||
func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
|
||||
return f.duplicateDataValidations(ws, sheet, row, row2)
|
||||
},
|
||||
func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
|
||||
return f.duplicateMergeCells(ws, sheet, row, row2)
|
||||
},
|
||||
}
|
||||
|
||||
// GetRows return all the rows in a sheet by given worksheet name, returned as
|
||||
// a two-dimensional array, where the value of the cell is converted to the
|
||||
// string type. If the cell format can be applied to the value of the cell,
|
||||
|
@ -50,19 +63,22 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results, cur, max := make([][]string, 0, 64), 0, 0
|
||||
results, cur, maxVal := make([][]string, 0, 64), 0, 0
|
||||
for rows.Next() {
|
||||
cur++
|
||||
row, err := rows.Columns(opts...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
results = append(results, row)
|
||||
if len(row) > 0 {
|
||||
max = cur
|
||||
if emptyRows := cur - maxVal - 1; emptyRows > 0 {
|
||||
results = append(results, make([][]string, emptyRows)...)
|
||||
}
|
||||
results = append(results, row)
|
||||
maxVal = cur
|
||||
}
|
||||
}
|
||||
return results[:max], rows.Close()
|
||||
return results[:maxVal], rows.Close()
|
||||
}
|
||||
|
||||
// Rows defines an iterator to a sheet.
|
||||
|
@ -79,7 +95,7 @@ type Rows struct {
|
|||
curRowOpts, seekRowOpts RowOpts
|
||||
}
|
||||
|
||||
// Next will return true if find the next row element.
|
||||
// Next will return true if it finds the next row element.
|
||||
func (rows *Rows) Next() bool {
|
||||
rows.seekRow++
|
||||
if rows.curRow >= rows.seekRow {
|
||||
|
@ -138,7 +154,7 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
|
|||
}
|
||||
var rowIterator rowXMLIterator
|
||||
var token xml.Token
|
||||
rows.rawCellValue = getOptions(opts...).RawCellValue
|
||||
rows.rawCellValue = rows.f.getOptions(opts...).RawCellValue
|
||||
if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {
|
||||
return rowIterator.cells, rowIterator.err
|
||||
}
|
||||
|
@ -202,15 +218,6 @@ func appendSpace(l int, s []string) []string {
|
|||
return s
|
||||
}
|
||||
|
||||
// ErrSheetNotExist defines an error of sheet that does not exist
|
||||
type ErrSheetNotExist struct {
|
||||
SheetName string
|
||||
}
|
||||
|
||||
func (err ErrSheetNotExist) Error() string {
|
||||
return fmt.Sprintf("sheet %s does not exist", err.SheetName)
|
||||
}
|
||||
|
||||
// rowXMLIterator defined runtime use field for the worksheet row SAX parser.
|
||||
type rowXMLIterator struct {
|
||||
err error
|
||||
|
@ -267,12 +274,12 @@ func (f *File) Rows(sheet string) (*Rows, error) {
|
|||
if !ok {
|
||||
return nil, ErrSheetNotExist{sheet}
|
||||
}
|
||||
if ws, ok := f.Sheet.Load(name); ok && ws != nil {
|
||||
worksheet := ws.(*xlsxWorksheet)
|
||||
worksheet.Lock()
|
||||
defer worksheet.Unlock()
|
||||
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
|
||||
ws := worksheet.(*xlsxWorksheet)
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
// Flush data
|
||||
output, _ := xml.Marshal(worksheet)
|
||||
output, _ := xml.Marshal(ws)
|
||||
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
|
||||
}
|
||||
var err error
|
||||
|
@ -297,7 +304,9 @@ func (f *File) getFromStringItem(index int) string {
|
|||
}
|
||||
needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings)
|
||||
if needClose && err == nil {
|
||||
defer tempFile.Close()
|
||||
defer func() {
|
||||
err = tempFile.Close()
|
||||
}()
|
||||
}
|
||||
f.sharedStringItem = [][]uint{}
|
||||
f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-")
|
||||
|
@ -344,8 +353,10 @@ func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
|
|||
return true, f.xmlNewDecoder(tempFile), tempFile, err
|
||||
}
|
||||
|
||||
// SetRowHeight provides a function to set the height of a single row. For
|
||||
// example, set the height of the first row in Sheet1:
|
||||
// SetRowHeight provides a function to set the height of a single row. If the
|
||||
// value of height is 0, will hide the specified row, if the value of height is
|
||||
// -1, will unset the custom row height. For example, set the height of the
|
||||
// first row in Sheet1:
|
||||
//
|
||||
// err := f.SetRowHeight("Sheet1", 1, 50)
|
||||
func (f *File) SetRowHeight(sheet string, row int, height float64) error {
|
||||
|
@ -355,31 +366,42 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
|
|||
if height > MaxRowHeight {
|
||||
return ErrMaxRowHeight
|
||||
}
|
||||
if height < -1 {
|
||||
return ErrParameterInvalid
|
||||
}
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prepareSheetXML(ws, 0, row)
|
||||
ws.prepareSheetXML(0, row)
|
||||
|
||||
rowIdx := row - 1
|
||||
if height == -1 {
|
||||
ws.SheetData.Row[rowIdx].Ht = nil
|
||||
ws.SheetData.Row[rowIdx].CustomHeight = false
|
||||
return err
|
||||
}
|
||||
ws.SheetData.Row[rowIdx].Ht = float64Ptr(height)
|
||||
ws.SheetData.Row[rowIdx].CustomHeight = true
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// getRowHeight provides a function to get row height in pixels by given sheet
|
||||
// name and row number.
|
||||
func (f *File) getRowHeight(sheet string, row int) int {
|
||||
ws, _ := f.workSheetReader(sheet)
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
for i := range ws.SheetData.Row {
|
||||
v := &ws.SheetData.Row[i]
|
||||
if v.R == row && v.Ht != nil {
|
||||
return int(convertRowHeightToPixels(*v.Ht))
|
||||
}
|
||||
}
|
||||
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultRowHeight > 0 {
|
||||
return int(convertRowHeightToPixels(ws.SheetFormatPr.DefaultRowHeight))
|
||||
}
|
||||
// Optimization for when the row heights haven't changed.
|
||||
return int(defaultRowHeightPixels)
|
||||
}
|
||||
|
@ -390,7 +412,7 @@ func (f *File) getRowHeight(sheet string, row int) int {
|
|||
// height, err := f.GetRowHeight("Sheet1", 1)
|
||||
func (f *File) GetRowHeight(sheet string, row int) (float64, error) {
|
||||
if row < 1 {
|
||||
return defaultRowHeightPixels, newInvalidRowNumberError(row)
|
||||
return defaultRowHeight, newInvalidRowNumberError(row)
|
||||
}
|
||||
ht := defaultRowHeight
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
|
@ -416,8 +438,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) {
|
|||
// after deserialization of xl/sharedStrings.xml.
|
||||
func (f *File) sharedStringsReader() (*xlsxSST, error) {
|
||||
var err error
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
relPath := f.getWorkbookRelsPath()
|
||||
if f.SharedStrings == nil {
|
||||
var sharedStrings xlsxSST
|
||||
|
@ -470,7 +492,7 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prepareSheetXML(ws, 0, row)
|
||||
ws.prepareSheetXML(0, row)
|
||||
ws.SheetData.Row[row-1].Hidden = !visible
|
||||
return nil
|
||||
}
|
||||
|
@ -511,7 +533,7 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prepareSheetXML(ws, 0, row)
|
||||
ws.prepareSheetXML(0, row)
|
||||
ws.SheetData.Row[row-1].OutlineLevel = level
|
||||
return nil
|
||||
}
|
||||
|
@ -623,7 +645,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
|
|||
}
|
||||
|
||||
if row2 < 1 || row == row2 {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
|
@ -631,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
|
|||
|
||||
for i, r := range ws.SheetData.Row {
|
||||
if r.R == row {
|
||||
rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow)
|
||||
deepcopy.Copy(&rowCopy, ws.SheetData.Row[i])
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
|
@ -642,7 +664,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
|
|||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
idx2 := -1
|
||||
|
@ -652,24 +674,106 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
|
|||
break
|
||||
}
|
||||
}
|
||||
if idx2 == -1 && len(ws.SheetData.Row) >= row2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
|
||||
f.adjustSingleRowDimensions(&rowCopy, row2)
|
||||
rowCopy.adjustSingleRowDimensions(row2 - row)
|
||||
_ = f.adjustSingleRowFormulas(sheet, sheet, &rowCopy, row, row2-row, true)
|
||||
|
||||
if idx2 != -1 {
|
||||
ws.SheetData.Row[idx2] = rowCopy
|
||||
} else {
|
||||
ws.SheetData.Row = append(ws.SheetData.Row, rowCopy)
|
||||
}
|
||||
return f.duplicateMergeCells(sheet, ws, row, row2)
|
||||
for _, fn := range duplicateHelperFunc {
|
||||
if err := fn(f, ws, sheet, row, row2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// duplicateSQRefHelper provides a function to adjust conditional formatting and
|
||||
// data validations cell reference when duplicate rows.
|
||||
func duplicateSQRefHelper(row, row2 int, ref string) (string, error) {
|
||||
if !strings.Contains(ref, ":") {
|
||||
ref += ":" + ref
|
||||
}
|
||||
abs := strings.Contains(ref, "$")
|
||||
coordinates, err := rangeRefToCoordinates(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
|
||||
if y1 == y2 && y1 == row {
|
||||
if ref, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ref, err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// duplicateConditionalFormat create conditional formatting for the destination
|
||||
// row if there are conditional formats in the copied row.
|
||||
func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, row2 int) error {
|
||||
var cfs []*xlsxConditionalFormatting
|
||||
for _, cf := range ws.ConditionalFormatting {
|
||||
if cf != nil {
|
||||
var SQRef []string
|
||||
for _, ref := range strings.Split(cf.SQRef, " ") {
|
||||
coordinates, err := duplicateSQRefHelper(row, row2, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if coordinates != "" {
|
||||
SQRef = append(SQRef, coordinates)
|
||||
}
|
||||
}
|
||||
if len(SQRef) > 0 {
|
||||
var cfCopy xlsxConditionalFormatting
|
||||
deepcopy.Copy(&cfCopy, *cf)
|
||||
cfCopy.SQRef = strings.Join(SQRef, " ")
|
||||
cfs = append(cfs, &cfCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
ws.ConditionalFormatting = append(ws.ConditionalFormatting, cfs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// duplicateDataValidations create data validations for the destination row if
|
||||
// there are data validation rules in the copied row.
|
||||
func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, row2 int) error {
|
||||
if ws.DataValidations == nil {
|
||||
return nil
|
||||
}
|
||||
var dvs []*xlsxDataValidation
|
||||
for _, dv := range ws.DataValidations.DataValidation {
|
||||
if dv != nil {
|
||||
var SQRef []string
|
||||
for _, ref := range strings.Split(dv.Sqref, " ") {
|
||||
coordinates, err := duplicateSQRefHelper(row, row2, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if coordinates != "" {
|
||||
SQRef = append(SQRef, coordinates)
|
||||
}
|
||||
}
|
||||
if len(SQRef) > 0 {
|
||||
var dvCopy xlsxDataValidation
|
||||
deepcopy.Copy(&dvCopy, *dv)
|
||||
dvCopy.Sqref = strings.Join(SQRef, " ")
|
||||
dvs = append(dvs, &dvCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dvs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// duplicateMergeCells merge cells in the destination row if there are single
|
||||
// row merged cells in the copied row.
|
||||
func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 int) error {
|
||||
func (f *File) duplicateMergeCells(ws *xlsxWorksheet, sheet string, row, row2 int) error {
|
||||
if ws.MergeCells == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -703,7 +807,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
|
|||
// checkRow provides a function to check and fill each column element for all
|
||||
// rows and make that is continuous in a worksheet of XML. For example:
|
||||
//
|
||||
// <row r="15" spans="1:22" x14ac:dyDescent="0.2">
|
||||
// <row r="15">
|
||||
// <c r="A15" s="2" />
|
||||
// <c r="B15" s="2" />
|
||||
// <c r="F15" s="1" />
|
||||
|
@ -712,7 +816,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
|
|||
//
|
||||
// in this case, we should to change it to
|
||||
//
|
||||
// <row r="15" spans="1:22" x14ac:dyDescent="0.2">
|
||||
// <row r="15">
|
||||
// <c r="A15" s="2" />
|
||||
// <c r="B15" s="2" />
|
||||
// <c r="C15" s="2" />
|
||||
|
@ -724,7 +828,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
|
|||
//
|
||||
// Notice: this method could be very slow for large spreadsheets (more than
|
||||
// 3000 rows one sheet).
|
||||
func checkRow(ws *xlsxWorksheet) error {
|
||||
func (ws *xlsxWorksheet) checkRow() error {
|
||||
for rowIdx := range ws.SheetData.Row {
|
||||
rowData := &ws.SheetData.Row[rowIdx]
|
||||
|
||||
|
@ -814,8 +918,8 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
|
||||
return newInvalidStyleID(styleID)
|
||||
}
|
||||
|
@ -823,7 +927,7 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prepareSheetXML(ws, 0, end)
|
||||
ws.prepareSheetXML(0, end)
|
||||
for row := start - 1; row < end; row++ {
|
||||
ws.SheetData.Row[row].S = styleID
|
||||
ws.SheetData.Row[row].CustomFormat = true
|
||||
|
@ -840,10 +944,8 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
|
|||
// cell from user's units to pixels. If the height hasn't been set by the user
|
||||
// we use the default value. If the row is hidden it has a value of zero.
|
||||
func convertRowHeightToPixels(height float64) float64 {
|
||||
var pixels float64
|
||||
if height == 0 {
|
||||
return pixels
|
||||
return 0
|
||||
}
|
||||
pixels = math.Ceil(4.0 / 3.0 * height)
|
||||
return pixels
|
||||
return math.Ceil(4.0 / 3.4 * height)
|
||||
}
|
||||
|
|
224
rows_test.go
224
rows_test.go
|
@ -11,6 +11,16 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetRows(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1"))
|
||||
// Test get rows with unsupported charset shared strings table
|
||||
f.SharedStrings = nil
|
||||
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
|
||||
_, err := f.GetRows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRows(t *testing.T) {
|
||||
const sheet2 = "Sheet2"
|
||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
|
@ -229,7 +239,7 @@ func TestColumns(t *testing.T) {
|
|||
rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`<worksheet><sheetData><row r="1"><c r="A" t="s"><v>1</v></c></row></sheetData></worksheet>`)))
|
||||
assert.True(t, rows.Next())
|
||||
_, err = rows.Columns()
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
|
||||
// Test token is nil
|
||||
rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil))
|
||||
|
@ -295,7 +305,7 @@ func TestRemoveRow(t *testing.T) {
|
|||
colCount = 10
|
||||
rowCount = 10
|
||||
)
|
||||
fillCells(f, sheet1, colCount, rowCount)
|
||||
assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
|
||||
|
||||
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
|
||||
|
||||
|
@ -343,6 +353,15 @@ func TestRemoveRow(t *testing.T) {
|
|||
assert.NoError(t, f.RemoveRow(sheet1, 10))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx")))
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "A1", "C1"))
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "A2", "C2"))
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 1))
|
||||
mergedCells, err := f.GetMergeCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A1", mergedCells[0].GetStartAxis())
|
||||
assert.Equal(t, "C1", mergedCells[0].GetEndAxis())
|
||||
|
||||
// Test remove row on not exist worksheet
|
||||
assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist")
|
||||
// Test remove row with invalid sheet name
|
||||
|
@ -358,7 +377,7 @@ func TestInsertRows(t *testing.T) {
|
|||
colCount = 10
|
||||
rowCount = 10
|
||||
)
|
||||
fillCells(f, sheet1, colCount, rowCount)
|
||||
assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
|
||||
|
||||
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
|
||||
|
||||
|
@ -406,6 +425,23 @@ func TestInsertRowsInEmptyFile(t *testing.T) {
|
|||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx")))
|
||||
}
|
||||
|
||||
func prepareTestBook2() (*File, error) {
|
||||
f := NewFile()
|
||||
for cell, val := range map[string]string{
|
||||
"A1": "A1 Value",
|
||||
"A2": "A2 Value",
|
||||
"A3": "A3 Value",
|
||||
"B1": "B1 Value",
|
||||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
} {
|
||||
if err := f.SetCellStr("Sheet1", cell, val); err != nil {
|
||||
return f, err
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func TestDuplicateRowFromSingleRow(t *testing.T) {
|
||||
const sheet = "Sheet1"
|
||||
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
|
||||
|
@ -502,7 +538,6 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) {
|
|||
func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
|
||||
const sheet = "Sheet1"
|
||||
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
|
||||
|
||||
cells := map[string]string{
|
||||
"A1": "A1 Value",
|
||||
"A2": "A2 Value",
|
||||
|
@ -511,18 +546,9 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
|
|||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
}
|
||||
|
||||
newFileWithDefaults := func() *File {
|
||||
f := NewFile()
|
||||
for cell, val := range cells {
|
||||
assert.NoError(t, f.SetCellStr(sheet, cell, val))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("FirstOfMultipleRows", func(t *testing.T) {
|
||||
f := newFileWithDefaults()
|
||||
|
||||
f, err := prepareTestBook2()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DuplicateRow(sheet, 1))
|
||||
|
||||
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FirstOfMultipleRows"))) {
|
||||
|
@ -625,18 +651,9 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
|
|||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
}
|
||||
|
||||
newFileWithDefaults := func() *File {
|
||||
f := NewFile()
|
||||
for cell, val := range cells {
|
||||
assert.NoError(t, f.SetCellStr(sheet, cell, val))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) {
|
||||
f := newFileWithDefaults()
|
||||
|
||||
f, err := prepareTestBook2()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3))
|
||||
|
||||
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToMiddleOfData"))) {
|
||||
|
@ -661,7 +678,6 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
|
|||
func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
|
||||
const sheet = "Sheet1"
|
||||
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
|
||||
|
||||
cells := map[string]string{
|
||||
"A1": "A1 Value",
|
||||
"A2": "A2 Value",
|
||||
|
@ -670,18 +686,9 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
|
|||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
}
|
||||
|
||||
newFileWithDefaults := func() *File {
|
||||
f := NewFile()
|
||||
for cell, val := range cells {
|
||||
assert.NoError(t, f.SetCellStr(sheet, cell, val))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) {
|
||||
f := newFileWithDefaults()
|
||||
|
||||
f, err := prepareTestBook2()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7))
|
||||
|
||||
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToEmptyRows"))) {
|
||||
|
@ -706,7 +713,6 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
|
|||
func TestDuplicateRowInsertBefore(t *testing.T) {
|
||||
const sheet = "Sheet1"
|
||||
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
|
||||
|
||||
cells := map[string]string{
|
||||
"A1": "A1 Value",
|
||||
"A2": "A2 Value",
|
||||
|
@ -715,18 +721,9 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
|
|||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
}
|
||||
|
||||
newFileWithDefaults := func() *File {
|
||||
f := NewFile()
|
||||
for cell, val := range cells {
|
||||
assert.NoError(t, f.SetCellStr(sheet, cell, val))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("InsertBefore", func(t *testing.T) {
|
||||
f := newFileWithDefaults()
|
||||
|
||||
f, err := prepareTestBook2()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1))
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 10, 4))
|
||||
|
||||
|
@ -753,7 +750,6 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
|
|||
func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
|
||||
const sheet = "Sheet1"
|
||||
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
|
||||
|
||||
cells := map[string]string{
|
||||
"A1": "A1 Value",
|
||||
"A2": "A2 Value",
|
||||
|
@ -762,18 +758,9 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
|
|||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
}
|
||||
|
||||
newFileWithDefaults := func() *File {
|
||||
f := NewFile()
|
||||
for cell, val := range cells {
|
||||
assert.NoError(t, f.SetCellStr(sheet, cell, val))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
|
||||
f := newFileWithDefaults()
|
||||
|
||||
f, err := prepareTestBook2()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1))
|
||||
|
||||
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithLargeOffset"))) {
|
||||
|
@ -799,28 +786,11 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
|
|||
func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) {
|
||||
const sheet = "Sheet1"
|
||||
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
|
||||
|
||||
cells := map[string]string{
|
||||
"A1": "A1 Value",
|
||||
"A2": "A2 Value",
|
||||
"A3": "A3 Value",
|
||||
"B1": "B1 Value",
|
||||
"B2": "B2 Value",
|
||||
"B3": "B3 Value",
|
||||
}
|
||||
|
||||
newFileWithDefaults := func() *File {
|
||||
f := NewFile()
|
||||
for cell, val := range cells {
|
||||
assert.NoError(t, f.SetCellStr(sheet, cell, val))
|
||||
}
|
||||
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
|
||||
f, err := prepareTestBook2()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.MergeCell(sheet, "B2", "C2"))
|
||||
assert.NoError(t, f.MergeCell(sheet, "C6", "C8"))
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
|
||||
f := newFileWithDefaults()
|
||||
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1))
|
||||
assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8))
|
||||
|
@ -909,6 +879,60 @@ func TestDuplicateRow(t *testing.T) {
|
|||
f := NewFile()
|
||||
// Test duplicate row with invalid sheet name
|
||||
assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error())
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Amount",
|
||||
RefersTo: "Sheet1!$B$1",
|
||||
}))
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1"))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10"))
|
||||
|
||||
format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := []ConditionalFormatOptions{
|
||||
{Type: "cell", Criteria: "greater than", Format: &format, Value: "0"},
|
||||
}
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1", expected))
|
||||
|
||||
dv := NewDataValidation(true)
|
||||
dv.Sqref = "A1"
|
||||
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1"
|
||||
|
||||
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
|
||||
formula, err := f.GetCellFormula("Sheet1", "A10")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Amount+C10", formula)
|
||||
value, err := f.GetCellValue("Sheet1", "A11")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A10", value)
|
||||
|
||||
cfs, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cfs, 2)
|
||||
assert.Equal(t, expected, cfs["A10:A10"])
|
||||
|
||||
dvs, err := f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, dvs, 2)
|
||||
assert.Equal(t, "A10:A10", dvs[1].Sqref)
|
||||
|
||||
// Test duplicate data validation with row number exceeds maximum limit
|
||||
assert.Equal(t, ErrMaxRows, f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1))
|
||||
// Test duplicate data validation with invalid range reference
|
||||
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A"
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, 10))
|
||||
|
||||
// Test duplicate conditional formatting with row number exceeds maximum limit
|
||||
assert.Equal(t, ErrMaxRows, f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1))
|
||||
// Test duplicate conditional formatting with invalid range reference
|
||||
ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "A"
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, 10))
|
||||
}
|
||||
|
||||
func TestDuplicateRowTo(t *testing.T) {
|
||||
|
@ -935,9 +959,9 @@ func TestDuplicateMergeCells(t *testing.T) {
|
|||
ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{
|
||||
Cells: []*xlsxMergeCell{{Ref: "A1:-"}},
|
||||
}}
|
||||
assert.EqualError(t, f.duplicateMergeCells("Sheet1", ws, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
|
||||
assert.EqualError(t, f.duplicateMergeCells(ws, "Sheet1", 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
|
||||
ws.MergeCells.Cells[0].Ref = "A1:B1"
|
||||
assert.EqualError(t, f.duplicateMergeCells("SheetN", ws, 1, 2), "sheet SheetN does not exist")
|
||||
assert.EqualError(t, f.duplicateMergeCells(ws, "SheetN", 1, 2), "sheet SheetN does not exist")
|
||||
}
|
||||
|
||||
func TestGetValueFromInlineStr(t *testing.T) {
|
||||
|
@ -971,8 +995,7 @@ func TestGetValueFromNumber(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestErrSheetNotExistError(t *testing.T) {
|
||||
err := ErrSheetNotExist{SheetName: "Sheet1"}
|
||||
assert.EqualValues(t, err.Error(), "sheet Sheet1 does not exist")
|
||||
assert.Equal(t, "sheet Sheet1 does not exist", ErrSheetNotExist{"Sheet1"}.Error())
|
||||
}
|
||||
|
||||
func TestCheckRow(t *testing.T) {
|
||||
|
@ -984,7 +1007,7 @@ func TestCheckRow(t *testing.T) {
|
|||
f = NewFile()
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="-"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
delete(f.checked, "xl/worksheets/sheet1.xml")
|
||||
f.checked.Delete("xl/worksheets/sheet1.xml")
|
||||
assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error())
|
||||
}
|
||||
|
||||
|
@ -1020,6 +1043,22 @@ func TestSetRowStyle(t *testing.T) {
|
|||
assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestSetRowHeight(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test hidden row by set row height to 0
|
||||
assert.NoError(t, f.SetRowHeight("Sheet1", 2, 0))
|
||||
ht, err := f.GetRowHeight("Sheet1", 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, ht)
|
||||
// Test unset custom row height
|
||||
assert.NoError(t, f.SetRowHeight("Sheet1", 2, -1))
|
||||
ht, err = f.GetRowHeight("Sheet1", 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, defaultRowHeight, ht)
|
||||
// Test set row height with invalid height value
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetRowHeight("Sheet1", 2, -2))
|
||||
}
|
||||
|
||||
func TestNumberFormats(t *testing.T) {
|
||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
if !assert.NoError(t, err) {
|
||||
|
@ -1104,9 +1143,18 @@ func TestNumberFormats(t *testing.T) {
|
|||
assert.NoError(t, f.SetCellValue("Sheet1", cell, value))
|
||||
result, err := f.GetCellValue("Sheet1", cell)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Equal(t, expected, result, cell)
|
||||
}
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx")))
|
||||
|
||||
f = NewFile(Options{ShortDatePattern: "yyyy/m/d"})
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519))
|
||||
numFmt14, err := f.NewStyle(&Style{NumFmt: 14})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", numFmt14))
|
||||
result, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2019/3/19", result, "A1")
|
||||
}
|
||||
|
||||
func BenchmarkRows(b *testing.B) {
|
||||
|
|
65
shape.go
65
shape.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -22,6 +22,9 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
|
|||
if opts == nil {
|
||||
return nil, ErrParameterInvalid
|
||||
}
|
||||
if opts.Type == "" {
|
||||
return nil, ErrParameterInvalid
|
||||
}
|
||||
if opts.Width == 0 {
|
||||
opts.Width = defaultShapeSize
|
||||
}
|
||||
|
@ -35,10 +38,10 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
|
|||
opts.Format.Locked = boolPtr(false)
|
||||
}
|
||||
if opts.Format.ScaleX == 0 {
|
||||
opts.Format.ScaleX = defaultPictureScale
|
||||
opts.Format.ScaleX = defaultDrawingScale
|
||||
}
|
||||
if opts.Format.ScaleY == 0 {
|
||||
opts.Format.ScaleY = defaultPictureScale
|
||||
opts.Format.ScaleY = defaultDrawingScale
|
||||
}
|
||||
if opts.Line.Width == nil {
|
||||
opts.Line.Width = float64Ptr(defaultShapeLineWidth)
|
||||
|
@ -47,13 +50,13 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
|
|||
}
|
||||
|
||||
// AddShape provides the method to add shape in a sheet by given worksheet
|
||||
// index, shape format set (such as offset, scale, aspect ratio setting and
|
||||
// print settings) and properties set. For example, add text box (rect shape)
|
||||
// in Sheet1:
|
||||
// name and shape format set (such as offset, scale, aspect ratio setting and
|
||||
// print settings). For example, add text box (rect shape) in Sheet1:
|
||||
//
|
||||
// lineWidth := 1.2
|
||||
// err := f.AddShape("Sheet1", "G6",
|
||||
// err := f.AddShape("Sheet1",
|
||||
// &excelize.Shape{
|
||||
// Cell: "G6",
|
||||
// Type: "rect",
|
||||
// Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth},
|
||||
// Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1},
|
||||
|
@ -285,12 +288,12 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
|
|||
// wavy
|
||||
// wavyHeavy
|
||||
// wavyDbl
|
||||
func (f *File) AddShape(sheet, cell string, opts *Shape) error {
|
||||
func (f *File) AddShape(sheet string, opts *Shape) error {
|
||||
options, err := parseShapeOptions(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Read sheet data.
|
||||
// Read sheet data
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -313,38 +316,33 @@ func (f *File) AddShape(sheet, cell string, opts *Shape) error {
|
|||
f.addSheetDrawing(sheet, rID)
|
||||
f.addSheetNameSpace(sheet, SourceRelationship)
|
||||
}
|
||||
if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil {
|
||||
if err = f.addDrawingShape(sheet, drawingXML, opts.Cell, options); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.addContentTypePart(drawingID, "drawings")
|
||||
}
|
||||
|
||||
// addDrawingShape provides a function to add preset geometry by given sheet,
|
||||
// drawingXMLand format sets.
|
||||
func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error {
|
||||
// twoCellAnchorShape create a two cell anchor shape size placeholder for a
|
||||
// group, a shape, or a drawing element.
|
||||
func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, width, height uint, format GraphicOptions) (*xlsxWsDr, *xdrCellAnchor, int, error) {
|
||||
fromCol, fromRow, err := CellNameToCoordinates(cell)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
colIdx := fromCol - 1
|
||||
rowIdx := fromRow - 1
|
||||
|
||||
width := int(float64(opts.Width) * opts.Format.ScaleX)
|
||||
height := int(float64(opts.Height) * opts.Format.ScaleY)
|
||||
|
||||
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY,
|
||||
width, height)
|
||||
w := int(float64(width) * format.ScaleX)
|
||||
h := int(float64(height) * format.ScaleY)
|
||||
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, format.OffsetX, format.OffsetY, w, h)
|
||||
content, cNvPrID, err := f.drawingParser(drawingXML)
|
||||
if err != nil {
|
||||
return err
|
||||
return content, nil, cNvPrID, err
|
||||
}
|
||||
twoCellAnchor := xdrCellAnchor{}
|
||||
twoCellAnchor.EditAs = opts.Format.Positioning
|
||||
twoCellAnchor.EditAs = format.Positioning
|
||||
from := xlsxFrom{}
|
||||
from.Col = colStart
|
||||
from.ColOff = opts.Format.OffsetX * EMU
|
||||
from.ColOff = format.OffsetX * EMU
|
||||
from.Row = rowStart
|
||||
from.RowOff = opts.Format.OffsetY * EMU
|
||||
from.RowOff = format.OffsetY * EMU
|
||||
to := xlsxTo{}
|
||||
to.Col = colEnd
|
||||
to.ColOff = x2 * EMU
|
||||
|
@ -352,6 +350,17 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro
|
|||
to.RowOff = y2 * EMU
|
||||
twoCellAnchor.From = &from
|
||||
twoCellAnchor.To = &to
|
||||
return content, &twoCellAnchor, cNvPrID, err
|
||||
}
|
||||
|
||||
// addDrawingShape provides a function to add preset geometry by given sheet,
|
||||
// drawingXML and format sets.
|
||||
func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error {
|
||||
content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(
|
||||
sheet, drawingXML, cell, opts.Width, opts.Height, opts.Format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var solidColor string
|
||||
if len(opts.Fill.Color) == 1 {
|
||||
solidColor = opts.Fill.Color[0]
|
||||
|
@ -462,7 +471,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro
|
|||
FLocksWithSheet: *opts.Format.Locked,
|
||||
FPrintsWithSheet: *opts.Format.PrintObject,
|
||||
}
|
||||
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
|
||||
content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor)
|
||||
f.Drawings.Store(drawingXML, content)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -12,18 +12,19 @@ func TestAddShape(t *testing.T) {
|
|||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
shape := &Shape{
|
||||
assert.NoError(t, f.AddShape("Sheet1", &Shape{
|
||||
Cell: "A30",
|
||||
Type: "rect",
|
||||
Paragraph: []RichTextRun{
|
||||
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
|
||||
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, f.AddShape("Sheet1", "A30", shape))
|
||||
assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}))
|
||||
assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"}))
|
||||
assert.EqualError(t, f.AddShape("Sheet3", "H1",
|
||||
}))
|
||||
assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}))
|
||||
assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "C30", Type: "rect"}))
|
||||
assert.EqualError(t, f.AddShape("Sheet3",
|
||||
&Shape{
|
||||
Cell: "H1",
|
||||
Type: "ellipseRibbon",
|
||||
Line: ShapeLine{Color: "4286F4"},
|
||||
Fill: Fill{Color: []string{"8EB9FF"}},
|
||||
|
@ -41,15 +42,24 @@ func TestAddShape(t *testing.T) {
|
|||
},
|
||||
},
|
||||
), "sheet Sheet3 does not exist")
|
||||
assert.EqualError(t, f.AddShape("Sheet3", "H1", nil), ErrParameterInvalid.Error())
|
||||
assert.EqualError(t, f.AddShape("Sheet1", "A", shape), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet3", nil))
|
||||
assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet1", &Shape{Cell: "A1"}))
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddShape("Sheet1", &Shape{
|
||||
Cell: "A",
|
||||
Type: "rect",
|
||||
Paragraph: []RichTextRun{
|
||||
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
|
||||
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))
|
||||
|
||||
// Test add first shape for given sheet
|
||||
f = NewFile()
|
||||
lineWidth := 1.2
|
||||
assert.NoError(t, f.AddShape("Sheet1", "A1",
|
||||
assert.NoError(t, f.AddShape("Sheet1",
|
||||
&Shape{
|
||||
Cell: "A1",
|
||||
Type: "ellipseRibbon",
|
||||
Line: ShapeLine{Color: "4286F4", Width: &lineWidth},
|
||||
Fill: Fill{Color: []string{"8EB9FF"}},
|
||||
|
@ -69,16 +79,23 @@ func TestAddShape(t *testing.T) {
|
|||
}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
|
||||
// Test add shape with invalid sheet name
|
||||
assert.EqualError(t, f.AddShape("Sheet:1", "A30", shape), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.AddShape("Sheet:1", &Shape{
|
||||
Cell: "A30",
|
||||
Type: "rect",
|
||||
Paragraph: []RichTextRun{
|
||||
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
|
||||
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
|
||||
},
|
||||
}))
|
||||
// Test add shape with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test add shape with unsupported charset content types
|
||||
f = NewFile()
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestAddDrawingShape(t *testing.T) {
|
||||
|
|
323
sheet.go
323
sheet.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -27,7 +27,7 @@ import (
|
|||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/tiendc/go-deepcopy"
|
||||
)
|
||||
|
||||
// NewSheet provides the function to create a new sheet by given a worksheet
|
||||
|
@ -70,8 +70,8 @@ func (f *File) NewSheet(sheet string) (int, error) {
|
|||
func (f *File) contentTypesReader() (*xlsxTypes, error) {
|
||||
if f.ContentTypes == nil {
|
||||
f.ContentTypes = new(xlsxTypes)
|
||||
f.ContentTypes.Lock()
|
||||
defer f.ContentTypes.Unlock()
|
||||
f.ContentTypes.mu.Lock()
|
||||
defer f.ContentTypes.mu.Unlock()
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))).
|
||||
Decode(f.ContentTypes); err != nil && err != io.EOF {
|
||||
return f.ContentTypes, err
|
||||
|
@ -124,7 +124,8 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) {
|
|||
Width: ws.Cols.Col[i-1].Width,
|
||||
}, ws.Cols.Col[i]); i++ {
|
||||
}
|
||||
column := deepcopy.Copy(ws.Cols.Col[left]).(xlsxCol)
|
||||
var column xlsxCol
|
||||
deepcopy.Copy(&column, ws.Cols.Col[left])
|
||||
if left < i-1 {
|
||||
column.Max = ws.Cols.Col[i-1].Min
|
||||
}
|
||||
|
@ -164,10 +165,10 @@ func (f *File) workSheetWriter() {
|
|||
// reusing buffer
|
||||
_ = encoder.Encode(sheet)
|
||||
f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes())))
|
||||
ok := f.checked[p.(string)]
|
||||
_, ok := f.checked.Load(p.(string))
|
||||
if ok {
|
||||
f.Sheet.Delete(p.(string))
|
||||
f.checked[p.(string)] = false
|
||||
f.checked.Delete(p.(string))
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
|
@ -179,35 +180,38 @@ func (f *File) workSheetWriter() {
|
|||
func trimRow(sheetData *xlsxSheetData) []xlsxRow {
|
||||
var (
|
||||
row xlsxRow
|
||||
rows []xlsxRow
|
||||
i int
|
||||
)
|
||||
for k, v := range sheetData.Row {
|
||||
|
||||
for k := range sheetData.Row {
|
||||
row = sheetData.Row[k]
|
||||
if row.C = trimCell(v.C); len(row.C) != 0 || row.hasAttr() {
|
||||
rows = append(rows, row)
|
||||
if row = trimCell(row); len(row.C) != 0 || row.hasAttr() {
|
||||
sheetData.Row[i] = row
|
||||
}
|
||||
i++
|
||||
}
|
||||
return rows
|
||||
return sheetData.Row[:i]
|
||||
}
|
||||
|
||||
// trimCell provides a function to trim blank cells which created by fillColumns.
|
||||
func trimCell(column []xlsxC) []xlsxC {
|
||||
func trimCell(row xlsxRow) xlsxRow {
|
||||
column := row.C
|
||||
rowFull := true
|
||||
for i := range column {
|
||||
rowFull = column[i].hasValue() && rowFull
|
||||
}
|
||||
if rowFull {
|
||||
return column
|
||||
return row
|
||||
}
|
||||
col := make([]xlsxC, len(column))
|
||||
i := 0
|
||||
for _, c := range column {
|
||||
if c.hasValue() {
|
||||
col[i] = c
|
||||
row.C[i] = c
|
||||
i++
|
||||
}
|
||||
}
|
||||
return col[:i]
|
||||
row.C = row.C[:i]
|
||||
return row
|
||||
}
|
||||
|
||||
// setContentTypes provides a function to read and update property of contents
|
||||
|
@ -217,8 +221,8 @@ func (f *File) setContentTypes(partName, contentType string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
content.Overrides = append(content.Overrides, xlsxOverride{
|
||||
PartName: partName,
|
||||
ContentType: contentType,
|
||||
|
@ -237,7 +241,7 @@ func (f *File) setSheet(index int, name string) {
|
|||
sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
|
||||
f.sheetMap[name] = sheetXMLPath
|
||||
f.Sheet.Store(sheetXMLPath, &ws)
|
||||
f.xmlAttr[sheetXMLPath] = []xml.Attr{NameSpaceSpreadSheet}
|
||||
f.xmlAttr.Store(sheetXMLPath, []xml.Attr{NameSpaceSpreadSheet})
|
||||
}
|
||||
|
||||
// relsWriter provides a function to save relationships after
|
||||
|
@ -360,7 +364,7 @@ func (f *File) SetSheetName(source, target string) error {
|
|||
if err = checkSheetName(target); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.EqualFold(target, source) {
|
||||
if target == source {
|
||||
return err
|
||||
}
|
||||
wb, _ := f.workbookReader()
|
||||
|
@ -371,6 +375,12 @@ func (f *File) SetSheetName(source, target string) error {
|
|||
delete(f.sheetMap, source)
|
||||
}
|
||||
}
|
||||
if wb.DefinedNames == nil {
|
||||
return err
|
||||
}
|
||||
for i, dn := range wb.DefinedNames.DefinedName {
|
||||
wb.DefinedNames.DefinedName[i].Data = adjustRangeSheetName(dn.Data, source, target)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -576,14 +586,14 @@ func (f *File) DeleteSheet(sheet string) error {
|
|||
}
|
||||
}
|
||||
target := f.deleteSheetFromWorkbookRels(v.ID)
|
||||
_ = f.deleteSheetFromContentTypes(target)
|
||||
_ = f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, target)
|
||||
_ = f.deleteCalcChain(f.getSheetID(sheet), "")
|
||||
delete(f.sheetMap, v.Name)
|
||||
f.Pkg.Delete(sheetXML)
|
||||
f.Pkg.Delete(rels)
|
||||
f.Relationships.Delete(rels)
|
||||
f.Sheet.Delete(sheetXML)
|
||||
delete(f.xmlAttr, sheetXML)
|
||||
f.xmlAttr.Delete(sheetXML)
|
||||
f.SheetCount--
|
||||
}
|
||||
index, err := f.GetSheetIndex(activeSheetName)
|
||||
|
@ -591,6 +601,49 @@ func (f *File) DeleteSheet(sheet string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// MoveSheet moves a sheet to a specified position in the workbook. The function
|
||||
// moves the source sheet before the target sheet. After moving, other sheets
|
||||
// will be shifted to the left or right. If the sheet is already at the target
|
||||
// position, the function will not perform any action. Not that this function
|
||||
// will be ungroup all sheets after moving. For example, move Sheet2 before
|
||||
// Sheet1:
|
||||
//
|
||||
// err := f.MoveSheet("Sheet2", "Sheet1")
|
||||
func (f *File) MoveSheet(source, target string) error {
|
||||
if strings.EqualFold(source, target) {
|
||||
return nil
|
||||
}
|
||||
wb, err := f.workbookReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceIdx, err := f.GetSheetIndex(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetIdx, err := f.GetSheetIndex(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sourceIdx < 0 {
|
||||
return ErrSheetNotExist{source}
|
||||
}
|
||||
if targetIdx < 0 {
|
||||
return ErrSheetNotExist{target}
|
||||
}
|
||||
_ = f.UngroupSheets()
|
||||
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
|
||||
sourceSheet := wb.Sheets.Sheet[sourceIdx]
|
||||
wb.Sheets.Sheet = append(wb.Sheets.Sheet[:sourceIdx], wb.Sheets.Sheet[sourceIdx+1:]...)
|
||||
if targetIdx > sourceIdx {
|
||||
targetIdx--
|
||||
}
|
||||
wb.Sheets.Sheet = append(wb.Sheets.Sheet[:targetIdx], append([]xlsxSheet{sourceSheet}, wb.Sheets.Sheet[targetIdx:]...)...)
|
||||
activeSheetIdx, _ := f.GetSheetIndex(activeSheetName)
|
||||
f.SetActiveSheet(activeSheetIdx)
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteAndAdjustDefinedNames delete and adjust defined name in the workbook
|
||||
// by given worksheet ID.
|
||||
func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) {
|
||||
|
@ -615,8 +668,8 @@ func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) {
|
|||
// relationships by given relationships ID in the file workbook.xml.rels.
|
||||
func (f *File) deleteSheetFromWorkbookRels(rID string) string {
|
||||
rels, _ := f.relsReader(f.getWorkbookRelsPath())
|
||||
rels.Lock()
|
||||
defer rels.Unlock()
|
||||
rels.mu.Lock()
|
||||
defer rels.mu.Unlock()
|
||||
for k, v := range rels.Relationships {
|
||||
if v.ID == rID {
|
||||
rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
|
||||
|
@ -626,24 +679,50 @@ func (f *File) deleteSheetFromWorkbookRels(rID string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// deleteSheetFromContentTypes provides a function to remove worksheet
|
||||
// relationships by given target name in the file [Content_Types].xml.
|
||||
func (f *File) deleteSheetFromContentTypes(target string) error {
|
||||
if !strings.HasPrefix(target, "/") {
|
||||
target = "/xl/" + target
|
||||
// deleteSheetRelationships provides a function to delete relationships in
|
||||
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
||||
// relationship index.
|
||||
func (f *File) deleteSheetRelationships(sheet, rID string) {
|
||||
name, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
sheetRels, _ := f.relsReader(rels)
|
||||
if sheetRels == nil {
|
||||
sheetRels = &xlsxRelationships{}
|
||||
}
|
||||
content.Lock()
|
||||
defer content.Unlock()
|
||||
for k, v := range content.Overrides {
|
||||
if v.PartName == target {
|
||||
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
|
||||
sheetRels.mu.Lock()
|
||||
defer sheetRels.mu.Unlock()
|
||||
for k, v := range sheetRels.Relationships {
|
||||
if v.ID == rID {
|
||||
sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
|
||||
}
|
||||
}
|
||||
return err
|
||||
f.Relationships.Store(rels, sheetRels)
|
||||
}
|
||||
|
||||
// getSheetRelationshipsTargetByID provides a function to get Target attribute
|
||||
// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
||||
// relationship index.
|
||||
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
|
||||
name, ok := f.getSheetXMLPath(sheet)
|
||||
if !ok {
|
||||
name = strings.ToLower(sheet) + ".xml"
|
||||
}
|
||||
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
||||
sheetRels, _ := f.relsReader(rels)
|
||||
if sheetRels == nil {
|
||||
sheetRels = &xlsxRelationships{}
|
||||
}
|
||||
sheetRels.mu.Lock()
|
||||
defer sheetRels.mu.Unlock()
|
||||
for _, v := range sheetRels.Relationships {
|
||||
if v.ID == rID {
|
||||
return v.Target
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CopySheet provides a function to duplicate a worksheet by gave source and
|
||||
|
@ -672,7 +751,8 @@ func (f *File) copySheet(from, to int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet)
|
||||
worksheet := &xlsxWorksheet{}
|
||||
deepcopy.Copy(worksheet, sheet)
|
||||
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
|
||||
sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml"
|
||||
if len(worksheet.SheetViews.SheetView) > 0 {
|
||||
|
@ -688,8 +768,8 @@ func (f *File) copySheet(from, to int) error {
|
|||
f.Pkg.Store(toRels, rels.([]byte))
|
||||
}
|
||||
fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet)
|
||||
fromSheetAttr := f.xmlAttr[fromSheetXMLPath]
|
||||
f.xmlAttr[sheetXMLPath] = fromSheetAttr
|
||||
fromSheetAttr, _ := f.xmlAttr.Load(fromSheetXMLPath)
|
||||
f.xmlAttr.Store(sheetXMLPath, fromSheetAttr)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -738,6 +818,11 @@ func (f *File) SetSheetVisible(sheet string, visible bool, veryHidden ...bool) e
|
|||
return err
|
||||
}
|
||||
tabSelected := false
|
||||
if ws.SheetViews == nil {
|
||||
ws.SheetViews = &xlsxSheetViews{
|
||||
SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
|
||||
}
|
||||
}
|
||||
if len(ws.SheetViews.SheetView) > 0 {
|
||||
tabSelected = ws.SheetViews.SheetView[0].TabSelected
|
||||
}
|
||||
|
@ -772,7 +857,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
|
|||
}
|
||||
}
|
||||
var s []*xlsxSelection
|
||||
for _, p := range panes.Panes {
|
||||
for _, p := range panes.Selection {
|
||||
s = append(s, &xlsxSelection{
|
||||
ActiveCell: p.ActiveCell,
|
||||
Pane: p.Pane,
|
||||
|
@ -859,7 +944,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
|
|||
// YSplit: 0,
|
||||
// TopLeftCell: "B1",
|
||||
// ActivePane: "topRight",
|
||||
// Panes: []excelize.PaneOptions{
|
||||
// Selection: []excelize.Selection{
|
||||
// {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
|
||||
// },
|
||||
// })
|
||||
|
@ -874,7 +959,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
|
|||
// YSplit: 9,
|
||||
// TopLeftCell: "A34",
|
||||
// ActivePane: "bottomLeft",
|
||||
// Panes: []excelize.PaneOptions{
|
||||
// Selection: []excelize.Selection{
|
||||
// {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"},
|
||||
// },
|
||||
// })
|
||||
|
@ -889,7 +974,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
|
|||
// YSplit: 1800,
|
||||
// TopLeftCell: "N57",
|
||||
// ActivePane: "bottomLeft",
|
||||
// Panes: []excelize.PaneOptions{
|
||||
// Selection: []excelize.Selection{
|
||||
// {SQRef: "I36", ActiveCell: "I36"},
|
||||
// {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"},
|
||||
// {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"},
|
||||
|
@ -908,6 +993,50 @@ func (f *File) SetPanes(sheet string, panes *Panes) error {
|
|||
return ws.setPanes(panes)
|
||||
}
|
||||
|
||||
// getPanes returns freeze panes, split panes, and views of the worksheet.
|
||||
func (ws *xlsxWorksheet) getPanes() Panes {
|
||||
var (
|
||||
panes Panes
|
||||
section []Selection
|
||||
)
|
||||
if ws.SheetViews == nil || len(ws.SheetViews.SheetView) < 1 {
|
||||
return panes
|
||||
}
|
||||
sw := ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1]
|
||||
for _, s := range sw.Selection {
|
||||
if s != nil {
|
||||
section = append(section, Selection{
|
||||
SQRef: s.SQRef,
|
||||
ActiveCell: s.ActiveCell,
|
||||
Pane: s.Pane,
|
||||
})
|
||||
}
|
||||
}
|
||||
panes.Selection = section
|
||||
if sw.Pane == nil {
|
||||
return panes
|
||||
}
|
||||
panes.ActivePane = sw.Pane.ActivePane
|
||||
if sw.Pane.State == "frozen" {
|
||||
panes.Freeze = true
|
||||
}
|
||||
panes.TopLeftCell = sw.Pane.TopLeftCell
|
||||
panes.XSplit = int(sw.Pane.XSplit)
|
||||
panes.YSplit = int(sw.Pane.YSplit)
|
||||
return panes
|
||||
}
|
||||
|
||||
// GetPanes provides a function to get freeze panes, split panes, and worksheet
|
||||
// views by given worksheet name.
|
||||
func (f *File) GetPanes(sheet string) (Panes, error) {
|
||||
var panes Panes
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return panes, err
|
||||
}
|
||||
return ws.getPanes(), err
|
||||
}
|
||||
|
||||
// GetSheetVisible provides a function to get worksheet visible by given worksheet
|
||||
// name. For example, get visible state of Sheet1:
|
||||
//
|
||||
|
@ -977,6 +1106,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string,
|
|||
if sst, err = f.sharedStringsReader(); err != nil {
|
||||
return
|
||||
}
|
||||
regex := regexp.MustCompile(value)
|
||||
decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name)))
|
||||
for {
|
||||
var token xml.Token
|
||||
|
@ -1001,7 +1131,6 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string,
|
|||
_ = decoder.DecodeElement(&colCell, &xmlElement)
|
||||
val, _ := colCell.getValueFrom(f, sst, false)
|
||||
if regSearch {
|
||||
regex := regexp.MustCompile(value)
|
||||
if !regex.MatchString(val) {
|
||||
continue
|
||||
}
|
||||
|
@ -1079,8 +1208,8 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
|
|||
// DifferentFirst | Different first-page header and footer indicator
|
||||
// DifferentOddEven | Different odd and even page headers and footers indicator
|
||||
// ScaleWithDoc | Scale header and footer with document scaling
|
||||
// OddFooter | Odd Page Footer
|
||||
// OddHeader | Odd Header
|
||||
// OddFooter | Odd Page Footer, or primary Page Footer if 'DifferentOddEven' is 'false'
|
||||
// OddHeader | Odd Header, or primary Page Header if 'DifferentOddEven' is 'false'
|
||||
// EvenFooter | Even Page Footer
|
||||
// EvenHeader | Even Page Header
|
||||
// FirstFooter | First Page Footer
|
||||
|
@ -1112,7 +1241,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
|
|||
// |
|
||||
// &F | Current workbook's file name
|
||||
// |
|
||||
// &G | Drawing object as background (Not support currently)
|
||||
// &G | Drawing object as background (Use AddHeaderFooterImage)
|
||||
// |
|
||||
// &H | Shadow text format
|
||||
// |
|
||||
|
@ -1216,6 +1345,32 @@ func (f *File) SetHeaderFooter(sheet string, opts *HeaderFooterOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// GetHeaderFooter provides a function to get worksheet header and footer by
|
||||
// given worksheet name.
|
||||
func (f *File) GetHeaderFooter(sheet string) (*HeaderFooterOptions, error) {
|
||||
var opts *HeaderFooterOptions
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
if ws.HeaderFooter == nil {
|
||||
return opts, err
|
||||
}
|
||||
opts = &HeaderFooterOptions{
|
||||
AlignWithMargins: ws.HeaderFooter.AlignWithMargins,
|
||||
DifferentFirst: ws.HeaderFooter.DifferentFirst,
|
||||
DifferentOddEven: ws.HeaderFooter.DifferentOddEven,
|
||||
ScaleWithDoc: ws.HeaderFooter.ScaleWithDoc,
|
||||
OddHeader: ws.HeaderFooter.OddHeader,
|
||||
OddFooter: ws.HeaderFooter.OddFooter,
|
||||
EvenHeader: ws.HeaderFooter.EvenHeader,
|
||||
EvenFooter: ws.HeaderFooter.EvenFooter,
|
||||
FirstHeader: ws.HeaderFooter.FirstHeader,
|
||||
FirstFooter: ws.HeaderFooter.FirstFooter,
|
||||
}
|
||||
return opts, err
|
||||
}
|
||||
|
||||
// ProtectSheet provides a function to prevent other users from accidentally or
|
||||
// deliberately changing, moving, or deleting data in a worksheet. The
|
||||
// optional field AlgorithmName specified hash algorithm, support XOR, MD4,
|
||||
|
@ -1456,8 +1611,7 @@ func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error {
|
|||
if opts == nil {
|
||||
return err
|
||||
}
|
||||
ws.setPageSetUp(opts)
|
||||
return err
|
||||
return ws.setPageSetUp(opts)
|
||||
}
|
||||
|
||||
// newPageSetUp initialize page setup settings for the worksheet if which not
|
||||
|
@ -1469,12 +1623,15 @@ func (ws *xlsxWorksheet) newPageSetUp() {
|
|||
}
|
||||
|
||||
// setPageSetUp set page setup settings for the worksheet by given options.
|
||||
func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
|
||||
func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error {
|
||||
if opts.Size != nil {
|
||||
ws.newPageSetUp()
|
||||
ws.PageSetUp.PaperSize = opts.Size
|
||||
}
|
||||
if opts.Orientation != nil && (*opts.Orientation == "portrait" || *opts.Orientation == "landscape") {
|
||||
if opts.Orientation != nil {
|
||||
if inStrSlice(supportedPageOrientation, *opts.Orientation, true) == -1 {
|
||||
return newInvalidPageLayoutValueError("Orientation", *opts.Orientation, strings.Join(supportedPageOrientation, ", "))
|
||||
}
|
||||
ws.newPageSetUp()
|
||||
ws.PageSetUp.Orientation = *opts.Orientation
|
||||
}
|
||||
|
@ -1483,7 +1640,10 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
|
|||
ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber))
|
||||
ws.PageSetUp.UseFirstPageNumber = true
|
||||
}
|
||||
if opts.AdjustTo != nil && 10 <= *opts.AdjustTo && *opts.AdjustTo <= 400 {
|
||||
if opts.AdjustTo != nil {
|
||||
if *opts.AdjustTo < 10 || 400 < *opts.AdjustTo {
|
||||
return ErrPageSetupAdjustTo
|
||||
}
|
||||
ws.newPageSetUp()
|
||||
ws.PageSetUp.Scale = int(*opts.AdjustTo)
|
||||
}
|
||||
|
@ -1499,13 +1659,21 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
|
|||
ws.newPageSetUp()
|
||||
ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite
|
||||
}
|
||||
if opts.PageOrder != nil {
|
||||
if inStrSlice(supportedPageOrder, *opts.PageOrder, true) == -1 {
|
||||
return newInvalidPageLayoutValueError("PageOrder", *opts.PageOrder, strings.Join(supportedPageOrder, ", "))
|
||||
}
|
||||
ws.newPageSetUp()
|
||||
ws.PageSetUp.PageOrder = *opts.PageOrder
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageLayout provides a function to gets worksheet page layout.
|
||||
func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
|
||||
opts := PageLayoutOptions{
|
||||
Size: intPtr(0),
|
||||
Orientation: stringPtr("portrait"),
|
||||
Orientation: stringPtr(supportedPageOrientation[0]),
|
||||
FirstPageNumber: uintPtr(1),
|
||||
AdjustTo: uintPtr(100),
|
||||
}
|
||||
|
@ -1533,6 +1701,9 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
|
|||
opts.FitToWidth = ws.PageSetUp.FitToWidth
|
||||
}
|
||||
opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite)
|
||||
if ws.PageSetUp.PageOrder != "" {
|
||||
opts.PageOrder = stringPtr(ws.PageSetUp.PageOrder)
|
||||
}
|
||||
}
|
||||
return opts, err
|
||||
}
|
||||
|
@ -1547,10 +1718,31 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
|
|||
// Comment: "defined name comment",
|
||||
// Scope: "Sheet2",
|
||||
// })
|
||||
//
|
||||
// If you fill the RefersTo property with only one columns range without a
|
||||
// comma, it will work as "Columns to repeat at left" only. For example:
|
||||
//
|
||||
// err := f.SetDefinedName(&excelize.DefinedName{
|
||||
// Name: "_xlnm.Print_Titles",
|
||||
// RefersTo: "Sheet1!$A:$A",
|
||||
// Scope: "Sheet1",
|
||||
// })
|
||||
//
|
||||
// If you fill the RefersTo property with only one rows range without a comma,
|
||||
// it will work as "Rows to repeat at top" only. For example:
|
||||
//
|
||||
// err := f.SetDefinedName(&excelize.DefinedName{
|
||||
// Name: "_xlnm.Print_Titles",
|
||||
// RefersTo: "Sheet1!$1:$1",
|
||||
// Scope: "Sheet1",
|
||||
// })
|
||||
func (f *File) SetDefinedName(definedName *DefinedName) error {
|
||||
if definedName.Name == "" || definedName.RefersTo == "" {
|
||||
return ErrParameterInvalid
|
||||
}
|
||||
if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 {
|
||||
return err
|
||||
}
|
||||
wb, err := f.workbookReader()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1666,12 +1858,9 @@ func (f *File) GroupSheets(sheets []string) error {
|
|||
}
|
||||
for _, ws := range wss {
|
||||
sheetViews := ws.SheetViews.SheetView
|
||||
if len(sheetViews) > 0 {
|
||||
for idx := range sheetViews {
|
||||
ws.SheetViews.SheetView[idx].TabSelected = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1685,12 +1874,10 @@ func (f *File) UngroupSheets() error {
|
|||
}
|
||||
ws, _ := f.workSheetReader(sheet)
|
||||
sheetViews := ws.SheetViews.SheetView
|
||||
if len(sheetViews) > 0 {
|
||||
for idx := range sheetViews {
|
||||
ws.SheetViews.SheetView[idx].TabSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1817,7 +2004,7 @@ func (f *File) RemovePageBreak(sheet, cell string) error {
|
|||
}
|
||||
|
||||
// relsReader provides a function to get the pointer to the structure
|
||||
// after deserialization of xl/worksheets/_rels/sheet%d.xml.rels.
|
||||
// after deserialization of relationships parts.
|
||||
func (f *File) relsReader(path string) (*xlsxRelationships, error) {
|
||||
rels, _ := f.Relationships.Load(path)
|
||||
if rels == nil {
|
||||
|
@ -1839,9 +2026,7 @@ func (f *File) relsReader(path string) (*xlsxRelationships, error) {
|
|||
// fillSheetData ensures there are enough rows, and columns in the chosen
|
||||
// row to accept data. Missing rows are backfilled and given their row number
|
||||
// Uses the last populated row as a hint for the size of the next row to add
|
||||
func prepareSheetXML(ws *xlsxWorksheet, col int, row int) {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
func (ws *xlsxWorksheet) prepareSheetXML(col int, row int) {
|
||||
rowCount := len(ws.SheetData.Row)
|
||||
sizeHint := 0
|
||||
var ht *float64
|
||||
|
@ -1875,9 +2060,7 @@ func fillColumns(rowData *xlsxRow, col, row int) {
|
|||
}
|
||||
|
||||
// makeContiguousColumns make columns in specific rows as contiguous.
|
||||
func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
func (ws *xlsxWorksheet) makeContiguousColumns(fromRow, toRow, colCount int) {
|
||||
for ; fromRow < toRow; fromRow++ {
|
||||
rowData := &ws.SheetData.Row[fromRow-1]
|
||||
fillColumns(rowData, colCount, fromRow)
|
||||
|
@ -1915,7 +2098,7 @@ func (f *File) SetSheetDimension(sheet string, rangeRef string) error {
|
|||
return err
|
||||
}
|
||||
_ = sortCoordinates(coordinates)
|
||||
ref, err := f.coordinatesToRangeRef(coordinates)
|
||||
ref, err := coordinatesToRangeRef(coordinates)
|
||||
ws.Dimension = &xlsxDimension{Ref: ref}
|
||||
return err
|
||||
}
|
||||
|
|
173
sheet_test.go
173
sheet_test.go
|
@ -8,6 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -37,25 +38,29 @@ func TestNewSheet(t *testing.T) {
|
|||
assert.Equal(t, -1, sheetID)
|
||||
}
|
||||
|
||||
func TestSetPanes(t *testing.T) {
|
||||
func TestPanes(t *testing.T) {
|
||||
f := NewFile()
|
||||
|
||||
assert.NoError(t, f.SetPanes("Sheet1", &Panes{Freeze: false, Split: false}))
|
||||
_, err := f.NewSheet("Panes 2")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetPanes("Panes 2",
|
||||
&Panes{
|
||||
|
||||
expected := Panes{
|
||||
Freeze: true,
|
||||
Split: false,
|
||||
XSplit: 1,
|
||||
YSplit: 0,
|
||||
TopLeftCell: "B1",
|
||||
ActivePane: "topRight",
|
||||
Panes: []PaneOptions{
|
||||
Selection: []Selection{
|
||||
{SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
assert.NoError(t, f.SetPanes("Panes 2", &expected))
|
||||
panes, err := f.GetPanes("Panes 2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, panes)
|
||||
|
||||
_, err = f.NewSheet("Panes 3")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetPanes("Panes 3",
|
||||
|
@ -66,7 +71,7 @@ func TestSetPanes(t *testing.T) {
|
|||
YSplit: 1800,
|
||||
TopLeftCell: "N57",
|
||||
ActivePane: "bottomLeft",
|
||||
Panes: []PaneOptions{
|
||||
Selection: []Selection{
|
||||
{SQRef: "I36", ActiveCell: "I36"},
|
||||
{SQRef: "G33", ActiveCell: "G33", Pane: "topRight"},
|
||||
{SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"},
|
||||
|
@ -84,7 +89,7 @@ func TestSetPanes(t *testing.T) {
|
|||
YSplit: 9,
|
||||
TopLeftCell: "A34",
|
||||
ActivePane: "bottomLeft",
|
||||
Panes: []PaneOptions{
|
||||
Selection: []Selection{
|
||||
{SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"},
|
||||
},
|
||||
},
|
||||
|
@ -94,9 +99,29 @@ func TestSetPanes(t *testing.T) {
|
|||
// Test set panes with invalid sheet name
|
||||
assert.EqualError(t, f.SetPanes("Sheet:1", &Panes{Freeze: false, Split: false}), ErrSheetNameInvalid.Error())
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx")))
|
||||
|
||||
// Test get panes with empty sheet views
|
||||
f = NewFile()
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{}
|
||||
_, err = f.GetPanes("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
// Test get panes without panes
|
||||
ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}}
|
||||
_, err = f.GetPanes("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
// Test get panes without sheet views
|
||||
ws.(*xlsxWorksheet).SheetViews = nil
|
||||
_, err = f.GetPanes("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
// Test get panes on not exists worksheet
|
||||
_, err = f.GetPanes("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
|
||||
// Test add pane on empty sheet views worksheet
|
||||
f = NewFile()
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`))
|
||||
assert.NoError(t, f.SetPanes("Sheet1",
|
||||
|
@ -107,7 +132,7 @@ func TestSetPanes(t *testing.T) {
|
|||
YSplit: 0,
|
||||
TopLeftCell: "B1",
|
||||
ActivePane: "topRight",
|
||||
Panes: []PaneOptions{
|
||||
Selection: []Selection{
|
||||
{SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
|
||||
},
|
||||
},
|
||||
|
@ -149,19 +174,19 @@ func TestSearchSheet(t *testing.T) {
|
|||
f = NewFile()
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
result, err = f.SearchSheet("Sheet1", "A")
|
||||
assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
|
||||
assert.Equal(t, []string(nil), result)
|
||||
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`))
|
||||
result, err = f.SearchSheet("Sheet1", "A")
|
||||
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
assert.Equal(t, []string(nil), result)
|
||||
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="0"><c r="A1" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`))
|
||||
result, err = f.SearchSheet("Sheet1", "A")
|
||||
assert.EqualError(t, err, "invalid cell reference [1, 0]")
|
||||
assert.Equal(t, newCoordinatesToCellNameError(1, 0), err)
|
||||
assert.Equal(t, []string(nil), result)
|
||||
|
||||
// Test search sheet with unsupported charset shared strings table
|
||||
|
@ -185,6 +210,7 @@ func TestSetPageLayout(t *testing.T) {
|
|||
FitToHeight: intPtr(2),
|
||||
FitToWidth: intPtr(2),
|
||||
BlackAndWhite: boolPtr(true),
|
||||
PageOrder: stringPtr("overThenDown"),
|
||||
}
|
||||
assert.NoError(t, f.SetPageLayout("Sheet1", &expected))
|
||||
opts, err := f.GetPageLayout("Sheet1")
|
||||
|
@ -194,6 +220,16 @@ func TestSetPageLayout(t *testing.T) {
|
|||
assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist")
|
||||
// Test set page layout with invalid sheet name
|
||||
assert.EqualError(t, f.SetPageLayout("Sheet:1", nil), ErrSheetNameInvalid.Error())
|
||||
// Test set page layout with invalid parameters
|
||||
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
|
||||
AdjustTo: uintPtr(5),
|
||||
}), "adjust to value must be between 10 and 400")
|
||||
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
|
||||
Orientation: stringPtr("x"),
|
||||
}), "invalid Orientation value \"x\", acceptable value should be one of portrait, landscape")
|
||||
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
|
||||
PageOrder: stringPtr("x"),
|
||||
}), "invalid PageOrder value \"x\", acceptable value should be one of overThenDown, downThenOver")
|
||||
}
|
||||
|
||||
func TestGetPageLayout(t *testing.T) {
|
||||
|
@ -206,8 +242,16 @@ func TestGetPageLayout(t *testing.T) {
|
|||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
}
|
||||
|
||||
func TestSetHeaderFooter(t *testing.T) {
|
||||
func TestHeaderFooter(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test get header and footer with default header and footer settings
|
||||
opts, err := f.GetHeaderFooter("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, (*HeaderFooterOptions)(nil), opts)
|
||||
// Test get header and footer on not exists worksheet
|
||||
_, err = f.GetHeaderFooter("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
|
||||
assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter"))
|
||||
// Test set header and footer on not exists worksheet
|
||||
assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist")
|
||||
|
@ -227,7 +271,7 @@ func TestSetHeaderFooter(t *testing.T) {
|
|||
EvenFooter: text,
|
||||
FirstHeader: text,
|
||||
}))
|
||||
assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{
|
||||
expected := &HeaderFooterOptions{
|
||||
DifferentFirst: true,
|
||||
DifferentOddEven: true,
|
||||
OddHeader: "&R&P",
|
||||
|
@ -235,14 +279,18 @@ func TestSetHeaderFooter(t *testing.T) {
|
|||
EvenHeader: "&L&P",
|
||||
EvenFooter: "&L&D&R&T",
|
||||
FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`,
|
||||
}))
|
||||
}
|
||||
assert.NoError(t, f.SetHeaderFooter("Sheet1", expected))
|
||||
opts, err = f.GetHeaderFooter("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, opts)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx")))
|
||||
}
|
||||
|
||||
func TestDefinedName(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Amount",
|
||||
Name: "Amount.",
|
||||
RefersTo: "Sheet1!$A$2:$D$5",
|
||||
Comment: "defined name comment",
|
||||
Scope: "Sheet1",
|
||||
|
@ -252,6 +300,16 @@ func TestDefinedName(t *testing.T) {
|
|||
RefersTo: "Sheet1!$A$2:$D$5",
|
||||
Comment: "defined name comment",
|
||||
}))
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: builtInDefinedNames[0],
|
||||
RefersTo: "Sheet1!$A$1:$Z$100",
|
||||
Scope: "Sheet1",
|
||||
}))
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: builtInDefinedNames[1],
|
||||
RefersTo: "Sheet1!$A:$A,Sheet1!$1:$1",
|
||||
Scope: "Sheet1",
|
||||
}))
|
||||
assert.EqualError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Amount",
|
||||
RefersTo: "Sheet1!$A$2:$D$5",
|
||||
|
@ -273,7 +331,7 @@ func TestDefinedName(t *testing.T) {
|
|||
Name: "Amount",
|
||||
}))
|
||||
assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo)
|
||||
assert.Len(t, f.GetDefinedName(), 1)
|
||||
assert.Len(t, f.GetDefinedName(), 3)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx")))
|
||||
// Test set defined name with unsupported charset workbook
|
||||
f.WorkBook = nil
|
||||
|
@ -418,8 +476,30 @@ func TestSetSheetName(t *testing.T) {
|
|||
// Test set worksheet with the same name
|
||||
assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1"))
|
||||
assert.Equal(t, "Sheet1", f.GetSheetName(0))
|
||||
// Test set worksheet with the different name
|
||||
assert.NoError(t, f.SetSheetName("Sheet1", "sheet1"))
|
||||
assert.Equal(t, "sheet1", f.GetSheetName(0))
|
||||
// Test set sheet name with invalid sheet name
|
||||
assert.EqualError(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid)
|
||||
|
||||
// Test set worksheet name with existing defined name and auto filter
|
||||
assert.NoError(t, f.AutoFilter("Sheet1", "A1:A2", nil))
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Name1",
|
||||
RefersTo: "$B$2",
|
||||
}))
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Name2",
|
||||
RefersTo: "$A1$2:A2",
|
||||
}))
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Name3",
|
||||
RefersTo: "Sheet1!$A$1:'Sheet1'!A1:Sheet1!$A$1,Sheet1!A1:Sheet3!A1,Sheet3!A1",
|
||||
}))
|
||||
assert.NoError(t, f.SetSheetName("Sheet1", "Sheet2"))
|
||||
for i, expected := range []string{"'Sheet2'!$A$1:$A$2", "$B$2", "$A1$2:A2", "Sheet2!$A$1:'Sheet2'!A1:Sheet2!$A$1,Sheet2!A1:Sheet3!A1,Sheet3!A1"} {
|
||||
assert.Equal(t, expected, f.WorkBook.DefinedNames.DefinedName[i].Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorksheetWriter(t *testing.T) {
|
||||
|
@ -428,7 +508,7 @@ func TestWorksheetWriter(t *testing.T) {
|
|||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
worksheet := xml.Header + `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetData><row r="1"><c r="A1"><v>%d</v></c></row></sheetData><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" Requires="a14"><xdr:twoCellAnchor editAs="oneCell"></xdr:twoCellAnchor></mc:Choice><mc:Fallback/></mc:AlternateContent></worksheet>`
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1)))
|
||||
f.checked = nil
|
||||
f.checked = sync.Map{}
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2))
|
||||
f.workSheetWriter()
|
||||
value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml")
|
||||
|
@ -478,6 +558,43 @@ func TestDeleteSheet(t *testing.T) {
|
|||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx")))
|
||||
}
|
||||
|
||||
func TestMoveSheet(t *testing.T) {
|
||||
f := NewFile()
|
||||
defer f.Close()
|
||||
for i := 2; i < 6; i++ {
|
||||
_, err := f.NewSheet("Sheet" + strconv.Itoa(i))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, []string{"Sheet1", "Sheet2", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList())
|
||||
|
||||
// Move target to first position
|
||||
assert.NoError(t, f.MoveSheet("Sheet2", "Sheet1"))
|
||||
assert.Equal(t, []string{"Sheet2", "Sheet1", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList())
|
||||
assert.Equal(t, "Sheet1", f.GetSheetName(f.GetActiveSheetIndex()))
|
||||
|
||||
// Move target to last position
|
||||
assert.NoError(t, f.MoveSheet("Sheet2", "Sheet5"))
|
||||
assert.NoError(t, f.MoveSheet("Sheet5", "Sheet2"))
|
||||
assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList())
|
||||
|
||||
// Move target to same position
|
||||
assert.NoError(t, f.MoveSheet("Sheet1", "Sheet1"))
|
||||
assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList())
|
||||
|
||||
// Test move sheet with invalid sheet name
|
||||
assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("", "Sheet2"))
|
||||
assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("Sheet1", ""))
|
||||
|
||||
// Test move sheet on not exists worksheet
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("SheetN", "Sheet2"))
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("Sheet1", "SheetN"))
|
||||
|
||||
// Test move sheet with unsupported workbook charset
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.MoveSheet("Sheet2", "Sheet1"), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestDeleteAndAdjustDefinedNames(t *testing.T) {
|
||||
deleteAndAdjustDefinedNames(nil, 0)
|
||||
deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0)
|
||||
|
@ -501,6 +618,18 @@ func TestSetSheetVisible(t *testing.T) {
|
|||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test set sheet visible with empty sheet views
|
||||
f = NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet2.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetViews = nil
|
||||
assert.NoError(t, f.SetSheetVisible("Sheet2", false))
|
||||
visible, err := f.GetSheetVisible("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, visible)
|
||||
}
|
||||
|
||||
func TestGetSheetVisible(t *testing.T) {
|
||||
|
@ -527,12 +656,12 @@ func TestSetContentTypes(t *testing.T) {
|
|||
assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestDeleteSheetFromContentTypes(t *testing.T) {
|
||||
func TestRemoveContentTypesPart(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test delete sheet from content types with unsupported charset content types
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteSheetFromContentTypes("/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualError(t, f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, "/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func BenchmarkNewSheet(b *testing.B) {
|
||||
|
|
15
sheetpr.go
15
sheetpr.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -116,7 +116,7 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) {
|
|||
prepareTabColor := func(ws *xlsxWorksheet) {
|
||||
ws.prepareSheetPr()
|
||||
if ws.SheetPr.TabColor == nil {
|
||||
ws.SheetPr.TabColor = new(xlsxTabColor)
|
||||
ws.SheetPr.TabColor = new(xlsxColor)
|
||||
}
|
||||
}
|
||||
if opts.CodeName != nil {
|
||||
|
@ -145,7 +145,12 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) {
|
|||
if !s.Field(i).IsNil() {
|
||||
prepareTabColor(ws)
|
||||
name := s.Type().Field(i).Name
|
||||
reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:]).Set(s.Field(i).Elem())
|
||||
fld := reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:])
|
||||
if s.Field(i).Kind() == reflect.Ptr && fld.Kind() == reflect.Ptr {
|
||||
fld.Set(s.Field(i))
|
||||
continue
|
||||
}
|
||||
fld.Set(s.Field(i).Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,7 +211,7 @@ func (f *File) GetSheetProps(sheet string) (SheetPropsOptions, error) {
|
|||
if ws.SheetPr.TabColor != nil {
|
||||
opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed)
|
||||
opts.TabColorRGB = stringPtr(ws.SheetPr.TabColor.RGB)
|
||||
opts.TabColorTheme = intPtr(ws.SheetPr.TabColor.Theme)
|
||||
opts.TabColorTheme = ws.SheetPr.TabColor.Theme
|
||||
opts.TabColorTint = float64Ptr(ws.SheetPr.TabColor.Tint)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestSetPageMargins(t *testing.T) {
|
|||
// Test set page margins on not exists worksheet
|
||||
assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist")
|
||||
// Test set page margins with invalid sheet name
|
||||
assert.EqualError(t, f.SetPageMargins("Sheet:1", nil), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetPageMargins("Sheet:1", nil))
|
||||
}
|
||||
|
||||
func TestGetPageMargins(t *testing.T) {
|
||||
|
@ -40,7 +40,7 @@ func TestGetPageMargins(t *testing.T) {
|
|||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test get page margins with invalid sheet name
|
||||
_, err = f.GetPageMargins("Sheet:1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
||||
func TestSetSheetProps(t *testing.T) {
|
||||
|
@ -88,7 +88,7 @@ func TestSetSheetProps(t *testing.T) {
|
|||
// Test set worksheet properties on not exists worksheet
|
||||
assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist")
|
||||
// Test set worksheet properties with invalid sheet name
|
||||
assert.EqualError(t, f.SetSheetProps("Sheet:1", nil), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.SetSheetProps("Sheet:1", nil))
|
||||
}
|
||||
|
||||
func TestGetSheetProps(t *testing.T) {
|
||||
|
@ -98,5 +98,5 @@ func TestGetSheetProps(t *testing.T) {
|
|||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test get worksheet properties with invalid sheet name
|
||||
_, err = f.GetSheetProps("Sheet:1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
|
10
sheetview.go
10
sheetview.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -61,11 +61,7 @@ func (view *xlsxSheetView) setSheetView(opts *ViewOptions) {
|
|||
view.TopLeftCell = *opts.TopLeftCell
|
||||
}
|
||||
if opts.View != nil {
|
||||
if _, ok := map[string]interface{}{
|
||||
"normal": nil,
|
||||
"pageLayout": nil,
|
||||
"pageBreakPreview": nil,
|
||||
}[*opts.View]; ok {
|
||||
if inStrSlice([]string{"normal", "pageLayout", "pageBreakPreview"}, *opts.View, true) != -1 {
|
||||
view.View = *opts.View
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,621 @@
|
|||
package excelize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSlicer(t *testing.T) {
|
||||
f := NewFile()
|
||||
disable, colName := false, "_!@#$%^&*()-+=|\\/<>"
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "B1", colName))
|
||||
// Create table in a worksheet
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
Caption: "Column1",
|
||||
}))
|
||||
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "I1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
Caption: "Column1",
|
||||
}))
|
||||
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: colName,
|
||||
Cell: "M1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
Caption: colName,
|
||||
Macro: "Button1_Click",
|
||||
Width: 200,
|
||||
Height: 200,
|
||||
DisplayHeader: &disable,
|
||||
ItemDesc: true,
|
||||
}))
|
||||
// Test get table slicers
|
||||
slicers, err := f.GetSlicers("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Column1", slicers[0].Name)
|
||||
assert.Equal(t, "E1", slicers[0].Cell)
|
||||
assert.Equal(t, "Sheet1", slicers[0].TableSheet)
|
||||
assert.Equal(t, "Table1", slicers[0].TableName)
|
||||
assert.Equal(t, "Column1", slicers[0].Caption)
|
||||
assert.Equal(t, "Column1 1", slicers[1].Name)
|
||||
assert.Equal(t, "I1", slicers[1].Cell)
|
||||
assert.Equal(t, "Sheet1", slicers[1].TableSheet)
|
||||
assert.Equal(t, "Table1", slicers[1].TableName)
|
||||
assert.Equal(t, "Column1", slicers[1].Caption)
|
||||
assert.Equal(t, colName, slicers[2].Name)
|
||||
assert.Equal(t, "M1", slicers[2].Cell)
|
||||
assert.Equal(t, "Sheet1", slicers[2].TableSheet)
|
||||
assert.Equal(t, "Table1", slicers[2].TableName)
|
||||
assert.Equal(t, colName, slicers[2].Caption)
|
||||
assert.Equal(t, "Button1_Click", slicers[2].Macro)
|
||||
assert.False(t, *slicers[2].DisplayHeader)
|
||||
assert.True(t, slicers[2].ItemDesc)
|
||||
// Test create two pivot tables in a new worksheet
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
// Create some data in a sheet
|
||||
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
year := []int{2017, 2018, 2019}
|
||||
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
|
||||
region := []string{"East", "West", "North", "South"}
|
||||
assert.NoError(t, f.SetSheetRow("Sheet2", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
|
||||
for row := 2; row < 32; row++ {
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("B%d", row), year[rand.Intn(3)]))
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("D%d", row), rand.Intn(5000)))
|
||||
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
|
||||
}
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet2!A1:E31",
|
||||
PivotTableRange: "Sheet2!G2:M34",
|
||||
Name: "PivotTable1",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Filter: []PivotTableField{{Data: "Region"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
ShowRowHeaders: true,
|
||||
ShowColHeaders: true,
|
||||
ShowLastColumn: true,
|
||||
ShowError: true,
|
||||
PivotTableStyleName: "PivotStyleLight16",
|
||||
}))
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
|
||||
DataRange: "Sheet2!A1:E31",
|
||||
PivotTableRange: "Sheet2!U34:O2",
|
||||
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
|
||||
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
|
||||
Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
|
||||
RowGrandTotals: true,
|
||||
ColGrandTotals: true,
|
||||
ShowDrill: true,
|
||||
ShowRowHeaders: true,
|
||||
ShowColHeaders: true,
|
||||
ShowLastColumn: true,
|
||||
}))
|
||||
// Test add a pivot table slicer
|
||||
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
|
||||
Name: "Month",
|
||||
Cell: "G42",
|
||||
TableSheet: "Sheet2",
|
||||
TableName: "PivotTable1",
|
||||
Caption: "Month",
|
||||
}))
|
||||
// Test add a pivot table slicer with duplicate field name
|
||||
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
|
||||
Name: "Month",
|
||||
Cell: "K42",
|
||||
TableSheet: "Sheet2",
|
||||
TableName: "PivotTable1",
|
||||
Caption: "Month",
|
||||
}))
|
||||
// Test add a pivot table slicer for another pivot table in a worksheet
|
||||
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
|
||||
Name: "Region",
|
||||
Cell: "O42",
|
||||
TableSheet: "Sheet2",
|
||||
TableName: "PivotTable2",
|
||||
Caption: "Region",
|
||||
ItemDesc: true,
|
||||
}))
|
||||
// Test get pivot table slicers
|
||||
slicers, err = f.GetSlicers("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Month", slicers[0].Name)
|
||||
assert.Equal(t, "G42", slicers[0].Cell)
|
||||
assert.Equal(t, "Sheet2", slicers[0].TableSheet)
|
||||
assert.Equal(t, "PivotTable1", slicers[0].TableName)
|
||||
assert.Equal(t, "Month", slicers[0].Caption)
|
||||
assert.Equal(t, "Month 1", slicers[1].Name)
|
||||
assert.Equal(t, "K42", slicers[1].Cell)
|
||||
assert.Equal(t, "Sheet2", slicers[1].TableSheet)
|
||||
assert.Equal(t, "PivotTable1", slicers[1].TableName)
|
||||
assert.Equal(t, "Month", slicers[1].Caption)
|
||||
assert.Equal(t, "Region", slicers[2].Name)
|
||||
assert.Equal(t, "O42", slicers[2].Cell)
|
||||
assert.Equal(t, "Sheet2", slicers[2].TableSheet)
|
||||
assert.Equal(t, "PivotTable2", slicers[2].TableName)
|
||||
assert.Equal(t, "Region", slicers[2].Caption)
|
||||
assert.True(t, slicers[2].ItemDesc)
|
||||
// Test add a table slicer with empty slicer options
|
||||
assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil))
|
||||
// Test add a table slicer with invalid slicer options
|
||||
for _, opts := range []*SlicerOptions{
|
||||
{Cell: "Q1", TableSheet: "Sheet1", TableName: "Table1"},
|
||||
{Name: "Column", Cell: "Q1", TableSheet: "Sheet1"},
|
||||
{Name: "Column", TableSheet: "Sheet1", TableName: "Table1"},
|
||||
} {
|
||||
assert.Equal(t, ErrParameterInvalid, f.AddSlicer("Sheet1", opts))
|
||||
}
|
||||
// Test add a table slicer with not exist worksheet
|
||||
assert.EqualError(t, f.AddSlicer("SheetN", &SlicerOptions{
|
||||
Name: "Column2",
|
||||
Cell: "Q1",
|
||||
TableSheet: "SheetN",
|
||||
TableName: "Table1",
|
||||
}), "sheet SheetN does not exist")
|
||||
// Test add a table slicer with not exist table name
|
||||
assert.Equal(t, newNoExistTableError("Table2"), f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column2",
|
||||
Cell: "Q1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table2",
|
||||
}))
|
||||
// Test add a table slicer with invalid slicer name
|
||||
assert.Equal(t, newInvalidSlicerNameError("Column6"), f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column6",
|
||||
Cell: "Q1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
}))
|
||||
workbookPath := filepath.Join("test", "TestAddSlicer.xlsm")
|
||||
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddVBAProject(file))
|
||||
assert.NoError(t, f.SaveAs(workbookPath))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add a pivot table slicer with unsupported charset pivot table
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/pivotTables/pivotTable2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddSlicer("Sheet2", &SlicerOptions{
|
||||
Name: "Month",
|
||||
Cell: "G42",
|
||||
TableSheet: "Sheet2",
|
||||
TableName: "PivotTable1",
|
||||
Caption: "Month",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test open a workbook and get already exist slicers
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
slicers, err = f.GetSlicers("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Column1", slicers[0].Name)
|
||||
assert.Equal(t, "E1", slicers[0].Cell)
|
||||
assert.Equal(t, "Sheet1", slicers[0].TableSheet)
|
||||
assert.Equal(t, "Table1", slicers[0].TableName)
|
||||
assert.Equal(t, "Column1", slicers[0].Caption)
|
||||
assert.Equal(t, "Column1 1", slicers[1].Name)
|
||||
assert.Equal(t, "I1", slicers[1].Cell)
|
||||
assert.Equal(t, "Sheet1", slicers[1].TableSheet)
|
||||
assert.Equal(t, "Table1", slicers[1].TableName)
|
||||
assert.Equal(t, "Column1", slicers[1].Caption)
|
||||
assert.Equal(t, colName, slicers[2].Name)
|
||||
assert.Equal(t, "M1", slicers[2].Cell)
|
||||
assert.Equal(t, "Sheet1", slicers[2].TableSheet)
|
||||
assert.Equal(t, "Table1", slicers[2].TableName)
|
||||
assert.Equal(t, colName, slicers[2].Caption)
|
||||
assert.Equal(t, "Button1_Click", slicers[2].Macro)
|
||||
assert.False(t, *slicers[2].DisplayHeader)
|
||||
assert.True(t, slicers[2].ItemDesc)
|
||||
slicers, err = f.GetSlicers("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Month", slicers[0].Name)
|
||||
assert.Equal(t, "G42", slicers[0].Cell)
|
||||
assert.Equal(t, "Sheet2", slicers[0].TableSheet)
|
||||
assert.Equal(t, "PivotTable1", slicers[0].TableName)
|
||||
assert.Equal(t, "Month", slicers[0].Caption)
|
||||
assert.Equal(t, "Month 1", slicers[1].Name)
|
||||
assert.Equal(t, "K42", slicers[1].Cell)
|
||||
assert.Equal(t, "Sheet2", slicers[1].TableSheet)
|
||||
assert.Equal(t, "PivotTable1", slicers[1].TableName)
|
||||
assert.Equal(t, "Month", slicers[1].Caption)
|
||||
assert.Equal(t, "Region", slicers[2].Name)
|
||||
assert.Equal(t, "O42", slicers[2].Cell)
|
||||
assert.Equal(t, "Sheet2", slicers[2].TableSheet)
|
||||
assert.Equal(t, "PivotTable2", slicers[2].TableName)
|
||||
assert.Equal(t, "Region", slicers[2].Caption)
|
||||
assert.True(t, slicers[2].ItemDesc)
|
||||
|
||||
// Test add a pivot table slicer with workbook which contains timeline
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/timelines/timeline1.xml", []byte(fmt.Sprintf(`<timelines xmlns="%s"><timeline name="a"/></timelines>`, NameSpaceSpreadSheetX15.Value)))
|
||||
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
|
||||
Name: "Month",
|
||||
Cell: "G42",
|
||||
TableSheet: "Sheet2",
|
||||
TableName: "PivotTable1",
|
||||
Caption: "Month",
|
||||
}))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add a pivot table slicer with unsupported charset timeline
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/timelines/timeline1.xml", MacintoshCyrillicCharset)
|
||||
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
|
||||
Name: "Month",
|
||||
Cell: "G42",
|
||||
TableSheet: "Sheet2",
|
||||
TableName: "PivotTable1",
|
||||
Caption: "Month",
|
||||
}))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add a table slicer with invalid worksheet extension list
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<>"}
|
||||
assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
}))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add a table slicer with unsupported charset slicer
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
f.Pkg.Store("xl/slicers/slicer2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableName: "Table1",
|
||||
TableSheet: "Sheet1",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add a table slicer with read workbook error
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
f.WorkBook.ExtLst = &xlsxExtLst{Ext: "<>"}
|
||||
assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableName: "Table1",
|
||||
TableSheet: "Sheet1",
|
||||
}))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add a table slicer with unsupported charset content types
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableName: "Table1",
|
||||
TableSheet: "Sheet1",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addSlicer(0, xlsxSlicer{}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
// Create table in a worksheet
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
f.Pkg.Store("xl/drawings/drawing2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
Caption: "Column1",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
// Test get sheet slicers without slicer
|
||||
slicers, err = f.GetSlicers("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, slicers)
|
||||
// Test get sheet slicers with not exist worksheet name
|
||||
_, err = f.GetSlicers("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
// Test get sheet slicers with unsupported charset slicer cache
|
||||
f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
// Test get sheet slicers with unsupported charset slicer
|
||||
f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get sheet slicers with invalid worksheet extension list
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst.Ext = "<>"
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.Error(t, err)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
// Test get sheet slicers without slicer cache
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
|
||||
f.Pkg.Delete(k.(string))
|
||||
}
|
||||
return true
|
||||
})
|
||||
slicers, err = f.GetSlicers("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, slicers)
|
||||
assert.NoError(t, f.Close())
|
||||
// Test open a workbook and get sheet slicer with invalid cell reference in the drawing part
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`<wsDr xmlns="%s"><twoCellAnchor><from><col>-1</col><row>-1</row></from><mc:AlternateContent><mc:Choice xmlns:sle15="%s"><graphicFrame><nvGraphicFramePr><cNvPr id="2" name="Column1"/></nvGraphicFramePr></graphicFrame></mc:Choice></mc:AlternateContent></twoCellAnchor></wsDr>`, NameSpaceDrawingMLSpreadSheet.Value, NameSpaceDrawingMLSlicerX15.Value)))
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.Equal(t, newCoordinatesToCellNameError(0, 0), err)
|
||||
// Test get sheet slicer without slicer shape in the drawing part
|
||||
f.Drawings.Delete("xl/drawings/drawing1.xml")
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`<wsDr xmlns="%s"><twoCellAnchor/></wsDr>`, NameSpaceDrawingMLSpreadSheet.Value)))
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
f.Drawings.Delete("xl/drawings/drawing1.xml")
|
||||
// Test get sheet slicers with unsupported charset drawing part
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get sheet slicers with unsupported charset table
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get sheet slicers with unsupported charset pivot table
|
||||
f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetSlicers("Sheet2")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test create a workbook and get sheet slicer with invalid cell reference in the drawing part
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
Caption: "Column1",
|
||||
}))
|
||||
drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml")
|
||||
assert.True(t, ok)
|
||||
drawing.(*xlsxWsDr).TwoCellAnchor[0].From = &xlsxFrom{Col: -1, Row: -1}
|
||||
_, err = f.GetSlicers("Sheet1")
|
||||
assert.Equal(t, newCoordinatesToCellNameError(0, 0), err)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test open a workbook and delete slicers
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
for _, name := range []string{colName, "Column1 1", "Column1"} {
|
||||
assert.NoError(t, f.DeleteSlicer(name))
|
||||
}
|
||||
for _, name := range []string{"Month", "Month 1", "Region"} {
|
||||
assert.NoError(t, f.DeleteSlicer(name))
|
||||
}
|
||||
// Test delete slicer with no exits slicer name
|
||||
assert.Equal(t, newNoExistSlicerError("x"), f.DeleteSlicer("x"))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test open a workbook and delete sheet slicer with unsupported charset slicer cache
|
||||
f, err = OpenFile(workbookPath)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeleteSlicer("Column1"), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddSheetSlicer(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test add sheet slicer with not exist worksheet name
|
||||
_, err := f.addSheetSlicer("SheetN", ExtURISlicerListX15)
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddSheetTableSlicer(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test add sheet table slicer with invalid worksheet extension
|
||||
assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0, ExtURISlicerListX15))
|
||||
// Test add sheet table slicer with existing worksheet extension
|
||||
assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("<ext uri=\"%s\"></ext>", ExtURITimelineRefs)}}, 1, ExtURISlicerListX15))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestSetSlicerCache(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset)
|
||||
_, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
|
||||
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer2" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
|
||||
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
|
||||
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"><tableSlicerCache tableId="1" column="2"/></ext></extLst></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
|
||||
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value)))
|
||||
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestDeleteSlicer(t *testing.T) {
|
||||
f, slicerXML := NewFile(), "xl/slicers/slicer1.xml"
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{
|
||||
Name: "Table1",
|
||||
Range: "A1:D5",
|
||||
}))
|
||||
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
|
||||
Name: "Column1",
|
||||
Cell: "E1",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
Caption: "Column1",
|
||||
}))
|
||||
// Test delete sheet slicers with invalid worksheet extension list
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst.Ext = "<>"
|
||||
assert.Error(t, f.deleteSlicer(SlicerOptions{
|
||||
slicerXML: slicerXML,
|
||||
slicerSheetName: "Sheet1",
|
||||
Name: "Column1",
|
||||
}))
|
||||
// Test delete slicer with unsupported charset worksheet
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteSlicer(SlicerOptions{
|
||||
slicerXML: slicerXML,
|
||||
slicerSheetName: "Sheet1",
|
||||
Name: "Column1",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test delete slicer with unsupported charset slicer
|
||||
f.Pkg.Store(slicerXML, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteSlicer(SlicerOptions{slicerXML: slicerXML}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestDeleteSlicerCache(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test delete slicer cache with unsupported charset workbook
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.deleteSlicerCache(nil, SlicerOptions{}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddSlicerCache(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}, nil), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test add a pivot table cache slicer with unsupported charset
|
||||
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml"
|
||||
f.Pkg.Store(pivotCacheXML, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, nil,
|
||||
&PivotTableOptions{pivotCacheXML: pivotCacheXML}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddDrawingSlicer(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test add a drawing slicer with not exist worksheet
|
||||
assert.EqualError(t, f.addDrawingSlicer("SheetN", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{
|
||||
Name: "Column2",
|
||||
Cell: "Q1",
|
||||
TableSheet: "SheetN",
|
||||
TableName: "Table1",
|
||||
}), "sheet SheetN does not exist")
|
||||
// Test add a drawing slicer with invalid cell reference
|
||||
assert.EqualError(t, f.addDrawingSlicer("Sheet1", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{
|
||||
Name: "Column2",
|
||||
Cell: "A",
|
||||
TableSheet: "Sheet1",
|
||||
TableName: "Table1",
|
||||
}), "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddWorkbookSlicerCache(t *testing.T) {
|
||||
// Test add a workbook slicer cache with unsupported charset workbook
|
||||
f := NewFile()
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addWorkbookSlicerCache(1, ExtURISlicerCachesX15), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestGenSlicerCacheName(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "Slicer_Column_1", RefersTo: formulaErrorNA}))
|
||||
assert.Equal(t, "Slicer_Column_11", f.genSlicerCacheName("Column 1"))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAddPivotCacheSlicer(t *testing.T) {
|
||||
f := NewFile()
|
||||
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml"
|
||||
// Test add a pivot table cache slicer with existing extension list
|
||||
f.Pkg.Store(pivotCacheXML, []byte(fmt.Sprintf(`<pivotCacheDefinition xmlns="%s"><extLst><ext uri="%s"><x14:pivotCacheDefinition pivotCacheId="1"/></ext></extLst></pivotCacheDefinition>`, NameSpaceSpreadSheet.Value, ExtURIPivotCacheDefinition)))
|
||||
_, err := f.addPivotCacheSlicer(&PivotTableOptions{
|
||||
pivotCacheXML: pivotCacheXML,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
551
sparkline.go
551
sparkline.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -18,353 +18,352 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// addSparklineGroupByStyle provides a function to create x14:sparklineGroups
|
||||
// element by given sparkline style ID.
|
||||
func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup {
|
||||
groups := []*xlsxX14SparklineGroup{
|
||||
// getSparklineGroupPresets returns the preset list of sparkline group to create
|
||||
// x14:sparklineGroups element.
|
||||
func getSparklineGroupPresets() []*xlsxX14SparklineGroup {
|
||||
return []*xlsxX14SparklineGroup{
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 5},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 4},
|
||||
ColorLow: &xlsxTabColor{Theme: 4},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(4)},
|
||||
}, // 0
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 5},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 4},
|
||||
ColorLow: &xlsxTabColor{Theme: 4},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(4)},
|
||||
}, // 1
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 6},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 5},
|
||||
ColorLow: &xlsxTabColor{Theme: 5},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(5)},
|
||||
}, // 2
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 7},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 6},
|
||||
ColorLow: &xlsxTabColor{Theme: 6},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(7)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(6)},
|
||||
}, // 3
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 8},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 7},
|
||||
ColorLow: &xlsxTabColor{Theme: 7},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(7)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(7)},
|
||||
}, // 4
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 9},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 8},
|
||||
ColorLow: &xlsxTabColor{Theme: 8},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(8)},
|
||||
}, // 5
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 4},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxTabColor{Theme: 9},
|
||||
ColorLow: &xlsxTabColor{Theme: 9},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(9)},
|
||||
}, // 6
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxTabColor{Theme: 5},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 5},
|
||||
ColorLow: &xlsxTabColor{Theme: 5},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(5)},
|
||||
}, // 7
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxTabColor{Theme: 6},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
}, // 8
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxTabColor{Theme: 7},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(7)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
}, // 9
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxTabColor{Theme: 8},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
}, // 10
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxTabColor{Theme: 9},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
}, // 11
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxTabColor{Theme: 4},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
}, // 12
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 4},
|
||||
ColorNegative: &xlsxTabColor{Theme: 5},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
}, // 13
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 5},
|
||||
ColorNegative: &xlsxTabColor{Theme: 6},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
}, // 14
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 6},
|
||||
ColorNegative: &xlsxTabColor{Theme: 7},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(7)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
}, // 15
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 7},
|
||||
ColorNegative: &xlsxTabColor{Theme: 8},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(7)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
}, // 16
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 8},
|
||||
ColorNegative: &xlsxTabColor{Theme: 9},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
}, // 17
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 9},
|
||||
ColorNegative: &xlsxTabColor{Theme: 4},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
}, // 18
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
}, // 19
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
|
||||
}, // 20
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
|
||||
}, // 21
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
|
||||
}, // 22
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
|
||||
}, // 23
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: 0.79998168889431442},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
|
||||
}, // 24
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.499984740745262},
|
||||
ColorNegative: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
|
||||
}, // 25
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.34998626667073579},
|
||||
ColorNegative: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
|
||||
ColorFirst: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
|
||||
ColorLast: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
|
||||
ColorHigh: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
|
||||
ColorLow: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.34998626667073579},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
|
||||
}, // 26
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF323232"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF323232"},
|
||||
ColorNegative: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorFirst: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorLast: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorHigh: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorLow: &xlsxColor{RGB: "FFD00000"},
|
||||
}, // 27
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF000000"},
|
||||
ColorNegative: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorFirst: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorLast: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorHigh: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorLow: &xlsxColor{RGB: "FF0070C0"},
|
||||
}, // 28
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF376092"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FFD00000"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF376092"},
|
||||
ColorNegative: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorFirst: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorLast: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorHigh: &xlsxColor{RGB: "FFD00000"},
|
||||
ColorLow: &xlsxColor{RGB: "FFD00000"},
|
||||
}, // 29
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FF000000"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorNegative: &xlsxColor{RGB: "FF000000"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FF000000"},
|
||||
ColorFirst: &xlsxColor{RGB: "FF000000"},
|
||||
ColorLast: &xlsxColor{RGB: "FF000000"},
|
||||
ColorHigh: &xlsxColor{RGB: "FF000000"},
|
||||
ColorLow: &xlsxColor{RGB: "FF000000"},
|
||||
}, // 30
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF5F5F5F"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FFFFB620"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FFD70077"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FF5687C2"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FF359CEB"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FF56BE79"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FFFF5055"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF5F5F5F"},
|
||||
ColorNegative: &xlsxColor{RGB: "FFFFB620"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FFD70077"},
|
||||
ColorFirst: &xlsxColor{RGB: "FF5687C2"},
|
||||
ColorLast: &xlsxColor{RGB: "FF359CEB"},
|
||||
ColorHigh: &xlsxColor{RGB: "FF56BE79"},
|
||||
ColorLow: &xlsxColor{RGB: "FFFF5055"},
|
||||
}, // 31
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF5687C2"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FFFFB620"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FFD70077"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FF777777"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FF359CEB"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FF56BE79"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FFFF5055"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF5687C2"},
|
||||
ColorNegative: &xlsxColor{RGB: "FFFFB620"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FFD70077"},
|
||||
ColorFirst: &xlsxColor{RGB: "FF777777"},
|
||||
ColorLast: &xlsxColor{RGB: "FF359CEB"},
|
||||
ColorHigh: &xlsxColor{RGB: "FF56BE79"},
|
||||
ColorLow: &xlsxColor{RGB: "FFFF5055"},
|
||||
}, // 32
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FFC6EFCE"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FFFFC7CE"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FF8CADD6"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FFFFDC47"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FFFFEB9C"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FF60D276"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FFFF5367"},
|
||||
ColorSeries: &xlsxColor{RGB: "FFC6EFCE"},
|
||||
ColorNegative: &xlsxColor{RGB: "FFFFC7CE"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FF8CADD6"},
|
||||
ColorFirst: &xlsxColor{RGB: "FFFFDC47"},
|
||||
ColorLast: &xlsxColor{RGB: "FFFFEB9C"},
|
||||
ColorHigh: &xlsxColor{RGB: "FF60D276"},
|
||||
ColorLow: &xlsxColor{RGB: "FFFF5367"},
|
||||
}, // 33
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{RGB: "FF00B050"},
|
||||
ColorNegative: &xlsxTabColor{RGB: "FFFF0000"},
|
||||
ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"},
|
||||
ColorFirst: &xlsxTabColor{RGB: "FFFFC000"},
|
||||
ColorLast: &xlsxTabColor{RGB: "FFFFC000"},
|
||||
ColorHigh: &xlsxTabColor{RGB: "FF00B050"},
|
||||
ColorLow: &xlsxTabColor{RGB: "FFFF0000"},
|
||||
ColorSeries: &xlsxColor{RGB: "FF00B050"},
|
||||
ColorNegative: &xlsxColor{RGB: "FFFF0000"},
|
||||
ColorMarkers: &xlsxColor{RGB: "FF0070C0"},
|
||||
ColorFirst: &xlsxColor{RGB: "FFFFC000"},
|
||||
ColorLast: &xlsxColor{RGB: "FFFFC000"},
|
||||
ColorHigh: &xlsxColor{RGB: "FF00B050"},
|
||||
ColorLow: &xlsxColor{RGB: "FFFF0000"},
|
||||
}, // 34
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 3},
|
||||
ColorNegative: &xlsxTabColor{Theme: 9},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 8},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4},
|
||||
ColorLast: &xlsxTabColor{Theme: 5},
|
||||
ColorHigh: &xlsxTabColor{Theme: 6},
|
||||
ColorLow: &xlsxTabColor{Theme: 7},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(3)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(7)},
|
||||
}, // 35
|
||||
{
|
||||
ColorSeries: &xlsxTabColor{Theme: 1},
|
||||
ColorNegative: &xlsxTabColor{Theme: 9},
|
||||
ColorMarkers: &xlsxTabColor{Theme: 8},
|
||||
ColorFirst: &xlsxTabColor{Theme: 4},
|
||||
ColorLast: &xlsxTabColor{Theme: 5},
|
||||
ColorHigh: &xlsxTabColor{Theme: 6},
|
||||
ColorLow: &xlsxTabColor{Theme: 7},
|
||||
ColorSeries: &xlsxColor{Theme: intPtr(1)},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(9)},
|
||||
ColorMarkers: &xlsxColor{Theme: intPtr(8)},
|
||||
ColorFirst: &xlsxColor{Theme: intPtr(4)},
|
||||
ColorLast: &xlsxColor{Theme: intPtr(5)},
|
||||
ColorHigh: &xlsxColor{Theme: intPtr(6)},
|
||||
ColorLow: &xlsxColor{Theme: intPtr(7)},
|
||||
}, // 36
|
||||
}
|
||||
return groups[ID]
|
||||
}
|
||||
|
||||
// AddSparkline provides a function to add sparklines to the worksheet by
|
||||
// given formatting options. Sparklines are small charts that fit in a single
|
||||
// cell and are used to show trends in data. Sparklines are a feature of Excel
|
||||
// 2010 and later only. You can write them to an XLSX file that can be read by
|
||||
// Excel 2007, but they won't be displayed. For example, add a grouped
|
||||
// sparkline. Changes are applied to all three:
|
||||
// 2010 and later only. You can write them to workbook that can be read by Excel
|
||||
// 2007, but they won't be displayed. For example, add a grouped sparkline.
|
||||
// Changes are applied to all three:
|
||||
//
|
||||
// err := f.AddSparkline("Sheet1", &excelize.SparklineOptions{
|
||||
// Location: []string{"A1", "A2", "A3"},
|
||||
|
@ -415,7 +414,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error {
|
|||
}
|
||||
sparkType = specifiedSparkTypes
|
||||
}
|
||||
group = f.addSparklineGroupByStyle(opts.Style)
|
||||
group = getSparklineGroupPresets()[opts.Style]
|
||||
group.Type = sparkType
|
||||
group.ColorAxis = &xlsxColor{RGB: "FF000000"}
|
||||
group.DisplayEmptyCellsAs = "gap"
|
||||
|
@ -427,7 +426,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error {
|
|||
group.DisplayXAxis = opts.Axis
|
||||
group.Markers = opts.Markers
|
||||
if opts.SeriesColor != "" {
|
||||
group.ColorSeries = &xlsxTabColor{
|
||||
group.ColorSeries = &xlsxColor{
|
||||
RGB: getPaletteColor(opts.SeriesColor),
|
||||
}
|
||||
}
|
||||
|
@ -489,9 +488,9 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup,
|
|||
err error
|
||||
idx int
|
||||
appendMode bool
|
||||
decodeExtLst = new(decodeWorksheetExt)
|
||||
decodeExtLst = new(decodeExtLst)
|
||||
decodeSparklineGroups *decodeX14SparklineGroups
|
||||
ext *xlsxWorksheetExt
|
||||
ext *xlsxExt
|
||||
sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte
|
||||
)
|
||||
sparklineGroupBytes, _ = xml.Marshal(group)
|
||||
|
@ -523,13 +522,13 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup,
|
|||
XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value,
|
||||
SparklineGroups: []*xlsxX14SparklineGroup{group},
|
||||
})
|
||||
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{
|
||||
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
|
||||
URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes),
|
||||
})
|
||||
}
|
||||
sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
|
||||
return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) <
|
||||
inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false)
|
||||
return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
|
||||
inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
|
||||
})
|
||||
extLstBytes, err = xml.Marshal(decodeExtLst)
|
||||
ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), "<extLst>"), "</extLst>")}
|
||||
|
|
|
@ -224,59 +224,57 @@ func TestAddSparkline(t *testing.T) {
|
|||
Range: []string{"Sheet2!A3:E3"},
|
||||
}), "sheet SheetN does not exist")
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error())
|
||||
assert.Equal(t, ErrParameterRequired, f.AddSparkline("Sheet1", nil))
|
||||
|
||||
// Test add sparkline with invalid sheet name
|
||||
assert.EqualError(t, f.AddSparkline("Sheet:1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.AddSparkline("Sheet:1", &SparklineOptions{
|
||||
Location: []string{"F3"},
|
||||
Range: []string{"Sheet2!A3:E3"},
|
||||
Type: "win_loss",
|
||||
Negative: true,
|
||||
}), ErrSheetNameInvalid.Error())
|
||||
}))
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSparklineLocation, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Range: []string{"Sheet2!A3:E3"},
|
||||
}), ErrSparklineLocation.Error())
|
||||
}))
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSparklineRange, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"F3"},
|
||||
}), ErrSparklineRange.Error())
|
||||
}))
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSparkline, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"F2", "F3"},
|
||||
Range: []string{"Sheet2!A3:E3"},
|
||||
}), ErrSparkline.Error())
|
||||
}))
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSparklineType, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"F3"},
|
||||
Range: []string{"Sheet2!A3:E3"},
|
||||
Type: "unknown_type",
|
||||
}), ErrSparklineType.Error())
|
||||
}))
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"F3"},
|
||||
Range: []string{"Sheet2!A3:E3"},
|
||||
Style: -1,
|
||||
}), ErrSparklineStyle.Error())
|
||||
}))
|
||||
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"F3"},
|
||||
Range: []string{"Sheet2!A3:E3"},
|
||||
Style: -1,
|
||||
}), ErrSparklineStyle.Error())
|
||||
}))
|
||||
// Test creating a conditional format with existing extension lists
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: `
|
||||
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
|
||||
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
|
||||
assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"A3"},
|
||||
Range: []string{"Sheet3!A2:J2"},
|
||||
Type: "column",
|
||||
}))
|
||||
// Test creating a conditional format with invalid extension list characters
|
||||
ws.(*xlsxWorksheet).ExtLst.Ext = `<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups><x14:sparklineGroup></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext>`
|
||||
ws.(*xlsxWorksheet).ExtLst.Ext = fmt.Sprintf(`<ext uri="%s"><x14:sparklineGroups><x14:sparklineGroup></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext>`, ExtURISparklineGroups)
|
||||
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
|
||||
Location: []string{"A2"},
|
||||
Range: []string{"Sheet3!A1:J1"},
|
||||
|
|
70
stream.go
70
stream.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -38,14 +38,16 @@ type StreamWriter struct {
|
|||
tableParts string
|
||||
}
|
||||
|
||||
// NewStreamWriter return stream writer struct by given worksheet name for
|
||||
// generate new worksheet with large amounts of data. Note that after set
|
||||
// rows, you must call the 'Flush' method to end the streaming writing process
|
||||
// and ensure that the order of row numbers is ascending, the normal mode
|
||||
// functions and stream mode functions can't be work mixed to writing data on
|
||||
// the worksheets, you can't get cell value when in-memory chunks data over
|
||||
// 16MB. For example, set data for worksheet of size 102400 rows x 50 columns
|
||||
// with numbers and style:
|
||||
// NewStreamWriter returns stream writer struct by given worksheet name used for
|
||||
// writing data on a new existing empty worksheet with large amounts of data.
|
||||
// Note that after writing data with the stream writer for the worksheet, you
|
||||
// must call the 'Flush' method to end the streaming writing process, ensure
|
||||
// that the order of row numbers is ascending when set rows, and the normal
|
||||
// mode functions and stream mode functions can not be work mixed to writing
|
||||
// data on the worksheets. The stream writer will try to use temporary files on
|
||||
// disk to reduce the memory usage when in-memory chunks data over 16MB, and
|
||||
// you can't get cell value at this time. For example, set data for worksheet
|
||||
// of size 102400 rows x 50 columns with numbers and style:
|
||||
//
|
||||
// f := excelize.NewFile()
|
||||
// defer func() {
|
||||
|
@ -116,7 +118,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
|
|||
}
|
||||
sheetID := f.getSheetID(sheet)
|
||||
if sheetID == -1 {
|
||||
return nil, newNoExistSheetError(sheet)
|
||||
return nil, ErrSheetNotExist{sheet}
|
||||
}
|
||||
sw := &StreamWriter{
|
||||
file: f,
|
||||
|
@ -182,7 +184,7 @@ func (sw *StreamWriter) AddTable(table *Table) error {
|
|||
}
|
||||
|
||||
// Correct table reference range, such correct C1:B3 to B1:C3.
|
||||
ref, err := sw.file.coordinatesToRangeRef(coordinates)
|
||||
ref, err := coordinatesToRangeRef(coordinates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -288,7 +290,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er
|
|||
}
|
||||
}
|
||||
|
||||
// Check if the token is an XLSX row with the matching row number.
|
||||
// Check if the token is an worksheet row with the matching row number.
|
||||
func getRowElement(token xml.Token, hRow int) (startElement xml.StartElement, ok bool) {
|
||||
startElement, ok = token.(xml.StartElement)
|
||||
if !ok {
|
||||
|
@ -437,24 +439,24 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt
|
|||
// the width column B:C as 20:
|
||||
//
|
||||
// err := sw.SetColWidth(2, 3, 20)
|
||||
func (sw *StreamWriter) SetColWidth(min, max int, width float64) error {
|
||||
func (sw *StreamWriter) SetColWidth(minVal, maxVal int, width float64) error {
|
||||
if sw.sheetWritten {
|
||||
return ErrStreamSetColWidth
|
||||
}
|
||||
if min < MinColumns || min > MaxColumns || max < MinColumns || max > MaxColumns {
|
||||
if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns {
|
||||
return ErrColumnNumber
|
||||
}
|
||||
if width > MaxColumnWidth {
|
||||
return ErrColumnWidth
|
||||
}
|
||||
if min > max {
|
||||
min, max = max, min
|
||||
if minVal > maxVal {
|
||||
minVal, maxVal = maxVal, minVal
|
||||
}
|
||||
|
||||
sw.cols.WriteString(`<col min="`)
|
||||
sw.cols.WriteString(strconv.Itoa(min))
|
||||
sw.cols.WriteString(strconv.Itoa(minVal))
|
||||
sw.cols.WriteString(`" max="`)
|
||||
sw.cols.WriteString(strconv.Itoa(max))
|
||||
sw.cols.WriteString(strconv.Itoa(maxVal))
|
||||
sw.cols.WriteString(`" width="`)
|
||||
sw.cols.WriteString(strconv.FormatFloat(width, 'f', -1, 64))
|
||||
sw.cols.WriteString(`" customWidth="1"/>`)
|
||||
|
@ -482,16 +484,16 @@ func (sw *StreamWriter) SetPanes(panes *Panes) error {
|
|||
// MergeCell provides a function to merge cells by a given range reference for
|
||||
// the StreamWriter. Don't create a merged cell that overlaps with another
|
||||
// existing merged cell.
|
||||
func (sw *StreamWriter) MergeCell(hCell, vCell string) error {
|
||||
_, err := cellRefsToCoordinates(hCell, vCell)
|
||||
func (sw *StreamWriter) MergeCell(topLeftCell, bottomRightCell string) error {
|
||||
_, err := cellRefsToCoordinates(topLeftCell, bottomRightCell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sw.mergeCellsCount++
|
||||
_, _ = sw.mergeCells.WriteString(`<mergeCell ref="`)
|
||||
_, _ = sw.mergeCells.WriteString(hCell)
|
||||
_, _ = sw.mergeCells.WriteString(topLeftCell)
|
||||
_, _ = sw.mergeCells.WriteString(`:`)
|
||||
_, _ = sw.mergeCells.WriteString(vCell)
|
||||
_, _ = sw.mergeCells.WriteString(bottomRightCell)
|
||||
_, _ = sw.mergeCells.WriteString(`"/>`)
|
||||
return nil
|
||||
}
|
||||
|
@ -525,11 +527,11 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
|
|||
var err error
|
||||
switch val := val.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
err = setCellIntFunc(c, val)
|
||||
setCellIntFunc(c, val)
|
||||
case float32:
|
||||
c.T, c.V = setCellFloat(float64(val), -1, 32)
|
||||
c.setCellFloat(float64(val), -1, 32)
|
||||
case float64:
|
||||
c.T, c.V = setCellFloat(val, -1, 64)
|
||||
c.setCellFloat(val, -1, 64)
|
||||
case string:
|
||||
c.setCellValue(val)
|
||||
case []byte:
|
||||
|
@ -552,7 +554,7 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
|
|||
}
|
||||
|
||||
// setCellIntFunc is a wrapper of SetCellInt.
|
||||
func setCellIntFunc(c *xlsxC, val interface{}) (err error) {
|
||||
func setCellIntFunc(c *xlsxC, val interface{}) {
|
||||
switch val := val.(type) {
|
||||
case int:
|
||||
c.T, c.V = setCellInt(val)
|
||||
|
@ -565,18 +567,16 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) {
|
|||
case int64:
|
||||
c.T, c.V = setCellInt(int(val))
|
||||
case uint:
|
||||
c.T, c.V = setCellInt(int(val))
|
||||
c.T, c.V = setCellUint(uint64(val))
|
||||
case uint8:
|
||||
c.T, c.V = setCellInt(int(val))
|
||||
c.T, c.V = setCellUint(uint64(val))
|
||||
case uint16:
|
||||
c.T, c.V = setCellInt(int(val))
|
||||
c.T, c.V = setCellUint(uint64(val))
|
||||
case uint32:
|
||||
c.T, c.V = setCellInt(int(val))
|
||||
c.T, c.V = setCellUint(uint64(val))
|
||||
case uint64:
|
||||
c.T, c.V = setCellInt(int(val))
|
||||
default:
|
||||
c.T, c.V = setCellUint(val)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// writeCell constructs a cell XML and writes it to the buffer.
|
||||
|
@ -676,7 +676,7 @@ func (sw *StreamWriter) Flush() error {
|
|||
|
||||
sheetPath := sw.file.sheetMap[sw.Sheet]
|
||||
sw.file.Sheet.Delete(sheetPath)
|
||||
delete(sw.file.checked, sheetPath)
|
||||
sw.file.checked.Delete(sheetPath)
|
||||
sw.file.Pkg.Delete(sheetPath)
|
||||
|
||||
return nil
|
||||
|
|
154
stream_test.go
154
stream_test.go
|
@ -3,6 +3,8 @@ package excelize
|
|||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -73,7 +75,9 @@ func TestStreamWriter(t *testing.T) {
|
|||
}))
|
||||
assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}))
|
||||
assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID}))
|
||||
assert.EqualError(t, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error())
|
||||
assert.Equal(t, ErrMaxRowHeight, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}))
|
||||
|
||||
assert.NoError(t, streamWriter.SetRow("A9", []interface{}{math.NaN(), math.Inf(0), math.Inf(-1)}))
|
||||
|
||||
for rowID := 10; rowID <= 51200; rowID++ {
|
||||
row := make([]interface{}, 50)
|
||||
|
@ -144,7 +148,7 @@ func TestStreamWriter(t *testing.T) {
|
|||
cells += len(row)
|
||||
}
|
||||
assert.NoError(t, rows.Close())
|
||||
assert.Equal(t, 2559559, cells)
|
||||
assert.Equal(t, 2559562, cells)
|
||||
// Save spreadsheet with password.
|
||||
assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"}))
|
||||
assert.NoError(t, file.Close())
|
||||
|
@ -158,11 +162,11 @@ func TestStreamSetColWidth(t *testing.T) {
|
|||
streamWriter, err := file.NewStreamWriter("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, streamWriter.SetColWidth(3, 2, 20))
|
||||
assert.ErrorIs(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber)
|
||||
assert.ErrorIs(t, streamWriter.SetColWidth(MaxColumns+1, 3, 20), ErrColumnNumber)
|
||||
assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error())
|
||||
assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20))
|
||||
assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20))
|
||||
assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1))
|
||||
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
|
||||
assert.ErrorIs(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth)
|
||||
assert.Equal(t, ErrStreamSetColWidth, streamWriter.SetColWidth(2, 3, 20))
|
||||
}
|
||||
|
||||
func TestStreamSetPanes(t *testing.T) {
|
||||
|
@ -173,7 +177,7 @@ func TestStreamSetPanes(t *testing.T) {
|
|||
YSplit: 0,
|
||||
TopLeftCell: "B1",
|
||||
ActivePane: "topRight",
|
||||
Panes: []PaneOptions{
|
||||
Selection: []Selection{
|
||||
{SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
|
||||
},
|
||||
}
|
||||
|
@ -183,9 +187,9 @@ func TestStreamSetPanes(t *testing.T) {
|
|||
streamWriter, err := file.NewStreamWriter("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, streamWriter.SetPanes(paneOpts))
|
||||
assert.EqualError(t, streamWriter.SetPanes(nil), ErrParameterInvalid.Error())
|
||||
assert.Equal(t, ErrParameterInvalid, streamWriter.SetPanes(nil))
|
||||
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
|
||||
assert.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes)
|
||||
assert.Equal(t, ErrStreamSetPanes, streamWriter.SetPanes(paneOpts))
|
||||
}
|
||||
|
||||
func TestStreamTable(t *testing.T) {
|
||||
|
@ -220,10 +224,12 @@ func TestStreamTable(t *testing.T) {
|
|||
assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"}))
|
||||
|
||||
// Test add table with illegal cell reference
|
||||
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.AddTable(&Table{Range: "A:B1"}))
|
||||
assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"}))
|
||||
// Test add table with invalid table name
|
||||
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidTableNameError("1Table").Error())
|
||||
assert.Equal(t, newInvalidNameError("1Table"), streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}))
|
||||
// Test add table with row number exceeds maximum limit
|
||||
assert.Equal(t, ErrMaxRows, streamWriter.AddTable(&Table{Range: "A1048576:C1048576"}))
|
||||
// Test add table with unsupported charset content types
|
||||
file.ContentTypes = nil
|
||||
file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
|
@ -239,7 +245,7 @@ func TestStreamMergeCells(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.NoError(t, streamWriter.MergeCell("A1", "D1"))
|
||||
// Test merge cells with illegal cell reference
|
||||
assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.MergeCell("A", "D1"))
|
||||
assert.NoError(t, streamWriter.Flush())
|
||||
// Save spreadsheet by the given path
|
||||
assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx")))
|
||||
|
@ -270,7 +276,7 @@ func TestNewStreamWriter(t *testing.T) {
|
|||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test new stream write with invalid sheet name
|
||||
_, err = file.NewStreamWriter("Sheet:1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
||||
func TestStreamMarshalAttrs(t *testing.T) {
|
||||
|
@ -288,10 +294,10 @@ func TestStreamSetRow(t *testing.T) {
|
|||
}()
|
||||
streamWriter, err := file.NewStreamWriter("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.SetRow("A", []interface{}{}))
|
||||
// Test set row with non-ascending row number
|
||||
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{}))
|
||||
assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), newStreamSetRowError(1).Error())
|
||||
assert.Equal(t, newStreamSetRowError(1), streamWriter.SetRow("A1", []interface{}{}))
|
||||
// Test set row with unsupported charset workbook
|
||||
file.WorkBook = nil
|
||||
file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
|
@ -332,16 +338,13 @@ func TestStreamSetRowWithStyle(t *testing.T) {
|
|||
Cell{StyleID: blueStyleID, Value: "value3"},
|
||||
&Cell{StyleID: blueStyleID, Value: "value3"},
|
||||
}, RowOpts{StyleID: grayStyleID}))
|
||||
err = streamWriter.Flush()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, streamWriter.Flush())
|
||||
|
||||
ws, err := file.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, grayStyleID, ws.SheetData.Row[0].C[0].S)
|
||||
assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[1].S)
|
||||
assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[2].S)
|
||||
assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[3].S)
|
||||
assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[4].S)
|
||||
for colIdx, expected := range []int{grayStyleID, zeroStyleID, zeroStyleID, blueStyleID, blueStyleID} {
|
||||
assert.Equal(t, expected, ws.SheetData.Row[0].C[colIdx].S)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamSetCellValFunc(t *testing.T) {
|
||||
|
@ -352,25 +355,29 @@ func TestStreamSetCellValFunc(t *testing.T) {
|
|||
sw, err := f.NewStreamWriter("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
c := &xlsxC{}
|
||||
assert.NoError(t, sw.setCellValFunc(c, 128))
|
||||
assert.NoError(t, sw.setCellValFunc(c, int8(-128)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, int16(-32768)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, int32(-2147483648)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, int64(-9223372036854775808)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, uint(128)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, uint8(255)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, uint16(65535)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, uint32(4294967295)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, uint64(18446744073709551615)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, float32(100.1588)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, 100.1588))
|
||||
assert.NoError(t, sw.setCellValFunc(c, " Hello"))
|
||||
assert.NoError(t, sw.setCellValFunc(c, []byte(" Hello")))
|
||||
assert.NoError(t, sw.setCellValFunc(c, time.Now().UTC()))
|
||||
assert.NoError(t, sw.setCellValFunc(c, time.Duration(1e13)))
|
||||
assert.NoError(t, sw.setCellValFunc(c, true))
|
||||
assert.NoError(t, sw.setCellValFunc(c, nil))
|
||||
assert.NoError(t, sw.setCellValFunc(c, complex64(5+10i)))
|
||||
for _, val := range []interface{}{
|
||||
128,
|
||||
int8(-128),
|
||||
int16(-32768),
|
||||
int32(-2147483648),
|
||||
int64(-9223372036854775808),
|
||||
uint(128),
|
||||
uint8(255),
|
||||
uint16(65535),
|
||||
uint32(4294967295),
|
||||
uint64(18446744073709551615),
|
||||
float32(100.1588),
|
||||
100.1588,
|
||||
" Hello",
|
||||
[]byte(" Hello"),
|
||||
time.Now().UTC(),
|
||||
time.Duration(1e13),
|
||||
true,
|
||||
nil,
|
||||
complex64(5 + 10i),
|
||||
} {
|
||||
assert.NoError(t, sw.setCellValFunc(c, val))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamWriterOutlineLevel(t *testing.T) {
|
||||
|
@ -389,14 +396,61 @@ func TestStreamWriterOutlineLevel(t *testing.T) {
|
|||
|
||||
file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx"))
|
||||
assert.NoError(t, err)
|
||||
level, err := file.GetRowOutlineLevel("Sheet1", 1)
|
||||
for rowIdx, expected := range []uint8{1, 7, 0} {
|
||||
level, err := file.GetRowOutlineLevel("Sheet1", rowIdx+1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint8(1), level)
|
||||
level, err = file.GetRowOutlineLevel("Sheet1", 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint8(7), level)
|
||||
level, err = file.GetRowOutlineLevel("Sheet1", 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint8(0), level)
|
||||
assert.Equal(t, expected, level)
|
||||
}
|
||||
assert.NoError(t, file.Close())
|
||||
}
|
||||
|
||||
func TestStreamWriterReader(t *testing.T) {
|
||||
var (
|
||||
err error
|
||||
sw = StreamWriter{
|
||||
rawData: bufferedWriter{},
|
||||
}
|
||||
)
|
||||
sw.rawData.tmp, err = os.CreateTemp(os.TempDir(), "excelize-")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, sw.rawData.tmp.Close())
|
||||
// Test reader stat a closed temp file
|
||||
_, err = sw.rawData.Reader()
|
||||
assert.Error(t, err)
|
||||
_, err = sw.getRowValues(1, 1, 1)
|
||||
assert.Error(t, err)
|
||||
os.Remove(sw.rawData.tmp.Name())
|
||||
|
||||
sw = StreamWriter{
|
||||
file: NewFile(),
|
||||
rawData: bufferedWriter{},
|
||||
}
|
||||
// Test getRowValues without expected row
|
||||
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c r=\"B1\"></c></row><worksheet/>")
|
||||
_, err = sw.getRowValues(1, 1, 1)
|
||||
assert.NoError(t, err)
|
||||
sw.rawData.buf.Reset()
|
||||
// Test getRowValues with illegal cell reference
|
||||
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c r=\"A\"></c></row><worksheet/>")
|
||||
_, err = sw.getRowValues(1, 1, 1)
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
|
||||
sw.rawData.buf.Reset()
|
||||
// Test getRowValues with invalid c element characters
|
||||
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c></row><worksheet/>")
|
||||
_, err = sw.getRowValues(1, 1, 1)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: element <c> closed by </row>")
|
||||
sw.rawData.buf.Reset()
|
||||
}
|
||||
|
||||
func TestStreamWriterGetRowElement(t *testing.T) {
|
||||
// Test get row element without r attribute
|
||||
dec := xml.NewDecoder(strings.NewReader("<row ht=\"0\" />"))
|
||||
for {
|
||||
token, err := dec.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
_, ok := getRowElement(token, 0)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
|
|
355
styles_test.go
355
styles_test.go
|
@ -1,6 +1,7 @@
|
|||
package excelize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -171,18 +172,20 @@ func TestSetConditionalFormat(t *testing.T) {
|
|||
// Test creating a conditional format with a solid color data bar style
|
||||
f := NewFile()
|
||||
condFmts := []ConditionalFormatOptions{
|
||||
{Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: 0, Criteria: "=", MinType: "min", MaxType: "max"},
|
||||
{Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: intPtr(0), Criteria: "=", MinType: "min", MaxType: "max"},
|
||||
}
|
||||
for _, ref := range []string{"A1:A2", "B1:B2"} {
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts))
|
||||
}
|
||||
f = NewFile()
|
||||
// Test creating a conditional format without cell reference
|
||||
assert.Equal(t, ErrParameterRequired, f.SetConditionalFormat("Sheet1", "", nil))
|
||||
// Test creating a conditional format with invalid cell reference
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2:A3", nil))
|
||||
// Test creating a conditional format with existing extension lists
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: `
|
||||
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
|
||||
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
|
||||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarSolid: true}}))
|
||||
f = NewFile()
|
||||
// Test creating a conditional format with invalid extension list characters
|
||||
|
@ -191,39 +194,113 @@ func TestSetConditionalFormat(t *testing.T) {
|
|||
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<ext><x14:conditionalFormattings></x14:conditionalFormatting></x14:conditionalFormattings></ext>"}
|
||||
assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element <conditionalFormattings> closed by </conditionalFormatting>")
|
||||
// Test creating a conditional format with invalid icon set style
|
||||
assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}), ErrParameterInvalid.Error())
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}))
|
||||
// Test unsupported conditional formatting rule types
|
||||
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: "unsupported"}}))
|
||||
|
||||
t.Run("multi_conditional_formatting_rules_priority", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
var condFmts []ConditionalFormatOptions
|
||||
for _, color := range []string{
|
||||
"#264B96", // Blue
|
||||
"#F9A73E", // Yellow
|
||||
"#006F3C", // Green
|
||||
} {
|
||||
condFmts = append(condFmts, ConditionalFormatOptions{
|
||||
Type: "data_bar",
|
||||
Criteria: "=",
|
||||
MinType: "num",
|
||||
MaxType: "num",
|
||||
MinValue: "0",
|
||||
MaxValue: "5",
|
||||
BarColor: color,
|
||||
BarSolid: true,
|
||||
})
|
||||
}
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A5", condFmts))
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "B1:B5", condFmts))
|
||||
for r := 1; r <= 20; r++ {
|
||||
cell, err := CoordinatesToCellName(1, r)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", cell, r))
|
||||
cell, err = CoordinatesToCellName(2, r)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", cell, r))
|
||||
}
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
var priorities []int
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
for _, condFmt := range ws.(*xlsxWorksheet).ConditionalFormatting {
|
||||
for _, rule := range condFmt.CfRule {
|
||||
priorities = append(priorities, rule.Priority)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, expected, priorities)
|
||||
assert.NoError(t, f.Close())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConditionalFormats(t *testing.T) {
|
||||
for _, format := range [][]ConditionalFormatOptions{
|
||||
{{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}},
|
||||
{{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}},
|
||||
{{Type: "top", Format: 1, Criteria: "=", Value: "6"}},
|
||||
{{Type: "bottom", Format: 1, Criteria: "=", Value: "6"}},
|
||||
{{Type: "average", AboveAverage: true, Format: 1, Criteria: "="}},
|
||||
{{Type: "duplicate", Format: 1, Criteria: "="}},
|
||||
{{Type: "unique", Format: 1, Criteria: "="}},
|
||||
{{Type: "cell", Format: intPtr(1), Criteria: "greater than", Value: "6"}},
|
||||
{{Type: "cell", Format: intPtr(1), Criteria: "between", MinValue: "6", MaxValue: "8"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "yesterday"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "today"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "tomorrow"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "last 7 days"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "last week"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "this week"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "continue week"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "last month"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "this month"}},
|
||||
{{Type: "time_period", Format: intPtr(1), Criteria: "continue month"}},
|
||||
{{Type: "text", Format: intPtr(1), Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}},
|
||||
{{Type: "text", Format: intPtr(1), Criteria: "not containing", Value: "text"}},
|
||||
{{Type: "text", Format: intPtr(1), Criteria: "begins with", Value: "prefix"}},
|
||||
{{Type: "text", Format: intPtr(1), Criteria: "ends with", Value: "suffix"}},
|
||||
{{Type: "top", Format: intPtr(1), Criteria: "=", Value: "6"}},
|
||||
{{Type: "bottom", Format: intPtr(1), Criteria: "=", Value: "6"}},
|
||||
{{Type: "average", AboveAverage: true, Format: intPtr(1), Criteria: "="}},
|
||||
{{Type: "duplicate", Format: intPtr(1), Criteria: "="}},
|
||||
{{Type: "unique", Format: intPtr(1), Criteria: "="}},
|
||||
{{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}},
|
||||
{{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}},
|
||||
{{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}},
|
||||
{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}},
|
||||
{{Type: "formula", Format: 1, Criteria: "="}},
|
||||
{{Type: "formula", Format: intPtr(1), Criteria: "="}},
|
||||
{{Type: "blanks", Format: intPtr(1)}},
|
||||
{{Type: "no_blanks", Format: intPtr(1)}},
|
||||
{{Type: "errors", Format: intPtr(1)}},
|
||||
{{Type: "no_errors", Format: intPtr(1)}},
|
||||
{{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}},
|
||||
} {
|
||||
f := NewFile()
|
||||
err := f.SetConditionalFormat("Sheet1", "A1:A2", format)
|
||||
err := f.SetConditionalFormat("Sheet1", "A2:A1,B:B,2:2", format)
|
||||
assert.NoError(t, err)
|
||||
opts, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, format, opts["A1:A2"])
|
||||
assert.Equal(t, format, opts["A2:A1 B1:B1048576 A2:XFD2"])
|
||||
}
|
||||
// Test get conditional formats on no exists worksheet
|
||||
// Test get multiple conditional formats
|
||||
f := NewFile()
|
||||
_, err := f.GetConditionalFormats("SheetN")
|
||||
expected := []ConditionalFormatOptions{
|
||||
{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true},
|
||||
{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: false, StopIfTrue: true},
|
||||
}
|
||||
err := f.SetConditionalFormat("Sheet1", "A1:A2", expected)
|
||||
assert.NoError(t, err)
|
||||
opts, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, opts["A1:A2"])
|
||||
|
||||
// Test get conditional formats on no exists worksheet
|
||||
f = NewFile()
|
||||
_, err = f.GetConditionalFormats("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test get conditional formats with invalid sheet name
|
||||
_, err = f.GetConditionalFormats("Sheet:1")
|
||||
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, err)
|
||||
}
|
||||
|
||||
func TestUnsetConditionalFormat(t *testing.T) {
|
||||
|
@ -232,12 +309,12 @@ func TestUnsetConditionalFormat(t *testing.T) {
|
|||
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10"))
|
||||
format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: format, Value: "6"}}))
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: &format, Value: "6"}}))
|
||||
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10"))
|
||||
// Test unset conditional format on not exists worksheet
|
||||
assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist")
|
||||
// Test unset conditional format with invalid sheet name
|
||||
assert.EqualError(t, f.UnsetConditionalFormat("Sheet:1", "A1:A10"), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.UnsetConditionalFormat("Sheet:1", "A1:A10"))
|
||||
// Save spreadsheet by the given path
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx")))
|
||||
}
|
||||
|
@ -264,13 +341,22 @@ func TestNewStyle(t *testing.T) {
|
|||
_, err = f.NewStyle(nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test gradient fills
|
||||
f = NewFile()
|
||||
styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "4E71BE"}, Shading: 1, Pattern: 1}})
|
||||
assert.NoError(t, err)
|
||||
styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FF0000", "4E71BE"}, Shading: 1, Pattern: 1}})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, styleID1, styleID2)
|
||||
|
||||
var exp string
|
||||
f = NewFile()
|
||||
_, err = f.NewStyle(&Style{CustomNumFmt: &exp})
|
||||
assert.EqualError(t, err, ErrCustomNumFmt.Error())
|
||||
assert.Equal(t, ErrCustomNumFmt, err)
|
||||
_, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}})
|
||||
assert.EqualError(t, err, ErrFontLength.Error())
|
||||
assert.Equal(t, ErrFontLength, err)
|
||||
_, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}})
|
||||
assert.EqualError(t, err, ErrFontSize.Error())
|
||||
assert.Equal(t, ErrFontSize, err)
|
||||
|
||||
// Test create numeric custom style
|
||||
numFmt := "####;####"
|
||||
|
@ -279,7 +365,7 @@ func TestNewStyle(t *testing.T) {
|
|||
CustomNumFmt: &numFmt,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, styleID)
|
||||
assert.Equal(t, 1, styleID)
|
||||
|
||||
assert.NotNil(t, f.Styles)
|
||||
assert.NotNil(t, f.Styles.CellXfs)
|
||||
|
@ -291,12 +377,10 @@ func TestNewStyle(t *testing.T) {
|
|||
// Test create currency custom style
|
||||
f.Styles.NumFmts = nil
|
||||
styleID, err = f.NewStyle(&Style{
|
||||
Lang: "ko-kr",
|
||||
NumFmt: 32, // must not be in currencyNumFmt
|
||||
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, styleID)
|
||||
assert.Equal(t, 2, styleID)
|
||||
|
||||
assert.NotNil(t, f.Styles)
|
||||
assert.NotNil(t, f.Styles.CellXfs)
|
||||
|
@ -330,14 +414,14 @@ func TestNewStyle(t *testing.T) {
|
|||
f = NewFile()
|
||||
f.Styles.NumFmts = nil
|
||||
f.Styles.CellXfs.Xf = nil
|
||||
style4, err := f.NewStyle(&Style{NumFmt: 160, Lang: "unknown"})
|
||||
style4, err := f.NewStyle(&Style{NumFmt: 160})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, style4)
|
||||
|
||||
f = NewFile()
|
||||
f.Styles.NumFmts = nil
|
||||
f.Styles.CellXfs.Xf = nil
|
||||
style5, err := f.NewStyle(&Style{NumFmt: 160, Lang: "zh-cn"})
|
||||
style5, err := f.NewStyle(&Style{NumFmt: 160})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, style5)
|
||||
|
||||
|
@ -355,13 +439,58 @@ func TestNewStyle(t *testing.T) {
|
|||
assert.Equal(t, ErrCellStyles, err)
|
||||
}
|
||||
|
||||
func TestNewConditionalStyle(t *testing.T) {
|
||||
func TestConditionalStyle(t *testing.T) {
|
||||
f := NewFile()
|
||||
expected := &Style{Protection: &Protection{Hidden: true, Locked: true}}
|
||||
idx, err := f.NewConditionalStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err := f.GetConditionalStyle(idx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, style)
|
||||
_, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(4), NumFmt: 165, NegRed: true})
|
||||
assert.NoError(t, err)
|
||||
_, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(-1)})
|
||||
assert.NoError(t, err)
|
||||
expected = &Style{NumFmt: 1}
|
||||
idx, err = f.NewConditionalStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetConditionalStyle(idx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected.NumFmt, style.NumFmt)
|
||||
assert.Zero(t, *style.DecimalPlaces)
|
||||
_, err = f.NewConditionalStyle(&Style{NumFmt: 27})
|
||||
assert.NoError(t, err)
|
||||
numFmt := "general"
|
||||
_, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt})
|
||||
assert.NoError(t, err)
|
||||
numFmt1 := "0.00"
|
||||
_, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt1})
|
||||
assert.NoError(t, err)
|
||||
// Test create conditional style with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
_, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
|
||||
_, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get conditional style with invalid style index
|
||||
_, err = f.GetConditionalStyle(1)
|
||||
assert.Equal(t, newInvalidStyleID(1), err)
|
||||
// Test get conditional style with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
_, err = f.GetConditionalStyle(1)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
f = NewFile()
|
||||
// Test get conditional style with background color and empty pattern type
|
||||
idx, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
|
||||
assert.NoError(t, err)
|
||||
f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.PatternType = ""
|
||||
f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.FgColor = nil
|
||||
f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.BgColor = &xlsxColor{Theme: intPtr(6)}
|
||||
style, err = f.GetConditionalStyle(idx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pattern", style.Fill.Type)
|
||||
assert.Equal(t, []string{"A5A5A5"}, style.Fill.Color)
|
||||
}
|
||||
|
||||
func TestGetDefaultFont(t *testing.T) {
|
||||
|
@ -407,7 +536,7 @@ func TestThemeReader(t *testing.T) {
|
|||
f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset)
|
||||
theme, err := f.themeReader()
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualValues(t, &xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value}, theme)
|
||||
assert.EqualValues(t, &decodeTheme{}, theme)
|
||||
}
|
||||
|
||||
func TestSetCellStyle(t *testing.T) {
|
||||
|
@ -415,9 +544,9 @@ func TestSetCellStyle(t *testing.T) {
|
|||
// Test set cell style on not exists worksheet
|
||||
assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist")
|
||||
// Test set cell style with invalid style ID
|
||||
assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", -1), newInvalidStyleID(-1).Error())
|
||||
assert.Equal(t, newInvalidStyleID(-1), f.SetCellStyle("Sheet1", "A1", "A2", -1))
|
||||
// Test set cell style with not exists style ID
|
||||
assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error())
|
||||
assert.Equal(t, newInvalidStyleID(10), f.SetCellStyle("Sheet1", "A1", "A2", 10))
|
||||
// Test set cell style with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
|
@ -474,3 +603,161 @@ func TestGetNumFmtID(t *testing.T) {
|
|||
assert.NotEqual(t, id1, id2)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx")))
|
||||
}
|
||||
|
||||
func TestGetThemeColor(t *testing.T) {
|
||||
assert.Empty(t, (&File{}).getThemeColor(&xlsxColor{}))
|
||||
f := NewFile()
|
||||
assert.Empty(t, f.getThemeColor(nil))
|
||||
var theme int
|
||||
assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{Theme: &theme}))
|
||||
assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{RGB: "FFFFFF"}))
|
||||
assert.Equal(t, "FF8080", f.getThemeColor(&xlsxColor{Indexed: 2, Tint: 0.5}))
|
||||
assert.Empty(t, f.getThemeColor(&xlsxColor{Indexed: len(IndexedColorMapping), Tint: 0.5}))
|
||||
clr := &decodeCTColor{}
|
||||
assert.Nil(t, clr.colorChoice())
|
||||
}
|
||||
|
||||
func TestGetStyle(t *testing.T) {
|
||||
f := NewFile()
|
||||
expected := &Style{
|
||||
Border: []Border{
|
||||
{Type: "left", Color: "0000FF", Style: 3},
|
||||
{Type: "right", Color: "FF0000", Style: 6},
|
||||
{Type: "top", Color: "00FF00", Style: 4},
|
||||
{Type: "bottom", Color: "FFFF00", Style: 5},
|
||||
{Type: "diagonalUp", Color: "A020F0", Style: 7},
|
||||
{Type: "diagonalDown", Color: "A020F0", Style: 7},
|
||||
},
|
||||
Fill: Fill{Type: "gradient", Shading: 16, Color: []string{"0000FF", "00FF00"}},
|
||||
Font: &Font{
|
||||
Bold: true, Italic: true, Underline: "single", Family: "Arial",
|
||||
Size: 8.5, Strike: true, Color: "777777", ColorIndexed: 1, ColorTint: 0.1,
|
||||
},
|
||||
Alignment: &Alignment{
|
||||
Horizontal: "center",
|
||||
Indent: 1,
|
||||
JustifyLastLine: true,
|
||||
ReadingOrder: 1,
|
||||
RelativeIndent: 1,
|
||||
ShrinkToFit: true,
|
||||
TextRotation: 180,
|
||||
Vertical: "center",
|
||||
WrapText: true,
|
||||
},
|
||||
Protection: &Protection{Hidden: true, Locked: true},
|
||||
NumFmt: 49,
|
||||
}
|
||||
styleID, err := f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err := f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected.Border, style.Border)
|
||||
assert.Equal(t, expected.Fill, style.Fill)
|
||||
assert.Equal(t, expected.Font, style.Font)
|
||||
assert.Equal(t, expected.Alignment, style.Alignment)
|
||||
assert.Equal(t, expected.Protection, style.Protection)
|
||||
assert.Equal(t, expected.NumFmt, style.NumFmt)
|
||||
assert.Nil(t, style.DecimalPlaces)
|
||||
|
||||
expected = &Style{
|
||||
Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"0000FF"}},
|
||||
}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected.Fill, style.Fill)
|
||||
assert.Nil(t, style.DecimalPlaces)
|
||||
|
||||
expected = &Style{NumFmt: 2}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected.NumFmt, style.NumFmt)
|
||||
assert.Equal(t, 2, *style.DecimalPlaces)
|
||||
|
||||
expected = &Style{NumFmt: 27}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected.NumFmt, style.NumFmt)
|
||||
assert.Nil(t, style.DecimalPlaces)
|
||||
|
||||
expected = &Style{NumFmt: 165}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected.NumFmt, style.NumFmt)
|
||||
assert.Equal(t, 2, *style.DecimalPlaces)
|
||||
|
||||
decimal := 4
|
||||
expected = &Style{NumFmt: 165, DecimalPlaces: &decimal, NegRed: true}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, style.NumFmt)
|
||||
assert.Equal(t, *expected.DecimalPlaces, *style.DecimalPlaces)
|
||||
assert.Equal(t, "[$$-409]#,##0.0000;[Red][$$-409]#,##0.0000", *style.CustomNumFmt)
|
||||
|
||||
for _, val := range [][]interface{}{
|
||||
{"$#,##0", 0},
|
||||
{"$#,##0.0", 1},
|
||||
{"_($* #,##0_);_($* (#,##0);_($* \"-\"_);_(@_)", 0},
|
||||
{"_($* #,##000_);_($* (#,##000);_($* \"-\"_);_(@_)", 0},
|
||||
{"_($* #,##0.0000_);_($* (#,##0.0000);_($* \"-\"????_);_(@_)", 4},
|
||||
} {
|
||||
numFmtCode := val[0].(string)
|
||||
expected = &Style{CustomNumFmt: &numFmtCode}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, val[1].(int), *style.DecimalPlaces, numFmtCode)
|
||||
}
|
||||
|
||||
for _, val := range []string{
|
||||
";$#,##0",
|
||||
";$#,##0;",
|
||||
";$#,##0.0",
|
||||
";$#,##0.0;",
|
||||
"$#,##0;0.0",
|
||||
"_($* #,##0_);;_($* \"-\"_);_(@_)",
|
||||
"_($* #,##0.0_);_($* (#,##0.00);_($* \"-\"_);_(@_)",
|
||||
} {
|
||||
expected = &Style{CustomNumFmt: &val}
|
||||
styleID, err = f.NewStyle(expected)
|
||||
assert.NoError(t, err)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, style.DecimalPlaces)
|
||||
}
|
||||
|
||||
// Test get style with custom color index
|
||||
f.Styles.Colors = &xlsxStyleColors{
|
||||
IndexedColors: &xlsxIndexedColors{
|
||||
RgbColor: []xlsxColor{{RGB: "FF012345"}},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "012345", f.getThemeColor(&xlsxColor{Indexed: 0}))
|
||||
|
||||
f.Styles.Fonts.Font[0].U = &attrValString{}
|
||||
f.Styles.CellXfs.Xf[0].FontID = intPtr(0)
|
||||
style, err = f.GetStyle(styleID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "single", style.Font.Underline)
|
||||
|
||||
// Test get style with invalid style index
|
||||
style, err = f.GetStyle(-1)
|
||||
assert.Nil(t, style)
|
||||
assert.Equal(t, err, newInvalidStyleID(-1))
|
||||
// Test get style with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
style, err = f.GetStyle(1)
|
||||
assert.Nil(t, style)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
|
283
table.go
283
table.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,20 +7,28 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
expressionFormat = regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
|
||||
conditionFormat = regexp.MustCompile(`(or|\|\|)`)
|
||||
blankFormat = regexp.MustCompile("blanks|nonblanks")
|
||||
matchFormat = regexp.MustCompile("[*?]")
|
||||
)
|
||||
|
||||
// parseTableOptions provides a function to parse the format settings of the
|
||||
// table with default value.
|
||||
func parseTableOptions(opts *Table) (*Table, error) {
|
||||
|
@ -31,7 +39,7 @@ func parseTableOptions(opts *Table) (*Table, error) {
|
|||
if opts.ShowRowStripes == nil {
|
||||
opts.ShowRowStripes = boolPtr(true)
|
||||
}
|
||||
if err = checkTableName(opts.Name); err != nil {
|
||||
if err = checkDefinedName(opts.Name); err != nil {
|
||||
return opts, err
|
||||
}
|
||||
return opts, err
|
||||
|
@ -75,6 +83,23 @@ func (f *File) AddTable(sheet string, table *Table) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var exist bool
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/tables/table") {
|
||||
var t xlsxTable
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
|
||||
Decode(&t); err != nil && err != io.EOF {
|
||||
return true
|
||||
}
|
||||
if exist = t.Name == options.Name; exist {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if exist {
|
||||
return ErrExistsTableName
|
||||
}
|
||||
// Coordinate conversion, convert C1:B3 to 2,0,1,2.
|
||||
coordinates, err := rangeRefToCoordinates(options.Range)
|
||||
if err != nil {
|
||||
|
@ -99,13 +124,125 @@ func (f *File) AddTable(sheet string, table *Table) error {
|
|||
return f.addContentTypePart(tableID, "table")
|
||||
}
|
||||
|
||||
// GetTables provides the method to get all tables in a worksheet by given
|
||||
// worksheet name.
|
||||
func (f *File) GetTables(sheet string) ([]Table, error) {
|
||||
var tables []Table
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return tables, err
|
||||
}
|
||||
if ws.TableParts == nil {
|
||||
return tables, err
|
||||
}
|
||||
for _, tbl := range ws.TableParts.TableParts {
|
||||
if tbl != nil {
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID)
|
||||
tableXML := strings.ReplaceAll(target, "..", "xl")
|
||||
content, ok := f.Pkg.Load(tableXML)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var t xlsxTable
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
|
||||
Decode(&t); err != nil && err != io.EOF {
|
||||
return tables, err
|
||||
}
|
||||
table := Table{
|
||||
rID: tbl.RID,
|
||||
tID: t.ID,
|
||||
tableXML: tableXML,
|
||||
Range: t.Ref,
|
||||
Name: t.Name,
|
||||
}
|
||||
if t.TableStyleInfo != nil {
|
||||
table.StyleName = t.TableStyleInfo.Name
|
||||
table.ShowColumnStripes = t.TableStyleInfo.ShowColumnStripes
|
||||
table.ShowFirstColumn = t.TableStyleInfo.ShowFirstColumn
|
||||
table.ShowLastColumn = t.TableStyleInfo.ShowLastColumn
|
||||
table.ShowRowStripes = &t.TableStyleInfo.ShowRowStripes
|
||||
}
|
||||
tables = append(tables, table)
|
||||
}
|
||||
}
|
||||
return tables, err
|
||||
}
|
||||
|
||||
// DeleteTable provides the method to delete table by given table name.
|
||||
func (f *File) DeleteTable(name string) error {
|
||||
if err := checkDefinedName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
tbls, err := f.getTables()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for sheet, tables := range tbls {
|
||||
for _, table := range tables {
|
||||
if table.Name != name {
|
||||
continue
|
||||
}
|
||||
ws, _ := f.workSheetReader(sheet)
|
||||
for i, tbl := range ws.TableParts.TableParts {
|
||||
if tbl.RID == table.rID {
|
||||
ws.TableParts.TableParts = append(ws.TableParts.TableParts[:i], ws.TableParts.TableParts[i+1:]...)
|
||||
f.Pkg.Delete(table.tableXML)
|
||||
_ = f.removeContentTypesPart(ContentTypeSpreadSheetMLTable, "/"+table.tableXML)
|
||||
f.deleteSheetRelationships(sheet, tbl.RID)
|
||||
break
|
||||
}
|
||||
}
|
||||
if ws.TableParts.Count = len(ws.TableParts.TableParts); ws.TableParts.Count == 0 {
|
||||
ws.TableParts = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return newNoExistTableError(name)
|
||||
}
|
||||
|
||||
// getTables provides a function to get all tables in a workbook.
|
||||
func (f *File) getTables() (map[string][]Table, error) {
|
||||
tables := map[string][]Table{}
|
||||
for _, sheetName := range f.GetSheetList() {
|
||||
tbls, err := f.GetTables(sheetName)
|
||||
e := ErrSheetNotExist{sheetName}
|
||||
if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
|
||||
return tables, err
|
||||
}
|
||||
tables[sheetName] = append(tables[sheetName], tbls...)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// countTables provides a function to get table files count storage in the
|
||||
// folder xl/tables.
|
||||
func (f *File) countTables() int {
|
||||
count := 0
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/tables/table") {
|
||||
if strings.Contains(k.(string), "xl/tables/tableSingleCells") {
|
||||
var cells xlsxSingleXMLCells
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
|
||||
Decode(&cells); err != nil && err != io.EOF {
|
||||
count++
|
||||
return true
|
||||
}
|
||||
for _, cell := range cells.SingleXmlCell {
|
||||
if count < cell.ID {
|
||||
count = cell.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(k.(string), "xl/tables/table") {
|
||||
var t xlsxTable
|
||||
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
|
||||
Decode(&t); err != nil && err != io.EOF {
|
||||
count++
|
||||
return true
|
||||
}
|
||||
if count < t.ID {
|
||||
count = t.ID
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
@ -130,58 +267,87 @@ func (f *File) addSheetTable(sheet string, rID int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// setTableHeader provides a function to set cells value in header row for the
|
||||
// setTableColumns provides a function to set cells value in header row for the
|
||||
// table.
|
||||
func (f *File) setTableHeader(sheet string, showHeaderRow bool, x1, y1, x2 int) ([]*xlsxTableColumn, error) {
|
||||
func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int, tbl *xlsxTable) error {
|
||||
var (
|
||||
tableColumns []*xlsxTableColumn
|
||||
idx int
|
||||
header []string
|
||||
tableColumns []*xlsxTableColumn
|
||||
getTableColumn = func(name string) *xlsxTableColumn {
|
||||
if tbl != nil && tbl.TableColumns != nil {
|
||||
for _, column := range tbl.TableColumns.TableColumn {
|
||||
if column.Name == name {
|
||||
return column
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
for i := x1; i <= x2; i++ {
|
||||
idx++
|
||||
cell, err := CoordinatesToCellName(i, y1)
|
||||
if err != nil {
|
||||
return tableColumns, err
|
||||
return err
|
||||
}
|
||||
name, _ := f.GetCellValue(sheet, cell)
|
||||
name, _ := f.GetCellValue(sheet, cell, Options{RawCellValue: true})
|
||||
if _, err := strconv.Atoi(name); err == nil {
|
||||
if showHeaderRow {
|
||||
_ = f.SetCellStr(sheet, cell, name)
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
if name == "" || inStrSlice(header, name, true) != -1 {
|
||||
name = "Column" + strconv.Itoa(idx)
|
||||
if showHeaderRow {
|
||||
_ = f.SetCellStr(sheet, cell, name)
|
||||
}
|
||||
}
|
||||
header = append(header, name)
|
||||
if column := getTableColumn(name); column != nil {
|
||||
column.ID, column.DataDxfID, column.QueryTableFieldID = idx, 0, 0
|
||||
tableColumns = append(tableColumns, column)
|
||||
continue
|
||||
}
|
||||
tableColumns = append(tableColumns, &xlsxTableColumn{
|
||||
ID: idx,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
return tableColumns, nil
|
||||
tbl.TableColumns = &xlsxTableColumns{
|
||||
Count: len(tableColumns),
|
||||
TableColumn: tableColumns,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSheetName check whether there are illegal characters in the table name.
|
||||
// Verify that the name:
|
||||
// checkDefinedName check whether there are illegal characters in the defined
|
||||
// name or table name. Verify that the name:
|
||||
// 1. Starts with a letter or underscore (_)
|
||||
// 2. Doesn't include a space or character that isn't allowed
|
||||
func checkTableName(name string) error {
|
||||
func checkDefinedName(name string) error {
|
||||
if utf8.RuneCountInString(name) > MaxFieldLength {
|
||||
return ErrTableNameLength
|
||||
return ErrNameLength
|
||||
}
|
||||
inCodeRange := func(code int, tbl []int) bool {
|
||||
for i := 0; i < len(tbl); i += 2 {
|
||||
if tbl[i] <= code && code <= tbl[i+1] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for i, c := range name {
|
||||
if string(c) == "_" {
|
||||
if i == 0 {
|
||||
if inCodeRange(int(c), supportedDefinedNameAtStartCharCodeRange) {
|
||||
continue
|
||||
}
|
||||
if unicode.IsLetter(c) {
|
||||
return newInvalidNameError(name)
|
||||
}
|
||||
if inCodeRange(int(c), supportedDefinedNameAfterStartCharCodeRange) {
|
||||
continue
|
||||
}
|
||||
if i > 0 && unicode.IsDigit(c) {
|
||||
continue
|
||||
}
|
||||
return newInvalidTableNameError(name)
|
||||
return newInvalidNameError(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -198,11 +364,10 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
|
|||
y1++
|
||||
}
|
||||
// Correct table range reference, such correct C1:B3 to B1:C3.
|
||||
ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2})
|
||||
ref, err := coordinatesToRangeRef([]int{x1, y1, x2, y2})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2)
|
||||
name := opts.Name
|
||||
if name == "" {
|
||||
name = "Table" + strconv.Itoa(i)
|
||||
|
@ -216,10 +381,6 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
|
|||
AutoFilter: &xlsxAutoFilter{
|
||||
Ref: ref,
|
||||
},
|
||||
TableColumns: &xlsxTableColumns{
|
||||
Count: len(tableColumns),
|
||||
TableColumn: tableColumns,
|
||||
},
|
||||
TableStyleInfo: &xlsxTableStyleInfo{
|
||||
Name: opts.StyleName,
|
||||
ShowFirstColumn: opts.ShowFirstColumn,
|
||||
|
@ -228,13 +389,14 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
|
|||
ShowColumnStripes: opts.ShowColumnStripes,
|
||||
},
|
||||
}
|
||||
_ = f.setTableColumns(sheet, !hideHeaderRow, x1, y1, x2, &t)
|
||||
if hideHeaderRow {
|
||||
t.AutoFilter = nil
|
||||
t.HeaderRowCount = intPtr(0)
|
||||
}
|
||||
table, _ := xml.Marshal(t)
|
||||
table, err := xml.Marshal(t)
|
||||
f.saveFileList(tableXML, table)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// AutoFilter provides the method to add auto filter in a worksheet by given
|
||||
|
@ -294,7 +456,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
|
|||
// x == *b // ends with b
|
||||
// x != *b // doesn't end with b
|
||||
// x == *b* // contains b
|
||||
// x != *b* // doesn't contains b
|
||||
// x != *b* // doesn't contain b
|
||||
//
|
||||
// You can also use '*' to match any character or number and '?' to match any
|
||||
// single character or number. No other regular expression quantifier is
|
||||
|
@ -315,8 +477,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
|
|||
}
|
||||
_ = sortCoordinates(coordinates)
|
||||
// Correct reference range, such correct C1:B3 to B1:C3.
|
||||
ref, _ := f.coordinatesToRangeRef(coordinates, true)
|
||||
filterDB := "_xlnm._FilterDatabase"
|
||||
ref, _ := coordinatesToRangeRef(coordinates, true)
|
||||
wb, err := f.workbookReader()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -327,7 +488,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
|
|||
}
|
||||
filterRange := fmt.Sprintf("'%s'!%s", sheet, ref)
|
||||
d := xlsxDefinedName{
|
||||
Name: filterDB,
|
||||
Name: builtInDefinedNames[3],
|
||||
Hidden: true,
|
||||
LocalSheetID: intPtr(sheetID),
|
||||
Data: filterRange,
|
||||
|
@ -339,8 +500,11 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
|
|||
} else {
|
||||
var definedNameExists bool
|
||||
for idx := range wb.DefinedNames.DefinedName {
|
||||
definedName := wb.DefinedNames.DefinedName[idx]
|
||||
if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden {
|
||||
definedName, localSheetID := wb.DefinedNames.DefinedName[idx], 0
|
||||
if definedName.LocalSheetID != nil {
|
||||
localSheetID = *definedName.LocalSheetID
|
||||
}
|
||||
if definedName.Name == builtInDefinedNames[3] && localSheetID == sheetID && definedName.Hidden {
|
||||
wb.DefinedNames.DefinedName[idx].Data = filterRange
|
||||
definedNameExists = true
|
||||
}
|
||||
|
@ -378,13 +542,12 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilter
|
|||
}
|
||||
offset := fsCol - col
|
||||
if offset < 0 || offset > columns {
|
||||
return fmt.Errorf("incorrect index of column '%s'", opt.Column)
|
||||
return newInvalidAutoFilterColumnError(opt.Column)
|
||||
}
|
||||
fc := &xlsxFilterColumn{ColID: offset}
|
||||
re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
|
||||
token := re.FindAllString(opt.Expression, -1)
|
||||
token := expressionFormat.FindAllString(opt.Expression, -1)
|
||||
if len(token) != 3 && len(token) != 7 {
|
||||
return fmt.Errorf("incorrect number of tokens in criteria '%s'", opt.Expression)
|
||||
return newInvalidAutoFilterExpError(opt.Expression)
|
||||
}
|
||||
expressions, tokens, err := f.parseFilterExpression(opt.Expression, token)
|
||||
if err != nil {
|
||||
|
@ -405,17 +568,19 @@ func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string)
|
|||
var filters []*xlsxFilter
|
||||
filters = append(filters, &xlsxFilter{Val: tokens[0]})
|
||||
fc.Filters = &xlsxFilters{Filter: filters}
|
||||
} else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
|
||||
return
|
||||
}
|
||||
if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
|
||||
// Double equality with "or" operator.
|
||||
var filters []*xlsxFilter
|
||||
for _, v := range tokens {
|
||||
filters = append(filters, &xlsxFilter{Val: v})
|
||||
}
|
||||
fc.Filters = &xlsxFilters{Filter: filters}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
// Non default custom filter.
|
||||
expRel := map[int]int{0: 0, 1: 2}
|
||||
andRel := map[int]bool{0: true, 1: false}
|
||||
expRel, andRel := map[int]int{0: 0, 1: 2}, map[int]bool{0: true, 1: false}
|
||||
for k, v := range tokens {
|
||||
f.writeCustomFilter(fc, exp[expRel[k]], v)
|
||||
if k == 1 {
|
||||
|
@ -423,7 +588,6 @@ func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string)
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeCustomFilter provides a function to write the <customFilter> element.
|
||||
func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string) {
|
||||
|
@ -442,12 +606,12 @@ func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string)
|
|||
}
|
||||
if fc.CustomFilters != nil {
|
||||
fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
var customFilters []*xlsxCustomFilter
|
||||
customFilters = append(customFilters, &customFilter)
|
||||
fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
|
||||
}
|
||||
}
|
||||
|
||||
// parseFilterExpression provides a function to converts the tokens of a
|
||||
// possibly conditional expression into 1 or 2 sub expressions for further
|
||||
|
@ -463,10 +627,8 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int,
|
|||
if len(tokens) == 7 {
|
||||
// The number of tokens will be either 3 (for 1 expression) or 7 (for 2
|
||||
// expressions).
|
||||
conditional := 0
|
||||
c := tokens[3]
|
||||
re, _ := regexp.Match(`(or|\|\|)`, []byte(c))
|
||||
if re {
|
||||
conditional, c := 0, tokens[3]
|
||||
if conditionFormat.MatchString(c) {
|
||||
conditional = 1
|
||||
}
|
||||
expression1, token1, err := f.parseFilterTokens(expression, tokens[:3])
|
||||
|
@ -477,17 +639,13 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int,
|
|||
if err != nil {
|
||||
return expressions, t, err
|
||||
}
|
||||
expressions = []int{expression1[0], conditional, expression2[0]}
|
||||
t = []string{token1, token2}
|
||||
} else {
|
||||
return []int{expression1[0], conditional, expression2[0]}, []string{token1, token2}, nil
|
||||
}
|
||||
exp, token, err := f.parseFilterTokens(expression, tokens)
|
||||
if err != nil {
|
||||
return expressions, t, err
|
||||
}
|
||||
expressions = exp
|
||||
t = []string{token}
|
||||
}
|
||||
return expressions, t, nil
|
||||
return exp, []string{token}, nil
|
||||
}
|
||||
|
||||
// parseFilterTokens provides a function to parse the 3 tokens of a filter
|
||||
|
@ -510,15 +668,15 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
|
|||
operator, ok := operators[strings.ToLower(tokens[1])]
|
||||
if !ok {
|
||||
// Convert the operator from a number to a descriptive string.
|
||||
return []int{}, "", fmt.Errorf("unknown operator: %s", tokens[1])
|
||||
return []int{}, "", newUnknownFilterTokenError(tokens[1])
|
||||
}
|
||||
token := tokens[2]
|
||||
// Special handling for Blanks/NonBlanks.
|
||||
re, _ := regexp.Match("blanks|nonblanks", []byte(strings.ToLower(token)))
|
||||
re := blankFormat.MatchString(strings.ToLower(token))
|
||||
if re {
|
||||
// Only allow Equals or NotEqual in this context.
|
||||
if operator != 2 && operator != 5 {
|
||||
return []int{operator}, token, fmt.Errorf("the operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression)
|
||||
return []int{operator}, token, newInvalidAutoFilterOperatorError(tokens[1], expression)
|
||||
}
|
||||
token = strings.ToLower(token)
|
||||
// The operator should always be 2 (=) to flag a "simple" equality in
|
||||
|
@ -539,8 +697,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
|
|||
}
|
||||
// If the string token contains an Excel match character then change the
|
||||
// operator type to indicate a non "simple" equality.
|
||||
re, _ = regexp.Match("[*?]", []byte(token))
|
||||
if operator == 2 && re {
|
||||
if re = matchFormat.MatchString(token); operator == 2 && re {
|
||||
operator = 22
|
||||
}
|
||||
return []int{operator}, token, nil
|
||||
|
|
133
table_test.go
133
table_test.go
|
@ -27,7 +27,13 @@ func TestAddTable(t *testing.T) {
|
|||
ShowHeaderRow: boolPtr(false),
|
||||
}))
|
||||
assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"}))
|
||||
// Test get tables in worksheet
|
||||
tables, err := f.GetTables("Sheet2")
|
||||
assert.Len(t, tables, 3)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test add table with already exist table name
|
||||
assert.Equal(t, f.AddTable("Sheet2", &Table{Name: "Table1"}), ErrExistsTableName)
|
||||
// Test add table with invalid table options
|
||||
assert.Equal(t, f.AddTable("Sheet1", nil), ErrParameterInvalid)
|
||||
// Test add table in not exist worksheet
|
||||
|
@ -39,36 +45,101 @@ func TestAddTable(t *testing.T) {
|
|||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx")))
|
||||
|
||||
// Test add table with invalid sheet name
|
||||
assert.EqualError(t, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}))
|
||||
// Test addTable with illegal cell reference
|
||||
f = NewFile()
|
||||
assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]")
|
||||
assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]")
|
||||
// Test add table with invalid table name
|
||||
assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil))
|
||||
assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil))
|
||||
// Test set defined name and add table with invalid name
|
||||
for _, cases := range []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{name: "1Table", err: newInvalidTableNameError("1Table")},
|
||||
{name: "-Table", err: newInvalidTableNameError("-Table")},
|
||||
{name: "'Table", err: newInvalidTableNameError("'Table")},
|
||||
{name: "Table 1", err: newInvalidTableNameError("Table 1")},
|
||||
{name: "A&B", err: newInvalidTableNameError("A&B")},
|
||||
{name: "_1Table'", err: newInvalidTableNameError("_1Table'")},
|
||||
{name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidTableNameError("\u0f5f\u0fb3\u0f0b\u0f21")},
|
||||
{name: strings.Repeat("c", MaxFieldLength+1), err: ErrTableNameLength},
|
||||
{name: "1Table", err: newInvalidNameError("1Table")},
|
||||
{name: "-Table", err: newInvalidNameError("-Table")},
|
||||
{name: "'Table", err: newInvalidNameError("'Table")},
|
||||
{name: "Table 1", err: newInvalidNameError("Table 1")},
|
||||
{name: "A&B", err: newInvalidNameError("A&B")},
|
||||
{name: "_1Table'", err: newInvalidNameError("_1Table'")},
|
||||
{name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidNameError("\u0f5f\u0fb3\u0f0b\u0f21")},
|
||||
{name: strings.Repeat("c", MaxFieldLength+1), err: ErrNameLength},
|
||||
} {
|
||||
assert.EqualError(t, f.AddTable("Sheet1", &Table{
|
||||
assert.Equal(t, cases.err, f.AddTable("Sheet1", &Table{
|
||||
Range: "A1:B2",
|
||||
Name: cases.name,
|
||||
}), cases.err.Error())
|
||||
}))
|
||||
assert.Equal(t, cases.err, f.SetDefinedName(&DefinedName{
|
||||
Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5",
|
||||
}))
|
||||
}
|
||||
// Test check duplicate table name with unsupported charset table parts
|
||||
f = NewFile()
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
|
||||
assert.NoError(t, f.Close())
|
||||
f = NewFile()
|
||||
// Test add table with workbook with single cells parts
|
||||
f.Pkg.Store("xl/tables/tableSingleCells1.xml", []byte("<singleXmlCells><singleXmlCell id=\"2\" r=\"A1\" connectionId=\"2\" /></singleXmlCells>"))
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
|
||||
// Test add table with workbook with unsupported charset single cells parts
|
||||
f.Pkg.Store("xl/tables/tableSingleCells1.xml", MacintoshCyrillicCharset)
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestSetTableHeader(t *testing.T) {
|
||||
func TestGetTables(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.setTableHeader("Sheet1", true, 1, 0, 1)
|
||||
assert.EqualError(t, err, "invalid cell reference [1, 0]")
|
||||
// Test get tables in none table worksheet
|
||||
tables, err := f.GetTables("Sheet1")
|
||||
assert.Len(t, tables, 0)
|
||||
assert.NoError(t, err)
|
||||
// Test get tables in not exist worksheet
|
||||
_, err = f.GetTables("SheetN")
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
// Test adjust table with unsupported charset
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21"}))
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetTables("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test adjust table with no exist table parts
|
||||
f.Pkg.Delete("xl/tables/table1.xml")
|
||||
tables, err = f.GetTables("Sheet1")
|
||||
assert.Len(t, tables, 0)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteTable(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B4", Name: "Table1"}))
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21", Name: "Table2"}))
|
||||
assert.NoError(t, f.DeleteTable("Table2"))
|
||||
assert.NoError(t, f.DeleteTable("Table1"))
|
||||
// Test delete table with invalid table name
|
||||
assert.Equal(t, newInvalidNameError("Table 1"), f.DeleteTable("Table 1"))
|
||||
// Test delete table with no exist table name
|
||||
assert.Equal(t, newNoExistTableError("Table"), f.DeleteTable("Table"))
|
||||
// Test delete table with unsupported charset
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeleteTable("Table1"), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test delete table without deleting table header
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Date"))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Values"))
|
||||
assert.NoError(t, f.UpdateLinkedValue())
|
||||
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2", Name: "Table1"}))
|
||||
assert.NoError(t, f.DeleteTable("Table1"))
|
||||
val, err := f.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Date", val)
|
||||
val, err = f.GetCellValue("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Values", val)
|
||||
}
|
||||
|
||||
func TestSetTableColumns(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.Equal(t, newCoordinatesToCellNameError(1, 0), f.setTableColumns("Sheet1", true, 1, 0, 1, nil))
|
||||
}
|
||||
|
||||
func TestAutoFilter(t *testing.T) {
|
||||
|
@ -93,14 +164,18 @@ func TestAutoFilter(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test add auto filter with invalid sheet name
|
||||
assert.EqualError(t, f.AutoFilter("Sheet:1", "A1:B1", nil), ErrSheetNameInvalid.Error())
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.AutoFilter("Sheet:1", "A1:B1", nil))
|
||||
// Test add auto filter with illegal cell reference
|
||||
assert.EqualError(t, f.AutoFilter("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
assert.EqualError(t, f.AutoFilter("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AutoFilter("Sheet1", "A:B1", nil))
|
||||
assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), f.AutoFilter("Sheet1", "A1:B", nil))
|
||||
// Test add auto filter with unsupported charset workbook
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test add auto filter with empty local sheet ID
|
||||
f = NewFile()
|
||||
f.WorkBook = &xlsxWorkbook{DefinedNames: &xlsxDefinedNames{DefinedName: []xlsxDefinedName{{Name: builtInDefinedNames[3], Hidden: true}}}}
|
||||
assert.NoError(t, f.AutoFilter("Sheet1", "A1:B1", nil))
|
||||
}
|
||||
|
||||
func TestAutoFilterError(t *testing.T) {
|
||||
|
@ -122,22 +197,22 @@ func TestAutoFilterError(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{
|
||||
Column: "A",
|
||||
Expression: "",
|
||||
}}), "sheet SheetN does not exist")
|
||||
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
|
||||
}}))
|
||||
assert.Equal(t, newInvalidColumnNameError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
|
||||
Column: "-",
|
||||
Expression: "-",
|
||||
}}), newInvalidColumnNameError("-").Error())
|
||||
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{
|
||||
}}))
|
||||
assert.Equal(t, newInvalidAutoFilterColumnError("A"), f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{
|
||||
Column: "A",
|
||||
Expression: "-",
|
||||
}}), `incorrect index of column 'A'`)
|
||||
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
|
||||
}}))
|
||||
assert.Equal(t, newInvalidAutoFilterExpError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
|
||||
Column: "A",
|
||||
Expression: "-",
|
||||
}}), `incorrect number of tokens in criteria '-'`)
|
||||
}}))
|
||||
}
|
||||
|
||||
func TestParseFilterTokens(t *testing.T) {
|
||||
|
@ -147,5 +222,5 @@ func TestParseFilterTokens(t *testing.T) {
|
|||
assert.EqualError(t, err, "unknown operator: !")
|
||||
// Test invalid operator in context
|
||||
_, _, err = f.parseFilterTokens("", []string{"", "<", "x != blanks"})
|
||||
assert.EqualError(t, err, "the operator '<' in expression '' is not valid in relation to Blanks/NonBlanks'")
|
||||
assert.Equal(t, newInvalidAutoFilterOperatorError("<", ""), err)
|
||||
}
|
||||
|
|
492
templates.go
492
templates.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,26 +7,510 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
//
|
||||
// This file contains default templates for XML files we don't yet populated
|
||||
// based on content.
|
||||
|
||||
package excelize
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Source relationship and namespace list, associated prefixes and schema in which it was
|
||||
// introduced.
|
||||
var (
|
||||
NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"}
|
||||
NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"}
|
||||
NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"}
|
||||
NameSpaceDrawingMLA14 = xml.Attr{Name: xml.Name{Local: "a14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/main"}
|
||||
NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
|
||||
NameSpaceDrawingMLSlicer = xml.Attr{Name: xml.Name{Local: "sle", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/slicer"}
|
||||
NameSpaceDrawingMLSlicerX15 = xml.Attr{Name: xml.Name{Local: "sle15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2012/slicer"}
|
||||
NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}
|
||||
NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"}
|
||||
NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
||||
NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
|
||||
NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
|
||||
NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
|
||||
NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"}
|
||||
SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
|
||||
SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"}
|
||||
SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"}
|
||||
SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"}
|
||||
SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"}
|
||||
)
|
||||
|
||||
// Source relationship and namespace.
|
||||
const (
|
||||
ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
|
||||
ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
|
||||
ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
|
||||
ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
|
||||
ContentTypeRelationships = "application/vnd.openxmlformats-package.relationships+xml"
|
||||
ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
||||
ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml"
|
||||
ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+xml"
|
||||
ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
|
||||
ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
|
||||
ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
|
||||
ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
||||
ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
|
||||
ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
|
||||
ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
||||
ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
|
||||
ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
|
||||
ContentTypeVBA = "application/vnd.ms-office.vbaProject"
|
||||
ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
|
||||
NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
|
||||
NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/"
|
||||
NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/"
|
||||
NameSpaceExtendedProperties = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||
NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
|
||||
NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
|
||||
SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
|
||||
SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
||||
SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
|
||||
SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
|
||||
SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
|
||||
SourceRelationshipExtendProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
|
||||
SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||
SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||||
SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
|
||||
SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
|
||||
SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
|
||||
SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer"
|
||||
SourceRelationshipSlicerCache = "http://schemas.microsoft.com/office/2007/relationships/slicerCache"
|
||||
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
|
||||
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
|
||||
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
|
||||
StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
|
||||
StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
|
||||
StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main"
|
||||
StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships"
|
||||
StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart"
|
||||
StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments"
|
||||
StrictSourceRelationshipExtendProperties = "http://purl.oclc.org/ooxml/officeDocument/relationships/extendedProperties"
|
||||
StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image"
|
||||
StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
|
||||
// The following constants defined the extLst child element
|
||||
// ([ISO/IEC29500-1:2016] section 18.2.10) of the workbook and worksheet
|
||||
// elements extended by the addition of new child ext elements.
|
||||
ExtURICalcFeatures = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"
|
||||
ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
|
||||
ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
|
||||
ExtURIDataField = "{E15A36E0-9728-4E99-A89B-3F7291B0FE68}"
|
||||
ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}"
|
||||
ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
|
||||
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
|
||||
ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}"
|
||||
ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
|
||||
ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
|
||||
ExtURIModelTimeGroupings = "{9835A34E-60A6-4A7C-AAB8-D5F71C897F49}"
|
||||
ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}"
|
||||
ExtURIPivotCachesX14 = "{876F7934-8845-4945-9796-88D515C7AA90}"
|
||||
ExtURIPivotCachesX15 = "{841E416B-1EF1-43b6-AB56-02D37102CBD5}"
|
||||
ExtURIPivotField = "{2946ED86-A175-432a-8AC1-64E0C546D7DE}"
|
||||
ExtURIPivotFilter = "{0605FD5F-26C8-4aeb-8148-2DB25E43C511}"
|
||||
ExtURIPivotHierarchy = "{F1805F06-0CD304483-9156-8803C3D141DF}"
|
||||
ExtURIPivotTableReferences = "{983426D0-5260-488c-9760-48F4B6AC55F4}"
|
||||
ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
|
||||
ExtURISlicerCacheDefinition = "{2F2917AC-EB37-4324-AD4E-5DD8C200BD13}"
|
||||
ExtURISlicerCacheHideItemsWithNoData = "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}"
|
||||
ExtURISlicerCachesX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
|
||||
ExtURISlicerCachesX15 = "{46BE6895-7355-4a93-B00E-2C351335B9C9}"
|
||||
ExtURISlicerListX14 = "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"
|
||||
ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}"
|
||||
ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
|
||||
ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
|
||||
ExtURITimelineCachePivotCaches = "{A2CB5862-8E78-49c6-8D9D-AF26E26ADB89}"
|
||||
ExtURITimelineCacheRefs = "{D0CA8CA8-9F24-4464-BF8E-62219DCF47F9}"
|
||||
ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
|
||||
ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
|
||||
ExtURIWorkbookPrX14 = "{79F54976-1DA5-4618-B147-ACDE4B953A38}"
|
||||
ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}"
|
||||
)
|
||||
|
||||
// workbookExtURIPriority is the priority of URI in the workbook extension lists.
|
||||
var workbookExtURIPriority = []string{
|
||||
ExtURIPivotCachesX14,
|
||||
ExtURISlicerCachesX14,
|
||||
ExtURISlicerCachesX15,
|
||||
ExtURIWorkbookPrX14,
|
||||
ExtURIPivotCachesX15,
|
||||
ExtURIPivotTableReferences,
|
||||
ExtURITimelineCachePivotCaches,
|
||||
ExtURITimelineCacheRefs,
|
||||
ExtURIWorkbookPrX15,
|
||||
ExtURIDataModel,
|
||||
ExtURICalcFeatures,
|
||||
ExtURIExternalLinkPr,
|
||||
ExtURIModelTimeGroupings,
|
||||
}
|
||||
|
||||
// worksheetExtURIPriority is the priority of URI in the worksheet extension lists.
|
||||
var worksheetExtURIPriority = []string{
|
||||
ExtURIConditionalFormattings,
|
||||
ExtURIDataValidations,
|
||||
ExtURISparklineGroups,
|
||||
ExtURISlicerListX14,
|
||||
ExtURIProtectedRanges,
|
||||
ExtURIIgnoredErrors,
|
||||
ExtURIWebExtensions,
|
||||
ExtURISlicerListX15,
|
||||
ExtURITimelineRefs,
|
||||
ExtURIExternalLinkPr,
|
||||
}
|
||||
|
||||
// Excel specifications and limits
|
||||
const (
|
||||
MaxCellStyles = 65430
|
||||
MaxColumns = 16384
|
||||
MaxColumnWidth = 255
|
||||
MaxFieldLength = 255
|
||||
MaxFilePathLength = 207
|
||||
MaxFormControlValue = 30000
|
||||
MaxFontFamilyLength = 31
|
||||
MaxFontSize = 409
|
||||
MaxRowHeight = 409
|
||||
MaxSheetNameLength = 31
|
||||
MinColumns = 1
|
||||
MinFontSize = 1
|
||||
StreamChunkSize = 1 << 24
|
||||
TotalCellChars = 32767
|
||||
TotalRows = 1048576
|
||||
TotalSheetHyperlinks = 65529
|
||||
UnzipSizeLimit = 1000 << 24
|
||||
// pivotTableVersion should be greater than 3. One or more of the
|
||||
// PivotTables chosen are created in a version of Excel earlier than
|
||||
// Excel 2007 or in compatibility mode. Slicer can only be used with
|
||||
// PivotTables created in Excel 2007 or a newer version of Excel.
|
||||
pivotTableVersion = 3
|
||||
pivotTableRefreshedVersion = 8
|
||||
defaultDrawingScale = 1.0
|
||||
defaultChartDimensionWidth = 480
|
||||
defaultChartDimensionHeight = 260
|
||||
defaultSlicerWidth = 200
|
||||
defaultSlicerHeight = 200
|
||||
defaultChartLegendPosition = "bottom"
|
||||
defaultChartShowBlanksAs = "gap"
|
||||
defaultShapeSize = 160
|
||||
defaultShapeLineWidth = 1
|
||||
)
|
||||
|
||||
// ColorMappingType is the type of color transformation.
|
||||
type ColorMappingType byte
|
||||
|
||||
// Color transformation types enumeration.
|
||||
const (
|
||||
ColorMappingTypeLight1 ColorMappingType = iota
|
||||
ColorMappingTypeDark1
|
||||
ColorMappingTypeLight2
|
||||
ColorMappingTypeDark2
|
||||
ColorMappingTypeAccent1
|
||||
ColorMappingTypeAccent2
|
||||
ColorMappingTypeAccent3
|
||||
ColorMappingTypeAccent4
|
||||
ColorMappingTypeAccent5
|
||||
ColorMappingTypeAccent6
|
||||
ColorMappingTypeHyperlink
|
||||
ColorMappingTypeFollowedHyperlink
|
||||
ColorMappingTypeUnset int = -1
|
||||
)
|
||||
|
||||
// ChartDataLabelPositionType is the type of chart data labels position.
|
||||
type ChartDataLabelPositionType byte
|
||||
|
||||
// Chart data labels positions types enumeration.
|
||||
const (
|
||||
ChartDataLabelsPositionUnset ChartDataLabelPositionType = iota
|
||||
ChartDataLabelsPositionBestFit
|
||||
ChartDataLabelsPositionBelow
|
||||
ChartDataLabelsPositionCenter
|
||||
ChartDataLabelsPositionInsideBase
|
||||
ChartDataLabelsPositionInsideEnd
|
||||
ChartDataLabelsPositionLeft
|
||||
ChartDataLabelsPositionOutsideEnd
|
||||
ChartDataLabelsPositionRight
|
||||
ChartDataLabelsPositionAbove
|
||||
)
|
||||
|
||||
// chartDataLabelsPositionTypes defined supported chart data labels position
|
||||
// types.
|
||||
var chartDataLabelsPositionTypes = map[ChartDataLabelPositionType]string{
|
||||
ChartDataLabelsPositionBestFit: "bestFit",
|
||||
ChartDataLabelsPositionBelow: "b",
|
||||
ChartDataLabelsPositionCenter: "ctr",
|
||||
ChartDataLabelsPositionInsideBase: "inBase",
|
||||
ChartDataLabelsPositionInsideEnd: "inEnd",
|
||||
ChartDataLabelsPositionLeft: "l",
|
||||
ChartDataLabelsPositionOutsideEnd: "outEnd",
|
||||
ChartDataLabelsPositionRight: "r",
|
||||
ChartDataLabelsPositionAbove: "t",
|
||||
}
|
||||
|
||||
// supportedChartDataLabelsPosition defined supported chart data labels position
|
||||
// types for each type of chart.
|
||||
var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionType{
|
||||
Bar: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
|
||||
BarStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
|
||||
BarPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
|
||||
Col: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
|
||||
ColStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
|
||||
ColPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
|
||||
Line: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
|
||||
Pie: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
|
||||
Pie3D: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
|
||||
Scatter: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
|
||||
Bubble: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
|
||||
Bubble3D: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTempFileSST = "sharedStrings"
|
||||
defaultXMLMetadata = "xl/metadata.xml"
|
||||
defaultXMLPathCalcChain = "xl/calcChain.xml"
|
||||
defaultXMLPathCellImages = "xl/cellimages.xml"
|
||||
defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels"
|
||||
defaultXMLPathContentTypes = "[Content_Types].xml"
|
||||
defaultXMLPathDocPropsApp = "docProps/app.xml"
|
||||
defaultXMLPathDocPropsCore = "docProps/core.xml"
|
||||
defaultXMLPathCalcChain = "xl/calcChain.xml"
|
||||
defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
|
||||
defaultXMLPathStyles = "xl/styles.xml"
|
||||
defaultXMLPathTheme = "xl/theme/theme1.xml"
|
||||
defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml"
|
||||
defaultXMLPathWorkbook = "xl/workbook.xml"
|
||||
defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels"
|
||||
defaultTempFileSST = "sharedStrings"
|
||||
defaultXMLRdRichValuePart = "xl/richData/rdrichvalue.xml"
|
||||
defaultXMLRdRichValueRel = "xl/richData/richValueRel.xml"
|
||||
defaultXMLRdRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels"
|
||||
defaultXMLRdRichValueWebImagePart = "xl/richData/rdRichValueWebImage.xml"
|
||||
defaultXMLRdRichValueWebImagePartRels = "xl/richData/_rels/rdRichValueWebImage.xml.rels"
|
||||
)
|
||||
|
||||
// IndexedColorMapping is the table of default mappings from indexed color value
|
||||
// to RGB value. Note that 0-7 are redundant of 8-15 to preserve backwards
|
||||
// compatibility. A legacy indexing scheme for colors that is still required
|
||||
// for some records, and for backwards compatibility with legacy formats. This
|
||||
// element contains a sequence of RGB color values that correspond to color
|
||||
// indexes (zero-based). When using the default indexed color palette, the
|
||||
// values are not written out, but instead are implied. When the color palette
|
||||
// has been modified from default, then the entire color palette is written
|
||||
// out.
|
||||
var IndexedColorMapping = []string{
|
||||
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
|
||||
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
|
||||
"800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080",
|
||||
"9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF",
|
||||
"000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF",
|
||||
"00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99",
|
||||
"3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696",
|
||||
"003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333",
|
||||
"000000", "FFFFFF",
|
||||
}
|
||||
|
||||
// supportedDefinedNameAtStartCharCodeRange list the valid first character of a
|
||||
// defined name ASCII letters.
|
||||
var supportedDefinedNameAtStartCharCodeRange = []int{
|
||||
65, 90, 92, 92, 95, 95, 97, 122, 161, 161, 164, 164,
|
||||
167, 168, 170, 170, 173, 173, 175, 186, 188, 696, 699, 705,
|
||||
711, 711, 713, 715, 717, 717, 720, 721, 728, 731, 733, 733,
|
||||
736, 740, 750, 750, 880, 883, 886, 887, 890, 893, 902, 902,
|
||||
904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1315,
|
||||
1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1610,
|
||||
1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788,
|
||||
1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026,
|
||||
2036, 2037, 2042, 2042, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401,
|
||||
2417, 2418, 2427, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480,
|
||||
2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
|
||||
2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611,
|
||||
2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701,
|
||||
2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749,
|
||||
2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864,
|
||||
2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
|
||||
2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972,
|
||||
2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084,
|
||||
3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161,
|
||||
3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257,
|
||||
3261, 3261, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368,
|
||||
3370, 3385, 3389, 3389, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505,
|
||||
3507, 3515, 3517, 3517, 3520, 3526, 3585, 3642, 3648, 3662, 3713, 3714,
|
||||
3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
|
||||
3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763,
|
||||
3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911,
|
||||
3913, 3948, 3976, 3979, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189,
|
||||
4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293,
|
||||
4304, 4346, 4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680,
|
||||
4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749,
|
||||
4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822,
|
||||
4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
|
||||
5743, 5750, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905,
|
||||
5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103,
|
||||
6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6400, 6428, 6480, 6509,
|
||||
6512, 6516, 6528, 6569, 6593, 6599, 6656, 6678, 6917, 6963, 6981, 6987,
|
||||
7043, 7072, 7086, 7087, 7168, 7203, 7245, 7247, 7258, 7293, 7424, 7615,
|
||||
7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025,
|
||||
8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126,
|
||||
8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180,
|
||||
8182, 8188, 8208, 8208, 8211, 8214, 8216, 8216, 8220, 8221, 8224, 8225,
|
||||
8229, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8305, 8305,
|
||||
8308, 8308, 8319, 8319, 8321, 8324, 8336, 8340, 8450, 8451, 8453, 8453,
|
||||
8455, 8455, 8457, 8467, 8469, 8470, 8473, 8477, 8481, 8482, 8484, 8484,
|
||||
8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
|
||||
8526, 8526, 8531, 8532, 8539, 8542, 8544, 8584, 8592, 8601, 8658, 8658,
|
||||
8660, 8660, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719,
|
||||
8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741,
|
||||
8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780,
|
||||
8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835,
|
||||
8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978,
|
||||
9312, 9397, 9424, 9449, 9472, 9547, 9552, 9588, 9601, 9615, 9618, 9621,
|
||||
9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665,
|
||||
9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734,
|
||||
9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794,
|
||||
9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 11264, 11310,
|
||||
11312, 11358, 11360, 11375, 11377, 11389, 11392, 11492, 11520, 11557, 11568, 11621,
|
||||
11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710,
|
||||
11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12288, 12291, 12293, 12311,
|
||||
12317, 12319, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447,
|
||||
12449, 12543, 12549, 12589, 12593, 12686, 12704, 12727, 12784, 12828, 12832, 12841,
|
||||
12849, 12850, 12857, 12857, 12896, 12923, 12927, 12927, 12963, 12968, 13059, 13059,
|
||||
13069, 13069, 13076, 13076, 13080, 13080, 13090, 13091, 13094, 13095, 13099, 13099,
|
||||
13110, 13110, 13115, 13115, 13129, 13130, 13133, 13133, 13137, 13137, 13143, 13143,
|
||||
13179, 13182, 13184, 13188, 13192, 13258, 13261, 13267, 13269, 13270, 13272, 13272,
|
||||
13275, 13277, 13312, 19893, 19968, 40899, 40960, 42124, 42240, 42508, 42512, 42527,
|
||||
42538, 42539, 42560, 42591, 42594, 42606, 42624, 42647, 42786, 42887, 42891, 42892,
|
||||
43003, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187,
|
||||
43274, 43301, 43312, 43334, 43520, 43560, 43584, 43586, 43588, 43595, 44032, 55203,
|
||||
57344, 63560, 63744, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279,
|
||||
64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321,
|
||||
64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
|
||||
65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131,
|
||||
65136, 65140, 65142, 65276, 65281, 65374, 65377, 65470, 65474, 65479, 65482, 65487,
|
||||
65490, 65495, 65498, 65500, 65504, 65510,
|
||||
}
|
||||
|
||||
// supportedDefinedNameAfterStartCharCodeRange list the valid after first
|
||||
// character of a defined name ASCII letters.
|
||||
var supportedDefinedNameAfterStartCharCodeRange = []int{
|
||||
46, 46, 48, 57, 63, 63, 65, 90, 92, 92, 95, 95,
|
||||
97, 122, 161, 161, 164, 164, 167, 168, 170, 170, 173, 173,
|
||||
175, 186, 188, 887, 890, 893, 900, 902, 904, 906, 908, 908,
|
||||
910, 929, 931, 1315, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469,
|
||||
1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522,
|
||||
1536, 1539, 1542, 1544, 1547, 1547, 1550, 1562, 1567, 1567, 1569, 1630,
|
||||
1632, 1641, 1646, 1747, 1749, 1791, 1807, 1866, 1869, 1969, 1984, 2038,
|
||||
2042, 2042, 2305, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415,
|
||||
2417, 2418, 2427, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472,
|
||||
2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510,
|
||||
2519, 2519, 2524, 2525, 2527, 2531, 2534, 2554, 2561, 2563, 2565, 2570,
|
||||
2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617,
|
||||
2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652,
|
||||
2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728,
|
||||
2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765,
|
||||
2768, 2768, 2784, 2787, 2790, 2799, 2801, 2801, 2817, 2819, 2821, 2828,
|
||||
2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884,
|
||||
2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2929,
|
||||
2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972,
|
||||
2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016,
|
||||
3018, 3021, 3024, 3024, 3031, 3031, 3046, 3066, 3073, 3075, 3077, 3084,
|
||||
3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144,
|
||||
3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3192, 3199,
|
||||
3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257,
|
||||
3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299,
|
||||
3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368,
|
||||
3370, 3385, 3389, 3396, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3427,
|
||||
3430, 3445, 3449, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515,
|
||||
3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551,
|
||||
3570, 3571, 3585, 3642, 3647, 3662, 3664, 3673, 3713, 3714, 3716, 3716,
|
||||
3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747,
|
||||
3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780,
|
||||
3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3843, 3859, 3897,
|
||||
3902, 3911, 3913, 3948, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028,
|
||||
4030, 4044, 4046, 4047, 4096, 4169, 4176, 4249, 4254, 4293, 4304, 4346,
|
||||
4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680, 4682, 4685,
|
||||
4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784,
|
||||
4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
|
||||
4882, 4885, 4888, 4954, 4959, 4960, 4969, 4988, 4992, 5017, 5024, 5108,
|
||||
5121, 5740, 5743, 5750, 5760, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
|
||||
5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003,
|
||||
6016, 6099, 6103, 6103, 6107, 6109, 6112, 6121, 6128, 6137, 6155, 6158,
|
||||
6160, 6169, 6176, 6263, 6272, 6314, 6400, 6428, 6432, 6443, 6448, 6459,
|
||||
6464, 6464, 6470, 6509, 6512, 6516, 6528, 6569, 6576, 6601, 6608, 6617,
|
||||
6624, 6683, 6912, 6987, 6992, 7001, 7009, 7036, 7040, 7082, 7086, 7097,
|
||||
7168, 7223, 7232, 7241, 7245, 7293, 7424, 7654, 7678, 7957, 7960, 7965,
|
||||
7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029,
|
||||
8031, 8061, 8064, 8116, 8118, 8132, 8134, 8147, 8150, 8155, 8157, 8175,
|
||||
8178, 8180, 8182, 8190, 8192, 8208, 8211, 8214, 8216, 8216, 8220, 8221,
|
||||
8224, 8225, 8229, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8260, 8260,
|
||||
8274, 8274, 8287, 8292, 8298, 8305, 8308, 8316, 8319, 8332, 8336, 8340,
|
||||
8352, 8373, 8400, 8432, 8448, 8527, 8531, 8584, 8592, 9000, 9003, 9191,
|
||||
9216, 9254, 9280, 9290, 9312, 9885, 9888, 9916, 9920, 9923, 9985, 9988,
|
||||
9990, 9993, 9996, 10023, 10025, 10059, 10061, 10061, 10063, 10066, 10070, 10070,
|
||||
10072, 10078, 10081, 10087, 10102, 10132, 10136, 10159, 10161, 10174, 10176, 10180,
|
||||
10183, 10186, 10188, 10188, 10192, 10213, 10224, 10626, 10649, 10711, 10716, 10747,
|
||||
10750, 11084, 11088, 11092, 11264, 11310, 11312, 11358, 11360, 11375, 11377, 11389,
|
||||
11392, 11498, 11517, 11517, 11520, 11557, 11568, 11621, 11631, 11631, 11648, 11670,
|
||||
11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
|
||||
11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 11904, 11929, 11931, 12019,
|
||||
12032, 12245, 12272, 12283, 12288, 12311, 12317, 12335, 12337, 12348, 12350, 12351,
|
||||
12353, 12438, 12441, 12447, 12449, 12543, 12549, 12589, 12593, 12686, 12688, 12727,
|
||||
12736, 12771, 12784, 12830, 12832, 12867, 12880, 13054, 13056, 19893, 19904, 40899,
|
||||
40960, 42124, 42128, 42182, 42240, 42508, 42512, 42539, 42560, 42591, 42594, 42610,
|
||||
42620, 42621, 42623, 42647, 42752, 42892, 43003, 43051, 43072, 43123, 43136, 43204,
|
||||
43216, 43225, 43264, 43310, 43312, 43347, 43520, 43574, 43584, 43597, 43600, 43609,
|
||||
44032, 55203, 55296, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279,
|
||||
64285, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
|
||||
64467, 64829, 64848, 64911, 64914, 64967, 65008, 65021, 65024, 65039, 65056, 65062,
|
||||
65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131,
|
||||
65136, 65140, 65142, 65276, 65279, 65279, 65281, 65374, 65377, 65470, 65474, 65479,
|
||||
65482, 65487, 65490, 65495, 65498, 65500, 65504, 65510, 65512, 65518, 65529, 65533,
|
||||
}
|
||||
|
||||
// supportedImageTypes defined supported image types.
|
||||
var supportedImageTypes = map[string]string{
|
||||
".bmp": ".bmp", ".emf": ".emf", ".emz": ".emz", ".gif": ".gif",
|
||||
".jpeg": ".jpeg", ".jpg": ".jpeg", ".png": ".png", ".svg": ".svg",
|
||||
".tif": ".tiff", ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz",
|
||||
}
|
||||
|
||||
// supportedContentTypes defined supported file format types.
|
||||
var supportedContentTypes = map[string]string{
|
||||
".xlam": ContentTypeAddinMacro,
|
||||
".xlsm": ContentTypeMacro,
|
||||
".xlsx": ContentTypeSheetML,
|
||||
".xltm": ContentTypeTemplateMacro,
|
||||
".xltx": ContentTypeTemplate,
|
||||
}
|
||||
|
||||
// supportedUnderlineTypes defined supported underline types.
|
||||
var supportedUnderlineTypes = []string{"none", "single", "double"}
|
||||
|
||||
// supportedDrawingUnderlineTypes defined supported underline types in drawing
|
||||
// markup language.
|
||||
var supportedDrawingUnderlineTypes = []string{
|
||||
"none", "words", "sng", "dbl", "heavy", "dotted", "dottedHeavy", "dash", "dashHeavy", "dashLong", "dashLongHeavy", "dotDash", "dotDashHeavy", "dotDotDash", "dotDotDashHeavy", "wavy", "wavyHeavy",
|
||||
"wavyDbl",
|
||||
}
|
||||
|
||||
// supportedDrawingTextVerticalType defined supported text vertical types in
|
||||
// drawing markup language.
|
||||
var supportedDrawingTextVerticalType = []string{"horz", "vert", "vert270", "wordArtVert", "eaVert", "mongolianVert", "wordArtVertRtl"}
|
||||
|
||||
// supportedPositioning defined supported positioning types.
|
||||
var supportedPositioning = []string{"absolute", "oneCell", "twoCell"}
|
||||
|
||||
// supportedPageOrientation defined supported page setup page orientation.
|
||||
var supportedPageOrientation = []string{"portrait", "landscape"}
|
||||
|
||||
// supportedPageOrder defined supported page setup page order.
|
||||
var supportedPageOrder = []string{"overThenDown", "downThenOver"}
|
||||
|
||||
// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix.
|
||||
var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm.Criteria", "_xlnm._FilterDatabase", "_xlnm.Extract", "_xlnm.Consolidate_Area", "_xlnm.Database", "_xlnm.Sheet_Title"}
|
||||
|
||||
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>`
|
||||
|
||||
const templateContentTypes = `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>`
|
||||
|
|
Binary file not shown.
254
vmlDrawing.go
254
vmlDrawing.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -20,16 +20,16 @@ type vmlDrawing struct {
|
|||
XMLNSv string `xml:"xmlns:v,attr"`
|
||||
XMLNSo string `xml:"xmlns:o,attr"`
|
||||
XMLNSx string `xml:"xmlns:x,attr"`
|
||||
XMLNSmv string `xml:"xmlns:mv,attr"`
|
||||
Shapelayout *xlsxShapelayout `xml:"o:shapelayout"`
|
||||
Shapetype *xlsxShapetype `xml:"v:shapetype"`
|
||||
XMLNSmv string `xml:"xmlns:mv,attr,omitempty"`
|
||||
ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"`
|
||||
ShapeType *xlsxShapeType `xml:"v:shapetype"`
|
||||
Shape []xlsxShape `xml:"v:shape"`
|
||||
}
|
||||
|
||||
// xlsxShapelayout directly maps the shapelayout element. This element contains
|
||||
// xlsxShapeLayout directly maps the shapelayout element. This element contains
|
||||
// child elements that store information used in the editing and layout of
|
||||
// shapes.
|
||||
type xlsxShapelayout struct {
|
||||
type xlsxShapeLayout struct {
|
||||
Ext string `xml:"v:ext,attr"`
|
||||
IDmap *xlsxIDmap `xml:"o:idmap"`
|
||||
}
|
||||
|
@ -44,33 +44,60 @@ type xlsxIDmap struct {
|
|||
type xlsxShape struct {
|
||||
XMLName xml.Name `xml:"v:shape"`
|
||||
ID string `xml:"id,attr"`
|
||||
SpID string `xml:"o:spid,attr,omitempty"`
|
||||
Type string `xml:"type,attr"`
|
||||
Style string `xml:"style,attr"`
|
||||
Fillcolor string `xml:"fillcolor,attr"`
|
||||
Insetmode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
|
||||
Strokecolor string `xml:"strokecolor,attr,omitempty"`
|
||||
Button string `xml:"o:button,attr,omitempty"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
FillColor string `xml:"fillcolor,attr,omitempty"`
|
||||
InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
|
||||
Stroked string `xml:"stroked,attr,omitempty"`
|
||||
StrokeColor string `xml:"strokecolor,attr,omitempty"`
|
||||
Val string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxShapetype directly maps the shapetype element.
|
||||
type xlsxShapetype struct {
|
||||
// xlsxShapeType directly maps the shapetype element.
|
||||
type xlsxShapeType struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Coordsize string `xml:"coordsize,attr"`
|
||||
CoordSize string `xml:"coordsize,attr"`
|
||||
Spt int `xml:"o:spt,attr"`
|
||||
PreferRelative string `xml:"o:preferrelative,attr,omitempty"`
|
||||
Path string `xml:"path,attr"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
Stroked string `xml:"stroked,attr,omitempty"`
|
||||
Stroke *xlsxStroke `xml:"v:stroke"`
|
||||
VFormulas *vFormulas `xml:"v:formulas"`
|
||||
VPath *vPath `xml:"v:path"`
|
||||
Lock *oLock `xml:"o:lock"`
|
||||
}
|
||||
|
||||
// xlsxStroke directly maps the stroke element.
|
||||
type xlsxStroke struct {
|
||||
Joinstyle string `xml:"joinstyle,attr"`
|
||||
JoinStyle string `xml:"joinstyle,attr"`
|
||||
}
|
||||
|
||||
// vPath directly maps the v:path element.
|
||||
type vPath struct {
|
||||
Gradientshapeok string `xml:"gradientshapeok,attr,omitempty"`
|
||||
Connecttype string `xml:"o:connecttype,attr"`
|
||||
ExtrusionOK string `xml:"o:extrusionok,attr,omitempty"`
|
||||
GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"`
|
||||
ConnectType string `xml:"o:connecttype,attr"`
|
||||
}
|
||||
|
||||
// oLock directly maps the o:lock element.
|
||||
type oLock struct {
|
||||
Ext string `xml:"v:ext,attr"`
|
||||
Rotation string `xml:"rotation,attr,omitempty"`
|
||||
AspectRatio string `xml:"aspectratio,attr,omitempty"`
|
||||
}
|
||||
|
||||
// vFormulas directly maps to the v:formulas element
|
||||
type vFormulas struct {
|
||||
Formulas []vFormula `xml:"v:f"`
|
||||
}
|
||||
|
||||
// vFormula directly maps to the v:f element
|
||||
type vFormula struct {
|
||||
Equation string `xml:"eqn,attr"`
|
||||
}
|
||||
|
||||
// vFill directly maps the v:fill element. This element must be defined within a
|
||||
|
@ -96,16 +123,31 @@ type vShadow struct {
|
|||
Obscured string `xml:"obscured,attr"`
|
||||
}
|
||||
|
||||
// vTextbox directly maps the v:textbox element. This element must be defined
|
||||
// vTextBox directly maps the v:textbox element. This element must be defined
|
||||
// within a Shape element.
|
||||
type vTextbox struct {
|
||||
type vTextBox struct {
|
||||
Style string `xml:"style,attr"`
|
||||
Div *xlsxDiv `xml:"div"`
|
||||
}
|
||||
|
||||
// vImageData directly maps the v:imagedata element. This element must be
|
||||
// defined within a Shape element.
|
||||
type vImageData struct {
|
||||
RelID string `xml:"o:relid,attr"`
|
||||
Title string `xml:"o:title,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxDiv directly maps the div element.
|
||||
type xlsxDiv struct {
|
||||
Style string `xml:"style,attr"`
|
||||
Font []vmlFont `xml:"font"`
|
||||
}
|
||||
|
||||
type vmlFont struct {
|
||||
Face string `xml:"face,attr,omitempty"`
|
||||
Size uint `xml:"size,attr,omitempty"`
|
||||
Color string `xml:"color,attr,omitempty"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xClientData (Attached Object Data) directly maps the x:ClientData element.
|
||||
|
@ -117,30 +159,194 @@ type xlsxDiv struct {
|
|||
// element.
|
||||
type xClientData struct {
|
||||
ObjectType string `xml:"ObjectType,attr"`
|
||||
MoveWithCells string `xml:"x:MoveWithCells,omitempty"`
|
||||
SizeWithCells string `xml:"x:SizeWithCells,omitempty"`
|
||||
MoveWithCells *string `xml:"x:MoveWithCells"`
|
||||
SizeWithCells *string `xml:"x:SizeWithCells"`
|
||||
Anchor string `xml:"x:Anchor"`
|
||||
AutoFill string `xml:"x:AutoFill"`
|
||||
Row int `xml:"x:Row"`
|
||||
Column int `xml:"x:Column"`
|
||||
Locked string `xml:"x:Locked,omitempty"`
|
||||
PrintObject string `xml:"x:PrintObject,omitempty"`
|
||||
AutoFill string `xml:"x:AutoFill,omitempty"`
|
||||
FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
|
||||
TextHAlign string `xml:"x:TextHAlign,omitempty"`
|
||||
TextVAlign string `xml:"x:TextVAlign,omitempty"`
|
||||
Row *int `xml:"x:Row"`
|
||||
Column *int `xml:"x:Column"`
|
||||
Checked int `xml:"x:Checked,omitempty"`
|
||||
FmlaLink string `xml:"x:FmlaLink,omitempty"`
|
||||
NoThreeD *string `xml:"x:NoThreeD"`
|
||||
FirstButton *string `xml:"x:FirstButton"`
|
||||
Val uint `xml:"x:Val,omitempty"`
|
||||
Min uint `xml:"x:Min,omitempty"`
|
||||
Max uint `xml:"x:Max,omitempty"`
|
||||
Inc uint `xml:"x:Inc,omitempty"`
|
||||
Page uint `xml:"x:Page,omitempty"`
|
||||
Horiz *string `xml:"x:Horiz"`
|
||||
Dx uint `xml:"x:Dx,omitempty"`
|
||||
}
|
||||
|
||||
// decodeVmlDrawing defines the structure used to parse the file
|
||||
// xl/drawings/vmlDrawing%d.vml.
|
||||
type decodeVmlDrawing struct {
|
||||
ShapeType decodeShapeType `xml:"urn:schemas-microsoft-com:vml shapetype"`
|
||||
Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"`
|
||||
}
|
||||
|
||||
// decodeShapeType defines the structure used to parse the shapetype element in
|
||||
// the file xl/drawings/vmlDrawing%d.vml.
|
||||
type decodeShapeType struct {
|
||||
ID string `xml:"id,attr"`
|
||||
CoordSize string `xml:"coordsize,attr"`
|
||||
Spt int `xml:"spt,attr"`
|
||||
PreferRelative string `xml:"preferrelative,attr,omitempty"`
|
||||
Path string `xml:"path,attr"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
Stroked string `xml:"stroked,attr,omitempty"`
|
||||
}
|
||||
|
||||
// decodeShape defines the structure used to parse the particular shape element.
|
||||
type decodeShape struct {
|
||||
ID string `xml:"id,attr"`
|
||||
SpID string `xml:"spid,attr,omitempty"`
|
||||
Type string `xml:"type,attr"`
|
||||
Style string `xml:"style,attr"`
|
||||
Button string `xml:"button,attr,omitempty"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
FillColor string `xml:"fillcolor,attr,omitempty"`
|
||||
InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
|
||||
Stroked string `xml:"stroked,attr,omitempty"`
|
||||
StrokeColor string `xml:"strokecolor,attr,omitempty"`
|
||||
Val string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// decodeShapeVal defines the structure used to parse the sub-element of the
|
||||
// shape in the file xl/drawings/vmlDrawing%d.vml.
|
||||
type decodeShapeVal struct {
|
||||
TextBox decodeVMLTextBox `xml:"textbox"`
|
||||
ClientData decodeVMLClientData `xml:"ClientData"`
|
||||
}
|
||||
|
||||
// decodeVMLFontU defines the structure used to parse the u element in the VML.
|
||||
type decodeVMLFontU struct {
|
||||
Class string `xml:"class,attr"`
|
||||
Val string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// decodeVMLFontI defines the structure used to parse the i element in the VML.
|
||||
type decodeVMLFontI struct {
|
||||
U *decodeVMLFontU `xml:"u"`
|
||||
Val string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// decodeVMLFontB defines the structure used to parse the b element in the VML.
|
||||
type decodeVMLFontB struct {
|
||||
I *decodeVMLFontI `xml:"i"`
|
||||
U *decodeVMLFontU `xml:"u"`
|
||||
Val string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// decodeVMLFont defines the structure used to parse the font element in the VML.
|
||||
type decodeVMLFont struct {
|
||||
Face string `xml:"face,attr,omitempty"`
|
||||
Size uint `xml:"size,attr,omitempty"`
|
||||
Color string `xml:"color,attr,omitempty"`
|
||||
B *decodeVMLFontB `xml:"b"`
|
||||
I *decodeVMLFontI `xml:"i"`
|
||||
U *decodeVMLFontU `xml:"u"`
|
||||
Val string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// decodeVMLDiv defines the structure used to parse the div element in the VML.
|
||||
type decodeVMLDiv struct {
|
||||
Font []decodeVMLFont `xml:"font"`
|
||||
}
|
||||
|
||||
// decodeVMLTextBox defines the structure used to parse the v:textbox element in
|
||||
// the file xl/drawings/vmlDrawing%d.vml.
|
||||
type decodeVMLTextBox struct {
|
||||
Div decodeVMLDiv `xml:"div"`
|
||||
}
|
||||
|
||||
// decodeVMLClientData defines the structure used to parse the x:ClientData
|
||||
// element in the file xl/drawings/vmlDrawing%d.vml.
|
||||
type decodeVMLClientData struct {
|
||||
ObjectType string `xml:"ObjectType,attr"`
|
||||
Anchor string
|
||||
FmlaMacro string
|
||||
Column *int
|
||||
Row *int
|
||||
Checked int
|
||||
FmlaLink string
|
||||
Val uint
|
||||
Min uint
|
||||
Max uint
|
||||
Inc uint
|
||||
Page uint
|
||||
Horiz *string
|
||||
}
|
||||
|
||||
// encodeShape defines the structure used to re-serialization shape element.
|
||||
type encodeShape struct {
|
||||
Fill *vFill `xml:"v:fill"`
|
||||
Shadow *vShadow `xml:"v:shadow"`
|
||||
Path *vPath `xml:"v:path"`
|
||||
Textbox *vTextbox `xml:"v:textbox"`
|
||||
TextBox *vTextBox `xml:"v:textbox"`
|
||||
ImageData *vImageData `xml:"v:imagedata"`
|
||||
ClientData *xClientData `xml:"x:ClientData"`
|
||||
Lock *oLock `xml:"o:lock"`
|
||||
}
|
||||
|
||||
// formCtrlPreset defines the structure used to form control presets.
|
||||
type formCtrlPreset struct {
|
||||
autoFill string
|
||||
fill *vFill
|
||||
fillColor string
|
||||
filled string
|
||||
firstButton *string
|
||||
noThreeD *string
|
||||
objectType string
|
||||
shadow *vShadow
|
||||
strokeButton string
|
||||
strokeColor string
|
||||
stroked string
|
||||
textHAlign string
|
||||
textVAlign string
|
||||
}
|
||||
|
||||
// vmlOptions defines the structure used to internal comments and form controls.
|
||||
type vmlOptions struct {
|
||||
formCtrl bool
|
||||
sheet string
|
||||
Comment
|
||||
FormControl
|
||||
}
|
||||
|
||||
// FormControl directly maps the form controls information.
|
||||
type FormControl struct {
|
||||
Cell string
|
||||
Macro string
|
||||
Width uint
|
||||
Height uint
|
||||
Checked bool
|
||||
CurrentVal uint
|
||||
MinVal uint
|
||||
MaxVal uint
|
||||
IncChange uint
|
||||
PageChange uint
|
||||
Horizontally bool
|
||||
CellLink string
|
||||
Text string
|
||||
Paragraph []RichTextRun
|
||||
Type FormControlType
|
||||
Format GraphicOptions
|
||||
}
|
||||
|
||||
// HeaderFooterImageOptions defines the settings for an image to be accessible
|
||||
// from the worksheet header and footer options.
|
||||
type HeaderFooterImageOptions struct {
|
||||
Position HeaderFooterImagePositionType
|
||||
File []byte
|
||||
IsFooter bool
|
||||
FirstPage bool
|
||||
Extension string
|
||||
Width string
|
||||
Height string
|
||||
}
|
||||
|
|
|
@ -0,0 +1,511 @@
|
|||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// 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 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
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddComment(t *testing.T) {
|
||||
f, err := prepareTestBook1()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
s := strings.Repeat("c", TotalCellChars+1)
|
||||
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Paragraph: []RichTextRun{{Text: s}, {Text: s}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
|
||||
|
||||
// Test add comment on not exists worksheet
|
||||
assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist")
|
||||
// Test add comment on with illegal cell reference
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
|
||||
comments, err := f.GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 2)
|
||||
comments, err = f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 1)
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx")))
|
||||
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><authors><author>Excelize: </author></authors><commentList><comment ref="B7" authorId="0"><text><t>Excelize: </t></text></comment></commentList></comments>`))
|
||||
comments, err = f.GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 2)
|
||||
comments, err = f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 1)
|
||||
comments, err = NewFile().GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 0)
|
||||
|
||||
// Test add comments with invalid sheet name
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}))
|
||||
|
||||
// Test add comments with unsupported charset
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetComments("Sheet2")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test add comments with unsupported charset
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test add comments with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test get comments on not exists worksheet
|
||||
comments, err = f.GetComments("SheetN")
|
||||
assert.Len(t, comments, 0)
|
||||
assert.EqualError(t, err, "sheet SheetN does not exist")
|
||||
}
|
||||
|
||||
func TestDeleteComment(t *testing.T) {
|
||||
f, err := prepareTestBook1()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}}))
|
||||
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
|
||||
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
|
||||
|
||||
comments, err := f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 5)
|
||||
|
||||
comments, err = NewFile().GetComments("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 0)
|
||||
|
||||
// Test delete comment with invalid sheet name
|
||||
assert.Equal(t, ErrSheetNameInvalid, f.DeleteComment("Sheet:1", "A1"))
|
||||
// Test delete all comments in a worksheet
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "A41"))
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "C41"))
|
||||
assert.NoError(t, f.DeleteComment("Sheet2", "C42"))
|
||||
comments, err = f.GetComments("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, len(comments))
|
||||
// Test delete comment on not exists worksheet
|
||||
assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist")
|
||||
// Test delete comment with worksheet part
|
||||
f.Pkg.Delete("xl/worksheets/sheet1.xml")
|
||||
assert.NoError(t, f.DeleteComment("Sheet1", "A22"))
|
||||
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.DeleteComment("Sheet2", "A41"), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
f = NewFile()
|
||||
// Test delete comment on a no comments worksheet
|
||||
assert.NoError(t, f.DeleteComment("Sheet1", "A1"))
|
||||
}
|
||||
|
||||
func TestDecodeVMLDrawingReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
path := "xl/drawings/vmlDrawing1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err := f.decodeVMLDrawingReader(path)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestCommentsReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test read comments with unsupported charset
|
||||
path := "xl/comments1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
_, err := f.commentsReader(path)
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestCountComments(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.Comments["xl/comments1.xml"] = nil
|
||||
assert.Equal(t, f.countComments(), 1)
|
||||
}
|
||||
|
||||
func TestAddDrawingVML(t *testing.T) {
|
||||
// Test addDrawingVML with illegal cell reference
|
||||
f := NewFile()
|
||||
assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
|
||||
|
||||
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{sheet: "Sheet1", FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestFormControl(t *testing.T) {
|
||||
f := NewFile()
|
||||
formControls := []FormControl{
|
||||
{
|
||||
Cell: "D1", Type: FormControlButton, Macro: "Button1_Click",
|
||||
},
|
||||
{
|
||||
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
|
||||
Width: 140, Height: 60, Text: "Button 1\n",
|
||||
Paragraph: []RichTextRun{
|
||||
{
|
||||
Font: &Font{
|
||||
Bold: true,
|
||||
Italic: true,
|
||||
Underline: "single",
|
||||
Family: "Times New Roman",
|
||||
Size: 14,
|
||||
Color: "777777",
|
||||
},
|
||||
Text: "C1=A1+B1",
|
||||
},
|
||||
},
|
||||
Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"},
|
||||
},
|
||||
{
|
||||
Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1",
|
||||
Checked: true, Format: GraphicOptions{
|
||||
PrintObject: boolPtr(false), Positioning: "oneCell",
|
||||
},
|
||||
},
|
||||
{
|
||||
Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2",
|
||||
Format: GraphicOptions{Positioning: "twoCell"},
|
||||
},
|
||||
{
|
||||
Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true,
|
||||
},
|
||||
{
|
||||
Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2",
|
||||
},
|
||||
{
|
||||
Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1",
|
||||
Width: 140, Height: 60,
|
||||
},
|
||||
{
|
||||
Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140,
|
||||
},
|
||||
{
|
||||
Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60,
|
||||
CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2",
|
||||
},
|
||||
{
|
||||
Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20,
|
||||
CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3",
|
||||
},
|
||||
{
|
||||
Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140,
|
||||
CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4",
|
||||
},
|
||||
}
|
||||
for _, formCtrl := range formControls {
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", formCtrl))
|
||||
}
|
||||
// Test get from controls
|
||||
result, err := f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 11)
|
||||
for i, formCtrl := range formControls {
|
||||
assert.Equal(t, formCtrl.Type, result[i].Type)
|
||||
assert.Equal(t, formCtrl.Cell, result[i].Cell)
|
||||
assert.Equal(t, formCtrl.Macro, result[i].Macro)
|
||||
assert.Equal(t, formCtrl.Checked, result[i].Checked)
|
||||
assert.Equal(t, formCtrl.CurrentVal, result[i].CurrentVal)
|
||||
assert.Equal(t, formCtrl.MinVal, result[i].MinVal)
|
||||
assert.Equal(t, formCtrl.MaxVal, result[i].MaxVal)
|
||||
assert.Equal(t, formCtrl.IncChange, result[i].IncChange)
|
||||
assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally)
|
||||
assert.Equal(t, formCtrl.CellLink, result[i].CellLink)
|
||||
assert.Equal(t, formCtrl.Text, result[i].Text)
|
||||
assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph))
|
||||
}
|
||||
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
|
||||
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddVBAProject(file))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddFormControl.xlsm")))
|
||||
assert.NoError(t, f.Close())
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
|
||||
assert.NoError(t, err)
|
||||
// Test get from controls before add form controls
|
||||
result, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 11)
|
||||
// Test add from control to a worksheet which already contains form controls
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "D4", Type: FormControlButton, Macro: "Button1_Click",
|
||||
Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}},
|
||||
}))
|
||||
// Test get from controls after add form controls
|
||||
result, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 12)
|
||||
// Test add unsupported form control
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A1", Type: 0x37, Macro: "Button1_Click",
|
||||
}), ErrParameterInvalid)
|
||||
// Test add form control on not exists worksheet
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddFormControl("SheetN", FormControl{
|
||||
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
|
||||
}))
|
||||
// Test add form control with invalid positioning types
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A1", Type: FormControlButton,
|
||||
Format: GraphicOptions{Positioning: "x"},
|
||||
}), ErrParameterInvalid)
|
||||
// Test add spin form control with illegal cell link reference
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "C5", Type: FormControlSpinButton, CellLink: "*",
|
||||
}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
|
||||
// Test add spin form control with invalid scroll value
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1,
|
||||
}), ErrFormControlValue)
|
||||
assert.NoError(t, f.Close())
|
||||
// Test delete form control
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.DeleteFormControl("Sheet1", "D1"))
|
||||
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
|
||||
// Test get from controls after delete form controls
|
||||
result, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 9)
|
||||
// Test delete form control on not exists worksheet
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.DeleteFormControl("SheetN", "A1"))
|
||||
// Test delete form control with illegal cell link reference
|
||||
assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm")))
|
||||
assert.NoError(t, f.Close())
|
||||
// Test delete form control with expected element
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
||||
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test delete form controls with invalid shape anchor
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>0</x:Anchor></x:ClientData>"}},
|
||||
}
|
||||
assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1"))
|
||||
assert.NoError(t, f.Close())
|
||||
// Test delete form control on a worksheet without form control
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
|
||||
// Test get form controls on a worksheet without form control
|
||||
_, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
// Test get form controls on not exists worksheet
|
||||
_, err = f.GetFormControls("SheetN")
|
||||
assert.Equal(t, ErrSheetNotExist{"SheetN"}, err)
|
||||
// Test get form controls with unsupported charset VML drawing
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetFormControls("Sheet1")
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test get form controls with unsupported shape type
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "_x0000_t202"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, formControls, 0)
|
||||
// Test get form controls with bold font format
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><b>Text</b></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, formControls[0].Paragraph[0].Font.Bold)
|
||||
// Test get form controls with italic font format
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
|
||||
// Test get form controls with font format
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font face=\"Calibri\" size=\"280\" color=\"#777777\">Text</font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family)
|
||||
assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size)
|
||||
assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color)
|
||||
// Test get form controls with italic font format
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
|
||||
// Test get form controls with invalid column number
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.Equal(t, err, ErrColumnNumber)
|
||||
assert.Len(t, formControls, 0)
|
||||
// Test get form controls with comment (Note) shape type
|
||||
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
|
||||
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, formControls, 0)
|
||||
// Test get form controls with unsupported shape type
|
||||
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||
Shape: []xlsxShape{{Type: "_x0000_t202"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, formControls, 0)
|
||||
// Test get form controls with invalid column number
|
||||
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.Equal(t, err, ErrColumnNumber)
|
||||
assert.Len(t, formControls, 0)
|
||||
// Test get form controls with invalid shape anchor
|
||||
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>x,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.Equal(t, ErrColumnNumber, err)
|
||||
assert.Len(t, formControls, 0)
|
||||
// Test get form controls with comment (Note) shape type
|
||||
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
|
||||
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},
|
||||
}
|
||||
formControls, err = f.GetFormControls("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, formControls, 0)
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestExtractFormControl(t *testing.T) {
|
||||
// Test extract form control with unsupported charset
|
||||
_, err := extractFormControl(string(MacintoshCyrillicCharset))
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestAddHeaderFooterImage(t *testing.T) {
|
||||
f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx")
|
||||
headerFooterOptions := HeaderFooterOptions{
|
||||
DifferentFirst: true,
|
||||
OddHeader: "&L&GExcelize&C&G&R&G",
|
||||
OddFooter: "&L&GExcelize&C&G&R&G",
|
||||
FirstHeader: "&L&GExcelize&C&G&R&G",
|
||||
FirstFooter: "&L&GExcelize&C&G&R&G",
|
||||
}
|
||||
assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions))
|
||||
assert.NoError(t, f.SetSheetView(sheet, -1, &ViewOptions{View: stringPtr("pageLayout")}))
|
||||
images := map[string][]byte{
|
||||
".wmf": nil, ".tif": nil, ".png": nil,
|
||||
".jpg": nil, ".gif": nil, ".emz": nil, ".emf": nil,
|
||||
}
|
||||
for ext := range images {
|
||||
img, err := os.ReadFile(filepath.Join("test", "images", "excel"+ext))
|
||||
assert.NoError(t, err)
|
||||
images[ext] = img
|
||||
}
|
||||
for _, opt := range []struct {
|
||||
position HeaderFooterImagePositionType
|
||||
file []byte
|
||||
isFooter bool
|
||||
firstPage bool
|
||||
ext string
|
||||
}{
|
||||
{position: HeaderFooterImagePositionLeft, file: images[".tif"], firstPage: true, ext: ".tif"},
|
||||
{position: HeaderFooterImagePositionCenter, file: images[".gif"], firstPage: true, ext: ".gif"},
|
||||
{position: HeaderFooterImagePositionRight, file: images[".png"], firstPage: true, ext: ".png"},
|
||||
{position: HeaderFooterImagePositionLeft, file: images[".emf"], isFooter: true, firstPage: true, ext: ".emf"},
|
||||
{position: HeaderFooterImagePositionCenter, file: images[".wmf"], isFooter: true, firstPage: true, ext: ".wmf"},
|
||||
{position: HeaderFooterImagePositionRight, file: images[".emz"], isFooter: true, firstPage: true, ext: ".emz"},
|
||||
{position: HeaderFooterImagePositionLeft, file: images[".png"], ext: ".png"},
|
||||
{position: HeaderFooterImagePositionCenter, file: images[".png"], ext: ".png"},
|
||||
{position: HeaderFooterImagePositionRight, file: images[".png"], ext: ".png"},
|
||||
{position: HeaderFooterImagePositionLeft, file: images[".tif"], isFooter: true, ext: ".tif"},
|
||||
{position: HeaderFooterImagePositionCenter, file: images[".tif"], isFooter: true, ext: ".tif"},
|
||||
{position: HeaderFooterImagePositionRight, file: images[".tif"], isFooter: true, ext: ".tif"},
|
||||
} {
|
||||
assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
|
||||
Position: opt.position,
|
||||
File: opt.file,
|
||||
IsFooter: opt.isFooter,
|
||||
FirstPage: opt.firstPage,
|
||||
Extension: opt.ext,
|
||||
Width: "50pt",
|
||||
Height: "32pt",
|
||||
}))
|
||||
}
|
||||
assert.NoError(t, f.SetCellValue(sheet, "A1", "Example"))
|
||||
|
||||
// Test add header footer image with not exist sheet
|
||||
assert.EqualError(t, f.AddHeaderFooterImage("SheetN", nil), "sheet SheetN does not exist")
|
||||
// Test add header footer image with unsupported file type
|
||||
assert.Equal(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
|
||||
Extension: "jpg",
|
||||
}), ErrImgExt)
|
||||
assert.NoError(t, f.SaveAs(wb))
|
||||
assert.NoError(t, f.Close())
|
||||
// Test change already exist header image with the different image
|
||||
f, err := OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
|
||||
File: images[".jpg"],
|
||||
FirstPage: true,
|
||||
Extension: ".jpg",
|
||||
Width: "50pt",
|
||||
Height: "32pt",
|
||||
}))
|
||||
assert.NoError(t, f.Save())
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
// Test add header image with unsupported charset VML drawing
|
||||
f, err = OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
|
||||
File: images[".jpg"],
|
||||
Extension: ".jpg",
|
||||
Width: "50pt",
|
||||
Height: "32pt",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
// Test set legacy drawing header/footer with unsupported charset content types
|
||||
f = NewFile()
|
||||
f.ContentTypes = nil
|
||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
|
||||
Extension: ".png",
|
||||
File: images[".png"],
|
||||
Width: "50pt",
|
||||
Height: "32pt",
|
||||
}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
183
workbook.go
183
workbook.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -145,8 +145,8 @@ func (f *File) setWorkbook(name string, sheetID, rid int) {
|
|||
// the spreadsheet.
|
||||
func (f *File) getWorkbookPath() (path string) {
|
||||
if rels, _ := f.relsReader("_rels/.rels"); rels != nil {
|
||||
rels.Lock()
|
||||
defer rels.Unlock()
|
||||
rels.mu.Lock()
|
||||
defer rels.mu.Unlock()
|
||||
for _, rel := range rels.Relationships {
|
||||
if rel.Type == SourceRelationshipOfficeDocument {
|
||||
path = strings.TrimPrefix(rel.Target, "/")
|
||||
|
@ -170,6 +170,26 @@ func (f *File) getWorkbookRelsPath() (path string) {
|
|||
return
|
||||
}
|
||||
|
||||
// deleteWorkbookRels provides a function to delete relationships in
|
||||
// xl/_rels/workbook.xml.rels by given type and target.
|
||||
func (f *File) deleteWorkbookRels(relType, relTarget string) (string, error) {
|
||||
var rID string
|
||||
rels, err := f.relsReader(f.getWorkbookRelsPath())
|
||||
if err != nil {
|
||||
return rID, err
|
||||
}
|
||||
if rels == nil {
|
||||
rels = &xlsxRelationships{}
|
||||
}
|
||||
for k, v := range rels.Relationships {
|
||||
if v.Type == relType && v.Target == relTarget {
|
||||
rID = v.ID
|
||||
rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
|
||||
}
|
||||
}
|
||||
return rID, err
|
||||
}
|
||||
|
||||
// workbookReader provides a function to get the pointer to the workbook.xml
|
||||
// structure after deserialization.
|
||||
func (f *File) workbookReader() (*xlsxWorkbook, error) {
|
||||
|
@ -177,9 +197,13 @@ func (f *File) workbookReader() (*xlsxWorkbook, error) {
|
|||
if f.WorkBook == nil {
|
||||
wbPath := f.getWorkbookPath()
|
||||
f.WorkBook = new(xlsxWorkbook)
|
||||
if _, ok := f.xmlAttr[wbPath]; !ok {
|
||||
if attrs, ok := f.xmlAttr.Load(wbPath); !ok {
|
||||
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath))))
|
||||
f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...)
|
||||
if attrs == nil {
|
||||
attrs = []xml.Attr{}
|
||||
}
|
||||
attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
|
||||
f.xmlAttr.Store(wbPath, attrs)
|
||||
f.addNameSpaces(wbPath, SourceRelationship)
|
||||
}
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))).
|
||||
|
@ -205,3 +229,150 @@ func (f *File) workBookWriter() {
|
|||
f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output)))
|
||||
}
|
||||
}
|
||||
|
||||
// setContentTypePartRelsExtensions provides a function to set the content type
|
||||
// for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartRelsExtensions() error {
|
||||
var rels bool
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range content.Defaults {
|
||||
if v.Extension == "rels" {
|
||||
rels = true
|
||||
}
|
||||
}
|
||||
if !rels {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: "rels",
|
||||
ContentType: ContentTypeRelationships,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// setContentTypePartImageExtensions provides a function to set the content type
|
||||
// for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartImageExtensions() error {
|
||||
imageTypes := map[string]string{
|
||||
"bmp": "image/", "jpeg": "image/", "png": "image/", "gif": "image/",
|
||||
"svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-",
|
||||
"emz": "image/x-", "wmz": "image/x-",
|
||||
}
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
for _, file := range content.Defaults {
|
||||
delete(imageTypes, file.Extension)
|
||||
}
|
||||
for extension, prefix := range imageTypes {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: extension,
|
||||
ContentType: prefix + extension,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// setContentTypePartVMLExtensions provides a function to set the content type
|
||||
// for relationship parts and the Main Document part.
|
||||
func (f *File) setContentTypePartVMLExtensions() error {
|
||||
var vml bool
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
for _, v := range content.Defaults {
|
||||
if v.Extension == "vml" {
|
||||
vml = true
|
||||
}
|
||||
}
|
||||
if !vml {
|
||||
content.Defaults = append(content.Defaults, xlsxDefault{
|
||||
Extension: "vml",
|
||||
ContentType: ContentTypeVML,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// addContentTypePart provides a function to add content type part relationships
|
||||
// in the file [Content_Types].xml by given index and content type.
|
||||
func (f *File) addContentTypePart(index int, contentType string) error {
|
||||
setContentType := map[string]func() error{
|
||||
"comments": f.setContentTypePartVMLExtensions,
|
||||
"drawings": f.setContentTypePartImageExtensions,
|
||||
}
|
||||
partNames := map[string]string{
|
||||
"chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
|
||||
"chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
|
||||
"comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
|
||||
"drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
|
||||
"table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
|
||||
"pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
|
||||
"pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
|
||||
"sharedStrings": "/xl/sharedStrings.xml",
|
||||
"slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
|
||||
"slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
|
||||
}
|
||||
contentTypes := map[string]string{
|
||||
"chart": ContentTypeDrawingML,
|
||||
"chartsheet": ContentTypeSpreadSheetMLChartsheet,
|
||||
"comments": ContentTypeSpreadSheetMLComments,
|
||||
"drawings": ContentTypeDrawing,
|
||||
"table": ContentTypeSpreadSheetMLTable,
|
||||
"pivotTable": ContentTypeSpreadSheetMLPivotTable,
|
||||
"pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
|
||||
"sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
|
||||
"slicer": ContentTypeSlicer,
|
||||
"slicerCache": ContentTypeSlicerCache,
|
||||
}
|
||||
s, ok := setContentType[contentType]
|
||||
if ok {
|
||||
if err := s(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
for _, v := range content.Overrides {
|
||||
if v.PartName == partNames[contentType] {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content.Overrides = append(content.Overrides, xlsxOverride{
|
||||
PartName: partNames[contentType],
|
||||
ContentType: contentTypes[contentType],
|
||||
})
|
||||
return f.setContentTypePartRelsExtensions()
|
||||
}
|
||||
|
||||
// removeContentTypesPart provides a function to remove relationships by given
|
||||
// content type and part name in the file [Content_Types].xml.
|
||||
func (f *File) removeContentTypesPart(contentType, partName string) error {
|
||||
if !strings.HasPrefix(partName, "/") {
|
||||
partName = "/xl/" + partName
|
||||
}
|
||||
content, err := f.contentTypesReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.mu.Lock()
|
||||
defer content.mu.Unlock()
|
||||
for k, v := range content.Overrides {
|
||||
if v.PartName == partName && v.ContentType == contentType {
|
||||
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -31,3 +31,13 @@ func TestWorkbookProps(t *testing.T) {
|
|||
_, err = f.GetWorkbookProps()
|
||||
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestDeleteWorkbookRels(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test delete pivot table without worksheet relationships
|
||||
f.Relationships.Delete("xl/_rels/workbook.xml.rels")
|
||||
f.Pkg.Delete("xl/_rels/workbook.xml.rels")
|
||||
rID, err := f.deleteWorkbookRels("", "")
|
||||
assert.Empty(t, rID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,13 +7,14 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// xlsxCalcChain directly maps the calcChain element. This element represents the root of the calculation chain.
|
||||
// xlsxCalcChain directly maps the calcChain element. This element represents
|
||||
// the root of the calculation chain.
|
||||
type xlsxCalcChain struct {
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"`
|
||||
C []xlsxCalcChainC `xml:"c"`
|
||||
|
@ -76,9 +77,48 @@ type xlsxCalcChain struct {
|
|||
// | boolean datatype.
|
||||
type xlsxCalcChainC struct {
|
||||
R string `xml:"r,attr"`
|
||||
I int `xml:"i,attr"`
|
||||
I int `xml:"i,attr,omitempty"`
|
||||
L bool `xml:"l,attr,omitempty"`
|
||||
S bool `xml:"s,attr,omitempty"`
|
||||
T bool `xml:"t,attr,omitempty"`
|
||||
A bool `xml:"a,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxVolTypes maps the volatileDependencies part provides a cache of data that
|
||||
// supports Real Time Data (RTD) and CUBE functions in the workbook.
|
||||
type xlsxVolTypes struct {
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main volTypes"`
|
||||
VolType []xlsxVolType `xml:"volType"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxVolType represents dependency information for a specific type of external
|
||||
// data server.
|
||||
type xlsxVolType struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Main []xlsxVolMain `xml:"main"`
|
||||
}
|
||||
|
||||
// xlsxVolMain represents dependency information for all topics within a
|
||||
// volatile dependency type that share the same first string or function
|
||||
// argument.
|
||||
type xlsxVolMain struct {
|
||||
First string `xml:"first,attr"`
|
||||
Tp []xlsxVolTopic `xml:"tp"`
|
||||
}
|
||||
|
||||
// xlsxVolTopic represents dependency information for all topics within a
|
||||
// volatile dependency type that share the same first string or argument.
|
||||
type xlsxVolTopic struct {
|
||||
T string `xml:"t,attr,omitempty"`
|
||||
V string `xml:"v"`
|
||||
Stp []string `xml:"stp"`
|
||||
Tr []xlsxVolTopicRef `xml:"tr"`
|
||||
}
|
||||
|
||||
// xlsxVolTopicRef represents the reference to a cell that depends on this
|
||||
// topic. Each topic can have one or more cells dependencies.
|
||||
type xlsxVolTopicRef struct {
|
||||
R string `xml:"r,attr"`
|
||||
S int `xml:"s,attr"`
|
||||
}
|
||||
|
|
61
xmlChart.go
61
xmlChart.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -74,7 +74,7 @@ type cTx struct {
|
|||
type cRich struct {
|
||||
BodyPr aBodyPr `xml:"a:bodyPr,omitempty"`
|
||||
LstStyle string `xml:"a:lstStyle,omitempty"`
|
||||
P aP `xml:"a:p"`
|
||||
P []aP `xml:"a:p"`
|
||||
}
|
||||
|
||||
// aBodyPr (Body Properties) directly maps the a:bodyPr element. This element
|
||||
|
@ -252,7 +252,7 @@ type aLn struct {
|
|||
Cap string `xml:"cap,attr,omitempty"`
|
||||
Cmpd string `xml:"cmpd,attr,omitempty"`
|
||||
W int `xml:"w,attr,omitempty"`
|
||||
NoFill string `xml:"a:noFill,omitempty"`
|
||||
NoFill *attrValString `xml:"a:noFill"`
|
||||
Round string `xml:"a:round,omitempty"`
|
||||
SolidFill *aSolidFill `xml:"a:solidFill"`
|
||||
}
|
||||
|
@ -301,21 +301,21 @@ type cView3D struct {
|
|||
// plot area of the chart.
|
||||
type cPlotArea struct {
|
||||
Layout *string `xml:"layout"`
|
||||
AreaChart *cCharts `xml:"areaChart"`
|
||||
Area3DChart *cCharts `xml:"area3DChart"`
|
||||
BarChart *cCharts `xml:"barChart"`
|
||||
Bar3DChart *cCharts `xml:"bar3DChart"`
|
||||
BubbleChart *cCharts `xml:"bubbleChart"`
|
||||
DoughnutChart *cCharts `xml:"doughnutChart"`
|
||||
LineChart *cCharts `xml:"lineChart"`
|
||||
Line3DChart *cCharts `xml:"line3DChart"`
|
||||
PieChart *cCharts `xml:"pieChart"`
|
||||
Pie3DChart *cCharts `xml:"pie3DChart"`
|
||||
OfPieChart *cCharts `xml:"ofPieChart"`
|
||||
RadarChart *cCharts `xml:"radarChart"`
|
||||
ScatterChart *cCharts `xml:"scatterChart"`
|
||||
Surface3DChart *cCharts `xml:"surface3DChart"`
|
||||
SurfaceChart *cCharts `xml:"surfaceChart"`
|
||||
AreaChart []*cCharts `xml:"areaChart"`
|
||||
Area3DChart []*cCharts `xml:"area3DChart"`
|
||||
BarChart []*cCharts `xml:"barChart"`
|
||||
Bar3DChart []*cCharts `xml:"bar3DChart"`
|
||||
BubbleChart []*cCharts `xml:"bubbleChart"`
|
||||
DoughnutChart []*cCharts `xml:"doughnutChart"`
|
||||
LineChart []*cCharts `xml:"lineChart"`
|
||||
Line3DChart []*cCharts `xml:"line3DChart"`
|
||||
PieChart []*cCharts `xml:"pieChart"`
|
||||
Pie3DChart []*cCharts `xml:"pie3DChart"`
|
||||
OfPieChart []*cCharts `xml:"ofPieChart"`
|
||||
RadarChart []*cCharts `xml:"radarChart"`
|
||||
ScatterChart []*cCharts `xml:"scatterChart"`
|
||||
Surface3DChart []*cCharts `xml:"surface3DChart"`
|
||||
SurfaceChart []*cCharts `xml:"surfaceChart"`
|
||||
CatAx []*cAxs `xml:"catAx"`
|
||||
ValAx []*cAxs `xml:"valAx"`
|
||||
SerAx []*cAxs `xml:"serAx"`
|
||||
|
@ -351,6 +351,7 @@ type cAxs struct {
|
|||
AxPos *attrValString `xml:"axPos"`
|
||||
MajorGridlines *cChartLines `xml:"majorGridlines"`
|
||||
MinorGridlines *cChartLines `xml:"minorGridlines"`
|
||||
Title *cTitle `xml:"title"`
|
||||
NumFmt *cNumFmt `xml:"numFmt"`
|
||||
MajorTickMark *attrValString `xml:"majorTickMark"`
|
||||
MinorTickMark *attrValString `xml:"minorTickMark"`
|
||||
|
@ -482,6 +483,7 @@ type cNumCache struct {
|
|||
// the specific formatting and positioning settings.
|
||||
type cDLbls struct {
|
||||
NumFmt *cNumFmt `xml:"numFmt"`
|
||||
DLblPos *attrValString `xml:"dLblPos"`
|
||||
ShowLegendKey *attrValBool `xml:"showLegendKey"`
|
||||
ShowVal *attrValBool `xml:"showVal"`
|
||||
ShowCatName *attrValBool `xml:"showCatName"`
|
||||
|
@ -532,13 +534,18 @@ type ChartAxis struct {
|
|||
MajorGridLines bool
|
||||
MinorGridLines bool
|
||||
MajorUnit float64
|
||||
TickLabelPosition ChartTickLabelPositionType
|
||||
TickLabelSkip int
|
||||
ReverseOrder bool
|
||||
Secondary bool
|
||||
Maximum *float64
|
||||
Minimum *float64
|
||||
Alignment Alignment
|
||||
Font Font
|
||||
LogBase float64
|
||||
NumFmt ChartNumFmt
|
||||
Title []RichTextRun
|
||||
axID int
|
||||
}
|
||||
|
||||
// ChartDimension directly maps the dimension of the chart.
|
||||
|
@ -556,6 +563,7 @@ type ChartPlotArea struct {
|
|||
ShowPercent bool
|
||||
ShowSerName bool
|
||||
ShowVal bool
|
||||
Fill Fill
|
||||
NumFmt ChartNumFmt
|
||||
}
|
||||
|
||||
|
@ -566,12 +574,15 @@ type Chart struct {
|
|||
Format GraphicOptions
|
||||
Dimension ChartDimension
|
||||
Legend ChartLegend
|
||||
Title ChartTitle
|
||||
Title []RichTextRun
|
||||
VaryColors *bool
|
||||
XAxis ChartAxis
|
||||
YAxis ChartAxis
|
||||
PlotArea ChartPlotArea
|
||||
Fill Fill
|
||||
Border ChartLine
|
||||
ShowBlanksAs string
|
||||
BubbleSize int
|
||||
HoleSize int
|
||||
order int
|
||||
}
|
||||
|
@ -584,12 +595,14 @@ type ChartLegend struct {
|
|||
|
||||
// ChartMarker directly maps the format settings of the chart marker.
|
||||
type ChartMarker struct {
|
||||
Fill Fill
|
||||
Symbol string
|
||||
Size int
|
||||
}
|
||||
|
||||
// ChartLine directly maps the format settings of the chart line.
|
||||
type ChartLine struct {
|
||||
Type ChartLineType
|
||||
Smooth bool
|
||||
Width float64
|
||||
}
|
||||
|
@ -598,14 +611,10 @@ type ChartLine struct {
|
|||
type ChartSeries struct {
|
||||
Name string
|
||||
Categories string
|
||||
Sizes string
|
||||
Values string
|
||||
Sizes string
|
||||
Fill Fill
|
||||
Line ChartLine
|
||||
Marker ChartMarker
|
||||
}
|
||||
|
||||
// ChartTitle directly maps the format settings of the chart title.
|
||||
type ChartTitle struct {
|
||||
Name string
|
||||
DataLabelPosition ChartDataLabelPositionType
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -9,7 +9,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -38,7 +38,7 @@ type xlsxChartsheetPr struct {
|
|||
XMLName xml.Name `xml:"sheetPr"`
|
||||
PublishedAttr bool `xml:"published,attr,omitempty"`
|
||||
CodeNameAttr string `xml:"codeName,attr,omitempty"`
|
||||
TabColor *xlsxTabColor `xml:"tabColor"`
|
||||
TabColor *xlsxColor `xml:"tabColor"`
|
||||
}
|
||||
|
||||
// xlsxChartsheetViews specifies chart sheet views.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -78,5 +78,7 @@ type Comment struct {
|
|||
AuthorID int
|
||||
Cell string
|
||||
Text string
|
||||
Runs []RichTextRun
|
||||
Width uint
|
||||
Height uint
|
||||
Paragraph []RichTextRun
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -20,7 +20,7 @@ import (
|
|||
// parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a
|
||||
// value.
|
||||
type xlsxTypes struct {
|
||||
sync.Mutex
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"`
|
||||
Defaults []xlsxDefault `xml:"Default"`
|
||||
Overrides []xlsxOverride `xml:"Override"`
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -22,23 +22,63 @@ type decodeCellAnchor struct {
|
|||
From *decodeFrom `xml:"from"`
|
||||
To *decodeTo `xml:"to"`
|
||||
Sp *decodeSp `xml:"sp"`
|
||||
Pic *decodePic `xml:"pic"`
|
||||
ClientData *decodeClientData `xml:"clientData"`
|
||||
AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xdrSp (Shape) directly maps the sp element. This element specifies the
|
||||
// existence of a single shape. A shape can either be a preset or a custom
|
||||
// geometry, defined using the SpreadsheetDrawingML framework. In addition to
|
||||
// a geometry each shape can have both visual and non-visual properties
|
||||
// attached. Text and corresponding styling information can also be attached
|
||||
// to a shape. This shape is specified along with all other shapes within
|
||||
// either the shape tree or group shape elements.
|
||||
// decodeCellAnchorPos defines the structure used to deserialize the cell anchor
|
||||
// for adjust drawing object on inserting/deleting column/rows.
|
||||
type decodeCellAnchorPos struct {
|
||||
EditAs string `xml:"editAs,attr,omitempty"`
|
||||
From *xlsxFrom `xml:"from"`
|
||||
To *xlsxTo `xml:"to"`
|
||||
Pos *xlsxInnerXML `xml:"pos"`
|
||||
Ext *xlsxInnerXML `xml:"ext"`
|
||||
Sp *xlsxSp `xml:"sp"`
|
||||
GrpSp *xlsxInnerXML `xml:"grpSp"`
|
||||
GraphicFrame *xlsxInnerXML `xml:"graphicFrame"`
|
||||
CxnSp *xlsxInnerXML `xml:"cxnSp"`
|
||||
Pic *xlsxInnerXML `xml:"pic"`
|
||||
ContentPart *xlsxInnerXML `xml:"contentPart"`
|
||||
AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
|
||||
ClientData *xlsxInnerXML `xml:"clientData"`
|
||||
}
|
||||
|
||||
// decodeChoice defines the structure used to deserialize the mc:Choice element.
|
||||
type decodeChoice struct {
|
||||
XMLName xml.Name `xml:"Choice"`
|
||||
XMLNSA14 string `xml:"a14,attr"`
|
||||
XMLNSSle15 string `xml:"sle15,attr"`
|
||||
Requires string `xml:"Requires,attr"`
|
||||
GraphicFrame decodeGraphicFrame `xml:"graphicFrame"`
|
||||
}
|
||||
|
||||
// decodeGraphicFrame defines the structure used to deserialize the
|
||||
// xdr:graphicFrame element.
|
||||
type decodeGraphicFrame struct {
|
||||
Macro string `xml:"macro,attr"`
|
||||
NvGraphicFramePr decodeNvGraphicFramePr `xml:"nvGraphicFramePr"`
|
||||
}
|
||||
|
||||
// decodeNvGraphicFramePr defines the structure used to deserialize the
|
||||
// xdr:nvGraphicFramePr element.
|
||||
type decodeNvGraphicFramePr struct {
|
||||
CNvPr decodeCNvPr `xml:"cNvPr"`
|
||||
}
|
||||
|
||||
// decodeSp defines the structure used to deserialize the sp element.
|
||||
type decodeSp struct {
|
||||
Macro string `xml:"macro,attr,omitempty"`
|
||||
TextLink string `xml:"textlink,attr,omitempty"`
|
||||
FLocksText bool `xml:"fLocksText,attr,omitempty"`
|
||||
FPublished *bool `xml:"fPublished,attr"`
|
||||
NvSpPr *decodeNvSpPr `xml:"nvSpPr"`
|
||||
SpPr *decodeSpPr `xml:"spPr"`
|
||||
}
|
||||
|
||||
// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr
|
||||
// decodeNvSpPr (Non-Visual Properties for a Shape) directly maps the nvSpPr
|
||||
// element. This element specifies all non-visual properties for a shape. This
|
||||
// element is a container for the non-visual identification properties, shape
|
||||
// properties and application properties that are to be associated with a
|
||||
|
@ -46,7 +86,7 @@ type decodeSp struct {
|
|||
// appearance of the shape to be stored.
|
||||
type decodeNvSpPr struct {
|
||||
CNvPr *decodeCNvPr `xml:"cNvPr"`
|
||||
ExtLst *decodeExt `xml:"extLst"`
|
||||
ExtLst *decodeAExt `xml:"extLst"`
|
||||
CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"`
|
||||
}
|
||||
|
||||
|
@ -63,7 +103,7 @@ type decodeCNvSpPr struct {
|
|||
// changed after serialization and deserialization, two different structures
|
||||
// are defined. decodeWsDr just for deserialization.
|
||||
type decodeWsDr struct {
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"`
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr"`
|
||||
A string `xml:"xmlns a,attr"`
|
||||
Xdr string `xml:"xmlns xdr,attr"`
|
||||
R string `xml:"xmlns r,attr"`
|
||||
|
@ -72,22 +112,12 @@ type decodeWsDr struct {
|
|||
TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"`
|
||||
}
|
||||
|
||||
// decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape
|
||||
// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element
|
||||
// specifies a two cell anchor placeholder for a group, a shape, or a drawing
|
||||
// element. It moves with cells and its extents are in EMU units.
|
||||
type decodeTwoCellAnchor struct {
|
||||
From *decodeFrom `xml:"from"`
|
||||
To *decodeTo `xml:"to"`
|
||||
Pic *decodePic `xml:"pic"`
|
||||
ClientData *decodeClientData `xml:"clientData"`
|
||||
}
|
||||
|
||||
// decodeCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
|
||||
// element specifies non-visual canvas properties. This allows for additional
|
||||
// information that does not affect the appearance of the picture to be
|
||||
// stored.
|
||||
type decodeCNvPr struct {
|
||||
XMLName xml.Name `xml:"cNvPr"`
|
||||
ID int `xml:"id,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Descr string `xml:"descr,attr"`
|
||||
|
@ -134,8 +164,8 @@ type decodeOff struct {
|
|||
Y int `xml:"y,attr"`
|
||||
}
|
||||
|
||||
// decodeExt directly maps the ext element.
|
||||
type decodeExt struct {
|
||||
// decodeAExt directly maps the a:ext element.
|
||||
type decodeAExt struct {
|
||||
Cx int `xml:"cx,attr"`
|
||||
Cy int `xml:"cy,attr"`
|
||||
}
|
||||
|
@ -154,7 +184,7 @@ type decodePrstGeom struct {
|
|||
// be for a shape or group shape.
|
||||
type decodeXfrm struct {
|
||||
Off decodeOff `xml:"off"`
|
||||
Ext decodeExt `xml:"ext"`
|
||||
Ext decodeAExt `xml:"ext"`
|
||||
}
|
||||
|
||||
// decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
|
||||
|
@ -232,3 +262,15 @@ type decodeClientData struct {
|
|||
FLocksWithSheet bool `xml:"fLocksWithSheet,attr"`
|
||||
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
|
||||
}
|
||||
|
||||
// decodeCellImages directly maps the Kingsoft WPS Office embedded cell images.
|
||||
type decodeCellImages struct {
|
||||
XMLName xml.Name `xml:"http://www.wps.cn/officeDocument/2017/etCustomData cellImages"`
|
||||
CellImage []decodeCellImage `xml:"cellImage"`
|
||||
}
|
||||
|
||||
// decodeCellImage defines the structure used to deserialize the Kingsoft WPS
|
||||
// Office embedded cell images.
|
||||
type decodeCellImage struct {
|
||||
Pic decodePic `xml:"pic"`
|
||||
}
|
||||
|
|
277
xmlDrawing.go
277
xmlDrawing.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -16,208 +16,6 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Source relationship and namespace list, associated prefixes and schema in which it was
|
||||
// introduced.
|
||||
var (
|
||||
NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"}
|
||||
NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"}
|
||||
NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"}
|
||||
NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
|
||||
NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}
|
||||
NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"}
|
||||
NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
||||
NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
|
||||
NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
|
||||
NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
|
||||
SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
|
||||
SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"}
|
||||
SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"}
|
||||
SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"}
|
||||
SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"}
|
||||
)
|
||||
|
||||
// Source relationship and namespace.
|
||||
const (
|
||||
ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
|
||||
ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
|
||||
ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
|
||||
ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
|
||||
ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
||||
ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
|
||||
ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
|
||||
ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
|
||||
ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
||||
ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
|
||||
ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
|
||||
ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
||||
ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
|
||||
ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
|
||||
ContentTypeVBA = "application/vnd.ms-office.vbaProject"
|
||||
ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
|
||||
NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
|
||||
NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/"
|
||||
NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/"
|
||||
NameSpaceExtendedProperties = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||
NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
|
||||
NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
|
||||
SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
|
||||
SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
||||
SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
|
||||
SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
|
||||
SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
|
||||
SourceRelationshipExtendProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
|
||||
SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||
SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||||
SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
|
||||
SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
|
||||
SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
|
||||
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
|
||||
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
|
||||
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
|
||||
StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
|
||||
StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
|
||||
StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main"
|
||||
StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships"
|
||||
StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart"
|
||||
StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments"
|
||||
StrictSourceRelationshipExtendProperties = "http://purl.oclc.org/ooxml/officeDocument/relationships/extendedProperties"
|
||||
StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image"
|
||||
StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
|
||||
// ExtURIConditionalFormattings is the extLst child element
|
||||
// ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element
|
||||
// ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of
|
||||
// new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7)
|
||||
ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
|
||||
ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
|
||||
ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}"
|
||||
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
|
||||
ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
|
||||
ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
|
||||
ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
|
||||
ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
|
||||
ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}"
|
||||
ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}"
|
||||
ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
|
||||
ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
|
||||
ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
|
||||
ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
|
||||
)
|
||||
|
||||
// extensionURIPriority is the priority of URI in the extension lists.
|
||||
var extensionURIPriority = []string{
|
||||
ExtURIConditionalFormattings,
|
||||
ExtURIDataValidations,
|
||||
ExtURISparklineGroups,
|
||||
ExtURISlicerListX14,
|
||||
ExtURIProtectedRanges,
|
||||
ExtURIIgnoredErrors,
|
||||
ExtURIWebExtensions,
|
||||
ExtURITimelineRefs,
|
||||
}
|
||||
|
||||
// Excel specifications and limits
|
||||
const (
|
||||
MaxCellStyles = 65430
|
||||
MaxColumns = 16384
|
||||
MaxColumnWidth = 255
|
||||
MaxFieldLength = 255
|
||||
MaxFilePathLength = 207
|
||||
MaxFontFamilyLength = 31
|
||||
MaxFontSize = 409
|
||||
MaxRowHeight = 409
|
||||
MaxSheetNameLength = 31
|
||||
MinColumns = 1
|
||||
MinFontSize = 1
|
||||
StreamChunkSize = 1 << 24
|
||||
TotalCellChars = 32767
|
||||
TotalRows = 1048576
|
||||
TotalSheetHyperlinks = 65529
|
||||
UnzipSizeLimit = 1000 << 24
|
||||
// pivotTableVersion should be greater than 3. One or more of the
|
||||
// PivotTables chosen are created in a version of Excel earlier than
|
||||
// Excel 2007 or in compatibility mode. Slicer can only be used with
|
||||
// PivotTables created in Excel 2007 or a newer version of Excel.
|
||||
pivotTableVersion = 3
|
||||
defaultPictureScale = 1.0
|
||||
defaultChartDimensionWidth = 480
|
||||
defaultChartDimensionHeight = 290
|
||||
defaultChartLegendPosition = "bottom"
|
||||
defaultChartShowBlanksAs = "gap"
|
||||
defaultShapeSize = 160
|
||||
defaultShapeLineWidth = 1
|
||||
)
|
||||
|
||||
// ColorMappingType is the type of color transformation.
|
||||
type ColorMappingType byte
|
||||
|
||||
// Color transformation types enumeration.
|
||||
const (
|
||||
ColorMappingTypeLight1 ColorMappingType = iota
|
||||
ColorMappingTypeDark1
|
||||
ColorMappingTypeLight2
|
||||
ColorMappingTypeDark2
|
||||
ColorMappingTypeAccent1
|
||||
ColorMappingTypeAccent2
|
||||
ColorMappingTypeAccent3
|
||||
ColorMappingTypeAccent4
|
||||
ColorMappingTypeAccent5
|
||||
ColorMappingTypeAccent6
|
||||
ColorMappingTypeHyperlink
|
||||
ColorMappingTypeFollowedHyperlink
|
||||
ColorMappingTypeUnset int = -1
|
||||
)
|
||||
|
||||
// IndexedColorMapping is the table of default mappings from indexed color value
|
||||
// to RGB value. Note that 0-7 are redundant of 8-15 to preserve backwards
|
||||
// compatibility. A legacy indexing scheme for colors that is still required
|
||||
// for some records, and for backwards compatibility with legacy formats. This
|
||||
// element contains a sequence of RGB color values that correspond to color
|
||||
// indexes (zero-based). When using the default indexed color palette, the
|
||||
// values are not written out, but instead are implied. When the color palette
|
||||
// has been modified from default, then the entire color palette is written
|
||||
// out.
|
||||
var IndexedColorMapping = []string{
|
||||
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
|
||||
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
|
||||
"800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080",
|
||||
"9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF",
|
||||
"000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF",
|
||||
"00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99",
|
||||
"3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696",
|
||||
"003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333",
|
||||
"000000", "FFFFFF",
|
||||
}
|
||||
|
||||
// supportedImageTypes defined supported image types.
|
||||
var supportedImageTypes = map[string]string{
|
||||
".bmp": ".bmp", ".emf": ".emf", ".emz": ".emz", ".gif": ".gif",
|
||||
".jpeg": ".jpeg", ".jpg": ".jpeg", ".png": ".png", ".svg": ".svg",
|
||||
".tif": ".tiff", ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz",
|
||||
}
|
||||
|
||||
// supportedContentTypes defined supported file format types.
|
||||
var supportedContentTypes = map[string]string{
|
||||
".xlam": ContentTypeAddinMacro,
|
||||
".xlsm": ContentTypeMacro,
|
||||
".xlsx": ContentTypeSheetML,
|
||||
".xltm": ContentTypeTemplateMacro,
|
||||
".xltx": ContentTypeTemplate,
|
||||
}
|
||||
|
||||
// supportedUnderlineTypes defined supported underline types.
|
||||
var supportedUnderlineTypes = []string{"none", "single", "double"}
|
||||
|
||||
// supportedDrawingUnderlineTypes defined supported underline types in drawing
|
||||
// markup language.
|
||||
var supportedDrawingUnderlineTypes = []string{
|
||||
"none", "words", "sng", "dbl", "heavy", "dotted", "dottedHeavy", "dash", "dashHeavy", "dashLong", "dashLongHeavy", "dotDash", "dotDashHeavy", "dotDotDash", "dotDotDashHeavy", "wavy", "wavyHeavy",
|
||||
"wavyDbl",
|
||||
}
|
||||
|
||||
// xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
|
||||
// element specifies non-visual canvas properties. This allows for additional
|
||||
// information that does not affect the appearance of the picture to be stored.
|
||||
|
@ -285,8 +83,8 @@ type xlsxOff struct {
|
|||
Y int `xml:"y,attr"`
|
||||
}
|
||||
|
||||
// xlsxExt directly maps the ext element.
|
||||
type xlsxExt struct {
|
||||
// aExt directly maps the a:ext element.
|
||||
type aExt struct {
|
||||
Cx int `xml:"cx,attr"`
|
||||
Cy int `xml:"cy,attr"`
|
||||
}
|
||||
|
@ -305,7 +103,7 @@ type xlsxPrstGeom struct {
|
|||
// be for a shape or group shape.
|
||||
type xlsxXfrm struct {
|
||||
Off xlsxOff `xml:"a:off"`
|
||||
Ext xlsxExt `xml:"a:ext"`
|
||||
Ext aExt `xml:"a:ext"`
|
||||
}
|
||||
|
||||
// xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
|
||||
|
@ -364,6 +162,7 @@ type xlsxBlipFill struct {
|
|||
// maximum value of less than or equal to 20116800.
|
||||
type xlsxLineProperties struct {
|
||||
W int `xml:"w,attr,omitempty"`
|
||||
SolidFill *xlsxInnerXML `xml:"a:solidFill"`
|
||||
}
|
||||
|
||||
// xlsxSpPr directly maps the spPr (Shape Properties). This element specifies
|
||||
|
@ -374,6 +173,7 @@ type xlsxLineProperties struct {
|
|||
type xlsxSpPr struct {
|
||||
Xfrm xlsxXfrm `xml:"a:xfrm"`
|
||||
PrstGeom xlsxPrstGeom `xml:"a:prstGeom"`
|
||||
SolidFill *xlsxInnerXML `xml:"a:solidFill"`
|
||||
Ln xlsxLineProperties `xml:"a:ln"`
|
||||
}
|
||||
|
||||
|
@ -414,22 +214,55 @@ type xdrClientData struct {
|
|||
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
|
||||
}
|
||||
|
||||
// xdrCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape Size)
|
||||
// and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies a two
|
||||
// cell anchor placeholder for a group, a shape, or a drawing element. It moves
|
||||
// with cells and its extents are in EMU units.
|
||||
// xdrCellAnchor specifies a oneCellAnchor (One Cell Anchor Shape Size) and
|
||||
// twoCellAnchor (Two Cell Anchor Shape Size) placeholder for a group, a shape,
|
||||
// or a drawing element. It moves with cells and its extents are in EMU units.
|
||||
type xdrCellAnchor struct {
|
||||
EditAs string `xml:"editAs,attr,omitempty"`
|
||||
Pos *xlsxPoint2D `xml:"xdr:pos"`
|
||||
From *xlsxFrom `xml:"xdr:from"`
|
||||
To *xlsxTo `xml:"xdr:to"`
|
||||
Ext *xlsxExt `xml:"xdr:ext"`
|
||||
Ext *aExt `xml:"xdr:ext"`
|
||||
Sp *xdrSp `xml:"xdr:sp"`
|
||||
Pic *xlsxPic `xml:"xdr:pic,omitempty"`
|
||||
GraphicFrame string `xml:",innerxml"`
|
||||
AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
|
||||
ClientData *xdrClientData `xml:"xdr:clientData"`
|
||||
}
|
||||
|
||||
// xlsxCellAnchorPos defines the structure used to serialize the cell anchor for
|
||||
// adjust drawing object on inserting/deleting column/rows.
|
||||
type xlsxCellAnchorPos struct {
|
||||
EditAs string `xml:"editAs,attr,omitempty"`
|
||||
From *xlsxFrom `xml:"xdr:from"`
|
||||
To *xlsxTo `xml:"xdr:to"`
|
||||
Pos *xlsxInnerXML `xml:"xdr:pos"`
|
||||
Ext *xlsxInnerXML `xml:"xdr:ext"`
|
||||
Sp *xlsxSp `xml:"xdr:sp"`
|
||||
GrpSp *xlsxInnerXML `xml:"xdr:grpSp"`
|
||||
GraphicFrame *xlsxInnerXML `xml:"xdr:graphicFrame"`
|
||||
CxnSp *xlsxInnerXML `xml:"xdr:cxnSp"`
|
||||
Pic *xlsxInnerXML `xml:"xdr:pic"`
|
||||
ContentPart *xlsxInnerXML `xml:"xdr:contentPart"`
|
||||
AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
|
||||
ClientData *xlsxInnerXML `xml:"xdr:clientData"`
|
||||
}
|
||||
|
||||
// xdrSp (Shape) directly maps the sp element. This element specifies the
|
||||
// existence of a single shape. A shape can either be a preset or a custom
|
||||
// geometry, defined using the SpreadsheetDrawingML framework. In addition to
|
||||
// a geometry each shape can have both visual and non-visual properties
|
||||
// attached. Text and corresponding styling information can also be attached
|
||||
// to a shape. This shape is specified along with all other shapes within
|
||||
// either the shape tree or group shape elements.
|
||||
type xlsxSp struct {
|
||||
Macro string `xml:"macro,attr,omitempty"`
|
||||
TextLink string `xml:"textlink,attr,omitempty"`
|
||||
FLocksText bool `xml:"fLocksText,attr,omitempty"`
|
||||
FPublished *bool `xml:"fPublished,attr"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxPoint2D describes the position of a drawing element within a spreadsheet.
|
||||
type xlsxPoint2D struct {
|
||||
XMLName xml.Name `xml:"xdr:pos"`
|
||||
|
@ -440,8 +273,9 @@ type xlsxPoint2D struct {
|
|||
// xlsxWsDr directly maps the root element for a part of this content type shall
|
||||
// wsDr.
|
||||
type xlsxWsDr struct {
|
||||
sync.Mutex
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"xdr:wsDr"`
|
||||
NS string `xml:"xmlns,attr,omitempty"`
|
||||
A string `xml:"xmlns:a,attr,omitempty"`
|
||||
Xdr string `xml:"xmlns:xdr,attr,omitempty"`
|
||||
R string `xml:"xmlns:r,attr,omitempty"`
|
||||
|
@ -491,6 +325,12 @@ type xlsxGraphic struct {
|
|||
type xlsxGraphicData struct {
|
||||
URI string `xml:"uri,attr"`
|
||||
Chart *xlsxChart `xml:"c:chart,omitempty"`
|
||||
Sle *xlsxSle `xml:"sle:slicer"`
|
||||
}
|
||||
|
||||
type xlsxSle struct {
|
||||
XMLNS string `xml:"xmlns:sle,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
}
|
||||
|
||||
// xlsxChart (Chart) directly maps the c:chart element.
|
||||
|
@ -508,6 +348,7 @@ type xlsxChart struct {
|
|||
// This shape is specified along with all other shapes within either the shape
|
||||
// tree or group shape elements.
|
||||
type xdrSp struct {
|
||||
XMLName xml.Name `xml:"xdr:sp"`
|
||||
Macro string `xml:"macro,attr"`
|
||||
Textlink string `xml:"textlink,attr"`
|
||||
NvSpPr *xdrNvSpPr `xml:"xdr:nvSpPr"`
|
||||
|
@ -586,6 +427,7 @@ type Picture struct {
|
|||
Extension string
|
||||
File []byte
|
||||
Format *GraphicOptions
|
||||
InsertType PictureInsertType
|
||||
}
|
||||
|
||||
// GraphicOptions directly maps the format settings of the picture.
|
||||
|
@ -595,6 +437,7 @@ type GraphicOptions struct {
|
|||
Locked *bool
|
||||
LockAspectRatio bool
|
||||
AutoFit bool
|
||||
AutoFitIgnoreAspect bool
|
||||
OffsetX int
|
||||
OffsetY int
|
||||
ScaleX float64
|
||||
|
@ -606,6 +449,7 @@ type GraphicOptions struct {
|
|||
|
||||
// Shape directly maps the format settings of the shape.
|
||||
type Shape struct {
|
||||
Cell string
|
||||
Type string
|
||||
Macro string
|
||||
Width uint
|
||||
|
@ -616,13 +460,6 @@ type Shape struct {
|
|||
Paragraph []RichTextRun
|
||||
}
|
||||
|
||||
// ShapeColor directly maps the color settings of the shape.
|
||||
type ShapeColor struct {
|
||||
Line string
|
||||
Fill string
|
||||
Effect string
|
||||
}
|
||||
|
||||
// ShapeLine directly maps the line settings of the shape.
|
||||
type ShapeLine struct {
|
||||
Color string
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// 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 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
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// xlsxMetadata directly maps the metadata element. A cell in a spreadsheet
|
||||
// application can have metadata associated with it. Metadata is just a set of
|
||||
// additional properties about the particular cell, and this metadata is stored
|
||||
// in the metadata xml part. There are two types of metadata: cell metadata and
|
||||
// value metadata. Cell metadata contains information about the cell itself,
|
||||
// and this metadata can be carried along with the cell as it moves
|
||||
// (insert, shift, copy/paste, merge, unmerge, etc). Value metadata is
|
||||
// information about the value of a particular cell. Value metadata properties
|
||||
// can be propagated along with the value as it is referenced in formulas.
|
||||
type xlsxMetadata struct {
|
||||
XMLName xml.Name `xml:"metadata"`
|
||||
MetadataTypes *xlsxInnerXML `xml:"metadataTypes"`
|
||||
MetadataStrings *xlsxInnerXML `xml:"metadataStrings"`
|
||||
MdxMetadata *xlsxInnerXML `xml:"mdxMetadata"`
|
||||
FutureMetadata []xlsxFutureMetadata `xml:"futureMetadata"`
|
||||
CellMetadata *xlsxMetadataBlocks `xml:"cellMetadata"`
|
||||
ValueMetadata *xlsxMetadataBlocks `xml:"valueMetadata"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxFutureMetadata directly maps the futureMetadata element. This element
|
||||
// represents future metadata information.
|
||||
type xlsxFutureMetadata struct {
|
||||
Bk []xlsxFutureMetadataBlock `xml:"bk"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxFutureMetadataBlock directly maps the kb element. This element represents
|
||||
// a block of future metadata information. This is a location for storing
|
||||
// feature extension information.
|
||||
type xlsxFutureMetadataBlock struct {
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxMetadataBlocks directly maps the metadata element. This element
|
||||
// represents cell metadata information. Cell metadata is information metadata
|
||||
// about a specific cell, and it stays tied to that cell position.
|
||||
type xlsxMetadataBlocks struct {
|
||||
Count int `xml:"count,attr,omitempty"`
|
||||
Bk []xlsxMetadataBlock `xml:"bk"`
|
||||
}
|
||||
|
||||
// xlsxMetadataBlock directly maps the bk element. This element represents a
|
||||
// block of metadata records.
|
||||
type xlsxMetadataBlock struct {
|
||||
Rc []xlsxMetadataRecord `xml:"rc"`
|
||||
}
|
||||
|
||||
// xlsxMetadataRecord directly maps the rc element. This element represents a
|
||||
// reference to a specific metadata record.
|
||||
type xlsxMetadataRecord struct {
|
||||
T int `xml:"t,attr"`
|
||||
V int `xml:"v,attr"`
|
||||
}
|
||||
|
||||
// xlsxRichValueData directly maps the rvData element that specifies rich value
|
||||
// data.
|
||||
type xlsxRichValueData struct {
|
||||
XMLName xml.Name `xml:"rvData"`
|
||||
Count int `xml:"count,attr,omitempty"`
|
||||
Rv []xlsxRichValue `xml:"rv"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxRichValue directly maps the rv element that specifies rich value data
|
||||
// information for a single rich value
|
||||
type xlsxRichValue struct {
|
||||
S int `xml:"s,attr"`
|
||||
V []string `xml:"v"`
|
||||
Fb *xlsxInnerXML `xml:"fb"`
|
||||
}
|
||||
|
||||
// xlsxRichValueRels directly maps the richValueRels element. This element that
|
||||
// specifies a list of rich value relationships.
|
||||
type xlsxRichValueRels struct {
|
||||
XMLName xml.Name `xml:"richValueRels"`
|
||||
Rels []xlsxRichValueRelRelationship `xml:"rel"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxRichValueRelRelationship directly maps the rel element. This element
|
||||
// specifies a relationship for a rich value property.
|
||||
type xlsxRichValueRelRelationship struct {
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
// xlsxWebImagesSupportingRichData directly maps the webImagesSrd element. This
|
||||
// element specifies a list of sets of properties associated with web image rich
|
||||
// values.
|
||||
type xlsxWebImagesSupportingRichData struct {
|
||||
XMLName xml.Name `xml:"webImagesSrd"`
|
||||
WebImageSrd []xlsxWebImageSupportingRichData `xml:"webImageSrd"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxWebImageSupportingRichData directly maps the webImageSrd element. This
|
||||
// element specifies a set of properties for a web image rich value.
|
||||
type xlsxWebImageSupportingRichData struct {
|
||||
Address xlsxExternalReference `xml:"address"`
|
||||
MoreImagesAddress xlsxExternalReference `xml:"moreImagesAddress"`
|
||||
Blip xlsxExternalReference `xml:"blip"`
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -134,12 +134,12 @@ type xlsxSharedItems struct {
|
|||
MaxDate string `xml:"maxDate,attr,omitempty"`
|
||||
Count int `xml:"count,attr"`
|
||||
LongText bool `xml:"longText,attr,omitempty"`
|
||||
M *xlsxMissing `xml:"m"`
|
||||
N *xlsxNumber `xml:"n"`
|
||||
B *xlsxBoolean `xml:"b"`
|
||||
E *xlsxError `xml:"e"`
|
||||
S *xlsxString `xml:"s"`
|
||||
D *xlsxDateTime `xml:"d"`
|
||||
M []xlsxMissing `xml:"m"`
|
||||
N []xlsxNumber `xml:"n"`
|
||||
B []xlsxBoolean `xml:"b"`
|
||||
E []xlsxError `xml:"e"`
|
||||
S []xlsxString `xml:"s"`
|
||||
D []xlsxDateTime `xml:"d"`
|
||||
}
|
||||
|
||||
// xlsxMissing represents a value that was not specified.
|
||||
|
@ -226,3 +226,17 @@ type xlsxMeasureGroups struct{}
|
|||
|
||||
// xlsxMaps represents the PivotTable OLAP measure group - Dimension maps.
|
||||
type xlsxMaps struct{}
|
||||
|
||||
// xlsxX14PivotCacheDefinition specifies the extended properties of a pivot
|
||||
// table cache definition.
|
||||
type xlsxX14PivotCacheDefinition struct {
|
||||
XMLName xml.Name `xml:"x14:pivotCacheDefinition"`
|
||||
PivotCacheID int `xml:"pivotCacheId,attr"`
|
||||
}
|
||||
|
||||
// decodeX14PivotCacheDefinition defines the structure used to parse the
|
||||
// x14:pivotCacheDefinition element of a pivot table cache.
|
||||
type decodeX14PivotCacheDefinition struct {
|
||||
XMLName xml.Name `xml:"pivotCacheDefinition"`
|
||||
PivotCacheID int `xml:"pivotCacheId,attr"`
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -56,15 +56,15 @@ type xlsxPivotTableDefinition struct {
|
|||
EnableDrill bool `xml:"enableDrill,attr,omitempty"`
|
||||
EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"`
|
||||
PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"`
|
||||
UseAutoFormatting *bool `xml:"useAutoFormatting,attr,omitempty"`
|
||||
UseAutoFormatting *bool `xml:"useAutoFormatting,attr"`
|
||||
PageWrap int `xml:"pageWrap,attr,omitempty"`
|
||||
PageOverThenDown *bool `xml:"pageOverThenDown,attr,omitempty"`
|
||||
PageOverThenDown *bool `xml:"pageOverThenDown,attr"`
|
||||
SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"`
|
||||
RowGrandTotals *bool `xml:"rowGrandTotals,attr,omitempty"`
|
||||
ColGrandTotals *bool `xml:"colGrandTotals,attr,omitempty"`
|
||||
RowGrandTotals *bool `xml:"rowGrandTotals,attr"`
|
||||
ColGrandTotals *bool `xml:"colGrandTotals,attr"`
|
||||
FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"`
|
||||
ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"`
|
||||
MergeItem *bool `xml:"mergeItem,attr,omitempty"`
|
||||
MergeItem *bool `xml:"mergeItem,attr"`
|
||||
ShowDropZones bool `xml:"showDropZones,attr,omitempty"`
|
||||
CreatedVersion int `xml:"createdVersion,attr,omitempty"`
|
||||
Indent int `xml:"indent,attr,omitempty"`
|
||||
|
@ -74,7 +74,7 @@ type xlsxPivotTableDefinition struct {
|
|||
Compact *bool `xml:"compact,attr"`
|
||||
Outline *bool `xml:"outline,attr"`
|
||||
OutlineData bool `xml:"outlineData,attr,omitempty"`
|
||||
CompactData *bool `xml:"compactData,attr,omitempty"`
|
||||
CompactData *bool `xml:"compactData,attr"`
|
||||
Published bool `xml:"published,attr,omitempty"`
|
||||
GridDropZones bool `xml:"gridDropZones,attr,omitempty"`
|
||||
Immersive bool `xml:"immersive,attr,omitempty"`
|
||||
|
@ -150,7 +150,7 @@ type xlsxPivotField struct {
|
|||
DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"`
|
||||
NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"`
|
||||
RankBy int `xml:"rankBy,attr,omitempty"`
|
||||
DefaultSubtotal *bool `xml:"defaultSubtotal,attr,omitempty"`
|
||||
DefaultSubtotal *bool `xml:"defaultSubtotal,attr"`
|
||||
SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"`
|
||||
CountASubtotal bool `xml:"countASubtotal,attr,omitempty"`
|
||||
AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"`
|
||||
|
@ -273,7 +273,7 @@ type xlsxDataField struct {
|
|||
ShowDataAs string `xml:"showDataAs,attr,omitempty"`
|
||||
BaseField int `xml:"baseField,attr,omitempty"`
|
||||
BaseItem int64 `xml:"baseItem,attr,omitempty"`
|
||||
NumFmtID string `xml:"numFmtId,attr,omitempty"`
|
||||
NumFmtID int `xml:"numFmtId,attr,omitempty"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,11 +7,14 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// xlsxSST directly maps the sst element from the namespace
|
||||
// http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may
|
||||
|
@ -21,6 +24,7 @@ import "encoding/xml"
|
|||
// is an indexed list of string values, shared across the workbook, which allows
|
||||
// implementations to store values only once.
|
||||
type xlsxSST struct {
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main sst"`
|
||||
Count int `xml:"count,attr"`
|
||||
UniqueCount int `xml:"uniqueCount,attr"`
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// 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 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
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// xlsxSlicers directly maps the slicers element that specifies a slicer view on
|
||||
// the worksheet.
|
||||
type xlsxSlicers struct {
|
||||
XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicers"`
|
||||
XMLNSXMC string `xml:"xmlns:mc,attr"`
|
||||
XMLNSX string `xml:"xmlns:x,attr"`
|
||||
XMLNSXR10 string `xml:"xmlns:xr10,attr"`
|
||||
Slicer []xlsxSlicer `xml:"slicer"`
|
||||
}
|
||||
|
||||
// xlsxSlicer is a complex type that specifies a slicer view.
|
||||
type xlsxSlicer struct {
|
||||
Name string `xml:"name,attr"`
|
||||
XR10UID string `xml:"xr10:uid,attr,omitempty"`
|
||||
Cache string `xml:"cache,attr"`
|
||||
Caption string `xml:"caption,attr,omitempty"`
|
||||
StartItem *int `xml:"startItem,attr"`
|
||||
ColumnCount *int `xml:"columnCount,attr"`
|
||||
ShowCaption *bool `xml:"showCaption,attr"`
|
||||
Level int `xml:"level,attr,omitempty"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
LockedPosition bool `xml:"lockedPosition,attr,omitempty"`
|
||||
RowHeight int `xml:"rowHeight,attr"`
|
||||
}
|
||||
|
||||
// slicerCacheDefinition directly maps the slicerCacheDefinition element that
|
||||
// specifies a slicer cache.
|
||||
type xlsxSlicerCacheDefinition struct {
|
||||
XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicerCacheDefinition"`
|
||||
XMLNSXMC string `xml:"xmlns:mc,attr"`
|
||||
XMLNSX string `xml:"xmlns:x,attr"`
|
||||
XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"`
|
||||
XMLNSXR10 string `xml:"xmlns:xr10,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
XR10UID string `xml:"xr10:uid,attr,omitempty"`
|
||||
SourceName string `xml:"sourceName,attr"`
|
||||
PivotTables *xlsxSlicerCachePivotTables `xml:"pivotTables"`
|
||||
Data *xlsxSlicerCacheData `xml:"data"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxSlicerCachePivotTables is a complex type that specifies a group of
|
||||
// pivotTable elements that specify the PivotTable views that are filtered by
|
||||
// the slicer cache.
|
||||
type xlsxSlicerCachePivotTables struct {
|
||||
PivotTable []xlsxSlicerCachePivotTable `xml:"pivotTable"`
|
||||
}
|
||||
|
||||
// xlsxSlicerCachePivotTable is a complex type that specifies a PivotTable view
|
||||
// filtered by a slicer cache.
|
||||
type xlsxSlicerCachePivotTable struct {
|
||||
TabID int `xml:"tabId,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
}
|
||||
|
||||
// xlsxSlicerCacheData is a complex type that specifies a data source for the
|
||||
// slicer cache.
|
||||
type xlsxSlicerCacheData struct {
|
||||
OLAP *xlsxInnerXML `xml:"olap"`
|
||||
Tabular *xlsxTabularSlicerCache `xml:"tabular"`
|
||||
}
|
||||
|
||||
// xlsxTabularSlicerCache is a complex type that specifies non-OLAP slicer items
|
||||
// that are cached within this slicer cache and properties of the slicer cache
|
||||
// specific to non-OLAP slicer items.
|
||||
type xlsxTabularSlicerCache struct {
|
||||
PivotCacheID int `xml:"pivotCacheId,attr"`
|
||||
SortOrder string `xml:"sortOrder,attr,omitempty"`
|
||||
CustomListSort *bool `xml:"customListSort,attr"`
|
||||
ShowMissing *bool `xml:"showMissing,attr"`
|
||||
CrossFilter string `xml:"crossFilter,attr,omitempty"`
|
||||
Items *xlsxTabularSlicerCacheItems `xml:"items"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxTabularSlicerCacheItems is a complex type that specifies non-OLAP slicer
|
||||
// items that are cached within this slicer cache.
|
||||
type xlsxTabularSlicerCacheItems struct {
|
||||
Count int `xml:"count,attr,omitempty"`
|
||||
I []xlsxTabularSlicerCacheItem `xml:"i"`
|
||||
}
|
||||
|
||||
// xlsxTabularSlicerCacheItem is a complex type that specifies a non-OLAP slicer
|
||||
// item that is cached within this slicer cache.
|
||||
type xlsxTabularSlicerCacheItem struct {
|
||||
X int `xml:"x,attr"`
|
||||
S bool `xml:"s,attr,omitempty"`
|
||||
ND bool `xml:"nd,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxTableSlicerCache specifies a table data source for the slicer cache.
|
||||
type xlsxTableSlicerCache struct {
|
||||
XMLName xml.Name `xml:"x15:tableSlicerCache"`
|
||||
TableID int `xml:"tableId,attr"`
|
||||
Column int `xml:"column,attr"`
|
||||
SortOrder string `xml:"sortOrder,attr,omitempty"`
|
||||
CustomListSort *bool `xml:"customListSort,attr"`
|
||||
CrossFilter string `xml:"crossFilter,attr,omitempty"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxX14SlicerList specifies a list of slicer.
|
||||
type xlsxX14SlicerList struct {
|
||||
XMLName xml.Name `xml:"x14:slicerList"`
|
||||
Slicer []*xlsxX14Slicer `xml:"x14:slicer"`
|
||||
}
|
||||
|
||||
// xlsxX14Slicer specifies a slicer view,
|
||||
type xlsxX14Slicer struct {
|
||||
XMLName xml.Name `xml:"x14:slicer"`
|
||||
RID string `xml:"r:id,attr"`
|
||||
}
|
||||
|
||||
// xlsxX14SlicerCaches directly maps the x14:slicerCache element.
|
||||
type xlsxX14SlicerCaches struct {
|
||||
XMLName xml.Name `xml:"x14:slicerCaches"`
|
||||
XMLNS string `xml:"xmlns:x14,attr"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxX15SlicerCaches directly maps the x14:slicerCache element.
|
||||
type xlsxX14SlicerCache struct {
|
||||
XMLName xml.Name `xml:"x14:slicerCache"`
|
||||
RID string `xml:"r:id,attr"`
|
||||
}
|
||||
|
||||
// xlsxX15SlicerCaches directly maps the x15:slicerCaches element.
|
||||
type xlsxX15SlicerCaches struct {
|
||||
XMLName xml.Name `xml:"x15:slicerCaches"`
|
||||
XMLNS string `xml:"xmlns:x14,attr"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// decodeTableSlicerCache defines the structure used to parse the
|
||||
// x15:tableSlicerCache element of the table slicer cache.
|
||||
type decodeTableSlicerCache struct {
|
||||
XMLName xml.Name `xml:"tableSlicerCache"`
|
||||
TableID int `xml:"tableId,attr"`
|
||||
Column int `xml:"column,attr"`
|
||||
SortOrder string `xml:"sortOrder,attr"`
|
||||
}
|
||||
|
||||
// decodeSlicerList defines the structure used to parse the x14:slicerList
|
||||
// element of a list of slicer.
|
||||
type decodeSlicerList struct {
|
||||
XMLName xml.Name `xml:"slicerList"`
|
||||
Slicer []*decodeSlicer `xml:"slicer"`
|
||||
}
|
||||
|
||||
// decodeSlicer defines the structure used to parse the x14:slicer element of a
|
||||
// slicer.
|
||||
type decodeSlicer struct {
|
||||
RID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
// decodeSlicerCaches defines the structure used to parse the
|
||||
// x14:slicerCaches and x15:slicerCaches element of a slicer cache.
|
||||
type decodeSlicerCaches struct {
|
||||
XMLName xml.Name `xml:"slicerCaches"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxTimelines is a mechanism for filtering data in pivot table views, cube
|
||||
// functions and charts based on non-worksheet pivot tables. In the case of
|
||||
// using OLAP Timeline source data, a Timeline is based on a key attribute of
|
||||
// an OLAP hierarchy. In the case of using native Timeline source data, a
|
||||
// Timeline is based on a data table column.
|
||||
type xlsxTimelines struct {
|
||||
XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2010/11/main timelines"`
|
||||
XMLNSXMC string `xml:"xmlns:mc,attr"`
|
||||
XMLNSX string `xml:"xmlns:x,attr"`
|
||||
XMLNSXR10 string `xml:"xmlns:xr10,attr"`
|
||||
Timeline []xlsxTimeline `xml:"timeline"`
|
||||
}
|
||||
|
||||
// xlsxTimeline is timeline view specifies the display of a timeline on a
|
||||
// worksheet.
|
||||
type xlsxTimeline struct {
|
||||
Name string `xml:"name,attr"`
|
||||
XR10UID string `xml:"xr10:uid,attr,omitempty"`
|
||||
Cache string `xml:"cache,attr"`
|
||||
Caption string `xml:"caption,attr,omitempty"`
|
||||
ShowHeader *bool `xml:"showHeader,attr"`
|
||||
ShowSelectionLabel *bool `xml:"showSelectionLabel,attr"`
|
||||
ShowTimeLevel *bool `xml:"showTimeLevel,attr"`
|
||||
ShowHorizontalScrollbar *bool `xml:"showHorizontalScrollbar,attr"`
|
||||
Level int `xml:"level,attr"`
|
||||
SelectionLevel int `xml:"selectionLevel,attr"`
|
||||
ScrollPosition string `xml:"scrollPosition,attr,omitempty"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
}
|
44
xmlStyles.go
44
xmlStyles.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
|
||||
// xlsxStyleSheet is the root element of the Styles part.
|
||||
type xlsxStyleSheet struct {
|
||||
sync.Mutex
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"`
|
||||
NumFmts *xlsxNumFmts `xml:"numFmts"`
|
||||
Fonts *xlsxFonts `xml:"fonts"`
|
||||
|
@ -65,10 +65,10 @@ type xlsxLine struct {
|
|||
|
||||
// xlsxColor is a common mapping used for both the fgColor and bgColor elements.
|
||||
// Foreground color of the cell fill pattern. Cell fill patterns operate with
|
||||
// two colors: a background color and a foreground color. These combine together
|
||||
// two colors: a background color and a foreground color. These combine
|
||||
// to make a patterned cell fill. Background color of the cell fill pattern.
|
||||
// Cell fill patterns operate with two colors: a background color and a
|
||||
// foreground color. These combine together to make a patterned cell fill.
|
||||
// foreground color. These combine to make a patterned cell fill.
|
||||
type xlsxColor struct {
|
||||
Auto bool `xml:"auto,attr,omitempty"`
|
||||
RGB string `xml:"rgb,attr,omitempty"`
|
||||
|
@ -103,7 +103,7 @@ type xlsxFont struct {
|
|||
Scheme *attrValString `xml:"scheme"`
|
||||
}
|
||||
|
||||
// xlsxFills directly maps the fills element. This element defines the cell
|
||||
// xlsxFills directly maps the fills' element. This element defines the cell
|
||||
// fills portion of the Styles part, consisting of a sequence of fill records. A
|
||||
// cell fill consists of a background color, foreground color, and pattern to be
|
||||
// applied across the cell.
|
||||
|
@ -147,7 +147,7 @@ type xlsxGradientFillStop struct {
|
|||
Color xlsxColor `xml:"color,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxBorders directly maps the borders element. This element contains borders
|
||||
// xlsxBorders directly maps the borders' element. This element contains borders
|
||||
// formatting information, specifying all border definitions for all cells in
|
||||
// the workbook.
|
||||
type xlsxBorders struct {
|
||||
|
@ -205,7 +205,7 @@ type xlsxCellStyleXfs struct {
|
|||
Xf []xlsxXf `xml:"xf,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxXf directly maps the xf element. A single xf element describes all of the
|
||||
// xlsxXf directly maps the xf element. A single xf element describes all the
|
||||
// formatting for a cell.
|
||||
type xlsxXf struct {
|
||||
NumFmtID *int `xml:"numFmtId,attr"`
|
||||
|
@ -236,8 +236,8 @@ type xlsxCellXfs struct {
|
|||
}
|
||||
|
||||
// xlsxDxfs directly maps the dxfs element. This element contains the master
|
||||
// differential formatting records (dxf's) which define formatting for all non-
|
||||
// cell formatting in this workbook. Whereas xf records fully specify a
|
||||
// differential formatting records (dxf's) which define formatting for all
|
||||
// non-cell formatting in this workbook. Whereas xf records fully specify a
|
||||
// particular aspect of formatting (e.g., cell borders) by referencing those
|
||||
// formatting definitions elsewhere in the Styles part, dxf records specify
|
||||
// incremental (or differential) aspects of formatting directly inline within
|
||||
|
@ -251,18 +251,13 @@ type xlsxDxfs struct {
|
|||
// xlsxDxf directly maps the dxf element. A single dxf record, expressing
|
||||
// incremental formatting to be applied.
|
||||
type xlsxDxf struct {
|
||||
Dxf string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// dxf directly maps the dxf element.
|
||||
type dxf struct {
|
||||
Font *xlsxFont `xml:"font"`
|
||||
NumFmt *xlsxNumFmt `xml:"numFmt"`
|
||||
Fill *xlsxFill `xml:"fill"`
|
||||
Alignment *xlsxAlignment `xml:"alignment"`
|
||||
Border *xlsxBorder `xml:"border"`
|
||||
Protection *xlsxProtection `xml:"protection"`
|
||||
ExtLst *xlsxExt `xml:"extLst"`
|
||||
ExtLst *aExt `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxTableStyles directly maps the tableStyles element. This element
|
||||
|
@ -301,15 +296,23 @@ type xlsxNumFmts struct {
|
|||
// of a cell.
|
||||
type xlsxNumFmt struct {
|
||||
NumFmtID int `xml:"numFmtId,attr"`
|
||||
FormatCode string `xml:"formatCode,attr,omitempty"`
|
||||
FormatCode string `xml:"formatCode,attr"`
|
||||
FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxStyleColors directly maps the colors element. Color information
|
||||
// xlsxIndexedColors directly maps the single ARGB entry for the corresponding
|
||||
// color index.
|
||||
type xlsxIndexedColors struct {
|
||||
RgbColor []xlsxColor `xml:"rgbColor"`
|
||||
}
|
||||
|
||||
// xlsxStyleColors directly maps the colors' element. Color information
|
||||
// associated with this style sheet. This collection is written whenever the
|
||||
// legacy color palette has been modified (backwards compatibility settings) or
|
||||
// a custom color has been selected while using this workbook.
|
||||
type xlsxStyleColors struct {
|
||||
Color string `xml:",innerxml"`
|
||||
IndexedColors *xlsxIndexedColors `xml:"indexedColors"`
|
||||
MruColors *xlsxInnerXML `xml:"mruColors"`
|
||||
}
|
||||
|
||||
// Alignment directly maps the alignment settings of the cells.
|
||||
|
@ -369,8 +372,7 @@ type Style struct {
|
|||
Alignment *Alignment
|
||||
Protection *Protection
|
||||
NumFmt int
|
||||
DecimalPlaces int
|
||||
DecimalPlaces *int
|
||||
CustomNumFmt *string
|
||||
Lang string
|
||||
NegRed bool
|
||||
}
|
||||
|
|
80
xmlTable.go
80
xmlTable.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -21,22 +21,28 @@ import "encoding/xml"
|
|||
type xlsxTable struct {
|
||||
XMLName xml.Name `xml:"table"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
|
||||
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
|
||||
DisplayName string `xml:"displayName,attr,omitempty"`
|
||||
HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
|
||||
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
|
||||
HeaderRowCount *int `xml:"headerRowCount,attr"`
|
||||
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
|
||||
ID int `xml:"id,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
DisplayName string `xml:"displayName,attr,omitempty"`
|
||||
Comment string `xml:"comment,attr,omitempty"`
|
||||
Ref string `xml:"ref,attr"`
|
||||
TableType string `xml:"tableType,attr,omitempty"`
|
||||
HeaderRowCount *int `xml:"headerRowCount,attr"`
|
||||
InsertRow bool `xml:"insertRow,attr,omitempty"`
|
||||
InsertRowShift bool `xml:"insertRowShift,attr,omitempty"`
|
||||
Name string `xml:"name,attr"`
|
||||
Published bool `xml:"published,attr,omitempty"`
|
||||
Ref string `xml:"ref,attr"`
|
||||
TotalsRowCount int `xml:"totalsRowCount,attr,omitempty"`
|
||||
TotalsRowShown *bool `xml:"totalsRowShown,attr"`
|
||||
Published bool `xml:"published,attr,omitempty"`
|
||||
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
|
||||
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
|
||||
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
|
||||
TotalsRowShown bool `xml:"totalsRowShown,attr"`
|
||||
HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
|
||||
TableBorderDxfID int `xml:"tableBorderDxfId,attr,omitempty"`
|
||||
TotalsRowBorderDxfID int `xml:"totalsRowBorderDxfId,attr,omitempty"`
|
||||
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
|
||||
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
|
||||
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
|
||||
ConnectionID int `xml:"connectionId,attr,omitempty"`
|
||||
AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
|
||||
TableColumns *xlsxTableColumns `xml:"tableColumns"`
|
||||
TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"`
|
||||
|
@ -171,18 +177,18 @@ type xlsxTableColumns struct {
|
|||
// xlsxTableColumn directly maps the element representing a single column for
|
||||
// this table.
|
||||
type xlsxTableColumn struct {
|
||||
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
|
||||
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
|
||||
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
|
||||
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
|
||||
ID int `xml:"id,attr"`
|
||||
UniqueName string `xml:"uniqueName,attr,omitempty"`
|
||||
Name string `xml:"name,attr"`
|
||||
QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"`
|
||||
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
|
||||
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
|
||||
TotalsRowFunction string `xml:"totalsRowFunction,attr,omitempty"`
|
||||
TotalsRowLabel string `xml:"totalsRowLabel,attr,omitempty"`
|
||||
UniqueName string `xml:"uniqueName,attr,omitempty"`
|
||||
QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"`
|
||||
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
|
||||
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
|
||||
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
|
||||
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
|
||||
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
|
||||
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxTableStyleInfo directly maps the tableStyleInfo element. This element
|
||||
|
@ -196,8 +202,40 @@ type xlsxTableStyleInfo struct {
|
|||
ShowColumnStripes bool `xml:"showColumnStripes,attr"`
|
||||
}
|
||||
|
||||
// xlsxSingleXMLCells is a single cell table is generated from an XML mapping.
|
||||
// These really just look like regular cells to the spreadsheet user, but shall
|
||||
// be implemented as Tables "under the covers."
|
||||
type xlsxSingleXMLCells struct {
|
||||
XMLName xml.Name `xml:"singleXmlCells"`
|
||||
SingleXmlCell []xlsxSingleXMLCell `xml:"singleXmlCell"`
|
||||
}
|
||||
|
||||
// xlsxSingleXMLCell is a element represents the table properties for a single
|
||||
// cell XML table.
|
||||
type xlsxSingleXMLCell struct {
|
||||
XMLName xml.Name `xml:"singleXmlCell"`
|
||||
ID int `xml:"id,attr"`
|
||||
R string `xml:"r,attr"`
|
||||
ConnectionID int `xml:"connectionId,attr"`
|
||||
XMLCellPr xlsxXMLCellPr `xml:"xmlCellPr"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// xlsxXMLCellPr is a element stores the XML properties for the cell of a single
|
||||
// cell xml table.
|
||||
type xlsxXMLCellPr struct {
|
||||
XMLName xml.Name `xml:"xmlCellPr"`
|
||||
ID int `xml:"id,attr"`
|
||||
UniqueName string `xml:"uniqueName,attr,omitempty"`
|
||||
XMLPr *xlsxInnerXML `xml:"xmlPr"`
|
||||
ExtLst *xlsxInnerXML `xml:"extLst"`
|
||||
}
|
||||
|
||||
// Table directly maps the format settings of the table.
|
||||
type Table struct {
|
||||
tID int
|
||||
rID string
|
||||
tableXML string
|
||||
Range string
|
||||
Name string
|
||||
StyleName string
|
||||
|
|
168
xmlTheme.go
168
xmlTheme.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -16,15 +16,15 @@ import "encoding/xml"
|
|||
// xlsxTheme directly maps the theme element in the namespace
|
||||
// http://schemas.openxmlformats.org/drawingml/2006/main
|
||||
type xlsxTheme struct {
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"`
|
||||
XMLName xml.Name `xml:"a:theme"`
|
||||
XMLNSa string `xml:"xmlns:a,attr"`
|
||||
XMLNSr string `xml:"xmlns:r,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
ThemeElements xlsxBaseStyles `xml:"themeElements"`
|
||||
ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"`
|
||||
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"`
|
||||
CustClrLst *xlsxInnerXML `xml:"custClrLst"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
ThemeElements xlsxBaseStyles `xml:"a:themeElements"`
|
||||
ObjectDefaults xlsxObjectDefaults `xml:"a:objectDefaults"`
|
||||
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"a:extraClrSchemeLst"`
|
||||
CustClrLst *xlsxInnerXML `xml:"a:custClrLst"`
|
||||
ExtLst *xlsxExtLst `xml:"a:extLst"`
|
||||
}
|
||||
|
||||
// xlsxBaseStyles defines the theme elements for a theme, and is the workhorse
|
||||
|
@ -33,40 +33,40 @@ type xlsxTheme struct {
|
|||
// scheme, a font scheme, and a style matrix (format scheme) that defines
|
||||
// different formatting options for different pieces of a document.
|
||||
type xlsxBaseStyles struct {
|
||||
ClrScheme xlsxColorScheme `xml:"clrScheme"`
|
||||
FontScheme xlsxFontScheme `xml:"fontScheme"`
|
||||
FmtScheme xlsxStyleMatrix `xml:"fmtScheme"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
ClrScheme xlsxColorScheme `xml:"a:clrScheme"`
|
||||
FontScheme xlsxFontScheme `xml:"a:fontScheme"`
|
||||
FmtScheme xlsxStyleMatrix `xml:"a:fmtScheme"`
|
||||
ExtLst *xlsxExtLst `xml:"a:extLst"`
|
||||
}
|
||||
|
||||
// xlsxCTColor holds the actual color values that are to be applied to a given
|
||||
// diagram and how those colors are to be applied.
|
||||
type xlsxCTColor struct {
|
||||
ScrgbClr *xlsxInnerXML `xml:"scrgbClr"`
|
||||
SrgbClr *attrValString `xml:"srgbClr"`
|
||||
HslClr *xlsxInnerXML `xml:"hslClr"`
|
||||
SysClr *xlsxSysClr `xml:"sysClr"`
|
||||
SchemeClr *xlsxInnerXML `xml:"schemeClr"`
|
||||
PrstClr *xlsxInnerXML `xml:"prstClr"`
|
||||
ScrgbClr *xlsxInnerXML `xml:"a:scrgbClr"`
|
||||
SrgbClr *attrValString `xml:"a:srgbClr"`
|
||||
HslClr *xlsxInnerXML `xml:"a:hslClr"`
|
||||
SysClr *xlsxSysClr `xml:"a:sysClr"`
|
||||
SchemeClr *xlsxInnerXML `xml:"a:schemeClr"`
|
||||
PrstClr *xlsxInnerXML `xml:"a:prstClr"`
|
||||
}
|
||||
|
||||
// xlsxColorScheme defines a set of colors for the theme. The set of colors
|
||||
// consists of twelve color slots that can each hold a color of choice.
|
||||
type xlsxColorScheme struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Dk1 xlsxCTColor `xml:"dk1"`
|
||||
Lt1 xlsxCTColor `xml:"lt1"`
|
||||
Dk2 xlsxCTColor `xml:"dk2"`
|
||||
Lt2 xlsxCTColor `xml:"lt2"`
|
||||
Accent1 xlsxCTColor `xml:"accent1"`
|
||||
Accent2 xlsxCTColor `xml:"accent2"`
|
||||
Accent3 xlsxCTColor `xml:"accent3"`
|
||||
Accent4 xlsxCTColor `xml:"accent4"`
|
||||
Accent5 xlsxCTColor `xml:"accent5"`
|
||||
Accent6 xlsxCTColor `xml:"accent6"`
|
||||
Hlink xlsxCTColor `xml:"hlink"`
|
||||
FolHlink xlsxCTColor `xml:"folHlink"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
Dk1 xlsxCTColor `xml:"a:dk1"`
|
||||
Lt1 xlsxCTColor `xml:"a:lt1"`
|
||||
Dk2 xlsxCTColor `xml:"a:dk2"`
|
||||
Lt2 xlsxCTColor `xml:"a:lt2"`
|
||||
Accent1 xlsxCTColor `xml:"a:accent1"`
|
||||
Accent2 xlsxCTColor `xml:"a:accent2"`
|
||||
Accent3 xlsxCTColor `xml:"a:accent3"`
|
||||
Accent4 xlsxCTColor `xml:"a:accent4"`
|
||||
Accent5 xlsxCTColor `xml:"a:accent5"`
|
||||
Accent6 xlsxCTColor `xml:"a:accent6"`
|
||||
Hlink xlsxCTColor `xml:"a:hlink"`
|
||||
FolHlink xlsxCTColor `xml:"a:folHlink"`
|
||||
ExtLst *xlsxExtLst `xml:"a:extLst"`
|
||||
}
|
||||
|
||||
// objectDefaults element allows for the definition of default shape, line,
|
||||
|
@ -95,11 +95,11 @@ type xlsxCTSupplementalFont struct {
|
|||
// Asian, and complex script. On top of these three definitions, one can also
|
||||
// define a font for use in a specific language or languages.
|
||||
type xlsxFontCollection struct {
|
||||
Latin *xlsxCTTextFont `xml:"latin"`
|
||||
Ea *xlsxCTTextFont `xml:"ea"`
|
||||
Cs *xlsxCTTextFont `xml:"cs"`
|
||||
Font []xlsxCTSupplementalFont `xml:"font"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
Latin *xlsxCTTextFont `xml:"a:latin"`
|
||||
Ea *xlsxCTTextFont `xml:"a:ea"`
|
||||
Cs *xlsxCTTextFont `xml:"a:cs"`
|
||||
Font []xlsxCTSupplementalFont `xml:"a:font"`
|
||||
ExtLst *xlsxExtLst `xml:"a:extLst"`
|
||||
}
|
||||
|
||||
// xlsxFontScheme element defines the font scheme within the theme. The font
|
||||
|
@ -109,9 +109,9 @@ type xlsxFontCollection struct {
|
|||
// paragraph areas.
|
||||
type xlsxFontScheme struct {
|
||||
Name string `xml:"name,attr"`
|
||||
MajorFont xlsxFontCollection `xml:"majorFont"`
|
||||
MinorFont xlsxFontCollection `xml:"minorFont"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
MajorFont xlsxFontCollection `xml:"a:majorFont"`
|
||||
MinorFont xlsxFontCollection `xml:"a:minorFont"`
|
||||
ExtLst *xlsxExtLst `xml:"a:extLst"`
|
||||
}
|
||||
|
||||
// xlsxStyleMatrix defines a set of formatting options, which can be referenced
|
||||
|
@ -121,10 +121,10 @@ type xlsxFontScheme struct {
|
|||
// change when the theme is changed.
|
||||
type xlsxStyleMatrix struct {
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"`
|
||||
LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"`
|
||||
EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"`
|
||||
BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"`
|
||||
FillStyleLst xlsxFillStyleLst `xml:"a:fillStyleLst"`
|
||||
LnStyleLst xlsxLnStyleLst `xml:"a:lnStyleLst"`
|
||||
EffectStyleLst xlsxEffectStyleLst `xml:"a:effectStyleLst"`
|
||||
BgFillStyleLst xlsxBgFillStyleLst `xml:"a:bgFillStyleLst"`
|
||||
}
|
||||
|
||||
// xlsxFillStyleLst element defines a set of three fill styles that are used
|
||||
|
@ -161,3 +161,85 @@ type xlsxSysClr struct {
|
|||
Val string `xml:"val,attr"`
|
||||
LastClr string `xml:"lastClr,attr"`
|
||||
}
|
||||
|
||||
// decodeTheme defines the structure used to parse the a:theme element for the
|
||||
// theme.
|
||||
type decodeTheme struct {
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"`
|
||||
Name string `xml:"name,attr"`
|
||||
ThemeElements decodeBaseStyles `xml:"themeElements"`
|
||||
ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"`
|
||||
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"`
|
||||
CustClrLst *xlsxInnerXML `xml:"custClrLst"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// decodeBaseStyles defines the structure used to parse the theme elements for a
|
||||
// theme, and is the workhorse of the theme.
|
||||
type decodeBaseStyles struct {
|
||||
ClrScheme decodeColorScheme `xml:"clrScheme"`
|
||||
FontScheme decodeFontScheme `xml:"fontScheme"`
|
||||
FmtScheme decodeStyleMatrix `xml:"fmtScheme"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// decodeColorScheme defines the structure used to parse a set of colors for the
|
||||
// theme.
|
||||
type decodeColorScheme struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Dk1 decodeCTColor `xml:"dk1"`
|
||||
Lt1 decodeCTColor `xml:"lt1"`
|
||||
Dk2 decodeCTColor `xml:"dk2"`
|
||||
Lt2 decodeCTColor `xml:"lt2"`
|
||||
Accent1 decodeCTColor `xml:"accent1"`
|
||||
Accent2 decodeCTColor `xml:"accent2"`
|
||||
Accent3 decodeCTColor `xml:"accent3"`
|
||||
Accent4 decodeCTColor `xml:"accent4"`
|
||||
Accent5 decodeCTColor `xml:"accent5"`
|
||||
Accent6 decodeCTColor `xml:"accent6"`
|
||||
Hlink decodeCTColor `xml:"hlink"`
|
||||
FolHlink decodeCTColor `xml:"folHlink"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// decodeFontScheme defines the structure used to parse font scheme within the
|
||||
// theme.
|
||||
type decodeFontScheme struct {
|
||||
Name string `xml:"name,attr"`
|
||||
MajorFont decodeFontCollection `xml:"majorFont"`
|
||||
MinorFont decodeFontCollection `xml:"minorFont"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// decodeFontCollection defines the structure used to parse a major and minor
|
||||
// font which is used in the font scheme.
|
||||
type decodeFontCollection struct {
|
||||
Latin *xlsxCTTextFont `xml:"latin"`
|
||||
Ea *xlsxCTTextFont `xml:"ea"`
|
||||
Cs *xlsxCTTextFont `xml:"cs"`
|
||||
Font []xlsxCTSupplementalFont `xml:"font"`
|
||||
ExtLst *xlsxExtLst `xml:"extLst"`
|
||||
}
|
||||
|
||||
// decodeCTColor defines the structure used to parse the actual color values
|
||||
// that are to be applied to a given diagram and how those colors are to be
|
||||
// applied.
|
||||
type decodeCTColor struct {
|
||||
ScrgbClr *xlsxInnerXML `xml:"scrgbClr"`
|
||||
SrgbClr *attrValString `xml:"srgbClr"`
|
||||
HslClr *xlsxInnerXML `xml:"hslClr"`
|
||||
SysClr *xlsxSysClr `xml:"sysClr"`
|
||||
SchemeClr *xlsxInnerXML `xml:"schemeClr"`
|
||||
PrstClr *xlsxInnerXML `xml:"prstClr"`
|
||||
}
|
||||
|
||||
// decodeStyleMatrix defines the structure used to parse a set of formatting
|
||||
// options, which can be referenced by documents that apply a certain style to
|
||||
// a given part of an object.
|
||||
type decodeStyleMatrix struct {
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"`
|
||||
LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"`
|
||||
EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"`
|
||||
BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"`
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -16,9 +16,10 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// xlsxRelationships describe references from parts to other internal resources in the package or to external resources.
|
||||
// xlsxRelationships describe references from parts to other internal resources
|
||||
// in the package or to external resources.
|
||||
type xlsxRelationships struct {
|
||||
sync.Mutex
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
|
||||
Relationships []xlsxRelationship `xml:"Relationship"`
|
||||
}
|
||||
|
@ -150,7 +151,7 @@ type xlsxWorkBookView struct {
|
|||
YWindow string `xml:"yWindow,attr,omitempty"`
|
||||
WindowWidth int `xml:"windowWidth,attr,omitempty"`
|
||||
WindowHeight int `xml:"windowHeight,attr,omitempty"`
|
||||
TabRatio int `xml:"tabRatio,attr,omitempty"`
|
||||
TabRatio float64 `xml:"tabRatio,attr,omitempty"`
|
||||
FirstSheet int `xml:"firstSheet,attr,omitempty"`
|
||||
ActiveTab int `xml:"activeTab,attr,omitempty"`
|
||||
AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"`
|
||||
|
@ -212,12 +213,70 @@ type xlsxPivotCache struct {
|
|||
// document are specified in the markup specification and can be used to store
|
||||
// extensions to the markup specification, whether those are future version
|
||||
// extensions of the markup specification or are private extensions implemented
|
||||
// independently from the markup specification. Markup within an extension might
|
||||
// independently of the markup specification. Markup within an extension might
|
||||
// not be understood by a consumer.
|
||||
type xlsxExtLst struct {
|
||||
Ext string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxExt represents a the future feature data storage area. Each extension
|
||||
// within an extension list shall be contained within an ext element.
|
||||
// Extensions shall be versioned by namespace, using the uri attribute, and
|
||||
// shall be allowed to appear in any order within the extension list. Any
|
||||
// number of extensions shall be allowed within an extension list.
|
||||
type xlsxExt struct {
|
||||
XMLName xml.Name `xml:"ext"`
|
||||
URI string `xml:"uri,attr"`
|
||||
Content string `xml:",innerxml"`
|
||||
xmlns []xml.Attr
|
||||
}
|
||||
|
||||
// xlsxAlternateContent is a container for a sequence of multiple
|
||||
// representations of a given piece of content. The program reading the file
|
||||
// should only process one of these, and the one chosen should be based on
|
||||
// which conditions match.
|
||||
type xlsxAlternateContent struct {
|
||||
XMLNSMC string `xml:"xmlns:mc,attr,omitempty"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxChoice element shall be an element in the Markup Compatibility namespace
|
||||
// with local name "Choice". Parent elements of Choice elements shall be
|
||||
// AlternateContent elements.
|
||||
type xlsxChoice struct {
|
||||
XMLName xml.Name `xml:"mc:Choice"`
|
||||
XMLNSA14 string `xml:"xmlns:a14,attr,omitempty"`
|
||||
XMLNSSle15 string `xml:"xmlns:sle15,attr,omitempty"`
|
||||
Requires string `xml:"Requires,attr,omitempty"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxFallback element shall be an element in the Markup Compatibility
|
||||
// namespace with local name "Fallback". Parent elements of Fallback elements
|
||||
// shall be AlternateContent elements.
|
||||
type xlsxFallback struct {
|
||||
XMLName xml.Name `xml:"mc:Fallback"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxInnerXML holds parts of XML content currently not unmarshal.
|
||||
type xlsxInnerXML struct {
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// decodeExtLst defines the structure used to parse the extLst element
|
||||
// of the future feature data storage area.
|
||||
type decodeExtLst struct {
|
||||
XMLName xml.Name `xml:"extLst"`
|
||||
Ext []*xlsxExt `xml:"ext"`
|
||||
}
|
||||
|
||||
// decodeExt defines the structure used to parse the ext element.
|
||||
type decodeExt struct {
|
||||
URI string `xml:"uri,attr,omitempty"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxDefinedNames directly maps the definedNames element. This element defines
|
||||
// the collection of defined names for this workbook. Defined names are
|
||||
// descriptive names to represent cells, ranges of cells, formulas, or constant
|
||||
|
@ -229,7 +288,7 @@ type xlsxDefinedNames struct {
|
|||
// xlsxDefinedName directly maps the definedName element from the namespace
|
||||
// http://schemas.openxmlformats.org/spreadsheetml/2006/main This element
|
||||
// defines a defined name within this workbook. A defined name is descriptive
|
||||
// text that is used to represents a cell, range of cells, formula, or constant
|
||||
// text that is used to represent a cell, range of cells, formula, or constant
|
||||
// value. For a descriptions of the attributes see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname
|
||||
type xlsxDefinedName struct {
|
||||
Comment string `xml:"comment,attr,omitempty"`
|
||||
|
@ -309,7 +368,7 @@ type xlsxCustomWorkbookView struct {
|
|||
ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
|
||||
ShowStatusbar *bool `xml:"showStatusbar,attr"`
|
||||
ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
|
||||
TabRatio *int `xml:"tabRatio,attr"`
|
||||
TabRatio *float64 `xml:"tabRatio,attr"`
|
||||
WindowHeight *int `xml:"windowHeight,attr"`
|
||||
WindowWidth *int `xml:"windowWidth,attr"`
|
||||
XWindow *int `xml:"xWindow,attr"`
|
||||
|
|
120
xmlWorksheet.go
120
xmlWorksheet.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
|
||||
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
//
|
||||
|
@ -7,7 +7,7 @@
|
|||
// 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
|
||||
// data. This library needs Go version 1.16 or later.
|
||||
// data. This library needs Go version 1.18 or later.
|
||||
|
||||
package excelize
|
||||
|
||||
|
@ -19,7 +19,7 @@ import (
|
|||
// xlsxWorksheet directly maps the worksheet element in the namespace
|
||||
// http://schemas.openxmlformats.org/spreadsheetml/2006/main.
|
||||
type xlsxWorksheet struct {
|
||||
sync.Mutex
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
|
||||
SheetPr *xlsxSheetPr `xml:"sheetPr"`
|
||||
Dimension *xlsxDimension `xml:"dimension"`
|
||||
|
@ -81,8 +81,8 @@ type xlsxHeaderFooter struct {
|
|||
XMLName xml.Name `xml:"headerFooter"`
|
||||
DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"`
|
||||
DifferentFirst bool `xml:"differentFirst,attr,omitempty"`
|
||||
ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"`
|
||||
AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"`
|
||||
ScaleWithDoc *bool `xml:"scaleWithDoc,attr"`
|
||||
AlignWithMargins *bool `xml:"alignWithMargins,attr"`
|
||||
OddHeader string `xml:"oddHeader,omitempty"`
|
||||
OddFooter string `xml:"oddFooter,omitempty"`
|
||||
EvenHeader string `xml:"evenHeader,omitempty"`
|
||||
|
@ -241,7 +241,7 @@ type xlsxSheetPr struct {
|
|||
CodeName string `xml:"codeName,attr,omitempty"`
|
||||
FilterMode bool `xml:"filterMode,attr,omitempty"`
|
||||
EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"`
|
||||
TabColor *xlsxTabColor `xml:"tabColor"`
|
||||
TabColor *xlsxColor `xml:"tabColor"`
|
||||
OutlinePr *xlsxOutlinePr `xml:"outlinePr"`
|
||||
PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr"`
|
||||
}
|
||||
|
@ -261,15 +261,6 @@ type xlsxPageSetUpPr struct {
|
|||
FitToPage bool `xml:"fitToPage,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxTabColor represents background color of the sheet tab.
|
||||
type xlsxTabColor struct {
|
||||
Auto bool `xml:"auto,attr,omitempty"`
|
||||
Indexed int `xml:"indexed,attr,omitempty"`
|
||||
RGB string `xml:"rgb,attr,omitempty"`
|
||||
Theme int `xml:"theme,attr,omitempty"`
|
||||
Tint float64 `xml:"tint,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxCols defines column width and column formatting for one or more columns
|
||||
// of the worksheet.
|
||||
type xlsxCols struct {
|
||||
|
@ -433,12 +424,12 @@ type xlsxDataValidations struct {
|
|||
DisablePrompts bool `xml:"disablePrompts,attr,omitempty"`
|
||||
XWindow int `xml:"xWindow,attr,omitempty"`
|
||||
YWindow int `xml:"yWindow,attr,omitempty"`
|
||||
DataValidation []*DataValidation `xml:"dataValidation"`
|
||||
DataValidation []*xlsxDataValidation `xml:"dataValidation"`
|
||||
}
|
||||
|
||||
// DataValidation directly maps the a single item of data validation defined
|
||||
// DataValidation directly maps the single item of data validation defined
|
||||
// on a range of the worksheet.
|
||||
type DataValidation struct {
|
||||
type xlsxDataValidation struct {
|
||||
AllowBlank bool `xml:"allowBlank,attr"`
|
||||
Error *string `xml:"error,attr"`
|
||||
ErrorStyle *string `xml:"errorStyle,attr"`
|
||||
|
@ -450,9 +441,10 @@ type DataValidation struct {
|
|||
ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"`
|
||||
ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
|
||||
Sqref string `xml:"sqref,attr"`
|
||||
XMSqref string `xml:"sqref,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Formula1 string `xml:",innerxml"`
|
||||
Formula2 string `xml:",innerxml"`
|
||||
Formula1 *xlsxInnerXML `xml:"formula1"`
|
||||
Formula2 *xlsxInnerXML `xml:"formula2"`
|
||||
}
|
||||
|
||||
// xlsxC collection represents a cell in the worksheet. Information about the
|
||||
|
@ -486,6 +478,7 @@ type xlsxC struct {
|
|||
F *xlsxF `xml:"f"` // Formula
|
||||
V string `xml:"v,omitempty"` // Value
|
||||
IS *xlsxSI `xml:"is"`
|
||||
f string
|
||||
}
|
||||
|
||||
// xlsxF represents a formula for the cell. The formula expression is
|
||||
|
@ -708,33 +701,6 @@ type xlsxLegacyDrawingHF struct {
|
|||
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
|
||||
}
|
||||
|
||||
// xlsxAlternateContent is a container for a sequence of multiple
|
||||
// representations of a given piece of content. The program reading the file
|
||||
// should only process one of these, and the one chosen should be based on
|
||||
// which conditions match.
|
||||
type xlsxAlternateContent struct {
|
||||
XMLNSMC string `xml:"xmlns:mc,attr,omitempty"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxInnerXML holds parts of XML content currently not unmarshal.
|
||||
type xlsxInnerXML struct {
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// xlsxWorksheetExt directly maps the ext element in the worksheet.
|
||||
type xlsxWorksheetExt struct {
|
||||
XMLName xml.Name `xml:"ext"`
|
||||
URI string `xml:"uri,attr"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// decodeWorksheetExt directly maps the ext element.
|
||||
type decodeWorksheetExt struct {
|
||||
XMLName xml.Name `xml:"extLst"`
|
||||
Ext []*xlsxWorksheetExt `xml:"ext"`
|
||||
}
|
||||
|
||||
// decodeX14SparklineGroups directly maps the sparklineGroups element.
|
||||
type decodeX14SparklineGroups struct {
|
||||
XMLName xml.Name `xml:"sparklineGroups"`
|
||||
|
@ -742,8 +708,7 @@ type decodeX14SparklineGroups struct {
|
|||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// decodeX14ConditionalFormattingExt directly maps the ext
|
||||
// element.
|
||||
// decodeX14ConditionalFormattingExt directly maps the ext element.
|
||||
type decodeX14ConditionalFormattingExt struct {
|
||||
XMLName xml.Name `xml:"ext"`
|
||||
ID string `xml:"id"`
|
||||
|
@ -757,6 +722,14 @@ type decodeX14ConditionalFormattings struct {
|
|||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// decodeX14ConditionalFormattingRules directly maps the conditionalFormattings
|
||||
// element.
|
||||
type decodeX14ConditionalFormattingRules struct {
|
||||
XMLName xml.Name `xml:"conditionalFormattings"`
|
||||
XMLNSXM string `xml:"xmlns:xm,attr"`
|
||||
CondFmt []decodeX14ConditionalFormatting `xml:"conditionalFormatting"`
|
||||
}
|
||||
|
||||
// decodeX14ConditionalFormatting directly maps the conditionalFormatting
|
||||
// element.
|
||||
type decodeX14ConditionalFormatting struct {
|
||||
|
@ -778,7 +751,7 @@ type decodeX14DataBar struct {
|
|||
MaxLength int `xml:"maxLength,attr"`
|
||||
MinLength int `xml:"minLength,attr"`
|
||||
Border bool `xml:"border,attr,omitempty"`
|
||||
Gradient bool `xml:"gradient,attr"`
|
||||
Gradient *bool `xml:"gradient,attr"`
|
||||
ShowValue bool `xml:"showValue,attr,omitempty"`
|
||||
Direction string `xml:"direction,attr,omitempty"`
|
||||
Cfvo []*xlsxCfvo `xml:"cfvo"`
|
||||
|
@ -850,14 +823,14 @@ type xlsxX14SparklineGroup struct {
|
|||
MinAxisType string `xml:"minAxisType,attr,omitempty"`
|
||||
MaxAxisType string `xml:"maxAxisType,attr,omitempty"`
|
||||
RightToLeft bool `xml:"rightToLeft,attr,omitempty"`
|
||||
ColorSeries *xlsxTabColor `xml:"x14:colorSeries"`
|
||||
ColorNegative *xlsxTabColor `xml:"x14:colorNegative"`
|
||||
ColorSeries *xlsxColor `xml:"x14:colorSeries"`
|
||||
ColorNegative *xlsxColor `xml:"x14:colorNegative"`
|
||||
ColorAxis *xlsxColor `xml:"x14:colorAxis"`
|
||||
ColorMarkers *xlsxTabColor `xml:"x14:colorMarkers"`
|
||||
ColorFirst *xlsxTabColor `xml:"x14:colorFirst"`
|
||||
ColorLast *xlsxTabColor `xml:"x14:colorLast"`
|
||||
ColorHigh *xlsxTabColor `xml:"x14:colorHigh"`
|
||||
ColorLow *xlsxTabColor `xml:"x14:colorLow"`
|
||||
ColorMarkers *xlsxColor `xml:"x14:colorMarkers"`
|
||||
ColorFirst *xlsxColor `xml:"x14:colorFirst"`
|
||||
ColorLast *xlsxColor `xml:"x14:colorLast"`
|
||||
ColorHigh *xlsxColor `xml:"x14:colorHigh"`
|
||||
ColorLow *xlsxColor `xml:"x14:colorLow"`
|
||||
Sparklines xlsxX14Sparklines `xml:"x14:sparklines"`
|
||||
}
|
||||
|
||||
|
@ -872,6 +845,24 @@ type xlsxX14Sparkline struct {
|
|||
Sqref string `xml:"xm:sqref"`
|
||||
}
|
||||
|
||||
// DataValidation directly maps the settings of the data validation rule.
|
||||
type DataValidation struct {
|
||||
AllowBlank bool
|
||||
Error *string
|
||||
ErrorStyle *string
|
||||
ErrorTitle *string
|
||||
Operator string
|
||||
Prompt *string
|
||||
PromptTitle *string
|
||||
ShowDropDown bool
|
||||
ShowErrorMessage bool
|
||||
ShowInputMessage bool
|
||||
Sqref string
|
||||
Type string
|
||||
Formula1 string
|
||||
Formula2 string
|
||||
}
|
||||
|
||||
// SparklineOptions directly maps the settings of the sparkline.
|
||||
type SparklineOptions struct {
|
||||
Location []string
|
||||
|
@ -903,8 +894,8 @@ type SparklineOptions struct {
|
|||
EmptyCells string
|
||||
}
|
||||
|
||||
// PaneOptions directly maps the settings of the pane.
|
||||
type PaneOptions struct {
|
||||
// Selection directly maps the settings of the worksheet selection.
|
||||
type Selection struct {
|
||||
SQRef string
|
||||
ActiveCell string
|
||||
Pane string
|
||||
|
@ -918,7 +909,7 @@ type Panes struct {
|
|||
YSplit int
|
||||
TopLeftCell string
|
||||
ActivePane string
|
||||
Panes []PaneOptions
|
||||
Selection []Selection
|
||||
}
|
||||
|
||||
// ConditionalFormatOptions directly maps the conditional format settings of the cells.
|
||||
|
@ -926,7 +917,7 @@ type ConditionalFormatOptions struct {
|
|||
Type string
|
||||
AboveAverage bool
|
||||
Percent bool
|
||||
Format int
|
||||
Format *int
|
||||
Criteria string
|
||||
Value string
|
||||
MinType string
|
||||
|
@ -972,10 +963,10 @@ type SheetProtectionOptions struct {
|
|||
|
||||
// HeaderFooterOptions directly maps the settings of header and footer.
|
||||
type HeaderFooterOptions struct {
|
||||
AlignWithMargins bool
|
||||
AlignWithMargins *bool
|
||||
DifferentFirst bool
|
||||
DifferentOddEven bool
|
||||
ScaleWithDoc bool
|
||||
ScaleWithDoc *bool
|
||||
OddHeader string
|
||||
OddFooter string
|
||||
EvenHeader string
|
||||
|
@ -1015,6 +1006,9 @@ type PageLayoutOptions struct {
|
|||
FitToWidth *int
|
||||
// BlackAndWhite specified print black and white.
|
||||
BlackAndWhite *bool
|
||||
// PageOrder specifies the ordering of multiple pages. Values
|
||||
// accepted: overThenDown, downThenOver
|
||||
PageOrder *string
|
||||
}
|
||||
|
||||
// ViewOptions directly maps the settings of sheet view.
|
||||
|
|
Loading…
Reference in New Issue