// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excelâ„¢ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.18 or later.
package excelize
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"sort"
"strconv"
"strings"
"unicode"
)
// SlicerOptions represents the settings of the slicer.
//
// Name specifies the slicer name, should be an existing field name of the given
// table or pivot table, this setting is required.
//
// Cell specifies the left top cell coordinates the position for inserting the
// slicer, this setting is required.
//
// TableSheet specifies the worksheet name of the table or pivot table, this
// setting is required.
//
// TableName specifies the name of the table or pivot table, this setting is
// required.
//
// Caption specifies the caption of the slicer, this setting is optional.
//
// Macro used for set macro for the slicer, the workbook extension should be
// XLSM or XLTM.
//
// Width specifies the width of the slicer, this setting is optional.
//
// Height specifies the height of the slicer, this setting is optional.
//
// DisplayHeader specifies if display header of the slicer, this setting is
// optional, the default setting is display.
//
// ItemDesc specifies descending (Z-A) item sorting, this setting is optional,
// and the default setting is false (represents ascending).
//
// Format specifies the format of the slicer, this setting is optional.
type SlicerOptions struct {
slicerXML string
slicerCacheXML string
slicerCacheName string
slicerSheetName string
slicerSheetRID string
drawingXML string
Name string
Cell string
TableSheet string
TableName string
Caption string
Macro string
Width uint
Height uint
DisplayHeader *bool
ItemDesc bool
Format GraphicOptions
}
// AddSlicer function inserts a slicer by giving the worksheet name and slicer
// settings.
//
// For example, insert a slicer on the Sheet1!E1 with field Column1 for the
// table named Table1:
//
// err := f.AddSlicer("Sheet1", &excelize.SlicerOptions{
// Name: "Column1",
// Cell: "E1",
// TableSheet: "Sheet1",
// TableName: "Table1",
// Caption: "Column1",
// Width: 200,
// Height: 200,
// })
func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error {
opts, err := parseSlicerOptions(opts)
if err != nil {
return err
}
table, pivotTable, colIdx, err := f.getSlicerSource(opts)
if err != nil {
return err
}
extURI, ns := ExtURISlicerListX14, NameSpaceDrawingMLA14
if table != nil {
extURI = ExtURISlicerListX15
ns = NameSpaceDrawingMLSlicerX15
}
slicerID, err := f.addSheetSlicer(sheet, extURI)
if err != nil {
return err
}
slicerCacheName, err := f.setSlicerCache(colIdx, opts, table, pivotTable)
if err != nil {
return err
}
slicerName := f.genSlicerName(opts.Name)
if err := f.addDrawingSlicer(sheet, slicerName, ns, opts); err != nil {
return err
}
return f.addSlicer(slicerID, xlsxSlicer{
Name: slicerName,
Cache: slicerCacheName,
Caption: opts.Caption,
ShowCaption: opts.DisplayHeader,
RowHeight: 251883,
})
}
// parseSlicerOptions provides a function to parse the format settings of the
// slicer with default value.
func parseSlicerOptions(opts *SlicerOptions) (*SlicerOptions, error) {
if opts == nil {
return nil, ErrParameterRequired
}
if opts.Name == "" || opts.Cell == "" || opts.TableSheet == "" || opts.TableName == "" {
return nil, ErrParameterInvalid
}
if opts.Width == 0 {
opts.Width = defaultSlicerWidth
}
if opts.Height == 0 {
opts.Height = defaultSlicerHeight
}
if opts.Format.PrintObject == nil {
opts.Format.PrintObject = boolPtr(true)
}
if opts.Format.Locked == nil {
opts.Format.Locked = boolPtr(false)
}
if opts.Format.ScaleX == 0 {
opts.Format.ScaleX = defaultDrawingScale
}
if opts.Format.ScaleY == 0 {
opts.Format.ScaleY = defaultDrawingScale
}
return opts, nil
}
// countSlicers provides a function to get slicer files count storage in the
// folder xl/slicers.
func (f *File) countSlicers() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/slicers/slicer") {
count++
}
return true
})
return count
}
// countSlicerCache provides a function to get slicer cache files count storage
// in the folder xl/SlicerCaches.
func (f *File) countSlicerCache() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
count++
}
return true
})
return count
}
// getSlicerSource returns the slicer data source table or pivot table settings
// and the index of the given slicer fields in the table or pivot table
// column.
func (f *File) getSlicerSource(opts *SlicerOptions) (*Table, *PivotTableOptions, int, error) {
var (
table *Table
pivotTable *PivotTableOptions
colIdx int
err error
dataRange string
tables []Table
pivotTables []PivotTableOptions
)
if tables, err = f.GetTables(opts.TableSheet); err != nil {
return table, pivotTable, colIdx, err
}
for _, tbl := range tables {
if tbl.Name == opts.TableName {
table = &tbl
dataRange = fmt.Sprintf("%s!%s", opts.TableSheet, tbl.Range)
break
}
}
if table == nil {
if pivotTables, err = f.GetPivotTables(opts.TableSheet); err != nil {
return table, pivotTable, colIdx, err
}
for _, tbl := range pivotTables {
if tbl.Name == opts.TableName {
pivotTable = &tbl
dataRange = tbl.DataRange
break
}
}
if pivotTable == nil {
return table, pivotTable, colIdx, newNoExistTableError(opts.TableName)
}
}
order, _ := f.getTableFieldsOrder(&PivotTableOptions{DataRange: dataRange})
if colIdx = inStrSlice(order, opts.Name, true); colIdx == -1 {
return table, pivotTable, colIdx, newInvalidSlicerNameError(opts.Name)
}
return table, pivotTable, colIdx, err
}
// addSheetSlicer adds a new slicer and updates the namespace and relationships
// parts of the worksheet by giving the worksheet name.
func (f *File) addSheetSlicer(sheet, extURI string) (int, error) {
var (
slicerID = f.countSlicers() + 1
ws, err = f.workSheetReader(sheet)
decodeExtLst = new(decodeExtLst)
)
if err != nil {
return slicerID, err
}
if ws.ExtLst != nil {
if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
Decode(decodeExtLst); err != nil && err != io.EOF {
return slicerID, err
}
for _, ext := range decodeExtLst.Ext {
if ext.URI == extURI {
slicerList := new(decodeSlicerList)
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList)
for _, slicer := range slicerList.Slicer {
if slicer.RID != "" {
sheetRelationshipsDrawingXML := f.getSheetRelationshipsTargetByID(sheet, slicer.RID)
slicerID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../slicers/slicer"), ".xml"))
return slicerID, err
}
}
}
}
}
sheetRelationshipsSlicerXML := "../slicers/slicer" + strconv.Itoa(slicerID) + ".xml"
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipSlicer, sheetRelationshipsSlicerXML, "")
f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14)
return slicerID, f.addSheetTableSlicer(ws, rID, extURI)
}
// addSheetTableSlicer adds a new table slicer for the worksheet by giving the
// worksheet relationships ID and extension URI.
func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int, extURI string) error {
var (
decodeExtLst = new(decodeExtLst)
err error
slicerListBytes, extLstBytes []byte
)
if ws.ExtLst != nil {
if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
Decode(decodeExtLst); err != nil && err != io.EOF {
return err
}
}
slicerListBytes, _ = xml.Marshal(&xlsxX14SlicerList{
Slicer: []*xlsxX14Slicer{{RID: "rId" + strconv.Itoa(rID)}},
})
ext := &xlsxExt{
xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
URI: extURI, Content: string(slicerListBytes),
}
if extURI == ExtURISlicerListX15 {
ext.xmlns = []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}
}
decodeExtLst.Ext = append(decodeExtLst.Ext, ext)
sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
})
extLstBytes, err = xml.Marshal(decodeExtLst)
ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
return err
}
// addSlicer adds a new slicer to the workbook by giving the slicer ID and
// settings.
func (f *File) addSlicer(slicerID int, slicer xlsxSlicer) error {
slicerXML := "xl/slicers/slicer" + strconv.Itoa(slicerID) + ".xml"
slicers, err := f.slicerReader(slicerXML)
if err != nil {
return err
}
if err := f.addContentTypePart(slicerID, "slicer"); err != nil {
return err
}
slicers.Slicer = append(slicers.Slicer, slicer)
output, err := xml.Marshal(slicers)
f.saveFileList(slicerXML, output)
return err
}
// genSlicerName generates a unique slicer cache name by giving the slicer name.
func (f *File) genSlicerName(name string) string {
var (
cnt int
slicerName string
names []string
)
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/slicers/slicer") {
slicers, err := f.slicerReader(k.(string))
if err != nil {
return true
}
for _, slicer := range slicers.Slicer {
names = append(names, slicer.Name)
}
}
if strings.Contains(k.(string), "xl/timelines/timeline") {
timelines, err := f.timelineReader(k.(string))
if err != nil {
return true
}
for _, timeline := range timelines.Timeline {
names = append(names, timeline.Name)
}
}
return true
})
slicerName = name
for {
tmp := slicerName
if cnt > 0 {
tmp = fmt.Sprintf("%s %d", slicerName, cnt)
}
if inStrSlice(names, tmp, true) == -1 {
slicerName = tmp
break
}
cnt++
}
return slicerName
}
// genSlicerCacheName generates a unique slicer cache name by giving the slicer name.
func (f *File) genSlicerCacheName(name string) string {
var (
cnt int
definedNames []string
slicerCacheName string
)
for _, dn := range f.GetDefinedName() {
if dn.Scope == "Workbook" {
definedNames = append(definedNames, dn.Name)
}
}
for i, c := range name {
if unicode.IsLetter(c) {
slicerCacheName += string(c)
continue
}
if i > 0 && (unicode.IsDigit(c) || c == '.') {
slicerCacheName += string(c)
continue
}
slicerCacheName += "_"
}
slicerCacheName = fmt.Sprintf("Slicer_%s", slicerCacheName)
for {
tmp := slicerCacheName
if cnt > 0 {
tmp = fmt.Sprintf("%s%d", slicerCacheName, cnt)
}
if inStrSlice(definedNames, tmp, true) == -1 {
slicerCacheName = tmp
break
}
cnt++
}
return slicerCacheName
}
// setSlicerCache check if a slicer cache already exists or add a new slicer
// cache by giving the column index, slicer, table options, and returns the
// slicer cache name.
func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) {
var ok bool
var slicerCacheName string
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
slicerCache, err := f.slicerCacheReader(k.(string))
if err != nil {
return true
}
if pivotTable != nil && slicerCache.PivotTables != nil {
for _, tbl := range slicerCache.PivotTables.PivotTable {
if tbl.Name == pivotTable.Name {
ok, slicerCacheName = true, slicerCache.Name
return false
}
}
}
if table == nil || slicerCache.ExtLst == nil {
return true
}
ext := new(xlsxExt)
_ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext)
if ext.URI == ExtURISlicerCacheDefinition {
tableSlicerCache := new(decodeTableSlicerCache)
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache)
if tableSlicerCache.TableID == table.tID && tableSlicerCache.Column == colIdx+1 {
ok, slicerCacheName = true, slicerCache.Name
return false
}
}
}
return true
})
if ok {
return slicerCacheName, nil
}
slicerCacheName = f.genSlicerCacheName(opts.Name)
return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table, pivotTable)
}
// slicerReader provides a function to get the pointer to the structure
// after deserialization of xl/slicers/slicer%d.xml.
func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) {
content, ok := f.Pkg.Load(slicerXML)
slicer := &xlsxSlicers{
XMLNSXMC: SourceRelationshipCompatibility.Value,
XMLNSX: NameSpaceSpreadSheet.Value,
XMLNSXR10: NameSpaceSpreadSheetXR10.Value,
}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(slicer); err != nil && err != io.EOF {
return nil, err
}
}
return slicer, nil
}
// slicerCacheReader provides a function to get the pointer to the structure
// after deserialization of xl/slicerCaches/slicerCache%d.xml.
func (f *File) slicerCacheReader(slicerCacheXML string) (*xlsxSlicerCacheDefinition, error) {
content, ok := f.Pkg.Load(slicerCacheXML)
slicerCache := &xlsxSlicerCacheDefinition{}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(slicerCache); err != nil && err != io.EOF {
return nil, err
}
}
return slicerCache, nil
}
// timelineReader provides a function to get the pointer to the structure
// after deserialization of xl/timelines/timeline%d.xml.
func (f *File) timelineReader(timelineXML string) (*xlsxTimelines, error) {
content, ok := f.Pkg.Load(timelineXML)
timeline := &xlsxTimelines{
XMLNSXMC: SourceRelationshipCompatibility.Value,
XMLNSX: NameSpaceSpreadSheet.Value,
XMLNSXR10: NameSpaceSpreadSheetXR10.Value,
}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(timeline); err != nil && err != io.EOF {
return nil, err
}
}
return timeline, nil
}
// addSlicerCache adds a new slicer cache by giving the slicer cache name,
// column index, slicer, and table or pivot table options.
func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) error {
var (
sortOrder string
slicerCacheBytes, tableSlicerBytes, extLstBytes []byte
extURI = ExtURISlicerCachesX14
slicerCacheID = f.countSlicerCache() + 1
decodeExtLst = new(decodeExtLst)
slicerCache = xlsxSlicerCacheDefinition{
XMLNSXMC: SourceRelationshipCompatibility.Value,
XMLNSX: NameSpaceSpreadSheet.Value,
XMLNSX15: NameSpaceSpreadSheetX15.Value,
XMLNSXR10: NameSpaceSpreadSheetXR10.Value,
Name: slicerCacheName,
SourceName: opts.Name,
}
)
if opts.ItemDesc {
sortOrder = "descending"
}
if pivotTable != nil {
pivotCacheID, err := f.addPivotCacheSlicer(pivotTable)
if err != nil {
return err
}
slicerCache.PivotTables = &xlsxSlicerCachePivotTables{
PivotTable: []xlsxSlicerCachePivotTable{
{TabID: f.getSheetID(opts.TableSheet), Name: pivotTable.Name},
},
}
slicerCache.Data = &xlsxSlicerCacheData{
Tabular: &xlsxTabularSlicerCache{
PivotCacheID: pivotCacheID,
SortOrder: sortOrder,
ShowMissing: boolPtr(false),
Items: &xlsxTabularSlicerCacheItems{
Count: 1, I: []xlsxTabularSlicerCacheItem{{S: true}},
},
},
}
}
if table != nil {
tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{
TableID: table.tID,
Column: colIdx + 1,
SortOrder: sortOrder,
})
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}},
URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes),
})
extLstBytes, _ = xml.Marshal(decodeExtLst)
slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
extURI = ExtURISlicerCachesX15
}
slicerCacheXML := "xl/slicerCaches/slicerCache" + strconv.Itoa(slicerCacheID) + ".xml"
slicerCacheBytes, _ = xml.Marshal(slicerCache)
f.saveFileList(slicerCacheXML, slicerCacheBytes)
if err := f.addContentTypePart(slicerCacheID, "slicerCache"); err != nil {
return err
}
if err := f.addWorkbookSlicerCache(slicerCacheID, extURI); err != nil {
return err
}
return f.SetDefinedName(&DefinedName{Name: slicerCacheName, RefersTo: formulaErrorNA})
}
// addPivotCacheSlicer adds a new slicer cache by giving the pivot table options
// and returns pivot table cache ID.
func (f *File) addPivotCacheSlicer(opts *PivotTableOptions) (int, error) {
var (
pivotCacheID int
pivotCacheBytes, extLstBytes []byte
decodeExtLst = new(decodeExtLst)
decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition)
)
pc, err := f.pivotCacheReader(opts.pivotCacheXML)
if err != nil {
return pivotCacheID, err
}
if pc.ExtLst != nil {
_ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst)
for _, ext := range decodeExtLst.Ext {
if ext.URI == ExtURIPivotCacheDefinition {
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition)
return decodeX14PivotCacheDefinition.PivotCacheID, err
}
}
}
pivotCacheID = f.genPivotCacheDefinitionID()
pivotCacheBytes, _ = xml.Marshal(&xlsxX14PivotCacheDefinition{PivotCacheID: pivotCacheID})
ext := &xlsxExt{
xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
URI: ExtURIPivotCacheDefinition, Content: string(pivotCacheBytes),
}
decodeExtLst.Ext = append(decodeExtLst.Ext, ext)
extLstBytes, _ = xml.Marshal(decodeExtLst)
pc.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
pivotCache, err := xml.Marshal(pc)
f.saveFileList(opts.pivotCacheXML, pivotCache)
return pivotCacheID, err
}
// addDrawingSlicer adds a slicer shape and fallback shape by giving the
// worksheet name, slicer name, and slicer options.
func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *SlicerOptions) error {
drawingID := f.countDrawings() + 1
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(sheet, drawingXML, opts.Cell, opts.Width, opts.Height, opts.Format)
if err != nil {
return err
}
graphicFrame := xlsxGraphicFrame{
Macro: opts.Macro,
NvGraphicFramePr: xlsxNvGraphicFramePr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
Name: slicerName,
},
},
Xfrm: xlsxXfrm{Off: xlsxOff{}, Ext: aExt{}},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
URI: NameSpaceDrawingMLSlicer.Value,
Sle: &xlsxSle{XMLNS: NameSpaceDrawingMLSlicer.Value, Name: slicerName},
},
},
}
graphic, _ := xml.Marshal(graphicFrame)
sp := xdrSp{
Macro: opts.Macro,
NvSpPr: &xdrNvSpPr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
},
CNvSpPr: &xdrCNvSpPr{
TxBox: true,
},
},
SpPr: &xlsxSpPr{
Xfrm: xlsxXfrm{Off: xlsxOff{X: 2914650, Y: 152400}, Ext: aExt{Cx: 1828800, Cy: 2238375}},
SolidFill: &xlsxInnerXML{Content: ""},
PrstGeom: xlsxPrstGeom{
Prst: "rect",
},
Ln: xlsxLineProperties{W: 1, SolidFill: &xlsxInnerXML{Content: ""}},
},
TxBody: &xdrTxBody{
BodyPr: &aBodyPr{VertOverflow: "clip", HorzOverflow: "clip"},
P: []*aP{
{R: &aR{T: "This shape represents a table slicer. Table slicers are not supported in this version of Excel."}},
{R: &aR{T: "If the shape was modified in an earlier version of Excel, or if the workbook was saved in Excel 2007 or earlier, the slicer can't be used."}},
},
},
}
shape, _ := xml.Marshal(sp)
twoCellAnchor.ClientData = &xdrClientData{
FLocksWithSheet: *opts.Format.Locked,
FPrintsWithSheet: *opts.Format.PrintObject,
}
choice := xlsxChoice{Requires: ns.Name.Local, Content: string(graphic)}
if ns.Value == NameSpaceDrawingMLA14.Value { // pivot table slicer
choice.XMLNSA14 = ns.Value
}
if ns.Value == NameSpaceDrawingMLSlicerX15.Value { // table slicer
choice.XMLNSSle15 = ns.Value
}
fallback := xlsxFallback{Content: string(shape)}
choiceBytes, _ := xml.Marshal(choice)
shapeBytes, _ := xml.Marshal(fallback)
twoCellAnchor.AlternateContent = append(twoCellAnchor.AlternateContent, &xlsxAlternateContent{
XMLNSMC: SourceRelationshipCompatibility.Value,
Content: string(choiceBytes) + string(shapeBytes),
})
content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor)
f.Drawings.Store(drawingXML, content)
return f.addContentTypePart(drawingID, "drawings")
}
// addWorkbookSlicerCache add the association ID of the slicer cache in
// workbook.xml.
func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error {
var (
wb *xlsxWorkbook
err error
idx int
appendMode bool
decodeExtLst = new(decodeExtLst)
decodeSlicerCaches = new(decodeSlicerCaches)
x14SlicerCaches = new(xlsxX14SlicerCaches)
x15SlicerCaches = new(xlsxX15SlicerCaches)
ext *xlsxExt
slicerCacheBytes, slicerCachesBytes, extLstBytes []byte
)
if wb, err = f.workbookReader(); err != nil {
return err
}
rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipSlicerCache, fmt.Sprintf("/xl/slicerCaches/slicerCache%d.xml", slicerCacheID), "")
if wb.ExtLst != nil { // append mode ext
if err = f.xmlNewDecoder(strings.NewReader("" + wb.ExtLst.Ext + "")).
Decode(decodeExtLst); err != nil && err != io.EOF {
return err
}
for idx, ext = range decodeExtLst.Ext {
if ext.URI == URI {
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches)
slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)}
slicerCacheBytes, _ = xml.Marshal(slicerCache)
if URI == ExtURISlicerCachesX14 { // pivot table slicer
x14SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes)
x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches)
}
if URI == ExtURISlicerCachesX15 { // table slicer
x15SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes)
x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches)
}
decodeExtLst.Ext[idx].Content = string(slicerCachesBytes)
appendMode = true
}
}
}
if !appendMode {
slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)}
slicerCacheBytes, _ = xml.Marshal(slicerCache)
if URI == ExtURISlicerCachesX14 {
x14SlicerCaches.Content = string(slicerCacheBytes)
x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches)
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
URI: ExtURISlicerCachesX14, Content: string(slicerCachesBytes),
})
}
if URI == ExtURISlicerCachesX15 {
x15SlicerCaches.Content = string(slicerCacheBytes)
x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches)
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}},
URI: ExtURISlicerCachesX15, Content: string(slicerCachesBytes),
})
}
}
sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
return inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[i].URI, false) <
inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[j].URI, false)
})
extLstBytes, err = xml.Marshal(decodeExtLst)
wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
return err
}
// GetSlicers provides the method to get all slicers in a worksheet by a given
// worksheet name. Note that, this function does not support getting the height,
// width, and graphic options of the slicer shape currently.
func (f *File) GetSlicers(sheet string) ([]SlicerOptions, error) {
var (
slicers []SlicerOptions
ws, err = f.workSheetReader(sheet)
decodeExtLst = new(decodeExtLst)
)
if err != nil {
return slicers, err
}
if ws.ExtLst == nil {
return slicers, err
}
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
Decode(decodeExtLst); err != nil && err != io.EOF {
return slicers, err
}
for _, ext := range decodeExtLst.Ext {
if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 {
slicerList := new(decodeSlicerList)
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(&slicerList)
for _, slicer := range slicerList.Slicer {
if slicer.RID != "" {
opts, err := f.getSlicers(sheet, slicer.RID, drawingXML)
if err != nil {
return slicers, err
}
slicers = append(slicers, opts...)
}
}
}
}
return slicers, err
}
// getSlicerCache provides a function to get a slicer cache by given slicer
// cache name and slicer options.
func (f *File) getSlicerCache(slicerCacheName string, opt *SlicerOptions) *xlsxSlicerCacheDefinition {
var (
err error
slicerCache *xlsxSlicerCacheDefinition
)
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
slicerCache, err = f.slicerCacheReader(k.(string))
if err != nil {
return true
}
if slicerCache.Name == slicerCacheName {
opt.slicerCacheXML = k.(string)
return false
}
}
return true
})
return slicerCache
}
// getSlicers provides a function to get slicers options by given worksheet
// name, slicer part relationship ID and drawing part path.
func (f *File) getSlicers(sheet, rID, drawingXML string) ([]SlicerOptions, error) {
var (
opts []SlicerOptions
sheetRelationshipsSlicerXML = f.getSheetRelationshipsTargetByID(sheet, rID)
slicerXML = strings.ReplaceAll(sheetRelationshipsSlicerXML, "..", "xl")
slicers, err = f.slicerReader(slicerXML)
)
if err != nil {
return opts, err
}
for _, slicer := range slicers.Slicer {
opt := SlicerOptions{
slicerXML: slicerXML,
slicerCacheName: slicer.Cache,
slicerSheetName: sheet,
slicerSheetRID: rID,
drawingXML: drawingXML,
Name: slicer.Name,
Caption: slicer.Caption,
DisplayHeader: slicer.ShowCaption,
}
slicerCache := f.getSlicerCache(slicer.Cache, &opt)
if slicerCache == nil {
return opts, err
}
if err := f.extractTableSlicer(slicerCache, &opt); err != nil {
return opts, err
}
if err := f.extractPivotTableSlicer(slicerCache, &opt); err != nil {
return opts, err
}
if err = f.extractSlicerCellAnchor(drawingXML, &opt); err != nil {
return opts, err
}
opts = append(opts, opt)
}
return opts, err
}
// extractTableSlicer extract table slicer options from slicer cache.
func (f *File) extractTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error {
if slicerCache.ExtLst != nil {
tables, err := f.getTables()
if err != nil {
return err
}
ext := new(xlsxExt)
_ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext)
if ext.URI == ExtURISlicerCacheDefinition {
tableSlicerCache := new(decodeTableSlicerCache)
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache)
opt.ItemDesc = tableSlicerCache.SortOrder == "descending"
for sheetName, sheetTables := range tables {
for _, table := range sheetTables {
if tableSlicerCache.TableID == table.tID {
opt.TableName = table.Name
opt.TableSheet = sheetName
}
}
}
}
}
return nil
}
// extractPivotTableSlicer extract pivot table slicer options from slicer cache.
func (f *File) extractPivotTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error {
pivotTables, err := f.getPivotTables()
if err != nil {
return err
}
if slicerCache.PivotTables != nil {
for _, pt := range slicerCache.PivotTables.PivotTable {
opt.TableName = pt.Name
for sheetName, sheetPivotTables := range pivotTables {
for _, pivotTable := range sheetPivotTables {
if opt.TableName == pivotTable.Name {
opt.TableSheet = sheetName
}
}
}
}
if slicerCache.Data != nil && slicerCache.Data.Tabular != nil {
opt.ItemDesc = slicerCache.Data.Tabular.SortOrder == "descending"
}
}
return nil
}
// extractSlicerCellAnchor extract slicer drawing object from two cell anchor by
// giving drawing part path and slicer options.
func (f *File) extractSlicerCellAnchor(drawingXML string, opt *SlicerOptions) error {
var (
wsDr *xlsxWsDr
deCellAnchor = new(decodeCellAnchor)
deChoice = new(decodeChoice)
err error
)
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return err
}
wsDr.mu.Lock()
defer wsDr.mu.Unlock()
cond := func(ac *xlsxAlternateContent) bool {
if ac != nil {
_ = f.xmlNewDecoder(strings.NewReader(ac.Content)).Decode(&deChoice)
if deChoice.XMLNSSle15 == NameSpaceDrawingMLSlicerX15.Value || deChoice.XMLNSA14 == NameSpaceDrawingMLA14.Value {
if deChoice.GraphicFrame.NvGraphicFramePr.CNvPr.Name == opt.Name {
return true
}
}
}
return false
}
for _, anchor := range wsDr.TwoCellAnchor {
for _, ac := range anchor.AlternateContent {
if cond(ac) {
if anchor.From != nil {
opt.Macro = deChoice.GraphicFrame.Macro
if opt.Cell, err = CoordinatesToCellName(anchor.From.Col+1, anchor.From.Row+1); err != nil {
return err
}
}
return err
}
}
_ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor)
for _, ac := range deCellAnchor.AlternateContent {
if cond(ac) {
if deCellAnchor.From != nil {
opt.Macro = deChoice.GraphicFrame.Macro
if opt.Cell, err = CoordinatesToCellName(deCellAnchor.From.Col+1, deCellAnchor.From.Row+1); err != nil {
return err
}
}
return err
}
}
}
return err
}
// getAllSlicers provides a function to get all slicers in a workbook.
func (f *File) getAllSlicers() (map[string][]SlicerOptions, error) {
slicers := map[string][]SlicerOptions{}
for _, sheetName := range f.GetSheetList() {
sles, err := f.GetSlicers(sheetName)
e := ErrSheetNotExist{sheetName}
if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
return slicers, err
}
slicers[sheetName] = append(slicers[sheetName], sles...)
}
return slicers, nil
}
// DeleteSlicer provides the method to delete a slicer by a given slicer name.
func (f *File) DeleteSlicer(name string) error {
sles, err := f.getAllSlicers()
if err != nil {
return err
}
for _, slicers := range sles {
for _, slicer := range slicers {
if slicer.Name != name {
continue
}
_ = f.deleteSlicer(slicer)
return f.deleteSlicerCache(sles, slicer)
}
}
return newNoExistSlicerError(name)
}
// getSlicers provides a function to delete slicer by given slicer options.
func (f *File) deleteSlicer(opts SlicerOptions) error {
slicers, err := f.slicerReader(opts.slicerXML)
if err != nil {
return err
}
for i := 0; i < len(slicers.Slicer); i++ {
if slicers.Slicer[i].Name == opts.Name {
slicers.Slicer = append(slicers.Slicer[:i], slicers.Slicer[i+1:]...)
i--
}
}
if len(slicers.Slicer) == 0 {
var (
extLstBytes []byte
ws, err = f.workSheetReader(opts.slicerSheetName)
decodeExtLst = new(decodeExtLst)
)
if err != nil {
return err
}
if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
Decode(decodeExtLst); err != nil && err != io.EOF {
return err
}
for i, ext := range decodeExtLst.Ext {
if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 {
slicerList := new(decodeSlicerList)
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList)
for _, slicer := range slicerList.Slicer {
if slicer.RID == opts.slicerSheetRID {
decodeExtLst.Ext = append(decodeExtLst.Ext[:i], decodeExtLst.Ext[i+1:]...)
extLstBytes, err = xml.Marshal(decodeExtLst)
ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
f.Pkg.Delete(opts.slicerXML)
_ = f.removeContentTypesPart(ContentTypeSlicer, "/"+opts.slicerXML)
f.deleteSheetRelationships(opts.slicerSheetName, opts.slicerSheetRID)
return err
}
}
}
}
}
output, err := xml.Marshal(slicers)
f.saveFileList(opts.slicerXML, output)
return err
}
// deleteSlicerCache provides a function to delete the slicer cache by giving
// slicer options if the slicer cache is no longer used.
func (f *File) deleteSlicerCache(sles map[string][]SlicerOptions, opts SlicerOptions) error {
for _, slicers := range sles {
for _, slicer := range slicers {
if slicer.Name != opts.Name && slicer.slicerCacheName == opts.slicerCacheName {
return nil
}
}
}
if err := f.DeleteDefinedName(&DefinedName{Name: opts.slicerCacheName}); err != nil {
return err
}
f.Pkg.Delete(opts.slicerCacheXML)
return f.removeContentTypesPart(ContentTypeSlicerCache, "/"+opts.slicerCacheXML)
}