32 changes: 24 additions & 8 deletions adjust.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down Expand Up @@ -53,7 +53,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
return err
}
checkSheet(xlsx)
checkRow(xlsx)
_ = checkRow(xlsx)

if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 {
xlsx.MergeCells = nil
Expand Down Expand Up @@ -196,22 +196,38 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu
// areaRefToCoordinates provides a function to convert area reference to a
// pair of coordinates.
func (f *File) areaRefToCoordinates(ref string) ([]int, error) {
coordinates := make([]int, 4)
rng := strings.Split(ref, ":")
firstCell := rng[0]
lastCell := rng[1]
return areaRangeToCoordinates(rng[0], rng[1])
}

// areaRangeToCoordinates provides a function to convert cell range to a
// pair of coordinates.
func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) {
coordinates := make([]int, 4)
var err error
coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell)
if err != nil {
return coordinates, err
}
coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell)
if err != nil {
return coordinates, err
}
return coordinates, err
}

// sortCoordinates provides a function to correct the coordinate area, such
// correct C1:B3 to B1:C3.
func sortCoordinates(coordinates []int) error {
if len(coordinates) != 4 {
return errors.New("coordinates length must be 4")
}
if coordinates[2] < coordinates[0] {
coordinates[2], coordinates[0] = coordinates[0], coordinates[2]
}
if coordinates[3] < coordinates[1] {
coordinates[3], coordinates[1] = coordinates[1], coordinates[3]
}
return nil
}

// coordinatesToAreaRef provides a function to convert a pair of coordinates
// to area reference.
func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) {
Expand Down
4 changes: 4 additions & 0 deletions adjust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ func TestCoordinatesToAreaRef(t *testing.T) {
assert.NoError(t, err)
assert.EqualValues(t, ref, "A1:A1")
}

func TestSortCoordinates(t *testing.T) {
assert.EqualError(t, sortCoordinates(make([]int, 3)), "coordinates length must be 4")
}
22 changes: 16 additions & 6 deletions calcchain.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -9,16 +9,26 @@

package excelize

import "encoding/xml"
import (
"bytes"
"encoding/xml"
"io"
"log"
)

// calcChainReader provides a function to get the pointer to the structure
// after deserialization of xl/calcChain.xml.
func (f *File) calcChainReader() *xlsxCalcChain {
var err error

if f.CalcChain == nil {
var c xlsxCalcChain
_ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")), &c)
f.CalcChain = &c
f.CalcChain = new(xlsxCalcChain)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))).
Decode(f.CalcChain); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
}

return f.CalcChain
}

Expand Down Expand Up @@ -56,7 +66,7 @@ type xlsxCalcChainCollection []xlsxCalcChainC

// Filter provides a function to filter calculation chain.
func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCalcChainC {
results := make([]xlsxCalcChainC, 0)
var results []xlsxCalcChainC
for _, v := range c {
if fn(v) {
results = append(results, v)
Expand Down
19 changes: 19 additions & 0 deletions calcchain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package excelize

import "testing"

func TestCalcChainReader(t *testing.T) {
f := NewFile()
f.CalcChain = nil
f.XLSX["xl/calcChain.xml"] = MacintoshCyrillicCharset
f.calcChainReader()
}

func TestDeleteCalcChain(t *testing.T) {
f := NewFile()
f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{}}
f.ContentTypes.Overrides = append(f.ContentTypes.Overrides, xlsxOverride{
PartName: "/xl/calcChain.xml",
})
f.deleteCalcChain(1, "A1")
}
197 changes: 102 additions & 95 deletions cell.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down Expand Up @@ -44,7 +44,8 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) {
})
}

// SetCellValue provides a function to set value of a cell. The following
// SetCellValue provides a function to set value of a cell. The specified
// coordinates should not be in the first row of the table. The following
// shows the supported data types:
//
// int
Expand Down Expand Up @@ -82,7 +83,8 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error {
case []byte:
err = f.SetCellStr(sheet, axis, string(v))
case time.Duration:
err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32))
_, d := setCellDuration(v)
err = f.SetCellDefault(sheet, axis, d)
if err != nil {
return err
}
Expand Down Expand Up @@ -130,28 +132,50 @@ func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error {
// setCellTimeFunc provides a method to process time type of value for
// SetCellValue.
func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error {
excelTime, err := timeToExcelTime(value)
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
if excelTime > 0 {
err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64))
if err != nil {
return err
}
cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
if err != nil {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)

var isNum bool
cellData.T, cellData.V, isNum, err = setCellTime(value)
if err != nil {
return err
}
if isNum {
err = f.setDefaultTimeStyle(sheet, axis, 22)
if err != nil {
return err
}
} else {
err = f.SetCellStr(sheet, axis, value.Format(time.RFC3339Nano))
if err != nil {
return err
}
}
return err
}

func setCellTime(value time.Time) (t string, b string, isNum bool, err error) {
var excelTime float64
excelTime, err = timeToExcelTime(value)
if err != nil {
return
}
isNum = excelTime > 0
if isNum {
t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64))
} else {
t, b = setCellDefault(value.Format(time.RFC3339Nano))
}
return
}

func setCellDuration(value time.Duration) (t string, v string) {
v = strconv.FormatFloat(value.Seconds()/86400.0, 'f', -1, 32)
return
}

// SetCellInt provides a function to set int type value of a cell by given
// worksheet name, cell coordinates and cell value.
func (f *File) SetCellInt(sheet, axis string, value int) error {
Expand All @@ -164,11 +188,15 @@ func (f *File) SetCellInt(sheet, axis string, value int) error {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = strconv.Itoa(value)
cellData.T, cellData.V = setCellInt(value)
return err
}

func setCellInt(value int) (t string, v string) {
v = strconv.Itoa(value)
return
}

// SetCellBool provides a function to set bool type value of a cell by given
// worksheet name, cell name and cell value.
func (f *File) SetCellBool(sheet, axis string, value bool) error {
Expand All @@ -181,13 +209,18 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = "b"
cellData.T, cellData.V = setCellBool(value)
return err
}

func setCellBool(value bool) (t string, v string) {
t = "b"
if value {
cellData.V = "1"
v = "1"
} else {
cellData.V = "0"
v = "0"
}
return err
return
}

// SetCellFloat sets a floating point value into a cell. The prec parameter
Expand All @@ -209,11 +242,15 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = strconv.FormatFloat(value, 'f', prec, bitSize)
cellData.T, cellData.V = setCellFloat(value, prec, bitSize)
return err
}

func setCellFloat(value float64, prec, bitSize int) (t string, v string) {
v = strconv.FormatFloat(value, 'f', prec, bitSize)
return
}

// SetCellStr provides a function to set string type value of a cell. Total
// number of characters that a cell can contain 32767 characters.
func (f *File) SetCellStr(sheet, axis, value string) error {
Expand All @@ -225,21 +262,25 @@ func (f *File) SetCellStr(sheet, axis, value string) error {
if err != nil {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T, cellData.V, cellData.XMLSpace = setCellStr(value)
return err
}

func setCellStr(value string) (t string, v string, ns xml.Attr) {
if len(value) > 32767 {
value = value[0:32767]
}
// Leading space(s) character detection.
if len(value) > 0 && value[0] == 32 {
cellData.XMLSpace = xml.Attr{
// Leading and ending space(s) character detection.
if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) {
ns = xml.Attr{
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
Value: "preserve",
}
}

cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = "str"
cellData.V = value
return err
t = "str"
v = value
return
}

// SetCellDefault provides a function to set string type value of a cell as
Expand All @@ -254,11 +295,15 @@ func (f *File) SetCellDefault(sheet, axis, value string) error {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = value
cellData.T, cellData.V = setCellDefault(value)
return err
}

func setCellDefault(value string) (t string, v string) {
v = value
return
}

// GetCellFormula provides a function to get formula from cell by given
// worksheet name and axis in XLSX file.
func (f *File) GetCellFormula(sheet, axis string) (string, error) {
Expand Down Expand Up @@ -395,7 +440,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error {
linkData = xlsxHyperlink{
Ref: axis,
}
sheetPath, _ := f.sheetMap[trimSheetName(sheet)]
sheetPath := f.sheetMap[trimSheetName(sheet)]
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipHyperLink, link, linkType)
linkData.RID = "rId" + strconv.Itoa(rID)
Expand All @@ -412,63 +457,6 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error {
return nil
}

// MergeCell provides a function to merge cells by given coordinate area and
// sheet name. For example create a merged cell of D3:E9 on Sheet1:
//
// err := f.MergeCell("Sheet1", "D3", "E9")
//
// If you create a merged cell that overlaps with another existing merged cell,
// those merged cells that already exist will be removed.
func (f *File) MergeCell(sheet, hcell, vcell string) error {
coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell)
if err != nil {
return err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]

if x1 == x2 && y1 == y2 {
return err
}

// Correct the coordinate area, such correct C1:B3 to B1:C3.
if x2 < x1 {
x1, x2 = x2, x1
}

if y2 < y1 {
y1, y2 = y2, y1
}

hcell, _ = CoordinatesToCellName(x1, y1)
vcell, _ = CoordinatesToCellName(x2, y2)

xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
if xlsx.MergeCells != nil {
ref := hcell + ":" + vcell
// Delete the merged cells of the overlapping area.
for _, cellData := range xlsx.MergeCells.Cells {
cc := strings.Split(cellData.Ref, ":")
if len(cc) != 2 {
return fmt.Errorf("invalid area %q", cellData.Ref)
}
c1, _ := checkCellInArea(hcell, cellData.Ref)
c2, _ := checkCellInArea(vcell, cellData.Ref)
c3, _ := checkCellInArea(cc[0], ref)
c4, _ := checkCellInArea(cc[1], ref)
if !(!c1 && !c2 && !c3 && !c4) {
return nil
}
}
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref})
} else {
xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}}
}
return err
}

