| 1 | package xlsx |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | |
| 6 | "github.com/xuri/excelize/v2" |
| 7 | |
| 8 | "amadan.net/oficina/sheets/internal/sheets/store" |
| 9 | ) |
| 10 | |
| 11 | // styleFromFormat builds the excelize style a Format should render as. |
| 12 | // ok is false when f describes nothing worth a style at all (the zero |
| 13 | // Format), so the caller can skip SetCellStyle entirely and leave a cell |
| 14 | // on the workbook's default style rather than writing a no-op one. |
| 15 | // |
| 16 | // NumFmt always goes through CustomNumFmt, never the builtin NumFmt id: |
| 17 | // excelize's own round trip (verified directly — see the package doc) |
| 18 | // only reflects a written style's NumFmt id back as CustomNumFmt when it |
| 19 | // isn't one of its ~50 builtins, so relying on the id for something as |
| 20 | // common as "0.00" would silently stop being lossless the day this |
| 21 | // package's number-format strings happen to collide with one. |
| 22 | func styleFromFormat(f *store.Format) (excelize.Style, bool) { |
| 23 | if f == nil { |
| 24 | return excelize.Style{}, false |
| 25 | } |
| 26 | var st excelize.Style |
| 27 | used := false |
| 28 | |
| 29 | if f.NumberFmt != "" { |
| 30 | nf := f.NumberFmt |
| 31 | st.CustomNumFmt = &nf |
| 32 | used = true |
| 33 | } |
| 34 | // The condition includes InkHex, not just bold and italic: a cell |
| 35 | // whose only font property is a colour would otherwise get no Font |
| 36 | // element at all, and the retained ink would be dropped on export — |
| 37 | // silently, and only for the cells §11.1 is about. |
| 38 | if f.Bold || f.Italic || f.InkHex != "" { |
| 39 | st.Font = &excelize.Font{Bold: f.Bold, Italic: f.Italic} |
| 40 | if len(f.InkHex) == 8 { |
| 41 | // Same 6-char RGB convention as Fill.Color below: excelize |
| 42 | // prepends its own alpha. |
| 43 | st.Font.Color = f.InkHex[2:] |
| 44 | } |
| 45 | used = true |
| 46 | } |
| 47 | if f.Align != "" { |
| 48 | st.Alignment = &excelize.Alignment{Horizontal: f.Align} |
| 49 | used = true |
| 50 | } |
| 51 | if hex := exportFillHex(f); hex != "" { |
| 52 | // excelize's Fill.Color always takes 6-char RGB (styles.go's |
| 53 | // getPaletteColor prepends its own "FF" alpha) — see the |
| 54 | // package doc on colors.go for why this app's own FillHex is |
| 55 | // always the 8-char ARGB form. |
| 56 | st.Fill = excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{hex[2:]}} |
| 57 | used = true |
| 58 | } |
| 59 | return st, used |
| 60 | } |
| 61 | |
| 62 | // exportFillHex resolves what hex a format's fill should render as: the |
| 63 | // retained import hex when present (lossless re-export — the whole point |
| 64 | // of FillHex, models.go), otherwise the intent's hue/chroma resolved at a |
| 65 | // fixed lightness (colors.go), otherwise no fill at all. |
| 66 | func exportFillHex(f *store.Format) string { |
| 67 | if f.FillHex != "" { |
| 68 | return f.FillHex |
| 69 | } |
| 70 | if f.FillIntentH != nil && f.FillIntentC != nil { |
| 71 | return oklchToHex(*f.FillIntentH, *f.FillIntentC) |
| 72 | } |
| 73 | return "" |
| 74 | } |
| 75 | |
| 76 | // formatFromStyle is styleFromFormat's inverse for import: it reads back |
| 77 | // what a cell's excelize style actually says and builds the Format this |
| 78 | // app stores. ok is false when the style carries nothing this app |
| 79 | // tracks (the workbook's default style, most commonly), so the caller |
| 80 | // can skip writing a Format at all. |
| 81 | func formatFromStyle(st *excelize.Style) (store.Format, bool) { |
| 82 | var f store.Format |
| 83 | used := false |
| 84 | |
| 85 | if st.CustomNumFmt != nil && *st.CustomNumFmt != "" { |
| 86 | f.NumberFmt = *st.CustomNumFmt |
| 87 | used = true |
| 88 | } |
| 89 | if st.Font != nil && (st.Font.Bold || st.Font.Italic) { |
| 90 | f.Bold = st.Font.Bold |
| 91 | f.Italic = st.Font.Italic |
| 92 | used = true |
| 93 | } |
| 94 | if st.Font != nil && st.Font.Color != "" { |
| 95 | // Retained, not resolved. Spec §11.1: we render an imported |
| 96 | // file as authored, and surface a pairing that fails AA rather |
| 97 | // than silently correcting it — correcting would lie about the |
| 98 | // document, which is the same class of wrong as inventing a |
| 99 | // font colour on export. |
| 100 | f.InkHex = argbFromStyleColor(st.Font.Color) |
| 101 | used = true |
| 102 | } |
| 103 | if st.Alignment != nil && st.Alignment.Horizontal != "" { |
| 104 | f.Align = st.Alignment.Horizontal |
| 105 | used = true |
| 106 | } |
| 107 | if st.Fill.Type == "pattern" && len(st.Fill.Color) > 0 && st.Fill.Color[0] != "" { |
| 108 | hex := argbFromStyleColor(st.Fill.Color[0]) |
| 109 | f.FillHex = hex |
| 110 | if hue, chroma, err := hexToOKLCH(hex); err == nil { |
| 111 | f.FillIntentH = &hue |
| 112 | f.FillIntentC = &chroma |
| 113 | } |
| 114 | used = true |
| 115 | } |
| 116 | return f, used |
| 117 | } |
| 118 | |
| 119 | // argbFromStyleColor turns whatever excelize.Style.Fill.Color[0] hands |
| 120 | // back (6-char RGB — see styleFromFormat's comment) into this package's |
| 121 | // canonical 8-char ARGB form. Already-8-char input (defensive: some other |
| 122 | // producer of *excelize.Style might not follow excelize's own round |
| 123 | // trip) is upper-cased and returned as-is rather than double-prefixed. |
| 124 | func argbFromStyleColor(color string) string { |
| 125 | c := strings.ToUpper(strings.TrimPrefix(color, "#")) |
| 126 | if len(c) == 8 { |
| 127 | return c |
| 128 | } |
| 129 | return "FF" + c |
| 130 | } |
| 131 | |