Compare commits

...

6 Commits

Author SHA1 Message Date
Ilia Mirkin 30d3561d0e
Rename SetLegacyDrawingHF to AddHeaderFooterImage (#2023)
- Add new exported HeaderFooterImagePositionType enumeration
- An error will be return if the image format is unsupported
- Rename exported data type HeaderFooterGraphics to HeaderFooterImageOptions
- Support add and update exist header and footer images
- Changes the VML data ID to sheet ID
- Update unit tests
- Update dependencies modules
2024-11-09 18:36:42 +08:00
Ilia Mirkin d2be5cdf8e
The SetPageLayout function support set page order of page layout (#2022)
- Add new fields PageOrder for PageLayoutOptions
- Add a new exported error variable ErrPageSetupAdjustTo
- An error will be return if the option value of the SetPageLayout function is invalid
- Updated unit tests
2024-11-08 16:59:07 +08:00
Ilia Mirkin b7375bc6d4
This closes #1395, add new function SetLegacyDrawingHF support to set graphics in a header/footer (#2018) 2024-11-04 10:39:55 +08:00
xuri 0d5d1c53b2
This closes #2015, fix a v2.9.0 regression bug introduced by commit 7715c1462a
- Fix corrupted workbook generated by open the workbook generated by stream writer
- Update unit tests
2024-10-25 08:52:59 +08:00
xuri af190c7fdc
This closes #2014, fix redundant none type pattern fill generated
- Simplify unit tests
2024-10-23 22:07:25 +08:00
wushiling50 d1937a0cde
This closes #1885, add new CultureNameJaJP, CultureNameKoKR and CultureNameZhTW enumeration values (#1895)
- Support apply number format for the Japanese calendar years, the Korean Danki calendar and the Republic of China year
- Update unit tests

Signed-off-by: wushiling50 <2531010934@qq.com>
2024-10-21 09:36:04 +08:00
20 changed files with 572 additions and 71 deletions

View File

@ -13629,7 +13629,9 @@ func (fn *formulaFuncs) DBCS(argsList *list.List) formulaArg {
if arg.Type == ArgError {
return arg
}
if fn.f.options.CultureInfo == CultureNameZhCN {
if fn.f.options.CultureInfo == CultureNameJaJP ||
fn.f.options.CultureInfo == CultureNameZhCN ||
fn.f.options.CultureInfo == CultureNameZhTW {
var chars []string
for _, r := range arg.Value() {
code := r
@ -16378,7 +16380,10 @@ func (fn *formulaFuncs) DOLLAR(argsList *list.List) formulaArg {
symbol := map[CultureName]string{
CultureNameUnknown: "$",
CultureNameEnUS: "$",
CultureNameJaJP: "¥",
CultureNameKoKR: "\u20a9",
CultureNameZhCN: "¥",
CultureNameZhTW: "NT$",
}[fn.f.options.CultureInfo]
numFmtCode := fmt.Sprintf("%s#,##0%s%s;(%s#,##0%s%s)",
symbol, dot, strings.Repeat("0", decimals), symbol, dot, strings.Repeat("0", decimals))

View File

@ -88,6 +88,9 @@ var (
// ErrOutlineLevel defined the error message on receive an invalid outline
// level number.
ErrOutlineLevel = errors.New("invalid outline level")
// ErrPageSetupAdjustTo defined the error message for receiving a page setup
// adjust to value exceeds limit.
ErrPageSetupAdjustTo = errors.New("adjust to value must be between 10 and 400")
// ErrParameterInvalid defined the error message on receive the invalid
// parameter.
ErrParameterInvalid = errors.New("parameter is invalid")
@ -249,6 +252,12 @@ func newInvalidNameError(name string) error {
return fmt.Errorf("invalid name %q, the name should be starts with a letter or underscore, can not include a space or character, and can not conflict with an existing name in the workbook", name)
}
// newInvalidPageLayoutValueError defined the error message on receiving the invalid
// page layout options value.
func newInvalidPageLayoutValueError(name, value, msg string) error {
return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, msg)
}
// newInvalidRowNumberError defined the error message on receiving the invalid
// row number.
func newInvalidRowNumberError(row int) error {

View File

@ -870,11 +870,17 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
}
func TestSetCellStyleLangNumberFormat(t *testing.T) {
rawCellValues := [][]string{{"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}
rawCellValues := make([][]string, 42)
for i := 0; i < 42; i++ {
rawCellValues[i] = []string{"45162"}
}
for lang, expected := range map[CultureName][][]string{
CultureNameUnknown: rawCellValues,
CultureNameEnUS: {{"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"45162"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0時00分"}, {"0時00分00秒"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"08-24-56"}, {"4356년 08월 24일"}, {"0시 00분"}, {"0시 00분 00초"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0时00分"}, {"0时00分00秒"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"00時00分"}, {"00時00分00秒"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang})
assert.NoError(t, err)
@ -886,7 +892,10 @@ func TestSetCellStyleLangNumberFormat(t *testing.T) {
// Test apply language number format code with date and time pattern
for lang, expected := range map[CultureName][][]string{
CultureNameEnUS: {{"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"45162"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"4356-8-24"}, {"4356년 08월 24일"}, {"00:00:00"}, {"00:00:00"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"})
assert.NoError(t, err)

6
go.mod
View File

@ -8,10 +8,10 @@ require (
github.com/stretchr/testify v1.8.4
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7
golang.org/x/crypto v0.28.0
golang.org/x/crypto v0.29.0
golang.org/x/image v0.18.0
golang.org/x/net v0.30.0
golang.org/x/text v0.19.0
golang.org/x/net v0.31.0
golang.org/x/text v0.20.0
)
require (

12
go.sum
View File

@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

9
lib.go
View File

@ -652,11 +652,16 @@ func getRootElement(d *xml.Decoder) []xml.Attr {
case xml.StartElement:
tokenIdx++
if tokenIdx == 1 {
var ns bool
for i := 0; i < len(startElement.Attr); i++ {
if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value {
startElement.Attr[i] = NameSpaceSpreadSheet
if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value &&
startElement.Attr[i].Name == NameSpaceSpreadSheet.Name {
ns = true
}
}
if !ns {
startElement.Attr = append(startElement.Attr, NameSpaceSpreadSheet)
}
return startElement.Attr
}
}

View File

@ -289,6 +289,10 @@ func TestBytesReplace(t *testing.T) {
func TestGetRootElement(t *testing.T) {
assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0)
// Test get workbook root element which all workbook XML namespace has prefix
f := NewFile()
d := f.xmlNewDecoder(bytes.NewReader([]byte(`<x:workbook xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"></x:workbook>`)))
assert.Len(t, getRootElement(d), 3)
}
func TestSetIgnorableNameSpace(t *testing.T) {

172
numfmt.go
View File

@ -57,7 +57,10 @@ type CultureName byte
const (
CultureNameUnknown CultureName = iota
CultureNameEnUS
CultureNameJaJP
CultureNameKoKR
CultureNameZhCN
CultureNameZhTW
)
var (
@ -791,7 +794,7 @@ var (
31748: {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
3076: {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
5124: {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
1028: {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
1028: {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2, useGannen: true},
9: {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
4096: {tags: []string{
"aa", "aa-DJ", "aa-ER", "aa-ER", "aa-NA", "agq", "agq-CM", "ak", "ak-GH", "sq-ML",
@ -1168,6 +1171,10 @@ var (
"JA-JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr},
"JA-JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true},
}
// republicOfChinaYear defined start time of the Republic of China
republicOfChinaYear = time.Date(1912, time.January, 1, 0, 0, 0, 0, time.UTC)
// republicOfChinaEraName defined the Republic of China era name for the Republic of China calendar.
republicOfChinaEraName = []string{"\u4e2d\u83ef\u6c11\u570b", "\u6c11\u570b", "\u524d"}
// japaneseEraYears list the Japanese era name periods.
japaneseEraYears = []time.Time{
time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC),
@ -4634,6 +4641,24 @@ var (
return r.Replace(s)
},
}
// langNumFmtFunc defines functions to apply language number format code.
langNumFmtFunc = map[CultureName]func(f *File, numFmtID int) string{
CultureNameEnUS: func(f *File, numFmtID int) string {
return f.langNumFmtFuncEnUS(numFmtID)
},
CultureNameJaJP: func(f *File, numFmtID int) string {
return f.langNumFmtFuncJaJP(numFmtID)
},
CultureNameKoKR: func(f *File, numFmtID int) string {
return f.langNumFmtFuncKoKR(numFmtID)
},
CultureNameZhCN: func(f *File, numFmtID int) string {
return f.langNumFmtFuncZhCN(numFmtID)
},
CultureNameZhTW: func(f *File, numFmtID int) string {
return f.langNumFmtFuncZhTW(numFmtID)
},
}
)
// getSupportedLanguageInfo returns language infomation by giving language code.
@ -4694,6 +4719,54 @@ func (f *File) langNumFmtFuncEnUS(numFmtID int) string {
return ""
}
// langNumFmtFuncJaJP returns number format code by given date and time pattern
// for country code ja-jp.
func (f *File) langNumFmtFuncJaJP(numFmtID int) string {
if numFmtID == 30 && f.options.ShortDatePattern != "" {
return f.options.ShortDatePattern
}
if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
return f.options.LongTimePattern
}
return langNumFmt["ja-jp"][numFmtID]
}
// langNumFmtFuncKoKR returns number format code by given date and time pattern
// for country code ko-kr.
func (f *File) langNumFmtFuncKoKR(numFmtID int) string {
if numFmtID == 30 && f.options.ShortDatePattern != "" {
return f.options.ShortDatePattern
}
if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
return f.options.LongTimePattern
}
return langNumFmt["ko-kr"][numFmtID]
}
// langNumFmtFuncZhCN returns number format code by given date and time pattern
// for country code zh-cn.
func (f *File) langNumFmtFuncZhCN(numFmtID int) string {
if numFmtID == 30 && f.options.ShortDatePattern != "" {
return f.options.ShortDatePattern
}
if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
return f.options.LongTimePattern
}
return langNumFmt["zh-cn"][numFmtID]
}
// langNumFmtFuncZhTW returns number format code by given date and time pattern
// for country code zh-tw.
func (f *File) langNumFmtFuncZhTW(numFmtID int) string {
if numFmtID == 30 && f.options.ShortDatePattern != "" {
return f.options.ShortDatePattern
}
if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
return f.options.LongTimePattern
}
return langNumFmt["zh-tw"][numFmtID]
}
// checkDateTimePattern check and validate date and time options field value.
func (f *File) checkDateTimePattern() error {
for _, pattern := range []string{f.options.LongDatePattern, f.options.LongTimePattern, f.options.ShortDatePattern} {
@ -4770,18 +4843,6 @@ func (f *File) extractNumFmtDecimal(fmtCode string) int {
return -1
}
// langNumFmtFuncZhCN returns number format code by given date and time pattern
// for country code zh-cn.
func (f *File) langNumFmtFuncZhCN(numFmtID int) string {
if numFmtID == 30 && f.options.ShortDatePattern != "" {
return f.options.ShortDatePattern
}
if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
return f.options.LongTimePattern
}
return langNumFmt["zh-cn"][numFmtID]
}
// getBuiltInNumFmtCode convert number format index to number format code with
// specified locale and language.
func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) {
@ -4789,11 +4850,8 @@ func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) {
return fmtCode, true
}
if isLangNumFmt(numFmtID) {
if f.options.CultureInfo == CultureNameEnUS {
return f.langNumFmtFuncEnUS(numFmtID), true
}
if f.options.CultureInfo == CultureNameZhCN {
return f.langNumFmtFuncZhCN(numFmtID), true
if fn, ok := langNumFmtFunc[f.options.CultureInfo]; ok {
return fn(f, numFmtID), true
}
}
return "", false
@ -6912,23 +6970,13 @@ func eraYear(t time.Time) (int, int) {
return i, year
}
// yearsHandler will be handling years in the date and times types tokens for a
// number format expression.
func (nf *numberFormat) yearsHandler(token nfp.Token) {
if strings.Contains(strings.ToUpper(token.TValue), "Y") {
if len(token.TValue) <= 2 {
nf.result += strconv.Itoa(nf.t.Year())[2:]
return
}
nf.result += strconv.Itoa(nf.t.Year())
return
}
// japaneseYearHandler handling the Japanease calendar years.
func (nf *numberFormat) japaneseYearHandler(token nfp.Token, langInfo languageInfo) {
if strings.Contains(strings.ToUpper(token.TValue), "G") {
i, year := eraYear(nf.t)
if year == -1 {
return
}
langInfo, _ := getSupportedLanguageInfo(nf.localCode)
nf.useGannen = langInfo.useGannen
switch len(token.TValue) {
case 1:
@ -6939,7 +6987,6 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) {
default:
nf.result += japaneseEraNames[i]
}
return
}
if strings.Contains(strings.ToUpper(token.TValue), "E") {
_, year := eraYear(nf.t)
@ -6961,6 +7008,69 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) {
}
}
// republicOfChinaYearHandler handling the Republic of China calendar years.
func (nf *numberFormat) republicOfChinaYearHandler(token nfp.Token, langInfo languageInfo) {
if strings.Contains(strings.ToUpper(token.TValue), "G") {
year := nf.t.Year() - republicOfChinaYear.Year() + 1
if year == 1 {
nf.useGannen = langInfo.useGannen
}
var name string
if name = republicOfChinaEraName[0]; len(token.TValue) < 3 {
name = republicOfChinaEraName[1]
}
if year < 0 {
name += republicOfChinaEraName[2]
}
nf.result += name
}
if strings.Contains(strings.ToUpper(token.TValue), "E") {
year := nf.t.Year() - republicOfChinaYear.Year() + 1
if year < 0 {
year = republicOfChinaYear.Year() - nf.t.Year()
}
if year == 1 && nf.useGannen {
nf.result += "\u5143"
return
}
if len(token.TValue) == 1 && !nf.useGannen {
nf.result += strconv.Itoa(year)
}
}
}
// yearsHandler will be handling years in the date and times types tokens for a
// number format expression.
func (nf *numberFormat) yearsHandler(token nfp.Token) {
langInfo, _ := getSupportedLanguageInfo(nf.localCode)
if strings.Contains(strings.ToUpper(token.TValue), "Y") {
year := nf.t.Year()
if nf.opts != nil && nf.opts.CultureInfo == CultureNameKoKR {
year += 2333
}
if len(token.TValue) <= 2 {
nf.result += strconv.Itoa(year)[2:]
return
}
nf.result += strconv.Itoa(year)
return
}
if inStrSlice(langInfo.tags, "zh-TW", false) != -1 ||
nf.opts != nil && nf.opts.CultureInfo == CultureNameZhTW {
nf.republicOfChinaYearHandler(token, langInfo)
return
}
if inStrSlice(langInfo.tags, "ja-JP", false) != -1 ||
nf.opts != nil && nf.opts.CultureInfo == CultureNameJaJP {
nf.japaneseYearHandler(token, langInfo)
return
}
if strings.Contains(strings.ToUpper(token.TValue), "E") {
nf.result += strconv.Itoa(nf.t.Year())
return
}
}
// daysHandler will be handling days in the date and times types tokens for a
// number format expression.
func (nf *numberFormat) daysHandler(token nfp.Token) {

View File

@ -289,6 +289,20 @@ func TestNumFmt(t *testing.T) {
{"43543.503206018519", "[$-401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
{"43543.503206018519", "[$-401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
{"43543.503206018519", "[$-401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
{"43466.189571759256", "[$-404]g\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5e74\u0031\u6708\u0031\u65e5"},
{"43466.189571759256", "[$-404]e\"年\"m\"月\"d\"日\";@", "\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
{"43466.189571759256", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
{"43466.189571759256", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
{"43466.189571759256", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
{"43466.189571759256", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
{"4385.5083333333332", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
{"4385.5083333333332", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
{"4385.5083333333332", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
{"4385.5083333333332", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
{"123", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
{"123", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
{"123", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
{"123", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
{"44562.189571759256", "[$-1010401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
{"44562.189571759256", "[$-1010401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
{"44562.189571759256", "[$-1010401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
@ -2722,6 +2736,9 @@ func TestNumFmt(t *testing.T) {
{"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM aaa", "\u0e15 01 2022 4:32 AM \u0E2A."},
{"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM ddd", "\u0e1e 01 2022 4:32 AM \u0E2D."},
{"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM dddd", "\u0e18 01 2022 4:32 AM \u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35"},
{"100", "g\"年\"m\"月\"d\"日\";@", "年4月9日"},
{"100", "e\"年\"m\"月\"d\"日\";@", "1900年4月9日"},
{"100", "ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"},
{"100", "[$-411]ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"},
{"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
{"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"},

View File

@ -295,6 +295,16 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
}
}
// addSheetLegacyDrawingHF provides a function to add legacy drawing
// header/footer element to xl/worksheets/sheet%d.xml by given
// worksheet name and relationship index.
func (f *File) addSheetLegacyDrawingHF(sheet string, rID int) {
ws, _ := f.workSheetReader(sheet)
ws.LegacyDrawingHF = &xlsxLegacyDrawingHF{
RID: "rId" + strconv.Itoa(rID),
}
}
// addSheetDrawing provides a function to add drawing element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetDrawing(sheet string, rID int) {

View File

@ -516,7 +516,7 @@ func TestDeleteWorkbookPivotCache(t *testing.T) {
f := NewFile()
// Test delete workbook pivot table cache with unsupported workbook charset
f.WorkBook = nil
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
// Test delete workbook pivot table cache with unsupported workbook relationships charset

View File

@ -1239,7 +1239,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
// |
// &F | Current workbook's file name
// |
// &G | Drawing object as background (Not support currently)
// &G | Drawing object as background (Use AddHeaderFooterImage)
// |
// &H | Shadow text format
// |
@ -1609,8 +1609,7 @@ func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error {
if opts == nil {
return err
}
ws.setPageSetUp(opts)
return err
return ws.setPageSetUp(opts)
}
// newPageSetUp initialize page setup settings for the worksheet if which not
@ -1622,12 +1621,15 @@ func (ws *xlsxWorksheet) newPageSetUp() {
}
// setPageSetUp set page setup settings for the worksheet by given options.
func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error {
if opts.Size != nil {
ws.newPageSetUp()
ws.PageSetUp.PaperSize = opts.Size
}
if opts.Orientation != nil && (*opts.Orientation == "portrait" || *opts.Orientation == "landscape") {
if opts.Orientation != nil {
if inStrSlice(supportedPageOrientation, *opts.Orientation, true) == -1 {
return newInvalidPageLayoutValueError("Orientation", *opts.Orientation, strings.Join(supportedPageOrientation, ", "))
}
ws.newPageSetUp()
ws.PageSetUp.Orientation = *opts.Orientation
}
@ -1636,7 +1638,10 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber))
ws.PageSetUp.UseFirstPageNumber = true
}
if opts.AdjustTo != nil && 10 <= *opts.AdjustTo && *opts.AdjustTo <= 400 {
if opts.AdjustTo != nil {
if *opts.AdjustTo < 10 || 400 < *opts.AdjustTo {
return ErrPageSetupAdjustTo
}
ws.newPageSetUp()
ws.PageSetUp.Scale = int(*opts.AdjustTo)
}
@ -1652,13 +1657,21 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
ws.newPageSetUp()
ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite
}
if opts.PageOrder != nil {
if inStrSlice(supportedPageOrder, *opts.PageOrder, true) == -1 {
return newInvalidPageLayoutValueError("PageOrder", *opts.PageOrder, strings.Join(supportedPageOrder, ", "))
}
ws.newPageSetUp()
ws.PageSetUp.PageOrder = *opts.PageOrder
}
return nil
}
// GetPageLayout provides a function to gets worksheet page layout.
func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
opts := PageLayoutOptions{
Size: intPtr(0),
Orientation: stringPtr("portrait"),
Orientation: stringPtr(supportedPageOrientation[0]),
FirstPageNumber: uintPtr(1),
AdjustTo: uintPtr(100),
}
@ -1686,6 +1699,9 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
opts.FitToWidth = ws.PageSetUp.FitToWidth
}
opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite)
if ws.PageSetUp.PageOrder != "" {
opts.PageOrder = stringPtr(ws.PageSetUp.PageOrder)
}
}
return opts, err
}

View File

@ -210,6 +210,7 @@ func TestSetPageLayout(t *testing.T) {
FitToHeight: intPtr(2),
FitToWidth: intPtr(2),
BlackAndWhite: boolPtr(true),
PageOrder: stringPtr("overThenDown"),
}
assert.NoError(t, f.SetPageLayout("Sheet1", &expected))
opts, err := f.GetPageLayout("Sheet1")
@ -219,6 +220,16 @@ func TestSetPageLayout(t *testing.T) {
assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist")
// Test set page layout with invalid sheet name
assert.EqualError(t, f.SetPageLayout("Sheet:1", nil), ErrSheetNameInvalid.Error())
// Test set page layout with invalid parameters
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
AdjustTo: uintPtr(5),
}), "adjust to value must be between 10 and 400")
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
Orientation: stringPtr("x"),
}), "invalid Orientation value \"x\", acceptable value should be one of portrait, landscape")
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
PageOrder: stringPtr("x"),
}), "invalid PageOrder value \"x\", acceptable value should be one of overThenDown, downThenOver")
}
func TestGetPageLayout(t *testing.T) {
@ -580,7 +591,7 @@ func TestMoveSheet(t *testing.T) {
// Test move sheet with unsupported workbook charset
f.WorkBook = nil
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.MoveSheet("Sheet2", "Sheet1"), "XML syntax error on line 1: invalid UTF-8")
}

View File

@ -597,7 +597,7 @@ func TestAddWorkbookSlicerCache(t *testing.T) {
// Test add a workbook slicer cache with unsupported charset workbook
f := NewFile()
f.WorkBook = nil
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.addWorkbookSlicerCache(1, ExtURISlicerCachesX15), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}

View File

@ -2051,11 +2051,12 @@ func newFills(style *Style, fg bool) *xlsxFill {
if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 {
break
}
if len(style.Fill.Color) < 1 {
break
}
var pattern xlsxPatternFill
pattern.PatternType = styleFillPatterns[style.Fill.Pattern]
if len(style.Fill.Color) < 1 {
fill.PatternFill = &pattern
break
}
if fg {
if pattern.FgColor == nil {
pattern.FgColor = new(xlsxColor)

View File

@ -498,6 +498,12 @@ var supportedDrawingUnderlineTypes = []string{
// supportedPositioning defined supported positioning types.
var supportedPositioning = []string{"absolute", "oneCell", "twoCell"}
// supportedPageOrientation defined supported page setup page orientation.
var supportedPageOrientation = []string{"portrait", "landscape"}
// supportedPageOrder defined supported page setup page order.
var supportedPageOrder = []string{"overThenDown", "downThenOver"}
// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix.
var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm.Criteria", "_xlnm._FilterDatabase", "_xlnm.Extract", "_xlnm.Consolidate_Area", "_xlnm.Database", "_xlnm.Sheet_Title"}

155
vml.go
View File

@ -36,6 +36,16 @@ const (
FormControlScrollBar
)
// HeaderFooterImagePositionType is the type of header and footer image position.
type HeaderFooterImagePositionType byte
// Worksheet header and footer image position types enumeration.
const (
HeaderFooterImagePositionLeft HeaderFooterImagePositionType = iota
HeaderFooterImagePositionCenter
HeaderFooterImagePositionRight
)
// GetComments retrieves all comments in a worksheet by given worksheet name.
func (f *File) GetComments(sheet string) ([]Comment, error) {
var comments []Comment
@ -519,6 +529,7 @@ func (f *File) addVMLObject(opts vmlOptions) error {
}
vmlID = f.countVMLDrawing() + 1
}
sheetID := f.getSheetID(opts.sheet)
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetXMLPath, _ := f.getSheetXMLPath(opts.sheet)
@ -534,7 +545,7 @@ func (f *File) addVMLObject(opts vmlOptions) error {
f.addSheetNameSpace(opts.sheet, SourceRelationship)
f.addSheetLegacyDrawing(opts.sheet, rID)
}
if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil {
if err = f.addDrawingVML(sheetID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil {
return err
}
if !opts.formCtrl {
@ -823,7 +834,7 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri
// 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 {
func (f *File) addDrawingVML(sheetID int, drawingVML string, opts *vmlOptions) error {
col, row, err := CellNameToCoordinates(opts.FormControl.Cell)
if err != nil {
return err
@ -843,7 +854,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
XMLNSx: "urn:schemas-microsoft-com:office:excel",
XMLNSmv: "http://macVmlSchemaUri",
ShapeLayout: &xlsxShapeLayout{
Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: dataID},
Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID},
},
ShapeType: &xlsxShapeType{
ID: fmt.Sprintf("_x0000_t%d", vmlID),
@ -1070,3 +1081,141 @@ func extractVMLFont(font []decodeVMLFont) []RichTextRun {
}
return runs
}
// AddHeaderFooterImage provides a mechanism to set the graphics that can be
// referenced in the header and footer definitions via &G, file base name,
// extension name and file bytes, supported image types: EMF, EMZ, GIF, JPEG,
// JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ.
//
// The extension should be provided with a "." in front, e.g. ".png".
// The width and height should have units in them, e.g. "100pt".
func (f *File) AddHeaderFooterImage(sheet string, opts *HeaderFooterImageOptions) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
ext, ok := supportedImageTypes[strings.ToLower(opts.Extension)]
if !ok {
return ErrImgExt
}
sheetID := f.getSheetID(sheet)
vmlID := f.countVMLDrawing() + 1
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
if ws.LegacyDrawingHF != nil {
// The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawingHF.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(sheet, SourceRelationship)
f.addSheetLegacyDrawingHF(sheet, rID)
}
shapeID := map[HeaderFooterImagePositionType]string{
HeaderFooterImagePositionLeft: "L",
HeaderFooterImagePositionCenter: "C",
HeaderFooterImagePositionRight: "R",
}[opts.Position] +
map[bool]string{false: "H", true: "F"}[opts.IsFooter] +
map[bool]string{false: "", true: "FIRST"}[opts.FirstPage]
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",
ShapeLayout: &xlsxShapeLayout{
Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID},
},
ShapeType: &xlsxShapeType{
ID: "_x0000_t75",
CoordSize: "21600,21600",
Spt: 75,
PreferRelative: "t",
Path: "m@4@5l@4@11@9@11@9@5xe",
Filled: "f",
Stroked: "f",
Stroke: &xlsxStroke{JoinStyle: "miter"},
VFormulas: &vFormulas{
Formulas: []vFormula{
{Equation: "if lineDrawn pixelLineWidth 0"},
{Equation: "sum @0 1 0"},
{Equation: "sum 0 0 @1"},
{Equation: "prod @2 1 2"},
{Equation: "prod @3 21600 pixelWidth"},
{Equation: "prod @3 21600 pixelHeight"},
{Equation: "sum @0 0 1"},
{Equation: "prod @6 1 2"},
{Equation: "prod @7 21600 pixelWidth"},
{Equation: "sum @8 21600 0"},
{Equation: "prod @7 21600 pixelHeight"},
{Equation: "sum @10 21600 0"},
},
},
VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"},
Lock: &oLock{Ext: "edit", AspectRatio: "t"},
},
}
// 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.PreferRelative = d.ShapeType.PreferRelative
vml.ShapeType.Path = d.ShapeType.Path
vml.ShapeType.Filled = d.ShapeType.Filled
vml.ShapeType.Stroked = d.ShapeType.Stroked
for _, v := range d.Shape {
s := xlsxShape{
ID: v.ID,
SpID: v.SpID,
Type: v.Type,
Style: v.Style,
Val: v.Val,
}
vml.Shape = append(vml.Shape, s)
}
}
}
for idx, shape := range vml.Shape {
if shape.ID == shapeID {
vml.Shape = append(vml.Shape[:idx], vml.Shape[idx+1:]...)
}
}
style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", opts.Width, opts.Height)
drawingVMLRels := "xl/drawings/_rels/vmlDrawing" + strconv.Itoa(vmlID) + ".vml.rels"
mediaStr := ".." + strings.TrimPrefix(f.addMedia(opts.File, ext), "xl")
imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "")
shape := xlsxShape{
ID: shapeID,
SpID: "_x0000_s1025",
Type: "#_x0000_t75",
Style: style,
}
sp, _ := xml.Marshal(encodeShape{
ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)},
Lock: &oLock{Ext: "edit", Rotation: "t"},
})
shape.Val = string(sp[13 : len(sp)-14])
vml.Shape = append(vml.Shape, shape)
f.VMLDrawing[drawingVML] = vml
if err := f.setContentTypePartImageExtensions(); err != nil {
return err
}
return f.setContentTypePartVMLExtensions()
}

View File

@ -20,7 +20,7 @@ type vmlDrawing struct {
XMLNSv string `xml:"xmlns:v,attr"`
XMLNSo string `xml:"xmlns:o,attr"`
XMLNSx string `xml:"xmlns:x,attr"`
XMLNSmv string `xml:"xmlns:mv,attr"`
XMLNSmv string `xml:"xmlns:mv,attr,omitempty"`
ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"`
ShapeType *xlsxShapeType `xml:"v:shapetype"`
Shape []xlsxShape `xml:"v:shape"`
@ -44,6 +44,7 @@ type xlsxIDmap struct {
type xlsxShape struct {
XMLName xml.Name `xml:"v:shape"`
ID string `xml:"id,attr"`
SpID string `xml:"o:spid,attr,omitempty"`
Type string `xml:"type,attr"`
Style string `xml:"style,attr"`
Button string `xml:"o:button,attr,omitempty"`
@ -57,12 +58,17 @@ type xlsxShape struct {
// xlsxShapeType directly maps the shapetype element.
type xlsxShapeType struct {
ID string `xml:"id,attr"`
CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"o:spt,attr"`
Path string `xml:"path,attr"`
Stroke *xlsxStroke `xml:"v:stroke"`
VPath *vPath `xml:"v:path"`
ID string `xml:"id,attr"`
CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"o:spt,attr"`
PreferRelative string `xml:"o:preferrelative,attr,omitempty"`
Path string `xml:"path,attr"`
Filled string `xml:"filled,attr,omitempty"`
Stroked string `xml:"stroked,attr,omitempty"`
Stroke *xlsxStroke `xml:"v:stroke"`
VFormulas *vFormulas `xml:"v:formulas"`
VPath *vPath `xml:"v:path"`
Lock *oLock `xml:"o:lock"`
}
// xlsxStroke directly maps the stroke element.
@ -72,10 +78,28 @@ type xlsxStroke struct {
// vPath directly maps the v:path element.
type vPath struct {
ExtrusionOK string `xml:"o:extrusionok,attr,omitempty"`
GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"`
ConnectType string `xml:"o:connecttype,attr"`
}
// oLock directly maps the o:lock element.
type oLock struct {
Ext string `xml:"v:ext,attr"`
Rotation string `xml:"rotation,attr,omitempty"`
AspectRatio string `xml:"aspectratio,attr,omitempty"`
}
// vFormulas directly maps to the v:formulas element
type vFormulas struct {
Formulas []vFormula `xml:"v:f"`
}
// vFormula directly maps to the v:f element
type vFormula struct {
Equation string `xml:"eqn,attr"`
}
// vFill directly maps the v:fill element. This element must be defined within a
// Shape element.
type vFill struct {
@ -106,6 +130,13 @@ type vTextBox struct {
Div *xlsxDiv `xml:"div"`
}
// vImageData directly maps the v:imagedata element. This element must be
// defined within a Shape element.
type vImageData struct {
RelID string `xml:"o:relid,attr"`
Title string `xml:"o:title,attr,omitempty"`
}
// xlsxDiv directly maps the div element.
type xlsxDiv struct {
Style string `xml:"style,attr"`
@ -162,15 +193,19 @@ type decodeVmlDrawing struct {
// decodeShapeType defines the structure used to parse the shapetype element in
// the file xl/drawings/vmlDrawing%d.vml.
type decodeShapeType struct {
ID string `xml:"id,attr"`
CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"spt,attr"`
Path string `xml:"path,attr"`
ID string `xml:"id,attr"`
CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"spt,attr"`
PreferRelative string `xml:"preferrelative,attr,omitempty"`
Path string `xml:"path,attr"`
Filled string `xml:"filled,attr,omitempty"`
Stroked string `xml:"stroked,attr,omitempty"`
}
// decodeShape defines the structure used to parse the particular shape element.
type decodeShape struct {
ID string `xml:"id,attr"`
SpID string `xml:"spid,attr,omitempty"`
Type string `xml:"type,attr"`
Style string `xml:"style,attr"`
Button string `xml:"button,attr,omitempty"`
@ -254,7 +289,9 @@ type encodeShape struct {
Shadow *vShadow `xml:"v:shadow"`
Path *vPath `xml:"v:path"`
TextBox *vTextBox `xml:"v:textbox"`
ImageData *vImageData `xml:"v:imagedata"`
ClientData *xClientData `xml:"x:ClientData"`
Lock *oLock `xml:"o:lock"`
}
// formCtrlPreset defines the structure used to form control presets.
@ -301,3 +338,15 @@ type FormControl struct {
Type FormControlType
Format GraphicOptions
}
// HeaderFooterImageOptions defines the settings for an image to be accessible
// from the worksheet header and footer options.
type HeaderFooterImageOptions struct {
Position HeaderFooterImagePositionType
File []byte
IsFooter bool
FirstPage bool
Extension string
Width string
Height string
}

View File

@ -412,3 +412,100 @@ func TestExtractFormControl(t *testing.T) {
_, err := extractFormControl(string(MacintoshCyrillicCharset))
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestAddHeaderFooterImage(t *testing.T) {
f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx")
headerFooterOptions := HeaderFooterOptions{
DifferentFirst: true,
OddHeader: "&L&GExcelize&C&G&R&G",
OddFooter: "&L&GExcelize&C&G&R&G",
FirstHeader: "&L&GExcelize&C&G&R&G",
FirstFooter: "&L&GExcelize&C&G&R&G",
}
assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions))
assert.NoError(t, f.SetSheetView(sheet, -1, &ViewOptions{View: stringPtr("pageLayout")}))
images := map[string][]byte{
".wmf": nil, ".tif": nil, ".png": nil,
".jpg": nil, ".gif": nil, ".emz": nil, ".emf": nil,
}
for ext := range images {
img, err := os.ReadFile(filepath.Join("test", "images", "excel"+ext))
assert.NoError(t, err)
images[ext] = img
}
for _, opt := range []struct {
position HeaderFooterImagePositionType
file []byte
isFooter bool
firstPage bool
ext string
}{
{position: HeaderFooterImagePositionLeft, file: images[".tif"], firstPage: true, ext: ".tif"},
{position: HeaderFooterImagePositionCenter, file: images[".gif"], firstPage: true, ext: ".gif"},
{position: HeaderFooterImagePositionRight, file: images[".png"], firstPage: true, ext: ".png"},
{position: HeaderFooterImagePositionLeft, file: images[".emf"], isFooter: true, firstPage: true, ext: ".emf"},
{position: HeaderFooterImagePositionCenter, file: images[".wmf"], isFooter: true, firstPage: true, ext: ".wmf"},
{position: HeaderFooterImagePositionRight, file: images[".emz"], isFooter: true, firstPage: true, ext: ".emz"},
{position: HeaderFooterImagePositionLeft, file: images[".png"], ext: ".png"},
{position: HeaderFooterImagePositionCenter, file: images[".png"], ext: ".png"},
{position: HeaderFooterImagePositionRight, file: images[".png"], ext: ".png"},
{position: HeaderFooterImagePositionLeft, file: images[".tif"], isFooter: true, ext: ".tif"},
{position: HeaderFooterImagePositionCenter, file: images[".tif"], isFooter: true, ext: ".tif"},
{position: HeaderFooterImagePositionRight, file: images[".tif"], isFooter: true, ext: ".tif"},
} {
assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Position: opt.position,
File: opt.file,
IsFooter: opt.isFooter,
FirstPage: opt.firstPage,
Extension: opt.ext,
Width: "50pt",
Height: "32pt",
}))
}
assert.NoError(t, f.SetCellValue(sheet, "A1", "Example"))
// Test add header footer image with not exist sheet
assert.EqualError(t, f.AddHeaderFooterImage("SheetN", nil), "sheet SheetN does not exist")
// Test add header footer image with unsupported file type
assert.Equal(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Extension: "jpg",
}), ErrImgExt)
assert.NoError(t, f.SaveAs(wb))
assert.NoError(t, f.Close())
// Test change already exist header image with the different image
f, err := OpenFile(wb)
assert.NoError(t, err)
assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
File: images[".jpg"],
FirstPage: true,
Extension: ".jpg",
Width: "50pt",
Height: "32pt",
}))
assert.NoError(t, f.Save())
assert.NoError(t, f.Close())
// Test add header image with unsupported charset VML drawing
f, err = OpenFile(wb)
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
File: images[".jpg"],
Extension: ".jpg",
Width: "50pt",
Height: "32pt",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test set legacy drawing header/footer with unsupported charset content types
f = NewFile()
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Extension: ".png",
File: images[".png"],
Width: "50pt",
Height: "32pt",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}

View File

@ -1006,6 +1006,9 @@ type PageLayoutOptions struct {
FitToWidth *int
// BlackAndWhite specified print black and white.
BlackAndWhite *bool
// PageOrder specifies the ordering of multiple pages. Values
// accepted: overThenDown, downThenOver
PageOrder *string
}
// ViewOptions directly maps the settings of sheet view.