package helper import ( "fmt" "math" "testing" ) func TestLinearize(t *testing.T) { tests := []struct { value uint32 want float64 }{ // the minimum and maximum legal values should map to 0 and 1, respectively. {0x0000, 0.0}, {0xffff, 1.0}, // check what would be 0x01 in 8-bit sRGB. // this is below the point where it the function switches from linear to exponential. {0x0101, 0x1.3e312a36f1977p-12}, // check the midpoint of the gamma curve. {0x7fff, 0x1.b6577fc57aa37p-03}, // We do support values beyond the maximum legal value, although you probably shouldn't depend on it // as the converse function will map these to the maximum legal value, creating an asymmetry. {0x10000, 0x1.000246626b604p+00}, {math.MaxUint32, 0x1.2912a0c535107p+38}, } for _, tt := range tests { t.Run(fmt.Sprintf("0x%04x", tt.value), func(t *testing.T) { if got := Linearize(tt.value); !EqFloat64Fuzzy(got, tt.want) { t.Errorf("Linearize(0x%04x) = %x: want %x", tt.value, got, tt.want) } }) } t.Run("monotonically increasing", func(t *testing.T) { for i, prev := uint32(1), Linearize(0); i < 0x10000; i++ { got := Linearize(i) if got <= prev { t.Errorf("Linearize(0x%04x) = %x; want > %x", i, got, prev) } prev = got } }) } func TestLinearizeF(t *testing.T) { // the positive finites were indirectly tested by TestLinearize, so we'll just test the // negative and non-finite inputs. tests := []struct { value float64 want float64 }{ {-1, -1 / 12.923210180787861094641554898407}, {0, 0}, {1, 1}, {2, 4.9538457515920408157613451180477}, {math.Inf(-1), math.Inf(-1)}, {math.Inf(1), math.Inf(1)}, {math.NaN(), math.NaN()}, } for _, tt := range tests { t.Run(fmt.Sprintf("%x", tt.value), func(t *testing.T) { if got := LinearizeF(tt.value); !EqFloat64Fuzzy(got, tt.want) { t.Errorf("LinearizeF(%x) = %x: want %x", tt.value, got, tt.want) } }) } } func TestDelinearize(t *testing.T) { tests := []struct { value float64 want uint32 }{ // make sure clamping to legal values is happening. {-1, 0x0000}, {2, 0xffff}, // again with the next values below 0 and above 1. {math.Nextafter(0, math.Inf(-1)), 0}, {math.Nextafter(1, math.Inf(1)), 0xffff}, // and lastly, the non-finites. {math.Inf(-1), 0x0000}, {math.Inf(1), 0xffff}, {math.NaN(), 0x0000}, } for _, tt := range tests { t.Run(fmt.Sprintf("%x", tt.value), func(t *testing.T) { if got := Delinearize(tt.value); got != tt.want { t.Errorf("Delinearize(%x) = 0x%04x: want 0x%04x", tt.value, got, tt.want) } }) } t.Run("lossless conversion of legal values", func(t *testing.T) { for c := uint32(0); c < 0x10000; c++ { got := Delinearize(Linearize(c)) if got != c { t.Errorf("Delinearize(Linearize(0x%04x)) != 0x%04x", c, got) return } } }) } func TestDelinearizeF(t *testing.T) { // values that would have been impossible for Delinearize to represent. tests := []struct { value float64 want float64 }{ {-1, -12.923210180787861094641554898407}, {math.Inf(-1), math.Inf(-1)}, {math.Inf(1), math.Inf(1)}, {math.NaN(), math.NaN()}, {2, 1.3532560461493862548965276346496}, } for _, tt := range tests { t.Run(fmt.Sprintf("%x", tt.value), func(t *testing.T) { if got := DelinearizeF(tt.value); !EqFloat64Fuzzy(got, tt.want) { t.Errorf("DelinearizeF(%x) = %x: want %x", tt.value, got, tt.want) } }) } t.Run("roundtrip conversions", func(t *testing.T) { for c := -0x1000; c < 0x11000; c++ { f := float64(c) / 0x10000 got := DelinearizeF(LinearizeF(f)) if !EqFloat64Fuzzy(got, f) { t.Errorf("DelinearizeF(LinearizeF(%x)) != %x", c, got) return } } }) }