├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── base83 ├── base83.go ├── base83_test.go └── error.go ├── decode.go ├── decode_test.go ├── encode.go ├── encode_test.go ├── error.go ├── fixtures ├── dalle.png ├── octocat.png └── test.png ├── fixtures_test.go ├── go.mod ├── go.sum └── util.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: [v*] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | matrix: 12 | strategy: 13 | matrix: 14 | go-version: [^1.*, 1.18.x, 1.16.x, 1.13.x] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - uses: actions/checkout@v3 21 | - run: go get -v -t -d ./... 22 | - run: go test -v ./... 23 | coverage: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Coverage on latest 1.x Go 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: ^1.* 30 | - uses: actions/checkout@v3 31 | - run: go get -v -t -d ./... 32 | - run: go test -v -coverprofile=coverage.out -covermode=count ./... 33 | - uses: codecov/codecov-action@v3 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | files: ./coverage.out 37 | fail_ci_if_error: true 38 | golangci: 39 | name: lint 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/setup-go@v3 43 | with: 44 | go-version: ^1.* 45 | - uses: actions/checkout@v3 46 | - name: golangci-lint 47 | uses: golangci/golangci-lint-action@v3 48 | with: 49 | version: latest 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ben Brooks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-blurhash [![Go Reference](https://pkg.go.dev/badge/github.com/bbrks/go-blurhash.svg)](https://pkg.go.dev/github.com/bbrks/go-blurhash) [![GitHub tag](https://img.shields.io/github/tag/bbrks/go-blurhash.svg)](https://github.com/bbrks/go-blurhash/releases) [![license](https://img.shields.io/github/license/bbrks/go-blurhash.svg)](https://github.com/bbrks/go-blurhash/blob/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/bbrks/go-blurhash)](https://goreportcard.com/report/github.com/bbrks/go-blurhash) [![codecov](https://codecov.io/gh/bbrks/go-blurhash/branch/master/graph/badge.svg)](https://codecov.io/gh/bbrks/go-blurhash) 2 | 3 | A pure Go implementation of [Blurhash](https://github.com/woltapp/blurhash). The API is stable, however the hashing function in either direction may not be. 4 | 5 | ![Blurhash Demo](https://i.imgur.com/9qxOXJW.png) 6 | 7 | Blurhash is an algorithm written by [Dag Ågren](https://github.com/DagAgren) for [Wolt (woltapp/blurhash)](https://github.com/woltapp/blurhash) that encodes an image into a short (~20-30 byte) ASCII string. When you decode the string back into an image, you get a gradient of colors that represent the original image. This can be useful for scenarios where you want an image placeholder before loading, or even to censor the contents of an image [a la Mastodon](https://blog.joinmastodon.org/2019/05/improving-support-for-adult-content-on-mastodon/). 8 | 9 | Under the covers, this library is almost a straight port of the [C version](https://github.com/woltapp/blurhash/tree/master/C), which is known to encode images slightly differently than the TypeScript implementation. 10 | 11 | ## Contributing 12 | 13 | Issues, feature requests or improvements welcome! 14 | 15 | ## Licence 16 | 17 | This project is licensed under the [MIT License](LICENSE). 18 | -------------------------------------------------------------------------------- /base83/base83.go: -------------------------------------------------------------------------------- 1 | package base83 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" 8 | 9 | // Decode decodes a base83 string into an integer value. 10 | func Decode(str string) (val int, err error) { 11 | for i, r := range str { 12 | idx := strings.IndexRune(chars, r) 13 | if idx == -1 { 14 | return 0, invalidError(r, i) 15 | } 16 | 17 | val = val*len(chars) + idx 18 | } 19 | return val, nil 20 | } 21 | 22 | // Encode encodes an integer value into a base83 string of the given length. 23 | func Encode(val, length int) (str string, err error) { 24 | 25 | divisor := 1 26 | for i := 0; i < length-1; i++ { 27 | divisor *= len(chars) 28 | } 29 | 30 | for i := 0; i < length; i++ { 31 | idx := val / divisor % len(chars) 32 | divisor /= len(chars) 33 | str += string(chars[idx]) 34 | } 35 | 36 | return str, nil 37 | } 38 | -------------------------------------------------------------------------------- /base83/base83_test.go: -------------------------------------------------------------------------------- 1 | package base83_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/matryer/is" 8 | 9 | "github.com/bbrks/go-blurhash/base83" 10 | ) 11 | 12 | var tests = []struct { 13 | str string 14 | val int 15 | }{ 16 | {"3", 3}, 17 | {"A", 10}, 18 | {":", 70}, 19 | {"~", 82}, 20 | {"01", 1}, // leading zeros are "trimmed" 21 | {"11", 84}, 22 | {"33", 252}, 23 | {"~$", 6869}, 24 | {"%%%%%%", 255172974336}, 25 | } 26 | 27 | func TestDecodeEncode(t *testing.T) { 28 | for _, test := range tests { 29 | t.Run(test.str, func(t *testing.T) { 30 | is := is.NewRelaxed(t) 31 | 32 | val, err := base83.Decode(test.str) 33 | is.NoErr(err) // Decode returned unexpected error 34 | is.Equal(val, test.val) // Decode got unexpected result 35 | }) 36 | } 37 | } 38 | 39 | func TestEncode(t *testing.T) { 40 | for _, test := range tests { 41 | t.Run(test.str, func(t *testing.T) { 42 | is := is.NewRelaxed(t) 43 | 44 | str, err := base83.Encode(test.val, len(test.str)) 45 | is.NoErr(err) // Encode returned unexpected error 46 | is.Equal(str, test.str) // Encode got unexpected result 47 | }) 48 | } 49 | } 50 | 51 | func TestDecodeInvalidInput(t *testing.T) { 52 | tests := []struct { 53 | str string 54 | val int 55 | err error 56 | }{ 57 | {"&", 0, base83.ErrInvalidInput}, 58 | } 59 | 60 | for _, test := range tests { 61 | t.Run(test.str, func(t *testing.T) { 62 | is := is.NewRelaxed(t) 63 | 64 | val, err := base83.Decode(test.str) 65 | is.True(err != nil) // Decode should've returned error for invalid input 66 | is.True(strings.Contains(err.Error(), test.err.Error())) // Decode returned wrong error 67 | is.Equal(val, test.val) // Decode got unexpected result 68 | }) 69 | } 70 | } 71 | 72 | func TestEncodeInvalidLength(t *testing.T) { 73 | tests := []struct { 74 | val int 75 | length int 76 | str string 77 | }{ 78 | {255172974336, 3, "%%%"}, 79 | {255172974336, 6, "%%%%%%"}, 80 | {255172974336, 9, "000%%%%%%"}, 81 | } 82 | 83 | for _, test := range tests { 84 | t.Run(test.str, func(t *testing.T) { 85 | is := is.NewRelaxed(t) 86 | 87 | output, err := base83.Encode(test.val, test.length) 88 | is.NoErr(err) // Encode should've returned error for invalid input 89 | is.Equal(output, test.str) // Encode got unexpected result 90 | }) 91 | } 92 | } 93 | 94 | func BenchmarkDecode(b *testing.B) { 95 | for _, test := range tests { 96 | b.Run(test.str, func(b *testing.B) { 97 | b.ReportAllocs() 98 | for i := 0; i < b.N; i++ { 99 | _, _ = base83.Decode("~$") 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func BenchmarkEncode(b *testing.B) { 106 | for _, test := range tests { 107 | b.Run(test.str, func(b *testing.B) { 108 | for i := 0; i < b.N; i++ { 109 | _, _ = base83.Encode(6869, 2) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /base83/error.go: -------------------------------------------------------------------------------- 1 | package base83 2 | 3 | import "errors" 4 | 5 | // ErrInvalidInput is returned when the given input to decode is not valid base83. 6 | var ErrInvalidInput = errors.New("base83: invalid input") 7 | 8 | func invalidError(r rune, i int) error { 9 | // No stdlib support for wrapped errors, so return as-is pre-1.13 10 | return ErrInvalidInput 11 | } 12 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package blurhash 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "math" 8 | 9 | "github.com/bbrks/go-blurhash/base83" 10 | ) 11 | 12 | // Components returns the X and Y components of a blurhash. 13 | func Components(hash string) (x, y int, err error) { 14 | sizeFlag, err := base83.Decode(string(hash[0])) 15 | if err != nil { 16 | return 0, 0, err 17 | } 18 | 19 | x = (sizeFlag % 9) + 1 20 | y = (sizeFlag / 9) + 1 21 | 22 | expectedLength := 4 + 2*x*y 23 | actualLength := len(hash) 24 | if expectedLength != actualLength { 25 | return 0, 0, lengthError(expectedLength, actualLength) 26 | } 27 | 28 | return x, y, nil 29 | } 30 | 31 | // Decode returns an NRGBA image of the given hash with the given size. 32 | func Decode(hash string, width, height int, punch int) (image.Image, error) { 33 | newImg := image.NewNRGBA(image.Rect(0, 0, width, height)) 34 | if err := DecodeDraw(newImg, hash, float64(punch)); err != nil { 35 | return nil, err 36 | } 37 | return newImg, nil 38 | } 39 | 40 | type drawImageNRGBA interface { 41 | SetNRGBA(x, y int, c color.NRGBA) 42 | } 43 | 44 | type drawImageRGBA interface { 45 | SetRGBA(x, y int, c color.RGBA) 46 | } 47 | 48 | // DecodeDraw decodes the given hash into the given image. 49 | func DecodeDraw(dst draw.Image, hash string, punch float64) error { 50 | numX, numY, err := Components(hash) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | quantisedMaximumValue, err := base83.Decode(string(hash[1])) 56 | if err != nil { 57 | return err 58 | } 59 | maximumValue := float64(quantisedMaximumValue+1) / 166 60 | 61 | // for each component 62 | colors := make([][3]float64, numX*numY) 63 | for i := range colors { 64 | if i == 0 { 65 | val, err := base83.Decode(hash[2:6]) 66 | if err != nil { 67 | return err 68 | } 69 | colors[i] = decodeDC(val) 70 | } else { 71 | val, err := base83.Decode(hash[4+i*2 : 6+i*2]) 72 | if err != nil { 73 | return err 74 | } 75 | colors[i] = decodeAC(float64(val), maximumValue*punch) 76 | } 77 | } 78 | 79 | bounds := dst.Bounds() 80 | width, height := bounds.Dx(), bounds.Dy() 81 | 82 | for y := 0; y < height; y++ { 83 | for x := 0; x < width; x++ { 84 | var r, g, b float64 85 | 86 | for j := 0; j < numY; j++ { 87 | for i := 0; i < numX; i++ { 88 | basis := math.Cos(math.Pi*float64(x)*float64(i)/float64(width)) * math.Cos(math.Pi*float64(y)*float64(j)/float64(height)) 89 | compColor := colors[i+j*numX] 90 | r += compColor[0] * basis 91 | g += compColor[1] * basis 92 | b += compColor[2] * basis 93 | } 94 | } 95 | 96 | sR := uint8(linearTosRGB(r)) 97 | sG := uint8(linearTosRGB(g)) 98 | sB := uint8(linearTosRGB(b)) 99 | sA := uint8(255) 100 | 101 | // interface smuggle 102 | switch d := dst.(type) { 103 | case drawImageNRGBA: 104 | d.SetNRGBA(x, y, color.NRGBA{sR, sG, sB, sA}) 105 | case drawImageRGBA: 106 | d.SetRGBA(x, y, color.RGBA{sR, sG, sB, sA}) 107 | default: 108 | d.Set(x, y, color.NRGBA{sR, sG, sB, sA}) 109 | } 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func decodeDC(val int) (c [3]float64) { 117 | c[0] = sRGBToLinear(val >> 16) 118 | c[1] = sRGBToLinear(val >> 8 & 255) 119 | c[2] = sRGBToLinear(val & 255) 120 | return c 121 | } 122 | 123 | func decodeAC(val, maximumValue float64) (c [3]float64) { 124 | quantR := math.Floor(val / (19 * 19)) 125 | quantG := math.Mod(math.Floor(val/19), 19) 126 | quantB := math.Mod(val, 19) 127 | c[0] = signPow((quantR-9)/9, 2.0) * maximumValue 128 | c[1] = signPow((quantG-9)/9, 2.0) * maximumValue 129 | c[2] = signPow((quantB-9)/9, 2.0) * maximumValue 130 | return c 131 | } 132 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package blurhash_test 2 | 3 | import ( 4 | "image" 5 | "image/png" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/matryer/is" 10 | 11 | "github.com/bbrks/go-blurhash" 12 | ) 13 | 14 | func TestDecodeRGBA(t *testing.T) { 15 | for _, test := range testFixtures { 16 | // skip tests without hashes 17 | if test.hash == "" { 18 | continue 19 | } 20 | 21 | t.Run(test.hash, func(t *testing.T) { 22 | is := is.New(t) 23 | 24 | img := image.NewRGBA(image.Rect(0, 0, 32, 32)) 25 | 26 | err := blurhash.DecodeDraw(img, test.hash, 1) 27 | is.NoErr(err) 28 | 29 | err = png.Encode(ioutil.Discard, img) 30 | is.NoErr(err) 31 | }) 32 | } 33 | } 34 | 35 | func TestDecode(t *testing.T) { 36 | for _, test := range testFixtures { 37 | // skip tests without hashes 38 | if test.hash == "" { 39 | continue 40 | } 41 | 42 | t.Run(test.hash, func(t *testing.T) { 43 | is := is.New(t) 44 | 45 | img, err := blurhash.Decode(test.hash, 32, 32, 1) 46 | is.NoErr(err) 47 | 48 | err = png.Encode(ioutil.Discard, img) 49 | is.NoErr(err) 50 | }) 51 | } 52 | } 53 | 54 | func TestComponents(t *testing.T) { 55 | for _, test := range testFixtures { 56 | // skip tests without expected component values 57 | if test.hash == "" || test.xComp == 0 || test.yComp == 0 { 58 | continue 59 | } 60 | 61 | t.Run(test.hash, func(t *testing.T) { 62 | is := is.NewRelaxed(t) 63 | 64 | x, y, err := blurhash.Components(test.hash) 65 | is.NoErr(err) // unexpected error getting components 66 | is.Equal(x, test.xComp) // blurhash component mismatch 67 | is.Equal(y, test.yComp) // blurhash component mismatch 68 | }) 69 | } 70 | } 71 | 72 | func BenchmarkComponents(b *testing.B) { 73 | for _, test := range testFixtures { 74 | // skip tests without hashes 75 | if test.hash == "" { 76 | continue 77 | } 78 | 79 | b.Run(test.hash, func(b *testing.B) { 80 | for i := 0; i < b.N; i++ { 81 | _, _, _ = blurhash.Components(test.hash) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func BenchmarkDecode(b *testing.B) { 88 | for _, test := range testFixtures { 89 | // skip tests without hashes 90 | if test.hash == "" { 91 | continue 92 | } 93 | 94 | b.Run(test.hash, func(b *testing.B) { 95 | for i := 0; i < b.N; i++ { 96 | _, _ = blurhash.Decode(test.hash, 32, 32, 1) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func BenchmarkDecodeDraw(b *testing.B) { 103 | for _, test := range testFixtures { 104 | // skip tests without hashes 105 | if test.hash == "" { 106 | continue 107 | } 108 | 109 | b.Run(test.hash, func(b *testing.B) { 110 | for i := 0; i < b.N; i++ { 111 | dst := image.NewRGBA(image.Rect(0, 0, 32, 32)) 112 | _ = blurhash.DecodeDraw(dst, test.hash, 1) 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package blurhash 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math" 7 | "strings" 8 | 9 | "github.com/bbrks/go-blurhash/base83" 10 | ) 11 | 12 | const ( 13 | minComponents = 1 14 | maxComponents = 9 15 | ) 16 | 17 | // Encode returns the blurhash for the given image. 18 | func Encode(xComponents, yComponents int, img image.Image) (hash string, err error) { 19 | if xComponents < minComponents || xComponents > maxComponents || 20 | yComponents < minComponents || yComponents > maxComponents { 21 | return "", ErrInvalidComponents 22 | } 23 | 24 | b := strings.Builder{} 25 | 26 | sizeFlag := (xComponents - 1) + (yComponents-1)*9 27 | sizeFlagEncoded, err := base83.Encode(sizeFlag, 1) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | _, err = b.WriteString(sizeFlagEncoded) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | // vector of yComponents*xComponents*(RGB) 38 | factors := make([][][3]float64, yComponents) 39 | for y := 0; y < yComponents; y++ { 40 | factors[y] = make([][3]float64, xComponents) 41 | for x := 0; x < xComponents; x++ { 42 | factor := multiplyBasisFunction(x, y, img) 43 | factors[y][x][0] = factor[0] 44 | factors[y][x][1] = factor[1] 45 | factors[y][x][2] = factor[2] 46 | } 47 | } 48 | 49 | maximumValue := 0.0 50 | if xComponents*yComponents-1 > 0 { 51 | actualMaximumValue := 0.0 52 | for y := 0; y < yComponents; y++ { 53 | for x := 0; x < xComponents; x++ { 54 | if y == 0 && x == 0 { 55 | continue 56 | } 57 | actualMaximumValue = math.Max(math.Abs(factors[y][x][0]), actualMaximumValue) 58 | actualMaximumValue = math.Max(math.Abs(factors[y][x][1]), actualMaximumValue) 59 | actualMaximumValue = math.Max(math.Abs(factors[y][x][2]), actualMaximumValue) 60 | } 61 | } 62 | 63 | quantisedMaximumValue := math.Max(0, math.Min(82, math.Floor(actualMaximumValue*166-0.5))) 64 | maximumValue = (quantisedMaximumValue + 1) / 166 65 | str, err := base83.Encode(int(quantisedMaximumValue), 1) 66 | if err != nil { 67 | return "", err 68 | } 69 | b.WriteString(str) 70 | } else { 71 | maximumValue = 1 72 | str, err := base83.Encode(0, 1) 73 | if err != nil { 74 | return "", err 75 | } 76 | b.WriteString(str) 77 | } 78 | 79 | dc := factors[0][0] 80 | str, err := base83.Encode(encodeDC(dc[0], dc[1], dc[2]), 4) 81 | if err != nil { 82 | return "", err 83 | } 84 | b.WriteString(str) 85 | 86 | for y := 0; y < yComponents; y++ { 87 | for x := 0; x < xComponents; x++ { 88 | if y == 0 && x == 0 { 89 | continue 90 | } 91 | str, err := base83.Encode(encodeAC(factors[y][x][0], factors[y][x][1], factors[y][x][2], maximumValue), 2) 92 | if err != nil { 93 | return "", err 94 | } 95 | b.WriteString(str) 96 | } 97 | } 98 | 99 | return b.String(), nil 100 | } 101 | 102 | func encodeDC(r, g, b float64) int { 103 | return (linearTosRGB(r) << 16) + (linearTosRGB(g) << 8) + linearTosRGB(b) 104 | } 105 | 106 | func encodeAC(r, g, b, maximumValue float64) int { 107 | quantR := math.Max(0, math.Min(18, math.Floor(signPow(r/maximumValue, 0.5)*9+9.5))) 108 | quantG := math.Max(0, math.Min(18, math.Floor(signPow(g/maximumValue, 0.5)*9+9.5))) 109 | quantB := math.Max(0, math.Min(18, math.Floor(signPow(b/maximumValue, 0.5)*9+9.5))) 110 | 111 | return int(quantR*19*19 + quantG*19 + quantB) 112 | } 113 | 114 | func multiplyBasisFunction(xComponents, yComponents int, img image.Image) [3]float64 { 115 | var r, g, b float64 116 | width, height := float64(img.Bounds().Dx()), float64(img.Bounds().Dy()) 117 | 118 | normalisation := 2.0 119 | if xComponents == 0 && yComponents == 0 { 120 | normalisation = 1.0 121 | } 122 | 123 | for x := 0; x < img.Bounds().Dx(); x++ { 124 | for y := 0; y < img.Bounds().Max.Y; y++ { 125 | //cR, cG, cB, _ := img.At(x, y).RGBA() 126 | c, ok := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) 127 | if !ok { 128 | panic("not color.NRGBA") 129 | } 130 | basis := math.Cos(math.Pi*float64(xComponents)*float64(x)/width) * 131 | math.Cos(math.Pi*float64(yComponents)*float64(y)/height) 132 | r += basis * sRGBToLinear(int(c.R)) 133 | g += basis * sRGBToLinear(int(c.G)) 134 | b += basis * sRGBToLinear(int(c.B)) 135 | } 136 | } 137 | 138 | scale := normalisation / (width * height) 139 | return [3]float64{ 140 | r * scale, 141 | g * scale, 142 | b * scale, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package blurhash_test 2 | 3 | import ( 4 | "image" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/matryer/is" 10 | 11 | "github.com/bbrks/go-blurhash" 12 | ) 13 | 14 | func TestEncode(t *testing.T) { 15 | for _, test := range testFixtures { 16 | if test.file == "" { 17 | // skip tests without files 18 | continue 19 | } 20 | 21 | t.Run(test.hash, func(t *testing.T) { 22 | is := is.New(t) 23 | 24 | f, err := os.Open(filepath.FromSlash(test.file)) 25 | is.NoErr(err) // error opening test fixture file 26 | defer f.Close() 27 | 28 | is.True(f != nil) // file should not be nil 29 | 30 | img, _, err := image.Decode(f) 31 | is.NoErr(err) // error decoding image from test fixture 32 | is.True(img != nil) // image should not be nil 33 | 34 | hash, err := blurhash.Encode(test.xComp, test.yComp, img) 35 | is.NoErr(err) // error hashing test fixture image 36 | is.Equal(hash, test.hash) // blurhash mismatch 37 | }) 38 | } 39 | } 40 | 41 | func BenchmarkEncode(b *testing.B) { 42 | for _, test := range testFixtures { 43 | if test.file == "" { 44 | // skip tests without files 45 | continue 46 | } 47 | 48 | b.Run(test.hash, func(b *testing.B) { 49 | is := is.New(b) 50 | 51 | f, err := os.Open(filepath.FromSlash(test.file)) 52 | is.NoErr(err) // error opening test fixture file 53 | defer f.Close() 54 | 55 | is.True(f != nil) // file should not be nil 56 | 57 | img, _, err := image.Decode(f) 58 | is.NoErr(err) // error decoding image from test fixture 59 | is.True(img != nil) // image should not be nil 60 | 61 | b.ResetTimer() 62 | for i := 0; i < b.N; i++ { 63 | _, _ = blurhash.Encode(test.xComp, test.yComp, img) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package blurhash 2 | 3 | import "errors" 4 | 5 | // ErrInvalidComponents is returned when components passed to Encode are invalid. 6 | var ErrInvalidComponents = errors.New("blurhash: must have between 1 and 9 components") 7 | 8 | // ErrInvalidHash is returned when the library encounters a hash it can't recognise. 9 | var ErrInvalidHash = errors.New("blurhash: invalid hash") 10 | 11 | func lengthError(expectedLength, actualLength int) error { 12 | // No stdlib support for wrapped errors, so return as-is pre-1.13 13 | return ErrInvalidHash 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/dalle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbrks/go-blurhash/c96d1fe454b55edb3adc20bab3678e35ed4d3c66/fixtures/dalle.png -------------------------------------------------------------------------------- /fixtures/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbrks/go-blurhash/c96d1fe454b55edb3adc20bab3678e35ed4d3c66/fixtures/octocat.png -------------------------------------------------------------------------------- /fixtures/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbrks/go-blurhash/c96d1fe454b55edb3adc20bab3678e35ed4d3c66/fixtures/test.png -------------------------------------------------------------------------------- /fixtures_test.go: -------------------------------------------------------------------------------- 1 | package blurhash_test 2 | 3 | var testFixtures = []struct { 4 | file string 5 | hash string 6 | xComp, yComp int 7 | }{ 8 | {"fixtures/test.png", "LFE.@D9F01_2%L%MIVD*9Goe-;WB", 4, 3}, 9 | {"fixtures/octocat.png", "LNAdApj[00aymkj[TKay9}ay-Sj[", 4, 3}, 10 | {"fixtures/dalle.png", "eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.", 5, 5}, 11 | {"", "LNMF%n00%#MwS|WCWEM{R*bbWBbH", 4, 3}, 12 | {"", "KJG8_@Dgx]_4V?xuyE%NRj", 3, 3}, 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bbrks/go-blurhash 2 | 3 | go 1.12 4 | 5 | require github.com/matryer/is v1.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 2 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 3 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package blurhash 2 | 3 | import "math" 4 | 5 | func signPow(val, exp float64) float64 { 6 | sign := 1.0 7 | if val < 0 { 8 | sign = -1 9 | } 10 | return sign * math.Pow(math.Abs(val), exp) 11 | } 12 | 13 | func sRGBToLinear(val int) float64 { 14 | v := float64(val) / 255 15 | if v <= 0.04045 { 16 | return v / 12.92 17 | } 18 | return math.Pow((v+0.055)/1.055, 2.4) 19 | } 20 | 21 | func linearTosRGB(val float64) int { 22 | v := math.Max(0, math.Min(1, val)) 23 | if v <= 0.0031308 { 24 | return int(v * 12.92 * 255 * 0.5) 25 | } 26 | return int((1.055*math.Pow(v, 1/2.4)-0.055)*255 + 0.5) 27 | } 28 | --------------------------------------------------------------------------------