430 lines
12 KiB
Go
430 lines
12 KiB
Go
// 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 and returns a map of worksheet name to
|
|
// the worksheet comments.
|
|
func (f *File) GetComments() (map[string][]Comment, error) {
|
|
comments := map[string][]Comment{}
|
|
for n, path := range f.sheetMap {
|
|
target := f.getSheetComments(filepath.Base(path))
|
|
if target == "" {
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(target, "/") {
|
|
target = "xl" + strings.TrimPrefix(target, "..")
|
|
}
|
|
cmts, err := f.commentsReader(strings.TrimPrefix(target, "/"))
|
|
if err != nil {
|
|
return comments, err
|
|
}
|
|
if cmts != nil {
|
|
var sheetComments []Comment
|
|
for _, comment := range cmts.CommentList.Comment {
|
|
sheetComment := Comment{}
|
|
if comment.AuthorID < len(cmts.Authors.Author) {
|
|
sheetComment.Author = cmts.Authors.Author[comment.AuthorID]
|
|
}
|
|
sheetComment.Cell = comment.Ref
|
|
sheetComment.AuthorID = comment.AuthorID
|
|
if comment.Text.T != nil {
|
|
sheetComment.Text += *comment.Text.T
|
|
}
|
|
for _, text := range comment.Text.R {
|
|
if text.T != nil {
|
|
run := RichTextRun{Text: text.T.Val}
|
|
if text.RPr != nil {
|
|
run.Font = newFont(text.RPr)
|
|
}
|
|
sheetComment.Runs = append(sheetComment.Runs, run)
|
|
}
|
|
}
|
|
sheetComments = append(sheetComments, sheetComment)
|
|
}
|
|
comments[n] = sheetComments
|
|
}
|
|
}
|
|
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.Lock()
|
|
defer sheetRels.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 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)
|
|
}
|
|
}
|
|
}
|