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 7e88db07..77b6bf82 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 00000000..ac68d101
--- /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 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