Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
Eng Zer Jun | c93618856a | |
xuri | 5f446f25f0 | |
Ilia Mirkin | 30d3561d0e | |
Ilia Mirkin | d2be5cdf8e | |
Ilia Mirkin | b7375bc6d4 | |
xuri | 0d5d1c53b2 | |
xuri | af190c7fdc | |
wushiling50 | d1937a0cde |
7
calc.go
7
calc.go
|
@ -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))
|
||||
|
|
20
chart.go
20
chart.go
|
@ -850,6 +850,7 @@ func (opts *Chart) parseTitle() {
|
|||
// ReverseOrder
|
||||
// Maximum
|
||||
// Minimum
|
||||
// Alignment
|
||||
// Font
|
||||
// NumFmt
|
||||
// Title
|
||||
|
@ -864,6 +865,7 @@ func (opts *Chart) parseTitle() {
|
|||
// ReverseOrder
|
||||
// Maximum
|
||||
// Minimum
|
||||
// Alignment
|
||||
// Font
|
||||
// LogBase
|
||||
// NumFmt
|
||||
|
@ -896,6 +898,24 @@ func (opts *Chart) parseTitle() {
|
|||
// Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property
|
||||
// is optional. The default value is auto.
|
||||
//
|
||||
// Alignment: Specifies that the alignment of the horizontal and vertical axis.
|
||||
// The properties of font that can be set are:
|
||||
//
|
||||
// TextRotation
|
||||
// Vertical
|
||||
//
|
||||
// The value of 'TextRotation' that can be set from -90 to 90:
|
||||
//
|
||||
// The value of 'Vertical' that can be set are:
|
||||
//
|
||||
// horz
|
||||
// vert
|
||||
// vert270
|
||||
// wordArtVert
|
||||
// eaVert
|
||||
// mongolianVert
|
||||
// wordArtVertRtl
|
||||
//
|
||||
// Font: Specifies that the font of the horizontal and vertical axis. The
|
||||
// properties of font that can be set are:
|
||||
//
|
||||
|
|
|
@ -216,9 +216,9 @@ func TestAddChart(t *testing.T) {
|
|||
}{
|
||||
{sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, Color: "000000"}, Title: []RichTextRun{{Text: "Primary Horizontal Axis Title"}}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}, Title: []RichTextRun{{Text: "Primary Vertical Axis Title", Font: &Font{Color: "777777", Bold: true, Italic: true, Size: 12}}}}}},
|
||||
{sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}},
|
||||
{sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert", TextRotation: 0}}}},
|
||||
{sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}},
|
||||
{sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
|
@ -233,7 +233,7 @@ func TestAddChart(t *testing.T) {
|
|||
{sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert270", TextRotation: 0}}}},
|
||||
{sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}},
|
||||
{sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Scatter Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
|
||||
{sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Doughnut Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}},
|
||||
|
|
8
col.go
8
col.go
|
@ -18,7 +18,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/tiendc/go-deepcopy"
|
||||
)
|
||||
|
||||
// Define the default cell size and EMU unit of measurement.
|
||||
|
@ -533,7 +533,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error
|
|||
func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol {
|
||||
var fc []xlsxCol
|
||||
for i := col.Min; i <= col.Max; i++ {
|
||||
c := deepcopy.Copy(col).(xlsxCol)
|
||||
var c xlsxCol
|
||||
deepcopy.Copy(&c, col)
|
||||
c.Min, c.Max = i, i
|
||||
fc = append(fc, c)
|
||||
}
|
||||
|
@ -551,7 +552,8 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
|
|||
fc[idx] = replacer(fc[idx], column)
|
||||
continue
|
||||
}
|
||||
c := deepcopy.Copy(column).(xlsxCol)
|
||||
var c xlsxCol
|
||||
deepcopy.Copy(&c, column)
|
||||
c.Min, c.Max = i, i
|
||||
fc = append(fc, c)
|
||||
}
|
||||
|
|
|
@ -1256,6 +1256,12 @@ func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr {
|
|||
}
|
||||
if opts != nil {
|
||||
drawChartFont(&opts.Font, &cTxPr.P.PPr.DefRPr)
|
||||
if -90 <= opts.Alignment.TextRotation && opts.Alignment.TextRotation <= 90 {
|
||||
cTxPr.BodyPr.Rot = opts.Alignment.TextRotation * 60000
|
||||
}
|
||||
if idx := inStrSlice(supportedDrawingTextVerticalType, opts.Alignment.Vertical, true); idx != -1 {
|
||||
cTxPr.BodyPr.Vert = supportedDrawingTextVerticalType[idx]
|
||||
}
|
||||
}
|
||||
return cTxPr
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
10
go.mod
10
go.mod
|
@ -3,15 +3,15 @@ module github.com/xuri/excelize/v2
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/richardlehane/mscfb v1.0.4
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tiendc/go-deepcopy v1.1.0
|
||||
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 (
|
||||
|
|
20
go.sum
20
go.sum
|
@ -1,7 +1,5 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
|
@ -9,20 +7,22 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7
|
|||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tiendc/go-deepcopy v1.1.0 h1:rBHhm5vg7WYnGLwktbQouodWjBXDoStOL4S7v/K8S4A=
|
||||
github.com/tiendc/go-deepcopy v1.1.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||
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
9
lib.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
172
numfmt.go
|
@ -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) {
|
||||
|
|
|
@ -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日"},
|
||||
|
|
10
picture.go
10
picture.go
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
10
rows.go
10
rows.go
|
@ -20,7 +20,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/tiendc/go-deepcopy"
|
||||
)
|
||||
|
||||
// duplicateHelperFunc defines functions to duplicate helper.
|
||||
|
@ -653,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
|
|||
|
||||
for i, r := range ws.SheetData.Row {
|
||||
if r.R == row {
|
||||
rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow)
|
||||
deepcopy.Copy(&rowCopy, ws.SheetData.Row[i])
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
|
@ -729,7 +729,8 @@ func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row,
|
|||
}
|
||||
}
|
||||
if len(SQRef) > 0 {
|
||||
cfCopy := deepcopy.Copy(*cf).(xlsxConditionalFormatting)
|
||||
var cfCopy xlsxConditionalFormatting
|
||||
deepcopy.Copy(&cfCopy, *cf)
|
||||
cfCopy.SQRef = strings.Join(SQRef, " ")
|
||||
cfs = append(cfs, &cfCopy)
|
||||
}
|
||||
|
@ -759,7 +760,8 @@ func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, ro
|
|||
}
|
||||
}
|
||||
if len(SQRef) > 0 {
|
||||
dvCopy := deepcopy.Copy(*dv).(xlsxDataValidation)
|
||||
var dvCopy xlsxDataValidation
|
||||
deepcopy.Copy(&dvCopy, *dv)
|
||||
dvCopy.Sqref = strings.Join(SQRef, " ")
|
||||
dvs = append(dvs, &dvCopy)
|
||||
}
|
||||
|
|
38
sheet.go
38
sheet.go
|
@ -27,7 +27,7 @@ import (
|
|||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/tiendc/go-deepcopy"
|
||||
)
|
||||
|
||||
// NewSheet provides the function to create a new sheet by given a worksheet
|
||||
|
@ -124,7 +124,8 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) {
|
|||
Width: ws.Cols.Col[i-1].Width,
|
||||
}, ws.Cols.Col[i]); i++ {
|
||||
}
|
||||
column := deepcopy.Copy(ws.Cols.Col[left]).(xlsxCol)
|
||||
var column xlsxCol
|
||||
deepcopy.Copy(&column, ws.Cols.Col[left])
|
||||
if left < i-1 {
|
||||
column.Max = ws.Cols.Col[i-1].Min
|
||||
}
|
||||
|
@ -750,7 +751,8 @@ func (f *File) copySheet(from, to int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet)
|
||||
worksheet := &xlsxWorksheet{}
|
||||
deepcopy.Copy(worksheet, sheet)
|
||||
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
|
||||
sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml"
|
||||
if len(worksheet.SheetViews.SheetView) > 0 {
|
||||
|
@ -1239,7 +1241,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 +1611,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 +1623,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 +1640,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 +1659,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 +1701,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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
10
templates.go
10
templates.go
|
@ -495,9 +495,19 @@ var supportedDrawingUnderlineTypes = []string{
|
|||
"wavyDbl",
|
||||
}
|
||||
|
||||
// supportedDrawingTextVerticalType defined supported text vertical types in
|
||||
// drawing markup language.
|
||||
var supportedDrawingTextVerticalType = []string{"horz", "vert", "vert270", "wordArtVert", "eaVert", "mongolianVert", "wordArtVertRtl"}
|
||||
|
||||
// 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"}
|
||||
|
||||
|
|
154
vml.go
154
vml.go
|
@ -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,140 @@ 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, 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()
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
@ -60,9 +61,14 @@ type xlsxShapeType struct {
|
|||
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"`
|
||||
|
@ -165,12 +196,16 @@ type decodeShapeType struct {
|
|||
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
|
||||
}
|
||||
|
|
97
vml_test.go
97
vml_test.go
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -540,6 +540,7 @@ type ChartAxis struct {
|
|||
Secondary bool
|
||||
Maximum *float64
|
||||
Minimum *float64
|
||||
Alignment Alignment
|
||||
Font Font
|
||||
LogBase float64
|
||||
NumFmt ChartNumFmt
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue