diff --git a/excelize.go b/excelize.go index ea8926a3b2..7836451859 100644 --- a/excelize.go +++ b/excelize.go @@ -672,3 +672,14 @@ func (f *File) getRichValueWebImageRelationships(rID string) *xlsxRelationship { } return nil } + +// richValueStructureReader provides a function to get the pointer to the structure after +// deserialization of xl/richData/rdrichvaluestructure.xml. +func (f *File) richValueStructureReader() (*xlsxRichValueStructuresData, error) { + var richValueStruct xlsxRichValueStructuresData + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueStructurePart)))). + Decode(&richValueStruct); err != nil && err != io.EOF { + return &richValueStruct, err + } + return &richValueStruct, nil +} diff --git a/picture.go b/picture.go index 5f7a7d3c53..f402e69192 100644 --- a/picture.go +++ b/picture.go @@ -31,10 +31,24 @@ type PictureInsertType byte const ( PictureInsertTypePlaceOverCells PictureInsertType = iota PictureInsertTypePlaceInCell - PictureInsertTypeIMAGE + PictureInsertTypeIMAGE // created directly by formula (ex, =IMAGE), A type of PlaceInCell PictureInsertTypeDISPIMG ) +// CalcOrigin: indicates how the rich value was created. +const ( + CalcOriginNone = string(iota + '0') + CalcOriginFormula // RichValue created directly by formula (ex, =IMAGE) + CalcOriginComplexFormula + CalcOriginDotNotation + CalcOriginReference + CalcOriginStandalone // Standalone RichValue directly stored in a cell without formula dependency (copy/paste as value or LocalImageValue) + CalcOriginStandaloneDecorative // Standalone RichValue created from the alt text pane after selecting "decorative" + CalcOriginNested + CalcOriginJSApi + CalcOriginPythonResult +) + // parseGraphicOptions provides a function to parse the format settings of // the picture with default value. func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { @@ -943,6 +957,33 @@ func (f *File) getImageCellRel(c *xlsxC, pic *Picture) (*xlsxRelationship, error return r, err } rv := richValue.Rv[richValueIdx].V + rs := richValue.Rv[richValueIdx].S + + richValueStructure, err := f.richValueStructureReader() + if err != nil { + return r, err + } + if rs < len(richValueStructure.S) { + pic.InsertType = PictureInsertTypePlaceInCell + for idx, key := range richValueStructure.S[rs].K { + if idx >= len(rv) { + break // invalid key + } + switch key.N { + case "_rvRel:LocalImageIdentifier", "WebImageIdentifier": + r, err = f.getRichDataRichValueRel(rv[idx]) + case "Text": + pic.Format.AltText = rv[idx] + case "CalcOrigin": + // cell image inserted by IMAGE formula function + if rv[idx] == CalcOriginFormula { + pic.InsertType = PictureInsertTypeIMAGE + } + } + } + return r, err + } + // fallback, there is no valid struct definition if len(rv) == 2 && rv[1] == "5" { pic.InsertType = PictureInsertTypePlaceInCell return f.getRichDataRichValueRel(rv[0]) diff --git a/picture_test.go b/picture_test.go index 28042a99be..394c6df952 100644 --- a/picture_test.go +++ b/picture_test.go @@ -500,6 +500,11 @@ func TestGetCellImages(t *testing.T) { }) return f } + addStructure := func(f *File) *File { + f.Pkg.Store(defaultXMLRdRichValueStructurePart, []byte(` + `)) + return f + } f = prepareWorkbook() pics, err := f.GetPictures("Sheet1", "A1") assert.NoError(t, err) @@ -509,6 +514,17 @@ func TestGetCellImages(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []string{"A1"}, cells) + f = addStructure(prepareWorkbook()) + // Test get the cell images with rich value struct + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 1, len(pics)) + assert.Equal(t, PictureInsertTypePlaceInCell, pics[0].InsertType) + cells, err = f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"A1"}, cells) + + f = prepareWorkbook() // Test get the cell images without image relationships parts f.Relationships.Delete(defaultXMLRdRichValueRelRels) f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) @@ -605,6 +621,18 @@ func TestGetCellImages(t *testing.T) { f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`100`)) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax") + + f = addStructure(prepareWorkbook()) + // Test get the cell images with no valid definition and fallback old-style + f.Pkg.Store(defaultXMLRdRichValueStructurePart, []byte(``)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 1, len(pics)) + + // Test get the cell images with unsupported charset rich value + f.Pkg.Store(defaultXMLRdRichValueStructurePart, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestGetImageCells(t *testing.T) { @@ -615,3 +643,33 @@ func TestGetImageCells(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } + +func TestGetCellImagesAndAltText(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "CellImage.xlsx")) + assert.NoError(t, err) + type imageType struct { + cell string + insertType PictureInsertType + altText string + } + want := []imageType{ + {"B1", PictureInsertTypePlaceInCell, "Smiling alarm clock face"}, + {"B2", PictureInsertTypePlaceInCell, ""}, + {"B3", PictureInsertTypePlaceInCell, "Bullseye outline"}, + {"B4", PictureInsertTypeIMAGE, ""}, + {"B5", PictureInsertTypeIMAGE, "other alt_text"}, + + {"D1", PictureInsertTypePlaceInCell, "Smiling alarm clock face"}, + {"D2", PictureInsertTypePlaceInCell, ""}, + {"D3", PictureInsertTypePlaceInCell, "Bullseye outline"}, + {"D4", PictureInsertTypePlaceInCell, ""}, + {"D5", PictureInsertTypePlaceInCell, "other alt_text"}, + } + for _, c := range want { + p, err := f.GetPictures("Sheet1", c.cell) + assert.NoError(t, err) + assert.Equal(t, 1, len(p)) + assert.Equal(t, c.insertType, p[0].InsertType, c.cell) + } + assert.NoError(t, f.Close()) +} diff --git a/templates.go b/templates.go index bd027b8a30..f62a7171de 100644 --- a/templates.go +++ b/templates.go @@ -309,6 +309,7 @@ const ( defaultXMLRdRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" defaultXMLRdRichValueWebImagePart = "xl/richData/rdRichValueWebImage.xml" defaultXMLRdRichValueWebImagePartRels = "xl/richData/_rels/rdRichValueWebImage.xml.rels" + defaultXMLRdRichValueStructurePart = "xl/richData/rdrichvaluestructure.xml" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/test/CellImage.xlsx b/test/CellImage.xlsx new file mode 100644 index 0000000000..3d6401fe70 Binary files /dev/null and b/test/CellImage.xlsx differ diff --git a/xmlMetaData.go b/xmlMetaData.go index 90542fbd39..0297acbbbd 100644 --- a/xmlMetaData.go +++ b/xmlMetaData.go @@ -115,3 +115,25 @@ type xlsxWebImageSupportingRichData struct { MoreImagesAddress xlsxExternalReference `xml:"moreImagesAddress"` Blip xlsxExternalReference `xml:"blip"` } + +// xlsxRichValueStructuresData directly maps the rvStructures element that specifies +// rich value data. +type xlsxRichValueStructuresData struct { + XMLName xml.Name `xml:"rvStructures"` + Count string `xml:"count,attr"` + S []xlsxRichValueStructure `xml:"s"` +} + +// xlsxRichValueStructure directly maps the RichValueStructure element that specifies rich +// value data. +type xlsxRichValueStructure struct { + T string `xml:"t,attr"` + K []xlsxRichValueKey `xml:"k"` +} + +// xlsxRichValueKey directly maps the rich value key element that specifies rich value +// data. +type xlsxRichValueKey struct { + N string `xml:"n,attr"` + T string `xml:"t,attr"` +}