Skip to content

Add new exported function GetCellPixelsWithCoordinates #1842

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
01b6d09
Add new exported function `GetCellPixelsWithCoordinates`
t4traw Mar 11, 2024
312e9bb
Merge branch 'qax-os:master' into feature-get-cell-pixels-with-coordi…
t4traw Apr 15, 2025
ce9061f
This fix corrupted workbook generated when any inner ZIP64 file's siz…
xuri Apr 16, 2025
55cf0d4
Improve cell read performance by optimizing XML parsing (#2116)
shcabin Apr 18, 2025
0f19d7f
This closes #2117, support add data table for chart
xuri Apr 21, 2025
b9f2c9e
This closes #1961, add shared formula cell cache for speedup calculat…
shcabin Apr 26, 2025
3ccbdaa
Clean shared formula cell cache on remove formula and simplify adjust…
xuri Apr 27, 2025
efdfc52
This closes #2119, remove all temporary files on close workbook (#2120)
tgulacsi Apr 28, 2025
8fdc26e
This fixes #2001 and fixes #2002. Breaking changes: offsets no longer…
R3dByt3 May 7, 2025
d230ecd
This closes #2126, fix sheet name error in defined name after rename …
R3dByt3 May 8, 2025
094ff39
This related with #2123, made editAs attribute value empty for one ce…
Now-Shimmer May 9, 2025
2c3d13f
Using binary search instead of sequential search for cell (#2129)
shcabin May 12, 2025
4180c20
This closes #2130, remove shared formula in calculation chain when re…
xuri May 12, 2025
72b731a
This closes #2132, fix a v2.9.1 regression bug introduced by commit c…
xuri May 14, 2025
bb1105b
This closes #2133, support delete data validations in the ext list (#…
DengY11 May 17, 2025
c853113
Update comment for xlsxDataValidation data structure (#2142)
JerryLuo-2005 May 24, 2025
53ae16b
Merge branch 'feature-get-cell-pixels-with-coordinates' of ssh://gith…
t4traw May 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 23 additions & 17 deletions adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions adjust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
}
118 changes: 82 additions & 36 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"math"
"os"
"reflect"
"sort"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1471,23 +1499,30 @@ 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
}
if ok {
return val, nil
}
break
}
}
return "", nil
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
Loading