// SetSheetRow writes an array to row by given worksheet name, starting
// coordinate and a pointer to array type 'slice'. For example, writes an
// array to row 6 start with the cell B6 on Sheet1:
Expand Down Expand Up @@ -601,7 +589,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error
axis = strings.ToUpper(axis)
if xlsx.MergeCells != nil {
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
ok, err := checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref)
ok, err := f.checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref)
if err != nil {
return axis, err
}
Expand All @@ -615,7 +603,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error

// checkCellInArea provides a function to determine if a given coordinate is
// within an area.
func checkCellInArea(cell, area string) (bool, error) {
func (f *File) checkCellInArea(cell, area string) (bool, error) {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return false, err
Expand All @@ -625,11 +613,30 @@ func checkCellInArea(cell, area string) (bool, error) {
if len(rng) != 2 {
return false, err
}
coordinates, err := f.areaRefToCoordinates(area)
if err != nil {
return false, err
}

return cellInRef([]int{col, row}, coordinates), err
}

firstCol, firstRow, _ := CellNameToCoordinates(rng[0])
lastCol, lastRow, _ := CellNameToCoordinates(rng[1])
// cellInRef provides a function to determine if a given range is within an
// range.
func cellInRef(cell, ref []int) bool {
return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3]
}

return col >= firstCol && col <= lastCol && row >= firstRow && row <= lastRow, err
// isOverlap find if the given two rectangles overlap or not.
func isOverlap(rect1, rect2 []int) bool {
return cellInRef([]int{rect1[0], rect1[1]}, rect2) ||
cellInRef([]int{rect1[2], rect1[1]}, rect2) ||
cellInRef([]int{rect1[0], rect1[3]}, rect2) ||
cellInRef([]int{rect1[2], rect1[3]}, rect2) ||
cellInRef([]int{rect2[0], rect2[1]}, rect1) ||
cellInRef([]int{rect2[2], rect2[1]}, rect1) ||
cellInRef([]int{rect2[0], rect2[3]}, rect1) ||
cellInRef([]int{rect2[2], rect2[3]}, rect1)
}

// getSharedForumula find a cell contains the same formula as another cell,
Expand Down
53 changes: 44 additions & 9 deletions cell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"fmt"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestCheckCellInArea(t *testing.T) {
f := NewFile()
expectedTrueCellInAreaList := [][2]string{
{"c2", "A1:AAZ32"},
{"B9", "A1:B9"},
Expand All @@ -18,7 +20,7 @@ func TestCheckCellInArea(t *testing.T) {
for _, expectedTrueCellInArea := range expectedTrueCellInAreaList {
cell := expectedTrueCellInArea[0]
area := expectedTrueCellInArea[1]
ok, err := checkCellInArea(cell, area)
ok, err := f.checkCellInArea(cell, area)
assert.NoError(t, err)
assert.Truef(t, ok,
"Expected cell %v to be in area %v, got false\n", cell, area)
Expand All @@ -33,13 +35,17 @@ func TestCheckCellInArea(t *testing.T) {
for _, expectedFalseCellInArea := range expectedFalseCellInAreaList {
cell := expectedFalseCellInArea[0]
area := expectedFalseCellInArea[1]
ok, err := checkCellInArea(cell, area)
ok, err := f.checkCellInArea(cell, area)
assert.NoError(t, err)
assert.Falsef(t, ok,
"Expected cell %v not to be inside of area %v, but got true\n", cell, area)
}

ok, err := checkCellInArea("AA0", "Z0:AB1")
ok, err := f.checkCellInArea("A1", "A:B")
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.False(t, ok)

ok, err = f.checkCellInArea("AA0", "Z0:AB1")
assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`)
assert.False(t, ok)
}
Expand All @@ -48,8 +54,8 @@ func TestSetCellFloat(t *testing.T) {
sheet := "Sheet1"
t.Run("with no decimal", func(t *testing.T) {
f := NewFile()
f.SetCellFloat(sheet, "A1", 123.0, -1, 64)
f.SetCellFloat(sheet, "A2", 123.0, 1, 64)
assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.0, -1, 64))
assert.NoError(t, f.SetCellFloat(sheet, "A2", 123.0, 1, 64))
val, err := f.GetCellValue(sheet, "A1")
assert.NoError(t, err)
assert.Equal(t, "123", val, "A1 should be 123")
Expand All @@ -60,25 +66,52 @@ func TestSetCellFloat(t *testing.T) {

t.Run("with a decimal and precision limit", func(t *testing.T) {
f := NewFile()
f.SetCellFloat(sheet, "A1", 123.42, 1, 64)
assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.42, 1, 64))
val, err := f.GetCellValue(sheet, "A1")
assert.NoError(t, err)
assert.Equal(t, "123.4", val, "A1 should be 123.4")
})

t.Run("with a decimal and no limit", func(t *testing.T) {
f := NewFile()
f.SetCellFloat(sheet, "A1", 123.42, -1, 64)
assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.42, -1, 64))
val, err := f.GetCellValue(sheet, "A1")
assert.NoError(t, err)
assert.Equal(t, "123.42", val, "A1 should be 123.42")
})
f := NewFile()
assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
}

func TestSetCellValue(t *testing.T) {
f := NewFile()
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
}

func TestSetCellBool(t *testing.T) {
f := NewFile()
assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
}

func TestGetCellFormula(t *testing.T) {
// Test get cell formula on not exist worksheet.
f := NewFile()
_, err := f.GetCellFormula("SheetN", "A1")
assert.EqualError(t, err, "sheet SheetN is not exist")

// Test get cell formula on no formula cell.
assert.NoError(t, f.SetCellValue("Sheet1", "A1", true))
_, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
}

func ExampleFile_SetCellFloat() {
f := NewFile()
var x = 3.14159265
f.SetCellFloat("Sheet1", "A1", x, 2, 64)
if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil {
println(err.Error())
}
val, _ := f.GetCellValue("Sheet1", "A1")
fmt.Println(val)
// Output: 3.14
Expand All @@ -91,7 +124,9 @@ func BenchmarkSetCellValue(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < len(values); j++ {
f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j])
if err := f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]); err != nil {
b.Error(err)
}
}
}
}
Expand Down
58 changes: 0 additions & 58 deletions cellmerged.go

This file was deleted.

54 changes: 0 additions & 54 deletions cellmerged_test.go

This file was deleted.

1,246 changes: 113 additions & 1,133 deletions chart.go

Large diffs are not rendered by default.

69 changes: 53 additions & 16 deletions chart_test.go

Large diffs are not rendered by default.

166 changes: 114 additions & 52 deletions col.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -13,6 +13,8 @@ import (
"errors"
"math"
"strings"

"github.com/mohae/deepcopy"
)

// Define the default cell size and EMU unit of measurement.
Expand All @@ -26,7 +28,7 @@ const (
// worksheet name and column name. For example, get visible state of column D
// in Sheet1:
//
// visiable, err := f.GetColVisible("Sheet1", "D")
// visible, err := f.GetColVisible("Sheet1", "D")
//
func (f *File) GetColVisible(sheet, col string) (bool, error) {
visible := true
Expand All @@ -52,43 +54,64 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
return visible, err
}

// SetColVisible provides a function to set visible of a single column by given
// worksheet name and column name. For example, hide column D in Sheet1:
// SetColVisible provides a function to set visible columns by given worksheet
// name, columns range and visibility.
//
// For example hide column D on Sheet1:
//
// err := f.SetColVisible("Sheet1", "D", false)
//
func (f *File) SetColVisible(sheet, col string, visible bool) error {
colNum, err := ColumnNameToNumber(col)
// Hide the columns from D to F (included):
//
// err := f.SetColVisible("Sheet1", "D:F", false)
//
func (f *File) SetColVisible(sheet, columns string, visible bool) error {
var max int

colsTab := strings.Split(columns, ":")
min, err := ColumnNameToNumber(colsTab[0])
if err != nil {
return err
}
colData := xlsxCol{
Min: colNum,
Max: colNum,
Hidden: !visible,
CustomWidth: true,
if len(colsTab) == 2 {
max, err = ColumnNameToNumber(colsTab[1])
if err != nil {
return err
}
} else {
max = min
}
if max < min {
min, max = max, min
}
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
colData := xlsxCol{
Min: min,
Max: max,
Width: 9, // default width
Hidden: !visible,
CustomWidth: true,
}
if xlsx.Cols == nil {
cols := xlsxCols{}
cols.Col = append(cols.Col, colData)
xlsx.Cols = &cols
return err
}
for v := range xlsx.Cols.Col {
if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max {
colData = xlsx.Cols.Col[v]
}
}
colData.Min = colNum
colData.Max = colNum
colData.Hidden = !visible
colData.CustomWidth = true
xlsx.Cols.Col = append(xlsx.Cols.Col, colData)
return err
return nil
}
xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.CustomWidth = c.CustomWidth
fc.OutlineLevel = c.OutlineLevel
fc.Phonetic = c.Phonetic
fc.Style = c.Style
fc.Width = c.Width
return fc
})
return nil
}

// GetColOutlineLevel provides a function to get outline level of a single
Expand Down Expand Up @@ -149,16 +172,16 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
xlsx.Cols = &cols
return err
}
for v := range xlsx.Cols.Col {
if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max {
colData = xlsx.Cols.Col[v]
}
}
colData.Min = colNum
colData.Max = colNum
colData.OutlineLevel = level
colData.CustomWidth = true
xlsx.Cols.Col = append(xlsx.Cols.Col, colData)
xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.CustomWidth = c.CustomWidth
fc.Hidden = c.Hidden
fc.Phonetic = c.Phonetic
fc.Style = c.Style
fc.Width = c.Width
return fc
})
return err
}

Expand Down Expand Up @@ -201,21 +224,21 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
if xlsx.Cols == nil {
xlsx.Cols = &xlsxCols{}
}
var find bool
for idx, col := range xlsx.Cols.Col {
if col.Min == min && col.Max == max {
xlsx.Cols.Col[idx].Style = styleID
find = true
}
}
if !find {
xlsx.Cols.Col = append(xlsx.Cols.Col, xlsxCol{
Min: min,
Max: max,
Width: 9,
Style: styleID,
})
}
xlsx.Cols.Col = flatCols(xlsxCol{
Min: min,
Max: max,
Width: 9,
Style: styleID,
}, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.CustomWidth = c.CustomWidth
fc.Hidden = c.Hidden
fc.OutlineLevel = c.OutlineLevel
fc.Phonetic = c.Phonetic
fc.Width = c.Width
return fc
})
return nil
}

Expand Down Expand Up @@ -248,16 +271,55 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error
Width: width,
CustomWidth: true,
}
if xlsx.Cols != nil {
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
} else {
if xlsx.Cols == nil {
cols := xlsxCols{}
cols.Col = append(cols.Col, col)
xlsx.Cols = &cols
return err
}
xlsx.Cols.Col = flatCols(col, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.Hidden = c.Hidden
fc.OutlineLevel = c.OutlineLevel
fc.Phonetic = c.Phonetic
fc.Style = c.Style
return fc
})
return err
}

// flatCols provides a method for the column's operation functions to flatten
// and check the worksheet columns.
func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol {
fc := []xlsxCol{}
for i := col.Min; i <= col.Max; i++ {
c := deepcopy.Copy(col).(xlsxCol)
c.Min, c.Max = i, i
fc = append(fc, c)
}
inFlat := func(colID int, cols []xlsxCol) (int, bool) {
for idx, c := range cols {
if c.Max == colID && c.Min == colID {
return idx, true
}
}
return -1, false
}
for _, column := range cols {
for i := column.Min; i <= column.Max; i++ {
if idx, ok := inFlat(i, fc); ok {
fc[idx] = replacer(fc[idx], column)
continue
}
c := deepcopy.Copy(column).(xlsxCol)
c.Min, c.Max = i, i
fc = append(fc, c)
}
}
return fc
}

// positionObjectPixels calculate the vertices that define the position of a
// graphical object within the worksheet in pixels.
//
Expand Down
88 changes: 67 additions & 21 deletions col_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,99 @@ func TestColumnVisibility(t *testing.T) {
f, err := prepareTestBook1()
assert.NoError(t, err)

// Hide/display a column with SetColVisible
assert.NoError(t, f.SetColVisible("Sheet1", "F", false))
assert.NoError(t, f.SetColVisible("Sheet1", "F", true))
visible, err := f.GetColVisible("Sheet1", "F")
assert.Equal(t, true, visible)
assert.NoError(t, err)

// Test get column visiable on not exists worksheet.
// Test hiding a few columns SetColVisible(...false)...
assert.NoError(t, f.SetColVisible("Sheet1", "F:V", false))
visible, err = f.GetColVisible("Sheet1", "F")
assert.Equal(t, false, visible)
assert.NoError(t, err)
visible, err = f.GetColVisible("Sheet1", "U")
assert.Equal(t, false, visible)
assert.NoError(t, err)
visible, err = f.GetColVisible("Sheet1", "V")
assert.Equal(t, false, visible)
assert.NoError(t, err)
// ...and displaying them back SetColVisible(...true)
assert.NoError(t, f.SetColVisible("Sheet1", "V:F", true))
visible, err = f.GetColVisible("Sheet1", "F")
assert.Equal(t, true, visible)
assert.NoError(t, err)
visible, err = f.GetColVisible("Sheet1", "U")
assert.Equal(t, true, visible)
assert.NoError(t, err)
visible, err = f.GetColVisible("Sheet1", "G")
assert.Equal(t, true, visible)
assert.NoError(t, err)

// Test get column visible on an inexistent worksheet.
_, err = f.GetColVisible("SheetN", "F")
assert.EqualError(t, err, "sheet SheetN is not exist")

// Test get column visiable with illegal cell coordinates.
// Test get column visible with illegal cell coordinates.
_, err = f.GetColVisible("Sheet1", "*")
assert.EqualError(t, err, `invalid column name "*"`)
assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`)

f.NewSheet("Sheet3")
assert.NoError(t, f.SetColVisible("Sheet3", "E", false))

assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), "invalid column name \"-1\"")
assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist")
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx")))
})

t.Run("TestBook3", func(t *testing.T) {
f, err := prepareTestBook3()
assert.NoError(t, err)
f.GetColVisible("Sheet1", "B")
visible, err := f.GetColVisible("Sheet1", "B")
assert.Equal(t, true, visible)
assert.NoError(t, err)
})
}

