excelize/comment.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 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.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 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)
}
}
}