This closes #1590, add the Japanese calendar number format support

- The `GetFormControl` now support to get text, rich-text and font format of the form controls
- Update the unit tests and the documentation
This commit is contained in:
xuri 2023-07-31 00:08:10 +08:00
parent a07c8cd0b4
commit 5fe30eb456
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
10 changed files with 346 additions and 97 deletions

15
cell.go
View File

@ -1368,15 +1368,24 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok { if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
} }
return f.applyNumFmt(c, styleSheet, numFmtID, date1904, cellType), err
}
// applyNumFmt provides a function to returns formatted cell value with custom
// number format code.
func (f *File) applyNumFmt(c *xlsxC, styleSheet *xlsxStyleSheet, numFmtID int, date1904 bool, cellType CellType) string {
if styleSheet.NumFmts == nil { if styleSheet.NumFmts == nil {
return c.V, err return c.V
} }
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
if xlsxFmt.NumFmtID == numFmtID { if xlsxFmt.NumFmtID == numFmtID {
return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err if xlsxFmt.FormatCode16 != "" {
return format(c.V, xlsxFmt.FormatCode16, date1904, cellType, f.options)
}
return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options)
} }
} }
return c.V, err return c.V
} }
// prepareCellStyle provides a function to prepare style index of cell in // prepareCellStyle provides a function to prepare style index of cell in

View File

