comment.go 10.9 KB
Newer Older
1
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
xurime's avatar
xurime 已提交
2 3 4
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
5 6 7 8 9 10
// 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.15 or later.
xurime's avatar
xurime 已提交
11

12 13 14
package excelize

import (
15
	"bytes"
16 17
	"encoding/json"
	"encoding/xml"
R
Rad Cirskis 已提交
18
	"fmt"
19 20
	"io"
	"log"
21
	"path/filepath"
22 23 24 25
	"strconv"
	"strings"
)

xurime's avatar
xurime 已提交
26 27
// parseFormatCommentsSet provides a function to parse the format settings of
// the comment with default value.
28
func parseFormatCommentsSet(formatSet string) (*formatComment, error) {
29 30 31 32
	format := formatComment{
		Author: "Author:",
		Text:   " ",
	}
33
	err := json.Unmarshal([]byte(formatSet), &format)
34
	return &format, err
35 36
}

xurime's avatar
xurime 已提交
37 38
// GetComments retrieves all comments and returns a map of worksheet name to
// the worksheet comments.
39 40
func (f *File) GetComments() (comments map[string][]Comment) {
	comments = map[string][]Comment{}
41
	for n, path := range f.sheetMap {
42 43 44 45
		target := f.getSheetComments(filepath.Base(path))
		if target == "" {
			continue
		}
46
		if !strings.HasPrefix(target, "/") {
47 48 49
			target = "xl" + strings.TrimPrefix(target, "..")
		}
		if d := f.commentsReader(strings.TrimPrefix(target, "/")); d != nil {
50
			var sheetComments []Comment
51 52
			for _, comment := range d.CommentList.Comment {
				sheetComment := Comment{}
53 54
				if comment.AuthorID < len(d.Authors.Author) {
					sheetComment.Author = d.Authors.Author[comment.AuthorID]
55 56 57
				}
				sheetComment.Ref = comment.Ref
				sheetComment.AuthorID = comment.AuthorID
xurime's avatar
xurime 已提交
58 59 60
				if comment.Text.T != nil {
					sheetComment.Text += *comment.Text.T
				}
61
				for _, text := range comment.Text.R {
xurime's avatar
xurime 已提交
62 63 64
					if text.T != nil {
						sheetComment.Text += text.T.Val
					}
65 66 67 68
				}
				sheetComments = append(sheetComments, sheetComment)
			}
			comments[n] = sheetComments
69 70 71 72 73
		}
	}
	return
}

74
// getSheetComments provides the method to get the target comment reference by
75 76
// given worksheet file path.
func (f *File) getSheetComments(sheetFile string) string {
77
	rels := "xl/worksheets/_rels/" + sheetFile + ".rels"
xurime's avatar
xurime 已提交
78
	if sheetRels := f.relsReader(rels); sheetRels != nil {
xurime's avatar
xurime 已提交
79 80
		sheetRels.Lock()
		defer sheetRels.Unlock()
81 82 83 84
		for _, v := range sheetRels.Relationships {
			if v.Type == SourceRelationshipComments {
				return v.Target
			}
85 86 87 88 89
		}
	}
	return ""
}

90
// AddComment provides the method to add comment in a sheet by given worksheet
91 92
// 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
93 94
// comment in Sheet1!$A$30:
//
95
//    err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
96
//
97 98 99 100 101
func (f *File) AddComment(sheet, cell, format string) error {
	formatSet, err := parseFormatCommentsSet(format)
	if err != nil {
		return err
	}
102
	// Read sheet data.
103
	ws, err := f.workSheetReader(sheet)
xurime's avatar
xurime 已提交
104 105 106
	if err != nil {
		return err
	}
107 108 109 110
	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"
111
	if ws.LegacyDrawing != nil {
112
		// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
113
		sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
114
		commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
115
		drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
116 117
	} else {
		// Add first comment for given sheet.
118 119
		sheetXMLPath, _ := f.getSheetXMLPath(sheet)
		sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
xurime's avatar
xurime 已提交
120 121
		rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
		f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
122
		f.addSheetNameSpace(sheet, SourceRelationship)
123 124 125
		f.addSheetLegacyDrawing(sheet, rID)
	}
	commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
R
Rad Cirskis 已提交
126 127 128 129 130 131 132 133 134
	var colCount int
	for i, l := range strings.Split(formatSet.Text, "\n") {
		if ll := len(l); ll > colCount {
			if i == 0 {
				ll += len(formatSet.Author)
			}
			colCount = ll
		}
	}
135 136 137 138
	err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
	if err != nil {
		return err
	}
139
	f.addComment(commentsXML, cell, formatSet)
xurime's avatar
xurime 已提交
140
	f.addContentTypePart(commentID, "comments")
141
	return err
142 143
}

xurime's avatar
xurime 已提交
144
// addDrawingVML provides a function to create comment as
145
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
146 147 148 149 150
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
	col, row, err := CellNameToCoordinates(cell)
	if err != nil {
		return err
	}
