├── .gitignore ├── LICENSE ├── README.md ├── blurhash.nimble ├── src └── blurhash.nim └── tests ├── blurred.png ├── image.png └── test1.nim /.gitignore: -------------------------------------------------------------------------------- 1 | tests/t* 2 | !tests/*.nim 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 SolitudeSF 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blurhash 2 | 3 | > Pure [Nim](https://nim-lang.org) implementation of [Blurhash](https://blurha.sh) 4 | 5 | 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/). 6 | 7 | ## Installation 8 | 9 | `nimble install blurhash` 10 | 11 | ## Usage 12 | 13 | Add `requires "blurhash"` to your `.nimble` file. 14 | 15 | ### Encoding 16 | 17 | ```nim 18 | import blurhash, imageman/[images, colors] 19 | 20 | let 21 | image = loadImage[ColorRGB]("image.png") 22 | hash = image.encode(5, 5) 23 | 24 | echo hash 25 | ``` 26 | 27 | This snippet hashes following image into this compact string: `UrQ]$mfQ~qj@ocofWFWB?bj[D%azf6WBj[t7` 28 | 29 | ![reference_image](tests/image.png) 30 | 31 | ### Decoding 32 | 33 | ```nim 34 | import blurhash, imageman/images 35 | 36 | let image = "UrQ]$mfQ~qj@ocofWFWB?bj[D%azf6WBj[t7".decode[ColorRGBU](500, 500) 37 | 38 | image.savePNG "blurred.png" 39 | ``` 40 | 41 | This results in following image: 42 | 43 | ![blurred_image](tests/blurred.png) 44 | 45 | ## Other 46 | 47 | Reference image author - https://rigani.me 48 | -------------------------------------------------------------------------------- /blurhash.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.1" 4 | author = "SolitudeSF" 5 | description = "Encoder/decoder for blurhash algorithm" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.2.0", "imageman >= 0.7.4" 13 | -------------------------------------------------------------------------------- /src/blurhash.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import imageman/[images, colors] 3 | 4 | const base83chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 5 | 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 6 | 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 7 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 8 | 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', '-', '.', ':', ';', 9 | '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'] 10 | 11 | template matchingRGB[T: Color](t: typedesc[T]): typedesc = 12 | when T is ColorHSL: 13 | ColorRGBF 14 | elif T is (ColorHSLuv | ColorHPLuv): 15 | ColorRGBF64 16 | else: 17 | T 18 | 19 | func intPow(a, b: int): int = 20 | result = 1 21 | for _ in 1..b: 22 | result *= a 23 | 24 | func signPow(a, b: float): float = 25 | if a < 0: 26 | pow(abs(a), b) * -1 27 | else: 28 | pow(a, b) 29 | 30 | func encode83(n, l: int): string = 31 | result = newString(l) 32 | for i in 1..l: 33 | result[i - 1] = base83chars[(n div intPow(83, l - i)) mod 83] 34 | 35 | func decode83(s: string): int = 36 | for c in s: 37 | result = result * 83 + base83chars.find(c) 38 | 39 | func toXYZ(n: uint8): float32 = 40 | result = n.float32 / 255.0 41 | if result <= 0.04045: 42 | result = result / 12.92 43 | else: 44 | result = pow((result + 0.055) / 1.055, 2.4) 45 | 46 | func toXYZ(n: float32): float32 = 47 | if n <= 0.04045: 48 | n / 12.92 49 | else: 50 | pow((n + 0.055) / 1.055, 2.4) 51 | 52 | func toFloatSrgb[T: float32 | float64](n: T): T = 53 | let v = n.clamp(0, 1) 54 | if v <= 0.0031308: 55 | (v * 12.92 * 255.0 + 0.5) / 255.0 56 | else: 57 | ((pow(v, 1.0 / 2.4) * 1.055 - 0.055) * 255.0 + 0.5) / 255.0 58 | 59 | func toUintSrgb(n: float32): uint8 = 60 | let v = n.clamp(0, 1) 61 | if v <= 0.0031308: 62 | uint8(v * 12.92 * 255.0 + 0.5) 63 | else: 64 | uint8((pow(v, 1.0 / 2.4) * 1.055 - 0.055) * 255.0 + 0.5) 65 | 66 | func components*(s: string): tuple[x, y: int] = 67 | ## Returns x, y components of given blurhash 68 | let size = base83chars.find(s[0]) 69 | result = ((size mod 9) + 1, (size div 9) + 1) 70 | 71 | if s.len != 4 + 2 * result.x * result.y: 72 | raise newException(ValueError, "Invalid Blurhash string.") 73 | 74 | func encode*[T: Color](img: Image[T], componentsX = 4, componentsY = 4): string = 75 | ## Calculates Blurhash for an image using the given x and y component counts. 76 | ## 77 | ## Component counts must be between 1 and 9 inclusive. 78 | assert componentsX <= 9 and componentsX >= 1 and 79 | componentsY <= 9 and componentsY >= 1 80 | 81 | let len = img.data.len.float 82 | var 83 | comps = newSeq[array[3, float]](componentsX * componentsY) 84 | maxComponent = 0.0 85 | 86 | for j in 0.. 1: 118 | normMaxValue = float(quantMaxValue + 1) / 166.0 119 | result &= quantMaxValue.encode83(1) 120 | else: 121 | normMaxValue = 1.0 122 | result &= 0.encode83(1) 123 | 124 | result &= dcValue.encode83(4) 125 | 126 | for i in 1..comps.high: 127 | result &= (int(max(0, min(18, floor(signPow(comps[i][0] / normMaxValue, 0.5) * 9 + 9.5)))) * 19 * 19 + 128 | int(max(0, min(18, floor(signPow(comps[i][1] / normMaxValue, 0.5) * 9 + 9.5)))) * 19 + 129 | int(max(0, min(18, floor(signPow(comps[i][2] / normMaxValue, 0.5) * 9 + 9.5))))). 130 | encode83(2) 131 | 132 | func decode*[T: Color](s: string, width, height: int, punch = 1.0): Image[T] = 133 | ## Decodes given blurhash to an RGB image with specified dimensions 134 | ## 135 | ## Punch parameter can be used to increase/decrease contrast of the resulting image 136 | let 137 | (sizeX, sizeY) = s.components 138 | quantMaxValue = base83chars.find s[1] 139 | maxValue = float(quantMaxValue + 1) / 166.0 * punch 140 | 141 | let dcValue = s[2..5].decode83 142 | var colors = newSeq[array[3, float32]](sizeX * sizeY) 143 | 144 | colors[0] = [(dcValue shr 16).uint8.toXYZ, 145 | ((dcValue shr 8) and 255).uint8.toXYZ, 146 | (dcValue and 255).uint8.toXYZ] 147 | 148 | for i in 1..colors.high: 149 | let acValue = s[4 + i * 2..5 + i * 2].decode83 150 | colors[i][0] = signPow((float(acValue div (19 * 19)) - 9) / 9, 2) * maxValue 151 | colors[i][1] = signPow((float((acValue div 19) mod 19) - 9) / 9, 2) * maxValue 152 | colors[i][2] = signPow((float(acValue mod 19) - 9) / 9, 2) * maxValue 153 | 154 | var r = initImage[T.matchingRGB](width, height) 155 | 156 | for y in 0..