This closes #1582, fixes the formula calculation bug, and improves form controls
- Fix incorrect formula calculate results on a nested argument function which returns a numeric result - Add a new exported error variable `ErrorFormControlValue` - Rename exported enumeration `FormControlCheckbox` to `FormControlCheckBox` - Rename exported enumeration `FormControlRadio` to `FormControlOptionButton` - The `AddFormControl` function supports new 5 form controls: spin button, check box, group box, label, and scroll bar - Update documentation for the `GraphicOptions` data type, `AddFormControl` and `NewStreamWriter` functions - Update the unit tests
This commit is contained in:
parent
b667987084
commit
8d996ca138
16
calc.go
16
calc.go
|
@ -1053,16 +1053,16 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex
|
|||
if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) {
|
||||
// mathematics calculate in formula function
|
||||
opfdStack.Push(arg)
|
||||
} else {
|
||||
argsStack.Peek().(*list.List).PushBack(arg)
|
||||
return newEmptyFormulaArg()
|
||||
}
|
||||
} else {
|
||||
val := arg.Value()
|
||||
if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 {
|
||||
val = arg.Matrix[0][0].Value()
|
||||
}
|
||||
opdStack.Push(newStringFormulaArg(val))
|
||||
argsStack.Peek().(*list.List).PushBack(arg)
|
||||
return newEmptyFormulaArg()
|
||||
}
|
||||
if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 {
|
||||
opdStack.Push(arg.Matrix[0][0])
|
||||
return newEmptyFormulaArg()
|
||||
}
|
||||
opdStack.Push(arg)
|
||||
return newEmptyFormulaArg()
|
||||
}
|
||||
|
||||
|
|
17
calc_test.go
17
calc_test.go
|
@ -5918,6 +5918,23 @@ func TestCalcCellResolver(t *testing.T) {
|
|||
assert.NoError(t, err, formula)
|
||||
assert.Equal(t, expected, result, formula)
|
||||
}
|
||||
// Test calculates formula that contains a nested argument function which returns a numeric result
|
||||
f = NewFile()
|
||||
for _, cell := range []string{"A1", "B2", "B3", "B4"} {
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", cell, "ABC"))
|
||||
}
|
||||
for cell, formula := range map[string]string{
|
||||
"A2": "IF(B2<>\"\",MAX(A1:A1)+1,\"\")",
|
||||
"A3": "IF(B3<>\"\",MAX(A1:A2)+1,\"\")",
|
||||
"A4": "IF(B4<>\"\",MAX(A1:A3)+1,\"\")",
|
||||
} {
|
||||
assert.NoError(t, f.SetCellFormula("Sheet1", cell, formula))
|
||||
}
|
||||
for cell, expected := range map[string]string{"A2": "1", "A3": "2", "A4": "3"} {
|
||||
result, err := f.CalcCellValue("Sheet1", cell)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalInfixExp(t *testing.T) {
|
||||
|
|
|
@ -255,4 +255,7 @@ var (
|
|||
// ErrUnprotectWorkbookPassword defined the error message on remove workbook
|
||||
// protection with password verification failed.
|
||||
ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
|
||||
// ErrorFormControlValue defined the error message for receiving a scroll
|
||||
// value exceeds limit.
|
||||
ErrorFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue)
|
||||
)
|
||||
|
|
64
picture.go
64
picture.go
|
@ -110,41 +110,46 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
|
|||
// }
|
||||
// }
|
||||
//
|
||||
// The optional parameter "AutoFit" specifies if you make image size auto-fits the
|
||||
// cell, the default value of that is 'false'.
|
||||
// The optional parameter "AltText" is used to add alternative text to a graph
|
||||
// object.
|
||||
//
|
||||
// The optional parameter "Hyperlink" specifies the hyperlink of the image.
|
||||
// The optional parameter "PrintObject" indicates whether the graph object is
|
||||
// printed when the worksheet is printed, the default value of that is 'true'.
|
||||
//
|
||||
// The optional parameter "Locked" indicates whether lock the graph object.
|
||||
// Locking an object has no effect unless the sheet is protected.
|
||||
//
|
||||
// The optional parameter "LockAspectRatio" indicates whether lock aspect ratio
|
||||
// for the graph object, the default value of that is 'false'.
|
||||
//
|
||||
// The optional parameter "AutoFit" specifies if you make graph object size
|
||||
// auto-fits the cell, the default value of that is 'false'.
|
||||
//
|
||||
// The optional parameter "OffsetX" specifies the horizontal offset of the graph
|
||||
// object with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "OffsetY" specifies the vertical offset of the graph
|
||||
// object with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "ScaleX" specifies the horizontal scale of graph
|
||||
// object, the default value of that is 1.0 which presents 100%.
|
||||
//
|
||||
// The optional parameter "ScaleY" specifies the vertical scale of graph object,
|
||||
// the default value of that is 1.0 which presents 100%.
|
||||
//
|
||||
// The optional parameter "Hyperlink" specifies the hyperlink of the graph
|
||||
// object.
|
||||
//
|
||||
// The optional parameter "HyperlinkType" defines two types of
|
||||
// hyperlink "External" for website or "Location" for moving to one of the
|
||||
// cells in this workbook. When the "HyperlinkType" is "Location",
|
||||
// coordinates need to start with "#".
|
||||
//
|
||||
// The optional parameter "Positioning" defines two types of the position of an
|
||||
// image in an Excel spreadsheet, "oneCell" (Move but don't size with
|
||||
// cells) or "absolute" (Don't move or size with cells). If you don't set this
|
||||
// parameter, the default positioning is move and size with cells.
|
||||
//
|
||||
// The optional parameter "PrintObject" indicates whether the image is printed
|
||||
// when the worksheet is printed, the default value of that is 'true'.
|
||||
//
|
||||
// The optional parameter "LockAspectRatio" indicates whether lock aspect
|
||||
// ratio for the image, the default value of that is 'false'.
|
||||
//
|
||||
// The optional parameter "Locked" indicates whether lock the image. Locking
|
||||
// an object has no effect unless the sheet is protected.
|
||||
//
|
||||
// The optional parameter "OffsetX" specifies the horizontal offset of the
|
||||
// image with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "ScaleX" specifies the horizontal scale of images,
|
||||
// the default value of that is 1.0 which presents 100%.
|
||||
//
|
||||
// The optional parameter "OffsetY" specifies the vertical offset of the
|
||||
// image with the cell, the default value of that is 0.
|
||||
//
|
||||
// The optional parameter "ScaleY" specifies the vertical scale of images,
|
||||
// the default value of that is 1.0 which presents 100%.
|
||||
// The optional parameter "Positioning" defines 3 types of the position of a
|
||||
// graph object in a spreadsheet: "oneCell" (Move but don't size with
|
||||
// cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or
|
||||
// size with cells). If you don't set this parameter, the default positioning
|
||||
// is to move and size with cells.
|
||||
func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error {
|
||||
var err error
|
||||
// Check picture exists first.
|
||||
|
@ -330,6 +335,9 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 {
|
||||
return ErrParameterInvalid
|
||||
}
|
||||
width, height := img.Width, img.Height
|
||||
if opts.AutoFit {
|
||||
if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil {
|
||||
|
|
|
@ -192,6 +192,8 @@ func TestAddDrawingPicture(t *testing.T) {
|
|||
f := NewFile()
|
||||
opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}
|
||||
assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
|
||||
// Test addDrawingPicture with invalid positioning types
|
||||
assert.Equal(t, f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}), ErrParameterInvalid)
|
||||
|
||||
path := "xl/drawings/drawing1.xml"
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
|
|
18
stream.go
18
stream.go
|
@ -38,14 +38,16 @@ type StreamWriter struct {
|
|||
tableParts string
|
||||
}
|
||||
|
||||
// NewStreamWriter return stream writer struct by given worksheet name for
|
||||
// generate new worksheet with large amounts of data. Note that after set
|
||||
// rows, you must call the 'Flush' method to end the streaming writing process
|
||||
// and ensure that the order of row numbers is ascending, the normal mode
|
||||
// functions and stream mode functions can't be work mixed to writing data on
|
||||
// the worksheets, you can't get cell value when in-memory chunks data over
|
||||
// 16MB. For example, set data for worksheet of size 102400 rows x 50 columns
|
||||
// with numbers and style:
|
||||
// NewStreamWriter returns stream writer struct by given worksheet name used for
|
||||
// writing data on a new existing empty worksheet with large amounts of data.
|
||||
// Note that after writing data with the stream writer for the worksheet, you
|
||||
// must call the 'Flush' method to end the streaming writing process, ensure
|
||||
// that the order of row numbers is ascending when set rows, and the normal
|
||||
// mode functions and stream mode functions can not be work mixed to writing
|
||||
// data on the worksheets. The stream writer will try to use temporary files on
|
||||
// disk to reduce the memory usage when in-memory chunks data over 16MB, and
|
||||
// you can't get cell value at this time. For example, set data for worksheet
|
||||
// of size 102400 rows x 50 columns with numbers and style:
|
||||
//
|
||||
// f := excelize.NewFile()
|
||||
// defer func() {
|
||||
|
|
323
vml.go
323
vml.go
|
@ -28,8 +28,12 @@ type FormControlType byte
|
|||
const (
|
||||
FormControlNote FormControlType = iota
|
||||
FormControlButton
|
||||
FormControlCheckbox
|
||||
FormControlRadio
|
||||
FormControlOptionButton
|
||||
FormControlSpinButton
|
||||
FormControlCheckBox
|
||||
FormControlGroupBox
|
||||
FormControlLabel
|
||||
FormControlScrollBar
|
||||
)
|
||||
|
||||
// GetComments retrieves all comments in a worksheet by given worksheet name.
|
||||
|
@ -105,13 +109,8 @@ func (f *File) getSheetComments(sheetFile string) string {
|
|||
// })
|
||||
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,
|
||||
sheet: sheet, Comment: opts,
|
||||
FormControl: FormControl{Cell: opts.Cell, Type: FormControlNote},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -184,18 +183,18 @@ func (f *File) addComment(commentsXML string, opts vmlOptions) error {
|
|||
return err
|
||||
}
|
||||
chars, cmt := 0, xlsxComment{
|
||||
Ref: opts.Cell,
|
||||
Ref: opts.Comment.Cell,
|
||||
AuthorID: authorID,
|
||||
Text: xlsxText{R: []xlsxR{}},
|
||||
}
|
||||
if opts.Text != "" {
|
||||
if len(opts.Text) > TotalCellChars {
|
||||
opts.Text = opts.Text[:TotalCellChars]
|
||||
if opts.Comment.Text != "" {
|
||||
if len(opts.Comment.Text) > TotalCellChars {
|
||||
opts.Comment.Text = opts.Comment.Text[:TotalCellChars]
|
||||
}
|
||||
cmt.Text.T = stringPtr(opts.Text)
|
||||
chars += len(opts.Text)
|
||||
cmt.Text.T = stringPtr(opts.Comment.Text)
|
||||
chars += len(opts.Comment.Text)
|
||||
}
|
||||
for _, run := range opts.Paragraph {
|
||||
for _, run := range opts.Comment.Paragraph {
|
||||
if chars == TotalCellChars {
|
||||
break
|
||||
}
|
||||
|
@ -277,9 +276,15 @@ func (f *File) commentsWriter() {
|
|||
|
||||
// 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:
|
||||
// type: button, check box, group box, label, option button, scroll bar and
|
||||
// spinner. If set macro for the form control, the workbook extension should be
|
||||
// XLSM or XLTM. Scroll value must be between 0 and 30000.
|
||||
//
|
||||
// Example 1, add button form control with macro, rich-text, custom button size,
|
||||
// print property on Sheet1!A1, and let the button do not move or size with
|
||||
// cells:
|
||||
//
|
||||
// enable := true
|
||||
// err := f.AddFormControl("Sheet1", excelize.FormControl{
|
||||
// Cell: "A1",
|
||||
// Type: excelize.FormControlButton,
|
||||
|
@ -300,20 +305,51 @@ func (f *File) commentsWriter() {
|
|||
// Text: "C1=A1+B1",
|
||||
// },
|
||||
// },
|
||||
// Format: excelize.GraphicOptions{
|
||||
// PrintObject: &enable,
|
||||
// Positioning: "absolute",
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// Example 2, add option button form control with checked status and text on
|
||||
// Sheet1!A1:
|
||||
//
|
||||
// err := f.AddFormControl("Sheet1", excelize.FormControl{
|
||||
// Cell: "A1",
|
||||
// Type: excelize.FormControlOptionButton,
|
||||
// Text: "Option Button 1",
|
||||
// Checked: true,
|
||||
// })
|
||||
//
|
||||
// Example 3, add spin button form control on Sheet1!A2 to increase or decrease
|
||||
// the value of Sheet1!A1:
|
||||
//
|
||||
// err := f.AddFormControl("Sheet1", excelize.FormControl{
|
||||
// Cell: "A2",
|
||||
// Type: excelize.FormControlSpinButton,
|
||||
// CurrentVal: 7,
|
||||
// MinVal: 5,
|
||||
// MaxVal: 10,
|
||||
// IncChange: 1,
|
||||
// CellLink: "A1",
|
||||
// })
|
||||
//
|
||||
// Example 4, add horizontally scroll bar form control on Sheet1!A2 to change
|
||||
// the value of Sheet1!A1 by click the scroll arrows or drag the scroll box:
|
||||
//
|
||||
// err := f.AddFormControl("Sheet1", excelize.FormControl{
|
||||
// Cell: "A2",
|
||||
// Type: excelize.FormControlScrollBar,
|
||||
// CurrentVal: 50,
|
||||
// MinVal: 10,
|
||||
// MaxVal: 100,
|
||||
// IncChange: 1,
|
||||
// PageChange: 1,
|
||||
// CellLink: "A1",
|
||||
// })
|
||||
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,
|
||||
formCtrl: true, sheet: sheet, FormControl: opts,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -443,41 +479,41 @@ func (f *File) vmlDrawingWriter() {
|
|||
// relationships for comments and form controls.
|
||||
func (f *File) addVMLObject(opts vmlOptions) error {
|
||||
// Read sheet data.
|
||||
ws, err := f.workSheetReader(opts.Sheet)
|
||||
ws, err := f.workSheetReader(opts.sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vmlID := f.countComments() + 1
|
||||
if opts.FormCtrl {
|
||||
if opts.Type > FormControlRadio {
|
||||
if opts.formCtrl {
|
||||
if opts.Type > FormControlScrollBar {
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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 {
|
||||
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)) == "" {
|
||||
if sheetXMLPath, ok := f.getSheetXMLPath(opts.sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" {
|
||||
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
|
||||
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
|
||||
}
|
||||
|
@ -488,7 +524,7 @@ 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.Paragraph {
|
||||
for _, runs := range opts.FormControl.Paragraph {
|
||||
for _, subStr := range strings.Split(runs.Text, "\n") {
|
||||
opts.rows++
|
||||
if chars := len(subStr); chars > opts.cols {
|
||||
|
@ -496,8 +532,8 @@ func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions {
|
|||
}
|
||||
}
|
||||
}
|
||||
if len(opts.Paragraph) == 0 {
|
||||
opts.rows, opts.cols = 1, len(opts.Text)
|
||||
if len(opts.FormControl.Paragraph) == 0 {
|
||||
opts.rows, opts.cols = 1, len(opts.FormControl.Text)
|
||||
}
|
||||
if opts.Format.ScaleX == 0 {
|
||||
opts.Format.ScaleX = 1
|
||||
|
@ -520,10 +556,10 @@ func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions {
|
|||
// 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})
|
||||
if opts.FormControl.Text != "" {
|
||||
font = append(font, vmlFont{Content: opts.FormControl.Text})
|
||||
}
|
||||
for _, run := range opts.Paragraph {
|
||||
for _, run := range opts.FormControl.Paragraph {
|
||||
fnt := vmlFont{
|
||||
Content: run.Text + "<br></br>\r\n",
|
||||
}
|
||||
|
@ -551,22 +587,10 @@ func formCtrlText(opts *vmlOptions) []vmlFont {
|
|||
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
|
||||
}{
|
||||
var formCtrlPresets = map[FormControlType]formCtrlPreset{
|
||||
FormControlNote: {
|
||||
objectType: "Note",
|
||||
autoFill: "True",
|
||||
filled: "",
|
||||
fillColor: "#FBF6D6",
|
||||
stroked: "",
|
||||
|
@ -586,6 +610,7 @@ var formCtrlPresets = map[FormControlType]struct {
|
|||
},
|
||||
FormControlButton: {
|
||||
objectType: "Button",
|
||||
autoFill: "True",
|
||||
filled: "",
|
||||
fillColor: "buttonFace [67]",
|
||||
stroked: "",
|
||||
|
@ -603,8 +628,9 @@ var formCtrlPresets = map[FormControlType]struct {
|
|||
firstButton: nil,
|
||||
shadow: nil,
|
||||
},
|
||||
FormControlCheckbox: {
|
||||
FormControlCheckBox: {
|
||||
objectType: "Checkbox",
|
||||
autoFill: "True",
|
||||
filled: "f",
|
||||
fillColor: "window [65]",
|
||||
stroked: "f",
|
||||
|
@ -617,8 +643,39 @@ var formCtrlPresets = map[FormControlType]struct {
|
|||
firstButton: nil,
|
||||
shadow: nil,
|
||||
},
|
||||
FormControlRadio: {
|
||||
FormControlGroupBox: {
|
||||
objectType: "GBox",
|
||||
autoFill: "False",
|
||||
filled: "f",
|
||||
fillColor: "",
|
||||
stroked: "f",
|
||||
strokeColor: "windowText [64]",
|
||||
strokeButton: "",
|
||||
fill: nil,
|
||||
textHAlign: "",
|
||||
textVAlign: "",
|
||||
noThreeD: stringPtr(""),
|
||||
firstButton: nil,
|
||||
shadow: nil,
|
||||
},
|
||||
FormControlLabel: {
|
||||
objectType: "Label",
|
||||
autoFill: "False",
|
||||
filled: "f",
|
||||
fillColor: "window [65]",
|
||||
stroked: "f",
|
||||
strokeColor: "windowText [64]",
|
||||
strokeButton: "",
|
||||
fill: nil,
|
||||
textHAlign: "",
|
||||
textVAlign: "",
|
||||
noThreeD: nil,
|
||||
firstButton: nil,
|
||||
shadow: nil,
|
||||
},
|
||||
FormControlOptionButton: {
|
||||
objectType: "Radio",
|
||||
autoFill: "False",
|
||||
filled: "f",
|
||||
fillColor: "window [65]",
|
||||
stroked: "f",
|
||||
|
@ -631,6 +688,116 @@ var formCtrlPresets = map[FormControlType]struct {
|
|||
firstButton: stringPtr(""),
|
||||
shadow: nil,
|
||||
},
|
||||
FormControlScrollBar: {
|
||||
objectType: "Scroll",
|
||||
autoFill: "",
|
||||
filled: "",
|
||||
fillColor: "",
|
||||
stroked: "f",
|
||||
strokeColor: "windowText [64]",
|
||||
strokeButton: "",
|
||||
fill: nil,
|
||||
textHAlign: "",
|
||||
textVAlign: "",
|
||||
noThreeD: nil,
|
||||
firstButton: nil,
|
||||
shadow: nil,
|
||||
},
|
||||
FormControlSpinButton: {
|
||||
objectType: "Spin",
|
||||
autoFill: "False",
|
||||
filled: "",
|
||||
fillColor: "",
|
||||
stroked: "f",
|
||||
strokeColor: "windowText [64]",
|
||||
strokeButton: "",
|
||||
fill: nil,
|
||||
textHAlign: "",
|
||||
textVAlign: "",
|
||||
noThreeD: nil,
|
||||
firstButton: nil,
|
||||
shadow: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// addFormCtrl check and add scroll bar or spinner form control by given options.
|
||||
func (sp *encodeShape) addFormCtrl(opts *vmlOptions) error {
|
||||
if opts.Type != FormControlScrollBar && opts.Type != FormControlSpinButton {
|
||||
return nil
|
||||
}
|
||||
if opts.CurrentVal > MaxFormControlValue ||
|
||||
opts.MinVal > MaxFormControlValue ||
|
||||
opts.MaxVal > MaxFormControlValue ||
|
||||
opts.IncChange > MaxFormControlValue ||
|
||||
opts.PageChange > MaxFormControlValue {
|
||||
return ErrorFormControlValue
|
||||
}
|
||||
if opts.CellLink != "" {
|
||||
if _, _, err := CellNameToCoordinates(opts.CellLink); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sp.ClientData.FmlaLink = opts.CellLink
|
||||
sp.ClientData.Val = opts.CurrentVal
|
||||
sp.ClientData.Min = opts.MinVal
|
||||
sp.ClientData.Max = opts.MaxVal
|
||||
sp.ClientData.Inc = opts.IncChange
|
||||
sp.ClientData.Page = opts.PageChange
|
||||
if opts.Type == FormControlScrollBar {
|
||||
if opts.Horizontally {
|
||||
sp.ClientData.Horiz = stringPtr("")
|
||||
}
|
||||
sp.ClientData.Dx = 15
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addFormCtrlShape returns a VML shape by given preset and options.
|
||||
func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor string, opts *vmlOptions) (*encodeShape, error) {
|
||||
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"},
|
||||
},
|
||||
ClientData: &xClientData{
|
||||
ObjectType: preset.objectType,
|
||||
Anchor: anchor,
|
||||
AutoFill: preset.autoFill,
|
||||
Row: row - 1,
|
||||
Column: col - 1,
|
||||
TextHAlign: preset.textHAlign,
|
||||
TextVAlign: preset.textVAlign,
|
||||
NoThreeD: preset.noThreeD,
|
||||
FirstButton: preset.firstButton,
|
||||
},
|
||||
}
|
||||
if opts.Format.PrintObject != nil && !*opts.Format.PrintObject {
|
||||
sp.ClientData.PrintObject = "False"
|
||||
}
|
||||
if opts.Format.Positioning != "" {
|
||||
idx := inStrSlice(supportedPositioning, opts.Format.Positioning, true)
|
||||
if idx == -1 {
|
||||
return &sp, ErrParameterInvalid
|
||||
}
|
||||
sp.ClientData.MoveWithCells = []*string{stringPtr(""), nil, nil}[idx]
|
||||
sp.ClientData.SizeWithCells = []*string{stringPtr(""), stringPtr(""), nil}[idx]
|
||||
}
|
||||
if opts.FormControl.Type == FormControlNote {
|
||||
sp.ClientData.MoveWithCells = stringPtr("")
|
||||
sp.ClientData.SizeWithCells = stringPtr("")
|
||||
}
|
||||
if !opts.formCtrl {
|
||||
return &sp, nil
|
||||
}
|
||||
sp.TextBox.Div.Font = formCtrlText(opts)
|
||||
sp.ClientData.FmlaMacro = opts.Macro
|
||||
if (opts.Type == FormControlCheckBox || opts.Type == FormControlOptionButton) && opts.Checked {
|
||||
sp.ClientData.Checked = 1
|
||||
}
|
||||
return &sp, sp.addFormCtrl(opts)
|
||||
}
|
||||
|
||||
// addDrawingVML provides a function to create VML drawing XML as
|
||||
|
@ -639,20 +806,18 @@ var formCtrlPresets = map[FormControlType]struct {
|
|||
// LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow,
|
||||
// BottomOffset.
|
||||
func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) error {
|
||||
col, row, err := CellNameToCoordinates(opts.Cell)
|
||||
col, row, err := CellNameToCoordinates(opts.FormControl.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 {
|
||||
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))
|
||||
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{
|
||||
|
@ -699,31 +864,9 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
|
|||
}
|
||||
}
|
||||
}
|
||||
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")
|
||||
sp, err := f.addFormCtrlShape(preset, col, row, anchor, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, _ := xml.Marshal(sp)
|
||||
shape := xlsxShape{
|
||||
|
|
|
@ -48,7 +48,7 @@ type xlsxShape struct {
|
|||
Style string `xml:"style,attr"`
|
||||
Button string `xml:"o:button,attr,omitempty"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
FillColor string `xml:"fillcolor,attr"`
|
||||
FillColor string `xml:"fillcolor,attr,omitempty"`
|
||||
InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
|
||||
Stroked string `xml:"stroked,attr,omitempty"`
|
||||
StrokeColor string `xml:"strokecolor,attr,omitempty"`
|
||||
|
@ -128,18 +128,28 @@ type vmlFont struct {
|
|||
// element.
|
||||
type xClientData struct {
|
||||
ObjectType string `xml:"ObjectType,attr"`
|
||||
MoveWithCells string `xml:"x:MoveWithCells"`
|
||||
SizeWithCells string `xml:"x:SizeWithCells"`
|
||||
MoveWithCells *string `xml:"x:MoveWithCells"`
|
||||
SizeWithCells *string `xml:"x:SizeWithCells"`
|
||||
Anchor string `xml:"x:Anchor"`
|
||||
AutoFill string `xml:"x:AutoFill"`
|
||||
Row int `xml:"x:Row"`
|
||||
Column int `xml:"x:Column"`
|
||||
Locked string `xml:"x:Locked,omitempty"`
|
||||
PrintObject string `xml:"x:PrintObject,omitempty"`
|
||||
AutoFill string `xml:"x:AutoFill,omitempty"`
|
||||
FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
|
||||
TextHAlign string `xml:"x:TextHAlign,omitempty"`
|
||||
TextVAlign string `xml:"x:TextVAlign,omitempty"`
|
||||
Checked *string `xml:"x:Checked,omitempty"`
|
||||
NoThreeD *string `xml:"x:NoThreeD,omitempty"`
|
||||
FirstButton *string `xml:"x:FirstButton,omitempty"`
|
||||
Row int `xml:"x:Row"`
|
||||
Column int `xml:"x:Column"`
|
||||
Checked int `xml:"x:Checked,omitempty"`
|
||||
FmlaLink string `xml:"x:FmlaLink,omitempty"`
|
||||
NoThreeD *string `xml:"x:NoThreeD"`
|
||||
FirstButton *string `xml:"x:FirstButton"`
|
||||
Val uint `xml:"x:Val,omitempty"`
|
||||
Min uint `xml:"x:Min,omitempty"`
|
||||
Max uint `xml:"x:Max,omitempty"`
|
||||
Inc uint `xml:"x:Inc,omitempty"`
|
||||
Page uint `xml:"x:Page,omitempty"`
|
||||
Horiz *string `xml:"x:Horiz"`
|
||||
Dx uint `xml:"x:Dx,omitempty"`
|
||||
}
|
||||
|
||||
// decodeVmlDrawing defines the structure used to parse the file
|
||||
|
@ -165,7 +175,7 @@ type decodeShape struct {
|
|||
Style string `xml:"style,attr"`
|
||||
Button string `xml:"button,attr,omitempty"`
|
||||
Filled string `xml:"filled,attr,omitempty"`
|
||||
FillColor string `xml:"fillcolor,attr"`
|
||||
FillColor string `xml:"fillcolor,attr,omitempty"`
|
||||
InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
|
||||
Stroked string `xml:"stroked,attr,omitempty"`
|
||||
StrokeColor string `xml:"strokecolor,attr,omitempty"`
|
||||
|
@ -195,34 +205,49 @@ type encodeShape struct {
|
|||
ClientData *xClientData `xml:"x:ClientData"`
|
||||
}
|
||||
|
||||
// formCtrlPreset defines the structure used to form control presets.
|
||||
type formCtrlPreset struct {
|
||||
autoFill string
|
||||
fill *vFill
|
||||
fillColor string
|
||||
filled string
|
||||
firstButton *string
|
||||
noThreeD *string
|
||||
objectType string
|
||||
shadow *vShadow
|
||||
strokeButton string
|
||||
strokeColor string
|
||||
stroked string
|
||||
textHAlign string
|
||||
textVAlign string
|
||||
}
|
||||
|
||||
// vmlOptions defines the structure used to internal comments and form controls.
|
||||
type vmlOptions struct {
|
||||
rows int
|
||||
cols int
|
||||
FormCtrl bool
|
||||
Sheet string
|
||||
Author string
|
||||
AuthorID int
|
||||
Cell string
|
||||
Checked bool
|
||||
Text string
|
||||
Macro string
|
||||
Width uint
|
||||
Height uint
|
||||
Paragraph []RichTextRun
|
||||
Type FormControlType
|
||||
Format GraphicOptions
|
||||
rows int
|
||||
cols int
|
||||
formCtrl bool
|
||||
sheet string
|
||||
Comment
|
||||
FormControl
|
||||
}
|
||||
|
||||
// FormControl directly maps the form controls information.
|
||||
type FormControl struct {
|
||||
Cell string
|
||||
Macro string
|
||||
Width uint
|
||||
Height uint
|
||||
Checked bool
|
||||
Text string
|
||||
Paragraph []RichTextRun
|
||||
Type FormControlType
|
||||
Format GraphicOptions
|
||||
Cell string
|
||||
Macro string
|
||||
Width uint
|
||||
Height uint
|
||||
Checked bool
|
||||
CurrentVal uint
|
||||
MinVal uint
|
||||
MaxVal uint
|
||||
IncChange uint
|
||||
PageChange uint
|
||||
Horizontally bool
|
||||
CellLink string
|
||||
Text string
|
||||
Paragraph []RichTextRun
|
||||
Type FormControlType
|
||||
Format GraphicOptions
|
||||
}
|
||||
|
|
84
vml_test.go
84
vml_test.go
|
@ -149,26 +149,20 @@ func TestCountComments(t *testing.T) {
|
|||
func TestAddDrawingVML(t *testing.T) {
|
||||
// Test addDrawingVML with illegal cell reference
|
||||
f := NewFile()
|
||||
assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error())
|
||||
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{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8")
|
||||
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
func TestFormControl(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "D1",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
Cell: "D1", Type: FormControlButton, Macro: "Button1_Click",
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A1",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
Width: 140,
|
||||
Height: 60,
|
||||
Text: "Button 1\r\n",
|
||||
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
|
||||
Width: 140, Height: 60, Text: "Button 1\r\n",
|
||||
Paragraph: []RichTextRun{
|
||||
{
|
||||
Font: &Font{
|
||||
|
@ -182,28 +176,42 @@ func TestFormControl(t *testing.T) {
|
|||
Text: "C1=A1+B1",
|
||||
},
|
||||
},
|
||||
Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"},
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A5",
|
||||
Type: FormControlCheckbox,
|
||||
Text: "Check Box 1",
|
||||
Checked: true,
|
||||
Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1",
|
||||
Checked: true, Format: GraphicOptions{
|
||||
PrintObject: boolPtr(false), Positioning: "oneCell",
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A6",
|
||||
Type: FormControlCheckbox,
|
||||
Text: "Check Box 2",
|
||||
Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2",
|
||||
Format: GraphicOptions{Positioning: "twoCell"},
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A7",
|
||||
Type: FormControlRadio,
|
||||
Text: "Option Button 1",
|
||||
Checked: true,
|
||||
Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true,
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A8",
|
||||
Type: FormControlRadio,
|
||||
Text: "Option Button 2",
|
||||
Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2",
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1",
|
||||
Width: 140, Height: 60,
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140,
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60,
|
||||
CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2",
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20,
|
||||
CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3",
|
||||
}))
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140,
|
||||
CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4",
|
||||
}))
|
||||
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
|
||||
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
|
||||
|
@ -214,23 +222,29 @@ func TestFormControl(t *testing.T) {
|
|||
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "D4",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
Text: "Button 2",
|
||||
Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2",
|
||||
}))
|
||||
// Test add unsupported form control
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A1",
|
||||
Type: 0x37,
|
||||
Macro: "Button1_Click",
|
||||
Cell: "A1", Type: 0x37, Macro: "Button1_Click",
|
||||
}), ErrParameterInvalid)
|
||||
// Test add form control on not exists worksheet
|
||||
assert.Equal(t, f.AddFormControl("SheetN", FormControl{
|
||||
Cell: "A1",
|
||||
Type: FormControlButton,
|
||||
Macro: "Button1_Click",
|
||||
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
|
||||
}), newNoExistSheetError("SheetN"))
|
||||
// Test add form control with invalid positioning types
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "A1", Type: FormControlButton,
|
||||
Format: GraphicOptions{Positioning: "x"},
|
||||
}), ErrParameterInvalid)
|
||||
// Test add spin form control with illegal cell link reference
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "C5", Type: FormControlSpinButton, CellLink: "*",
|
||||
}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
|
||||
// Test add spin form control with invalid scroll value
|
||||
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
|
||||
Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1,
|
||||
}), ErrorFormControlValue)
|
||||
assert.NoError(t, f.Close())
|
||||
// Test delete form control
|
||||
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
|
||||
|
|
|
@ -126,6 +126,7 @@ const (
|
|||
MaxColumnWidth = 255
|
||||
MaxFieldLength = 255
|
||||
MaxFilePathLength = 207
|
||||
MaxFormControlValue = 30000
|
||||
MaxFontFamilyLength = 31
|
||||
MaxFontSize = 409
|
||||
MaxRowHeight = 409
|
||||
|
@ -218,6 +219,9 @@ var supportedDrawingUnderlineTypes = []string{
|
|||
"wavyDbl",
|
||||
}
|
||||
|
||||
// supportedPositioning defined supported positioning types.
|
||||
var supportedPositioning = []string{"absolute", "oneCell", "twoCell"}
|
||||
|
||||
// xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
|
||||
// element specifies non-visual canvas properties. This allows for additional
|
||||
// information that does not affect the appearance of the picture to be stored.
|
||||
|
|
Loading…
Reference in New Issue