diff --git a/go.mod b/go.mod index 22ba8e1..48848c6 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.29.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.30.0 - golang.org/x/text v0.19.0 + golang.org/x/net v0.31.0 + golang.org/x/text v0.20.0 ) require ( diff --git a/go.sum b/go.sum index 33f90a0..2c4284e 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/sheet.go b/sheet.go index 5bff9b7..f57797b 100644 --- a/sheet.go +++ b/sheet.go @@ -1239,7 +1239,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // | // &F | Current workbook's file name // | -// &G | Drawing object as background (Use SetLegacyDrawingHF) +// &G | Drawing object as background (Use AddHeaderFooterImage) // | // &H | Shadow text format // | diff --git a/vml.go b/vml.go index 53ef815..d8fcbb0 100644 --- a/vml.go +++ b/vml.go @@ -36,6 +36,16 @@ const ( FormControlScrollBar ) +// HeaderFooterImagePositionType is the type of header and footer image position. +type HeaderFooterImagePositionType byte + +// Worksheet header and footer image position types enumeration. +const ( + HeaderFooterImagePositionLeft HeaderFooterImagePositionType = iota + HeaderFooterImagePositionCenter + HeaderFooterImagePositionRight +) + // GetComments retrieves all comments in a worksheet by given worksheet name. func (f *File) GetComments(sheet string) ([]Comment, error) { var comments []Comment @@ -519,6 +529,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { } vmlID = f.countVMLDrawing() + 1 } + sheetID := f.getSheetID(opts.sheet) drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetXMLPath, _ := f.getSheetXMLPath(opts.sheet) @@ -534,7 +545,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { f.addSheetNameSpace(opts.sheet, SourceRelationship) f.addSheetLegacyDrawing(opts.sheet, rID) } - if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { + if err = f.addDrawingVML(sheetID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { return err } if !opts.formCtrl { @@ -823,7 +834,7 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri // anchor value is a comma-separated list of data written out as: LeftColumn, // LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, // BottomOffset. -func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) error { +func (f *File) addDrawingVML(sheetID int, drawingVML string, opts *vmlOptions) error { col, row, err := CellNameToCoordinates(opts.FormControl.Cell) if err != nil { return err @@ -843,7 +854,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er XMLNSx: "urn:schemas-microsoft-com:office:excel", XMLNSmv: "http://macVmlSchemaUri", ShapeLayout: &xlsxShapeLayout{ - Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: dataID}, + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID}, }, ShapeType: &xlsxShapeType{ ID: fmt.Sprintf("_x0000_t%d", vmlID), @@ -1071,79 +1082,138 @@ func extractVMLFont(font []decodeVMLFont) []RichTextRun { return runs } -// SetLegacyDrawingHF provides a mechanism to set the graphics that -// can be referenced in the Header/Footer defitions via &G. +// AddHeaderFooterImage provides a mechanism to set the graphics that can be +// referenced in the header and footer definitions via &G, file base name, +// extension name and file bytes, supported image types: EMF, EMZ, GIF, JPEG, +// JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. // // The extension should be provided with a "." in front, e.g. ".png". -// The width/height should have units in them, e.g. "100pt". -func (f *File) SetLegacyDrawingHF(sheet string, g *HeaderFooterGraphics) error { +// The width and height should have units in them, e.g. "100pt". +func (f *File) AddHeaderFooterImage(sheet string, opts *HeaderFooterImageOptions) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ext, ok := supportedImageTypes[strings.ToLower(opts.Extension)] + if !ok { + return ErrImgExt + } + sheetID := f.getSheetID(sheet) vmlID := f.countVMLDrawing() + 1 - - vml := &vmlDrawing{ - XMLNSv: "urn:schemas-microsoft-com:vml", - XMLNSo: "urn:schemas-microsoft-com:office:office", - XMLNSx: "urn:schemas-microsoft-com:office:excel", - ShapeLayout: &xlsxShapeLayout{ - Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID}, - }, - ShapeType: &xlsxShapeType{ - ID: "_x0000_t75", - CoordSize: "21600,21600", - Spt: 75, - PreferRelative: "t", - Path: "m@4@5l@4@11@9@11@9@5xe", - Filled: "f", - Stroked: "f", - Stroke: &xlsxStroke{JoinStyle: "miter"}, - VFormulas: &vFormulas{ - Formulas: []vFormula{ - {Equation: "if lineDrawn pixelLineWidth 0"}, - {Equation: "sum @0 1 0"}, - {Equation: "sum 0 0 @1"}, - {Equation: "prod @2 1 2"}, - {Equation: "prod @3 21600 pixelWidth"}, - {Equation: "prod @3 21600 pixelHeight"}, - {Equation: "sum @0 0 1"}, - {Equation: "prod @6 1 2"}, - {Equation: "prod @7 21600 pixelWidth"}, - {Equation: "sum @8 21600 0"}, - {Equation: "prod @7 21600 pixelHeight"}, - {Equation: "sum @10 21600 0"}, - }, - }, - VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"}, - Lock: &oLock{Ext: "edit", AspectRatio: "t"}, - }, - } - - style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", g.Width, g.Height) drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" - drawingVMLRels := "xl/drawings/_rels/vmlDrawing" + strconv.Itoa(vmlID) + ".vml.rels" - - mediaStr := ".." + strings.TrimPrefix(f.addMedia(g.File, g.Extension), "xl") - imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "") - - shape := xlsxShape{ - ID: "RH", - Spid: "_x0000_s1025", - Type: "#_x0000_t75", - Style: style, - } - s, _ := xml.Marshal(encodeShape{ - ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)}, - Lock: &oLock{Ext: "edit", Rotation: "t"}, - }) - shape.Val = string(s[13 : len(s)-14]) - vml.Shape = append(vml.Shape, shape) - f.VMLDrawing[drawingVML] = vml - sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetXMLPath, _ := f.getSheetXMLPath(sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" + if ws.LegacyDrawingHF != nil { + // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawingHF.RID) + vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) + drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") + } else { + // Add first VML drawing for given sheet. + rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + f.addSheetNameSpace(sheet, SourceRelationship) + f.addSheetLegacyDrawingHF(sheet, rID) + } + + shapeID := map[HeaderFooterImagePositionType]string{ + HeaderFooterImagePositionLeft: "L", + HeaderFooterImagePositionCenter: "C", + HeaderFooterImagePositionRight: "R", + }[opts.Position] + + map[bool]string{false: "H", true: "F"}[opts.IsFooter] + + map[bool]string{false: "", true: "FIRST"}[opts.FirstPage] + vml := f.VMLDrawing[drawingVML] + if vml == nil { + vml = &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID}, + }, + ShapeType: &xlsxShapeType{ + ID: "_x0000_t75", + CoordSize: "21600,21600", + Spt: 75, + PreferRelative: "t", + Path: "m@4@5l@4@11@9@11@9@5xe", + Filled: "f", + Stroked: "f", + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VFormulas: &vFormulas{ + Formulas: []vFormula{ + {Equation: "if lineDrawn pixelLineWidth 0"}, + {Equation: "sum @0 1 0"}, + {Equation: "sum 0 0 @1"}, + {Equation: "prod @2 1 2"}, + {Equation: "prod @3 21600 pixelWidth"}, + {Equation: "prod @3 21600 pixelHeight"}, + {Equation: "sum @0 0 1"}, + {Equation: "prod @6 1 2"}, + {Equation: "prod @7 21600 pixelWidth"}, + {Equation: "sum @8 21600 0"}, + {Equation: "prod @7 21600 pixelHeight"}, + {Equation: "sum @10 21600 0"}, + }, + }, + VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"}, + Lock: &oLock{Ext: "edit", AspectRatio: "t"}, + }, + } + // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return err + } + if d != nil { + vml.ShapeType.ID = d.ShapeType.ID + vml.ShapeType.CoordSize = d.ShapeType.CoordSize + vml.ShapeType.Spt = d.ShapeType.Spt + vml.ShapeType.PreferRelative = d.ShapeType.PreferRelative + vml.ShapeType.Path = d.ShapeType.Path + vml.ShapeType.Filled = d.ShapeType.Filled + vml.ShapeType.Stroked = d.ShapeType.Stroked + for _, v := range d.Shape { + s := xlsxShape{ + ID: v.ID, + SpID: v.SpID, + Type: v.Type, + Style: v.Style, + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } + } + + for idx, shape := range vml.Shape { + if shape.ID == shapeID { + vml.Shape = append(vml.Shape[:idx], vml.Shape[idx+1:]...) + } + } + + style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", opts.Width, opts.Height) + drawingVMLRels := "xl/drawings/_rels/vmlDrawing" + strconv.Itoa(vmlID) + ".vml.rels" + + mediaStr := ".." + strings.TrimPrefix(f.addMedia(opts.File, ext), "xl") + imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "") + + shape := xlsxShape{ + ID: shapeID, + SpID: "_x0000_s1025", + Type: "#_x0000_t75", + Style: style, + } + sp, _ := xml.Marshal(encodeShape{ + ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)}, + Lock: &oLock{Ext: "edit", Rotation: "t"}, + }) + + shape.Val = string(sp[13 : len(sp)-14]) + vml.Shape = append(vml.Shape, shape) + f.VMLDrawing[drawingVML] = vml - drawingID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addSheetNameSpace(sheet, SourceRelationship) - f.addSheetLegacyDrawingHF(sheet, drawingID) if err := f.setContentTypePartImageExtensions(); err != nil { return err } diff --git a/vmlDrawing.go b/vmlDrawing.go index c021018..182b531 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -44,7 +44,7 @@ type xlsxIDmap struct { type xlsxShape struct { XMLName xml.Name `xml:"v:shape"` ID string `xml:"id,attr"` - Spid string `xml:"o:spid,attr,omitempty"` + SpID string `xml:"o:spid,attr,omitempty"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` Button string `xml:"o:button,attr,omitempty"` @@ -193,15 +193,19 @@ type decodeVmlDrawing struct { // decodeShapeType defines the structure used to parse the shapetype element in // the file xl/drawings/vmlDrawing%d.vml. type decodeShapeType struct { - ID string `xml:"id,attr"` - CoordSize string `xml:"coordsize,attr"` - Spt int `xml:"spt,attr"` - Path string `xml:"path,attr"` + ID string `xml:"id,attr"` + CoordSize string `xml:"coordsize,attr"` + Spt int `xml:"spt,attr"` + PreferRelative string `xml:"preferrelative,attr,omitempty"` + Path string `xml:"path,attr"` + Filled string `xml:"filled,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` } // decodeShape defines the structure used to parse the particular shape element. type decodeShape struct { ID string `xml:"id,attr"` + SpID string `xml:"spid,attr,omitempty"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` Button string `xml:"button,attr,omitempty"` @@ -335,10 +339,13 @@ type FormControl struct { Format GraphicOptions } -// HeaderFooterGraphics defines the settings for an image to be -// accessible from the header/footer options. -type HeaderFooterGraphics struct { +// HeaderFooterImageOptions defines the settings for an image to be accessible +// from the worksheet header and footer options. +type HeaderFooterImageOptions struct { + Position HeaderFooterImagePositionType File []byte + IsFooter bool + FirstPage bool Extension string Width string Height string diff --git a/vml_test.go b/vml_test.go index b79e63d..50571a1 100644 --- a/vml_test.go +++ b/vml_test.go @@ -413,32 +413,97 @@ func TestExtractFormControl(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } -func TestSetLegacyDrawingHF(t *testing.T) { - f := NewFile() - sheet := "Sheet1" +func TestAddHeaderFooterImage(t *testing.T) { + f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx") headerFooterOptions := HeaderFooterOptions{ - OddHeader: "&LExcelize&R&G", + DifferentFirst: true, + OddHeader: "&L&GExcelize&C&G&R&G", + OddFooter: "&L&GExcelize&C&G&R&G", + FirstHeader: "&L&GExcelize&C&G&R&G", + FirstFooter: "&L&GExcelize&C&G&R&G", } assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions)) - file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) + assert.NoError(t, f.SetSheetView(sheet, -1, &ViewOptions{View: stringPtr("pageLayout")})) + images := map[string][]byte{ + ".wmf": nil, ".tif": nil, ".png": nil, + ".jpg": nil, ".gif": nil, ".emz": nil, ".emf": nil, + } + for ext := range images { + img, err := os.ReadFile(filepath.Join("test", "images", "excel"+ext)) + assert.NoError(t, err) + images[ext] = img + } + for _, opt := range []struct { + position HeaderFooterImagePositionType + file []byte + isFooter bool + firstPage bool + ext string + }{ + {position: HeaderFooterImagePositionLeft, file: images[".tif"], firstPage: true, ext: ".tif"}, + {position: HeaderFooterImagePositionCenter, file: images[".gif"], firstPage: true, ext: ".gif"}, + {position: HeaderFooterImagePositionRight, file: images[".png"], firstPage: true, ext: ".png"}, + {position: HeaderFooterImagePositionLeft, file: images[".emf"], isFooter: true, firstPage: true, ext: ".emf"}, + {position: HeaderFooterImagePositionCenter, file: images[".wmf"], isFooter: true, firstPage: true, ext: ".wmf"}, + {position: HeaderFooterImagePositionRight, file: images[".emz"], isFooter: true, firstPage: true, ext: ".emz"}, + {position: HeaderFooterImagePositionLeft, file: images[".png"], ext: ".png"}, + {position: HeaderFooterImagePositionCenter, file: images[".png"], ext: ".png"}, + {position: HeaderFooterImagePositionRight, file: images[".png"], ext: ".png"}, + {position: HeaderFooterImagePositionLeft, file: images[".tif"], isFooter: true, ext: ".tif"}, + {position: HeaderFooterImagePositionCenter, file: images[".tif"], isFooter: true, ext: ".tif"}, + {position: HeaderFooterImagePositionRight, file: images[".tif"], isFooter: true, ext: ".tif"}, + } { + assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + Position: opt.position, + File: opt.file, + IsFooter: opt.isFooter, + FirstPage: opt.firstPage, + Extension: opt.ext, + Width: "50pt", + Height: "32pt", + })) + } + assert.NoError(t, f.SetCellValue(sheet, "A1", "Example")) + + // Test add header footer image with not exist sheet + assert.EqualError(t, f.AddHeaderFooterImage("SheetN", nil), "sheet SheetN does not exist") + // Test add header footer image with unsupported file type + assert.Equal(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + Extension: "jpg", + }), ErrImgExt) + assert.NoError(t, f.SaveAs(wb)) + assert.NoError(t, f.Close()) + // Test change already exist header image with the different image + f, err := OpenFile(wb) assert.NoError(t, err) - assert.NoError(t, f.SetLegacyDrawingHF(sheet, &HeaderFooterGraphics{ - Extension: ".png", - File: file, + assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + File: images[".jpg"], + FirstPage: true, + Extension: ".jpg", Width: "50pt", Height: "32pt", })) - assert.NoError(t, f.SetCellValue(sheet, "A1", "Example")) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetLegacyDrawingHF.xlsx"))) + assert.NoError(t, f.Save()) assert.NoError(t, f.Close()) + // Test add header image with unsupported charset VML drawing + f, err = OpenFile(wb) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + File: images[".jpg"], + Extension: ".jpg", + Width: "50pt", + Height: "32pt", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) // Test set legacy drawing header/footer with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.SetLegacyDrawingHF(sheet, &HeaderFooterGraphics{ + assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ Extension: ".png", - File: file, + File: images[".png"], Width: "50pt", Height: "32pt", }), "XML syntax error on line 1: invalid UTF-8")