func TestOutlineLevel(t *testing.T) {
f := NewFile()
f.GetColOutlineLevel("Sheet1", "D")
level, err := f.GetColOutlineLevel("Sheet1", "D")
assert.Equal(t, uint8(0), level)
assert.NoError(t, err)

f.NewSheet("Sheet2")
assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4))
f.GetColOutlineLevel("Sheet1", "D")
f.GetColOutlineLevel("Shee2", "A")

level, err = f.GetColOutlineLevel("Sheet1", "D")
assert.Equal(t, uint8(4), level)
assert.NoError(t, err)

level, err = f.GetColOutlineLevel("Shee2", "A")
assert.Equal(t, uint8(0), level)
assert.EqualError(t, err, "sheet Shee2 is not exist")

assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13))
assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2))
assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7))
assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level")
assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level")
// Test set row outline level on not exists worksheet.
assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is not exist")
// Test get row outline level on not exists worksheet.
_, err = f.GetRowOutlineLevel("SheetN", 1)
assert.EqualError(t, err, "sheet SheetN is not exist")

// Test set and get column outline level with illegal cell coordinates.
assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`)
_, err := f.GetColOutlineLevel("Sheet1", "*")
_, err = f.GetColOutlineLevel("Sheet1", "*")
assert.EqualError(t, err, `invalid column name "*"`)

// Test set column outline level on not exists worksheet.
assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist")

assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0")
level, err := f.GetRowOutlineLevel("Sheet1", 2)
level, err = f.GetRowOutlineLevel("Sheet1", 2)
assert.NoError(t, err)
assert.Equal(t, uint8(7), level)

Expand All @@ -78,7 +119,7 @@ func TestOutlineLevel(t *testing.T) {

f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
f.SetColOutlineLevel("Sheet2", "B", 2)
assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2))
}

func TestSetColStyle(t *testing.T) {
Expand All @@ -100,13 +141,18 @@ func TestSetColStyle(t *testing.T) {

func TestColWidth(t *testing.T) {
f := NewFile()
f.SetColWidth("Sheet1", "B", "A", 12)
f.SetColWidth("Sheet1", "A", "B", 12)
f.GetColWidth("Sheet1", "A")
f.GetColWidth("Sheet1", "C")
assert.NoError(t, f.SetColWidth("Sheet1", "B", "A", 12))
assert.NoError(t, f.SetColWidth("Sheet1", "A", "B", 12))
width, err := f.GetColWidth("Sheet1", "A")
assert.Equal(t, float64(12), width)
assert.NoError(t, err)
width, err = f.GetColWidth("Sheet1", "C")
assert.Equal(t, float64(64), width)
assert.NoError(t, err)

// Test set and get column width with illegal cell coordinates.
_, err := f.GetColWidth("Sheet1", "*")
width, err = f.GetColWidth("Sheet1", "*")
assert.Equal(t, float64(64), width)
assert.EqualError(t, err, `invalid column name "*"`)
assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`)
assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`)
Expand All @@ -128,8 +174,8 @@ func TestInsertCol(t *testing.T) {

fillCells(f, sheet1, 10, 10)

f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
f.MergeCell(sheet1, "A1", "C3")
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
assert.NoError(t, f.MergeCell(sheet1, "A1", "C3"))

assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`))
assert.NoError(t, f.InsertCol(sheet1, "A"))
Expand All @@ -146,11 +192,11 @@ func TestRemoveCol(t *testing.T) {

fillCells(f, sheet1, 10, 15)

f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External")
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External"))

f.MergeCell(sheet1, "A1", "B1")
f.MergeCell(sheet1, "A2", "B2")
assert.NoError(t, f.MergeCell(sheet1, "A1", "B1"))
assert.NoError(t, f.MergeCell(sheet1, "A2", "B2"))

assert.NoError(t, f.RemoveCol(sheet1, "A"))
assert.NoError(t, f.RemoveCol(sheet1, "A"))
Expand Down
40 changes: 25 additions & 15 deletions comment.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -10,9 +10,12 @@
package excelize

import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"log"
"strconv"
"strings"
)
Expand Down Expand Up @@ -98,8 +101,7 @@ func (f *File) AddComment(sheet, cell, format string) error {
drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
} else {
// Add first comment for given sheet.
sheetPath, _ := f.sheetMap[trimSheetName(sheet)]
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels"
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
f.addSheetLegacyDrawing(sheet, rID)
Expand Down Expand Up @@ -253,23 +255,23 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
{
RPr: &xlsxRPr{
B: " ",
Sz: &attrValFloat{Val: 9},
Sz: &attrValFloat{Val: float64Ptr(9)},
Color: &xlsxColor{
Indexed: 81,
},
RFont: &attrValString{Val: defaultFont},
Family: &attrValInt{Val: 2},
RFont: &attrValString{Val: stringPtr(defaultFont)},
Family: &attrValInt{Val: intPtr(2)},
},
T: a,
},
{
RPr: &xlsxRPr{
Sz: &attrValFloat{Val: 9},
Sz: &attrValFloat{Val: float64Ptr(9)},
Color: &xlsxColor{
Indexed: 81,
},
RFont: &attrValString{Val: defaultFont},
Family: &attrValInt{Val: 2},
RFont: &attrValString{Val: stringPtr(defaultFont)},
Family: &attrValInt{Val: intPtr(2)},
},
T: t,
},
Expand Down Expand Up @@ -303,12 +305,16 @@ func (f *File) countComments() int {
// decodeVMLDrawingReader provides a function to get the pointer to the
// structure after deserialization of xl/drawings/vmlDrawing%d.xml.
func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing {
var err error

if f.DecodeVMLDrawing[path] == nil {
c, ok := f.XLSX[path]
if ok {
d := decodeVmlDrawing{}
_ = xml.Unmarshal(namespaceStrictToTransitional(c), &d)
f.DecodeVMLDrawing[path] = &d
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))).
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
}
}
return f.DecodeVMLDrawing[path]
Expand All @@ -328,12 +334,16 @@ func (f *File) vmlDrawingWriter() {
// commentsReader provides a function to get the pointer to the structure
// after deserialization of xl/comments%d.xml.
func (f *File) commentsReader(path string) *xlsxComments {
var err error

if f.Comments[path] == nil {
content, ok := f.XLSX[path]
if ok {
c := xlsxComments{}
_ = xml.Unmarshal(namespaceStrictToTransitional(content), &c)
f.Comments[path] = &c
f.Comments[path] = new(xlsxComments)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))).
Decode(f.Comments[path]); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
}
}
return f.Comments[path]
Expand Down
56 changes: 56 additions & 0 deletions comment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to
// and read from XLSX files. Support reads and writes XLSX file generated by
// Microsoft Excelâ„¢ 2007 and later. Support save file without losing original
// charts of XLSX. This library needs Go version 1.10 or later.

