forked from p30928647/excelize
Compare commits
155 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 |
|
@ -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@v4
|
||||
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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
890
adjust_test.go
890
adjust_test.go
|
@ -1,10 +1,14 @@
|
|||
package excelize
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "image/jpeg"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -19,7 +23,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
|
||||
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
|
||||
assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{
|
||||
MergeCells: &xlsxMergeCells{
|
||||
Cells: []*xlsxMergeCell{
|
||||
|
@ -28,7 +32,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
|
||||
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
|
||||
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
|
||||
MergeCells: &xlsxMergeCells{
|
||||
Cells: []*xlsxMergeCell{
|
||||
|
@ -37,7 +41,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}, rows, 1, -1))
|
||||
}, "Sheet1", rows, 1, -1, 1))
|
||||
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
|
||||
MergeCells: &xlsxMergeCells{
|
||||
Cells: []*xlsxMergeCell{
|
||||
|
@ -46,7 +50,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}, columns, 1, -1))
|
||||
}, "Sheet1", columns, 1, -1, 1))
|
||||
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
|
||||
MergeCells: &xlsxMergeCells{
|
||||
Cells: []*xlsxMergeCell{
|
||||
|
@ -55,7 +59,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}, columns, 1, -1))
|
||||
}, "Sheet1", columns, 1, -1, 1))
|
||||
|
||||
// Test adjust merge cells
|
||||
var cases []struct {
|
||||
|
@ -134,7 +138,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, 1))
|
||||
assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, 1, 1))
|
||||
assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label)
|
||||
assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label)
|
||||
}
|
||||
|
@ -223,7 +227,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1))
|
||||
assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1))
|
||||
assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label)
|
||||
}
|
||||
|
||||
|
@ -271,7 +275,7 @@ func TestAdjustMergeCells(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1))
|
||||
assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1))
|
||||
assert.Len(t, c.ws.MergeCells.Cells, 0, c.label)
|
||||
}
|
||||
|
||||
|
@ -291,18 +295,18 @@ func TestAdjustAutoFilter(t *testing.T) {
|
|||
AutoFilter: &xlsxAutoFilter{
|
||||
Ref: "A1:A3",
|
||||
},
|
||||
}, rows, 1, -1))
|
||||
}, "Sheet1", rows, 1, -1, 1))
|
||||
// Test adjustAutoFilter with illegal cell reference
|
||||
assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{
|
||||
AutoFilter: &xlsxAutoFilter{
|
||||
Ref: "A:B1",
|
||||
},
|
||||
}, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
|
||||
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
|
||||
assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{
|
||||
AutoFilter: &xlsxAutoFilter{
|
||||
Ref: "A1:B",
|
||||
},
|
||||
}, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
|
||||
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
|
||||
}
|
||||
|
||||
func TestAdjustTable(t *testing.T) {
|
||||
|
@ -331,10 +335,10 @@ func TestAdjustTable(t *testing.T) {
|
|||
assert.NoError(t, f.RemoveRow(sheetName, 1))
|
||||
// Test adjust table with unsupported charset
|
||||
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
|
||||
assert.NoError(t, f.RemoveRow(sheetName, 1))
|
||||
assert.EqualError(t, f.RemoveRow(sheetName, 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
// Test adjust table with invalid table range reference
|
||||
f.Pkg.Store("xl/tables/table1.xml", []byte(`<table ref="-" />`))
|
||||
assert.NoError(t, f.RemoveRow(sheetName, 1))
|
||||
assert.Equal(t, ErrParameterInvalid, f.RemoveRow(sheetName, 1))
|
||||
}
|
||||
|
||||
func TestAdjustHelper(t *testing.T) {
|
||||
|
@ -357,13 +361,18 @@ func TestAdjustHelper(t *testing.T) {
|
|||
func TestAdjustCalcChain(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.CalcChain = &xlsxCalcChain{
|
||||
C: []xlsxCalcChainC{
|
||||
{R: "B2", I: 2}, {R: "B2", I: 1},
|
||||
},
|
||||
C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}, {R: "A1", I: 1}},
|
||||
}
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
|
||||
f.CalcChain = &xlsxCalcChain{
|
||||
C: []xlsxCalcChainC{{R: "B2", I: 1}, {R: "B3"}, {R: "A1"}},
|
||||
}
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 3))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
|
||||
f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}}}
|
||||
f.CalcChain.C[1].R = "invalid coordinates"
|
||||
assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")))
|
||||
f.CalcChain = nil
|
||||
|
@ -444,16 +453,112 @@ func TestAdjustCols(t *testing.T) {
|
|||
assert.NoError(t, f.RemoveCol(sheetName, "A"))
|
||||
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetColWidth("Sheet1", "XFB", "XFC", 12))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 2))
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Min)
|
||||
assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Max)
|
||||
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 2))
|
||||
assert.Nil(t, ws.(*xlsxWorksheet).Cols)
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "(1-0.5)/2"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
formula, err := f.GetCellFormula("Sheet1", "B2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "(1-0.5)/2", formula)
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAdjustColDimensions(t *testing.T) {
|
||||
f := NewFile()
|
||||
ws, err := f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
|
||||
assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns))
|
||||
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
f.Sheet.Delete("xl/worksheets/sheet2.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.adjustColDimensions("Sheet2", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestAdjustRowDimensions(t *testing.T) {
|
||||
f := NewFile()
|
||||
ws, err := f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
|
||||
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows))
|
||||
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
f.Sheet.Delete("xl/worksheets/sheet2.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.adjustRowDimensions("Sheet1", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
f = NewFile()
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
ws, err = f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("Sheet2!A%d", TotalRows)))
|
||||
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet2", ws, 1, TotalRows))
|
||||
}
|
||||
|
||||
func TestAdjustHyperlinks(t *testing.T) {
|
||||
f := NewFile()
|
||||
ws, err := f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
|
||||
f.adjustHyperlinks(ws, "Sheet1", rows, 3, -1)
|
||||
|
||||
// Test adjust hyperlinks location with positive offset
|
||||
assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
link, target, err := f.GetCellHyperLink("Sheet1", "F6")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, link)
|
||||
assert.Equal(t, target, "Sheet1!A1")
|
||||
|
||||
// Test adjust hyperlinks location with negative offset
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 1))
|
||||
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, link)
|
||||
assert.Equal(t, target, "Sheet1!A1")
|
||||
|
||||
// Test adjust hyperlinks location on remove row
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 5))
|
||||
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, link)
|
||||
assert.Empty(t, target)
|
||||
|
||||
// Test adjust hyperlinks location on remove column
|
||||
assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "F"))
|
||||
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, link)
|
||||
assert.Empty(t, target)
|
||||
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustHyperlinks.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
}
|
||||
|
||||
func TestAdjustFormula(t *testing.T) {
|
||||
f := NewFile()
|
||||
formulaType, ref := STCellFormulaTypeShared, "C1:C5"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "B", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
for cell, expected := range map[string]string{"D2": "=A1+B1", "D3": "=A2+B2", "D11": "=A1+B1"} {
|
||||
for cell, expected := range map[string]string{"D2": "A2+C2", "D3": "A3+C3", "D11": "A11+C11"} {
|
||||
formula, err := f.GetCellFormula("Sheet1", cell)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, formula)
|
||||
|
@ -461,7 +566,750 @@ func TestAdjustFormula(t *testing.T) {
|
|||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
|
||||
assert.NoError(t, f.Close())
|
||||
|
||||
assert.NoError(t, f.adjustFormula(nil, rows, 0, false))
|
||||
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "-"}, rows, 0, false), ErrParameterInvalid)
|
||||
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "XFD1:XFD1"}, columns, 1, false), ErrColumnNumber)
|
||||
assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{}, rows, 0, 0, false))
|
||||
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "-"}}, rows, 0, 0, false))
|
||||
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "XFD1:XFD1"}}, columns, 0, 1, false))
|
||||
|
||||
_, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1)
|
||||
assert.Equal(t, ErrColumnNumber, err)
|
||||
_, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", false, columns, 0, 1)
|
||||
assert.Equal(t, ErrColumnNumber, err)
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "XFD1"))
|
||||
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
|
||||
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows)))
|
||||
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(1048576:1:2)"))
|
||||
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(XFD:A:B)"))
|
||||
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(A:B:XFD)"))
|
||||
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
|
||||
|
||||
// Test adjust formula with defined name in formula text
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Amount",
|
||||
RefersTo: "Sheet1!$B$2",
|
||||
}))
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "Amount+B3"))
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 1))
|
||||
formula, err := f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Amount+B2", formula)
|
||||
|
||||
// Test adjust formula with array formula
|
||||
f = NewFile()
|
||||
formulaType, reference := STCellFormulaTypeArray, "A3:A3"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A4")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A2:A3", formula)
|
||||
|
||||
// Test adjust formula on duplicate row with array formula
|
||||
f = NewFile()
|
||||
formulaType, reference = STCellFormulaTypeArray, "A3"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A4")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A2:A3", formula)
|
||||
|
||||
// Test adjust formula on duplicate row with relative and absolute cell references
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11&\" \""))
|
||||
assert.NoError(t, f.DuplicateRowTo("Sheet1", 10, 2))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A$2+$A3&\" \"", formula)
|
||||
|
||||
t.Run("for_cells_affected_directly", func(t *testing.T) {
|
||||
// Test insert row in middle of range with relative and absolute cell references
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "$A1+A$2"))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
formula, err := f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "$A1+A$3", formula)
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 2))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "$A1+A$2", formula)
|
||||
|
||||
// Test insert column in middle of range
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1+C1"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "B1+D1", formula)
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "B1+C1", formula)
|
||||
|
||||
// Test insert row and column in a rectangular range
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4+D5+E4+E5"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "D4+D6+F4+F6", formula)
|
||||
|
||||
// Test insert row in middle of range
|
||||
f = NewFile()
|
||||
formulaType, reference := STCellFormulaTypeArray, "B1:B1"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A1:A3", formula)
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 2))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A1:A2", formula)
|
||||
|
||||
// Test insert column in middle of range
|
||||
f = NewFile()
|
||||
formulaType, reference = STCellFormulaTypeArray, "A1:A1"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1:C1", FormulaOpts{Ref: &reference, Type: &formulaType}))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "B1:D1", formula)
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "B1:C1", formula)
|
||||
|
||||
// Test insert row and column in a rectangular range
|
||||
f = NewFile()
|
||||
formulaType, reference = STCellFormulaTypeArray, "A1:A1"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4:E5", FormulaOpts{Ref: &reference, Type: &formulaType}))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "D4:F6", formula)
|
||||
})
|
||||
t.Run("for_cells_affected_indirectly", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A3+A4"))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
formula, err := f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A4+A5", formula)
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 2))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "A3+A4", formula)
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "D3+D4"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "E3+E4", formula)
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "D3+D4", formula)
|
||||
})
|
||||
t.Run("for_entire_cols_rows_reference", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test adjust formula on insert row in the middle of the range
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2:A3:A4,,Table1[])"))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
|
||||
formula, err := f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(A2:A4:A5,,Table1[])", formula)
|
||||
|
||||
// Test adjust formula on insert at the top of the range
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(A3:A5:A6,,Table1[])", formula)
|
||||
|
||||
f = NewFile()
|
||||
// Test adjust formula on insert row in the middle of the range
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet 1'!A2,A3)"))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM('Sheet 1'!A2,A4)", formula)
|
||||
|
||||
// Test adjust formula on insert row at the top of the range
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM('Sheet 1'!A2,A5)", formula)
|
||||
|
||||
f = NewFile()
|
||||
// Test adjust formula on insert col in the middle of the range
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3:D3)"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(C3:E3)", formula)
|
||||
|
||||
// Test adjust formula on insert at the top of the range
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(D3:F3)", formula)
|
||||
|
||||
f = NewFile()
|
||||
// Test adjust formula on insert column in the middle of the range
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3,D3)"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(C3,E3)", formula)
|
||||
|
||||
// Test adjust formula on insert column at the top of the range
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(D3,F3)", formula)
|
||||
|
||||
f = NewFile()
|
||||
// Test adjust formula on insert row in the middle of the range (range of whole row)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(2:3)"))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(2:4)", formula)
|
||||
|
||||
// Test adjust formula on insert row at the top of the range (range of whole row)
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(3:5)", formula)
|
||||
|
||||
f = NewFile()
|
||||
// Test adjust formula on insert row in the middle of the range (range of whole column)
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C:D)"))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(C:E)", formula)
|
||||
|
||||
// Test adjust formula on insert row at the top of the range (range of whole column)
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
formula, err = f.GetCellFormula("Sheet1", "B1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SUM(D:F)", formula)
|
||||
})
|
||||
t.Run("for_all_worksheet_cells_with_rows_insert", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
// Tests formulas referencing Sheet2 should update but those referencing the original sheet should not
|
||||
tbl := [][]string{
|
||||
{"B1", "Sheet2!A1+Sheet2!A2", "Sheet2!A1+Sheet2!A3", "Sheet2!A2+Sheet2!A4"},
|
||||
{"C1", "A1+A2", "A1+A2", "A1+A2"},
|
||||
{"D1", "Sheet2!B1:B2", "Sheet2!B1:B3", "Sheet2!B2:B4"},
|
||||
{"E1", "B1:B2", "B1:B2", "B1:B2"},
|
||||
{"F1", "SUM(Sheet2!C1:C2)", "SUM(Sheet2!C1:C3)", "SUM(Sheet2!C2:C4)"},
|
||||
{"G1", "SUM(C1:C2)", "SUM(C1:C2)", "SUM(C1:C2)"},
|
||||
{"H1", "SUM(Sheet2!D1,Sheet2!D2)", "SUM(Sheet2!D1,Sheet2!D3)", "SUM(Sheet2!D2,Sheet2!D4)"},
|
||||
{"I1", "SUM(D1,D2)", "SUM(D1,D2)", "SUM(D1,D2)"},
|
||||
}
|
||||
for _, preset := range tbl {
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
|
||||
}
|
||||
// Test adjust formula on insert row in the middle of the range
|
||||
assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[2], formula)
|
||||
}
|
||||
|
||||
// Test adjust formula on insert row in the top of the range
|
||||
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[3], formula)
|
||||
}
|
||||
})
|
||||
t.Run("for_all_worksheet_cells_with_cols_insert", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
tbl := [][]string{
|
||||
{"A1", "Sheet2!A1+Sheet2!B1", "Sheet2!A1+Sheet2!C1", "Sheet2!B1+Sheet2!D1"},
|
||||
{"A2", "A1+B1", "A1+B1", "A1+B1"},
|
||||
{"A3", "Sheet2!A2:B2", "Sheet2!A2:C2", "Sheet2!B2:D2"},
|
||||
{"A4", "A2:B2", "A2:B2", "A2:B2"},
|
||||
{"A5", "SUM(Sheet2!A3:B3)", "SUM(Sheet2!A3:C3)", "SUM(Sheet2!B3:D3)"},
|
||||
{"A6", "SUM(A3:B3)", "SUM(A3:B3)", "SUM(A3:B3)"},
|
||||
{"A7", "SUM(Sheet2!A4,Sheet2!B4)", "SUM(Sheet2!A4,Sheet2!C4)", "SUM(Sheet2!B4,Sheet2!D4)"},
|
||||
{"A8", "SUM(A4,B4)", "SUM(A4,B4)", "SUM(A4,B4)"},
|
||||
}
|
||||
for _, preset := range tbl {
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
|
||||
}
|
||||
// Test adjust formula on insert column in the middle of the range
|
||||
assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[2], formula)
|
||||
}
|
||||
// Test adjust formula on insert column in the top of the range
|
||||
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[3], formula)
|
||||
}
|
||||
})
|
||||
t.Run("for_cross_sheet_ref_with_rows_insert)", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
_, err = f.NewSheet("Sheet3")
|
||||
assert.NoError(t, err)
|
||||
// Tests formulas referencing Sheet2 should update but those referencing
|
||||
// the original sheet or Sheet 3 should not update
|
||||
tbl := [][]string{
|
||||
{"B1", "Sheet2!A1+Sheet2!A2+Sheet1!A3+Sheet1!A4", "Sheet2!A1+Sheet2!A3+Sheet1!A3+Sheet1!A4", "Sheet2!A2+Sheet2!A4+Sheet1!A3+Sheet1!A4"},
|
||||
{"C1", "Sheet2!B1+Sheet2!B2+B3+B4", "Sheet2!B1+Sheet2!B3+B3+B4", "Sheet2!B2+Sheet2!B4+B3+B4"},
|
||||
{"D1", "Sheet2!C1+Sheet2!C2+Sheet3!A3+Sheet3!A4", "Sheet2!C1+Sheet2!C3+Sheet3!A3+Sheet3!A4", "Sheet2!C2+Sheet2!C4+Sheet3!A3+Sheet3!A4"},
|
||||
{"E1", "SUM(Sheet2!D1:D2,Sheet1!A3:A4)", "SUM(Sheet2!D1:D3,Sheet1!A3:A4)", "SUM(Sheet2!D2:D4,Sheet1!A3:A4)"},
|
||||
{"F1", "SUM(Sheet2!E1:E2,A3:A4)", "SUM(Sheet2!E1:E3,A3:A4)", "SUM(Sheet2!E2:E4,A3:A4)"},
|
||||
{"G1", "SUM(Sheet2!F1:F2,Sheet3!A3:A4)", "SUM(Sheet2!F1:F3,Sheet3!A3:A4)", "SUM(Sheet2!F2:F4,Sheet3!A3:A4)"},
|
||||
}
|
||||
for _, preset := range tbl {
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
|
||||
}
|
||||
// Test adjust formula on insert row in the middle of the range
|
||||
assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[2], formula)
|
||||
}
|
||||
// Test adjust formula on insert row in the top of the range
|
||||
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[3], formula)
|
||||
}
|
||||
})
|
||||
t.Run("for_cross_sheet_ref_with_cols_insert)", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
_, err = f.NewSheet("Sheet3")
|
||||
assert.NoError(t, err)
|
||||
// Tests formulas referencing Sheet2 should update but those referencing
|
||||
// the original sheet or Sheet 3 should not update
|
||||
tbl := [][]string{
|
||||
{"A1", "Sheet2!A1+Sheet2!B1+Sheet1!C1+Sheet1!D1", "Sheet2!A1+Sheet2!C1+Sheet1!C1+Sheet1!D1", "Sheet2!B1+Sheet2!D1+Sheet1!C1+Sheet1!D1"},
|
||||
{"A2", "Sheet2!A2+Sheet2!B2+C2+D2", "Sheet2!A2+Sheet2!C2+C2+D2", "Sheet2!B2+Sheet2!D2+C2+D2"},
|
||||
{"A3", "Sheet2!A3+Sheet2!B3+Sheet3!C3+Sheet3!D3", "Sheet2!A3+Sheet2!C3+Sheet3!C3+Sheet3!D3", "Sheet2!B3+Sheet2!D3+Sheet3!C3+Sheet3!D3"},
|
||||
{"A4", "SUM(Sheet2!A4:B4,Sheet1!C4:D4)", "SUM(Sheet2!A4:C4,Sheet1!C4:D4)", "SUM(Sheet2!B4:D4,Sheet1!C4:D4)"},
|
||||
{"A5", "SUM(Sheet2!A5:B5,C5:D5)", "SUM(Sheet2!A5:C5,C5:D5)", "SUM(Sheet2!B5:D5,C5:D5)"},
|
||||
{"A6", "SUM(Sheet2!A6:B6,Sheet3!C6:D6)", "SUM(Sheet2!A6:C6,Sheet3!C6:D6)", "SUM(Sheet2!B6:D6,Sheet3!C6:D6)"},
|
||||
}
|
||||
for _, preset := range tbl {
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
|
||||
}
|
||||
// Test adjust formula on insert row in the middle of the range
|
||||
assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[2], formula)
|
||||
}
|
||||
// Test adjust formula on insert row in the top of the range
|
||||
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
|
||||
for _, preset := range tbl {
|
||||
formula, err := f.GetCellFormula("Sheet1", preset[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, preset[3], formula)
|
||||
}
|
||||
})
|
||||
t.Run("for_cross_sheet_ref_with_chart_sheet)", func(t *testing.T) {
|
||||
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
})
|
||||
t.Run("for_array_formula_cell", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
|
||||
assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
|
||||
formulaType, ref := STCellFormulaTypeArray, "C1:C2"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
result, err := f.CalcCellValue("Sheet1", "D2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2", result)
|
||||
result, err = f.CalcCellValue("Sheet1", "D3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "12", result)
|
||||
|
||||
// Test adjust array formula with invalid range reference
|
||||
formulaType, ref = STCellFormulaTypeArray, "E1:E2"
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "XFD1:XFD1", FormulaOpts{Ref: &ref, Type: &formulaType}))
|
||||
assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "the column number must be greater than or equal to 1 and less than or equal to 16384")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdjustVolatileDeps(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`<volTypes xmlns="%s"><volType><main><tp><tr r="C2" s="2"/><tr r="C2" s="1"/><tr r="D3" s="1"/></tp></main></volType></volTypes>`, NameSpaceSpreadSheet.Value)))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
assert.Equal(t, "D3", f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr[1].R)
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "D"))
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 4))
|
||||
assert.Len(t, f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr, 1)
|
||||
|
||||
f = NewFile()
|
||||
f.Pkg.Store(defaultXMLPathVolatileDeps, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.InsertRows("Sheet1", 2, 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
f = NewFile()
|
||||
f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`<volTypes xmlns="%s"><volType><main><tp><tr r="A" s="1"/></tp></main></volType></volTypes>`, NameSpaceSpreadSheet.Value)))
|
||||
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1))
|
||||
f.volatileDepsWriter()
|
||||
}
|
||||
|
||||
func TestAdjustConditionalFormats(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.SetSheetRow("Sheet1", "B1", &[]interface{}{1, nil, 1, 1}))
|
||||
formatID, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "09600B"}, Fill: Fill{Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1}})
|
||||
assert.NoError(t, err)
|
||||
format := []ConditionalFormatOptions{
|
||||
{
|
||||
Type: "cell",
|
||||
Criteria: "greater than",
|
||||
Format: &formatID,
|
||||
Value: "0",
|
||||
},
|
||||
}
|
||||
for _, ref := range []string{"B1", "D1:E1"} {
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, format))
|
||||
}
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
opts, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, format, 1)
|
||||
assert.Equal(t, format, opts["C1:D1"])
|
||||
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "-"
|
||||
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
|
||||
|
||||
ws.(*xlsxWorksheet).ConditionalFormatting[0] = nil
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
|
||||
t.Run("for_remove_conditional_formats_column", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
format := []ConditionalFormatOptions{{
|
||||
Type: "data_bar",
|
||||
Criteria: "=",
|
||||
MinType: "min",
|
||||
MaxType: "max",
|
||||
BarColor: "#638EC6",
|
||||
}}
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format))
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "D"))
|
||||
opts, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, opts, 0)
|
||||
})
|
||||
t.Run("for_remove_conditional_formats_row", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
format := []ConditionalFormatOptions{{
|
||||
Type: "data_bar",
|
||||
Criteria: "=",
|
||||
MinType: "min",
|
||||
MaxType: "max",
|
||||
BarColor: "#638EC6",
|
||||
}}
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:E2", format))
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "F2", format))
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 2))
|
||||
opts, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, opts, 0)
|
||||
})
|
||||
t.Run("for_adjust_conditional_formats_row", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
format := []ConditionalFormatOptions{{
|
||||
Type: "data_bar",
|
||||
Criteria: "=",
|
||||
MinType: "min",
|
||||
MaxType: "max",
|
||||
BarColor: "#638EC6",
|
||||
}}
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format))
|
||||
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format))
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 1))
|
||||
opts, err := f.GetConditionalFormats("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, opts, 2)
|
||||
assert.Equal(t, format, opts["D1:D2"])
|
||||
assert.Equal(t, format, opts["D4:D4"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdjustDataValidations(t *testing.T) {
|
||||
f := NewFile()
|
||||
dv := NewDataValidation(true)
|
||||
dv.Sqref = "B1"
|
||||
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
dvs, err := f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, dvs, 0)
|
||||
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "F2", 1))
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "F3", 2))
|
||||
dv = NewDataValidation(true)
|
||||
dv.Sqref = "C2:D3"
|
||||
dv.SetSqrefDropList("$F$2:$F$3")
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
|
||||
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
|
||||
_, err = f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetSheetRow("Sheet2", "C1", &[]interface{}{1, 10}))
|
||||
dv = NewDataValidation(true)
|
||||
dv.Sqref = "C5:D6"
|
||||
assert.NoError(t, dv.SetRange("Sheet2!C1", "Sheet2!D1", DataValidationTypeWhole, DataValidationOperatorBetween))
|
||||
dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
assert.NoError(t, f.RemoveCol("Sheet2", "B"))
|
||||
dvs, err = f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "B2:C3", dvs[0].Sqref)
|
||||
assert.Equal(t, "$E$2:$E$3", dvs[0].Formula1)
|
||||
assert.Equal(t, "B5:C6", dvs[1].Sqref)
|
||||
assert.Equal(t, "Sheet2!B1", dvs[1].Formula1)
|
||||
assert.Equal(t, "Sheet2!C1", dvs[1].Formula2)
|
||||
|
||||
dv = NewDataValidation(true)
|
||||
dv.Sqref = "C8:D10"
|
||||
assert.NoError(t, dv.SetDropList([]string{`A<`, `B>`, `C"`, "D\t", `E'`, `F`}))
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
dvs, err = f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1)
|
||||
|
||||
// Test adjust data validation with multiple cell range
|
||||
dv = NewDataValidation(true)
|
||||
dv.Sqref = "G1:G3 H1:H3 A3:A1048576"
|
||||
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
|
||||
dvs, err = f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "G1:G4 H1:H4 A4:A1048576", dvs[3].Sqref)
|
||||
|
||||
dv = NewDataValidation(true)
|
||||
dv.Sqref = "C5:D6"
|
||||
assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween))
|
||||
dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
|
||||
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
|
||||
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "-"
|
||||
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
|
||||
|
||||
ws.(*xlsxWorksheet).DataValidations.DataValidation[0] = nil
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
|
||||
ws.(*xlsxWorksheet).DataValidations = nil
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
|
||||
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
t.Run("for_escaped_data_validation_rules_formula", func(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
dv := NewDataValidation(true)
|
||||
dv.Sqref = "A1"
|
||||
assert.NoError(t, dv.SetDropList([]string{"option1", strings.Repeat("\"", 4)}))
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
|
||||
// The double quote symbol in none formula data validation rules will be escaped in the Kingsoft WPS Office
|
||||
formula := strings.ReplaceAll(fmt.Sprintf("\"option1, %s", strings.Repeat("\"", 9)), "\"", """)
|
||||
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Formula1.Content = formula
|
||||
assert.NoError(t, f.RemoveCol("Sheet2", "A"))
|
||||
dvs, err := f.GetDataValidations("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, formula, dvs[0].Formula1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdjustDrawings(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test add pictures to sheet with positioning
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "B2", filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "B11", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "oneCell"}))
|
||||
assert.NoError(t, f.AddPicture("Sheet1", "B21", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "absolute"}))
|
||||
|
||||
// Test adjust pictures on inserting columns and rows
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 15, 1))
|
||||
cells, err := f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"D3", "D13", "B21"}, cells)
|
||||
wb := filepath.Join("test", "TestAdjustDrawings.xlsx")
|
||||
assert.NoError(t, f.SaveAs(wb))
|
||||
|
||||
// Test adjust pictures on deleting columns and rows
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "A"))
|
||||
assert.NoError(t, f.RemoveRow("Sheet1", 1))
|
||||
cells, err = f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"C2", "C12", "B21"}, cells)
|
||||
|
||||
// Test adjust existing pictures on inserting columns and rows
|
||||
f, err = OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 16, 1))
|
||||
cells, err = f.GetPictureCells("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"F4", "F15", "B21"}, cells)
|
||||
|
||||
// Test adjust drawings with unsupported charset
|
||||
f, err = OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
errors := []error{ErrColumnNumber, ErrColumnNumber, ErrMaxRows, ErrMaxRows}
|
||||
cells = []string{"XFD1", "XFB1"}
|
||||
for i, cell := range cells {
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1))
|
||||
assert.NoError(t, f.SaveAs(wb))
|
||||
f, err = OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1))
|
||||
}
|
||||
errors = []error{ErrMaxRows, ErrMaxRows}
|
||||
cells = []string{"A1048576", "A1048570"}
|
||||
for i, cell := range cells {
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil))
|
||||
assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1))
|
||||
assert.NoError(t, f.SaveAs(wb))
|
||||
f, err = OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1))
|
||||
}
|
||||
|
||||
a := xdrCellAnchor{}
|
||||
assert.NoError(t, a.adjustDrawings(columns, 0, 0))
|
||||
p := xlsxCellAnchorPos{}
|
||||
assert.NoError(t, p.adjustDrawings(columns, 0, 0, ""))
|
||||
|
||||
f, err = OpenFile(wb)
|
||||
assert.NoError(t, err)
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+`<wsDr xmlns="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"><twoCellAnchor><from><col>0</col><colOff>0</colOff><row>0</row><rowOff>0</rowOff></from><to><col>1</col><colOff>0</colOff><row>1</row><rowOff>0</rowOff></to><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"></mc:AlternateContent><clientData/></twoCellAnchor></wsDr>`))
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
}
|
||||
|
||||
func TestAdjustDefinedNames(t *testing.T) {
|
||||
f := NewFile()
|
||||
_, err := f.NewSheet("Sheet2")
|
||||
assert.NoError(t, err)
|
||||
for _, dn := range []*DefinedName{
|
||||
{Name: "Name1", RefersTo: "Sheet1!$XFD$1"},
|
||||
{Name: "Name2", RefersTo: "Sheet2!$C$1", Scope: "Sheet1"},
|
||||
{Name: "Name3", RefersTo: "Sheet2!$C$1:$D$2", Scope: "Sheet1"},
|
||||
{Name: "Name4", RefersTo: "Sheet2!$C1:D$2"},
|
||||
{Name: "Name5", RefersTo: "Sheet2!C$1:$D2"},
|
||||
{Name: "Name6", RefersTo: "Sheet2!C:$D"},
|
||||
{Name: "Name7", RefersTo: "Sheet2!$C:D"},
|
||||
{Name: "Name8", RefersTo: "Sheet2!C:D"},
|
||||
{Name: "Name9", RefersTo: "Sheet2!$C:$D"},
|
||||
{Name: "Name10", RefersTo: "Sheet2!1:2"},
|
||||
} {
|
||||
assert.NoError(t, f.SetDefinedName(dn))
|
||||
}
|
||||
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
|
||||
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
|
||||
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
|
||||
definedNames := f.GetDefinedName()
|
||||
for i, expected := range []string{
|
||||
"Sheet1!$XFD$2",
|
||||
"Sheet2!$D$2",
|
||||
"Sheet2!$D$2:$E$3",
|
||||
"Sheet2!$D1:D$3",
|
||||
"Sheet2!C$2:$E2",
|
||||
"Sheet2!C:$E",
|
||||
"Sheet2!$D:D",
|
||||
"Sheet2!C:D",
|
||||
"Sheet2!$D:$E",
|
||||
"Sheet2!1:2",
|
||||
} {
|
||||
assert.Equal(t, expected, definedNames[i].RefersTo)
|
||||
}
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Name1",
|
||||
RefersTo: "Sheet1!$A$1",
|
||||
Scope: "Sheet1",
|
||||
}))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "A"))
|
||||
definedNames = f.GetDefinedName()
|
||||
assert.Equal(t, "Sheet1!$A$1", definedNames[0].RefersTo)
|
||||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "Name1",
|
||||
RefersTo: "'1.A & B C'!#REF!",
|
||||
Scope: "Sheet1",
|
||||
}))
|
||||
assert.NoError(t, f.RemoveCol("Sheet1", "A"))
|
||||
definedNames = f.GetDefinedName()
|
||||
assert.Equal(t, "'1.A & B C'!#REF!", definedNames[0].RefersTo)
|
||||
|
||||
f = NewFile()
|
||||
f.WorkBook = nil
|
||||
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.adjustDefinedNames(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
|
777
calc_test.go
777
calc_test.go
File diff suppressed because it is too large
Load Diff
40
calcchain.go
40
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
|
||||
|
||||
|
@ -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:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
257
cell.go
257
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,6 +15,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
@ -72,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
|
||||
})
|
||||
}
|
||||
|
@ -143,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:
|
||||
|
@ -216,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
|
||||
}
|
||||
|
@ -255,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
|
||||
}
|
||||
|
@ -307,13 +308,41 @@ func (f *File) SetCellInt(sheet, cell string, value int) error {
|
|||
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 {
|
||||
|
@ -336,8 +365,8 @@ func (f *File) SetCellBool(sheet, cell string, value bool) error {
|
|||
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 {
|
||||
|
@ -357,6 +386,9 @@ 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 {
|
||||
|
@ -371,17 +403,20 @@ func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSiz
|
|||
return err
|
||||
}
|
||||
c.S = ws.prepareCellStyle(col, row, c.S)
|
||||
c.T, c.V = setCellFloat(value, precision, bitSize)
|
||||
c.IS = nil
|
||||
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.
|
||||
|
@ -407,8 +442,7 @@ 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 utf8.RuneCountInString(value) > TotalCellChars {
|
||||
value = string([]rune(value)[:TotalCellChars])
|
||||
|
@ -462,11 +496,11 @@ func (f *File) setSharedString(val string) (int, error) {
|
|||
}
|
||||
sst.mu.Lock()
|
||||
defer sst.mu.Unlock()
|
||||
sst.Count++
|
||||
sst.UniqueCount++
|
||||
t := xlsxT{Val: 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
|
||||
}
|
||||
|
@ -478,7 +512,9 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) {
|
|||
}
|
||||
if escape {
|
||||
var buf bytes.Buffer
|
||||
_ = xml.EscapeText(&buf, []byte(value))
|
||||
enc := xml.NewEncoder(&buf)
|
||||
_ = enc.EncodeToken(xml.CharData(value))
|
||||
enc.Flush()
|
||||
value = buf.String()
|
||||
}
|
||||
if len(value) > 0 {
|
||||
|
@ -520,7 +556,7 @@ func (c *xlsxC) setStr(val string) {
|
|||
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" {
|
||||
|
@ -565,7 +601,7 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) {
|
|||
c.V = strconv.FormatFloat(excelTime, 'G', 15, 64)
|
||||
}
|
||||
}
|
||||
return f.formattedValue(c, raw, CellTypeBool)
|
||||
return f.formattedValue(c, raw, CellTypeDate)
|
||||
}
|
||||
|
||||
// getValueFrom return a value from a column/row cell, this function is
|
||||
|
@ -590,6 +626,8 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
|
|||
}
|
||||
}
|
||||
return f.formattedValue(c, raw, CellTypeSharedString)
|
||||
case "str":
|
||||
return c.V, nil
|
||||
case "inlineStr":
|
||||
if c.IS != nil {
|
||||
return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString)
|
||||
|
@ -631,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
|
||||
}
|
||||
|
@ -756,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
|
||||
|
@ -770,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)
|
||||
|
@ -845,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",
|
||||
|
@ -920,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 {
|
||||
|
@ -942,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,
|
||||
|
@ -965,8 +1111,15 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro
|
|||
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()
|
||||
|
@ -1288,10 +1441,15 @@ func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) {
|
|||
// 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
|
||||
}
|
||||
f.mu.Unlock()
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
cell, err = ws.mergeCellsParser(cell)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -1300,10 +1458,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
lastRowNum := 0
|
||||
if l := len(ws.SheetData.Row); l > 0 {
|
||||
lastRowNum = ws.SheetData.Row[l-1].R
|
||||
|
@ -1365,18 +1519,29 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er
|
|||
if wb != nil && wb.WorkbookPr != nil {
|
||||
date1904 = wb.WorkbookPr.Date1904
|
||||
}
|
||||
if fmtCode, ok := styleSheet.getCustomNumFmtCode(numFmtID); ok {
|
||||
return format(c.V, fmtCode, date1904, cellType, f.options), err
|
||||
}
|
||||
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
|
||||
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
|
||||
}
|
||||
if styleSheet.NumFmts == nil {
|
||||
return c.V, err
|
||||
}
|
||||
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
|
||||
|
||||
// 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(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err
|
||||
if xlsxFmt.FormatCode16 != "" {
|
||||
return xlsxFmt.FormatCode16, true
|
||||
}
|
||||
return xlsxFmt.FormatCode, true
|
||||
}
|
||||
}
|
||||
return c.V, err
|
||||
return "", false
|
||||
}
|
||||
|
||||
// prepareCellStyle provides a function to prepare style index of cell in
|
||||
|
@ -1523,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)
|
||||
|
|
224
cell_test.go
224
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{
|
||||
|
@ -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,9 +187,29 @@ 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) {
|
||||
|
@ -211,8 +247,8 @@ func TestSetCellValuesMultiByte(t *testing.T) {
|
|||
|
||||
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)
|
||||
|
@ -230,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)
|
||||
|
@ -239,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) {
|
||||
|
@ -248,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))
|
||||
|
@ -261,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) {
|
||||
|
@ -292,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)
|
||||
|
@ -308,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)
|
||||
|
@ -351,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>
|
||||
|
@ -389,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",
|
||||
|
@ -438,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) {
|
||||
|
@ -451,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) {
|
||||
|
@ -480,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))
|
||||
|
@ -508,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() {
|
||||
|
@ -566,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())
|
||||
|
@ -601,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
|
||||
|
@ -613,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) {
|
||||
|
@ -654,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
|
||||
|
@ -696,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) {
|
||||
|
@ -795,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())
|
||||
|
@ -927,6 +1075,16 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) {
|
|||
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)
|
||||
|
|
135
chart.go
135
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,6 +850,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// ReverseOrder
|
||||
// Maximum
|
||||
// Minimum
|
||||
// Alignment
|
||||
// Font
|
||||
// NumFmt
|
||||
// Title
|
||||
|
@ -807,6 +865,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// ReverseOrder
|
||||
// Maximum
|
||||
// Minimum
|
||||
// Alignment
|
||||
// Font
|
||||
// LogBase
|
||||
// NumFmt
|
||||
|
@ -839,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:
|
||||
//
|
||||
|
@ -863,6 +940,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
|
|||
// Set chart size by 'Dimension' property. The 'Dimension' property is optional.
|
||||
// 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
|
||||
// data Sheet1!$E$1:$L$15:
|
||||
|
@ -895,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",
|
||||
|
@ -912,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",
|
||||
|
@ -927,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",
|
||||
|
@ -1097,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
|
||||
|
|
204
chart_test.go
204
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()
|
||||
}
|
||||
|
@ -120,13 +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")
|
||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||
_, 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),
|
||||
}}})
|
||||
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")
|
||||
}
|
||||
|
||||
func TestAddChart(t *testing.T) {
|
||||
|
@ -156,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{
|
||||
{
|
||||
|
@ -174,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),
|
||||
|
@ -201,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"}, 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: 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, Secondary: 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))
|
||||
}
|
||||
|
@ -280,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}, YAxis: ChartAxis{Secondary: 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) {
|
||||
|
@ -323,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() {
|
||||
|
@ -338,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())
|
||||
|
||||
|
@ -351,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) {
|
||||
|
@ -369,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),
|
||||
|
@ -386,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
|
||||
|
@ -401,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{
|
||||
|
@ -435,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)
|
||||
|
||||
|
|
52
col.go
52
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
|
||||
}
|
||||
|
@ -290,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
|
||||
}
|
||||
|
@ -301,8 +301,8 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error {
|
|||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
colData := xlsxCol{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Min: minVal,
|
||||
Max: maxVal,
|
||||
Width: float64Ptr(defaultColWidth),
|
||||
Hidden: !visible,
|
||||
CustomWidth: true,
|
||||
|
@ -354,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
|
||||
}
|
||||
|
@ -427,7 +427,7 @@ 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
|
||||
}
|
||||
|
@ -453,10 +453,14 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
|
|||
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
|
||||
|
@ -470,7 +474,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
|
|||
})
|
||||
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)
|
||||
|
@ -484,7 +488,7 @@ 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
|
||||
}
|
||||
|
@ -501,8 +505,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error
|
|||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
col := xlsxCol{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Min: minVal,
|
||||
Max: maxVal,
|
||||
Width: float64Ptr(width),
|
||||
CustomWidth: true,
|
||||
}
|
||||
|
@ -529,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)
|
||||
}
|
||||
|
@ -547,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)
|
||||
}
|
||||
|
|
33
col_test.go
33
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) {
|
||||
|
|
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,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
|
||||
|
||||
|
@ -39,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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
32
date.go
32
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
691
drawing.go
691
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, "")
|
||||
}
|
||||
|
|
485
errors.go
485
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,18 +228,96 @@ 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)
|
||||
}
|
||||
|
||||
// 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 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
|
||||
// type are unsupported.
|
||||
func newUnsupportedChartType(chartType ChartType) error {
|
||||
|
@ -58,201 +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)
|
||||
}
|
||||
|
||||
// newUnknownFilterTokenError defined the error message on receiving a unknown
|
||||
// filter operator token.
|
||||
func newUnknownFilterTokenError(token string) error {
|
||||
return fmt.Errorf("unknown operator: %s", token)
|
||||
}
|
||||
|
||||
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 = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength)
|
||||
// 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)
|
||||
// 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)
|
||||
// ErrExistsTableName defined the error message on given table already exists.
|
||||
ErrExistsTableName = errors.New("the same name table already exists")
|
||||
// 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")
|
||||
)
|
||||
|
|
225
excelize.go
225
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
|
||||
|
@ -29,31 +29,34 @@ import (
|
|||
// File define a populated spreadsheet file struct.
|
||||
type File struct {
|
||||
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
|
||||
|
@ -133,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),
|
||||
|
@ -177,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
|
||||
}
|
||||
|
@ -216,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
|
||||
}
|
||||
|
@ -225,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.
|
||||
|
@ -239,15 +242,18 @@ 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
|
||||
|
@ -261,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 {
|
||||
|
@ -275,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 {
|
||||
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
|
||||
|
@ -301,54 +308,65 @@ 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 (ws *xlsxWorksheet) checkSheet() {
|
||||
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:]
|
||||
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)
|
||||
}
|
||||
ws.checkSheetR0(&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 (ws *xlsxWorksheet) checkSheetR0(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{})
|
||||
|
@ -356,6 +374,19 @@ func (ws *xlsxWorksheet) checkSheetR0(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
|
||||
|
@ -425,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>
|
||||
|
@ -441,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>
|
||||
|
@ -561,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
|
||||
}
|
||||
|
|
106
excelize_test.go
106
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)
|
||||
|
||||
|
@ -399,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())
|
||||
|
@ -430,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) {
|
||||
|
@ -741,10 +778,10 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
|
|||
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", "37,947.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", "00:00.0", "37947.7500001", "37947.7500001"},
|
||||
{"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.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", "-37947.7500001", "-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", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "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", "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", "24:00.0", "2.1", "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"},
|
||||
}
|
||||
|
||||
|
@ -833,11 +870,17 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSetCellStyleLangNumberFormat(t *testing.T) {
|
||||
rawCellValues := [][]string{{"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}
|
||||
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)
|
||||
|
@ -849,7 +892,10 @@ func TestSetCellStyleLangNumberFormat(t *testing.T) {
|
|||
// 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)
|
||||
|
@ -961,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")))
|
||||
})
|
||||
|
||||
|
@ -1087,7 +1133,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "cell",
|
||||
Criteria: "between",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
MinValue: "6",
|
||||
MaxValue: "8",
|
||||
},
|
||||
|
@ -1099,7 +1145,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "cell",
|
||||
Criteria: ">",
|
||||
Format: format3,
|
||||
Format: &format3,
|
||||
Value: "6",
|
||||
},
|
||||
},
|
||||
|
@ -1110,7 +1156,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "top",
|
||||
Criteria: "=",
|
||||
Format: format3,
|
||||
Format: &format3,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1120,7 +1166,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "unique",
|
||||
Criteria: "=",
|
||||
Format: format2,
|
||||
Format: &format2,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1130,7 +1176,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "duplicate",
|
||||
Criteria: "=",
|
||||
Format: format2,
|
||||
Format: &format2,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1140,7 +1186,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "top",
|
||||
Criteria: "=",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
Value: "6",
|
||||
Percent: true,
|
||||
},
|
||||
|
@ -1152,7 +1198,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "average",
|
||||
Criteria: "=",
|
||||
Format: format3,
|
||||
Format: &format3,
|
||||
AboveAverage: true,
|
||||
},
|
||||
},
|
||||
|
@ -1163,7 +1209,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "average",
|
||||
Criteria: "=",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
AboveAverage: false,
|
||||
},
|
||||
},
|
||||
|
@ -1186,7 +1232,7 @@ func TestConditionalFormat(t *testing.T) {
|
|||
{
|
||||
Type: "formula",
|
||||
Criteria: "L2<3",
|
||||
Format: format1,
|
||||
Format: &format1,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -1196,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: "",
|
||||
|
@ -1222,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",
|
||||
|
@ -1236,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"))
|
||||
|
@ -1531,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)
|
||||
}
|
||||
|
@ -1607,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 {
|
||||
|
|
53
file.go
53
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"
|
||||
)
|
||||
|
@ -49,7 +50,7 @@ func NewFile(opts ...Options) *File {
|
|||
ws, _ := f.workSheetReader("Sheet1")
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
|
||||
f.Theme, _ = f.themeReader()
|
||||
f.options = getOptions(opts...)
|
||||
f.options = f.getOptions(opts...)
|
||||
return f
|
||||
}
|
||||
|
||||
|
@ -176,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()
|
||||
|
@ -191,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-20230422071738-01f4e37c47e9
|
||||
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/image v0.5.0
|
||||
golang.org/x/net v0.10.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-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
|
||||
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw=
|
||||
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/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.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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.8.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
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=
|
||||
|
|
16
hsl.go
16
hsl.go
|
@ -65,21 +65,21 @@ 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 {
|
||||
|
|
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
|
||||
|
|
38
lib_test.go
38
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) {
|
||||
|
|
45
merge.go
45
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,16 +49,16 @@ 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 {
|
||||
|
@ -66,7 +66,18 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
|
|||
}
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
ref := hCell + ":" + vCell
|
||||
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.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
rect1, err := rangeRefToCoordinates(hCell + ":" + vCell)
|
||||
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]
|
||||
}
|
||||
|
|
|
@ -80,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) {
|
||||
|
|
3122
numfmt_test.go
3122
numfmt_test.go
File diff suppressed because it is too large
Load Diff
725
picture.go
725
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,12 +232,15 @@ 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 {
|
||||
|
@ -226,7 +255,18 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
|
|||
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" {
|
||||
|
@ -246,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.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:]...)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
@ -278,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) {
|
||||
|
@ -303,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,
|
||||
|
@ -330,6 +354,9 @@ 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 {
|
||||
if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil {
|
||||
|
@ -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.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.
|
||||
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.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 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.mu.Lock()
|
||||
defer sheetRels.mu.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
|
||||
|
@ -599,19 +502,54 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) {
|
|||
}
|
||||
f.mu.Unlock()
|
||||
if ws.Drawing == nil {
|
||||
return nil, err
|
||||
return f.getCellImages(sheet, cell)
|
||||
}
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
|
||||
drawingXML := strings.ReplaceAll(target, "..", "xl")
|
||||
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
|
||||
drawingRelationships := strings.ReplaceAll(
|
||||
strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
|
||||
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
|
||||
imgs, err := f.getCellImages(sheet, cell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pics, err := f.getPicture(row, col, drawingXML, drawingRelationships)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
|
@ -627,85 +565,128 @@ 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)
|
||||
}
|
||||
}
|
||||
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.mu.Lock()
|
||||
defer wsDr.mu.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
|
||||
|
@ -752,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)
|
||||
}
|
||||
|
@ -778,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
|
||||
}
|
||||
|
|
347
picture_test.go
347
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,17 +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"}}))
|
||||
|
||||
for cell, ext := range map[string]string{"Q8": "gif", "Q15": "jpg", "Q22": "tif", "Q28": "bmp"} {
|
||||
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), 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
|
||||
|
@ -114,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) {
|
||||
|
@ -129,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")
|
||||
|
@ -169,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")
|
||||
|
@ -179,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) {
|
||||
|
@ -192,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)
|
||||
|
@ -221,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) {
|
||||
|
@ -243,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) {
|
||||
|
@ -273,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")
|
||||
}
|
||||
|
|
165
rows.go
165
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.
|
||||
|
@ -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
|
||||
|
@ -346,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 {
|
||||
|
@ -357,6 +366,9 @@ 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
|
||||
|
@ -365,9 +377,14 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
|
|||
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
|
||||
|
@ -628,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
|
||||
|
@ -636,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
|
||||
}
|
||||
|
@ -647,7 +664,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
|
|||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
idx2 := -1
|
||||
|
@ -657,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, row2-row, true)
|
||||
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
|
||||
}
|
||||
|
@ -708,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" />
|
||||
|
@ -717,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" />
|
||||
|
|
90
rows_test.go
90
rows_test.go
|
@ -239,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))
|
||||
|
@ -353,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
|
||||
|
@ -870,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) {
|
||||
|
@ -896,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) {
|
||||
|
@ -932,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) {
|
||||
|
@ -945,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())
|
||||
}
|
||||
|
||||
|
@ -981,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) {
|
||||
|
|
55
shape.go
55
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
|
||||
|
||||
|
@ -38,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)
|
||||
|
@ -50,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},
|
||||
|
@ -293,7 +293,7 @@ func (f *File) AddShape(sheet string, opts *Shape) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Read sheet data.
|
||||
// Read sheet data
|
||||
ws, err := f.workSheetReader(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -322,29 +322,27 @@ func (f *File) AddShape(sheet string, opts *Shape) error {
|
|||
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
|
||||
}
|
||||
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, fromCol, fromRow, 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
|
||||
}
|
||||
|
|
|
@ -42,16 +42,16 @@ func TestAddShape(t *testing.T) {
|
|||
},
|
||||
},
|
||||
), "sheet Sheet3 does not exist")
|
||||
assert.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error())
|
||||
assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "A1"}), ErrParameterInvalid.Error())
|
||||
assert.EqualError(t, f.AddShape("Sheet1", &Shape{
|
||||
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"}},
|
||||
},
|
||||
}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))
|
||||
|
||||
// Test add first shape for given sheet
|
||||
|
@ -79,14 +79,14 @@ 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", &Shape{
|
||||
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"}},
|
||||
},
|
||||
}), ErrSheetNameInvalid.Error())
|
||||
}))
|
||||
// Test add shape with unsupported charset style sheet
|
||||
f.Styles = nil
|
||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
||||
|
|
248
sheet.go
248
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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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.mu.Lock()
|
||||
defer content.mu.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
|
||||
}
|
||||
|
@ -1123,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
|
||||
|
@ -1156,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
|
||||
// |
|
||||
|
@ -1260,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,
|
||||
|
@ -1500,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
|
||||
|
@ -1513,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
|
||||
}
|
||||
|
@ -1527,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)
|
||||
}
|
||||
|
@ -1543,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),
|
||||
}
|
||||
|
@ -1577,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
|
||||
}
|
||||
|
@ -1591,11 +1718,29 @@ 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 {
|
||||
if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 {
|
||||
return err
|
||||
}
|
||||
wb, err := f.workbookReader()
|
||||
|
@ -1713,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
|
||||
}
|
||||
|
@ -1732,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
|
||||
}
|
||||
|
||||
|
@ -1864,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 {
|
||||
|
@ -1958,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
|
||||
}
|
||||
|
|
131
sheet_test.go
131
sheet_test.go
|
@ -8,6 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -120,7 +121,7 @@ func TestPanes(t *testing.T) {
|
|||
|
||||
// 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",
|
||||
|
@ -173,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
|
||||
|
@ -209,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")
|
||||
|
@ -218,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) {
|
||||
|
@ -230,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")
|
||||
|
@ -251,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",
|
||||
|
@ -259,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",
|
||||
|
@ -276,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",
|
||||
|
@ -297,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
|
||||
|
@ -442,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) {
|
||||
|
@ -452,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")
|
||||
|
@ -502,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)
|
||||
|
@ -525,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) {
|
||||
|
@ -551,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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
31
sparkline.go
31
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,10 +18,10 @@ 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: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
|
||||
ColorNegative: &xlsxColor{Theme: intPtr(5)},
|
||||
|
@ -356,15 +356,14 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup {
|
|||
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"
|
||||
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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"}), newInvalidNameError("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,8 +338,7 @@ 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)
|
||||
|
@ -398,3 +403,54 @@ func TestStreamWriterOutlineLevel(t *testing.T) {
|
|||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
349
styles_test.go
349
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)
|
||||
|
@ -294,7 +380,7 @@ func TestNewStyle(t *testing.T) {
|
|||
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)
|
||||
|
@ -353,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) {
|
||||
|
@ -405,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) {
|
||||
|
@ -413,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)
|
||||
|
@ -472,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")
|
||||
}
|
||||
|
|
200
table.go
200
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,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,6 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
|
@ -125,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
|
||||
})
|
||||
|
@ -156,37 +267,58 @@ 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
|
||||
}
|
||||
|
||||
// checkDefinedName check whether there are illegal characters in the defined
|
||||
|
@ -197,14 +329,22 @@ func checkDefinedName(name string) error {
|
|||
if utf8.RuneCountInString(name) > MaxFieldLength {
|
||||
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) {
|
||||
continue
|
||||
return newInvalidNameError(name)
|
||||
}
|
||||
if i > 0 && unicode.IsDigit(c) {
|
||||
if inCodeRange(int(c), supportedDefinedNameAfterStartCharCodeRange) {
|
||||
continue
|
||||
}
|
||||
return newInvalidNameError(name)
|
||||
|
@ -224,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)
|
||||
|
@ -242,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,
|
||||
|
@ -254,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
|
||||
|
@ -341,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
|
||||
|
@ -353,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,
|
||||
|
@ -365,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
|
||||
}
|
||||
|
@ -404,12 +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}
|
||||
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 {
|
||||
|
@ -538,7 +676,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
|
|||
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
|
||||
|
|
110
table_test.go
110
table_test.go
|
@ -27,6 +27,10 @@ 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)
|
||||
|
@ -41,11 +45,11 @@ 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]")
|
||||
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
|
||||
|
@ -60,24 +64,82 @@ func TestAddTable(t *testing.T) {
|
|||
{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.EqualError(t, f.SetDefinedName(&DefinedName{
|
||||
}))
|
||||
assert.Equal(t, cases.err, f.SetDefinedName(&DefinedName{
|
||||
Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5",
|
||||
}), cases.err.Error())
|
||||
}))
|
||||
}
|
||||
// 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) {
|
||||
|
@ -102,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) {
|
||||
|
@ -131,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) {
|
||||
|
@ -156,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>`
|
||||
|
|
184
vmlDrawing.go
184
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,7 +20,7 @@ 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"`
|
||||
XMLNSmv string `xml:"xmlns:mv,attr,omitempty"`
|
||||
ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"`
|
||||
ShapeType *xlsxShapeType `xml:"v:shapetype"`
|
||||
Shape []xlsxShape `xml:"v:shape"`
|
||||
|
@ -44,11 +44,12 @@ 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"`
|
||||
Button string `xml:"o:button,attr,omitempty"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
FillColor string `xml:"fillcolor,attr"`
|
||||
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"`
|
||||
|
@ -60,9 +61,14 @@ type xlsxShapeType struct {
|
|||
ID string `xml:"id,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.
|
||||
|
@ -72,10 +78,28 @@ type xlsxStroke struct {
|
|||
|
||||
// vPath directly maps the v:path element.
|
||||
type vPath struct {
|
||||
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
|
||||
// Shape element.
|
||||
type vFill struct {
|
||||
|
@ -106,6 +130,13 @@ type vTextBox struct {
|
|||
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"`
|
||||
|
@ -128,18 +159,28 @@ type vmlFont struct {
|
|||
// element.
|
||||
type xClientData struct {
|
||||
ObjectType string `xml:"ObjectType,attr"`
|
||||
MoveWithCells string `xml:"x:MoveWithCells"`
|
||||
SizeWithCells string `xml:"x:SizeWithCells"`
|
||||
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"`
|
||||
Checked *string `xml:"x:Checked,omitempty"`
|
||||
NoThreeD *string `xml:"x:NoThreeD,omitempty"`
|
||||
FirstButton *string `xml:"x:FirstButton,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
|
||||
|
@ -155,17 +196,21 @@ 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"`
|
||||
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"`
|
||||
|
@ -175,15 +220,67 @@ type decodeShape struct {
|
|||
// 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"`
|
||||
Column int
|
||||
Row int
|
||||
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.
|
||||
|
@ -192,26 +289,34 @@ type encodeShape struct {
|
|||
Shadow *vShadow `xml:"v:shadow"`
|
||||
Path *vPath `xml:"v:path"`
|
||||
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 {
|
||||
rows int
|
||||
cols int
|
||||
FormCtrl bool
|
||||
Sheet string
|
||||
Author string
|
||||
AuthorID int
|
||||
Cell string
|
||||
Checked bool
|
||||
Text string
|
||||
Macro string
|
||||
Width uint
|
||||
Height uint
|
||||
Paragraph []RichTextRun
|
||||
Type FormControlType
|
||||
Format GraphicOptions
|
||||
formCtrl bool
|
||||
sheet string
|
||||
Comment
|
||||
FormControl
|
||||
}
|
||||
|
||||
// FormControl directly maps the form controls information.
|
||||
|
@ -221,8 +326,27 @@ type FormControl struct {
|
|||
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
|
||||
}
|
||||
|
|
368
vml_test.go
368
vml_test.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,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 (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -34,7 +35,7 @@ func TestAddComment(t *testing.T) {
|
|||
// 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.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
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)
|
||||
|
@ -56,7 +57,7 @@ func TestAddComment(t *testing.T) {
|
|||
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())
|
||||
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
|
||||
|
@ -104,7 +105,7 @@ func TestDeleteComment(t *testing.T) {
|
|||
assert.Len(t, comments, 0)
|
||||
|
||||
// Test delete comment with invalid sheet name
|
||||
assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error())
|
||||
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"))
|
||||
|
@ -121,6 +122,10 @@ func TestDeleteComment(t *testing.T) {
|
|||
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) {
|
||||
|
@ -149,26 +154,21 @@ func TestCountComments(t *testing.T) {
|
|||
func TestAddDrawingVML(t *testing.T) {
|
||||
// Test addDrawingVML with illegal cell reference
|
||||
f := NewFile()
|
||||
assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error())
|
||||
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{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
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()
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "D1",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A1",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
Width: 140,
|
||||
Height: 60,
|
||||
Text: "Button 1\r\n",
|
||||
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{
|
||||
|
@ -182,29 +182,65 @@ func TestFormControl(t *testing.T) {
|
|||
Text: "C1=A1+B1",
|
||||
},
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A5",
|
||||
Type: FormControlCheckbox,
|
||||
Text: "Check Box 1",
|
||||
Checked: true,
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A6",
|
||||
Type: FormControlCheckbox,
|
||||
Text: "Check Box 2",
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A7",
|
||||
Type: FormControlRadio,
|
||||
Text: "Option Button 1",
|
||||
Checked: true,
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A8",
|
||||
Type: FormControlRadio,
|
||||
Text: "Option Button 2",
|
||||
}))
|
||||
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)
|
||||
|
@ -213,33 +249,53 @@ func TestFormControl(t *testing.T) {
|
|||
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",
|
||||
Text: "Button 2",
|
||||
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",
|
||||
Cell: "A1", Type: 0x37, Macro: "Button1_Click",
|
||||
}), ErrParameterInvalid)
|
||||
// Test add form control on not exists worksheet
|
||||
assert.Equal(t, f.AddFormControl("SheetN", FormControl{
|
||||
Cell: "A1",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
}), newNoExistSheetError("SheetN"))
|
||||
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, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN"))
|
||||
// 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())
|
||||
|
@ -248,8 +304,208 @@ func TestFormControl(t *testing.T) {
|
|||
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())
|
||||
}
|
||||
|
|
179
workbook.go
179
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
|
||||
|
||||
|
@ -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"`
|
||||
}
|
||||
|
|
55
xmlChart.go
55
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
|
||||
|
||||
|
@ -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"`
|
||||
|
@ -483,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"`
|
||||
|
@ -533,11 +534,13 @@ 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
|
||||
|
@ -560,6 +563,7 @@ type ChartPlotArea struct {
|
|||
ShowPercent bool
|
||||
ShowSerName bool
|
||||
ShowVal bool
|
||||
Fill Fill
|
||||
NumFmt ChartNumFmt
|
||||
}
|
||||
|
||||
|
@ -570,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
|
||||
}
|
||||
|
@ -588,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
|
||||
}
|
||||
|
@ -602,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
|
||||
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
274
xmlDrawing.go
274
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 = 260
|
||||
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"`
|
||||
|
@ -442,6 +275,7 @@ type xlsxPoint2D struct {
|
|||
type xlsxWsDr struct {
|
||||
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
|
||||
|
@ -617,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,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
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
23
xmlStyles.go
23
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
|
||||
|
||||
|
@ -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,7 +296,14 @@ 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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -309,7 +311,8 @@ type xlsxNumFmt struct {
|
|||
// 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.
|
||||
|
|
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,7 +16,8 @@ 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 {
|
||||
mu sync.Mutex
|
||||
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
|
||||
|
@ -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"`
|
||||
|
@ -218,6 +219,64 @@ 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
|
||||
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -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"`
|
||||
|
@ -424,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 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"`
|
||||
|
@ -441,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
|
||||
|
@ -477,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
|
||||
|
@ -699,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"`
|
||||
|
@ -733,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"`
|
||||
|
@ -748,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 {
|
||||
|
@ -769,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"`
|
||||
|
@ -863,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
|
||||
|
@ -917,7 +917,7 @@ type ConditionalFormatOptions struct {
|
|||
Type string
|
||||
AboveAverage bool
|
||||
Percent bool
|
||||
Format int
|
||||
Format *int
|
||||
Criteria string
|
||||
Value string
|
||||
MinType string
|
||||
|
@ -963,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
|
||||
|
@ -1006,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