From d9a0da7b48bac4175a23193a60f973c64d27835f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 11 Oct 2023 00:04:38 +0800 Subject: [PATCH] This closes #1687 and closes #1688 - Using sync map internally to get cell value concurrency safe - Support set the height and width for the comment box - Update the unit test --- cell.go | 9 ++++---- cell_test.go | 14 ++++++------- col_test.go | 3 ++- excelize.go | 23 +++++++++++---------- excelize_test.go | 3 ++- lib.go | 33 +++++++++++++++++++----------- lib_test.go | 6 ++++-- rows_test.go | 2 +- sheet.go | 12 +++++------ sheet_test.go | 7 ++++--- stream.go | 2 +- styles.go | 2 +- vml.go | 53 ++++++++++++++++++++++-------------------------- vmlDrawing.go | 2 -- vml_test.go | 2 +- workbook.go | 8 ++++++-- xmlComments.go | 2 ++ 17 files changed, 99 insertions(+), 84 deletions(-) diff --git a/cell.go b/cell.go index c56b587..dd9980d 100644 --- a/cell.go +++ b/cell.go @@ -1317,10 +1317,15 @@ func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) { // value function. Passed function implements specific part of required // logic. func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return "", err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() cell, err = ws.mergeCellsParser(cell) if err != nil { return "", err @@ -1329,10 +1334,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c if err != nil { return "", err } - - ws.mu.Lock() - defer ws.mu.Unlock() - lastRowNum := 0 if l := len(ws.SheetData.Row); l > 0 { lastRowNum = ws.SheetData.Row[l-1].R diff --git a/cell_test.go b/cell_test.go index a4a2ddf..1307688 100644 --- a/cell_test.go +++ b/cell_test.go @@ -313,7 +313,7 @@ func TestGetCellValue(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) - f.checked = nil + f.checked = sync.Map{} cells := []string{"A3", "A4", "B4", "A7", "B7"} rows, err := f.GetRows("Sheet1") assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) @@ -329,35 +329,35 @@ func TestGetCellValue(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) - f.checked = nil + f.checked = sync.Map{} cell, err := f.GetCellValue("Sheet1", "A2") assert.Equal(t, "A2", cell) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) - f.checked = nil + f.checked = sync.Map{} rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1B1`))) - f.checked = nil + f.checked = sync.Map{} rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{{"A1", "B1"}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) - f.checked = nil + f.checked = sync.Map{} rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) - f.checked = nil + f.checked = sync.Map{} cell, err = f.GetCellValue("Sheet1", "H6") assert.Equal(t, "H6", cell) assert.NoError(t, err) @@ -410,7 +410,7 @@ func TestGetCellValue(t *testing.T) { 20221022T150529Z 2022-10-22T15:05:29Z 2020-07-10 15:00:00.000`))) - f.checked = nil + f.checked = sync.Map{} rows, err = f.GetCols("Sheet1") assert.Equal(t, []string{ "2422.3", diff --git a/col_test.go b/col_test.go index f1fe032..9af8ca0 100644 --- a/col_test.go +++ b/col_test.go @@ -2,6 +2,7 @@ package excelize import ( "path/filepath" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -125,7 +126,7 @@ func TestGetColsError(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) - f.checked = nil + f.checked = sync.Map{} _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) diff --git a/excelize.go b/excelize.go index 0f1073f..80ba1d3 100644 --- a/excelize.go +++ b/excelize.go @@ -30,8 +30,8 @@ import ( type File struct { mu sync.Mutex options *Options - xmlAttr map[string][]xml.Attr - checked map[string]bool + xmlAttr sync.Map + checked sync.Map sheetMap map[string]string streams map[string]*StreamWriter tempFiles sync.Map @@ -133,8 +133,8 @@ func OpenFile(filename string, opts ...Options) (*File, error) { func newFile() *File { return &File{ options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize}, - xmlAttr: make(map[string][]xml.Attr), - checked: make(map[string]bool), + xmlAttr: sync.Map{}, + checked: sync.Map{}, sheetMap: make(map[string]string), tempFiles: sync.Map{}, Comments: make(map[string]*xlsxComments), @@ -275,24 +275,25 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { } } ws = new(xlsxWorksheet) - if _, ok := f.xmlAttr[name]; !ok { + if attrs, ok := f.xmlAttr.Load(name); !ok { d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))) - f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) + if attrs == nil { + attrs = []xml.Attr{} + } + attrs = append(attrs.([]xml.Attr), getRootElement(d)...) + f.xmlAttr.Store(name, attrs) } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))). Decode(ws); err != nil && err != io.EOF { return } err = nil - if f.checked == nil { - f.checked = make(map[string]bool) - } - if ok = f.checked[name]; !ok { + if _, ok = f.checked.Load(name); !ok { ws.checkSheet() if err = ws.checkRow(); err != nil { return } - f.checked[name] = true + f.checked.Store(name, true) } f.Sheet.Store(name, ws) return diff --git a/excelize_test.go b/excelize_test.go index 5eb19aa..e1c8401 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "testing" "time" @@ -1531,7 +1532,7 @@ func TestWorkSheetReader(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) - f.checked = nil + f.checked = sync.Map{} _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) } diff --git a/lib.go b/lib.go index 417ce79..a694463 100644 --- a/lib.go +++ b/lib.go @@ -682,8 +682,8 @@ func getXMLNamespace(space string, attr []xml.Attr) string { func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte { sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) targetXmlns := []byte(templateNamespaceIDMap) - if attr, ok := f.xmlAttr[path]; ok { - targetXmlns = []byte(genXMLNamespace(attr)) + if attrs, ok := f.xmlAttr.Load(path); ok { + targetXmlns = []byte(genXMLNamespace(attrs.([]xml.Attr))) } return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1) } @@ -694,29 +694,36 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { exist := false mc := false ignore := -1 - if attr, ok := f.xmlAttr[path]; ok { - for i, attribute := range attr { - if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space { + if attrs, ok := f.xmlAttr.Load(path); ok { + for i, attr := range attrs.([]xml.Attr) { + if attr.Name.Local == ns.Name.Local && attr.Name.Space == ns.Name.Space { exist = true } - if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" { + if attr.Name.Local == "Ignorable" && getXMLNamespace(attr.Name.Space, attrs.([]xml.Attr)) == "mc" { ignore = i } - if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" { + if attr.Name.Local == "mc" && attr.Name.Space == "xmlns" { mc = true } } } if !exist { - f.xmlAttr[path] = append(f.xmlAttr[path], ns) + attrs, _ := f.xmlAttr.Load(path) + if attrs == nil { + attrs = []xml.Attr{} + } + attrs = append(attrs.([]xml.Attr), ns) + f.xmlAttr.Store(path, attrs) if !mc { - f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility) + attrs = append(attrs.([]xml.Attr), SourceRelationshipCompatibility) + f.xmlAttr.Store(path, attrs) } if ignore == -1 { - f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ + attrs = append(attrs.([]xml.Attr), xml.Attr{ Name: xml.Name{Local: "Ignorable", Space: "mc"}, Value: ns.Name.Local, }) + f.xmlAttr.Store(path, attrs) return } f.setIgnorableNameSpace(path, ignore, ns) @@ -727,8 +734,10 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { // by the given attribute. func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} - if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 { - f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local)) + xmlAttrs, _ := f.xmlAttr.Load(path) + if inStrSlice(strings.Fields(xmlAttrs.([]xml.Attr)[index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 { + xmlAttrs.([]xml.Attr)[index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", xmlAttrs.([]xml.Attr)[index].Value, ns.Name.Local)) + f.xmlAttr.Store(path, xmlAttrs) } } diff --git a/lib_test.go b/lib_test.go index 46650e5..48e730d 100644 --- a/lib_test.go +++ b/lib_test.go @@ -294,9 +294,11 @@ func TestGetRootElement(t *testing.T) { func TestSetIgnorableNameSpace(t *testing.T) { f := NewFile() - f.xmlAttr["xml_path"] = []xml.Attr{{}} + f.xmlAttr.Store("xml_path", []xml.Attr{{}}) f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}}) - assert.EqualValues(t, "c14", f.xmlAttr["xml_path"][0].Value) + attrs, ok := f.xmlAttr.Load("xml_path") + assert.EqualValues(t, "c14", attrs.([]xml.Attr)[0].Value) + assert.True(t, ok) } func TestStack(t *testing.T) { diff --git a/rows_test.go b/rows_test.go index 1cba0a7..768f8b0 100644 --- a/rows_test.go +++ b/rows_test.go @@ -944,7 +944,7 @@ func TestCheckRow(t *testing.T) { f = NewFile() f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`12345`)) f.Sheet.Delete("xl/worksheets/sheet1.xml") - delete(f.checked, "xl/worksheets/sheet1.xml") + f.checked.Delete("xl/worksheets/sheet1.xml") assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error()) } diff --git a/sheet.go b/sheet.go index 8f678b5..5672484 100644 --- a/sheet.go +++ b/sheet.go @@ -164,10 +164,10 @@ func (f *File) workSheetWriter() { // reusing buffer _ = encoder.Encode(sheet) f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes()))) - ok := f.checked[p.(string)] + _, ok := f.checked.Load(p.(string)) if ok { f.Sheet.Delete(p.(string)) - f.checked[p.(string)] = false + f.checked.Store(p.(string), false) } buffer.Reset() } @@ -237,7 +237,7 @@ func (f *File) setSheet(index int, name string) { sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[name] = sheetXMLPath f.Sheet.Store(sheetXMLPath, &ws) - f.xmlAttr[sheetXMLPath] = []xml.Attr{NameSpaceSpreadSheet} + f.xmlAttr.Store(sheetXMLPath, []xml.Attr{NameSpaceSpreadSheet}) } // relsWriter provides a function to save relationships after @@ -583,7 +583,7 @@ func (f *File) DeleteSheet(sheet string) error { f.Pkg.Delete(rels) f.Relationships.Delete(rels) f.Sheet.Delete(sheetXML) - delete(f.xmlAttr, sheetXML) + f.xmlAttr.Delete(sheetXML) f.SheetCount-- } index, err := f.GetSheetIndex(activeSheetName) @@ -714,8 +714,8 @@ func (f *File) copySheet(from, to int) error { f.Pkg.Store(toRels, rels.([]byte)) } fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet) - fromSheetAttr := f.xmlAttr[fromSheetXMLPath] - f.xmlAttr[sheetXMLPath] = fromSheetAttr + fromSheetAttr, _ := f.xmlAttr.Load(fromSheetXMLPath) + f.xmlAttr.Store(sheetXMLPath, fromSheetAttr) return err } diff --git a/sheet_test.go b/sheet_test.go index 6851dfc..bb9a786 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -120,7 +121,7 @@ func TestPanes(t *testing.T) { // Test add pane on empty sheet views worksheet f = NewFile() - f.checked = nil + f.checked = sync.Map{} f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) assert.NoError(t, f.SetPanes("Sheet1", @@ -173,7 +174,7 @@ func TestSearchSheet(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) - f.checked = nil + f.checked = sync.Map{} result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax") assert.Equal(t, []string(nil), result) @@ -462,7 +463,7 @@ func TestWorksheetWriter(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") worksheet := xml.Header + `%d` f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1))) - f.checked = nil + f.checked = sync.Map{} assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2)) f.workSheetWriter() value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml") diff --git a/stream.go b/stream.go index fe637b9..2247bcb 100644 --- a/stream.go +++ b/stream.go @@ -678,7 +678,7 @@ func (sw *StreamWriter) Flush() error { sheetPath := sw.file.sheetMap[sw.Sheet] sw.file.Sheet.Delete(sheetPath) - delete(sw.file.checked, sheetPath) + sw.file.checked.Delete(sheetPath) sw.file.Pkg.Delete(sheetPath) return nil diff --git a/styles.go b/styles.go index 40505b7..433cb8d 100644 --- a/styles.go +++ b/styles.go @@ -2161,7 +2161,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // The 'Criteria' parameter is used to set the criteria by which the cell data // will be evaluated. It has no default value. The most common criteria as -// applied to {"type":"cell"} are: +// applied to {Type: "cell"} are: // // between | // not between | diff --git a/vml.go b/vml.go index 8660193..993f2f1 100644 --- a/vml.go +++ b/vml.go @@ -94,23 +94,33 @@ func (f *File) getSheetComments(sheetFile string) string { 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: +// AddComment provides the method to add comments in a sheet by giving the +// worksheet name, cell reference, and format set (such as author and text). +// Note that the maximum author name length is 255 and the max text length is +// 32512. For example, add a rich-text comment with a specified comments box +// size in Sheet1!A5: // // err := f.AddComment("Sheet1", excelize.Comment{ -// Cell: "A12", +// Cell: "A5", // Author: "Excelize", // Paragraph: []excelize.RichTextRun{ // {Text: "Excelize: ", Font: &excelize.Font{Bold: true}}, // {Text: "This is a comment."}, // }, +// Height: 40, +// Width: 180, // }) func (f *File) AddComment(sheet string, opts Comment) error { return f.addVMLObject(vmlOptions{ sheet: sheet, Comment: opts, - FormControl: FormControl{Cell: opts.Cell, Type: FormControlNote}, + FormControl: FormControl{ + Cell: opts.Cell, + Type: FormControlNote, + Text: opts.Text, + Paragraph: opts.Paragraph, + Width: opts.Width, + Height: opts.Height, + }, }) } @@ -529,31 +539,17 @@ func (f *File) addVMLObject(opts vmlOptions) error { // 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.FormControl.Paragraph { - for _, subStr := range strings.Split(runs.Text, "\n") { - opts.rows++ - if chars := len(subStr); chars > opts.cols { - opts.cols = chars - } - } - } - if len(opts.FormControl.Paragraph) == 0 { - opts.rows, opts.cols = 1, len(opts.FormControl.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.FormControl.Width == 0 { + opts.FormControl.Width = 140 } - if opts.Width == 0 { - opts.Width = uint(opts.cols * 9) - } - if opts.Height == 0 { - opts.Height = uint(opts.rows * 25) + if opts.FormControl.Height == 0 { + opts.FormControl.Height = 60 } return opts } @@ -818,15 +814,14 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er 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] + leftOffset, vmlID, vml, preset := 23, 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden" if opts.formCtrl { - vmlID = 201 + leftOffset, vmlID = 0, 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) } + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.FormControl.Width), int(opts.FormControl.Height)) + anchor := fmt.Sprintf("%d, %d, %d, 0, %d, %d, %d, %d", colStart, leftOffset, rowStart, colEnd, x2, rowEnd, y2) if vml == nil { vml = &vmlDrawing{ XMLNSv: "urn:schemas-microsoft-com:vml", diff --git a/vmlDrawing.go b/vmlDrawing.go index 7ebedb7..fa293be 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -276,8 +276,6 @@ type formCtrlPreset struct { // vmlOptions defines the structure used to internal comments and form controls. type vmlOptions struct { - rows int - cols int formCtrl bool sheet string Comment diff --git a/vml_test.go b/vml_test.go index 5491454..8e38fbe 100644 --- a/vml_test.go +++ b/vml_test.go @@ -153,7 +153,7 @@ func TestAddDrawingVML(t *testing.T) { assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*"))) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{sheet: "Sheet1", FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8") } func TestFormControl(t *testing.T) { diff --git a/workbook.go b/workbook.go index 0e635d2..5dfe9d2 100644 --- a/workbook.go +++ b/workbook.go @@ -197,9 +197,13 @@ func (f *File) workbookReader() (*xlsxWorkbook, error) { if f.WorkBook == nil { wbPath := f.getWorkbookPath() f.WorkBook = new(xlsxWorkbook) - if _, ok := f.xmlAttr[wbPath]; !ok { + if attrs, ok := f.xmlAttr.Load(wbPath); !ok { d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))) - f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...) + if attrs == nil { + attrs = []xml.Attr{} + } + attrs = append(attrs.([]xml.Attr), getRootElement(d)...) + f.xmlAttr.Store(wbPath, attrs) f.addNameSpaces(wbPath, SourceRelationship) } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))). diff --git a/xmlComments.go b/xmlComments.go index c27cd70..a28f5cc 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -78,5 +78,7 @@ type Comment struct { AuthorID int Cell string Text string + Width uint + Height uint Paragraph []RichTextRun }