@ -927,6 +927,14 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) {
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
} }
func TestApplyNumFmt(t *testing.T) {
f := NewFile()
assert.Equal(t, "\u4EE4\u548C\u5143年9月1日", f.applyNumFmt(&xlsxC{V: "43709"},
&xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{
{NumFmtID: 164, FormatCode16: "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"},
}}}, 164, false, CellTypeNumber))
}
func TestSharedStringsError(t *testing.T) { func TestSharedStringsError(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
assert.NoError(t, err) assert.NoError(t, err)

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/richardlehane/mscfb v1.0.4 github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9
github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4
golang.org/x/crypto v0.11.0 golang.org/x/crypto v0.11.0
golang.org/x/image v0.5.0 golang.org/x/image v0.5.0
golang.org/x/net v0.12.0 golang.org/x/net v0.12.0

4
go.sum
View File

@ -17,8 +17,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 h1:1SQuQwUorWlROdGAbsAJrMInj02yCUsYFNi/MzTJ6cA= github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 h1:7TXNzvlvE0E/oLDazWm2Xip72G9Su+jRzvziSxwO6Ww=
github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View File

@ -26,6 +26,7 @@ import (
type languageInfo struct { type languageInfo struct {
apFmt string apFmt string
tags []string tags []string
useGannen bool
localMonth func(t time.Time, abbr int) string localMonth func(t time.Time, abbr int) string
} }
@ -795,6 +796,8 @@ var (
"10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]},
"11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
"411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
"800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese},
"JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true},
"12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean},
"412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean},
"7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]},
@ -833,6 +836,18 @@ var (
"35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]},
"435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]},
} }
// japaneseEraYears list the Japanese era name periods.
japaneseEraYears = []time.Time{
time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC),
time.Date(1912, time.June, 30, 0, 0, 0, 0, time.UTC),
time.Date(1926, time.November, 25, 0, 0, 0, 0, time.UTC),
time.Date(1989, time.January, 8, 0, 0, 0, 0, time.UTC),
time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC),
}
// japaneseEraNames list the Japanese era name for the Japanese emperor reign calendar.
japaneseEraNames = []string{"\u660E\u6CBB", "\u5927\u6B63", "\u662D\u548C", "\u5E73\u6210", "\u4EE4\u548C"}
// japaneseEraYear list the Japanese era name symbols.
japaneseEraSymbols = []string{"M", "T", "S", "H", "R"}
// monthNamesBangla list the month names in the Bangla. // monthNamesBangla list the month names in the Bangla.
monthNamesBangla = []string{ monthNamesBangla = []string{
"\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0", "\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0",
@ -1329,8 +1344,10 @@ func (nf *numberFormat) dateTimeHandler() string {
if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode {
return nf.value return nf.value
} }
if !supportedLanguageInfo[nf.localCode].useGannen {
nf.result += nf.currencyString nf.result += nf.currencyString
} }
}
if token.TType == nfp.TokenTypeDateTimes { if token.TType == nfp.TokenTypeDateTimes {
nf.dateTimesHandler(i, token) nf.dateTimesHandler(i, token)
} }
@ -1752,15 +1769,35 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) {
// yearsHandler will be handling years in the date and times types tokens for a // yearsHandler will be handling years in the date and times types tokens for a
// number format expression. // number format expression.
func (nf *numberFormat) yearsHandler(token nfp.Token) { func (nf *numberFormat) yearsHandler(token nfp.Token) {
years := strings.Contains(strings.ToUpper(token.TValue), "Y") if strings.Contains(strings.ToUpper(token.TValue), "Y") {
if years && len(token.TValue) <= 2 { if len(token.TValue) <= 2 {
nf.result += strconv.Itoa(nf.t.Year())[2:] nf.result += strconv.Itoa(nf.t.Year())[2:]
return return
} }
if years && len(token.TValue) > 2 {
nf.result += strconv.Itoa(nf.t.Year()) nf.result += strconv.Itoa(nf.t.Year())
return return
} }
if strings.Contains(strings.ToUpper(token.TValue), "G") {
for i := len(japaneseEraYears) - 1; i > 0; i-- {
if y := japaneseEraYears[i]; nf.t.After(y) {
switch len(token.TValue) {
case 1:
nf.result += japaneseEraSymbols[i]
case 2:
nf.result += japaneseEraNames[i][:3]
default:
nf.result += japaneseEraNames[i]
}
year := nf.t.Year() - y.Year() + 1
if year == 1 && len(token.TValue) > 1 && supportedLanguageInfo[nf.localCode].useGannen {
nf.result += "\u5143"
break
}
nf.result += strconv.Itoa(year)
break
}
}
}
} }
// daysHandler will be handling days in the date and times types tokens for a // daysHandler will be handling days in the date and times types tokens for a

View File

@ -420,6 +420,18 @@ func TestNumFmt(t *testing.T) {
{"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"},
{"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"},
{"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"},
{"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
{"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"},
{"43709", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"},
{"43709", "[$-ja-JP-x-gannen,80]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
{"43709", "[$-ja-JP-x-gannen,80]gge\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"},
{"43709", "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"},
{"43466.189571759256", "[$-411]ge\"年\"m\"月\"d\"日\";@", "H31年1月1日"},
{"43466.189571759256", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u5E7331年1月1日"},
{"43466.189571759256", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u5E73\u621031年1月1日"},
{"44896.18957170139", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R4年12月1日"},
{"44896.18957170139", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE44年12月1日"},
{"44896.18957170139", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C4年12月1日"},
{"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
{"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
{"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},

110
vml.go
View File

@ -420,11 +420,17 @@ func (f *File) DeleteFormControl(sheet, cell string) error {
for i, sp := range vml.Shape { for i, sp := range vml.Shape {
var shapeVal decodeShapeVal var shapeVal decodeShapeVal
if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil && if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil &&
shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 { shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Anchor != "" {
leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor)
if err != nil {
return err
}
if leftCol == col-1 && topRow == row-1 {
vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
break break
} }
} }
}
f.VMLDrawing[drawingVML] = vml f.VMLDrawing[drawingVML] = vml
return err return err
} }
@ -454,7 +460,7 @@ func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) {
c, ok := f.Pkg.Load(path) c, ok := f.Pkg.Load(path)
if ok && c != nil { if ok && c != nil {
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))). if err := f.xmlNewDecoder(bytes.NewReader(bytesReplace(namespaceStrictToTransitional(c.([]byte)), []byte("<br>\r\n"), []byte("<br></br>\r\n"), -1))).
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
return nil, err return nil, err
} }
@ -574,6 +580,9 @@ func formCtrlText(opts *vmlOptions) []vmlFont {
if run.Font.Underline == "single" { if run.Font.Underline == "single" {
fnt.Content = "<u>" + fnt.Content + "</u>" fnt.Content = "<u>" + fnt.Content + "</u>"
} }
if run.Font.Underline == "double" {
fnt.Content = "<u class=\"font1\">" + fnt.Content + "</u>"
}
if run.Font.Italic { if run.Font.Italic {
fnt.Content = "<i>" + fnt.Content + "</i>" fnt.Content = "<i>" + fnt.Content + "</i>"
} }
@ -765,8 +774,8 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri
ObjectType: preset.objectType, ObjectType: preset.objectType,
Anchor: anchor, Anchor: anchor,
AutoFill: preset.autoFill, AutoFill: preset.autoFill,
Row: row - 1, Row: intPtr(row - 1),
Column: col - 1, Column: intPtr(col - 1),
TextHAlign: preset.textHAlign, TextHAlign: preset.textHAlign,
TextVAlign: preset.textVAlign, TextVAlign: preset.textVAlign,
NoThreeD: preset.noThreeD, NoThreeD: preset.noThreeD,
@ -885,8 +894,8 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
} }
// GetFormControls retrieves all form controls in a worksheet by a given // GetFormControls retrieves all form controls in a worksheet by a given
// worksheet name. Note that, this function does not support getting the width, // worksheet name. Note that, this function does not support getting the width
// height, text, rich text, and format currently. // and height of the form controls currently.
func (f *File) GetFormControls(sheet string) ([]FormControl, error) { func (f *File) GetFormControls(sheet string) ([]FormControl, error) {
var formControls []FormControl var formControls []FormControl
// Read sheet data // Read sheet data
@ -949,9 +958,18 @@ func extractFormControl(clientData string) (FormControl, error) {
return formControl, err return formControl, err
} }
for formCtrlType, preset := range formCtrlPresets { for formCtrlType, preset := range formCtrlPresets {
if shapeVal.ClientData.ObjectType == preset.objectType { if shapeVal.ClientData.ObjectType == preset.objectType && shapeVal.ClientData.Anchor != "" {
formControl.Paragraph = extractVMLFont(shapeVal.TextBox.Div.Font)
if len(formControl.Paragraph) > 0 && formControl.Paragraph[0].Font == nil {
formControl.Text = formControl.Paragraph[0].Text
formControl.Paragraph = formControl.Paragraph[1:]
}
formControl.Type = formCtrlType formControl.Type = formCtrlType
if formControl.Cell, err = CoordinatesToCellName(shapeVal.ClientData.Column+1, shapeVal.ClientData.Row+1); err != nil { col, row, err := extractAnchorCell(shapeVal.ClientData.Anchor)
if err != nil {
return formControl, err
}
if formControl.Cell, err = CoordinatesToCellName(col+1, row+1); err != nil {
return formControl, err return formControl, err
} }
formControl.Macro = shapeVal.ClientData.FmlaMacro formControl.Macro = shapeVal.ClientData.FmlaMacro
@ -967,3 +985,79 @@ func extractFormControl(clientData string) (FormControl, error) {
} }
return formControl, err return formControl, err
} }
// extractAnchorCell extract left-top cell coordinates from given VML anchor
// comma-separated list values.
func extractAnchorCell(anchor string) (int, int, error) {
var (
leftCol, topRow int
err error
pos = strings.Split(anchor, ",")
)
if len(pos) != 8 {
return leftCol, topRow, ErrParameterInvalid
}
leftCol, err = strconv.Atoi(strings.TrimSpace(pos[0]))
if err != nil {
return leftCol, topRow, ErrColumnNumber
}
topRow, err = strconv.Atoi(strings.TrimSpace(pos[2]))
return leftCol, topRow, err
}
// extractVMLFont extract rich-text and font format from given VML font element.
func extractVMLFont(font []decodeVMLFont) []RichTextRun {
var runs []RichTextRun
extractU := func(u *decodeVMLFontU, run *RichTextRun) {
if u == nil {
return
}
run.Text += u.Val
if run.Font == nil {
run.Font = &Font{}
}
run.Font.Underline = "single"
if u.Class == "font1" {
run.Font.Underline = "double"
}
}
extractI := func(i *decodeVMLFontI, run *RichTextRun) {
if i == nil {
return
}
extractU(i.U, run)
run.Text += i.Val
if run.Font == nil {
run.Font = &Font{}
}
run.Font.Italic = true
}
extractB := func(b *decodeVMLFontB, run *RichTextRun) {
if b == nil {
return
}
extractI(b.I, run)
run.Text += b.Val
if run.Font == nil {
run.Font = &Font{}
}
run.Font.Bold = true
}
for _, fnt := range font {
var run RichTextRun
extractB(fnt.B, &run)
extractI(fnt.I, &run)
extractU(fnt.U, &run)
run.Text += fnt.Val
if fnt.Face != "" || fnt.Size > 0 || fnt.Color != "" {
if run.Font == nil {
run.Font = &Font{}
}
run.Font.Family = fnt.Face
run.Font.Size = float64(fnt.Size / 20)
run.Font.Color = fnt.Color
}
runs = append(runs, run)
}
return runs
}

View File

@ -137,8 +137,8 @@ type xClientData struct {
FmlaMacro string `xml:"x:FmlaMacro,omitempty"` FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
TextHAlign string `xml:"x:TextHAlign,omitempty"` TextHAlign string `xml:"x:TextHAlign,omitempty"`
TextVAlign string `xml:"x:TextVAlign,omitempty"` TextVAlign string `xml:"x:TextVAlign,omitempty"`
Row int `xml:"x:Row"` Row *int `xml:"x:Row"`
Column int `xml:"x:Column"` Column *int `xml:"x:Column"`
Checked int `xml:"x:Checked,omitempty"` Checked int `xml:"x:Checked,omitempty"`
FmlaLink string `xml:"x:FmlaLink,omitempty"` FmlaLink string `xml:"x:FmlaLink,omitempty"`
NoThreeD *string `xml:"x:NoThreeD"` NoThreeD *string `xml:"x:NoThreeD"`
@ -185,16 +185,59 @@ type decodeShape struct {
// decodeShapeVal defines the structure used to parse the sub-element of the // decodeShapeVal defines the structure used to parse the sub-element of the
// shape in the file xl/drawings/vmlDrawing%d.vml. // shape in the file xl/drawings/vmlDrawing%d.vml.
type decodeShapeVal struct { type decodeShapeVal struct {
TextBox decodeVMLTextBox `xml:"textbox"`
ClientData decodeVMLClientData `xml:"ClientData"` ClientData decodeVMLClientData `xml:"ClientData"`
} }
// decodeVMLFontU defines the structure used to parse the u element in the VML.
type decodeVMLFontU struct {
Class string `xml:"class,attr"`
Val string `xml:",chardata"`
}
// decodeVMLFontI defines the structure used to parse the i element in the VML.
type decodeVMLFontI struct {
U *decodeVMLFontU `xml:"u"`
Val string `xml:",chardata"`
}
// decodeVMLFontB defines the structure used to parse the b element in the VML.
type decodeVMLFontB struct {
I *decodeVMLFontI `xml:"i"`
U *decodeVMLFontU `xml:"u"`
Val string `xml:",chardata"`
}
// decodeVMLFont defines the structure used to parse the font element in the VML.
type decodeVMLFont struct {
Face string `xml:"face,attr,omitempty"`
Size uint `xml:"size,attr,omitempty"`
Color string `xml:"color,attr,omitempty"`
B *decodeVMLFontB `xml:"b"`
I *decodeVMLFontI `xml:"i"`
U *decodeVMLFontU `xml:"u"`
Val string `xml:",chardata"`
}
// decodeVMLDiv defines the structure used to parse the div element in the VML.
type decodeVMLDiv struct {
Font []decodeVMLFont `xml:"font"`
}
// decodeVMLTextBox defines the structure used to parse the v:textbox element in
// the file xl/drawings/vmlDrawing%d.vml.
type decodeVMLTextBox struct {
Div decodeVMLDiv `xml:"div"`
}
// decodeVMLClientData defines the structure used to parse the x:ClientData // decodeVMLClientData defines the structure used to parse the x:ClientData
// element in the file xl/drawings/vmlDrawing%d.vml. // element in the file xl/drawings/vmlDrawing%d.vml.
type decodeVMLClientData struct { type decodeVMLClientData struct {
ObjectType string `xml:"ObjectType,attr"` ObjectType string `xml:"ObjectType,attr"`
Anchor string
FmlaMacro string FmlaMacro string
Column int Column *int
Row int Row *int
Checked int Checked int
FmlaLink string FmlaLink string
Val uint Val uint

View File

@ -164,7 +164,7 @@ func TestFormControl(t *testing.T) {
}, },
{ {
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
Width: 140, Height: 60, Text: "Button 1\r\n", Width: 140, Height: 60, Text: "Button 1\n",
Paragraph: []RichTextRun{ Paragraph: []RichTextRun{
{ {
Font: &Font{ Font: &Font{
@ -234,6 +234,8 @@ func TestFormControl(t *testing.T) {
assert.Equal(t, formCtrl.IncChange, result[i].IncChange) assert.Equal(t, formCtrl.IncChange, result[i].IncChange)
assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally) assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally)
assert.Equal(t, formCtrl.CellLink, result[i].CellLink) assert.Equal(t, formCtrl.CellLink, result[i].CellLink)
assert.Equal(t, formCtrl.Text, result[i].Text)
assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph))
} }
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
@ -249,7 +251,8 @@ func TestFormControl(t *testing.T) {
assert.Len(t, result, 11) assert.Len(t, result, 11)
// Test add from control to a worksheet which already contains form controls // Test add from control to a worksheet which already contains form controls
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2", Cell: "D4", Type: FormControlButton, Macro: "Button1_Click",
Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}},
})) }))
// Test get from controls after add form controls // Test get from controls after add form controls
result, err = f.GetFormControls("Sheet1") result, err = f.GetFormControls("Sheet1")
@ -297,6 +300,11 @@ func TestFormControl(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8") assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete form controls with invalid shape anchor
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>0</x:Anchor></x:ClientData>"}},
}
assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1"))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
// Test delete form control on a worksheet without form control // Test delete form control on a worksheet without form control
f = NewFile() f = NewFile()
@ -320,9 +328,39 @@ func TestFormControl(t *testing.T) {
formControls, err = f.GetFormControls("Sheet1") formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, formControls, 0) assert.Len(t, formControls, 0)
// Test get form controls with bold font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><b>Text</b></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.True(t, formControls[0].Paragraph[0].Font.Bold)
// Test get form controls with italic font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
// Test get form controls with font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font face=\"Calibri\" size=\"280\" color=\"#777777\">Text</font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family)
assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size)
assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color)
// Test get form controls with italic font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
// Test get form controls with invalid column number // Test get form controls with invalid column number
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Column>%d</x:Column></x:ClientData>", MaxColumns)}}, Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
} }
formControls, err = f.GetFormControls("Sheet1") formControls, err = f.GetFormControls("Sheet1")
assert.Equal(t, err, ErrColumnNumber) assert.Equal(t, err, ErrColumnNumber)
@ -343,11 +381,18 @@ func TestFormControl(t *testing.T) {
assert.Len(t, formControls, 0) assert.Len(t, formControls, 0)
// Test get form controls with invalid column number // Test get form controls with invalid column number
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Column>%d</x:Column></x:ClientData>", MaxColumns)}}, Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
} }
formControls, err = f.GetFormControls("Sheet1") formControls, err = f.GetFormControls("Sheet1")
assert.Equal(t, err, ErrColumnNumber) assert.Equal(t, err, ErrColumnNumber)
assert.Len(t, formControls, 0) assert.Len(t, formControls, 0)
// Test get form controls with invalid shape anchor
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>x,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.Equal(t, ErrColumnNumber, err)
assert.Len(t, formControls, 0)
// Test get form controls with comment (Note) shape type // Test get form controls with comment (Note) shape type
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}}, Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},

View File

@ -302,6 +302,7 @@ type xlsxNumFmts struct {
type xlsxNumFmt struct { type xlsxNumFmt struct {
NumFmtID int `xml:"numFmtId,attr"` NumFmtID int `xml:"numFmtId,attr"`
FormatCode string `xml:"formatCode,attr,omitempty"` FormatCode string `xml:"formatCode,attr,omitempty"`
FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"`
} }
// xlsxStyleColors directly maps the colors' element. Color information // xlsxStyleColors directly maps the colors' element. Color information