From db224523987ea14f10fbe638e10f375f6741631c Mon Sep 17 00:00:00 2001 From: cnmlgbgithub Date: Thu, 24 Aug 2023 23:51:07 +0800 Subject: [PATCH] This closes #314, closes #1520 and closes #1521 (#1574) - Add new function GetStyle support for get style definition --- lib.go | 24 ++++ lib_test.go | 6 + numfmt.go | 2 +- styles.go | 305 ++++++++++++++++++++++++++++++++++++++++++++++++- styles_test.go | 102 +++++++++++++++++ xmlStyles.go | 11 +- 6 files changed, 446 insertions(+), 4 deletions(-) diff --git a/lib.go b/lib.go index 379880755b..25fdc3b16d 100644 --- a/lib.go +++ b/lib.go @@ -430,6 +430,30 @@ func float64Ptr(f float64) *float64 { return &f } // stringPtr returns a pointer to a string with the given value. func stringPtr(s string) *string { return &s } +// Value extracts string data type text from a attribute value. +func (attr *attrValString) Value() string { + if attr != nil && attr.Val != nil { + return *attr.Val + } + return "" +} + +// Value extracts boolean data type value from a attribute value. +func (attr *attrValBool) Value() bool { + if attr != nil && attr.Val != nil { + return *attr.Val + } + return false +} + +// Value extracts float64 data type numeric from a attribute value. +func (attr *attrValFloat) Value() float64 { + if attr != nil && attr.Val != nil { + return *attr.Val + } + return 0 +} + // MarshalXML convert the boolean data type to literal values 0 or 1 on // serialization. func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error { diff --git a/lib_test.go b/lib_test.go index fe8d6a8ffb..b41f5f1c48 100644 --- a/lib_test.go +++ b/lib_test.go @@ -238,6 +238,12 @@ func TestInStrSlice(t *testing.T) { assert.EqualValues(t, -1, inStrSlice([]string{}, "", true)) } +func TestAttrValue(t *testing.T) { + assert.Empty(t, (&attrValString{}).Value()) + assert.False(t, (&attrValBool{}).Value()) + assert.Zero(t, (&attrValFloat{}).Value()) +} + func TestBoolValMarshal(t *testing.T) { bold := true node := &xlsxFont{B: &attrValBool{Val: &bold}} diff --git a/numfmt.go b/numfmt.go index 4037f827c2..b63e74a1c8 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4692,7 +4692,7 @@ func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) { if fmtCode, ok := builtInNumFmt[numFmtID]; ok { return fmtCode, true } - if (27 <= numFmtID && numFmtID <= 36) || (50 <= numFmtID && numFmtID <= 81) { + if isLangNumFmt(numFmtID) { if f.options.CultureInfo == CultureNameEnUS { return f.langNumFmtFuncEnUS(numFmtID), true } diff --git a/styles.go b/styles.go index c1587aea0e..05d49511e9 100644 --- a/styles.go +++ b/styles.go @@ -1032,6 +1032,303 @@ func (f *File) NewStyle(style *Style) (int, error) { return setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection) } +var ( + // styleBorders list all types of the cell border style. + styleBorders = []string{ + "none", + "thin", + "medium", + "dashed", + "dotted", + "thick", + "double", + "hair", + "mediumDashed", + "dashDot", + "mediumDashDot", + "dashDotDot", + "mediumDashDotDot", + "slantDashDot", + } + // styleBorderTypes list all types of the cell border. + styleBorderTypes = []string{ + "left", "right", "top", "bottom", "diagonalUp", "diagonalDown", + } + // styleFillPatterns list all types of the cell fill style. + styleFillPatterns = []string{ + "none", + "solid", + "mediumGray", + "darkGray", + "lightGray", + "darkHorizontal", + "darkVertical", + "darkDown", + "darkUp", + "darkGrid", + "darkTrellis", + "lightHorizontal", + "lightVertical", + "lightDown", + "lightUp", + "lightGrid", + "lightTrellis", + "gray125", + "gray0625", + } + // styleFillVariants list all preset variants of the fill style. + styleFillVariants = []xlsxGradientFill{ + {Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 270, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 180, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 255, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 315, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}}, + {Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path"}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Left: 1, Right: 1}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Top: 1}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Left: 1, Right: 1, Top: 1}, + {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 0.5, Left: 0.5, Right: 0.5, Top: 0.5}, + } +) + +// getThemeColor provides a function to convert theme color or index color to +// RGB color. +func (f *File) getThemeColor(clr *xlsxColor) string { + var RGB string + if clr == nil || f.Theme == nil { + return RGB + } + if clrScheme := f.Theme.ThemeElements.ClrScheme; clr.Theme != nil { + if val, ok := map[int]*string{ + 0: &clrScheme.Lt1.SysClr.LastClr, + 1: &clrScheme.Dk1.SysClr.LastClr, + 2: clrScheme.Lt2.SrgbClr.Val, + 3: clrScheme.Dk2.SrgbClr.Val, + 4: clrScheme.Accent1.SrgbClr.Val, + 5: clrScheme.Accent2.SrgbClr.Val, + 6: clrScheme.Accent3.SrgbClr.Val, + 7: clrScheme.Accent4.SrgbClr.Val, + 8: clrScheme.Accent5.SrgbClr.Val, + 9: clrScheme.Accent6.SrgbClr.Val, + }[*clr.Theme]; ok && val != nil { + return strings.TrimPrefix(ThemeColor(*val, clr.Tint), "FF") + } + } + if len(clr.RGB) == 6 { + return clr.RGB + } + if len(clr.RGB) == 8 { + return strings.TrimPrefix(clr.RGB, "FF") + } + if f.Styles.Colors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) { + return strings.TrimPrefix(ThemeColor(strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[clr.Indexed].RGB, "FF"), clr.Tint), "FF") + } + if clr.Indexed < len(IndexedColorMapping) { + return strings.TrimPrefix(ThemeColor(IndexedColorMapping[clr.Indexed], clr.Tint), "FF") + } + return RGB +} + +// extractBorders provides a function to extract borders styles settings by +// given border styles definition. +func (f *File) extractBorders(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyBorder != nil && *xf.ApplyBorder && + xf.BorderID != nil && s.Borders != nil && + *xf.BorderID < len(s.Borders.Border) { + if bdr := s.Borders.Border[*xf.BorderID]; bdr != nil { + + var borders []Border + extractBorder := func(lineType string, line xlsxLine) { + if line.Style != "" { + borders = append(borders, Border{ + Type: lineType, + Color: f.getThemeColor(line.Color), + Style: inStrSlice(styleBorders, line.Style, false), + }) + } + } + for i, line := range []xlsxLine{ + bdr.Left, bdr.Right, bdr.Top, bdr.Bottom, bdr.Diagonal, bdr.Diagonal, + } { + if i < 4 { + extractBorder(styleBorderTypes[i], line) + } + if i == 4 && bdr.DiagonalUp { + extractBorder(styleBorderTypes[i], line) + } + if i == 5 && bdr.DiagonalDown { + extractBorder(styleBorderTypes[i], line) + } + } + style.Border = borders + } + } +} + +// extractFills provides a function to extract fill styles settings by +// given fill styles definition. +func (f *File) extractFills(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if fl := s.Fills.Fill[*xf.FillID]; fl != nil { + var fill Fill + if fl.GradientFill != nil { + fill.Type = "gradient" + for shading, variants := range styleFillVariants { + if fl.GradientFill.Bottom == variants.Bottom && + fl.GradientFill.Degree == variants.Degree && + fl.GradientFill.Left == variants.Left && + fl.GradientFill.Right == variants.Right && + fl.GradientFill.Top == variants.Top && + fl.GradientFill.Type == variants.Type { + fill.Shading = shading + break + } + } + for _, stop := range fl.GradientFill.Stop { + fill.Color = append(fill.Color, f.getThemeColor(&stop.Color)) + } + } + if fl.PatternFill != nil { + fill.Type = "pattern" + fill.Pattern = inStrSlice(styleFillPatterns, fl.PatternFill.PatternType, false) + if fl.PatternFill.FgColor != nil { + fill.Color = []string{f.getThemeColor(fl.PatternFill.FgColor)} + } + } + style.Fill = fill + } +} + +// extractFont provides a function to extract font styles settings by given +// font styles definition. +func (f *File) extractFont(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyFont != nil && *xf.ApplyFont && + xf.FontID != nil && s.Fonts != nil && + *xf.FontID < len(s.Fonts.Font) { + if fnt := s.Fonts.Font[*xf.FontID]; fnt != nil { + var font Font + if fnt.B != nil { + font.Bold = fnt.B.Value() + } + if fnt.I != nil { + font.Italic = fnt.I.Value() + } + if fnt.U != nil { + font.Underline = fnt.U.Value() + } + if fnt.Name != nil { + font.Family = fnt.Name.Value() + } + if fnt.Sz != nil { + font.Size = fnt.Sz.Value() + } + if fnt.Strike != nil { + font.Strike = fnt.Strike.Value() + } + if fnt.Color != nil { + font.Color = strings.TrimPrefix(fnt.Color.RGB, "FF") + font.ColorIndexed = fnt.Color.Indexed + font.ColorTheme = fnt.Color.Theme + font.ColorTint = fnt.Color.Tint + } + style.Font = &font + } + } +} + +// extractNumFmt provides a function to extract number format by given styles +// definition. +func (f *File) extractNumFmt(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.NumFmtID != nil { + numFmtID := *xf.NumFmtID + if _, ok := builtInNumFmt[numFmtID]; ok || isLangNumFmt(numFmtID) { + style.NumFmt = numFmtID + return + } + if s.NumFmts != nil { + for _, numFmt := range s.NumFmts.NumFmt { + style.CustomNumFmt = &numFmt.FormatCode + if strings.Contains(numFmt.FormatCode, ";[Red]") { + style.NegRed = true + } + for numFmtID, fmtCode := range currencyNumFmt { + if style.NegRed { + fmtCode += ";[Red]" + fmtCode + } + if numFmt.FormatCode == fmtCode { + style.NumFmt = numFmtID + } + } + } + } + } +} + +// extractAlignment provides a function to extract alignment format by +// given style definition. +func (f *File) extractAlignment(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyAlignment != nil && *xf.ApplyAlignment && xf.Alignment != nil { + style.Alignment = &Alignment{ + Horizontal: xf.Alignment.Horizontal, + Indent: xf.Alignment.Indent, + JustifyLastLine: xf.Alignment.JustifyLastLine, + ReadingOrder: xf.Alignment.ReadingOrder, + RelativeIndent: xf.Alignment.RelativeIndent, + ShrinkToFit: xf.Alignment.ShrinkToFit, + TextRotation: xf.Alignment.TextRotation, + Vertical: xf.Alignment.Vertical, + WrapText: xf.Alignment.WrapText, + } + } +} + +// extractProtection provides a function to extract protection settings by +// given format definition. +func (f *File) extractProtection(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyProtection != nil && *xf.ApplyProtection && xf.Protection != nil { + style.Protection = &Protection{} + if xf.Protection.Hidden != nil { + style.Protection.Hidden = *xf.Protection.Hidden + } + if xf.Protection.Locked != nil { + style.Protection.Locked = *xf.Protection.Locked + } + } +} + +// GetStyle get style details by given style index. +func (f *File) GetStyle(idx int) (*Style, error) { + var style *Style + f.mu.Lock() + s, err := f.stylesReader() + if err != nil { + return style, err + } + f.mu.Unlock() + if idx < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= idx { + return style, newInvalidStyleID(idx) + } + style = &Style{} + xf := s.CellXfs.Xf[idx] + if xf.ApplyFill != nil && *xf.ApplyFill && + xf.FillID != nil && s.Fills != nil && + *xf.FillID < len(s.Fills.Fill) { + f.extractFills(xf, s, style) + } + f.extractBorders(xf, s, style) + f.extractFont(xf, s, style) + f.extractAlignment(xf, s, style) + f.extractProtection(xf, s, style) + f.extractNumFmt(xf, s, style) + return style, nil +} + // getXfIDFuncs provides a function to get xfID by given style. var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { @@ -1406,9 +1703,15 @@ func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID return } +// isLangNumFmt provides a function to returns if a given number format ID is a +// built-in language glyphs number format code. +func isLangNumFmt(ID int) bool { + return (27 <= ID && ID <= 36) || (50 <= ID && ID <= 62) || (67 <= ID && ID <= 81) +} + // setLangNumFmt provides a function to set number format code with language. func setLangNumFmt(style *Style) int { - if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) { + if isLangNumFmt(style.NumFmt) { return style.NumFmt } return 0 diff --git a/styles_test.go b/styles_test.go index 856e90f03c..f202bcf02a 100644 --- a/styles_test.go +++ b/styles_test.go @@ -488,3 +488,105 @@ func TestGetNumFmtID(t *testing.T) { assert.NotEqual(t, id1, id2) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx"))) } + +func TestGetThemeColor(t *testing.T) { + assert.Empty(t, (&File{}).getThemeColor(&xlsxColor{})) + f := NewFile() + assert.Empty(t, f.getThemeColor(nil)) + var theme int + assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{Theme: &theme})) + assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{RGB: "FFFFFF"})) + assert.Equal(t, "FF8080", f.getThemeColor(&xlsxColor{Indexed: 2, Tint: 0.5})) + assert.Empty(t, f.getThemeColor(&xlsxColor{Indexed: len(IndexedColorMapping), Tint: 0.5})) +} + +func TestGetStyle(t *testing.T) { + f := NewFile() + expected := &Style{ + Border: []Border{ + {Type: "left", Color: "0000FF", Style: 3}, + {Type: "right", Color: "FF0000", Style: 6}, + {Type: "top", Color: "00FF00", Style: 4}, + {Type: "bottom", Color: "FFFF00", Style: 5}, + {Type: "diagonalUp", Color: "A020F0", Style: 7}, + {Type: "diagonalDown", Color: "A020F0", Style: 7}, + }, + Fill: Fill{Type: "gradient", Shading: 16, Color: []string{"0000FF", "00FF00"}}, + Font: &Font{ + Bold: true, Italic: true, Underline: "single", Family: "Arial", + Size: 8.5, Strike: true, Color: "777777", ColorIndexed: 1, ColorTint: 0.1, + }, + Alignment: &Alignment{ + Horizontal: "center", + Indent: 1, + JustifyLastLine: true, + ReadingOrder: 1, + RelativeIndent: 1, + ShrinkToFit: true, + TextRotation: 180, + Vertical: "center", + WrapText: true, + }, + Protection: &Protection{Hidden: true, Locked: true}, + NumFmt: 49, + } + styleID, err := f.NewStyle(expected) + assert.NoError(t, err) + style, err := f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.Border, style.Border) + assert.Equal(t, expected.Fill, style.Fill) + assert.Equal(t, expected.Font, style.Font) + assert.Equal(t, expected.Alignment, style.Alignment) + assert.Equal(t, expected.Protection, style.Protection) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + expected = &Style{ + Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"0000FF"}}, + } + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.Fill, style.Fill) + + expected = &Style{NumFmt: 27} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + expected = &Style{NumFmt: 165} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + expected = &Style{NumFmt: 165, NegRed: true} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + // Test get style with custom color index + f.Styles.Colors = &xlsxStyleColors{ + IndexedColors: xlsxIndexedColors{ + RgbColor: []xlsxColor{{RGB: "FF012345"}}, + }, + } + assert.Equal(t, "012345", f.getThemeColor(&xlsxColor{Indexed: 0})) + + // Test get style with invalid style index + style, err = f.GetStyle(-1) + assert.Nil(t, style) + assert.Equal(t, err, newInvalidStyleID(-1)) + // Test get style with unsupported charset style sheet + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + style, err = f.GetStyle(1) + assert.Nil(t, style) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") +} diff --git a/xmlStyles.go b/xmlStyles.go index 067cff92d4..1dda04a0aa 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -300,12 +300,19 @@ type xlsxNumFmt struct { FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"` } +// xlsxIndexedColors directly maps the single ARGB entry for the corresponding +// color index. +type xlsxIndexedColors struct { + RgbColor []xlsxColor `xml:"rgbColor"` +} + // xlsxStyleColors directly maps the colors' element. Color information -// associated with this stylesheet. This collection is written whenever the +// associated with this style sheet. This collection is written whenever the // legacy color palette has been modified (backwards compatibility settings) or // a custom color has been selected while using this workbook. type xlsxStyleColors struct { - Color string `xml:",innerxml"` + IndexedColors xlsxIndexedColors `xml:"indexedColors"` + MruColors xlsxInnerXML `xml:"mruColors"` } // Alignment directly maps the alignment settings of the cells.