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+`<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)) + + f, err = OpenFile(wb) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+fmt.Sprintf(`<wsDr xmlns="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"><oneCellAnchor><from><col>%d</col><row>0</row></from><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"></mc:AlternateContent><clientData/></oneCellAnchor></wsDr>`, 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(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="2"><c r="B2"><f t="shared" si="0"></f></c></row></sheetData></worksheet>`)) 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("<extLst>" + ws.ExtLst.Ext + "</extLst>")). + 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), "<extLst>"), "</extLst>")} + 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, + `<worksheet xmlns="%s"><sheetData/><extLst><ext xmlns:x14="%s" uri="%s"><x14:dataValidations xmlns:xm="%s" count="2"><x14:dataValidation allowBlank="true" showErrorMessage="true" showInputMessage="true" sqref="" type="list"><xm:sqref>A1:A2</xm:sqref><x14:formula1><xm:f>Sheet1!$A$2:$A$4</xm:f></x14:formula1></x14:dataValidation><x14:dataValidation allowBlank="true" showErrorMessage="true" showInputMessage="true" sqref="" type="list"><xm:sqref>B1:B2</xm:sqref><x14:formula1><xm:f>Sheet1!$B$2:$B$3</xm:f></x14:formula1></x14:dataValidation></x14:dataValidations></ext></extLst></worksheet>`, + 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: "<extLst><x14:dataValidations></x14:dataValidation></x14:dataValidations></ext></extLst>"}, + }, nil), "XML syntax error on line 1: element <dataValidations> closed by </dataValidation>") + }) } 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(`<worksheet xmlns="%s"><sheetData><row r="1"><c r="A1" t="s"><v>10</v></c><c r="B1"><is><t>String</t></is></c></row><row r="2"><c r="A2" s="4" t="str"><f>2*A1</f><v>0</v></c><c r="C2" s="1"><f>A3</f><v>2422.3000000000002</v></c><c r="D2" t="d"><v>2022-10-22T15:05:29Z</v></c><c r="F2"></c><c r="G2"></c></row></sheetData></worksheet>`, 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{ + `<row spans="1:17" r="1"><c r="A1" t="s" s="A"><v>10</v></c></row></sheetData></worksheet>`, // s need number + `<row spans="1:17" r="1"><c r="A1"><v>10</v> </row></sheetData></worksheet>`, // missing </c> + `<row spans="1:17" r="1"><c r="B1"><is><t>`, // incorrect data + } { + ws := xlsxWorksheet{} + content := []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData>%s</sheetData></worksheet>`, 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: "<a:prstClr val=\"white\"/>"}, 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 + `<worksheet` + templateNamespaceIDMap) - bulkAppendFields(&sw.rawData, sw.worksheet, 2, 3) + bulkAppendFields(&sw.rawData, sw.worksheet, 3, 4) return sw, err } @@ -662,7 +662,7 @@ func writeCell(buf *bufferedWriter, c xlsxC) { // sheetData XML start element to the buffer. func (sw *StreamWriter) writeSheetData() { if !sw.sheetWritten { - bulkAppendFields(&sw.rawData, sw.worksheet, 4, 5) + bulkAppendFields(&sw.rawData, sw.worksheet, 5, 6) if sw.worksheet.Cols != nil { _, _ = sw.rawData.WriteString("<cols>") 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(`</sheetData>`) - bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15) + bulkAppendFields(&sw.rawData, sw.worksheet, 9, 16) mergeCells := strings.Builder{} if sw.mergeCellsCount > 0 { _, _ = mergeCells.WriteString(`<mergeCells count="`) @@ -704,9 +704,9 @@ func (sw *StreamWriter) Flush() error { _, _ = mergeCells.WriteString(`</mergeCells>`) } _, _ = sw.rawData.WriteString(mergeCells.String()) - bulkAppendFields(&sw.rawData, sw.worksheet, 17, 38) + bulkAppendFields(&sw.rawData, sw.worksheet, 18, 39) _, _ = sw.rawData.WriteString(sw.tableParts) - bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40) + bulkAppendFields(&sw.rawData, sw.worksheet, 41, 41) _, _ = sw.rawData.WriteString(`</worksheet>`) if err := sw.rawData.Flush(); err != nil { return err @@ -775,7 +775,7 @@ func (bw *bufferedWriter) Sync() (err error) { return nil } if bw.tmp == nil { - bw.tmp, err = os.CreateTemp(os.TempDir(), "excelize-") + bw.tmp, err = os.CreateTemp("", "excelize-") if err != nil { // can not use local storage return nil diff --git a/vml.go b/vml.go index 08d54985d2..4ba0ded2d5 100644 --- a/vml.go +++ b/vml.go @@ -852,7 +852,7 @@ func (f *File) addDrawingVML(sheetID int, drawingVML string, opts *vmlOptions) e leftOffset, vmlID = 0, 201 style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight" } - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.FormControl.Width), int(opts.FormControl.Height)) + colStart, rowStart, colEnd, rowEnd, _, _, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, int(opts.FormControl.Width), int(opts.FormControl.Height), &opts.Format) anchor := fmt.Sprintf("%d, %d, %d, 0, %d, %d, %d, %d", colStart, leftOffset, rowStart, colEnd, x2, rowEnd, y2) if vml == nil { vml = &vmlDrawing{ diff --git a/xmlChart.go b/xmlChart.go index abb0e4adbe..629aa33d1a 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -215,6 +215,17 @@ type aRPr struct { Cs *aCs `xml:"a:cs"` } +// cDTable (Data Table) directly maps the dTable element. +type cDTable struct { + ShowHorzBorder *attrValBool `xml:"showHorzBorder"` + ShowVertBorder *attrValBool `xml:"showVertBorder"` + ShowOutline *attrValBool `xml:"showOutline"` + ShowKeys *attrValBool `xml:"showKeys"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + // cSpPr (Shape Properties) directly maps the spPr element. This element // specifies the visual shape properties that can be applied to a shape. These // properties include the shape fill, outline, geometry, effects, and 3D @@ -319,6 +330,7 @@ type cPlotArea struct { CatAx []*cAxs `xml:"catAx"` ValAx []*cAxs `xml:"valAx"` SerAx []*cAxs `xml:"serAx"` + DTable *cDTable `xml:"dTable"` SpPr *cSpPr `xml:"spPr"` } @@ -559,15 +571,17 @@ type ChartDimension struct { // ChartPlotArea directly maps the format settings of the plot area. type ChartPlotArea struct { - SecondPlotValues int - ShowBubbleSize bool - ShowCatName bool - ShowLeaderLines bool - ShowPercent bool - ShowSerName bool - ShowVal bool - Fill Fill - NumFmt ChartNumFmt + SecondPlotValues int + ShowBubbleSize bool + ShowCatName bool + ShowDataTable bool + ShowDataTableKeys bool + ShowLeaderLines bool + ShowPercent bool + ShowSerName bool + ShowVal bool + Fill Fill + NumFmt ChartNumFmt } // Chart directly maps the format settings of the chart. diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 8a20c5d5c6..747352182e 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -21,6 +21,7 @@ type decodeCellAnchor struct { EditAs string `xml:"editAs,attr,omitempty"` From *decodeFrom `xml:"from"` To *decodeTo `xml:"to"` + Ext *decodePositiveSize2D `xml:"ext"` Sp *decodeSp `xml:"sp"` Pic *decodePic `xml:"pic"` ClientData *decodeClientData `xml:"clientData"` @@ -35,7 +36,7 @@ type decodeCellAnchorPos struct { From *xlsxFrom `xml:"from"` To *xlsxTo `xml:"to"` Pos *xlsxInnerXML `xml:"pos"` - Ext *xlsxInnerXML `xml:"ext"` + Ext *xlsxPositiveSize2D `xml:"ext"` Sp *xlsxSp `xml:"sp"` GrpSp *xlsxInnerXML `xml:"grpSp"` GraphicFrame *xlsxInnerXML `xml:"graphicFrame"` @@ -85,9 +86,9 @@ type decodeSp struct { // shape. This allows for additional information that does not affect the // appearance of the shape to be stored. type decodeNvSpPr struct { - CNvPr *decodeCNvPr `xml:"cNvPr"` - ExtLst *decodeAExt `xml:"extLst"` - CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"` + CNvPr *decodeCNvPr `xml:"cNvPr"` + ExtLst *decodePositiveSize2D `xml:"extLst"` + CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"` } // decodeCNvSpPr (Connection Non-Visual Shape Properties) directly maps the @@ -164,8 +165,8 @@ type decodeOff struct { Y int `xml:"y,attr"` } -// decodeAExt directly maps the a:ext element. -type decodeAExt struct { +// decodePositiveSize2D directly maps the a:ext element. +type decodePositiveSize2D struct { Cx int `xml:"cx,attr"` Cy int `xml:"cy,attr"` } @@ -183,8 +184,8 @@ type decodePrstGeom struct { // frame. This transformation is applied to the graphic frame just as it would // be for a shape or group shape. type decodeXfrm struct { - Off decodeOff `xml:"off"` - Ext decodeAExt `xml:"ext"` + Off decodeOff `xml:"off"` + Ext decodePositiveSize2D `xml:"ext"` } // decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing diff --git a/xmlDrawing.go b/xmlDrawing.go index f363849014..89fe986cec 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -83,8 +83,8 @@ type xlsxOff struct { Y int `xml:"y,attr"` } -// aExt directly maps the a:ext element. -type aExt struct { +// xlsxPositiveSize2D directly maps the a:ext element. +type xlsxPositiveSize2D struct { Cx int `xml:"cx,attr"` Cy int `xml:"cy,attr"` } @@ -102,8 +102,8 @@ type xlsxPrstGeom struct { // frame. This transformation is applied to the graphic frame just as it would // be for a shape or group shape. type xlsxXfrm struct { - Off xlsxOff `xml:"a:off"` - Ext aExt `xml:"a:ext"` + Off xlsxOff `xml:"a:off"` + Ext xlsxPositiveSize2D `xml:"a:ext"` } // xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing @@ -222,7 +222,7 @@ type xdrCellAnchor struct { Pos *xlsxPoint2D `xml:"xdr:pos"` From *xlsxFrom `xml:"xdr:from"` To *xlsxTo `xml:"xdr:to"` - Ext *aExt `xml:"xdr:ext"` + Ext *xlsxPositiveSize2D `xml:"xdr:ext"` Sp *xdrSp `xml:"xdr:sp"` Pic *xlsxPic `xml:"xdr:pic,omitempty"` GraphicFrame string `xml:",innerxml"` @@ -237,7 +237,7 @@ type xlsxCellAnchorPos struct { From *xlsxFrom `xml:"xdr:from"` To *xlsxTo `xml:"xdr:to"` Pos *xlsxInnerXML `xml:"xdr:pos"` - Ext *xlsxInnerXML `xml:"xdr:ext"` + Ext *xlsxPositiveSize2D `xml:"xdr:ext"` Sp *xlsxSp `xml:"xdr:sp"` GrpSp *xlsxInnerXML `xml:"xdr:grpSp"` GraphicFrame *xlsxInnerXML `xml:"xdr:graphicFrame"` diff --git a/xmlStyles.go b/xmlStyles.go index 93ad33cce3..76826c1cc4 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -253,13 +253,13 @@ type xlsxDxfs struct { // xlsxDxf directly maps the dxf element. A single dxf record, expressing // incremental formatting to be applied. type xlsxDxf 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 *aExt `xml:"extLst"` + 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 *xlsxPositiveSize2D `xml:"extLst"` } // xlsxTableStyles directly maps the tableStyles element. This element diff --git a/xmlWorksheet.go b/xmlWorksheet.go index dab4caf321..f0f9e76128 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -20,6 +20,7 @@ import ( // http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { mu sync.Mutex + formulaSI sync.Map XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"` @@ -427,7 +428,7 @@ type xlsxDataValidations struct { DataValidation []*xlsxDataValidation `xml:"dataValidation"` } -// DataValidation directly maps the single item of data validation defined +// xlsxDataValidation directly maps the single item of data validation defined // on a range of the worksheet. type xlsxDataValidation struct { AllowBlank bool `xml:"allowBlank,attr"` @@ -447,6 +448,39 @@ type xlsxDataValidation struct { Formula2 *xlsxInnerXML `xml:"formula2"` } +// xlsxX14DataValidation directly maps the single item of data validation +// defined on a extLst element of the worksheet. +type xlsxX14DataValidation struct { + XMLName xml.Name `xml:"x14:dataValidation"` + AllowBlank bool `xml:"allowBlank,attr"` + Error *string `xml:"error,attr"` + ErrorStyle *string `xml:"errorStyle,attr"` + ErrorTitle *string `xml:"errorTitle,attr"` + Operator string `xml:"operator,attr,omitempty"` + Prompt *string `xml:"prompt,attr"` + PromptTitle *string `xml:"promptTitle,attr"` + ShowDropDown bool `xml:"showDropDown,attr,omitempty"` + ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` + ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` + Sqref string `xml:"sqref,attr"` + Type string `xml:"type,attr,omitempty"` + Formula1 *xlsxInnerXML `xml:"x14:formula1"` + Formula2 *xlsxInnerXML `xml:"x14:formula2"` + XMSqref string `xml:"xm:sqref,omitempty"` +} + +// xlsxX14DataValidations expresses all data validation information for cells in +// a sheet extLst element which have data validation features applied. +type xlsxX14DataValidations struct { + XMLName xml.Name `xml:"x14:dataValidations"` + XMLNSXM string `xml:"xmlns:xm,attr,omitempty"` + Count int `xml:"count,attr,omitempty"` + DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` + XWindow int `xml:"xWindow,attr,omitempty"` + YWindow int `xml:"yWindow,attr,omitempty"` + DataValidation []*xlsxX14DataValidation +} + // xlsxC collection represents a cell in the worksheet. Information about the // cell's location (reference), value, data type, formatting, and formula is // expressed here.