This closes #664, support get embedded cell images (#1759)

Co-authored-by: liying05 <liying05@zhidemai.com>
This commit is contained in:
li 2023-12-15 13:09:42 +08:00 committed by GitHub
parent dfaf418f34
commit 00d62590f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 151 additions and 19 deletions

11
calc.go
View File

@ -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)
}

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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
}

View File

@ -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(`<etc:cellImages xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData"><etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="ID_********************************" descr="CellImage1"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/></xdr:blipFill></xdr:pic></etc:cellImage></etc:cellImages>`))
f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.jpeg"/></Relationships>`))
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())
}

View File

@ -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

View File

@ -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"`
}