package excelize

import (
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAddComments(t *testing.T) {
f, err := prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}

s := strings.Repeat("c", 32768)
assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`))
assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`))

// Test add comment on not exists worksheet.
assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist")

if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) {
assert.Len(t, f.GetComments(), 2)
}
}

func TestDecodeVMLDrawingReader(t *testing.T) {
f := NewFile()
path := "xl/drawings/vmlDrawing1.xml"
f.XLSX[path] = MacintoshCyrillicCharset
f.decodeVMLDrawingReader(path)
}

func TestCommentsReader(t *testing.T) {
f := NewFile()
path := "xl/comments1.xml"
f.XLSX[path] = MacintoshCyrillicCharset
f.commentsReader(path)
}

func TestCountComments(t *testing.T) {
f := NewFile()
f.Comments["xl/comments1.xml"] = nil
assert.Equal(t, f.countComments(), 1)
}
2 changes: 1 addition & 1 deletion datavalidation.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down
57 changes: 28 additions & 29 deletions datavalidation_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -11,6 +11,7 @@ package excelize

import (
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -23,53 +24,45 @@ func TestDataValidation(t *testing.T) {

dvRange := NewDataValidation(true)
dvRange.Sqref = "A1:B2"
dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)
assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body")
dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body")
dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body")
f.AddDataValidation("Sheet1", dvRange)
if !assert.NoError(t, f.SaveAs(resultFile)) {
t.FailNow()
}
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
assert.NoError(t, f.SaveAs(resultFile))

dvRange = NewDataValidation(true)
dvRange.Sqref = "A3:B4"
dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)
assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
dvRange.SetInput("input title", "input body")
f.AddDataValidation("Sheet1", dvRange)
if !assert.NoError(t, f.SaveAs(resultFile)) {
t.FailNow()
}
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
assert.NoError(t, f.SaveAs(resultFile))

dvRange = NewDataValidation(true)
dvRange.Sqref = "A5:B6"
dvRange.SetDropList([]string{"1", "2", "3"})
f.AddDataValidation("Sheet1", dvRange)
if !assert.NoError(t, f.SaveAs(resultFile)) {
t.FailNow()
}
assert.NoError(t, dvRange.SetDropList([]string{"1", "2", "3"}))
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
assert.NoError(t, f.SaveAs(resultFile))
}

func TestDataValidationError(t *testing.T) {
resultFile := filepath.Join("test", "TestDataValidationError.xlsx")

f := NewFile()
f.SetCellStr("Sheet1", "E1", "E1")
f.SetCellStr("Sheet1", "E2", "E2")
f.SetCellStr("Sheet1", "E3", "E3")
assert.NoError(t, f.SetCellStr("Sheet1", "E1", "E1"))
assert.NoError(t, f.SetCellStr("Sheet1", "E2", "E2"))
assert.NoError(t, f.SetCellStr("Sheet1", "E3", "E3"))

dvRange := NewDataValidation(true)
dvRange.SetSqref("A7:B8")
dvRange.SetSqref("A7:B8")
dvRange.SetSqrefDropList("$E$1:$E$3", true)
assert.NoError(t, dvRange.SetSqrefDropList("$E$1:$E$3", true))

err := dvRange.SetSqrefDropList("$E$1:$E$3", false)
assert.EqualError(t, err, "cross-sheet sqref cell are not supported")

f.AddDataValidation("Sheet1", dvRange)
if !assert.NoError(t, f.SaveAs(resultFile)) {
t.FailNow()
}
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
assert.NoError(t, f.SaveAs(resultFile))

dvRange = NewDataValidation(true)
err = dvRange.SetDropList(make([]string, 258))
Expand All @@ -78,11 +71,17 @@ func TestDataValidationError(t *testing.T) {
return
}
assert.EqualError(t, err, "data validation must be 0-255 characters")
dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)
assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
dvRange.SetSqref("A9:B10")

f.AddDataValidation("Sheet1", dvRange)
if !assert.NoError(t, f.SaveAs(resultFile)) {
t.FailNow()
}
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
assert.NoError(t, f.SaveAs(resultFile))

