Skip to content

Commit 838232f

Browse files
authored
Add support for get the Microsoft 365 cell images (#1857)
- Update unit tests
1 parent 703b737 commit 838232f

File tree

5 files changed

+259
-22
lines changed

5 files changed

+259
-22
lines changed

excelize.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,41 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error {
589589
}
590590
return err
591591
}
592+
593+
// metadataReader provides a function to get the pointer to the structure
594+
// after deserialization of xl/metadata.xml.
595+
func (f *File) metadataReader() (*xlsxMetadata, error) {
596+
var mataData xlsxMetadata
597+
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLMetadata)))).
598+
Decode(&mataData); err != nil && err != io.EOF {
599+
return &mataData, err
600+
}
601+
return &mataData, nil
602+
}
603+
604+
// richValueRelReader provides a function to get the pointer to the structure
605+
// after deserialization of xl/richData/richValueRel.xml.
606+
func (f *File) richValueRelReader() (*xlsxRichValueRels, error) {
607+
var richValueRels xlsxRichValueRels
608+
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValueRel)))).
609+
Decode(&richValueRels); err != nil && err != io.EOF {
610+
return &richValueRels, err
611+
}
612+
return &richValueRels, nil
613+
}
614+
615+
// getRichDataRichValueRelRelationships provides a function to get drawing
616+
// relationships from xl/richData/_rels/richValueRel.xml.rels by given
617+
// relationship ID.
618+
func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship {
619+
if rels, _ := f.relsReader(defaultXMLRichDataRichValueRelRels); rels != nil {
620+
rels.mu.Lock()
621+
defer rels.mu.Unlock()
622+
for _, v := range rels.Relationships {
623+
if v.ID == rID {
624+
return &v
625+
}
626+
}
627+
}
628+
return nil
629+
}

picture.go

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -497,13 +497,13 @@ func (f *File) GetPictureCells(sheet string) ([]string, error) {
497497
}
498498
f.mu.Unlock()
499499
if ws.Drawing == nil {
500-
return f.getEmbeddedImageCells(sheet)
500+
return f.getImageCells(sheet)
501501
}
502502
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
503503
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
504504
drawingRelationships := strings.ReplaceAll(
505505
strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
506-
embeddedImageCells, err := f.getEmbeddedImageCells(sheet)
506+
embeddedImageCells, err := f.getImageCells(sheet)
507507
if err != nil {
508508
return nil, err
509509
}
@@ -771,9 +771,9 @@ func (f *File) cellImagesReader() (*decodeCellImages, error) {
771771
return f.DecodeCellImages, nil
772772
}
773773

774-
// getEmbeddedImageCells returns all the Kingsoft WPS Office embedded image
775-
// cells reference by given worksheet name.
776-
func (f *File) getEmbeddedImageCells(sheet string) ([]string, error) {
774+
// getImageCells returns all the Microsoft 365 cell images and the Kingsoft WPS
775+
// Office embedded image cells reference by given worksheet name.
776+
func (f *File) getImageCells(sheet string) ([]string, error) {
777777
var (
778778
err error
779779
cells []string
@@ -791,14 +791,73 @@ func (f *File) getEmbeddedImageCells(sheet string) ([]string, error) {
791791
}
792792
cells = append(cells, c.R)
793793
}
794+
r, err := f.getImageCellRel(&c)
795+
if err != nil {
796+
return cells, err
797+
}
798+
if r != nil {
799+
cells = append(cells, c.R)
800+
}
801+
794802
}
795803
}
796804
return cells, err
797805
}
798806

