From 2c8dc5c1504ad2bd209d07c21ea878734464fcba Mon Sep 17 00:00:00 2001 From: David Date: Tue, 11 Jul 2023 11:43:45 -0400 Subject: [PATCH] This closes #1169, initialize add form controls supported (#1181) - Breaking changes: * Change `func (f *File) AddShape(sheet, cell string, opts *Shape) error` to `func (f *File) AddShape(sheet string, opts *Shape) error` * Rename the `Runs` field to `Paragraph` in the exported `Comment` data type - Add new exported function `AddFormControl` support to add button and radio form controls - Add check for shape type for the `AddShape` function, an error will be returned if no shape type is specified - Updated functions documentation and the unit tests --- comment.go | 429 --------------------- excelize_test.go | 11 +- shape.go | 7 +- shape_test.go | 41 ++- test/vbaProject.bin | Bin 15360 -> 16384 bytes vml.go | 655 +++++++++++++++++++++++++++++++++ vmlDrawing.go | 120 ++++-- comment_test.go => vml_test.go | 96 ++++- xmlComments.go | 10 +- xmlDrawing.go | 1 + 10 files changed, 877 insertions(+), 493 deletions(-) delete mode 100644 comment.go create mode 100644 vml.go rename comment_test.go => vml_test.go (58%) diff --git a/comment.go b/comment.go deleted file mode 100644 index 28ba40bc..00000000 --- a/comment.go +++ /dev/null @@ -1,429 +0,0 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of -// this source code is governed by a BSD-style license that can be found in -// the LICENSE file. -// -// Package excelize providing a set of functions that allow you to write to and -// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and -// writing spreadsheet documents generated by Microsoft Excelâ„¢ 2007 and later. -// Supports complex components by high compatibility, and provided streaming -// API for generating or reading data from a worksheet with huge amounts of -// data. This library needs Go version 1.16 or later. - -package excelize - -import ( - "bytes" - "encoding/xml" - "fmt" - "io" - "path/filepath" - "strconv" - "strings" -) - -// GetComments retrieves all comments in a worksheet by given worksheet name. -func (f *File) GetComments(sheet string) ([]Comment, error) { - var comments []Comment - sheetXMLPath, ok := f.getSheetXMLPath(sheet) - if !ok { - return comments, newNoExistSheetError(sheet) - } - commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) - if !strings.HasPrefix(commentsXML, "/") { - commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..") - } - commentsXML = strings.TrimPrefix(commentsXML, "/") - cmts, err := f.commentsReader(commentsXML) - if err != nil { - return comments, err - } - if cmts != nil { - for _, cmt := range cmts.CommentList.Comment { - comment := Comment{} - if cmt.AuthorID < len(cmts.Authors.Author) { - comment.Author = cmts.Authors.Author[cmt.AuthorID] - } - comment.Cell = cmt.Ref - comment.AuthorID = cmt.AuthorID - if cmt.Text.T != nil { - comment.Text += *cmt.Text.T - } - for _, text := range cmt.Text.R { - if text.T != nil { - run := RichTextRun{Text: text.T.Val} - if text.RPr != nil { - run.Font = newFont(text.RPr) - } - comment.Runs = append(comment.Runs, run) - } - } - comments = append(comments, comment) - } - } - return comments, nil -} - -// getSheetComments provides the method to get the target comment reference by -// given worksheet file path. -func (f *File) getSheetComments(sheetFile string) string { - rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels") - if sheetRels := rels; sheetRels != nil { - sheetRels.mu.Lock() - defer sheetRels.mu.Unlock() - for _, v := range sheetRels.Relationships { - if v.Type == SourceRelationshipComments { - return v.Target - } - } - } - return "" -} - -// AddComment provides the method to add comment in a sheet by given worksheet -// index, cell and format set (such as author and text). Note that the max -// author length is 255 and the max text length is 32512. For example, add a -// comment in Sheet1!$A$30: -// -// err := f.AddComment("Sheet1", excelize.Comment{ -// Cell: "A12", -// Author: "Excelize", -// Runs: []excelize.RichTextRun{ -// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}}, -// {Text: "This is a comment."}, -// }, -// }) -func (f *File) AddComment(sheet string, comment Comment) error { - // Read sheet data. - ws, err := f.workSheetReader(sheet) - if err != nil { - return err - } - commentID := f.countComments() + 1 - drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" - sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml" - sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" - if ws.LegacyDrawing != nil { - // The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. - sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) - commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) - drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") - } else { - // Add first comment for given sheet. - sheetXMLPath, _ := f.getSheetXMLPath(sheet) - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" - rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") - f.addSheetNameSpace(sheet, SourceRelationship) - f.addSheetLegacyDrawing(sheet, rID) - } - commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" - var rows, cols int - for _, runs := range comment.Runs { - for _, subStr := range strings.Split(runs.Text, "\n") { - rows++ - if chars := len(subStr); chars > cols { - cols = chars - } - } - } - if len(comment.Runs) == 0 { - rows, cols = 1, len(comment.Text) - } - if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil { - return err - } - if err = f.addComment(commentsXML, comment); err != nil { - return err - } - return f.addContentTypePart(commentID, "comments") -} - -// DeleteComment provides the method to delete comment in a sheet by given -// worksheet name. For example, delete the comment in Sheet1!$A$30: -// -// err := f.DeleteComment("Sheet1", "A30") -func (f *File) DeleteComment(sheet, cell string) error { - if err := checkSheetName(sheet); err != nil { - return err - } - sheetXMLPath, ok := f.getSheetXMLPath(sheet) - if !ok { - return newNoExistSheetError(sheet) - } - commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) - if !strings.HasPrefix(commentsXML, "/") { - commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..") - } - commentsXML = strings.TrimPrefix(commentsXML, "/") - cmts, err := f.commentsReader(commentsXML) - if err != nil { - return err - } - if cmts != nil { - for i := 0; i < len(cmts.CommentList.Comment); i++ { - cmt := cmts.CommentList.Comment[i] - if cmt.Ref != cell { - continue - } - if len(cmts.CommentList.Comment) > 1 { - cmts.CommentList.Comment = append( - cmts.CommentList.Comment[:i], - cmts.CommentList.Comment[i+1:]..., - ) - i-- - continue - } - cmts.CommentList.Comment = nil - } - f.Comments[commentsXML] = cmts - } - return err -} - -// addDrawingVML provides a function to create comment as -// xl/drawings/vmlDrawing%d.vml by given commit ID and cell. -func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error { - col, row, err := CellNameToCoordinates(cell) - if err != nil { - return err - } - yAxis := col - 1 - xAxis := row - 1 - 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", - XMLNSmv: "http://macVmlSchemaUri", - Shapelayout: &xlsxShapelayout{ - Ext: "edit", - IDmap: &xlsxIDmap{ - Ext: "edit", - Data: commentID, - }, - }, - Shapetype: &xlsxShapetype{ - ID: "_x0000_t202", - Coordsize: "21600,21600", - Spt: 202, - Path: "m0,0l0,21600,21600,21600,21600,0xe", - Stroke: &xlsxStroke{ - Joinstyle: "miter", - }, - VPath: &vPath{ - Gradientshapeok: "t", - Connecttype: "rect", - }, - }, - } - // load exist comment shapes from xl/drawings/vmlDrawing%d.vml - d, err := f.decodeVMLDrawingReader(drawingVML) - if err != nil { - return err - } - if d != nil { - for _, v := range d.Shape { - s := xlsxShape{ - ID: "_x0000_s1025", - Type: "#_x0000_t202", - Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden", - Fillcolor: "#FBF6D6", - Strokecolor: "#EDEAA1", - Val: v.Val, - } - vml.Shape = append(vml.Shape, s) - } - } - } - sp := encodeShape{ - Fill: &vFill{ - Color2: "#FBFE82", - Angle: -180, - Type: "gradient", - Fill: &oFill{ - Ext: "view", - Type: "gradientUnscaled", - }, - }, - Shadow: &vShadow{ - On: "t", - Color: "black", - Obscured: "t", - }, - Path: &vPath{ - Connecttype: "none", - }, - Textbox: &vTextbox{ - Style: "mso-direction-alt:auto", - Div: &xlsxDiv{ - Style: "text-align:left", - }, - }, - ClientData: &xClientData{ - ObjectType: "Note", - Anchor: fmt.Sprintf( - "%d, 23, %d, 0, %d, %d, %d, 5", - 1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount), - AutoFill: "True", - Row: xAxis, - Column: yAxis, - }, - } - s, _ := xml.Marshal(sp) - shape := xlsxShape{ - ID: "_x0000_s1025", - Type: "#_x0000_t202", - Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden", - Fillcolor: "#FBF6D6", - Strokecolor: "#EDEAA1", - Val: string(s[13 : len(s)-14]), - } - vml.Shape = append(vml.Shape, shape) - f.VMLDrawing[drawingVML] = vml - return err -} - -// addComment provides a function to create chart as xl/comments%d.xml by -// given cell and format sets. -func (f *File) addComment(commentsXML string, comment Comment) error { - if comment.Author == "" { - comment.Author = "Author" - } - if len(comment.Author) > MaxFieldLength { - comment.Author = comment.Author[:MaxFieldLength] - } - cmts, err := f.commentsReader(commentsXML) - if err != nil { - return err - } - var authorID int - if cmts == nil { - cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}} - } - if inStrSlice(cmts.Authors.Author, comment.Author, true) == -1 { - cmts.Authors.Author = append(cmts.Authors.Author, comment.Author) - authorID = len(cmts.Authors.Author) - 1 - } - defaultFont, err := f.GetDefaultFont() - if err != nil { - return err - } - chars, cmt := 0, xlsxComment{ - Ref: comment.Cell, - AuthorID: authorID, - Text: xlsxText{R: []xlsxR{}}, - } - if comment.Text != "" { - if len(comment.Text) > TotalCellChars { - comment.Text = comment.Text[:TotalCellChars] - } - cmt.Text.T = stringPtr(comment.Text) - chars += len(comment.Text) - } - for _, run := range comment.Runs { - if chars == TotalCellChars { - break - } - if chars+len(run.Text) > TotalCellChars { - run.Text = run.Text[:TotalCellChars-chars] - } - chars += len(run.Text) - r := xlsxR{ - RPr: &xlsxRPr{ - Sz: &attrValFloat{Val: float64Ptr(9)}, - Color: &xlsxColor{ - Indexed: 81, - }, - RFont: &attrValString{Val: stringPtr(defaultFont)}, - Family: &attrValInt{Val: intPtr(2)}, - }, - T: &xlsxT{Val: run.Text, Space: xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", - }}, - } - if run.Font != nil { - r.RPr = newRpr(run.Font) - } - cmt.Text.R = append(cmt.Text.R, r) - } - cmts.CommentList.Comment = append(cmts.CommentList.Comment, cmt) - f.Comments[commentsXML] = cmts - return err -} - -// countComments provides a function to get comments files count storage in -// the folder xl. -func (f *File) countComments() int { - c1, c2 := 0, 0 - f.Pkg.Range(func(k, v interface{}) bool { - if strings.Contains(k.(string), "xl/comments") { - c1++ - } - return true - }) - for rel := range f.Comments { - if strings.Contains(rel, "xl/comments") { - c2++ - } - } - if c1 < c2 { - return c2 - } - return c1 -} - -// decodeVMLDrawingReader provides a function to get the pointer to the -// structure after deserialization of xl/drawings/vmlDrawing%d.xml. -func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) { - if f.DecodeVMLDrawing[path] == nil { - c, ok := f.Pkg.Load(path) - if ok && c != nil { - f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))). - Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { - return nil, err - } - } - } - return f.DecodeVMLDrawing[path], nil -} - -// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml -// after serialize structure. -func (f *File) vmlDrawingWriter() { - for path, vml := range f.VMLDrawing { - if vml != nil { - v, _ := xml.Marshal(vml) - f.Pkg.Store(path, v) - } - } -} - -// commentsReader provides a function to get the pointer to the structure -// after deserialization of xl/comments%d.xml. -func (f *File) commentsReader(path string) (*xlsxComments, error) { - if f.Comments[path] == nil { - content, ok := f.Pkg.Load(path) - if ok && content != nil { - f.Comments[path] = new(xlsxComments) - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). - Decode(f.Comments[path]); err != nil && err != io.EOF { - return nil, err - } - } - } - return f.Comments[path], nil -} - -// commentsWriter provides a function to save xl/comments%d.xml after -// serialize structure. -func (f *File) commentsWriter() { - for path, c := range f.Comments { - if c != nil { - v, _ := xml.Marshal(c) - f.saveFileList(path, v) - } - } -} diff --git a/excelize_test.go b/excelize_test.go index 9372be58..9bc0107d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -383,15 +383,6 @@ func TestNewFile(t *testing.T) { assert.NoError(t, f.Save()) } -func TestAddDrawingVML(t *testing.T) { - // Test addDrawingVML with illegal cell reference - f := NewFile() - assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) - - f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", "A1", 0, 0), "XML syntax error on line 1: invalid UTF-8") -} - func TestSetCellHyperLink(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) @@ -978,7 +969,7 @@ func TestSetDeleteSheet(t *testing.T) { f, err := prepareTestBook4() assert.NoError(t, err) assert.NoError(t, f.DeleteSheet("Sheet1")) - assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) + assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) } diff --git a/shape.go b/shape.go index 8d9f8145..3f3dbe2e 100644 --- a/shape.go +++ b/shape.go @@ -22,6 +22,9 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { if opts == nil { return nil, ErrParameterInvalid } + if opts.Type == "" { + return nil, ErrParameterInvalid + } if opts.Width == 0 { opts.Width = defaultShapeSize } @@ -285,7 +288,7 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { // wavy // wavyHeavy // wavyDbl -func (f *File) AddShape(sheet, cell string, opts *Shape) error { +func (f *File) AddShape(sheet string, opts *Shape) error { options, err := parseShapeOptions(opts) if err != nil { return err @@ -313,7 +316,7 @@ func (f *File) AddShape(sheet, cell string, opts *Shape) error { f.addSheetDrawing(sheet, rID) f.addSheetNameSpace(sheet, SourceRelationship) } - if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil { + if err = f.addDrawingShape(sheet, drawingXML, opts.Cell, options); err != nil { return err } return f.addContentTypePart(drawingID, "drawings") diff --git a/shape_test.go b/shape_test.go index c9ba9d90..2a9fa08c 100644 --- a/shape_test.go +++ b/shape_test.go @@ -12,18 +12,19 @@ func TestAddShape(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - shape := &Shape{ + assert.NoError(t, f.AddShape("Sheet1", &Shape{ + Cell: "A30", Type: "rect", Paragraph: []RichTextRun{ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, }, - } - assert.NoError(t, f.AddShape("Sheet1", "A30", shape)) - assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}})) - assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"})) - assert.EqualError(t, f.AddShape("Sheet3", "H1", + })) + assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}})) + assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "C30", Type: "rect"})) + assert.EqualError(t, f.AddShape("Sheet3", &Shape{ + Cell: "H1", Type: "ellipseRibbon", Line: ShapeLine{Color: "4286F4"}, Fill: Fill{Color: []string{"8EB9FF"}}, @@ -41,15 +42,24 @@ func TestAddShape(t *testing.T) { }, }, ), "sheet Sheet3 does not exist") - assert.EqualError(t, f.AddShape("Sheet3", "H1", nil), ErrParameterInvalid.Error()) - assert.EqualError(t, f.AddShape("Sheet1", "A", shape), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "A1"}), ErrParameterInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet1", &Shape{ + Cell: "A", + Type: "rect", + Paragraph: []RichTextRun{ + {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, + {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, + }, + }), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) // Test add first shape for given sheet f = NewFile() lineWidth := 1.2 - assert.NoError(t, f.AddShape("Sheet1", "A1", + assert.NoError(t, f.AddShape("Sheet1", &Shape{ + Cell: "A1", Type: "ellipseRibbon", Line: ShapeLine{Color: "4286F4", Width: &lineWidth}, Fill: Fill{Color: []string{"8EB9FF"}}, @@ -69,16 +79,23 @@ func TestAddShape(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) // Test add shape with invalid sheet name - assert.EqualError(t, f.AddShape("Sheet:1", "A30", shape), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet:1", &Shape{ + Cell: "A30", + Type: "rect", + Paragraph: []RichTextRun{ + {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, + {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, + }, + }), ErrSheetNameInvalid.Error()) // Test add shape with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") // Test add shape with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddDrawingShape(t *testing.T) { diff --git a/test/vbaProject.bin b/test/vbaProject.bin index 7e88db07c1f26ef2d4c082543627c13a90d546f6..77b6bf829215544023e152349675c5e7fbffad6c 100644 GIT binary patch literal 16384 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 literal 15360 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 1 { + cmts.CommentList.Comment = append( + cmts.CommentList.Comment[:i], + cmts.CommentList.Comment[i+1:]..., + ) + i-- + continue + } + cmts.CommentList.Comment = nil + } + f.Comments[commentsXML] = cmts + } + return err +} + +// addComment provides a function to create chart as xl/comments%d.xml by +// given cell and format sets. +func (f *File) addComment(commentsXML string, opts vmlOptions) error { + if opts.Author == "" { + opts.Author = "Author" + } + if len(opts.Author) > MaxFieldLength { + opts.Author = opts.Author[:MaxFieldLength] + } + cmts, err := f.commentsReader(commentsXML) + if err != nil { + return err + } + var authorID int + if cmts == nil { + cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{opts.Author}}} + } + if inStrSlice(cmts.Authors.Author, opts.Author, true) == -1 { + cmts.Authors.Author = append(cmts.Authors.Author, opts.Author) + authorID = len(cmts.Authors.Author) - 1 + } + defaultFont, err := f.GetDefaultFont() + if err != nil { + return err + } + chars, cmt := 0, xlsxComment{ + Ref: opts.Cell, + AuthorID: authorID, + Text: xlsxText{R: []xlsxR{}}, + } + if opts.Text != "" { + if len(opts.Text) > TotalCellChars { + opts.Text = opts.Text[:TotalCellChars] + } + cmt.Text.T = stringPtr(opts.Text) + chars += len(opts.Text) + } + for _, run := range opts.Paragraph { + if chars == TotalCellChars { + break + } + if chars+len(run.Text) > TotalCellChars { + run.Text = run.Text[:TotalCellChars-chars] + } + chars += len(run.Text) + r := xlsxR{ + RPr: &xlsxRPr{ + Sz: &attrValFloat{Val: float64Ptr(9)}, + Color: &xlsxColor{ + Indexed: 81, + }, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, + }, + T: &xlsxT{Val: run.Text, Space: xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", + }}, + } + if run.Font != nil { + r.RPr = newRpr(run.Font) + } + cmt.Text.R = append(cmt.Text.R, r) + } + cmts.CommentList.Comment = append(cmts.CommentList.Comment, cmt) + f.Comments[commentsXML] = cmts + return err +} + +// countComments provides a function to get comments files count storage in +// the folder xl. +func (f *File) countComments() int { + c1, c2 := 0, 0 + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/comments") { + c1++ + } + return true + }) + for rel := range f.Comments { + if strings.Contains(rel, "xl/comments") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 +} + +// commentsReader provides a function to get the pointer to the structure +// after deserialization of xl/comments%d.xml. +func (f *File) commentsReader(path string) (*xlsxComments, error) { + if f.Comments[path] == nil { + content, ok := f.Pkg.Load(path) + if ok && content != nil { + f.Comments[path] = new(xlsxComments) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(f.Comments[path]); err != nil && err != io.EOF { + return nil, err + } + } + } + return f.Comments[path], nil +} + +// commentsWriter provides a function to save xl/comments%d.xml after +// serialize structure. +func (f *File) commentsWriter() { + for path, c := range f.Comments { + if c != nil { + v, _ := xml.Marshal(c) + f.saveFileList(path, v) + } + } +} + +// AddFormControl provides the method to add form control button in a worksheet +// by given worksheet name and form control options. Supported form control +// type: button and radio. The file extension should be XLSM or XLTM. For +// example: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A1", +// Type: excelize.FormControlButton, +// Macro: "Button1_Click", +// Width: 140, +// Height: 60, +// Text: "Button 1\r\n", +// Paragraph: []excelize.RichTextRun{ +// { +// Font: &excelize.Font{ +// Bold: true, +// Italic: true, +// Underline: "single", +// Family: "Times New Roman", +// Size: 14, +// Color: "777777", +// }, +// Text: "C1=A1+B1", +// }, +// }, +// }) +func (f *File) AddFormControl(sheet string, opts FormControl) error { + return f.addVMLObject(vmlOptions{ + FormCtrl: true, + Sheet: sheet, + Type: opts.Type, + Checked: opts.Checked, + Cell: opts.Cell, + Macro: opts.Macro, + Width: opts.Width, + Height: opts.Height, + Format: opts.Format, + Text: opts.Text, + Paragraph: opts.Paragraph, + }) +} + +// countVMLDrawing provides a function to get VML drawing files count storage +// in the folder xl/drawings. +func (f *File) countVMLDrawing() int { + c1, c2 := 0, 0 + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/drawings/vmlDrawing") { + c1++ + } + return true + }) + for rel := range f.VMLDrawing { + if strings.Contains(rel, "xl/drawings/vmlDrawing") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 +} + +// decodeVMLDrawingReader provides a function to get the pointer to the +// structure after deserialization of xl/drawings/vmlDrawing%d.xml. +func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) { + if f.DecodeVMLDrawing[path] == nil { + c, ok := f.Pkg.Load(path) + if ok && c != nil { + f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))). + Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { + return nil, err + } + } + } + return f.DecodeVMLDrawing[path], nil +} + +// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml +// after serialize structure. +func (f *File) vmlDrawingWriter() { + for path, vml := range f.VMLDrawing { + if vml != nil { + v, _ := xml.Marshal(vml) + f.Pkg.Store(path, v) + } + } +} + +// addVMLObject provides a function to create VML drawing parts and +// relationships for comments and form controls. +func (f *File) addVMLObject(opts vmlOptions) error { + // Read sheet data. + ws, err := f.workSheetReader(opts.Sheet) + if err != nil { + return err + } + vmlID := f.countComments() + 1 + if opts.FormCtrl { + if opts.Type > FormControlRadio { + return ErrParameterInvalid + } + vmlID = f.countVMLDrawing() + 1 + } + drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + if ws.LegacyDrawing != nil { + // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.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. + sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + if !opts.FormCtrl { + sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" + f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") + } + f.addSheetNameSpace(opts.Sheet, SourceRelationship) + f.addSheetLegacyDrawing(opts.Sheet, rID) + } + if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { + return err + } + if !opts.FormCtrl { + commentsXML := "xl/comments" + strconv.Itoa(vmlID) + ".xml" + if err = f.addComment(commentsXML, opts); err != nil { + return err + } + } + return f.addContentTypePart(vmlID, "comments") +} + +// prepareFormCtrlOptions provides a function to parse the format settings of +// the form control with default value. +func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { + for _, runs := range opts.Paragraph { + for _, subStr := range strings.Split(runs.Text, "\n") { + opts.rows++ + if chars := len(subStr); chars > opts.cols { + opts.cols = chars + } + } + } + if len(opts.Paragraph) == 0 { + opts.rows, opts.cols = 1, len(opts.Text) + } + if opts.Format.ScaleX == 0 { + opts.Format.ScaleX = 1 + } + if opts.Format.ScaleY == 0 { + opts.Format.ScaleY = 1 + } + if opts.cols == 0 { + opts.cols = 8 + } + if opts.Width == 0 { + opts.Width = uint(opts.cols * 9) + } + if opts.Height == 0 { + opts.Height = uint(opts.rows * 25) + } + return opts +} + +// formCtrlText returns font element in the VML for control form text. +func formCtrlText(opts *vmlOptions) []vmlFont { + var font []vmlFont + if opts.Text != "" { + font = append(font, vmlFont{Content: opts.Text}) + } + for _, run := range opts.Paragraph { + fnt := vmlFont{ + Content: run.Text + "