// Test width invalid data validation formula.
dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22)
assert.EqualError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan), "data validation must be 0-255 characters")

// Test add data validation on no exists worksheet.
f = NewFile()
assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist")
}
2 changes: 1 addition & 1 deletion date.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down
68 changes: 42 additions & 26 deletions docProps.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -10,7 +10,10 @@
package excelize

import (
"bytes"
"encoding/xml"
"fmt"
"io"
"reflect"
)

Expand Down Expand Up @@ -65,13 +68,23 @@ import (
// Version: "1.0.0",
// })
//
func (f *File) SetDocProps(docProperties *DocProperties) error {
core := decodeCoreProperties{}
err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core)
if err != nil {
return err
func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
var (
core *decodeCoreProperties
newProps *xlsxCoreProperties
fields []string
output []byte
immutable, mutable reflect.Value
field, val string
)

core = new(decodeCoreProperties)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
Decode(core); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
}
newProps := xlsxCoreProperties{
newProps, err = &xlsxCoreProperties{
Dc: NameSpaceDublinCore,
Dcterms: NameSpaceDublinCoreTerms,
Dcmitype: NameSpaceDublinCoreMetadataIntiative,
Expand All @@ -88,18 +101,16 @@ func (f *File) SetDocProps(docProperties *DocProperties) error {
ContentStatus: core.ContentStatus,
Category: core.Category,
Version: core.Version,
}, nil
newProps.Created.Text, newProps.Created.Type, newProps.Modified.Text, newProps.Modified.Type =
core.Created.Text, core.Created.Type, core.Modified.Text, core.Modified.Type
fields = []string{
"Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords",
"LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version",
}
newProps.Created.Text = core.Created.Text
newProps.Created.Type = core.Created.Type
newProps.Modified.Text = core.Modified.Text
newProps.Modified.Type = core.Modified.Type

fields := []string{"Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version"}
immutable := reflect.ValueOf(*docProperties)
mutable := reflect.ValueOf(&newProps).Elem()
for _, field := range fields {
val := immutable.FieldByName(field).String()
if val != "" {
immutable, mutable = reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem()
for _, field = range fields {
if val = immutable.FieldByName(field).String(); val != "" {
mutable.FieldByName(field).SetString(val)
}
}
Expand All @@ -109,19 +120,22 @@ func (f *File) SetDocProps(docProperties *DocProperties) error {
if docProperties.Modified != "" {
newProps.Modified.Text = docProperties.Modified
}
output, err := xml.Marshal(&newProps)
output, err = xml.Marshal(newProps)
f.saveFileList("docProps/core.xml", output)
return err

return
}

// GetDocProps provides a function to get document core properties.
func (f *File) GetDocProps() (*DocProperties, error) {
core := decodeCoreProperties{}
err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core)
if err != nil {
return nil, err
func (f *File) GetDocProps() (ret *DocProperties, err error) {
var core = new(decodeCoreProperties)

if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
Decode(core); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
}
return &DocProperties{
ret, err = &DocProperties{
Category: core.Category,
ContentStatus: core.ContentStatus,
Created: core.Created.Text,
Expand All @@ -137,4 +151,6 @@ func (f *File) GetDocProps() (*DocProperties, error) {
Language: core.Language,
Version: core.Version,
}, nil

return
}
19 changes: 16 additions & 3 deletions docProps_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -16,6 +16,8 @@ import (
"github.com/stretchr/testify/assert"
)

var MacintoshCyrillicCharset = []byte{0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0}

func TestSetDocProps(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) {
Expand All @@ -39,7 +41,12 @@ func TestSetDocProps(t *testing.T) {
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx")))
f.XLSX["docProps/core.xml"] = nil
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "EOF")
assert.NoError(t, f.SetDocProps(&DocProperties{}))

// Test unsupport charset
f = NewFile()
f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
}

func TestGetDocProps(t *testing.T) {
Expand All @@ -52,5 +59,11 @@ func TestGetDocProps(t *testing.T) {
assert.Equal(t, props.Creator, "Microsoft Office User")
f.XLSX["docProps/core.xml"] = nil
_, err = f.GetDocProps()
assert.EqualError(t, err, "EOF")
assert.NoError(t, err)

// Test unsupport charset
f = NewFile()
f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset
_, err = f.GetDocProps()
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
}
1,252 changes: 1,252 additions & 0 deletions drawing.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion errors.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down
133 changes: 81 additions & 52 deletions excelize.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.

Expand All @@ -22,6 +22,8 @@ import (
"path"
"strconv"
"strings"

"golang.org/x/net/html/charset"
)

// File define a populated XLSX file struct.
Expand All @@ -43,8 +45,11 @@ type File struct {
WorkBook *xlsxWorkbook
Relationships map[string]*xlsxRelationships
XLSX map[string][]byte
CharsetReader charsetTranscoderFn
}

type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)

// OpenFile take the name of an XLSX file and returns a populated XLSX file
// struct for it.
func OpenFile(filename string) (*File, error) {
Expand All @@ -61,6 +66,21 @@ func OpenFile(filename string) (*File, error) {
return f, nil
}

// newFile is object builder
func newFile() *File {
return &File{
checked: make(map[string]bool),
sheetMap: make(map[string]string),
Comments: make(map[string]*xlsxComments),
Drawings: make(map[string]*xlsxWsDr),
Sheet: make(map[string]*xlsxWorksheet),
DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
VMLDrawing: make(map[string]*vmlDrawing),
Relationships: make(map[string]*xlsxRelationships),
CharsetReader: charset.NewReaderLabel,
}
}

// OpenReader take an io.Reader and return a populated XLSX file.
func OpenReader(r io.Reader) (*File, error) {
b, err := ioutil.ReadAll(r)
Expand Down Expand Up @@ -88,24 +108,26 @@ func OpenReader(r io.Reader) (*File, error) {
if err != nil {
return nil, err
}
f := &File{
checked: make(map[string]bool),
Comments: make(map[string]*xlsxComments),
Drawings: make(map[string]*xlsxWsDr),
Sheet: make(map[string]*xlsxWorksheet),
SheetCount: sheetCount,
DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
VMLDrawing: make(map[string]*vmlDrawing),
Relationships: make(map[string]*xlsxRelationships),
XLSX: file,
}
f := newFile()
f.SheetCount, f.XLSX = sheetCount, file
f.CalcChain = f.calcChainReader()
f.sheetMap = f.getSheetMap()
f.Styles = f.stylesReader()
f.Theme = f.themeReader()
return f, nil
}

// CharsetTranscoder Set user defined codepage transcoder function for open
// XLSX from non UTF-8 encoding.
func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f }

// Creates new XML decoder with charset reader.
func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
ret = xml.NewDecoder(rdr)
ret.CharsetReader = f.CharsetReader
return
}

// setDefaultTimeStyle provides a function to set default numbers format for
// time.Time type cell value by given worksheet name, cell coordinates and
// number format code.
Expand All @@ -116,33 +138,45 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error {
}
if s == 0 {
style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`)
f.SetCellStyle(sheet, axis, axis, style)
_ = f.SetCellStyle(sheet, axis, axis, style)
}
return err
}

// workSheetReader provides a function to get the pointer to the structure
// after deserialization by given worksheet name.
func (f *File) workSheetReader(sheet string) (*xlsxWorksheet, error) {
name, ok := f.sheetMap[trimSheetName(sheet)]
if !ok {
return nil, fmt.Errorf("sheet %s is not exist", sheet)
func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) {
var (
name string
ok bool
)

if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok {
err = fmt.Errorf("sheet %s is not exist", sheet)
return
}
if f.Sheet[name] == nil {
var xlsx xlsxWorksheet
_ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(name)), &xlsx)
if xlsx = f.Sheet[name]; f.Sheet[name] == nil {
xlsx = new(xlsxWorksheet)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))).
Decode(xlsx); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
}
err = nil
if f.checked == nil {
f.checked = make(map[string]bool)
}
ok := f.checked[name]
if !ok {
checkSheet(&xlsx)
checkRow(&xlsx)
if ok = f.checked[name]; !ok {
checkSheet(xlsx)
if err = checkRow(xlsx); err != nil {
return
}
f.checked[name] = true
}
f.Sheet[name] = &xlsx
f.Sheet[name] = xlsx
}
return f.Sheet[name], nil

return
}

// checkSheet provides a function to fill each row element and make that is
Expand All @@ -155,20 +189,12 @@ func checkSheet(xlsx *xlsxWorksheet) {
row = lastRow
}
}
sheetData := xlsxSheetData{}
existsRows := map[int]int{}
for k := range xlsx.SheetData.Row {
existsRows[xlsx.SheetData.Row[k].R] = k
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
for _, r := range xlsx.SheetData.Row {
sheetData.Row[r.R-1] = r
}
for i := 0; i < row; i++ {
_, ok := existsRows[i+1]
if ok {
sheetData.Row = append(sheetData.Row, xlsx.SheetData.Row[existsRows[i+1]])
} else {
sheetData.Row = append(sheetData.Row, xlsxRow{
R: i + 1,
})
}
for i := 1; i <= row; i++ {
sheetData.Row[i-1].R = i
}
xlsx.SheetData = sheetData
}
Expand All @@ -177,11 +203,17 @@ func checkSheet(xlsx *xlsxWorksheet) {
// relationship type, target and target mode.
func (f *File) addRels(relPath, relType, target, targetMode string) int {
rels := f.relsReader(relPath)
rID := 0
if rels == nil {
rels = &xlsxRelationships{}
}
rID = len(rels.Relationships) + 1
var rID int
for _, rel := range rels.Relationships {
ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
if ID > rID {
rID = ID
}
}
rID++
var ID bytes.Buffer
ID.WriteString("rId")
ID.WriteString(strconv.Itoa(rID))
Expand All @@ -200,7 +232,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
// Office Excel 2007.
func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte {
var oldXmlns = []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
var newXmlns = []byte(`<worksheet xr:uid="{00000000-0001-0000-0000-000000000000}" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" mc:Ignorable="x14ac xr xr2 xr3 xr6 xr10 x15" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
var newXmlns = []byte(`<worksheet` + templateNamespaceIDMap)
workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
return workbookMarshal
}
Expand All @@ -210,7 +242,7 @@ func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte
// Excel 2007.
func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte {
var oldXmlns = []byte(`<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
var newXmlns = []byte(`<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac x16r2 xr xr9" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9">`)
var newXmlns = []byte(`<styleSheet` + templateNamespaceIDMap)
contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1)
return contentMarshal
}
Expand Down Expand Up @@ -261,17 +293,14 @@ func (f *File) UpdateLinkedValue() error {
// AddVBAProject provides the method to add vbaProject.bin file which contains
// functions and/or macros. The file extension should be .xlsm. For example:
//
// err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1"))
// if err != nil {
// fmt.Println(err)
// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil {
// println(err.Error())
// }
// err = f.AddVBAProject("vbaProject.bin")
// if err != nil {
// fmt.Println(err)
// if err := f.AddVBAProject("vbaProject.bin"); err != nil {
// println(err.Error())
// }
// err = f.SaveAs("macros.xlsm")
// if err != nil {
// fmt.Println(err)
// if err := f.SaveAs("macros.xlsm"); err != nil {
// println(err.Error())
// }
//
func (f *File) AddVBAProject(bin string) error {
Expand Down
498 changes: 246 additions & 252 deletions excelize_test.go

Large diffs are not rendered by default.

10 changes: 3 additions & 7 deletions file.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down Expand Up @@ -33,12 +33,8 @@ func NewFile() *File {
file["xl/styles.xml"] = []byte(XMLHeader + templateStyles)
file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook)
file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes)
f := &File{
sheetMap: make(map[string]string),
Sheet: make(map[string]*xlsxWorksheet),
SheetCount: 1,
XLSX: file,
}
f := newFile()
f.SheetCount, f.XLSX = 1, file
f.CalcChain = f.calcChainReader()
f.Comments = make(map[string]*xlsxComments)
f.ContentTypes = f.contentTypesReader()
Expand Down
28 changes: 28 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package excelize

import (
"testing"
)

func BenchmarkWrite(b *testing.B) {
const s = "This is test data"
for i := 0; i < b.N; i++ {
f := NewFile()
for row := 1; row <= 10000; row++ {
for col := 1; col <= 20; col++ {
val, err := CoordinatesToCellName(col, row)
if err != nil {
b.Error(err)
}
if err := f.SetCellDefault("Sheet1", val, s); err != nil {
b.Error(err)
}
}
}
// Save xlsx file by the given path.
err := f.SaveAs("./test.xlsx")
if err != nil {
b.Error(err)
}
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/stretchr/testify v1.3.0
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
golang.org/x/text v0.3.2 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
22 changes: 15 additions & 7 deletions lib.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -22,14 +22,12 @@ import (
// ReadZipReader can be used to read an XLSX in memory without touching the
// filesystem.
func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
fileList := make(map[string][]byte)
fileList := make(map[string][]byte, len(r.File))
worksheets := 0
for _, v := range r.File {
fileList[v.Name] = readFile(v)
if len(v.Name) > 18 {
if v.Name[0:19] == "xl/worksheets/sheet" {
worksheets++
}
if strings.HasPrefix(v.Name, "xl/worksheets/sheet") {
worksheets++
}
}
return fileList, worksheets, nil
Expand Down Expand Up @@ -58,7 +56,8 @@ func readFile(file *zip.File) []byte {
if err != nil {
log.Fatal(err)
}
buff := bytes.NewBuffer(nil)
dat := make([]byte, 0, file.FileInfo().Size())
buff := bytes.NewBuffer(dat)
_, _ = io.Copy(buff, rc)
rc.Close()
return buff.Bytes()
Expand Down Expand Up @@ -199,6 +198,15 @@ func CoordinatesToCellName(col, row int) (string, error) {
// boolPtr returns a pointer to a bool with the given value.
func boolPtr(b bool) *bool { return &b }

// intPtr returns a pointer to a int with the given value.
func intPtr(i int) *int { return &i }

// float64Ptr returns a pofloat64er to a float64 with the given value.
func float64Ptr(f float64) *float64 { return &f }

// stringPtr returns a pointer to a string with the given value.
func stringPtr(s string) *string { return &s }

// defaultTrue returns true if b is nil, or the pointed value.
func defaultTrue(b *bool) bool {
if b == nil {
Expand Down
194 changes: 194 additions & 0 deletions merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to
// and read from XLSX files. Support reads and writes XLSX file generated by
// Microsoft Excelâ„¢ 2007 and later. Support save file without losing original
// charts of XLSX. This library needs Go version 1.10 or later.

package excelize

import (
"fmt"
"strings"
)

// MergeCell provides a function to merge cells by given coordinate area and
// sheet name. For example create a merged cell of D3:E9 on Sheet1:
//
// err := f.MergeCell("Sheet1", "D3", "E9")
//
// If you create a merged cell that overlaps with another existing merged cell,
// those merged cells that already exist will be removed.
//
// B1(x1,y1) D1(x2,y1)
// +------------------------+
// | |
// A4(x3,y3) | C4(x4,y3) |
// +------------------------+ |
// | | | |
// | |B5(x1,y2) | D5(x2,y2)|
// | +------------------------+
// | |
// |A8(x3,y4) C8(x4,y4)|
// +------------------------+
//
func (f *File) MergeCell(sheet, hcell, vcell string) error {
rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell)
if err != nil {
return err
}
// Correct the coordinate area, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect1)

hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])

xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
ref := hcell + ":" + vcell
if xlsx.MergeCells != nil {
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
cellData := xlsx.MergeCells.Cells[i]
if cellData == nil {
continue
}
cc := strings.Split(cellData.Ref, ":")
if len(cc) != 2 {
return fmt.Errorf("invalid area %q", cellData.Ref)
}

rect2, err := f.areaRefToCoordinates(cellData.Ref)
if err != nil {
return err
}

// Delete the merged cells of the overlapping area.
if isOverlap(rect1, rect2) {
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
i--

if rect1[0] > rect2[0] {
rect1[0], rect2[0] = rect2[0], rect1[0]
}

if rect1[2] < rect2[2] {
rect1[2], rect2[2] = rect2[2], rect1[2]
}

if rect1[1] > rect2[1] {
rect1[1], rect2[1] = rect2[1], rect1[1]
}

if rect1[3] < rect2[3] {
rect1[3], rect2[3] = rect2[3], rect1[3]
}
hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])
ref = hcell + ":" + vcell
}
}
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref})
} else {
xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}}
}
return err
}

// UnmergeCell provides a function to unmerge a given coordinate area.
// For example unmerge area D3:E9 on Sheet1:
//
// err := f.UnmergeCell("Sheet1", "D3", "E9")
//
// Attention: overlapped areas will also be unmerged.
func (f *File) UnmergeCell(sheet string, hcell, vcell string) error {
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell)
if err != nil {
return err
}

// Correct the coordinate area, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect1)

// return nil since no MergeCells in the sheet
if xlsx.MergeCells == nil {
return nil
}

i := 0
for _, cellData := range xlsx.MergeCells.Cells {
if cellData == nil {
continue
}
cc := strings.Split(cellData.Ref, ":")
if len(cc) != 2 {
return fmt.Errorf("invalid area %q", cellData.Ref)
}

rect2, err := f.areaRefToCoordinates(cellData.Ref)
if err != nil {
return err
}

if isOverlap(rect1, rect2) {
continue
}
xlsx.MergeCells.Cells[i] = cellData
i++
}
xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i]
return nil
}

// GetMergeCells provides a function to get all merged cells from a worksheet
// currently.
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
var mergeCells []MergeCell
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return mergeCells, err
}
if xlsx.MergeCells != nil {
mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells))

for i := range xlsx.MergeCells.Cells {
ref := xlsx.MergeCells.Cells[i].Ref
axis := strings.Split(ref, ":")[0]
val, _ := f.GetCellValue(sheet, axis)
mergeCells = append(mergeCells, []string{ref, val})
}
}

return mergeCells, err
}

// MergeCell define a merged cell data.
// It consists of the following structure.
// example: []string{"D4:E10", "cell value"}
type MergeCell []string

// GetCellValue returns merged cell value.
func (m *MergeCell) GetCellValue() string {
return (*m)[1]
}

// GetStartAxis returns the merge start axis.
// example: "C2"
func (m *MergeCell) GetStartAxis() string {
axis := strings.Split((*m)[0], ":")
return axis[0]
}

// GetEndAxis returns the merge end axis.
// example: "D4"
func (m *MergeCell) GetEndAxis() string {
axis := strings.Split((*m)[0], ":")
return axis[1]
}
169 changes: 169 additions & 0 deletions merge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package excelize

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMergeCell(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9"))
assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9"))
assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13"))
assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8"))
assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13"))
assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15"))
assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13"))
assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12"))
assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell"))
assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100))
assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5)))
assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)"))
value, err := f.GetCellValue("Sheet1", "H11")
assert.Equal(t, "0.5", value)
assert.NoError(t, err)
value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate.
assert.Equal(t, "", value)
assert.NoError(t, err)
value, err = f.GetCellFormula("Sheet1", "G12")
assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value)
assert.NoError(t, err)

f.NewSheet("Sheet3")
assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13"))
assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12"))

assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5
assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5

assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5"))
assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6

assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5"))
assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6

assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7"))
assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7

assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12"))
assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12

assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10"))
assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12"))

assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12"))
assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10"))

assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13"))
assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11"))

// Test get merged cells on not exists worksheet.
assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist")

assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx")))

f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))

f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`)

f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
}

func TestGetMergeCells(t *testing.T) {
wants := []struct {
value string
start string
end string
}{{
value: "A1",
start: "A1",
end: "B1",
}, {
value: "A2",
start: "A2",
end: "A3",
}, {
value: "A4",
start: "A4",
end: "B5",
}, {
value: "A7",
start: "A7",
end: "C10",
}}

f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
sheet1 := f.GetSheetName(1)

mergeCells, err := f.GetMergeCells(sheet1)
if !assert.Len(t, mergeCells, len(wants)) {
t.FailNow()
}
assert.NoError(t, err)

for i, m := range mergeCells {
assert.Equal(t, wants[i].value, m.GetCellValue())
assert.Equal(t, wants[i].start, m.GetStartAxis())
assert.Equal(t, wants[i].end, m.GetEndAxis())
}

// Test get merged cells on not exists worksheet.
_, err = f.GetMergeCells("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
}

func TestUnmergeCell(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
sheet1 := f.GetSheetName(1)

xlsx, err := f.workSheetReader(sheet1)
assert.NoError(t, err)

mergeCellNum := len(xlsx.MergeCells.Cells)

assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)

// unmerge the mergecell that contains A1
assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1"))
if len(xlsx.MergeCells.Cells) != mergeCellNum-1 {
t.FailNow()
}

assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx")))

f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
// Test unmerged area on not exists worksheet.
assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist")

f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil
assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15"))

f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7"))

f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`)

f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)

}
149 changes: 96 additions & 53 deletions picture.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -14,7 +14,9 @@ import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"image"
"io"
"io/ioutil"
"os"
"path"
Expand Down Expand Up @@ -46,7 +48,6 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) {
// package main
//
// import (
// "fmt"
// _ "image/gif"
// _ "image/jpeg"
// _ "image/png"
Expand All @@ -57,23 +58,19 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) {
// func main() {
// f := excelize.NewFile()
// // Insert a picture.
// err := f.AddPicture("Sheet1", "A2", "./image1.jpg", "")
// if err != nil {
// fmt.Println(err)
// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil {
// println(err.Error())
// }
// // Insert a picture scaling in the cell with location hyperlink.
// err = f.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)
// if err != nil {
// fmt.Println(err)
// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil {
// println(err.Error())
// }
// // Insert a picture offset in the cell with external hyperlink, printing and positioning support.
// err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`)
// if err != nil {
// fmt.Println(err)
// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil {
// println(err.Error())
// }
// err = f.SaveAs("./Book1.xlsx")
// if err != nil {
// fmt.Println(err)
// if err := f.SaveAs("Book1.xlsx"); err != nil {
// println(err.Error())
// }
// }
//
Expand Down Expand Up @@ -107,7 +104,6 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error {
// package main
//
// import (
// "fmt"
// _ "image/jpeg"
// "io/ioutil"
//
Expand All @@ -117,17 +113,15 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error {
// func main() {
// f := excelize.NewFile()
//
// file, err := ioutil.ReadFile("./image1.jpg")
// file, err := ioutil.ReadFile("image.jpg")
// if err != nil {
// fmt.Println(err)
// println(err.Error())
// }
// err = f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file)
// if err != nil {
// fmt.Println(err)
// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil {
// println(err.Error())
// }
// err = f.SaveAs("./Book1.xlsx")
// if err != nil {
// fmt.Println(err)
// if err := f.SaveAs("Book1.xlsx"); err != nil {
// println(err.Error())
// }
// }
//
Expand Down Expand Up @@ -428,19 +422,18 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
// embed in XLSX by given worksheet and cell name. This function returns the
// file name in XLSX and file contents as []byte data types. For example:
//
// f, err := excelize.OpenFile("./Book1.xlsx")
// f, err := excelize.OpenFile("Book1.xlsx")
// if err != nil {
// fmt.Println(err)
// println(err.Error())
// return
// }
// file, raw, err := f.GetPicture("Sheet1", "A2")
// if err != nil {
// fmt.Println(err)
// println(err.Error())
// return
// }
// err = ioutil.WriteFile(file, raw, 0644)
// if err != nil {
// fmt.Println(err)
// if err := ioutil.WriteFile(file, raw, 0644); err != nil {
// println(err.Error())
// }
//
func (f *File) GetPicture(sheet, cell string) (string, []byte, error) {
Expand Down Expand Up @@ -469,41 +462,91 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) {
return f.getPicture(row, col, drawingXML, drawingRelationships)
}

// DeletePicture provides a function to delete charts in XLSX by given
// worksheet and cell name. Note that the image file won't be deleted from the
// document currently.
func (f *File) DeletePicture(sheet, cell string) (err error) {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return
}
col--
row--
ws, err := f.workSheetReader(sheet)
if err != nil {
return
}
if ws.Drawing == nil {
return
}
drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1)
return f.deleteDrawing(col, row, drawingXML, "Pic")
}

// getPicture provides a function to get picture base name and raw content
// embed in XLSX by given coordinates and drawing relationships.
func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (string, []byte, error) {
wsDr, _ := f.drawingParser(drawingXML)
for _, anchor := range wsDr.TwoCellAnchor {
if anchor.From != nil && anchor.Pic != nil {
if anchor.From.Col == col && anchor.From.Row == row {
xlsxRelationship := f.getDrawingRelationships(drawingRelationships,
anchor.Pic.BlipFill.Blip.Embed)
_, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)]
if ok {
return filepath.Base(xlsxRelationship.Target),
[]byte(f.XLSX[strings.Replace(xlsxRelationship.Target,
"..", "xl", -1)]), nil
func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) {
var (
wsDr *xlsxWsDr
ok bool
deWsDr *decodeWsDr
drawRel *xlsxRelationship
deTwoCellAnchor *decodeTwoCellAnchor
)

wsDr, _ = f.drawingParser(drawingXML)
if ret, buf = f.getPictureFromWsDr(row, col, drawingRelationships, wsDr); len(buf) > 0 {
return
}
deWsDr = new(decodeWsDr)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))).
Decode(deWsDr); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
}
err = nil
for _, anchor := range deWsDr.TwoCellAnchor {
deTwoCellAnchor = new(decodeTwoCellAnchor)
if err = f.xmlNewDecoder(bytes.NewReader([]byte("<decodeTwoCellAnchor>" + anchor.Content + "</decodeTwoCellAnchor>"))).
Decode(deTwoCellAnchor); err != nil && err != io.EOF {
err = fmt.Errorf("xml decode error: %s", err)
return
}
if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil {
if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row {
drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed)
if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok {
ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]
return
}
}
}
}
return
}

decodeWsDr := decodeWsDr{}
_ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr)
for _, anchor := range decodeWsDr.TwoCellAnchor {
decodeTwoCellAnchor := decodeTwoCellAnchor{}
_ = xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+anchor.Content+"</decodeTwoCellAnchor>"), &decodeTwoCellAnchor)
if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil {
if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row {
xlsxRelationship := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed)
_, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)]
if ok {
return filepath.Base(xlsxRelationship.Target), []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil
// getPictureFromWsDr provides a function to get picture base name and raw
// content in worksheet drawing by given coordinates and drawing
// relationships.
func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (ret string, buf []byte) {
var (
ok bool
anchor *xdrCellAnchor
drawRel *xlsxRelationship
)
for _, anchor = range wsDr.TwoCellAnchor {
if anchor.From != nil && anchor.Pic != nil {
if anchor.From.Col == col && anchor.From.Row == row {
drawRel = f.getDrawingRelationships(drawingRelationships,
anchor.Pic.BlipFill.Blip.Embed)
if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok {
ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]
return
}
}
}
}
return "", nil, nil
return
}

