| 1 | package xlsx |
| 2 | |
| 3 | import ( |
| 4 | "math" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // The OKLCH round trip only needs to be good enough that snapping a hex |
| 9 | // to hue/chroma and back doesn't visibly drift — FillHex, not this |
| 10 | // conversion, is what actually drives lossless export (colors.go's own |
| 11 | // doc comment). A few ULP of float error is fine; a wrong channel is not. |
| 12 | func TestHexOKLCHRoundTrip(t *testing.T) { |
| 13 | cases := []string{"FF00B050", "FFFF0000", "FF0000FF", "FFFFFFFF", "FF000000", "FF808080"} |
| 14 | for _, hex := range cases { |
| 15 | hue, chroma, err := hexToOKLCH(hex) |
| 16 | if err != nil { |
| 17 | t.Fatalf("hexToOKLCH(%s): %v", hex, err) |
| 18 | } |
| 19 | got := oklchToHex2(hue, chroma, hex) |
| 20 | r1, g1, b1, _ := parseHexRGB(hex) |
| 21 | r2, g2, b2, _ := parseHexRGB(got) |
| 22 | if diff(r1, r2) > 2 || diff(g1, g2) > 2 || diff(b1, b2) > 2 { |
| 23 | t.Errorf("%s round-tripped to %s (hue=%.2f chroma=%.4f), channels drifted more than 2/255", hex, got, hue, chroma) |
| 24 | } |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // oklchToHex2 mirrors oklchToHex but at the SOURCE lightness rather than |
| 29 | // defaultFillLightness, isolating the hue/chroma conversion from the |
| 30 | // lightness-loss that's expected (and documented) when re-deriving a hex |
| 31 | // from a lightness-free intent. |
| 32 | func oklchToHex2(hue, chroma float64, srcHex string) string { |
| 33 | r, g, b, _ := parseHexRGB(srcHex) |
| 34 | l, _, _ := srgbToOKLCH(r, g, b) |
| 35 | rr, gg, bb := oklchToSRGB(l, chroma, hue) |
| 36 | return argbHex(rr, gg, bb) |
| 37 | } |
| 38 | |
| 39 | func diff(a, b float64) float64 { return math.Abs(a - b) } |
| 40 | |
| 41 | func TestParseHexRGBAcceptsSixAndEightChar(t *testing.T) { |
| 42 | r, g, b, err := parseHexRGB("FF00B050") |
| 43 | if err != nil || r != 0 || g != 176 || b != 80 { |
| 44 | t.Fatalf("8-char parse = %v %v %v %v", r, g, b, err) |
| 45 | } |
| 46 | r2, g2, b2, err := parseHexRGB("00B050") |
| 47 | if err != nil || r2 != r || g2 != g || b2 != b { |
| 48 | t.Fatalf("6-char parse mismatch: %v %v %v %v", r2, g2, b2, err) |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | func TestParseHexRGBRejectsGarbage(t *testing.T) { |
| 53 | if _, _, _, err := parseHexRGB("not-a-color"); err == nil { |
| 54 | t.Fatal("want error for invalid hex") |
| 55 | } |
| 56 | if _, _, _, err := parseHexRGB("ABCD"); err == nil { |
| 57 | t.Fatal("want error for wrong-length hex") |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | func TestArgbHexAlwaysOpaqueEightChar(t *testing.T) { |
| 62 | h := argbHex(0, 176, 80) |
| 63 | if h != "FF00B050" { |
| 64 | t.Fatalf("argbHex(0,176,80) = %s, want FF00B050", h) |
| 65 | } |
| 66 | } |
| 67 | |