From 5dc22e874b0e687ff0888d82259692b5852386b7 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Apr 2024 08:47:57 +0800 Subject: [PATCH] Support get the cell images inserted by IMAGE formula function --- excelize.go | 37 ++++++++++++++++---- picture.go | 91 +++++++++++++++++++++++++++++++------------------ picture_test.go | 64 +++++++++++++++++++++++++++------- templates.go | 36 ++++++++++--------- xmlMetaData.go | 17 +++++++++ 5 files changed, 176 insertions(+), 69 deletions(-) diff --git a/excelize.go b/excelize.go index 8b1b763..e0959aa 100644 --- a/excelize.go +++ b/excelize.go @@ -605,7 +605,7 @@ func (f *File) metadataReader() (*xlsxMetadata, error) { // deserialization of xl/richData/richvalue.xml. func (f *File) richValueReader() (*xlsxRichValueData, error) { var richValue xlsxRichValueData - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValue)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValuePart)))). Decode(&richValue); err != nil && err != io.EOF { return &richValue, err } @@ -616,18 +616,43 @@ func (f *File) richValueReader() (*xlsxRichValueData, error) { // after deserialization of xl/richData/richValueRel.xml. func (f *File) richValueRelReader() (*xlsxRichValueRels, error) { var richValueRels xlsxRichValueRels - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValueRel)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))). Decode(&richValueRels); err != nil && err != io.EOF { return &richValueRels, err } return &richValueRels, nil } -// getRichDataRichValueRelRelationships provides a function to get drawing -// relationships from xl/richData/_rels/richValueRel.xml.rels by given -// relationship ID. +// richValueWebImageReader provides a function to get the pointer to the +// structure after deserialization of xl/richData/rdRichValueWebImage.xml. +func (f *File) richValueWebImageReader() (*xlsxWebImagesSupportingRichData, error) { + var richValueWebImages xlsxWebImagesSupportingRichData + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueWebImagePart)))). + Decode(&richValueWebImages); err != nil && err != io.EOF { + return &richValueWebImages, err + } + return &richValueWebImages, nil +} + +// getRichDataRichValueRelRelationships provides a function to get relationships +// from xl/richData/_rels/richValueRel.xml.rels by given relationship ID. func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship { - if rels, _ := f.relsReader(defaultXMLRichDataRichValueRelRels); rels != nil { + if rels, _ := f.relsReader(defaultXMLRdRichValueRelRels); rels != nil { + rels.mu.Lock() + defer rels.mu.Unlock() + for _, v := range rels.Relationships { + if v.ID == rID { + return &v + } + } + } + return nil +} + +// getRichValueWebImageRelationships provides a function to get relationships +// from xl/richData/_rels/rdRichValueWebImage.xml.rels by given relationship ID. +func (f *File) getRichValueWebImageRelationships(rID string) *xlsxRelationship { + if rels, _ := f.relsReader(defaultXMLRdRichValueWebImagePartRels); rels != nil { rels.mu.Lock() defer rels.mu.Unlock() for _, v := range rels.Relationships { diff --git a/picture.go b/picture.go index cc0c2b8..27d6b7f 100644 --- a/picture.go +++ b/picture.go @@ -31,6 +31,7 @@ type PictureInsertType int const ( PictureInsertTypePlaceOverCells PictureInsertType = iota PictureInsertTypePlaceInCell + PictureInsertTypeIMAGE PictureInsertTypeDISPIMG ) @@ -450,8 +451,7 @@ func (f *File) addMedia(file []byte, ext string) string { // GetPictures provides a function to get picture meta info and raw content // embed in spreadsheet by given worksheet and cell name. This function // returns the image contents as []byte data types. This function is -// concurrency safe. Note that, this function doesn't support getting cell image -// inserted by IMAGE formula function currently. For example: +// concurrency safe. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -507,8 +507,7 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } // GetPictureCells returns all picture cell references in a worksheet by a -// specific worksheet name. Note that, this function doesn't support getting -// cell image inserted by IMAGE formula function currently. +// specific worksheet name. func (f *File) GetPictureCells(sheet string) ([]string, error) { f.mu.Lock() ws, err := f.workSheetReader(sheet) @@ -812,7 +811,7 @@ func (f *File) getImageCells(sheet string) ([]string, error) { } cells = append(cells, c.R) } - r, err := f.getImageCellRel(&c) + r, err := f.getImageCellRel(&c, &Picture{}) if err != nil { return cells, err } @@ -825,30 +824,52 @@ func (f *File) getImageCells(sheet string) ([]string, error) { return cells, err } -// getImageCellRichValueIdx returns index of the cell image rich value by given -// cell value meta index and meta blocks. -func (f *File) getImageCellRichValueIdx(vm uint, blocks *xlsxMetadataBlocks) (int, error) { - richValueIdx := blocks.Bk[vm-1].Rc[0].V - richValue, err := f.richValueReader() +// getRichDataRichValueRel returns relationship of the cell image by given meta +// blocks value. +func (f *File) getRichDataRichValueRel(val string) (*xlsxRelationship, error) { + var r *xlsxRelationship + idx, err := strconv.Atoi(val) if err != nil { - return -1, err + return r, err } - if richValueIdx >= len(richValue.Rv) { - return -1, err - } - rv := richValue.Rv[richValueIdx].V - if len(rv) != 2 || rv[1] != "5" { - return -1, err - } - richValueRelIdx, err := strconv.Atoi(rv[0]) + richValueRel, err := f.richValueRelReader() if err != nil { - return -1, err + return r, err } - return richValueRelIdx, err + if idx >= len(richValueRel.Rels) { + return r, err + } + rID := richValueRel.Rels[idx].ID + if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage { + return nil, err + } + return r, err +} + +// getRichDataWebImagesRel returns relationship of a web image by given meta +// blocks value. +func (f *File) getRichDataWebImagesRel(val string) (*xlsxRelationship, error) { + var r *xlsxRelationship + idx, err := strconv.Atoi(val) + if err != nil { + return r, err + } + richValueWebImages, err := f.richValueWebImageReader() + if err != nil { + return r, err + } + if idx >= len(richValueWebImages.WebImageSrd) { + return r, err + } + rID := richValueWebImages.WebImageSrd[idx].Blip.RID + if r = f.getRichValueWebImageRelationships(rID); r != nil && r.Type != SourceRelationshipImage { + return nil, err + } + return r, err } // getImageCellRel returns the cell image relationship. -func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { +func (f *File) getImageCellRel(c *xlsxC, pic *Picture) (*xlsxRelationship, error) { var r *xlsxRelationship if c.Vm == nil || c.V != formulaErrorVALUE { return r, nil @@ -861,20 +882,23 @@ func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 { return r, err } - richValueRelIdx, err := f.getImageCellRichValueIdx(*c.Vm, vmd) - if err != nil || richValueRelIdx == -1 { - return r, err - } - richValueRel, err := f.richValueRelReader() + richValueIdx := vmd.Bk[*c.Vm-1].Rc[0].V + richValue, err := f.richValueReader() if err != nil { return r, err } - if richValueRelIdx >= len(richValueRel.Rels) { + if richValueIdx >= len(richValue.Rv) { return r, err } - rID := richValueRel.Rels[richValueRelIdx].ID - if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage { - return nil, err + rv := richValue.Rv[richValueIdx].V + if len(rv) == 2 && rv[1] == "5" { + pic.InsertType = PictureInsertTypePlaceInCell + return f.getRichDataRichValueRel(rv[0]) + } + // cell image inserted by IMAGE formula function + if len(rv) > 3 && rv[1]+rv[2] == "10" { + pic.InsertType = PictureInsertTypeIMAGE + return f.getRichDataWebImagesRel(rv[0]) } return r, err } @@ -888,11 +912,12 @@ func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { return pics, err } _, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { - r, err := f.getImageCellRel(c) + pic := Picture{Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell} + r, err := f.getImageCellRel(c, &pic) if err != nil || r == nil { return "", true, err } - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell} + pic.Extension = filepath.Ext(r.Target) if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil { pic.File = buffer.([]byte) pics = append(pics, pic) diff --git a/picture_test.go b/picture_test.go index bb90634..23054c1 100644 --- a/picture_test.go +++ b/picture_test.go @@ -246,7 +246,7 @@ func TestGetPicture(t *testing.T) { 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(``)) + f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) pics, err = f.GetPictures("Sheet1", "F21") assert.NoError(t, err) assert.Len(t, pics, 2) @@ -457,9 +457,9 @@ func TestGetCellImages(t *testing.T) { f := NewFile() assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) f.Pkg.Store(defaultXMLMetadata, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`05`)) - f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`05`)) + f.Pkg.Store(defaultXMLRdRichValueRel, []byte(``)) + f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ SheetData: xlsxSheetData{Row: []xlsxRow{ {R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}}, @@ -477,19 +477,19 @@ func TestGetCellImages(t *testing.T) { assert.Equal(t, []string{"A1"}, cells) // Test get the cell images without image relationships parts - f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) + f.Relationships.Delete(defaultXMLRdRichValueRelRels) + f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) // Test get the cell images with unsupported charset rich data rich value relationships - f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, MacintoshCyrillicCharset) + f.Relationships.Delete(defaultXMLRdRichValueRelRels) + f.Pkg.Store(defaultXMLRdRichValueRelRels, MacintoshCyrillicCharset) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) // Test get the cell images with unsupported charset rich data rich value - f.Pkg.Store(defaultXMLRichDataRichValueRel, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLRdRichValueRel, MacintoshCyrillicCharset) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") // Test get the image cells without block of metadata records @@ -498,7 +498,7 @@ func TestGetCellImages(t *testing.T) { assert.Empty(t, cells) // Test get the cell images with rich data rich value relationships f.Pkg.Store(defaultXMLMetadata, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) + f.Pkg.Store(defaultXMLRdRichValueRel, []byte(``)) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) @@ -514,17 +514,17 @@ func TestGetCellImages(t *testing.T) { f = prepareWorkbook() // Test get the cell images with empty image cell rich value - f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`5`)) + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`5`)) pics, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax") assert.Empty(t, pics) // Test get the cell images without image cell rich value - f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`01`)) + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`01`)) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) // Test get the cell images with unsupported charset rich value - f.Pkg.Store(defaultXMLRichDataRichValue, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLRdRichValuePart, MacintoshCyrillicCharset) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") @@ -534,6 +534,44 @@ func TestGetCellImages(t *testing.T) { pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) + + f = prepareWorkbook() + // Test get the cell images inserted by IMAGE formula function + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`0100`)) + f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`
+ `)) + f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink, SourceRelationshipImage))) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 1, len(pics)) + assert.Equal(t, PictureInsertTypeIMAGE, pics[0].InsertType) + + // Test get the cell images inserted by IMAGE formula function with unsupported charset web images relationships + f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels) + f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, MacintoshCyrillicCharset) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + + // Test get the cell images inserted by IMAGE formula function without image part + f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels) + f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink, SourceRelationshipHyperLink))) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the cell images inserted by IMAGE formula function with unsupported charset web images part + f.Pkg.Store(defaultXMLRdRichValueWebImagePart, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get the cell images inserted by IMAGE formula function with empty charset web images part + f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(``)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the cell images inserted by IMAGE formula function with invalid rich value index + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`100`)) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax") } func TestGetImageCells(t *testing.T) { diff --git a/templates.go b/templates.go index 7d72e3b..3bf4c5f 100644 --- a/templates.go +++ b/templates.go @@ -266,23 +266,25 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp } const ( - defaultTempFileSST = "sharedStrings" - defaultXMLMetadata = "xl/metadata.xml" - 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" - defaultXMLRichDataRichValue = "xl/richData/rdrichvalue.xml" - defaultXMLRichDataRichValueRel = "xl/richData/richValueRel.xml" - defaultXMLRichDataRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" + defaultTempFileSST = "sharedStrings" + defaultXMLMetadata = "xl/metadata.xml" + 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" + defaultXMLRdRichValuePart = "xl/richData/rdrichvalue.xml" + defaultXMLRdRichValueRel = "xl/richData/richValueRel.xml" + defaultXMLRdRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" + defaultXMLRdRichValueWebImagePart = "xl/richData/rdRichValueWebImage.xml" + defaultXMLRdRichValueWebImagePartRels = "xl/richData/_rels/rdRichValueWebImage.xml.rels" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlMetaData.go b/xmlMetaData.go index b3f97b6..016e348 100644 --- a/xmlMetaData.go +++ b/xmlMetaData.go @@ -98,3 +98,20 @@ type xlsxRichValueRels struct { type xlsxRichValueRelRelationship struct { ID string `xml:"id,attr"` } + +// xlsxWebImagesSupportingRichData directly maps the webImagesSrd element. This +// element specifies a list of sets of properties associated with web image rich +// values. +type xlsxWebImagesSupportingRichData struct { + XMLName xml.Name `xml:"webImagesSrd"` + WebImageSrd []xlsxWebImageSupportingRichData `xml:"webImageSrd"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxWebImageSupportingRichData directly maps the webImageSrd element. This +// element specifies a set of properties for a web image rich value. +type xlsxWebImageSupportingRichData struct { + Address xlsxExternalReference `xml:"address"` + MoreImagesAddress xlsxExternalReference `xml:"moreImagesAddress"` + Blip xlsxExternalReference `xml:"blip"` +}