// 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 FormControlCheckbox 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 worksheet by given // worksheet name and cell reference. 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, }) } // DeleteFormControl provides the method to delete form control in a worksheet // by given worksheet name and cell reference. For example, delete the form // control in Sheet1!$A$30: // // err := f.DeleteFormControl("Sheet1", "A30") func (f *File) DeleteFormControl(sheet, cell string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } col, row, err := CellNameToCoordinates(cell) if err != nil { return err } if ws.LegacyDrawing == nil { return err } sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") 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: vmlID}, }, ShapeType: &xlsxShapeType{ 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) } } } for i, sp := range vml.Shape { var shapeVal decodeShapeVal if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil && shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 { vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) break } } f.VMLDrawing[drawingVML] = vml return err } // 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" sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" 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. rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") 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 } if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" { sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") } } 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 + "

\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 = "" + fnt.Content + "" } if run.Font.Italic { fnt.Content = "" + fnt.Content + "" } if run.Font.Bold { fnt.Content = "" + fnt.Content + "" } } 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, }, FormControlCheckbox: { objectType: "Checkbox", filled: "f", fillColor: "window [65]", stroked: "f", strokeColor: "windowText [64]", strokeButton: "", fill: nil, textHAlign: "", textVAlign: "Center", noThreeD: stringPtr(""), 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 == FormControlCheckbox || 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 }