diff --git a/comment.go b/comment.go deleted file mode 100644 index 28ba40b..0000000 --- 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 9372be5..9bc0107 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 8d9f814..3f3dbe2 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 c9ba9d9..2a9fa08 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 7e88db0..77b6bf8 100644 Binary files a/test/vbaProject.bin and b/test/vbaProject.bin differ diff --git a/vml.go b/vml.go new file mode 100644 index 0000000..ac68d10 --- /dev/null +++ b/vml.go @@ -0,0 +1,655 @@ +// 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" +) + +// FormControlType is the type of supported form controls. +type FormControlType byte + +// This section defines the currently supported form control types enumeration. +const ( + FormControlNote FormControlType = iota + FormControlButton + FormControlRadio +) + +// 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.Paragraph = append(comment.Paragraph, 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 +// name, cell reference 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", +// Paragraph: []excelize.RichTextRun{ +// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}}, +// {Text: "This is a comment."}, +// }, +// }) +func (f *File) AddComment(sheet string, opts Comment) error { + return f.addVMLObject(vmlOptions{ + Sheet: sheet, + Author: opts.Author, + AuthorID: opts.AuthorID, + Cell: opts.Cell, + Text: opts.Text, + Type: FormControlNote, + Paragraph: opts.Paragraph, + }) +} + +// 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 +} + +// 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 1a49d72..89f91d9 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 b6ea2aa..47ec33d 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 214c15e..c27cd70 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 dae3bcc..affa039 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