799-
// getCellImages provides a function to get the Kingsoft WPS Office embedded
800-
// cell images by given worksheet name and cell reference.
807+
// getImageCellRel returns the Microsoft 365 cell image relationship.
808+
func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) {
809+
var r *xlsxRelationship
810+
if c.Vm == nil || c.V != formulaErrorVALUE {
811+
return r, nil
812+
}
813+
metaData, err := f.metadataReader()
814+
if err != nil {
815+
return r, err
816+
}
817+
vmd := metaData.ValueMetadata
818+
if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 {
819+
return r, err
820+
}
821+
richValueRel, err := f.richValueRelReader()
822+
if err != nil {
823+
return r, err
824+
}
825+
if vmd.Bk[*c.Vm-1].Rc[0].V >= len(richValueRel.Rels) {
826+
return r, err
827+
}
828+
rID := richValueRel.Rels[vmd.Bk[*c.Vm-1].Rc[0].V].ID
829+
if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
830+
return nil, err
831+
}
832+
return r, err
833+
}
834+
835+
// getCellImages provides a function to get the Microsoft 365 cell images and
836+
// the Kingsoft WPS Office embedded cell images by given worksheet name and cell
837+
// reference.
801838
func (f *File) getCellImages(sheet, cell string) ([]Picture, error) {
839+
pics, err := f.getDispImages(sheet, cell)
840+
if err != nil {
841+
return pics, err
842+
}
843+
_, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
844+
r, err := f.getImageCellRel(c)
845+
if err != nil || r == nil {
846+
return "", true, err
847+
}
848+
pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}}
849+
if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil {
850+
pic.File = buffer.([]byte)
851+
pics = append(pics, pic)
852+
}
853+
return "", true, nil
854+
})
855+
return pics, err
856+
}
857+
858+
// getDispImages provides a function to get the Kingsoft WPS Office embedded
859+
// cell images by given worksheet name and cell reference.
860+
func (f *File) getDispImages(sheet, cell string) ([]Picture, error) {
802861
formula, err := f.GetCellFormula(sheet, cell)
803862
if err != nil {
804863
return nil, err

picture_test.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,67 @@ func TestGetCellImages(t *testing.T) {
448448
_, err := f.getCellImages("Sheet1", "A1")
449449
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
450450
assert.NoError(t, f.Close())
451+
452+
// Test get the Microsoft 365 cell images
453+
f = NewFile()
454+
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
455+
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
456+
f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(`<richValueRels><rel r:id="rId1"/></richValueRels>`))
457+
f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipImage)))
458+
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
459+
SheetData: xlsxSheetData{Row: []xlsxRow{
460+
{R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}},
461+
}},
462+
})
463+
pics, err := f.GetPictures("Sheet1", "A1")
464+
assert.NoError(t, err)
465+
assert.Equal(t, 1, len(pics))
466+
cells, err := f.GetPictureCells("Sheet1")
467+
assert.NoError(t, err)
468+
assert.Equal(t, []string{"A1"}, cells)
469+
470+
// Test get the Microsoft 365 cell images without image relationships parts
471+
f.Relationships.Delete(defaultXMLRichDataRichValueRelRels)
472+
f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink)))
473+
pics, err = f.GetPictures("Sheet1", "A1")
474+
assert.NoError(t, err)
475+
assert.Empty(t, pics)
476+
// Test get the Microsoft 365 cell images with unsupported charset rich data rich value relationships
477+
f.Relationships.Delete(defaultXMLRichDataRichValueRelRels)
478+
f.Pkg.Store(defaultXMLRichDataRichValueRelRels, MacintoshCyrillicCharset)
479+
pics, err = f.GetPictures("Sheet1", "A1")
480+
assert.NoError(t, err)
481+
assert.Empty(t, pics)
482+
// Test get the Microsoft 365 cell images with unsupported charset rich data rich value
483+
f.Pkg.Store(defaultXMLRichDataRichValueRel, MacintoshCyrillicCharset)
484+
_, err = f.GetPictures("Sheet1", "A1")
485+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
486+
// Test get the Microsoft 365 image cells without block of metadata records
487+
cells, err = f.GetPictureCells("Sheet1")
488+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
489+
assert.Empty(t, cells)
490+
// Test get the Microsoft 365 cell images with rich data rich value relationships
491+
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
492+
f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(`<richValueRels/>`))
493+
pics, err = f.GetPictures("Sheet1", "A1")
494+
assert.NoError(t, err)
495+
assert.Empty(t, pics)
496+
// Test get the Microsoft 365 cell images with unsupported charset meta data
497+
f.Pkg.Store(defaultXMLMetadata, MacintoshCyrillicCharset)
498+
_, err = f.GetPictures("Sheet1", "A1")
499+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
500+
// Test get the Microsoft 365 cell images without block of metadata records
501+
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata/></metadata>`))
502+
pics, err = f.GetPictures("Sheet1", "A1")
503+
assert.NoError(t, err)
504+
assert.Empty(t, pics)
451505
}
452506

453-
func TestGetEmbeddedImageCells(t *testing.T) {
507+
func TestGetImageCells(t *testing.T) {
454508
f := NewFile()
455509
f.Sheet.Delete("xl/worksheets/sheet1.xml")
456510
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
457-
_, err := f.getEmbeddedImageCells("Sheet1")
511+
_, err := f.getImageCells("Sheet1")
458512
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
459513
assert.NoError(t, f.Close())
460514
}

templates.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,22 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp
266266
}
267267

268268
const (
269-
defaultTempFileSST = "sharedStrings"
270-
defaultXMLPathCalcChain = "xl/calcChain.xml"
271-
defaultXMLPathCellImages = "xl/cellimages.xml"
272-
defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels"
273-
defaultXMLPathContentTypes = "[Content_Types].xml"
274-
defaultXMLPathDocPropsApp = "docProps/app.xml"
275-
defaultXMLPathDocPropsCore = "docProps/core.xml"
276-
defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
277-
defaultXMLPathStyles = "xl/styles.xml"
278-
defaultXMLPathTheme = "xl/theme/theme1.xml"
279-
defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml"
280-
defaultXMLPathWorkbook = "xl/workbook.xml"
281-
defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels"
269+
defaultTempFileSST = "sharedStrings"
270+
defaultXMLMetadata = "xl/metadata.xml"
271+
defaultXMLPathCalcChain = "xl/calcChain.xml"
272+
defaultXMLPathCellImages = "xl/cellimages.xml"
273+
defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels"
274+
defaultXMLPathContentTypes = "[Content_Types].xml"
275+
defaultXMLPathDocPropsApp = "docProps/app.xml"
276+
defaultXMLPathDocPropsCore = "docProps/core.xml"
277+
defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
278+
defaultXMLPathStyles = "xl/styles.xml"
279+
defaultXMLPathTheme = "xl/theme/theme1.xml"
280+
defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml"
281+
defaultXMLPathWorkbook = "xl/workbook.xml"
282+
defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels"
283+
defaultXMLRichDataRichValueRel = "xl/richData/richValueRel.xml"
284+
defaultXMLRichDataRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels"
282285
)
283286

284287
// IndexedColorMapping is the table of default mappings from indexed color value

xmlMetaData.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
2+
// this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
//
5+
// Package excelize providing a set of functions that allow you to write to and
6+
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
7+
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
8+
// Supports complex components by high compatibility, and provided streaming
9+
// API for generating or reading data from a worksheet with huge amounts of
10+
// data. This library needs Go version 1.18 or later.
11+
12+
package excelize
13+
14+
import "encoding/xml"
15+
16+
// xlsxMetadata directly maps the metadata element. A cell in a spreadsheet
17+
// application can have metadata associated with it. Metadata is just a set of
18+
// additional properties about the particular cell, and this metadata is stored
19+
// in the metadata xml part. There are two types of metadata: cell metadata and
20+
// value metadata. Cell metadata contains information about the cell itself,
21+
// and this metadata can be carried along with the cell as it moves
22+
// (insert, shift, copy/paste, merge, unmerge, etc). Value metadata is
23+
// information about the value of a particular cell. Value metadata properties
24+
// can be propagated along with the value as it is referenced in formulas.
25+
type xlsxMetadata struct {
26+
XMLName xml.Name `xml:"metadata"`
27+
MetadataTypes *xlsxInnerXML `xml:"metadataTypes"`
28+
MetadataStrings *xlsxInnerXML `xml:"metadataStrings"`
29+
MdxMetadata *xlsxInnerXML `xml:"mdxMetadata"`
30+
FutureMetadata []xlsxFutureMetadata `xml:"futureMetadata"`
31+
CellMetadata *xlsxMetadataBlocks `xml:"cellMetadata"`
32+
ValueMetadata *xlsxMetadataBlocks `xml:"valueMetadata"`
33+
ExtLst *xlsxInnerXML `xml:"extLst"`
34+
}
35+
36+
// xlsxFutureMetadata directly maps the futureMetadata element. This element
37+
// represents future metadata information.
38+
type xlsxFutureMetadata struct {
39+
Bk []xlsxFutureMetadataBlock `xml:"bk"`
40+
ExtLst *xlsxInnerXML `xml:"extLst"`
41+
}
42+
43+
// xlsxFutureMetadataBlock directly maps the kb element. This element represents
44+
// a block of future metadata information. This is a location for storing
45+
// feature extension information.
46+
type xlsxFutureMetadataBlock struct {
47+
ExtLst *xlsxInnerXML `xml:"extLst"`
48+
}
49+
50+
// xlsxMetadataBlocks directly maps the metadata element. This element
51+
// represents cell metadata information. Cell metadata is information metadata
52+
// about a specific cell, and it stays tied to that cell position.
53+
type xlsxMetadataBlocks struct {
54+
Count int `xml:"count,attr,omitempty"`
55+
Bk []xlsxMetadataBlock `xml:"bk"`
56+
}
57+
58+
// xlsxMetadataBlock directly maps the bk element. This element represents a
59+
// block of metadata records.
60+
type xlsxMetadataBlock struct {
61+
Rc []xlsxMetadataRecord `xml:"rc"`
62+
}
63+
64+
// xlsxMetadataRecord directly maps the rc element. This element represents a
65+
// reference to a specific metadata record.
66+
type xlsxMetadataRecord struct {
67+
T int `xml:"t,attr"`
68+
V int `xml:"v,attr"`
69+
}
70+
71+
// xlsxRichValueRels directly maps the richValueRels element. This element that
72+
// specifies a list of rich value relationships.
73+
type xlsxRichValueRels struct {
74+
XMLName xml.Name `xml:"richValueRels"`
75+
Rels []xlsxRichValueRelRelationship `xml:"rel"`
76+
ExtLst *xlsxInnerXML `xml:"extLst"`
77+
}
78+
79+
// xlsxRichValueRelRelationship directly maps the rel element. This element
80+
// specifies a relationship for a rich value property.
81+
type xlsxRichValueRelRelationship struct {
82+
ID string `xml:"id,attr"`
83+
}

0 commit comments

Comments
 (0)