diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 5e16cfccae..f61cb199ed 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -28,8 +28,17 @@ jobs:
- name: Build
run: go build -v .
+ - name: Build on ARM
+ if: runner.os == 'Linux'
+ run: |
+ GOARCH=arm GOARM=5 go build .
+ GOARCH=arm GOARM=6 go build .
+ GOARCH=arm GOARM=7 go build .
+ GOARCH=arm64 go build .
+ GOARCH=arm64 GOOS=android go build .
+
- 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 50m -race ./... -coverprofile='coverage.txt' -covermode=atomic
- name: Codecov
uses: codecov/codecov-action@v5
diff --git a/adjust.go b/adjust.go
index 28e54e5809..38e6207689 100644
--- a/adjust.go
+++ b/adjust.go
@@ -38,10 +38,10 @@ var adjustHelperFunc = [9]func(*File, *xlsxWorksheet, string, adjustDirection, i
return f.adjustDataValidations(ws, sheet, dir, num, offset, sheetID)
},
func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
- return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID)
+ return f.adjustDefinedNames(sheet, dir, num, offset)
},
func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
- return f.adjustDrawings(ws, sheet, dir, num, offset, sheetID)
+ return f.adjustDrawings(ws, sheet, dir, num, offset)
},
func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
return f.adjustMergeCells(ws, sheet, dir, num, offset, sheetID)
@@ -475,20 +475,15 @@ func transformParenthesesToken(token efp.Token) string {
// adjustRangeSheetName returns replaced range reference by given source and
// target sheet name.
func adjustRangeSheetName(rng, source, target string) string {
+ source = escapeSheetName(source)
cellRefs := strings.Split(rng, ",")
for i, cellRef := range cellRefs {
rangeRefs := strings.Split(cellRef, ":")
for j, rangeRef := range rangeRefs {
parts := strings.Split(rangeRef, "!")
for k, part := range parts {
- singleQuote := strings.HasPrefix(part, "'") && strings.HasSuffix(part, "'")
- if singleQuote {
- part = strings.TrimPrefix(strings.TrimSuffix(part, "'"), "'")
- }
- if part == source {
- if part = target; singleQuote {
- part = "'" + part + "'"
- }
+ if strings.TrimPrefix(strings.TrimSuffix(part, "'"), "'") == source {
+ part = escapeSheetName(target)
}
parts[k] = part
}
@@ -1034,7 +1029,7 @@ func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editA
// adjustDrawings updates the ending anchor of the two cell anchor pictures
// and charts object when inserting or deleting rows or columns.
-func (to *xlsxTo) adjustDrawings(dir adjustDirection, num, offset int, editAs string, ok bool) error {
+func (to *xlsxTo) adjustDrawings(dir adjustDirection, num, offset int, ok bool) error {
if dir == columns && to.Col+1 >= num && to.Col+offset >= 0 && ok {
if to.Col+offset >= MaxColumns {
return ErrColumnNumber
@@ -1054,32 +1049,38 @@ func (to *xlsxTo) adjustDrawings(dir adjustDirection, num, offset int, editAs st
// inserting or deleting rows or columns.
func (a *xdrCellAnchor) adjustDrawings(dir adjustDirection, num, offset int) error {
editAs := a.EditAs
- if a.From == nil || a.To == nil || editAs == "absolute" {
+ if (a.From == nil && (a.To == nil || a.Ext == nil)) || editAs == "absolute" {
return nil
}
ok, err := a.From.adjustDrawings(dir, num, offset, editAs)
if err != nil {
return err
}
- return a.To.adjustDrawings(dir, num, offset, editAs, ok || editAs == "")
+ if a.To != nil {
+ return a.To.adjustDrawings(dir, num, offset, ok || editAs == "")
+ }
+ return err
}
// adjustDrawings updates the existing two cell anchor pictures and charts
// object when inserting or deleting rows or columns.
func (a *xlsxCellAnchorPos) adjustDrawings(dir adjustDirection, num, offset int, editAs string) error {
- if a.From == nil || a.To == nil || editAs == "absolute" {
+ if (a.From == nil && (a.To == nil || a.Ext == nil)) || editAs == "absolute" {
return nil
}
ok, err := a.From.adjustDrawings(dir, num, offset, editAs)
if err != nil {
return err
}
- return a.To.adjustDrawings(dir, num, offset, editAs, ok || editAs == "")
+ if a.To != nil {
+ return a.To.adjustDrawings(dir, num, offset, ok || editAs == "")
+ }
+ return err
}
// adjustDrawings updates the pictures and charts object when inserting or
// deleting rows or columns.
-func (f *File) adjustDrawings(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+func (f *File) adjustDrawings(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) error {
if ws.Drawing == nil {
return nil
}
@@ -1128,12 +1129,17 @@ func (f *File) adjustDrawings(ws *xlsxWorksheet, sheet string, dir adjustDirecti
return err
}
}
+ for _, anchor := range wsDr.OneCellAnchor {
+ if err = anchorCb(anchor); err != nil {
+ return err
+ }
+ }
return nil
}
// adjustDefinedNames updates the cell reference of the defined names when
// inserting or deleting rows or columns.
-func (f *File) adjustDefinedNames(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+func (f *File) adjustDefinedNames(sheet string, dir adjustDirection, num, offset int) error {
wb, err := f.workbookReader()
if err != nil {
return err
diff --git a/adjust_test.go b/adjust_test.go
index 0acc8bf2eb..07ccaa5e12 100644
--- a/adjust_test.go
+++ b/adjust_test.go
@@ -1206,7 +1206,7 @@ func TestAdjustDrawings(t *testing.T) {
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)
+ assert.Equal(t, []string{"D3", "B21", "D13"}, cells)
wb := filepath.Join("test", "TestAdjustDrawings.xlsx")
assert.NoError(t, f.SaveAs(wb))
@@ -1215,7 +1215,7 @@ func TestAdjustDrawings(t *testing.T) {
assert.NoError(t, f.RemoveRow("Sheet1", 1))
cells, err = f.GetPictureCells("Sheet1")
assert.NoError(t, err)
- assert.Equal(t, []string{"C2", "C12", "B21"}, cells)
+ assert.Equal(t, []string{"C2", "B21", "C12"}, cells)
// Test adjust existing pictures on inserting columns and rows
f, err = OpenFile(wb)
@@ -1227,7 +1227,7 @@ func TestAdjustDrawings(t *testing.T) {
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)
+ assert.Equal(t, []string{"F4", "B21", "F15"}, cells)
// Test adjust drawings with unsupported charset
f, err = OpenFile(wb)
@@ -1267,6 +1267,11 @@ func TestAdjustDrawings(t *testing.T) {
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+`000
0101
0`))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+fmt.Sprintf(`%d0
`, MaxColumns)))
+ 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 TestAdjustDefinedNames(t *testing.T) {
@@ -1330,5 +1335,5 @@ func TestAdjustDefinedNames(t *testing.T) {
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")
+ assert.EqualError(t, f.adjustDefinedNames("Sheet1", columns, 0, 0), "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/cell.go b/cell.go
index f2df30478c..af918555b5 100644
--- a/cell.go
+++ b/cell.go
@@ -17,6 +17,7 @@ import (
"math"
"os"
"reflect"
+ "sort"
"strconv"
"strings"
"time"
@@ -193,6 +194,7 @@ func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error {
for col, cell := range row.C {
if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si {
ws.SheetData.Row[r].C[col].F = nil
+ ws.formulaSI.Delete(si)
_ = f.deleteCalcChain(sheetID, cell.R)
}
}
@@ -689,7 +691,8 @@ func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, err
return "", false, nil
}
if c.F.T == STCellFormulaTypeShared && c.F.Si != nil {
- return getSharedFormula(x, *c.F.Si, c.R), true, nil
+ formula, err := getSharedFormula(x, *c.F.Si, c.R)
+ return formula, true, err
}
return c.F.Content, true, nil
})
@@ -793,6 +796,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
return err
}
if formula == "" {
+ ws.deleteSharedFormula(c)
c.F = nil
return f.deleteCalcChain(f.getSheetID(sheet), cell)
}
@@ -815,7 +819,8 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
}
}
if c.F.T == STCellFormulaTypeShared {
- if err = ws.setSharedFormula(*opt.Ref); err != nil {
+ ws.deleteSharedFormula(c)
+ if err = ws.setSharedFormula(cell, *opt.Ref); err != nil {
return err
}
}
@@ -890,22 +895,28 @@ func (f *File) setArrayFormulaCells() error {
}
// setSharedFormula set shared formula for the cells.
-func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
+func (ws *xlsxWorksheet) setSharedFormula(cell, ref string) error {
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return err
}
_ = sortCoordinates(coordinates)
- cnt := ws.countSharedFormula()
- for c := coordinates[0]; c <= coordinates[2]; c++ {
- for r := coordinates[1]; r <= coordinates[3]; r++ {
- ws.prepareSheetXML(c, r)
- cell := &ws.SheetData.Row[r-1].C[c-1]
- if cell.F == nil {
- cell.F = &xlsxF{}
+ si := ws.countSharedFormula()
+ for col := coordinates[0]; col <= coordinates[2]; col++ {
+ for rol := coordinates[1]; rol <= coordinates[3]; rol++ {
+ ws.prepareSheetXML(col, rol)
+ c := &ws.SheetData.Row[rol-1].C[col-1]
+ if c.F == nil {
+ c.F = &xlsxF{}
+ }
+ c.F.T = STCellFormulaTypeShared
+ if c.R == cell {
+ if c.F.Ref != "" {
+ si = *c.F.Si
+ continue
+ }
}
- cell.F.T = STCellFormulaTypeShared
- cell.F.Si = &cnt
+ c.F.Si = &si
}
}
return err
@@ -923,6 +934,23 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) {
return
}
+// deleteSharedFormula delete shared formula cell from worksheet shared formula
+// index cache and remove all shared cells formula which refer to the cell which
+// containing the formula.
+func (ws *xlsxWorksheet) deleteSharedFormula(c *xlsxC) {
+ if c.F != nil && c.F.Si != nil && c.F.Ref != "" {
+ si := *c.F.Si
+ ws.formulaSI.Delete(si)
+ for r, row := range ws.SheetData.Row {
+ for c, cell := range row.C {
+ if cell.F != nil && cell.F.Si != nil && *cell.F.Si == si && cell.F.Ref == "" {
+ ws.SheetData.Row[r].C[c].F = nil
+ }
+ }
+ }
+ }
+}
+
// GetCellHyperLink gets a cell hyperlink based on the given worksheet name and
// cell reference. If the cell has a hyperlink, it will return 'true' and
// the link address, otherwise it will return 'false' and an empty link
@@ -1471,16 +1499,22 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
return "", nil
}
- for rowIdx := range ws.SheetData.Row {
- rowData := &ws.SheetData.Row[rowIdx]
- if rowData.R != row {
- continue
+ idx, found := sort.Find(len(ws.SheetData.Row), func(i int) int {
+ if ws.SheetData.Row[i].R == row {
+ return 0
}
- for colIdx := range rowData.C {
- colData := &rowData.C[colIdx]
- if cell != colData.R {
- continue
- }
+ if ws.SheetData.Row[i].R > row {
+ return -1
+ }
+ return 1
+ })
+ if !found {
+ return "", nil
+ }
+ rowData := ws.SheetData.Row[idx]
+ for colIdx := range rowData.C {
+ colData := &rowData.C[colIdx]
+ if cell == colData.R {
val, ok, err := fn(ws, colData)
if err != nil {
return "", err
@@ -1488,6 +1522,7 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
if ok {
return val, nil
}
+ break
}
}
return "", nil
@@ -1640,18 +1675,27 @@ func isOverlap(rect1, rect2 []int) bool {
cellInRange([]int{rect2[2], rect2[3]}, rect1)
}
-// parseSharedFormula generate dynamic part of shared formula for target cell
-// by given column and rows distance and origin shared formula.
-func parseSharedFormula(dCol, dRow int, orig string) string {
+// convertSharedFormula creates a non shared formula from the shared formula
+// counterpart by given cell reference which not containing the formula.
+func (c *xlsxC) convertSharedFormula(cell string) (string, error) {
+ col, row, err := CellNameToCoordinates(cell)
+ if err != nil {
+ return "", err
+ }
+ sharedCol, sharedRow, err := CellNameToCoordinates(c.R)
+ if err != nil {
+ return "", err
+ }
+ dCol, dRow := col-sharedCol, row-sharedRow
ps := efp.ExcelParser()
- tokens := ps.Parse(string(orig))
- for i := 0; i < len(tokens); i++ {
+ tokens := ps.Parse(c.F.Content)
+ for i := range tokens {
token := tokens[i]
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
tokens[i].TValue = shiftCell(token.TValue, dCol, dRow)
}
}
- return ps.Render()
+ return ps.Render(), nil
}
// getSharedFormula find a cell contains the same formula as another cell,
@@ -1662,21 +1706,23 @@ func parseSharedFormula(dCol, dRow int, orig string) string {
//
// 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 row := 0; row < len(ws.SheetData.Row); row++ {
+func getSharedFormula(ws *xlsxWorksheet, si int, cell string) (string, error) {
+ val, ok := ws.formulaSI.Load(si)
+
+ if ok {
+ return val.(*xlsxC).convertSharedFormula(cell)
+ }
+ for row := range ws.SheetData.Row {
r := &ws.SheetData.Row[row]
- for column := 0; column < len(r.C); column++ {
+ for column := range r.C {
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)
- dCol := col - sharedCol
- dRow := row - sharedRow
- return parseSharedFormula(dCol, dRow, c.F.Content)
+ ws.formulaSI.Store(si, c)
+ return c.convertSharedFormula(cell)
}
}
}
- return ""
+ return "", nil
}
// shiftCell returns the cell shifted according to dCol and dRow taking into
diff --git a/cell_test.go b/cell_test.go
index b9069a354e..88abb95c32 100644
--- a/cell_test.go
+++ b/cell_test.go
@@ -563,7 +563,7 @@ func TestGetValueFrom(t *testing.T) {
assert.NoError(t, err)
value, err := c.getValueFrom(f, sst, false)
assert.NoError(t, err)
- assert.Equal(t, "", value)
+ assert.Empty(t, value)
c = xlsxC{T: "s", V: " 1 "}
value, err = c.getValueFrom(f, &xlsxSST{Count: 1, SI: []xlsxSI{{}, {T: &xlsxT{Val: "s"}}}}, false)
@@ -602,13 +602,17 @@ func TestGetCellFormula(t *testing.T) {
formula, err := f.GetCellFormula("Sheet1", "B3")
assert.NoError(t, err)
assert.Equal(t, expected, formula)
+ // Test get shared formula form cache
+ formula, err = f.GetCellFormula("Sheet1", "B3")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, formula)
}
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`
`))
formula, err := f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err)
- assert.Equal(t, "", formula)
+ assert.Empty(t, formula)
// Test get array formula with invalid cell range reference
f = NewFile()
@@ -628,6 +632,81 @@ func TestGetCellFormula(t *testing.T) {
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")
+
+ // Test get shared formula after updated refer cell formula, the shared
+ // formula cell reference range covered the previous.
+ f = NewFile()
+ formulaType, ref = STCellFormulaTypeShared, "C2:C6"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ formula, err = f.GetCellFormula("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2+B2", formula)
+ formula, err = f.GetCellFormula("Sheet1", "C6")
+ assert.NoError(t, err)
+ assert.Equal(t, "A6+B6", formula)
+
+ formulaType, ref = STCellFormulaTypeShared, "C2:C8"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ formula, err = f.GetCellFormula("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2*B2", formula)
+ formula, err = f.GetCellFormula("Sheet1", "C8")
+ assert.NoError(t, err)
+ assert.Equal(t, "A8*B8", formula)
+ assert.NoError(t, f.Close())
+
+ // Test get shared formula after updated refer cell formula, the shared
+ // formula cell reference range not over the previous.
+ f = NewFile()
+ formulaType, ref = STCellFormulaTypeShared, "C2:C6"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ formula, err = f.GetCellFormula("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2+B2", formula)
+ formula, err = f.GetCellFormula("Sheet1", "C6")
+ assert.NoError(t, err)
+ assert.Equal(t, "A6+B6", formula)
+
+ formulaType, ref = STCellFormulaTypeShared, "C2:C4"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ formula, err = f.GetCellFormula("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2*B2", formula)
+ formula, err = f.GetCellFormula("Sheet1", "C6")
+ assert.NoError(t, err)
+ assert.Empty(t, formula)
+
+ // Test get shared formula after remove refer cell formula
+ f = NewFile()
+ formulaType, ref = STCellFormulaTypeShared, "C2:C6"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", ""))
+
+ formula, err = f.GetCellFormula("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Empty(t, formula)
+ formula, err = f.GetCellFormula("Sheet1", "C6")
+ assert.NoError(t, err)
+ assert.Empty(t, formula)
+
+ formulaType, ref = STCellFormulaTypeShared, "C2:C8"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ formula, err = f.GetCellFormula("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2*B2", formula)
+ formula, err = f.GetCellFormula("Sheet1", "C8")
+ assert.NoError(t, err)
+ assert.Equal(t, "A8*B8", formula)
+ assert.NoError(t, f.Close())
+}
+
+func TestConvertSharedFormula(t *testing.T) {
+ c := xlsxC{R: "A"}
+ _, err := c.convertSharedFormula("A")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
+ _, err = c.convertSharedFormula("A1")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
}
func ExampleFile_SetCellFloat() {
@@ -1186,3 +1265,14 @@ func TestSetCellIntFunc(t *testing.T) {
func TestSIString(t *testing.T) {
assert.Empty(t, xlsxSI{}.String())
}
+
+func TestGetCellStringFunc(t *testing.T) {
+ f := NewFile()
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row = []xlsxRow{{R: 2}}
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.Empty(t, val)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+}
diff --git a/chart.go b/chart.go
index 88df5c2f21..011fbd1c3c 100644
--- a/chart.go
+++ b/chart.go
@@ -863,6 +863,14 @@ func (opts *Chart) parseTitle() {
// ShowCatName: Specifies that the category name shall be shown in the data
// label. The 'ShowCatName' property is optional. The default value is true.
//
+// ShowDataTable: Used for add data table under chart, depending on the chart
+// type, only available for area, bar, column and line series type charts. The
+// 'ShowDataTable' property is optional. The default value is false.
+//
+// ShowDataTableKeys: Used for add legend key in data table, only works on
+// 'ShowDataTable' is enabled. The 'ShowDataTableKeys' property is optional.
+// The default value is false.
+//
// ShowLeaderLines: Specifies leader lines shall be shown for data labels. The
// 'ShowLeaderLines' property is optional. The default value is false.
//
diff --git a/chart_test.go b/chart_test.go
index 42e2a65764..989d9337d8 100644
--- a/chart_test.go
+++ b/chart_test.go
@@ -93,11 +93,8 @@ func TestChartSize(t *testing.T) {
t.FailNow()
}
- if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") ||
- !assert.Equal(t, 29, anchor.To.Row, "Expected 'to' row 29") {
-
- t.FailNow()
- }
+ assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14")
+ assert.Equal(t, 29, anchor.To.Row, "Expected 'to' row 29")
}
func TestAddDrawingChart(t *testing.T) {
@@ -293,7 +290,7 @@ 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: []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}}))
+ 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, ShowDataTable: true, ShowDataTableKeys: true, 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"},
diff --git a/col.go b/col.go
index 6608048a83..d8c3d0dbe9 100644
--- a/col.go
+++ b/col.go
@@ -14,7 +14,6 @@ package excelize
import (
"bytes"
"encoding/xml"
- "math"
"strconv"
"strings"
@@ -621,40 +620,35 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
//
// width # Width of object frame.
// height # Height of object frame.
-func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) {
+func (f *File) positionObjectPixels(sheet string, col, row, width, height int, opts *GraphicOptions) (int, int, int, int, int, int, int, int) {
colIdx, rowIdx := col-1, row-1
- // Adjust start column for offsets that are greater than the col width.
- for x1 >= f.getColWidth(sheet, colIdx+1) {
- colIdx++
- x1 -= f.getColWidth(sheet, colIdx)
- }
-
- // Adjust start row for offsets that are greater than the row height.
- for y1 >= f.getRowHeight(sheet, rowIdx+1) {
- rowIdx++
- y1 -= f.getRowHeight(sheet, rowIdx)
- }
-
// Initialized end cell to the same as the start cell.
colEnd, rowEnd := colIdx, rowIdx
+ x1, y1, x2, y2 := opts.OffsetX, opts.OffsetY, width, height
+ if opts.Positioning != "oneCell" {
+ // Using a twoCellAnchor, the maximum possible offset is limited by the
+ // "from" cell dimensions. If these were to be exceeded the "toPoint" would
+ // be calculated incorrectly, since the requested "fromPoint" is not possible
+
+ x1 = min(x1, f.getColWidth(sheet, col))
+ y1 = min(y1, f.getRowHeight(sheet, row))
+
+ x2 += x1
+ y2 += y1
+ // Subtract the underlying cell widths to find end cell of the object.
+ for x2 >= f.getColWidth(sheet, colEnd+1) {
+ colEnd++
+ x2 -= f.getColWidth(sheet, colEnd)
+ }
- width += x1
- height += y1
-
- // Subtract the underlying cell widths to find end cell of the object.
- for width >= f.getColWidth(sheet, colEnd+1) {
- colEnd++
- width -= f.getColWidth(sheet, colEnd)
- }
-
- // Subtract the underlying cell heights to find end cell of the object.
- for height >= f.getRowHeight(sheet, rowEnd+1) {
- rowEnd++
- height -= f.getRowHeight(sheet, rowEnd)
+ // Subtract the underlying cell heights to find end cell of the object.
+ for y2 >= f.getRowHeight(sheet, rowEnd+1) {
+ rowEnd++
+ y2 -= f.getRowHeight(sheet, rowEnd)
+ }
}
-
// The end vertices are whatever is left from the width and height.
- return colIdx, rowIdx, colEnd, rowEnd, width, height
+ return colIdx, rowIdx, colEnd, rowEnd, x1, y1, x2, y2
}
// getColWidth provides a function to get column width in pixels by given
@@ -664,13 +658,14 @@ func (f *File) getColWidth(sheet string, col int) int {
ws.mu.Lock()
defer ws.mu.Unlock()
if ws.Cols != nil {
- var width float64
+ width := -1.0
for _, v := range ws.Cols.Col {
if v.Min <= col && col <= v.Max && v.Width != nil {
width = *v.Width
+ break
}
}
- if width != 0 {
+ if width != -1.0 {
return int(convertColWidthToPixels(width))
}
}
@@ -782,6 +777,7 @@ func (f *File) RemoveCol(sheet, col string) error {
if err != nil {
return err
}
+ ws.formulaSI.Clear()
for rowIdx := range ws.SheetData.Row {
rowData := &ws.SheetData.Row[rowIdx]
for colIdx := range rowData.C {
@@ -800,16 +796,11 @@ func (f *File) RemoveCol(sheet, col string) error {
// pixel. If the width hasn't been set by the user we use the default value.
// If the column is hidden it has a value of zero.
func convertColWidthToPixels(width float64) float64 {
- var padding float64 = 5
var pixels float64
- var maxDigitWidth float64 = 7
+ var maxDigitWidth float64 = 8
if width == 0 {
return pixels
}
- if width < 1 {
- pixels = (width * 12) + 0.5
- return math.Ceil(pixels)
- }
- pixels = (width*maxDigitWidth + 0.5) + padding
- return math.Ceil(pixels)
+ pixels = (width*maxDigitWidth + 0.5)
+ return float64(int(pixels))
}
diff --git a/col_test.go b/col_test.go
index 2e7aeb80c7..5c4bfc5559 100644
--- a/col_test.go
+++ b/col_test.go
@@ -396,7 +396,7 @@ func TestColWidth(t *testing.T) {
width, err = f.GetColWidth("Sheet1", "A")
assert.NoError(t, err)
assert.Equal(t, 10.0, width)
- assert.Equal(t, 76, f.getColWidth("Sheet1", 1))
+ assert.Equal(t, 80, f.getColWidth("Sheet1", 1))
// Test set and get column width with illegal cell reference
width, err = f.GetColWidth("Sheet1", "*")
@@ -484,5 +484,5 @@ func TestRemoveCol(t *testing.T) {
}
func TestConvertColWidthToPixels(t *testing.T) {
- assert.Equal(t, -11.0, convertColWidthToPixels(-1))
+ assert.Equal(t, -7.0, convertColWidthToPixels(-1))
}
diff --git a/datavalidation.go b/datavalidation.go
index ab61931625..3b5a852dbc 100644
--- a/datavalidation.go
+++ b/datavalidation.go
@@ -12,9 +12,11 @@
package excelize
import (
+ "encoding/xml"
"fmt"
"io"
"math"
+ "slices"
"strings"
"unicode/utf16"
)
@@ -361,9 +363,27 @@ func getDataValidations(dvs *xlsxDataValidations) []*DataValidation {
}
// DeleteDataValidation delete data validation by given worksheet name and
-// reference sequence. This function is concurrency safe.
-// All data validations in the worksheet will be deleted
-// if not specify reference sequence parameter.
+// reference sequence. This function is concurrency safe. All data validations
+// in the worksheet will be deleted if not specify reference sequence parameter.
+//
+// Example 1, delete data validation on Sheet1!A1:B2:
+//
+// err := f.DeleteDataValidation("Sheet1", "A1:B2")
+//
+// Example 2, delete data validations on Sheet1 with multiple cell ranges
+// A1:B2 and C1:C3 with reference sequence slice:
+//
+// err := f.DeleteDataValidation("Sheet1", []string{"A1:B2", "C1:C3"}...)
+//
+// Example 3, delete data validations on Sheet1 with multiple cell ranges
+// A1:B2 and C1:C3 with blank separated reference sequence string, the result
+// same as example 2:
+//
+// err := f.DeleteDataValidation("Sheet1", "A1:B2 C1:C3")
+//
+// Example 4, delete all data validations on Sheet1:
+//
+// err := f.DeleteDataValidation("Sheet1")
func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
@@ -371,17 +391,31 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
}
ws.mu.Lock()
defer ws.mu.Unlock()
- if ws.DataValidations == nil {
+ if ws.DataValidations == nil && ws.ExtLst == nil {
return nil
}
if sqref == nil {
ws.DataValidations = nil
return nil
}
- delCells, err := flatSqref(sqref[0])
+ delCells, err := flatSqref(strings.Join(sqref, " "))
if err != nil {
return err
}
+ if ws.DataValidations != nil {
+ if err = f.deleteDataValidation(ws, delCells); err != nil {
+ return err
+ }
+ }
+ if ws.ExtLst != nil {
+ return f.deleteX14DataValidation(ws, sqref)
+ }
+ return nil
+}
+
+// deleteDataValidation deletes data validation by given worksheet and cell
+// reference list.
+func (f *File) deleteDataValidation(ws *xlsxWorksheet, delCells map[int][][]int) error {
dv := ws.DataValidations
for i := 0; i < len(dv.DataValidation); i++ {
var applySqref []string
@@ -413,6 +447,64 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
return nil
}
+// deleteX14DataValidation deletes data validation in the extLst element by
+// given worksheet and cell reference list.
+func (f *File) deleteX14DataValidation(ws *xlsxWorksheet, sqref []string) error {
+ var (
+ decodeExtLst = new(decodeExtLst)
+ decodeDataValidations *xlsxDataValidations
+ x14DataValidations *xlsxX14DataValidations
+ )
+ if err := f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return err
+ }
+ for i, ext := range decodeExtLst.Ext {
+ if ext.URI == ExtURIDataValidations {
+ decodeDataValidations = new(xlsxDataValidations)
+ x14DataValidations = new(xlsxX14DataValidations)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeDataValidations)
+ x14DataValidations.XMLNSXM = NameSpaceSpreadSheetExcel2006Main.Value
+ x14DataValidations.DisablePrompts = decodeDataValidations.DisablePrompts
+ x14DataValidations.XWindow = decodeDataValidations.XWindow
+ x14DataValidations.YWindow = decodeDataValidations.YWindow
+ for _, dv := range decodeDataValidations.DataValidation {
+ if inStrSlice(sqref, dv.XMSqref, false) == -1 {
+ x14DataValidations.DataValidation = append(x14DataValidations.DataValidation, &xlsxX14DataValidation{
+ 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,
+ XMSqref: dv.XMSqref,
+ Type: dv.Type,
+ Formula1: dv.Formula1,
+ Formula2: dv.Formula2,
+ })
+ }
+ }
+ x14DataValidations.Count = len(x14DataValidations.DataValidation)
+ x14DataValidationsBytes, _ := xml.Marshal(x14DataValidations)
+ decodeExtLst.Ext[i] = &xlsxExt{
+ xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
+ URI: ExtURIDataValidations, Content: string(x14DataValidationsBytes),
+ }
+ if x14DataValidations.Count == 0 {
+ decodeExtLst.Ext = slices.Delete(decodeExtLst.Ext, i, i+1)
+ }
+ }
+ }
+ extLstBytes, err := xml.Marshal(decodeExtLst)
+ ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ return err
+}
+
// squashSqref generates cell reference sequence by given cells coordinates list.
func squashSqref(cells [][]int) []string {
if len(cells) == 1 {
diff --git a/datavalidation_test.go b/datavalidation_test.go
index a5d2becaf3..a979e2bc4b 100644
--- a/datavalidation_test.go
+++ b/datavalidation_test.go
@@ -16,6 +16,7 @@ import (
"math"
"path/filepath"
"strings"
+ "sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -81,7 +82,7 @@ func TestDataValidation(t *testing.T) {
dv.Formula1 = ""
assert.NoError(t, dv.SetDropList(listValid),
"SetDropList failed for valid input %v", listValid)
- assert.NotEqual(t, "", dv.Formula1,
+ assert.NotEmpty(t, dv.Formula1,
"Formula1 should not be empty for valid input %v", listValid)
}
assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1)
@@ -242,4 +243,31 @@ func TestDeleteDataValidation(t *testing.T) {
// Test delete all data validations in the worksheet
assert.NoError(t, f.DeleteDataValidation("Sheet1"))
assert.Nil(t, ws.(*xlsxWorksheet).DataValidations)
+
+ t.Run("delete_data_validation_from_extLst", func(t *testing.T) {
+ f := NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", fmt.Appendf(nil,
+ `A1:A2Sheet1!$A$2:$A$4B1:B2Sheet1!$B$2:$B$3`,
+ NameSpaceSpreadSheet.Value, NameSpaceSpreadSheetExcel2006Main.Value,
+ ExtURIDataValidations, NameSpaceSpreadSheetExcel2006Main.Value))
+ f.checked = sync.Map{}
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:A2"))
+ dvs, err := f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dvs, 1)
+ assert.Equal(t, "B1:B2", dvs[0].Sqref)
+
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", "B1:B2"))
+ dvs, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Empty(t, dvs)
+ })
+
+ t.Run("delete_data_validation_failed_from_extLst", func(t *testing.T) {
+ f := NewFile()
+ assert.EqualError(t, f.deleteX14DataValidation(&xlsxWorksheet{
+ ExtLst: &xlsxExtLst{Ext: ""},
+ }, nil), "XML syntax error on line 1: element closed by ")
+ })
}
diff --git a/drawing.go b/drawing.go
index c029fdf7d3..5f1c9dd4b0 100644
--- a/drawing.go
+++ b/drawing.go
@@ -169,6 +169,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) {
xlsxChartSpace.Chart.Legend = nil
}
xlsxChartSpace.Chart.PlotArea.SpPr = f.drawShapeFill(opts.PlotArea.Fill, xlsxChartSpace.Chart.PlotArea.SpPr)
+ xlsxChartSpace.Chart.PlotArea.DTable = f.drawPlotAreaDTable(opts)
addChart := func(c, p *cPlotArea) {
immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem()
for i := 0; i < mutable.NumField(); i++ {
@@ -1232,6 +1233,19 @@ func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle {
return title
}
+// drawPlotAreaDTable provides a function to draw the c:dTable element.
+func (f *File) drawPlotAreaDTable(opts *Chart) *cDTable {
+ if _, ok := plotAreaChartGrouping[opts.Type]; ok && opts.PlotArea.ShowDataTable {
+ return &cDTable{
+ ShowHorzBorder: &attrValBool{Val: boolPtr(true)},
+ ShowVertBorder: &attrValBool{Val: boolPtr(true)},
+ ShowOutline: &attrValBool{Val: boolPtr(true)},
+ ShowKeys: &attrValBool{Val: boolPtr(opts.PlotArea.ShowDataTableKeys)},
+ }
+ }
+ return nil
+}
+
// drawPlotAreaSpPr provides a function to draw the c:spPr element.
func (f *File) drawPlotAreaSpPr() *cSpPr {
return &cSpPr{
@@ -1392,7 +1406,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI
}
width = int(float64(width) * opts.ScaleX)
height = int(float64(height) * opts.ScaleY)
- colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
+ colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2 := f.positionObjectPixels(sheet, col, row, width, height, opts)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
@@ -1401,9 +1415,9 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI
twoCellAnchor.EditAs = opts.Positioning
from := xlsxFrom{}
from.Col = colStart
- from.ColOff = opts.OffsetX * EMU
+ from.ColOff = x1 * EMU
from.Row = rowStart
- from.RowOff = opts.OffsetY * EMU
+ from.RowOff = y1 * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
@@ -1452,7 +1466,7 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOpt
absoluteAnchor := xdrCellAnchor{
EditAs: opts.Positioning,
Pos: &xlsxPoint2D{},
- Ext: &aExt{},
+ Ext: &xlsxPositiveSize2D{},
}
graphicFrame := xlsxGraphicFrame{
diff --git a/excelize.go b/excelize.go
index 8448999ab2..61bb6d3489 100644
--- a/excelize.go
+++ b/excelize.go
@@ -31,6 +31,7 @@ type File struct {
mu sync.Mutex
checked sync.Map
formulaChecked bool
+ zip64Entries []string
options *Options
sharedStringItem [][]uint
sharedStringsMap map[string]int
diff --git a/excelize_test.go b/excelize_test.go
index 9684db2cd6..b88911e4c7 100644
--- a/excelize_test.go
+++ b/excelize_test.go
@@ -86,13 +86,13 @@ func TestOpenFile(t *testing.T) {
f.SetActiveSheet(2)
// Test get cell formula with given rows number
- _, err = f.GetCellFormula("Sheet1", "B19")
+ formula, err := f.GetCellFormula("Sheet1", "B19")
assert.NoError(t, err)
+ assert.Equal(t, "SUM(Sheet2!D2,Sheet2!D11)", formula)
// Test get cell formula with illegal worksheet name
- _, err = f.GetCellFormula("Sheet2", "B20")
- assert.NoError(t, err)
- _, err = f.GetCellFormula("Sheet1", "B20")
+ formula, err = f.GetCellFormula("Sheet2", "B20")
assert.NoError(t, err)
+ assert.Empty(t, formula)
// Test get cell formula with illegal rows number
_, err = f.GetCellFormula("Sheet1", "B")
@@ -1060,7 +1060,7 @@ func TestCopySheetError(t *testing.T) {
func TestGetSheetComments(t *testing.T) {
f := NewFile()
- assert.Equal(t, "", f.getSheetComments("sheet0"))
+ assert.Empty(t, f.getSheetComments("sheet0"))
}
func TestGetActiveSheetIndex(t *testing.T) {
@@ -1414,7 +1414,7 @@ func TestProtectSheet(t *testing.T) {
assert.NoError(t, f.UnprotectSheet(sheetName, "password"))
// Test protect worksheet with empty password
assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{}))
- assert.Equal(t, "", ws.SheetProtection.Password)
+ assert.Empty(t, ws.SheetProtection.Password)
// Test protect worksheet with password exceeds the limit length
assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
AlgorithmName: "MD4",
diff --git a/file.go b/file.go
index aa0816c9c2..1ef8a8a5fb 100644
--- a/file.go
+++ b/file.go
@@ -14,8 +14,10 @@ package excelize
import (
"archive/zip"
"bytes"
+ "encoding/binary"
"encoding/xml"
"io"
+ "math"
"os"
"path/filepath"
"sort"
@@ -85,22 +87,23 @@ func (f *File) SaveAs(name string, opts ...Options) error {
// Close closes and cleanup the open temporary file for the spreadsheet.
func (f *File) Close() error {
- var err error
+ var firstErr error
if f.sharedStringTemp != nil {
- if err := f.sharedStringTemp.Close(); err != nil {
- return err
- }
+ firstErr = f.sharedStringTemp.Close()
+ f.sharedStringTemp = nil
+ }
+ for _, stream := range f.streams {
+ _ = stream.rawData.Close()
}
+ f.streams = nil
f.tempFiles.Range(func(k, v interface{}) bool {
- if err = os.Remove(v.(string)); err != nil {
- return false
+ if err := os.Remove(v.(string)); err != nil && firstErr == nil {
+ firstErr = err
}
return true
})
- for _, stream := range f.streams {
- _ = stream.rawData.Close()
- }
- return err
+ f.tempFiles.Clear()
+ return firstErr
}
// Write provides a function to write to an io.Writer.
@@ -123,17 +126,11 @@ func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) {
return 0, err
}
}
- if f.options != nil && f.options.Password != "" {
- buf, err := f.WriteToBuffer()
- if err != nil {
- return 0, err
- }
- return buf.WriteTo(w)
- }
- if err := f.writeDirectToWriter(w); err != nil {
+ buf, err := f.WriteToBuffer()
+ if err != nil {
return 0, err
}
- return 0, nil
+ return buf.WriteTo(w)
}
// WriteToBuffer provides a function to get bytes.Buffer from the saved file,
@@ -143,32 +140,22 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) {
zw := zip.NewWriter(buf)
if err := f.writeToZip(zw); err != nil {
- return buf, zw.Close()
+ _ = zw.Close()
+ return buf, err
}
-
+ if err := zw.Close(); err != nil {
+ return buf, err
+ }
+ f.writeZip64LFH(buf)
if f.options != nil && f.options.Password != "" {
- if err := zw.Close(); err != nil {
- return buf, err
- }
b, err := Encrypt(buf.Bytes(), f.options)
if err != nil {
return buf, err
}
buf.Reset()
buf.Write(b)
- return buf, nil
}
- return buf, zw.Close()
-}
-
-// writeDirectToWriter provides a function to write to io.Writer.
-func (f *File) writeDirectToWriter(w io.Writer) error {
- zw := zip.NewWriter(w)
- if err := f.writeToZip(zw); err != nil {
- _ = zw.Close()
- return err
- }
- return zw.Close()
+ return buf, nil
}
// writeToZip provides a function to write to zip.Writer
@@ -197,11 +184,16 @@ func (f *File) writeToZip(zw *zip.Writer) error {
_ = stream.rawData.Close()
return err
}
- if _, err = io.Copy(fi, from); err != nil {
+ written, err := io.Copy(fi, from)
+ if err != nil {
return err
}
+ if written > math.MaxUint32 {
+ f.zip64Entries = append(f.zip64Entries, path)
+ }
}
var (
+ n int
err error
files, tempFiles []string
)
@@ -219,7 +211,9 @@ func (f *File) writeToZip(zw *zip.Writer) error {
break
}
content, _ := f.Pkg.Load(path)
- _, err = fi.Write(content.([]byte))
+ if n, err = fi.Write(content.([]byte)); int64(n) > math.MaxUint32 {
+ f.zip64Entries = append(f.zip64Entries, path)
+ }
}
f.tempFiles.Range(func(path, content interface{}) bool {
if _, ok := f.Pkg.Load(path); ok {
@@ -234,7 +228,46 @@ func (f *File) writeToZip(zw *zip.Writer) error {
if fi, err = zw.Create(path); err != nil {
break
}
- _, err = fi.Write(f.readBytes(path))
+ if n, err = fi.Write(f.readBytes(path)); int64(n) > math.MaxUint32 {
+ f.zip64Entries = append(f.zip64Entries, path)
+ }
}
return err
}
+
+// writeZip64LFH function sets the ZIP version to 0x2D (45) in the Local File
+// Header (LFH). Excel strictly enforces ZIP64 format validation rules. When any
+// file within the workbook (OCP) exceeds 4GB in size, the ZIP64 format must be
+// used according to the PKZIP specification. However, ZIP files generated using
+// Go's standard archive/zip library always set the version in the local file
+// header to 20 (ZIP version 2.0) by default, as defined in the internal
+// 'writeHeader' function during ZIP creation. The archive/zip package only sets
+// the 'ReaderVersion' to 45 (ZIP64 version 4.5) in the central directory for
+// entries larger than 4GB. This results in a version mismatch between the
+// central directory and the local file header. As a result, opening the
+// generated workbook with spreadsheet application will prompt file corruption.
+func (f *File) writeZip64LFH(buf *bytes.Buffer) error {
+ if len(f.zip64Entries) == 0 {
+ return nil
+ }
+ data, offset := buf.Bytes(), 0
+ for offset < len(data) {
+ idx := bytes.Index(data[offset:], []byte{0x50, 0x4b, 0x03, 0x04})
+ if idx == -1 {
+ break
+ }
+ idx += offset
+ if idx+30 > len(data) {
+ break
+ }
+ filenameLen := int(binary.LittleEndian.Uint16(data[idx+26 : idx+28]))
+ if idx+30+filenameLen > len(data) {
+ break
+ }
+ if inStrSlice(f.zip64Entries, string(data[idx+30:idx+30+filenameLen]), true) != -1 {
+ binary.LittleEndian.PutUint16(data[idx+4:idx+6], 45)
+ }
+ offset = idx + 1
+ }
+ return nil
+}
diff --git a/file_test.go b/file_test.go
index 4272a7b4f1..58c9e4a265 100644
--- a/file_test.go
+++ b/file_test.go
@@ -3,8 +3,11 @@ package excelize
import (
"bufio"
"bytes"
+ "encoding/binary"
+ "math"
"os"
"path/filepath"
+ "strconv"
"strings"
"sync"
"testing"
@@ -95,3 +98,91 @@ func TestClose(t *testing.T) {
f.tempFiles.Store("/d/", "/d/")
require.Error(t, f.Close())
}
+
+func TestZip64(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ sw, err := f.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ for r := range 131 {
+ rowData := make([]interface{}, 1000)
+ for c := range 1000 {
+ rowData[c] = strings.Repeat("c", TotalCellChars)
+ }
+ cell, err := CoordinatesToCellName(1, r+1)
+ assert.NoError(t, err)
+ assert.NoError(t, sw.SetRow(cell, rowData))
+ }
+ assert.NoError(t, sw.Flush())
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestZip64.xlsx")))
+ assert.NoError(t, f.Close())
+
+ // Test with filename length overflow
+ f = NewFile()
+ f.zip64Entries = append(f.zip64Entries, defaultXMLPathSharedStrings)
+ buf := new(bytes.Buffer)
+ buf.Write([]byte{0x50, 0x4b, 0x03, 0x04})
+ buf.Write(make([]byte, 20))
+ assert.NoError(t, f.writeZip64LFH(buf))
+
+ // Test with file header less than the required 30 for the fixed header part
+ f = NewFile()
+ f.zip64Entries = append(f.zip64Entries, defaultXMLPathSharedStrings)
+ buf.Reset()
+ buf.Write([]byte{0x50, 0x4b, 0x03, 0x04})
+ buf.Write(make([]byte, 22))
+ binary.Write(buf, binary.LittleEndian, uint16(10))
+ buf.Write(make([]byte, 2))
+ buf.WriteString("test")
+ assert.NoError(t, f.writeZip64LFH(buf))
+
+ t.Run("for_save_zip64_with_in_memory_file_over_4GB", func(t *testing.T) {
+ // Test save workbook in ZIP64 format with in memory file with size over 4GB.
+ f := NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", make([]byte, math.MaxUint32+1))
+ _, err := f.WriteToBuffer()
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+ })
+
+ t.Run("for_save_zip64_with_in_temporary_file_over_4GB", func(t *testing.T) {
+ // Test save workbook in ZIP64 format with temporary file with size over 4GB.
+ if os.Getenv("GITHUB_ACTIONS") == "true" {
+ t.Skip()
+ }
+ f := NewFile()
+ f.Pkg.Delete("xl/worksheets/sheet1.xml")
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ tmp, err := os.CreateTemp(os.TempDir(), "excelize-")
+ assert.NoError(t, err)
+ assert.NoError(t, tmp.Truncate(math.MaxUint32+1))
+ f.tempFiles.Store("xl/worksheets/sheet1.xml", tmp.Name())
+ assert.NoError(t, tmp.Close())
+ _, err = f.WriteToBuffer()
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+ })
+}
+
+func TestRemoveTempFiles(t *testing.T) {
+ tmp, err := os.CreateTemp("", "excelize-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ tmpName := tmp.Name()
+ tmp.Close()
+ f := NewFile()
+ // fill the tempFiles map with non-existing (erroring on Remove) "files"
+ for i := 0; i < 1000; i++ {
+ f.tempFiles.Store(strconv.Itoa(i), "/hopefully not existing")
+ }
+ f.tempFiles.Store("existing", tmpName)
+
+ require.Error(t, f.Close())
+ if _, err := os.Stat(tmpName); err == nil {
+ t.Errorf("temp file %q still exist", tmpName)
+ os.Remove(tmpName)
+ }
+}
diff --git a/go.mod b/go.mod
index 53b05c8e78..1a01180b1e 100644
--- a/go.mod
+++ b/go.mod
@@ -5,13 +5,13 @@ go 1.23.0
require (
github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.10.0
- github.com/tiendc/go-deepcopy v1.5.1
- github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79
- github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba
- golang.org/x/crypto v0.36.0
+ github.com/tiendc/go-deepcopy v1.6.0
+ github.com/xuri/efp v0.0.1
+ github.com/xuri/nfp v0.0.1
+ golang.org/x/crypto v0.38.0
golang.org/x/image v0.25.0
- golang.org/x/net v0.38.0
- golang.org/x/text v0.23.0
+ golang.org/x/net v0.40.0
+ golang.org/x/text v0.25.0
)
require (
diff --git a/go.sum b/go.sum
index e66a0ba3e4..0bb04b8cee 100644
--- a/go.sum
+++ b/go.sum
@@ -9,20 +9,20 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/tiendc/go-deepcopy v1.5.1 h1:5ymXIB8ReIywehne6oy3HgywC8LicXYucPBNnj5QQxE=
-github.com/tiendc/go-deepcopy v1.5.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
-github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA=
-github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
-github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U=
-github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
+github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
+github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
+github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
+github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/lib.go b/lib.go
index e06e7f5105..113d574fbf 100644
--- a/lib.go
+++ b/lib.go
@@ -80,7 +80,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
// unzipToTemp unzip the zip entity to the system temporary directory and
// returned the unzipped file path.
func (f *File) unzipToTemp(zipFile *zip.File) (string, error) {
- tmp, err := os.CreateTemp(os.TempDir(), "excelize-")
+ tmp, err := os.CreateTemp("", "excelize-")
if err != nil {
return "", err
}
diff --git a/lib_test.go b/lib_test.go
index f6bd94fe64..6a46f1ef1f 100644
--- a/lib_test.go
+++ b/lib_test.go
@@ -95,12 +95,12 @@ func TestColumnNumberToName_OK(t *testing.T) {
func TestColumnNumberToName_Error(t *testing.T) {
out, err := ColumnNumberToName(-1)
if assert.Error(t, err) {
- assert.Equal(t, "", out)
+ assert.Empty(t, out)
}
out, err = ColumnNumberToName(0)
if assert.Error(t, err) {
- assert.Equal(t, "", out)
+ assert.Empty(t, out)
}
_, err = ColumnNumberToName(MaxColumns + 1)
diff --git a/merge_test.go b/merge_test.go
index fcdbcfd647..93a7ff7044 100644
--- a/merge_test.go
+++ b/merge_test.go
@@ -35,7 +35,7 @@ func TestMergeCell(t *testing.T) {
assert.NoError(t, err)
// Merged cell ref is single coordinate
value, err = f.GetCellValue("Sheet2", "A6")
- assert.Equal(t, "", value)
+ assert.Empty(t, value)
assert.NoError(t, err)
value, err = f.GetCellFormula("Sheet1", "G12")
assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value)
@@ -104,7 +104,7 @@ func TestMergeCellOverlap(t *testing.T) {
assert.Len(t, mc, 1)
assert.Equal(t, "A1", mc[0].GetStartAxis())
assert.Equal(t, "D3", mc[0].GetEndAxis())
- assert.Equal(t, "", mc[0].GetCellValue())
+ assert.Empty(t, mc[0].GetCellValue())
assert.NoError(t, f.Close())
}
diff --git a/picture.go b/picture.go
index de0d555870..c6c8c2bd8f 100644
--- a/picture.go
+++ b/picture.go
@@ -366,25 +366,29 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
width = int(float64(width) * opts.ScaleX)
height = int(float64(height) * opts.ScaleY)
}
- colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
+ colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2 := f.positionObjectPixels(sheet, col, row, width, height, opts)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
}
- twoCellAnchor := xdrCellAnchor{}
- twoCellAnchor.EditAs = opts.Positioning
+ cellAnchor := xdrCellAnchor{}
from := xlsxFrom{}
from.Col = colStart
- from.ColOff = opts.OffsetX * EMU
+ from.ColOff = x1 * EMU
from.Row = rowStart
- from.RowOff = opts.OffsetY * EMU
- to := xlsxTo{}
- to.Col = colEnd
- to.ColOff = x2 * EMU
- to.Row = rowEnd
- to.RowOff = y2 * EMU
- twoCellAnchor.From = &from
- twoCellAnchor.To = &to
+ from.RowOff = y1 * EMU
+ cellAnchor.From = &from
+
+ if opts.Positioning != "oneCell" {
+ to := xlsxTo{}
+ to.Col = colEnd
+ to.ColOff = x2 * EMU
+ to.Row = rowEnd
+ to.RowOff = y2 * EMU
+ cellAnchor.To = &to
+ cellAnchor.EditAs = opts.Positioning
+ }
+
pic := xlsxPic{}
pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = opts.LockAspectRatio
pic.NvPicPr.CNvPr.ID = cNvPrID
@@ -413,14 +417,29 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
}
pic.SpPr.PrstGeom.Prst = "rect"
- twoCellAnchor.Pic = &pic
- twoCellAnchor.ClientData = &xdrClientData{
+ if opts.Positioning == "oneCell" {
+ cx := x2 * EMU
+ cy := y2 * EMU
+ cellAnchor.Ext = &xlsxPositiveSize2D{
+ Cx: cx,
+ Cy: cy,
+ }
+ pic.SpPr.Xfrm.Ext.Cx = cx
+ pic.SpPr.Xfrm.Ext.Cy = cy
+ }
+
+ cellAnchor.Pic = &pic
+ cellAnchor.ClientData = &xdrClientData{
FLocksWithSheet: *opts.Locked,
FPrintsWithSheet: *opts.PrintObject,
}
content.mu.Lock()
defer content.mu.Unlock()
- content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
+ if opts.Positioning == "oneCell" {
+ content.OneCellAnchor = append(content.OneCellAnchor, &cellAnchor)
+ } else {
+ content.TwoCellAnchor = append(content.TwoCellAnchor, &cellAnchor)
+ }
f.Drawings.Store(drawingXML, content)
return err
}
@@ -721,8 +740,10 @@ func (f *File) drawingsWriter() {
})
}
-// drawingResize calculate the height and width after resizing.
-func (f *File) drawingResize(sheet, cell string, width, height float64, opts *GraphicOptions) (w, h, c, r int, err error) {
+// GetCellPixelsWithCoordinates returns the pixel dimensions of a specified cell within a given sheet,
+// accounting for merged cells. This function calculates the total pixel width and height
+// for individual or merged cells and provides the column and row index of the cell.
+func (f *File) GetCellPixelsWithCoordinates(sheet, cell string) (cellWidth, cellHeight, c, r int, err error) {
var mergeCells []MergeCell
mergeCells, err = f.GetMergeCells(sheet)
if err != nil {
@@ -733,7 +754,7 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
if c, r, err = CellNameToCoordinates(cell); err != nil {
return
}
- cellWidth, cellHeight := f.getColWidth(sheet, c), f.getRowHeight(sheet, r)
+ cellWidth, cellHeight = f.getColWidth(sheet, c), f.getRowHeight(sheet, r)
for _, mergeCell := range mergeCells {
if inMergeCell {
continue
@@ -753,18 +774,21 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
cellHeight += f.getRowHeight(sheet, row)
}
}
- if float64(cellWidth) < width {
- asp := float64(cellWidth) / width
- width, height = float64(cellWidth), height*asp
- }
- if float64(cellHeight) < height {
- asp := float64(cellHeight) / height
- height, width = float64(cellHeight), width*asp
+ return
+}
+
+// drawingResize calculate the height and width after resizing.
+func (f *File) drawingResize(sheet, cell string, width, height float64, opts *GraphicOptions) (w, h, c, r int, err error) {
+ cellWidth, cellHeight, c, r, err := f.GetCellPixelsWithCoordinates(sheet, cell)
+ if float64(cellWidth) < width || float64(cellHeight) < height {
+ aspWidth := float64(cellWidth) / width
+ aspHeight := float64(cellHeight) / height
+ asp := min(aspWidth, aspHeight)
+ width, height = width*asp, height*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
}
diff --git a/picture_test.go b/picture_test.go
index c0c9075583..38cd5df2dc 100644
--- a/picture_test.go
+++ b/picture_test.go
@@ -42,6 +42,16 @@ func TestAddPicture(t *testing.T) {
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
&GraphicOptions{OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", HyperlinkType: "External", Positioning: "oneCell"}))
+ // Test add pictures to single cell with offsets
+ assert.NoError(t, f.AddPicture("Sheet2", "K22", filepath.Join("test", "images", "excel.jpg"),
+ &GraphicOptions{Positioning: "oneCell"}))
+ assert.NoError(t, f.AddPicture("Sheet2", "K22", filepath.Join("test", "images", "excel.jpg"),
+ &GraphicOptions{OffsetX: 200, Positioning: "oneCell"}))
+ assert.NoError(t, f.AddPicture("Sheet2", "K22", filepath.Join("test", "images", "excel.jpg"),
+ &GraphicOptions{OffsetX: 400, Positioning: "oneCell"}))
+ assert.NoError(t, f.AddPicture("Sheet2", "K22", filepath.Join("test", "images", "excel.jpg"),
+ &GraphicOptions{OffsetX: 600, Positioning: "oneCell"}))
+
file, err := os.ReadFile(filepath.Join("test", "images", "excel.png"))
assert.NoError(t, err)
@@ -83,7 +93,7 @@ func TestAddPicture(t *testing.T) {
// 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.Equal(t, []string{"A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28", "F21"}, cells)
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
@@ -92,7 +102,7 @@ func TestAddPicture(t *testing.T) {
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)
+ assert.Equal(t, []string{"A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28", "F21"}, cells)
// Test get picture cells with unsupported charset
f.Drawings.Delete(path)
f.Pkg.Store(path, MacintoshCyrillicCharset)
diff --git a/rows.go b/rows.go
index 436a5d6abf..5dbfaf86ea 100644
--- a/rows.go
+++ b/rows.go
@@ -139,8 +139,10 @@ func (rows *Rows) Error() error {
// Close closes the open worksheet XML file in the system temporary
// directory.
func (rows *Rows) Close() error {
- if rows.tempFile != nil {
- return rows.tempFile.Close()
+ tempFile := rows.tempFile
+ rows.tempFile = nil
+ if tempFile != nil {
+ return tempFile.Close()
}
return nil
}
@@ -231,7 +233,7 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta
if rowIterator.inElement == "c" {
rowIterator.cellCol++
colCell := xlsxC{}
- _ = rows.decoder.DecodeElement(&colCell, xmlElement)
+ colCell.cellXMLHandler(rows.decoder, xmlElement)
if colCell.R != "" {
if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil {
return
@@ -244,6 +246,63 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta
}
}
+// cellXMLAttrHandler parse the cell XML element attributes of the worksheet.
+func (cell *xlsxC) cellXMLAttrHandler(start *xml.StartElement) error {
+ for _, attr := range start.Attr {
+ switch attr.Name.Local {
+ case "r":
+ cell.R = attr.Value
+ case "s":
+ val, err := strconv.ParseInt(attr.Value, 10, 64)
+ if err != nil {
+ return err
+ }
+ if math.MinInt <= val && val <= math.MaxInt {
+ cell.S = int(val)
+ }
+ case "t":
+ cell.T = attr.Value
+ default:
+ }
+ }
+ return nil
+}
+
+// cellXMLHandler parse the cell XML element of the worksheet.
+func (cell *xlsxC) cellXMLHandler(decoder *xml.Decoder, start *xml.StartElement) error {
+ cell.XMLName = start.Name
+ err := cell.cellXMLAttrHandler(start)
+ if err != nil {
+ return err
+ }
+ for {
+ tok, err := decoder.Token()
+ if err != nil {
+ return err
+ }
+ var se xml.StartElement
+ switch el := tok.(type) {
+ case xml.StartElement:
+ se = el
+ switch se.Name.Local {
+ case "v":
+ err = decoder.DecodeElement(&cell.V, &se)
+ case "f":
+ err = decoder.DecodeElement(&cell.F, &se)
+ case "is":
+ err = decoder.DecodeElement(&cell.IS, &se)
+ }
+ if err != nil {
+ return err
+ }
+ case xml.EndElement:
+ if el == start.End() {
+ return nil
+ }
+ }
+ }
+}
+
// Rows returns a rows iterator, used for streaming reading data for a
// worksheet with a large data. This function is concurrency safe. For
// example:
@@ -309,7 +368,7 @@ func (f *File) getFromStringItem(index int) string {
}()
}
f.sharedStringItem = [][]uint{}
- f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-")
+ f.sharedStringTemp, _ = os.CreateTemp("", "excelize-")
f.tempFiles.Store(defaultTempFileSST, f.sharedStringTemp.Name())
var (
inElement string
@@ -393,12 +452,17 @@ func (f *File) getRowHeight(sheet string, row int) int {
ws, _ := f.workSheetReader(sheet)
ws.mu.Lock()
defer ws.mu.Unlock()
+ height := -1.0
for i := range ws.SheetData.Row {
v := &ws.SheetData.Row[i]
if v.R == row && v.Ht != nil {
- return int(convertRowHeightToPixels(*v.Ht))
+ height = *v.Ht
+ break
}
}
+ if height != -1.0 {
+ return int(convertRowHeightToPixels(height))
+ }
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultRowHeight > 0 {
return int(convertRowHeightToPixels(ws.SheetFormatPr.DefaultRowHeight))
}
@@ -575,11 +639,22 @@ func (f *File) RemoveRow(sheet string, row int) error {
if err != nil {
return err
}
+ ws.formulaSI.Clear()
if row > len(ws.SheetData.Row) {
return f.adjustHelper(sheet, rows, row, -1)
}
+ for rowIdx := range ws.SheetData.Row {
+ v := &ws.SheetData.Row[rowIdx]
+ if v.R == row {
+ for _, c := range v.C {
+ if err := f.removeFormula(&c, ws, sheet); err != nil {
+ return err
+ }
+ }
+ }
+ }
keep := 0
- for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ {
+ for rowIdx := range ws.SheetData.Row {
v := &ws.SheetData.Row[rowIdx]
if v.R != row {
ws.SheetData.Row[keep] = *v
diff --git a/rows_test.go b/rows_test.go
index 01b20a0fcf..acc6105a6a 100644
--- a/rows_test.go
+++ b/rows_test.go
@@ -5,6 +5,7 @@ import (
"encoding/xml"
"fmt"
"path/filepath"
+ "strconv"
"testing"
"github.com/stretchr/testify/assert"
@@ -314,41 +315,27 @@ func TestRemoveRow(t *testing.T) {
assert.EqualError(t, f.RemoveRow(sheet1, 0), newInvalidRowNumberError(0).Error())
assert.NoError(t, f.RemoveRow(sheet1, 4))
- if !assert.Len(t, r.SheetData.Row, rowCount-1) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount-1)
assert.NoError(t, f.MergeCell(sheet1, "B3", "B5"))
assert.NoError(t, f.RemoveRow(sheet1, 2))
- if !assert.Len(t, r.SheetData.Row, rowCount-2) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount-2)
assert.NoError(t, f.RemoveRow(sheet1, 4))
- if !assert.Len(t, r.SheetData.Row, rowCount-3) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount-3)
err = f.AutoFilter(sheet1, "A2:A2", []AutoFilterOptions{{Column: "A", Expression: "x != blanks"}})
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.RemoveRow(sheet1, 1))
- if !assert.Len(t, r.SheetData.Row, rowCount-4) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount-4)
assert.NoError(t, f.RemoveRow(sheet1, 2))
- if !assert.Len(t, r.SheetData.Row, rowCount-5) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount-5)
assert.NoError(t, f.RemoveRow(sheet1, 1))
- if !assert.Len(t, r.SheetData.Row, rowCount-6) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount-6)
assert.NoError(t, f.RemoveRow(sheet1, 10))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx")))
@@ -366,6 +353,14 @@ func TestRemoveRow(t *testing.T) {
assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist")
// Test remove row with invalid sheet name
assert.EqualError(t, f.RemoveRow("Sheet:1", 1), ErrSheetNameInvalid.Error())
+
+ f = NewFile()
+ formulaType, ref := STCellFormulaTypeShared, "C1:C5"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1",
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ f.CalcChain = nil
+ f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.RemoveRow("Sheet1", 1), "XML syntax error on line 1: invalid UTF-8")
}
func TestInsertRows(t *testing.T) {
@@ -382,19 +377,13 @@ func TestInsertRows(t *testing.T) {
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.InsertRows(sheet1, 1, 1))
- if !assert.Len(t, r.SheetData.Row, rowCount+1) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount+1)
assert.NoError(t, f.InsertRows(sheet1, 4, 1))
- if !assert.Len(t, r.SheetData.Row, rowCount+2) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount+2)
assert.NoError(t, f.InsertRows(sheet1, 4, 2))
- if !assert.Len(t, r.SheetData.Row, rowCount+4) {
- t.FailNow()
- }
+ assert.Len(t, r.SheetData.Row, rowCount+4)
// Test insert rows with invalid sheet name
assert.EqualError(t, f.InsertRows("Sheet:1", 1, 1), ErrSheetNameInvalid.Error())
@@ -585,16 +574,16 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) {
val, err := f.GetCellValue(sheet, "A1")
assert.NoError(t, err)
- assert.Equal(t, "", val)
+ assert.Empty(t, val)
val, err = f.GetCellValue(sheet, "B1")
assert.NoError(t, err)
- assert.Equal(t, "", val)
+ assert.Empty(t, val)
val, err = f.GetCellValue(sheet, "A2")
assert.NoError(t, err)
- assert.Equal(t, "", val)
+ assert.Empty(t, val)
val, err = f.GetCellValue(sheet, "B2")
assert.NoError(t, err)
- assert.Equal(t, "", val)
+ assert.Empty(t, val)
assert.NoError(t, err)
expect := map[string]string{
@@ -970,7 +959,7 @@ func TestGetValueFromInlineStr(t *testing.T) {
d := &xlsxSST{}
val, err := c.getValueFrom(f, d, false)
assert.NoError(t, err)
- assert.Equal(t, "", val)
+ assert.Empty(t, val)
}
func TestGetValueFromNumber(t *testing.T) {
@@ -1157,6 +1146,66 @@ func TestNumberFormats(t *testing.T) {
assert.Equal(t, "2019/3/19", result, "A1")
}
+func TestCellXMLHandler(t *testing.T) {
+ var (
+ content = []byte(fmt.Sprintf(`10String
2*A10A32422.30000000000022022-10-22T15:05:29Z
`, NameSpaceSpreadSheet.Value))
+ expected, ws xlsxWorksheet
+ row *xlsxRow
+ )
+ assert.NoError(t, xml.Unmarshal(content, &expected))
+ decoder := xml.NewDecoder(bytes.NewReader(content))
+ rows := Rows{decoder: decoder}
+ for {
+ token, _ := decoder.Token()
+ if token == nil {
+ break
+ }
+ switch element := token.(type) {
+ case xml.StartElement:
+ if element.Name.Local == "row" {
+ r, err := strconv.Atoi(element.Attr[0].Value)
+ assert.NoError(t, err)
+ ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: r})
+ row = &ws.SheetData.Row[len(ws.SheetData.Row)-1]
+ }
+ if element.Name.Local == "c" {
+ colCell := xlsxC{}
+ assert.NoError(t, colCell.cellXMLHandler(rows.decoder, &element))
+ row.C = append(row.C, colCell)
+ }
+ }
+ }
+ assert.Equal(t, expected.SheetData.Row, ws.SheetData.Row)
+
+ for _, rowXML := range []string{
+ `10
`, // s need number
+ `10
`, // missing
+ ``, // incorrect data
+ } {
+ ws := xlsxWorksheet{}
+ content := []byte(fmt.Sprintf(`%s`, NameSpaceSpreadSheet.Value, rowXML))
+ expected := xml.Unmarshal(content, &ws)
+ assert.Error(t, expected)
+ decoder := xml.NewDecoder(bytes.NewReader(content))
+ rows := Rows{decoder: decoder}
+ for {
+ token, _ := decoder.Token()
+ if token == nil {
+ break
+ }
+ switch element := token.(type) {
+ case xml.StartElement:
+ if element.Name.Local == "c" {
+ colCell := xlsxC{}
+ err := colCell.cellXMLHandler(rows.decoder, &element)
+ assert.Error(t, err)
+ assert.Equal(t, expected, err)
+ }
+ }
+ }
+ }
+}
+
func BenchmarkRows(b *testing.B) {
f, _ := OpenFile(filepath.Join("test", "Book1.xlsx"))
for i := 0; i < b.N; i++ {
diff --git a/shape.go b/shape.go
index 1bbf6964d6..b0228748a7 100644
--- a/shape.go
+++ b/shape.go
@@ -331,7 +331,7 @@ func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, 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)
+ colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, w, h, &format)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return content, nil, cNvPrID, err
@@ -340,9 +340,9 @@ func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, width, height
twoCellAnchor.EditAs = format.Positioning
from := xlsxFrom{}
from.Col = colStart
- from.ColOff = format.OffsetX * EMU
+ from.ColOff = x1 * EMU
from.Row = rowStart
- from.RowOff = format.OffsetY * EMU
+ from.RowOff = y1 * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
diff --git a/sheet_test.go b/sheet_test.go
index 48bb423447..d5cc4cfa7b 100644
--- a/sheet_test.go
+++ b/sheet_test.go
@@ -418,8 +418,8 @@ func TestGetSheetName(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "Sheet1", f.GetSheetName(0))
assert.Equal(t, "Sheet2", f.GetSheetName(1))
- assert.Equal(t, "", f.GetSheetName(-1))
- assert.Equal(t, "", f.GetSheetName(2))
+ assert.Empty(t, f.GetSheetName(-1))
+ assert.Empty(t, f.GetSheetName(2))
assert.NoError(t, f.Close())
}
@@ -481,6 +481,8 @@ func TestSetSheetName(t *testing.T) {
assert.Equal(t, "sheet1", f.GetSheetName(0))
// Test set sheet name with invalid sheet name
assert.Equal(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid)
+ _, err := f.NewSheet("Sheet 3")
+ assert.NoError(t, err)
// Test set worksheet name with existing defined name and auto filter
assert.NoError(t, f.AutoFilter("Sheet1", "A1:A2", nil))
@@ -496,8 +498,12 @@ func TestSetSheetName(t *testing.T) {
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.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Name4",
+ RefersTo: "'Sheet 3'!$A1$2:A2",
+ }))
+ assert.NoError(t, f.SetSheetName("Sheet1", "Sheet 2"))
+ for i, expected := range []string{"'Sheet 2'!$A$1:$A$2", "$B$2", "$A1$2:A2", "'Sheet 2'!$A$1:'Sheet 2'!A1:'Sheet 2'!$A$1,'Sheet 2'!A1:Sheet3!A1,Sheet3!A1", "'Sheet 3'!$A1$2:A2"} {
assert.Equal(t, expected, f.WorkBook.DefinedNames.DefinedName[i].Data)
}
}
@@ -519,7 +525,7 @@ func TestWorksheetWriter(t *testing.T) {
func TestGetWorkbookPath(t *testing.T) {
f := NewFile()
f.Pkg.Delete("_rels/.rels")
- assert.Equal(t, "", f.getWorkbookPath())
+ assert.Empty(t, f.getWorkbookPath())
}
func TestGetWorkbookRelsPath(t *testing.T) {
@@ -786,7 +792,7 @@ func TestSheetDimension(t *testing.T) {
assert.NoError(t, err)
dimension, err = f.GetSheetDimension(sheetName)
assert.NoError(t, err)
- assert.Equal(t, "", dimension)
+ assert.Empty(t, dimension)
// Test set the worksheet dimension
for _, excepted := range []string{"A1", "A1:D5", "A1:XFD1048576", "a1", "A1:d5"} {
err = f.SetSheetDimension(sheetName, excepted)
diff --git a/slicer.go b/slicer.go
index 8073cf72ff..c20b053571 100644
--- a/slicer.go
+++ b/slicer.go
@@ -612,7 +612,7 @@ func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *Sli
Name: slicerName,
},
},
- Xfrm: xlsxXfrm{Off: xlsxOff{}, Ext: aExt{}},
+ Xfrm: xlsxXfrm{Off: xlsxOff{}, Ext: xlsxPositiveSize2D{}},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
URI: NameSpaceDrawingMLSlicer.Value,
@@ -632,7 +632,7 @@ func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *Sli
},
},
SpPr: &xlsxSpPr{
- Xfrm: xlsxXfrm{Off: xlsxOff{X: 2914650, Y: 152400}, Ext: aExt{Cx: 1828800, Cy: 2238375}},
+ Xfrm: xlsxXfrm{Off: xlsxOff{X: 2914650, Y: 152400}, Ext: xlsxPositiveSize2D{Cx: 1828800, Cy: 2238375}},
SolidFill: &xlsxInnerXML{Content: ""},
PrstGeom: xlsxPrstGeom{
Prst: "rect",
diff --git a/stream.go b/stream.go
index 89081b8dde..63309ff3bd 100644
--- a/stream.go
+++ b/stream.go
@@ -137,7 +137,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
f.streams[sheetXMLPath] = sw
_, _ = sw.rawData.WriteString(xml.Header + `")
for _, col := range sw.worksheet.Cols.Col {
@@ -694,7 +694,7 @@ func (sw *StreamWriter) writeSheetData() {
func (sw *StreamWriter) Flush() error {
sw.writeSheetData()
_, _ = sw.rawData.WriteString(``)
- bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15)
+ bulkAppendFields(&sw.rawData, sw.worksheet, 9, 16)
mergeCells := strings.Builder{}
if sw.mergeCellsCount > 0 {
_, _ = mergeCells.WriteString(`