// getDrawingRelationships provides a function to get drawing relationships
Expand Down
90 changes: 49 additions & 41 deletions picture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ func BenchmarkAddPictureFromBytes(b *testing.B) {
}
b.ResetTimer()
for i := 1; i <= b.N; i++ {
f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile)
if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile); err != nil {
b.Error(err)
}
}
}

Expand All @@ -36,23 +38,14 @@ func TestAddPicture(t *testing.T) {
}

// Test add picture to worksheet with offset and location hyperlink.
err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"),
`{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)
if !assert.NoError(t, err) {
t.FailNow()
}

assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"),
`{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`))
// Test add picture to worksheet with offset, external hyperlink and positioning.
err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
`{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
`{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`))

file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png"))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, err)

// Test add picture to worksheet from bytes.
assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file))
Expand All @@ -69,9 +62,7 @@ func TestAddPicture(t *testing.T) {

func TestAddPictureErrors(t *testing.T) {
xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, err)

// Test add picture to worksheet with invalid file path.
err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "")
Expand All @@ -92,12 +83,12 @@ func TestAddPictureErrors(t *testing.T) {
}

func TestGetPicture(t *testing.T) {
xlsx, err := prepareTestBook1()
f, err := prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}

file, raw, err := xlsx.GetPicture("Sheet1", "F21")
file, raw, err := f.GetPicture("Sheet1", "F21")
assert.NoError(t, err)
if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) ||
!assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) {
Expand All @@ -106,37 +97,33 @@ func TestGetPicture(t *testing.T) {
}

// Try to get picture from a worksheet with illegal cell coordinates.
_, _, err = xlsx.GetPicture("Sheet1", "A")
_, _, err = f.GetPicture("Sheet1", "A")
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)