\r\n", + } + if run.Font != nil { + fnt.Face = run.Font.Family + fnt.Color = run.Font.Color + if !strings.HasPrefix(run.Font.Color, "#") { + fnt.Color = "#" + fnt.Color + } + if run.Font.Size != 0 { + fnt.Size = uint(run.Font.Size * 20) + } + if run.Font.Underline == "single" { + fnt.Content = "" + fnt.Content + "" + } + if run.Font.Italic { + fnt.Content = "" + fnt.Content + "" + } + if run.Font.Bold { + fnt.Content = "" + fnt.Content + "" + } + } + font = append(font, fnt) + } + return font +} + +var ( + formCtrlPresets = map[FormControlType]struct { + objectType string + filled string + fillColor string + stroked string + strokeColor string + strokeButton string + fill *vFill + textHAlign string + textVAlign string + noThreeD *string + firstButton *string + shadow *vShadow + }{ + FormControlNote: { + objectType: "Note", + filled: "", + fillColor: "#FBF6D6", + stroked: "", + strokeColor: "#EDEAA1", + strokeButton: "", + fill: &vFill{ + Color2: "#FBFE82", + Angle: -180, + Type: "gradient", + Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, + }, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: &vShadow{On: "t", Color: "black", Obscured: "t"}, + }, + FormControlButton: { + objectType: "Button", + filled: "", + fillColor: "buttonFace [67]", + stroked: "", + strokeColor: "windowText [64]", + strokeButton: "t", + fill: &vFill{ + Color2: "buttonFace [67]", + Angle: -180, + Type: "gradient", + Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, + }, + textHAlign: "Center", + textVAlign: "Center", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlRadio: { + objectType: "Radio", + filled: "f", + fillColor: "window [65]", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "Center", + noThreeD: stringPtr(""), + firstButton: stringPtr(""), + shadow: nil, + }, + } +) + +// addDrawingVML provides a function to create VML drawing XML as +// xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The +// 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 { + col, row, err := CellNameToCoordinates(opts.Cell) + if err != nil { + return err + } + anchor := fmt.Sprintf("%d, 23, %d, 0, %d, %d, %d, 5", col, row, col+opts.rows+2, col+opts.cols-1, row+opts.rows+2) + vmlID, vml, preset := 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] + style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden" + var font []vmlFont + if opts.FormCtrl { + vmlID = 201 + style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight" + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.Sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) + anchor = fmt.Sprintf("%d, 0, %d, 0, %d, %d, %d, %d", colStart, rowStart, colEnd, x2, rowEnd, y2) + font = formCtrlText(opts) + } + 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", + XMLNSmv: "http://macVmlSchemaUri", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: dataID}, + }, + ShapeType: &xlsxShapeType{ + ID: fmt.Sprintf("_x0000_t%d", vmlID), + CoordSize: "21600,21600", + Spt: 202, + Path: "m0,0l0,21600,21600,21600,21600,0xe", + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, + }, + } + // 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.Path = d.ShapeType.Path + for _, v := range d.Shape { + s := xlsxShape{ + ID: v.ID, + Type: v.Type, + Style: v.Style, + Button: v.Button, + Filled: v.Filled, + FillColor: v.FillColor, + InsetMode: v.InsetMode, + Stroked: v.Stroked, + StrokeColor: v.StrokeColor, + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } + } + sp := encodeShape{ + Fill: preset.fill, + Shadow: preset.shadow, + Path: &vPath{ConnectType: "none"}, + TextBox: &vTextBox{ + Style: "mso-direction-alt:auto", + Div: &xlsxDiv{Style: "text-align:left", Font: font}, + }, + ClientData: &xClientData{ + ObjectType: preset.objectType, + Anchor: anchor, + AutoFill: "True", + Row: row - 1, + Column: col - 1, + TextHAlign: preset.textHAlign, + TextVAlign: preset.textVAlign, + NoThreeD: preset.noThreeD, + FirstButton: preset.firstButton, + }, + } + if opts.FormCtrl { + sp.ClientData.FmlaMacro = opts.Macro + } + if opts.Type == FormControlRadio && opts.Checked { + sp.ClientData.Checked = stringPtr("1") + } + s, _ := xml.Marshal(sp) + shape := xlsxShape{ + ID: "_x0000_s1025", + Type: fmt.Sprintf("#_x0000_t%d", vmlID), + Style: style, + Button: preset.strokeButton, + Filled: preset.filled, + FillColor: preset.fillColor, + Stroked: preset.stroked, + StrokeColor: preset.strokeColor, + Val: string(s[13 : len(s)-14]), + } + vml.Shape = append(vml.Shape, shape) + f.VMLDrawing[drawingVML] = vml + return err +} diff --git a/vmlDrawing.go b/vmlDrawing.go index 1a49d722..89f91d92 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -21,15 +21,15 @@ type vmlDrawing struct { XMLNSo string `xml:"xmlns:o,attr"` XMLNSx string `xml:"xmlns:x,attr"` XMLNSmv string `xml:"xmlns:mv,attr"` - Shapelayout *xlsxShapelayout `xml:"o:shapelayout"` - Shapetype *xlsxShapetype `xml:"v:shapetype"` + ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"` + ShapeType *xlsxShapeType `xml:"v:shapetype"` Shape []xlsxShape `xml:"v:shape"` } -// xlsxShapelayout directly maps the shapelayout element. This element contains +// xlsxShapeLayout directly maps the shapelayout element. This element contains // child elements that store information used in the editing and layout of // shapes. -type xlsxShapelayout struct { +type xlsxShapeLayout struct { Ext string `xml:"v:ext,attr"` IDmap *xlsxIDmap `xml:"o:idmap"` } @@ -46,16 +46,19 @@ type xlsxShape struct { ID string `xml:"id,attr"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` - Fillcolor string `xml:"fillcolor,attr"` - Insetmode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` - Strokecolor string `xml:"strokecolor,attr,omitempty"` + Button string `xml:"o:button,attr,omitempty"` + Filled string `xml:"filled,attr,omitempty"` + FillColor string `xml:"fillcolor,attr"` + InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` + StrokeColor string `xml:"strokecolor,attr,omitempty"` Val string `xml:",innerxml"` } -// xlsxShapetype directly maps the shapetype element. -type xlsxShapetype struct { +// xlsxShapeType directly maps the shapetype element. +type xlsxShapeType struct { ID string `xml:"id,attr"` - Coordsize string `xml:"coordsize,attr"` + CoordSize string `xml:"coordsize,attr"` Spt int `xml:"o:spt,attr"` Path string `xml:"path,attr"` Stroke *xlsxStroke `xml:"v:stroke"` @@ -64,13 +67,13 @@ type xlsxShapetype struct { // xlsxStroke directly maps the stroke element. type xlsxStroke struct { - Joinstyle string `xml:"joinstyle,attr"` + JoinStyle string `xml:"joinstyle,attr"` } // vPath directly maps the v:path element. type vPath struct { - Gradientshapeok string `xml:"gradientshapeok,attr,omitempty"` - Connecttype string `xml:"o:connecttype,attr"` + GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"` + ConnectType string `xml:"o:connecttype,attr"` } // vFill directly maps the v:fill element. This element must be defined within a @@ -96,16 +99,24 @@ type vShadow struct { Obscured string `xml:"obscured,attr"` } -// vTextbox directly maps the v:textbox element. This element must be defined +// vTextBox directly maps the v:textbox element. This element must be defined // within a Shape element. -type vTextbox struct { +type vTextBox struct { Style string `xml:"style,attr"` Div *xlsxDiv `xml:"div"` } // xlsxDiv directly maps the div element. type xlsxDiv struct { - Style string `xml:"style,attr"` + Style string `xml:"style,attr"` + Font []vmlFont `xml:"font"` +} + +type vmlFont struct { + Face string `xml:"face,attr,omitempty"` + Size uint `xml:"size,attr,omitempty"` + Color string `xml:"color,attr,omitempty"` + Content string `xml:",innerxml"` } // xClientData (Attached Object Data) directly maps the x:ClientData element. @@ -116,24 +127,49 @@ type xlsxDiv struct { // child elements is appropriate. Relevant groups are identified for each child // element. type xClientData struct { - ObjectType string `xml:"ObjectType,attr"` - MoveWithCells string `xml:"x:MoveWithCells"` - SizeWithCells string `xml:"x:SizeWithCells"` - Anchor string `xml:"x:Anchor"` - AutoFill string `xml:"x:AutoFill"` - Row int `xml:"x:Row"` - Column int `xml:"x:Column"` + ObjectType string `xml:"ObjectType,attr"` + MoveWithCells string `xml:"x:MoveWithCells"` + SizeWithCells string `xml:"x:SizeWithCells"` + Anchor string `xml:"x:Anchor"` + AutoFill string `xml:"x:AutoFill"` + Row int `xml:"x:Row"` + Column int `xml:"x:Column"` + FmlaMacro string `xml:"x:FmlaMacro,omitempty"` + TextHAlign string `xml:"x:TextHAlign,omitempty"` + TextVAlign string `xml:"x:TextVAlign,omitempty"` + Checked *string `xml:"x:Checked,omitempty"` + NoThreeD *string `xml:"x:NoThreeD,omitempty"` + FirstButton *string `xml:"x:FirstButton,omitempty"` } // decodeVmlDrawing defines the structure used to parse the file // xl/drawings/vmlDrawing%d.vml. type decodeVmlDrawing struct { - Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"` + ShapeType decodeShapeType `xml:"urn:schemas-microsoft-com:vml shapetype"` + Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"` +} + +// 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"` } // decodeShape defines the structure used to parse the particular shape element. type decodeShape struct { - Val string `xml:",innerxml"` + ID string `xml:"id,attr"` + Type string `xml:"type,attr"` + Style string `xml:"style,attr"` + Button string `xml:"button,attr,omitempty"` + Filled string `xml:"filled,attr,omitempty"` + FillColor string `xml:"fillcolor,attr"` + InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` + StrokeColor string `xml:"strokecolor,attr,omitempty"` + Val string `xml:",innerxml"` } // encodeShape defines the structure used to re-serialization shape element. @@ -141,6 +177,38 @@ type encodeShape struct { Fill *vFill `xml:"v:fill"` Shadow *vShadow `xml:"v:shadow"` Path *vPath `xml:"v:path"` - Textbox *vTextbox `xml:"v:textbox"` + TextBox *vTextBox `xml:"v:textbox"` ClientData *xClientData `xml:"x:ClientData"` } + +// vmlOptions defines the structure used to internal comments and form controls. +type vmlOptions struct { + rows int + cols int + FormCtrl bool + Sheet string + Author string + AuthorID int + Cell string + Checked bool + Text string + Macro string + Width uint + Height uint + Paragraph []RichTextRun + Type FormControlType + Format GraphicOptions +} + +// FormControl directly maps the form controls information. +type FormControl struct { + Cell string + Macro string + Width uint + Height uint + Checked bool + Text string + Paragraph []RichTextRun + Type FormControlType + Format GraphicOptions +} diff --git a/comment_test.go b/vml_test.go similarity index 58% rename from comment_test.go rename to vml_test.go index b6ea2aa0..47ec33d1 100644 --- a/comment_test.go +++ b/vml_test.go @@ -13,6 +13,7 @@ package excelize import ( "encoding/xml" + "os" "path/filepath" "strings" "testing" @@ -27,13 +28,13 @@ func TestAddComment(t *testing.T) { } s := strings.Repeat("c", TotalCellChars+1) - assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Runs: []RichTextRun{{Text: s}, {Text: s}}})) - assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) + assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Paragraph: []RichTextRun{{Text: s}, {Text: s}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) // Test add comment on not exists worksheet - assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") + assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") // Test add comment on with illegal cell reference - assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) comments, err := f.GetComments("Sheet1") assert.NoError(t, err) assert.Len(t, comments, 2) @@ -86,11 +87,11 @@ func TestDeleteComment(t *testing.T) { } assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."})) - assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) - assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}})) - assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}})) - assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}})) - assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) assert.NoError(t, f.DeleteComment("Sheet2", "A40")) @@ -144,3 +145,80 @@ func TestCountComments(t *testing.T) { f.Comments["xl/comments1.xml"] = nil assert.Equal(t, f.countComments(), 1) } + +func TestAddDrawingVML(t *testing.T) { + // Test addDrawingVML with illegal cell reference + f := NewFile() + assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) + + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8") +} + +func TestAddFormControl(t *testing.T) { + f := NewFile() + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D1", + Type: FormControlButton, + Macro: "Button1_Click", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", + Type: FormControlButton, + Macro: "Button1_Click", + Width: 140, + Height: 60, + Text: "Button 1\r\n", + Paragraph: []RichTextRun{ + { + Font: &Font{ + Bold: true, + Italic: true, + Underline: "single", + Family: "Times New Roman", + Size: 14, + Color: "777777", + }, + Text: "C1=A1+B1", + }, + }, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A5", + Type: FormControlRadio, + Text: "Option Button 1", + Checked: true, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A6", + Type: FormControlRadio, + Text: "Option Button 2", + })) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) + file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) + assert.NoError(t, err) + assert.NoError(t, f.AddVBAProject(file)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddFormControl.xlsm"))) + assert.NoError(t, f.Close()) + f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) + assert.NoError(t, err) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D4", + Type: FormControlButton, + Macro: "Button1_Click", + Text: "Button 2", + })) + // Test add unsupported form control + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", + Type: 0x37, + Macro: "Button1_Click", + }), ErrParameterInvalid) + // Test add form control on not exists worksheet + assert.Equal(t, f.AddFormControl("SheetN", FormControl{ + Cell: "A1", + Type: FormControlButton, + Macro: "Button1_Click", + }), newNoExistSheetError("SheetN")) + assert.NoError(t, f.Close()) +} diff --git a/xmlComments.go b/xmlComments.go index 214c15e7..c27cd70e 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -74,9 +74,9 @@ type xlsxPhoneticRun struct { // Comment directly maps the comment information. type Comment struct { - Author string - AuthorID int - Cell string - Text string - Runs []RichTextRun + Author string + AuthorID int + Cell string + Text string + Paragraph []RichTextRun } diff --git a/xmlDrawing.go b/xmlDrawing.go index dae3bcc3..affa0390 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -606,6 +606,7 @@ type GraphicOptions struct { // Shape directly maps the format settings of the shape. type Shape struct { + Cell string Type string Macro string Width uint