151
	yAxis := col - 1
152
	xAxis := row - 1
153 154 155 156 157 158 159 160 161 162 163 164 165
	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,
				},
166
			},
167 168 169 170 171 172 173 174 175 176
			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",
177
					Connecttype:     "rect",
178
				},
179
			},
180
		}
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
		// load exist comment shapes from xl/drawings/vmlDrawing%d.vml (only once)
		d := f.decodeVMLDrawingReader(drawingVML)
		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)
			}
		}
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
	}
	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",
R
Rad Cirskis 已提交
223 224
			Anchor: fmt.Sprintf(
				"%d, 23, %d, 0, %d, %d, %d, 5",
xurime's avatar
xurime 已提交
225
				1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
R
Rad Cirskis 已提交
226 227 228
			AutoFill: "True",
			Row:      xAxis,
			Column:   yAxis,
229 230 231 232 233 234 235 236 237 238 239 240
		},
	}
	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)
241
	f.VMLDrawing[drawingVML] = vml
242
	return err
243 244
}

xurime's avatar
xurime 已提交
245 246
// addComment provides a function to create chart as xl/comments%d.xml by
// given cell and format sets.
247
func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
248 249
	a := formatSet.Author
	t := formatSet.Text
250 251
	if len(a) > MaxFieldLength {
		a = a[:MaxFieldLength]
252 253
	}
	if len(t) > 32512 {
254
		t = t[:32512]
255
	}
256
	comments := f.commentsReader(commentsXML)
257
	authorID := 0
258
	if comments == nil {
259 260
		comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{formatSet.Author}}}
	}
261
	if inStrSlice(comments.Authors.Author, formatSet.Author, true) == -1 {
262 263
		comments.Authors.Author = append(comments.Authors.Author, formatSet.Author)
		authorID = len(comments.Authors.Author) - 1
264
	}
265
	defaultFont := f.GetDefaultFont()
266
	bold := ""
267 268
	cmt := xlsxComment{
		Ref:      cell,
269
		AuthorID: authorID,
270 271
		Text: xlsxText{
			R: []xlsxR{
272
				{
273
					RPr: &xlsxRPr{
274
						B:  &bold,
275
						Sz: &attrValFloat{Val: float64Ptr(9)},
276 277 278
						Color: &xlsxColor{
							Indexed: 81,
						},
279 280
						RFont:  &attrValString{Val: stringPtr(defaultFont)},
						Family: &attrValInt{Val: intPtr(2)},
281
					},
xurime's avatar
xurime 已提交
282
					T: &xlsxT{Val: a},
283
				},
284
				{
285
					RPr: &xlsxRPr{
286
						Sz: &attrValFloat{Val: float64Ptr(9)},
287 288 289
						Color: &xlsxColor{
							Indexed: 81,
						},
290 291
						RFont:  &attrValString{Val: stringPtr(defaultFont)},
						Family: &attrValInt{Val: intPtr(2)},
292
					},
xurime's avatar
xurime 已提交
293
					T: &xlsxT{Val: t},
294 295 296 297 298
				},
			},
		},
	}
	comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
299
	f.Comments[commentsXML] = comments
300 301
}

xurime's avatar
xurime 已提交
302 303
// countComments provides a function to get comments files count storage in
// the folder xl.
304
func (f *File) countComments() int {
305
	c1, c2 := 0, 0
306 307
	f.Pkg.Range(func(k, v interface{}) bool {
		if strings.Contains(k.(string), "xl/comments") {
308
			c1++
309
		}
310 311
		return true
	})
312 313 314 315 316 317 318 319 320
	for rel := range f.Comments {
		if strings.Contains(rel, "xl/comments") {
			c2++
		}
	}
	if c1 < c2 {
		return c2
	}
	return c1
321
}
322 323 324 325

// 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 {
326 327
	var err error

328
	if f.DecodeVMLDrawing[path] == nil {
329 330
		c, ok := f.Pkg.Load(path)
		if ok && c != nil {
331
			f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
332
			if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))).
333 334 335
				Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
				log.Printf("xml decode error: %s", err)
			}
336 337 338 339 340
		}
	}
	return f.DecodeVMLDrawing[path]
}

341
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
342 343 344 345 346
// after serialize structure.
func (f *File) vmlDrawingWriter() {
	for path, vml := range f.VMLDrawing {
		if vml != nil {
			v, _ := xml.Marshal(vml)
347
			f.Pkg.Store(path, v)
348 349 350 351 352 353 354
		}
	}
}

// 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 {
355
	var err error
356
	if f.Comments[path] == nil {
357 358
		content, ok := f.Pkg.Load(path)
		if ok && content != nil {
359
			f.Comments[path] = new(xlsxComments)
360
			if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
361 362 363
				Decode(f.Comments[path]); err != nil && err != io.EOF {
				log.Printf("xml decode error: %s", err)
			}
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
		}
	}
	return f.Comments[path]
}

// 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)
		}
	}
}