From 00d62590f4791013aca4a27f4d2f1ddc8546a09a Mon Sep 17 00:00:00 2001 From: li Date: Fri, 15 Dec 2023 13:09:42 +0800 Subject: [PATCH] This closes #664, support get embedded cell images (#1759) Co-authored-by: liying05 --- calc.go | 11 ++++++++ calc_test.go | 4 +++ chart.go | 10 +++---- excelize.go | 1 + picture.go | 66 +++++++++++++++++++++++++++++++++++++++++++-- picture_test.go | 40 +++++++++++++++++++++++++++ templates.go | 24 +++++++++-------- xmlDecodeDrawing.go | 14 +++++++++- 8 files changed, 151 insertions(+), 19 deletions(-) diff --git a/calc.go b/calc.go index a5dbd72..8fd207a 100644 --- a/calc.go +++ b/calc.go @@ -18640,3 +18640,14 @@ func (fn *formulaFuncs) DVAR(argsList *list.List) formulaArg { func (fn *formulaFuncs) DVARP(argsList *list.List) formulaArg { return fn.database("DVARP", argsList) } + +// DISPIMG function calculates the Kingsoft WPS Office embedded image ID. The +// syntax of the function is: +// +// DISPIMG(picture_name,display_mode) +func (fn *formulaFuncs) DISPIMG(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DISPIMG requires 2 numeric arguments") + } + return argsList.Front().Value.(formulaArg) +} diff --git a/calc_test.go b/calc_test.go index 71f3396..a3a6a83 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2236,6 +2236,8 @@ func TestCalcCellValue(t *testing.T) { // YIELDMAT "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101)": "0.0419422478838651", "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": "0.0419422478838651", + // DISPIMG + "=_xlfn.DISPIMG(\"ID_********************************\",1)": "ID_********************************", } for formula, expected := range mathCalc { f := prepareCalcData(cellData) @@ -4609,6 +4611,8 @@ func TestCalcCellValue(t *testing.T) { "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",-1,101,0)": {"#NUM!", "YIELDMAT requires rate >= 0"}, "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",1,0,0)": {"#NUM!", "YIELDMAT requires pr > 0"}, "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,5)": {"#NUM!", "invalid basis"}, + // DISPIMG + "=_xlfn.DISPIMG()": {"#VALUE!", "DISPIMG requires 2 numeric arguments"}, } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) diff --git a/chart.go b/chart.go index ffc0456..8296826 100644 --- a/chart.go +++ b/chart.go @@ -892,9 +892,9 @@ func (opts *Chart) parseTitle() { // The default width is 480, and height is 260. // // Set the bubble size in all data series for the bubble chart or 3D bubble -// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. -// The default width is 100, and the value should be great than 0 and less or -// equal than 300. +// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. The +// default width is 100, and the value should be great than 0 and less or equal +// than 300. // // Set the doughnut hole size in all data series for the doughnut chart by // 'HoleSize' property. The 'HoleSize' property is optional. The default width @@ -932,7 +932,7 @@ func (opts *Chart) parseTitle() { // } // enable, disable := true, false // if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ -// Type: "col", +// Type: excelize.Col, // Series: []excelize.ChartSeries{ // { // Name: "Sheet1!$A$2", @@ -966,7 +966,7 @@ func (opts *Chart) parseTitle() { // ShowVal: true, // }, // }, &excelize.Chart{ -// Type: "line", +// Type: excelize.Line, // Series: []excelize.ChartSeries{ // { // Name: "Sheet1!$A$4", diff --git a/excelize.go b/excelize.go index 0b85760..b7dd508 100644 --- a/excelize.go +++ b/excelize.go @@ -43,6 +43,7 @@ type File struct { Comments map[string]*xlsxComments ContentTypes *xlsxTypes DecodeVMLDrawing map[string]*decodeVmlDrawing + DecodeCellImages *decodeCellImages Drawings sync.Map Path string Pkg sync.Map diff --git a/picture.go b/picture.go index 9411f07..1c0c8c7 100644 --- a/picture.go +++ b/picture.go @@ -15,6 +15,7 @@ import ( "bytes" "encoding/xml" "image" + "io" "os" "path" "path/filepath" @@ -467,14 +468,22 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } f.mu.Unlock() if ws.Drawing == nil { - return nil, err + return f.getCellImages(sheet, cell) } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") - return f.getPicture(row, col, drawingXML, drawingRelationships) + imgs, err := f.getCellImages(sheet, cell) + if err != nil { + return nil, err + } + pics, err := f.getPicture(row, col, drawingXML, drawingRelationships) + if err != nil { + return nil, err + } + return append(imgs, pics...), err } // GetPictureCells returns all picture cell references in a worksheet by a @@ -741,3 +750,56 @@ func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]strin } return cells, err } + +// cellImagesReader provides a function to get the pointer to the structure +// after deserialization of xl/cellimages.xml. +func (f *File) cellImagesReader() (*decodeCellImages, error) { + if f.DecodeCellImages == nil { + f.DecodeCellImages = new(decodeCellImages) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCellImages)))). + Decode(f.DecodeCellImages); err != nil && err != io.EOF { + return f.DecodeCellImages, err + } + } + return f.DecodeCellImages, nil +} + +// getCellImages provides a function to get the Kingsoft WPS Office embedded +// cell images by given worksheet name and cell reference. +func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { + formula, err := f.GetCellFormula(sheet, cell) + if err != nil { + return nil, err + } + if !strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(formula, "="), "_xlfn."), "DISPIMG") { + return nil, err + } + imgID, err := f.CalcCellValue(sheet, cell) + if err != nil { + return nil, err + } + cellImages, err := f.cellImagesReader() + if err != nil { + return nil, err + } + rels, err := f.relsReader(defaultXMLPathCellImagesRels) + if rels == nil { + return nil, err + } + var pics []Picture + for _, cellImg := range cellImages.CellImage { + if cellImg.Pic.NvPicPr.CNvPr.Name == imgID { + for _, r := range rels.Relationships { + if r.ID == cellImg.Pic.BlipFill.Blip.Embed { + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + if buffer, _ := f.Pkg.Load("xl/" + r.Target); buffer != nil { + pic.File = buffer.([]byte) + pic.Format.AltText = cellImg.Pic.NvPicPr.CNvPr.Descr + pics = append(pics, pic) + } + } + } + } + } + return pics, err +} diff --git a/picture_test.go b/picture_test.go index b98941f..3573f45 100644 --- a/picture_test.go +++ b/picture_test.go @@ -216,6 +216,7 @@ func TestGetPicture(t *testing.T) { cells, err := f.GetPictureCells("Sheet2") assert.NoError(t, err) assert.Equal(t, []string{"K16"}, cells) + assert.NoError(t, f.Close()) // Test get picture from none drawing worksheet f = NewFile() @@ -229,11 +230,41 @@ func TestGetPicture(t *testing.T) { path := "xl/drawings/drawing1.xml" f.Drawings.Delete(path) f.Pkg.Store(path, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "F21") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") f.Drawings.Delete(path) _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test get embedded cell pictures + f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.SetCellFormula("Sheet1", "F21", "=_xlfn.DISPIMG(\"ID_********************************\",1)")) + f.Pkg.Store(defaultXMLPathCellImages, []byte(``)) + f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(``)) + pics, err = f.GetPictures("Sheet1", "F21") + assert.NoError(t, err) + assert.Len(t, pics, 2) + assert.Equal(t, "CellImage1", pics[0].Format.AltText) + + // Test get embedded cell pictures with invalid formula + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=_xlfn.DISPIMG()")) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments") + + // Test get embedded cell pictures with unsupported charset + f.Relationships.Delete(defaultXMLPathCellImagesRels) + f.Pkg.Store(defaultXMLPathCellImagesRels, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "F21") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + f.Pkg.Store(defaultXMLPathCellImages, MacintoshCyrillicCharset) + f.DecodeCellImages = nil + _, err = f.GetPictures("Sheet1", "F21") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) } func TestAddDrawingPicture(t *testing.T) { @@ -394,3 +425,12 @@ func TestExtractDecodeCellAnchor(t *testing.T) { cb := func(a *decodeCellAnchor, r *xlsxRelationship) {} f.extractDecodeCellAnchor(&xdrCellAnchor{GraphicFrame: string(MacintoshCyrillicCharset)}, "", cond, cb) } + +func TestGetCellImages(t *testing.T) { + f := NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + _, err := f.getCellImages("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} diff --git a/templates.go b/templates.go index 3861f6b..43d6df4 100644 --- a/templates.go +++ b/templates.go @@ -266,17 +266,19 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp } const ( - defaultTempFileSST = "sharedStrings" - defaultXMLPathCalcChain = "xl/calcChain.xml" - defaultXMLPathContentTypes = "[Content_Types].xml" - defaultXMLPathDocPropsApp = "docProps/app.xml" - defaultXMLPathDocPropsCore = "docProps/core.xml" - defaultXMLPathSharedStrings = "xl/sharedStrings.xml" - defaultXMLPathStyles = "xl/styles.xml" - defaultXMLPathTheme = "xl/theme/theme1.xml" - defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" - defaultXMLPathWorkbook = "xl/workbook.xml" - defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" + defaultTempFileSST = "sharedStrings" + defaultXMLPathCalcChain = "xl/calcChain.xml" + defaultXMLPathCellImages = "xl/cellimages.xml" + defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels" + defaultXMLPathContentTypes = "[Content_Types].xml" + defaultXMLPathDocPropsApp = "docProps/app.xml" + defaultXMLPathDocPropsCore = "docProps/core.xml" + defaultXMLPathSharedStrings = "xl/sharedStrings.xml" + defaultXMLPathStyles = "xl/styles.xml" + defaultXMLPathTheme = "xl/theme/theme1.xml" + defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" + defaultXMLPathWorkbook = "xl/workbook.xml" + defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index fb4ea07..1473817 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -83,7 +83,7 @@ type decodeCNvSpPr struct { // changed after serialization and deserialization, two different structures // are defined. decodeWsDr just for deserialization. type decodeWsDr struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr"` A string `xml:"xmlns a,attr"` Xdr string `xml:"xmlns xdr,attr"` R string `xml:"xmlns r,attr"` @@ -242,3 +242,15 @@ type decodeClientData struct { FLocksWithSheet bool `xml:"fLocksWithSheet,attr"` FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"` } + +// decodeCellImages directly maps the Kingsoft WPS Office embedded cell images. +type decodeCellImages struct { + XMLName xml.Name `xml:"http://www.wps.cn/officeDocument/2017/etCustomData cellImages"` + CellImage []decodeCellImage `xml:"cellImage"` +} + +// decodeCellImage defines the structure used to deserialize the Kingsoft WPS +// Office embedded cell images. +type decodeCellImage struct { + Pic decodePic `xml:"pic"` +}