// Try to get picture from a worksheet that doesn't contain any images.
file, raw, err = xlsx.GetPicture("Sheet3", "I9")
file, raw, err = f.GetPicture("Sheet3", "I9")
assert.EqualError(t, err, "sheet Sheet3 is not exist")
assert.Empty(t, file)
assert.Empty(t, raw)

// Try to get picture from a cell that doesn't contain an image.
file, raw, err = xlsx.GetPicture("Sheet2", "A2")
file, raw, err = f.GetPicture("Sheet2", "A2")
assert.NoError(t, err)
assert.Empty(t, file)
assert.Empty(t, raw)

xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8")
xlsx.getDrawingRelationships("", "")
xlsx.getSheetRelationshipsTargetByID("", "")
xlsx.deleteSheetRelationships("", "")
f.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8")
f.getDrawingRelationships("", "")
f.getSheetRelationshipsTargetByID("", "")
f.deleteSheetRelationships("", "")

// Try to get picture from a local storage file.
if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) {
t.FailNow()
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx")))

xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
assert.NoError(t, err)

file, raw, err = xlsx.GetPicture("Sheet1", "F21")
file, raw, err = f.GetPicture("Sheet1", "F21")
assert.NoError(t, err)
if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) ||
!assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) {
Expand All @@ -145,7 +132,14 @@ func TestGetPicture(t *testing.T) {
}

// Try to get picture from a local storage file that doesn't contain an image.
file, raw, err = xlsx.GetPicture("Sheet1", "F22")
file, raw, err = f.GetPicture("Sheet1", "F22")
assert.NoError(t, err)
assert.Empty(t, file)
assert.Empty(t, raw)

// Test get picture from none drawing worksheet.
f = NewFile()
file, raw, err = f.GetPicture("Sheet1", "F22")
assert.NoError(t, err)
assert.Empty(t, file)
assert.Empty(t, raw)
Expand All @@ -160,16 +154,30 @@ func TestAddDrawingPicture(t *testing.T) {
func TestAddPictureFromBytes(t *testing.T) {
f := NewFile()
imgFile, err := ioutil.ReadFile("logo.png")
if err != nil {
t.Error("Unable to load logo for test")
}
f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile)
f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile)
assert.NoError(t, err, "Unable to load logo for test")
assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile))
assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile))
imageCount := 0
for fileName := range f.XLSX {
if strings.Contains(fileName, "media/image") {
imageCount++
}
}
assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.")
assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN is not exist")
}

