comment.go 9.3 KB
Newer Older
xurime's avatar
xurime 已提交
1
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
xurime's avatar
xurime 已提交
2 3 4 5 6 7 8
// 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 XLSX files. Support reads and writes XLSX file generated by
// Microsoft Excel™ 2007 and later. Support save file without losing original
// charts of XLSX. This library needs Go version 1.8 or later.
xurime's avatar
xurime 已提交
9

10 11 12 13 14
package excelize

import (
	"encoding/json"
	"encoding/xml"
R
Rad Cirskis 已提交
15
	"fmt"
16 17 18 19
	"strconv"
	"strings"
)

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

xurime's avatar
xurime 已提交
31 32
// GetComments retrieves all comments and returns a map of worksheet name to
// the worksheet comments.
33 34
func (f *File) GetComments() (comments map[string][]Comment) {
	comments = map[string][]Comment{}
35
	for n := range f.sheetMap {
36
		if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")); d != nil {
37 38 39 40 41 42 43 44 45 46 47 48 49 50
			sheetComments := []Comment{}
			for _, comment := range d.CommentList.Comment {
				sheetComment := Comment{}
				if comment.AuthorID < len(d.Authors) {
					sheetComment.Author = d.Authors[comment.AuthorID].Author
				}
				sheetComment.Ref = comment.Ref
				sheetComment.AuthorID = comment.AuthorID
				for _, text := range comment.Text.R {
					sheetComment.Text += text.T
				}
				sheetComments = append(sheetComments, sheetComment)
			}
			comments[n] = sheetComments
51 52 53 54 55
		}
	}
	return
}

56 57 58 59
// getSheetComments provides the method to get the target comment reference by
// given worksheet index.
func (f *File) getSheetComments(sheetID int) string {
	var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels"
60 61 62 63 64
	if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil {
		for _, v := range sheetRels.Relationships {
			if v.Type == SourceRelationshipComments {
				return v.Target
			}
65 66 67 68 69
		}
	}
	return ""
}

70
// AddComment provides the method to add comment in a sheet by given worksheet
71 72
// 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
73 74
// comment in Sheet1!$A$30:
//
75
//    err := xlsx.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
76
//
77 78 79 80 81
func (f *File) AddComment(sheet, cell, format string) error {
	formatSet, err := parseFormatCommentsSet(format)
	if err != nil {
		return err
	}
82
	// Read sheet data.
xurime's avatar
xurime 已提交
83 84 85 86
	xlsx, err := f.workSheetReader(sheet)
	if err != nil {
		return err
	}
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
	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 xlsx.LegacyDrawing != nil {
		// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
		sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, xlsx.LegacyDrawing.RID)
		commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
		drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
	} else {
		// Add first comment for given sheet.
		rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
		f.addSheetRelationships(sheet, SourceRelationshipComments, sheetRelationshipsComments, "")
		f.addSheetLegacyDrawing(sheet, rID)
	}
	commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
	f.addComment(commentsXML, cell, formatSet)
R
Rad Cirskis 已提交
104 105 106 107 108 109 110 111 112
	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
		}
	}
113 114 115 116
	err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
	if err != nil {
		return err
	}
xurime's avatar
xurime 已提交
117
	f.addContentTypePart(commentID, "comments")
118
	return err
119 120
}

xurime's avatar
xurime 已提交
121
// addDrawingVML provides a function to create comment as
122
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
123 124 125 126 127
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
	col, row, err := CellNameToCoordinates(cell)
	if err != nil {
		return err
	}
128
	yAxis := col - 1
129
	xAxis := row - 1
130 131 132 133 134 135 136 137 138 139 140 141 142
	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,
				},
143
			},
144 145 146 147 148 149 150 151 152 153 154 155
			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:     "miter",
				},
156
			},
157
		}
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
	}
	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 已提交
185 186
			Anchor: fmt.Sprintf(
				"%d, 23, %d, 0, %d, %d, %d, 5",
xurime's avatar
xurime 已提交
187
				1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
R
Rad Cirskis 已提交
188 189 190
			AutoFill: "True",
			Row:      xAxis,
			Column:   yAxis,
191 192 193 194 195 196 197 198 199 200 201
		},
	}
	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]),
	}
202 203
	d := f.decodeVMLDrawingReader(drawingVML)
	if d != nil {
204 205 206 207 208 209 210 211 212 213 214 215 216
		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)
		}
	}
	vml.Shape = append(vml.Shape, shape)
217
	f.VMLDrawing[drawingVML] = vml
218
	return err
219 220
}

xurime's avatar
xurime 已提交
221 222
// addComment provides a function to create chart as xl/comments%d.xml by
// given cell and format sets.
223
func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
224 225 226 227 228 229 230 231
	a := formatSet.Author
	t := formatSet.Text
	if len(a) > 255 {
		a = a[0:255]
	}
	if len(t) > 32512 {
		t = t[0:32512]
	}
232 233 234 235 236 237 238
	comments := f.commentsReader(commentsXML)
	if comments == nil {
		comments = &xlsxComments{
			Authors: []xlsxAuthor{
				{
					Author: formatSet.Author,
				},
239
			},
240
		}
241 242 243 244 245 246
	}
	cmt := xlsxComment{
		Ref:      cell,
		AuthorID: 0,
		Text: xlsxText{
			R: []xlsxR{
247
				{
248 249
					RPr: &xlsxRPr{
						B:  " ",
xurime's avatar
xurime 已提交
250
						Sz: &attrValFloat{Val: 9},
251 252 253 254 255 256
						Color: &xlsxColor{
							Indexed: 81,
						},
						RFont:  &attrValString{Val: "Calibri"},
						Family: &attrValInt{Val: 2},
					},
257
					T: a,
258
				},
259
				{
260
					RPr: &xlsxRPr{
xurime's avatar
xurime 已提交
261
						Sz: &attrValFloat{Val: 9},
262 263 264 265 266 267
						Color: &xlsxColor{
							Indexed: 81,
						},
						RFont:  &attrValString{Val: "Calibri"},
						Family: &attrValInt{Val: 2},
					},
268
					T: t,
269 270 271 272 273
				},
			},
		},
	}
	comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
274
	f.Comments[commentsXML] = comments
275 276
}

xurime's avatar
xurime 已提交
277 278
// countComments provides a function to get comments files count storage in
// the folder xl.
279 280 281 282 283 284 285 286 287
func (f *File) countComments() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/comments") {
			count++
		}
	}
	return count
}
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302

// 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 {
	if f.DecodeVMLDrawing[path] == nil {
		c, ok := f.XLSX[path]
		if ok {
			d := decodeVmlDrawing{}
			_ = xml.Unmarshal(namespaceStrictToTransitional(c), &d)
			f.DecodeVMLDrawing[path] = &d
		}
	}
	return f.DecodeVMLDrawing[path]
}

303
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
// after serialize structure.
func (f *File) vmlDrawingWriter() {
	for path, vml := range f.VMLDrawing {
		if vml != nil {
			v, _ := xml.Marshal(vml)
			f.XLSX[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 {
	if f.Comments[path] == nil {
		content, ok := f.XLSX[path]
		if ok {
			c := xlsxComments{}
			_ = xml.Unmarshal(namespaceStrictToTransitional(content), &c)
			f.Comments[path] = &c
		}
	}
	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)
		}
	}
}