- Breaking changes: * Change `func (f *File) AddShape(sheet, cell string, opts *Shape) error` to `func (f *File) AddShape(sheet string, opts *Shape) error` * Rename the `Runs` field to `Paragraph` in the exported `Comment` data type - Add new exported function `AddFormControl` support to add button and radio form controls - Add check for shape type for the `AddShape` function, an error will be returned if no shape type is specified - Updated functions documentation and the unit tests
This commit is contained in:
parent
8418bd7afd
commit
2c8dc5c150
429
comment.go
429
comment.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -383,15 +383,6 @@ func TestNewFile(t *testing.T) {
|
||||||
assert.NoError(t, f.Save())
|
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) {
|
func TestSetCellHyperLink(t *testing.T) {
|
||||||
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -978,7 +969,7 @@ func TestSetDeleteSheet(t *testing.T) {
|
||||||
f, err := prepareTestBook4()
|
f, err := prepareTestBook4()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NoError(t, f.DeleteSheet("Sheet1"))
|
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")))
|
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx")))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
7
shape.go
7
shape.go
|
@ -22,6 +22,9 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
return nil, ErrParameterInvalid
|
return nil, ErrParameterInvalid
|
||||||
}
|
}
|
||||||
|
if opts.Type == "" {
|
||||||
|
return nil, ErrParameterInvalid
|
||||||
|
}
|
||||||
if opts.Width == 0 {
|
if opts.Width == 0 {
|
||||||
opts.Width = defaultShapeSize
|
opts.Width = defaultShapeSize
|
||||||
}
|
}
|
||||||
|
@ -285,7 +288,7 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
|
||||||
// wavy
|
// wavy
|
||||||
// wavyHeavy
|
// wavyHeavy
|
||||||
// wavyDbl
|
// wavyDbl
|
||||||
func (f *File) AddShape(sheet, cell string, opts *Shape) error {
|
func (f *File) AddShape(sheet string, opts *Shape) error {
|
||||||
options, err := parseShapeOptions(opts)
|
options, err := parseShapeOptions(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -313,7 +316,7 @@ func (f *File) AddShape(sheet, cell string, opts *Shape) error {
|
||||||
f.addSheetDrawing(sheet, rID)
|
f.addSheetDrawing(sheet, rID)
|
||||||
f.addSheetNameSpace(sheet, SourceRelationship)
|
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 err
|
||||||
}
|
}
|
||||||
return f.addContentTypePart(drawingID, "drawings")
|
return f.addContentTypePart(drawingID, "drawings")
|
||||||
|
|
|
@ -12,18 +12,19 @@ func TestAddShape(t *testing.T) {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
shape := &Shape{
|
assert.NoError(t, f.AddShape("Sheet1", &Shape{
|
||||||
|
Cell: "A30",
|
||||||
Type: "rect",
|
Type: "rect",
|
||||||
Paragraph: []RichTextRun{
|
Paragraph: []RichTextRun{
|
||||||
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
|
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
|
||||||
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
|
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
|
||||||
},
|
},
|
||||||
}
|
}))
|
||||||
assert.NoError(t, f.AddShape("Sheet1", "A30", shape))
|
assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}))
|
||||||
assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}))
|
assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "C30", Type: "rect"}))
|
||||||
assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"}))
|
assert.EqualError(t, f.AddShape("Sheet3",
|
||||||
assert.EqualError(t, f.AddShape("Sheet3", "H1",
|
|
||||||
&Shape{
|
&Shape{
|
||||||
|
Cell: "H1",
|
||||||
Type: "ellipseRibbon",
|
Type: "ellipseRibbon",
|
||||||
Line: ShapeLine{Color: "4286F4"},
|
Line: ShapeLine{Color: "4286F4"},
|
||||||
Fill: Fill{Color: []string{"8EB9FF"}},
|
Fill: Fill{Color: []string{"8EB9FF"}},
|
||||||
|
@ -41,15 +42,24 @@ func TestAddShape(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
), "sheet Sheet3 does not exist")
|
), "sheet Sheet3 does not exist")
|
||||||
assert.EqualError(t, f.AddShape("Sheet3", "H1", nil), ErrParameterInvalid.Error())
|
assert.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error())
|
||||||
assert.EqualError(t, f.AddShape("Sheet1", "A", shape), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).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")))
|
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))
|
||||||
|
|
||||||
// Test add first shape for given sheet
|
// Test add first shape for given sheet
|
||||||
f = NewFile()
|
f = NewFile()
|
||||||
lineWidth := 1.2
|
lineWidth := 1.2
|
||||||
assert.NoError(t, f.AddShape("Sheet1", "A1",
|
assert.NoError(t, f.AddShape("Sheet1",
|
||||||
&Shape{
|
&Shape{
|
||||||
|
Cell: "A1",
|
||||||
Type: "ellipseRibbon",
|
Type: "ellipseRibbon",
|
||||||
Line: ShapeLine{Color: "4286F4", Width: &lineWidth},
|
Line: ShapeLine{Color: "4286F4", Width: &lineWidth},
|
||||||
Fill: Fill{Color: []string{"8EB9FF"}},
|
Fill: Fill{Color: []string{"8EB9FF"}},
|
||||||
|
@ -69,16 +79,23 @@ func TestAddShape(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
|
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
|
||||||
// Test add shape with invalid sheet name
|
// 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
|
// Test add shape with unsupported charset style sheet
|
||||||
f.Styles = nil
|
f.Styles = nil
|
||||||
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
|
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
|
// Test add shape with unsupported charset content types
|
||||||
f = NewFile()
|
f = NewFile()
|
||||||
f.ContentTypes = nil
|
f.ContentTypes = nil
|
||||||
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
|
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) {
|
func TestAddDrawingShape(t *testing.T) {
|
||||||
|
|
Binary file not shown.
|
@ -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 + "<br></br>\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 = "<u>" + fnt.Content + "</u>"
|
||||||
|
}
|
||||||
|
if run.Font.Italic {
|
||||||
|
fnt.Content = "<i>" + fnt.Content + "</i>"
|
||||||
|
}
|
||||||
|
if run.Font.Bold {
|
||||||
|
fnt.Content = "<b>" + fnt.Content + "</b>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
120
vmlDrawing.go
120
vmlDrawing.go
|
@ -21,15 +21,15 @@ type vmlDrawing struct {
|
||||||
XMLNSo string `xml:"xmlns:o,attr"`
|
XMLNSo string `xml:"xmlns:o,attr"`
|
||||||
XMLNSx string `xml:"xmlns:x,attr"`
|
XMLNSx string `xml:"xmlns:x,attr"`
|
||||||
XMLNSmv string `xml:"xmlns:mv,attr"`
|
XMLNSmv string `xml:"xmlns:mv,attr"`
|
||||||
Shapelayout *xlsxShapelayout `xml:"o:shapelayout"`
|
ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"`
|
||||||
Shapetype *xlsxShapetype `xml:"v:shapetype"`
|
ShapeType *xlsxShapeType `xml:"v:shapetype"`
|
||||||
Shape []xlsxShape `xml:"v:shape"`
|
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
|
// child elements that store information used in the editing and layout of
|
||||||
// shapes.
|
// shapes.
|
||||||
type xlsxShapelayout struct {
|
type xlsxShapeLayout struct {
|
||||||
Ext string `xml:"v:ext,attr"`
|
Ext string `xml:"v:ext,attr"`
|
||||||
IDmap *xlsxIDmap `xml:"o:idmap"`
|
IDmap *xlsxIDmap `xml:"o:idmap"`
|
||||||
}
|
}
|
||||||
|
@ -46,16 +46,19 @@ type xlsxShape struct {
|
||||||
ID string `xml:"id,attr"`
|
ID string `xml:"id,attr"`
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
Style string `xml:"style,attr"`
|
Style string `xml:"style,attr"`
|
||||||
Fillcolor string `xml:"fillcolor,attr"`
|
Button string `xml:"o:button,attr,omitempty"`
|
||||||
Insetmode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
|
Filled string `xml:"filled,attr,omitempty"`
|
||||||
Strokecolor string `xml:"strokecolor,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"`
|
Val string `xml:",innerxml"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// xlsxShapetype directly maps the shapetype element.
|
// xlsxShapeType directly maps the shapetype element.
|
||||||
type xlsxShapetype struct {
|
type xlsxShapeType struct {
|
||||||
ID string `xml:"id,attr"`
|
ID string `xml:"id,attr"`
|
||||||
Coordsize string `xml:"coordsize,attr"`
|
CoordSize string `xml:"coordsize,attr"`
|
||||||
Spt int `xml:"o:spt,attr"`
|
Spt int `xml:"o:spt,attr"`
|
||||||
Path string `xml:"path,attr"`
|
Path string `xml:"path,attr"`
|
||||||
Stroke *xlsxStroke `xml:"v:stroke"`
|
Stroke *xlsxStroke `xml:"v:stroke"`
|
||||||
|
@ -64,13 +67,13 @@ type xlsxShapetype struct {
|
||||||
|
|
||||||
// xlsxStroke directly maps the stroke element.
|
// xlsxStroke directly maps the stroke element.
|
||||||
type xlsxStroke struct {
|
type xlsxStroke struct {
|
||||||
Joinstyle string `xml:"joinstyle,attr"`
|
JoinStyle string `xml:"joinstyle,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// vPath directly maps the v:path element.
|
// vPath directly maps the v:path element.
|
||||||
type vPath struct {
|
type vPath struct {
|
||||||
Gradientshapeok string `xml:"gradientshapeok,attr,omitempty"`
|
GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"`
|
||||||
Connecttype string `xml:"o:connecttype,attr"`
|
ConnectType string `xml:"o:connecttype,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// vFill directly maps the v:fill element. This element must be defined within a
|
// 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"`
|
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.
|
// within a Shape element.
|
||||||
type vTextbox struct {
|
type vTextBox struct {
|
||||||
Style string `xml:"style,attr"`
|
Style string `xml:"style,attr"`
|
||||||
Div *xlsxDiv `xml:"div"`
|
Div *xlsxDiv `xml:"div"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// xlsxDiv directly maps the div element.
|
// xlsxDiv directly maps the div element.
|
||||||
type xlsxDiv struct {
|
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.
|
// 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
|
// child elements is appropriate. Relevant groups are identified for each child
|
||||||
// element.
|
// element.
|
||||||
type xClientData struct {
|
type xClientData struct {
|
||||||
ObjectType string `xml:"ObjectType,attr"`
|
ObjectType string `xml:"ObjectType,attr"`
|
||||||
MoveWithCells string `xml:"x:MoveWithCells"`
|
MoveWithCells string `xml:"x:MoveWithCells"`
|
||||||
SizeWithCells string `xml:"x:SizeWithCells"`
|
SizeWithCells string `xml:"x:SizeWithCells"`
|
||||||
Anchor string `xml:"x:Anchor"`
|
Anchor string `xml:"x:Anchor"`
|
||||||
AutoFill string `xml:"x:AutoFill"`
|
AutoFill string `xml:"x:AutoFill"`
|
||||||
Row int `xml:"x:Row"`
|
Row int `xml:"x:Row"`
|
||||||
Column int `xml:"x:Column"`
|
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
|
// decodeVmlDrawing defines the structure used to parse the file
|
||||||
// xl/drawings/vmlDrawing%d.vml.
|
// xl/drawings/vmlDrawing%d.vml.
|
||||||
type decodeVmlDrawing struct {
|
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.
|
// decodeShape defines the structure used to parse the particular shape element.
|
||||||
type decodeShape struct {
|
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.
|
// encodeShape defines the structure used to re-serialization shape element.
|
||||||
|
@ -141,6 +177,38 @@ type encodeShape struct {
|
||||||
Fill *vFill `xml:"v:fill"`
|
Fill *vFill `xml:"v:fill"`
|
||||||
Shadow *vShadow `xml:"v:shadow"`
|
Shadow *vShadow `xml:"v:shadow"`
|
||||||
Path *vPath `xml:"v:path"`
|
Path *vPath `xml:"v:path"`
|
||||||
Textbox *vTextbox `xml:"v:textbox"`
|
TextBox *vTextBox `xml:"v:textbox"`
|
||||||
ClientData *xClientData `xml:"x:ClientData"`
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ package excelize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -27,13 +28,13 @@ func TestAddComment(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s := strings.Repeat("c", TotalCellChars+1)
|
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("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], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
|
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
|
// 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
|
// 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")
|
comments, err := f.GetComments("Sheet1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, comments, 2)
|
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: "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: "A41", Paragraph: []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", Paragraph: []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: "C41", Paragraph: []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: "C42", Paragraph: []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: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
|
||||||
|
|
||||||
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
|
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
|
||||||
|
|
||||||
|
@ -144,3 +145,80 @@ func TestCountComments(t *testing.T) {
|
||||||
f.Comments["xl/comments1.xml"] = nil
|
f.Comments["xl/comments1.xml"] = nil
|
||||||
assert.Equal(t, f.countComments(), 1)
|
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())
|
||||||
|
}
|
|
@ -74,9 +74,9 @@ type xlsxPhoneticRun struct {
|
||||||
|
|
||||||
// Comment directly maps the comment information.
|
// Comment directly maps the comment information.
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
Author string
|
Author string
|
||||||
AuthorID int
|
AuthorID int
|
||||||
Cell string
|
Cell string
|
||||||
Text string
|
Text string
|
||||||
Runs []RichTextRun
|
Paragraph []RichTextRun
|
||||||
}
|
}
|
||||||
|
|
|
@ -606,6 +606,7 @@ type GraphicOptions struct {
|
||||||
|
|
||||||
// Shape directly maps the format settings of the shape.
|
// Shape directly maps the format settings of the shape.
|
||||||
type Shape struct {
|
type Shape struct {
|
||||||
|
Cell string
|
||||||
Type string
|
Type string
|
||||||
Macro string
|
Macro string
|
||||||
Width uint
|
Width uint
|
||||||
|
|
Loading…
Reference in New Issue