func TestDeletePicture(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), ""))
assert.NoError(t, f.DeletePicture("Sheet1", "P1"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx")))
// Test delete picture on not exists worksheet.
assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist")
// Test delete picture with invalid coordinates.
assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`)
// Test delete picture on no chart worksheet.
assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1"))
}
55 changes: 36 additions & 19 deletions pivotTable.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand Down Expand Up @@ -56,19 +56,17 @@ type PivotTableOption struct {
// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000))
// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)])
// }
// err := f.AddPivotTable(&excelize.PivotTableOption{
// if err := f.AddPivotTable(&excelize.PivotTableOption{
// DataRange: "Sheet1!$A$1:$E$31",
// PivotTableRange: "Sheet1!$G$2:$M$34",
// Rows: []string{"Month", "Year"},
// Columns: []string{"Type"},
// Data: []string{"Sales"},
// })
// if err != nil {
// fmt.Println(err)
// }); err != nil {
// println(err.Error())
// }
// err = f.SaveAs("Book1.xlsx")
// if err != nil {
// fmt.Println(err)
// if err := f.SaveAs("Book1.xlsx"); err != nil {
// println(err.Error())
// }
// }
//
Expand Down Expand Up @@ -253,7 +251,10 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
},
},
},
ColFields: &xlsxColFields{},
ColItems: &xlsxColItems{
Count: 1,
I: []*xlsxI{{}},
},
DataFields: &xlsxDataFields{},
PivotTableStyleInfo: &xlsxPivotTableStyleInfo{
Name: "PivotStyleLight16",
Expand Down Expand Up @@ -286,19 +287,10 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
// count row fields
pt.RowFields.Count = len(pt.RowFields.Field)

// col fields
colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt)
err = f.addPivotColFields(&pt, opt)
if err != nil {
return err
}
for _, filedIdx := range colFieldsIndex {
pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{
X: filedIdx,
})
}

// count col fields
pt.ColFields.Count = len(pt.ColFields.Field)

// data fields
dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt)
Expand Down Expand Up @@ -330,6 +322,31 @@ func inStrSlice(a []string, x string) int {
return -1
}

// addPivotColFields create pivot column fields by given pivot table
// definition and option.
func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
if len(opt.Columns) == 0 {
return nil
}

pt.ColFields = &xlsxColFields{}

// col fields
colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt)
if err != nil {
return err
}
for _, filedIdx := range colFieldsIndex {
pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{
X: filedIdx,
})
}

// count col fields
pt.ColFields.Count = len(pt.ColFields.Field)
return err
}

// addPivotFields create pivot fields based on the column order of the first
// row in the data region by given pivot table definition and option.
func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
Expand Down
18 changes: 12 additions & 6 deletions pivotTable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ func TestAddPivotTable(t *testing.T) {
year := []int{2017, 2018, 2019}
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
region := []string{"East", "West", "North", "South"}
f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
for i := 0; i < 30; i++ {
f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)])
f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)])
f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)])
f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000))
f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)])
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]))
}
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
DataRange: "Sheet1!$A$1:$E$31",
Expand Down Expand Up @@ -54,6 +54,12 @@ func TestAddPivotTable(t *testing.T) {
Columns: []string{"Region", "Year"},
Data: []string{"Sales"},
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AE$2:$AG$33",
Rows: []string{"Month", "Year"},
Data: []string{"Sales"},
}))
f.NewSheet("Sheet2")
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
DataRange: "Sheet1!$A$1:$E$31",
Expand Down
Loading