├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib ├── blurhash_dart.dart ├── blurhash_extensions.dart └── src │ ├── blurhash.dart │ ├── encoding.dart │ ├── exception.dart │ ├── extension.dart │ └── foundation.dart ├── pubspec.yaml └── test ├── blurhash_extensions_test.dart ├── blurhash_test.dart └── images ├── darkness_test_0.png ├── darkness_test_1.png ├── darkness_test_2.png ├── test0.png ├── test1.png ├── test2.png └── test3.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | tags 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | build/ 33 | pubspec.lock 34 | 35 | # Android related 36 | **/android/**/gradle-wrapper.jar 37 | **/android/.gradle 38 | **/android/captures/ 39 | **/android/gradlew 40 | **/android/gradlew.bat 41 | **/android/local.properties 42 | **/android/**/GeneratedPluginRegistrant.java 43 | 44 | # iOS/XCode related 45 | **/ios/**/*.mode1v3 46 | **/ios/**/*.mode2v3 47 | **/ios/**/*.moved-aside 48 | **/ios/**/*.pbxuser 49 | **/ios/**/*.perspectivev3 50 | **/ios/**/*sync/ 51 | **/ios/**/.sconsign.dblite 52 | **/ios/**/.tags* 53 | **/ios/**/.vagrant/ 54 | **/ios/**/DerivedData/ 55 | **/ios/**/Icon? 56 | **/ios/**/Pods/ 57 | **/ios/**/.symlinks/ 58 | **/ios/**/profile 59 | **/ios/**/xcuserdata 60 | **/ios/.generated/ 61 | **/ios/Flutter/App.framework 62 | **/ios/Flutter/Flutter.framework 63 | **/ios/Flutter/Flutter.podspec 64 | **/ios/Flutter/Generated.xcconfig 65 | **/ios/Flutter/app.flx 66 | **/ios/Flutter/app.zip 67 | **/ios/Flutter/flutter_assets/ 68 | **/ios/Flutter/flutter_export_environment.sh 69 | **/ios/ServiceDefinitions.json 70 | **/ios/Runner/GeneratedPluginRegistrant.* 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.2.1] 4 | 5 | * Fix crash for some specific `Image`s by using an pixel iterator 6 | instead of raw byte access. 7 | 8 | ## [1.2.0] 9 | 10 | * Update image dependency to >=4.0.8 11 | 12 | ## [1.1.0] 13 | 14 | * Improve performance of `BlurHash.encode` 15 | 16 | ## [1.0.2] 17 | 18 | * Add note in readme on usage with Flutter 19 | * Fix incorrect precondition in `BlurHash.encode` 20 | 21 | ## [1.0.1] 22 | 23 | * Fix missing export for exception types 24 | 25 | ## [1.0.0] 26 | 27 | * Migrated to null-safety 28 | * **Deprecated**: `encodeBlurHash` and `decodeBlurHash` are now deprecated 29 | Please use `BlurHash.encode` and `BlurHash.decode` instead 30 | * Added BlurHash extension methods 31 | 32 | ## [0.2.3] 33 | 34 | * Make the pub.dev analysis tool happy 35 | * Formatting 36 | 37 | ## [0.2.2] 38 | 39 | * Fix minor style issues 40 | * Make the pub.dev analysis tool happy by providing a longer description 41 | 42 | ## [0.2.1] 43 | 44 | * Change import name as suggested by pub publish tool 45 | 46 | ## [0.2.0] 47 | 48 | * Add support for encoding blurhashes 49 | * Decoder now returns raw pixels in RGBA32 50 | 51 | ## [0.1.0] 52 | 53 | * First release 54 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # BlurHash-Dart Contributors 2 | 3 | * [Robin Alter](https://github.com/robin-alter) 4 | * Darkness tests and utility functions 5 | * [Tobias Schwackenhofer](https://github.com/justacid) 6 | * Author and maintainer 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT Licence 2 | 3 | Copyright (c) 2020 Tobias Schwackenhofer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies 13 | or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 17 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlurHash Dart 2 | 3 | A pure dart implementation of BlurHash. Supports both encoding and decoding. Runs on 4 | every supported Dart platform. See the [BlurHash](https://blurha.sh/) website or [GitHub 5 | repository](https://github.com/woltapp/blurhash) for more information. 6 | 7 | The encoder of this dart implementation produces slightly different hashes than the 8 | TypeScript implementation but matches the official C and Python implementation. In 9 | practice this should not be relevant. 10 | 11 | ## Basic usage 12 | 13 | ### Decoding a BlurHash 14 | 15 | ```dart 16 | import 'package:blurhash_dart/blurhash_dart.dart'; 17 | import 'package:image/image.dart' as img; 18 | 19 | const hash = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; 20 | BlurHash blurHash = BlurHash.decode(hash); 21 | img.Image image = blurHash.toImage(35, 20); 22 | 23 | // Display image... 24 | ``` 25 | 26 | #### Note: Usage with Flutter 27 | 28 | Flutter does not expect raw bytes for its `Image.memory` widget, but fully encoded 29 | images with their corresponding headers (e.g. bitmap, jpg, png). The easiest way to 30 | accomplish this, is to use, for example, `encodeJpg` from `package:image`: 31 | 32 | ```dart 33 | Widget build(BuildContext context) { 34 | final image = BlurHash.decode('LEHV6nWB2yk8pyo0adR*.7kCMdnj').toImage(35, 20); 35 | return Image.memory(Uint8List.fromList(encodeJpg(image))); 36 | } 37 | ``` 38 | 39 | It is recommended to cache the resulting image in a stateful widget, such that it 40 | does not need to be recomputed on every build call. 41 | 42 | ### Encoding an Image 43 | 44 | ```dart 45 | import 'dart:io'; 46 | 47 | import 'package:blurhash_dart/blurhash_dart.dart'; 48 | import 'package:image/image.dart' as img; 49 | 50 | final data = File('path/to/image.png').readAsBytesSync(); 51 | final image = img.decodeImage(data.toList()); 52 | final blurHash = BlurHash.encode(image!, numCompX: 4, numCompY: 3); 53 | 54 | print(blurHash.hash); 55 | ``` 56 | 57 | ### Extension Methods 58 | 59 | Additional extension methods to compute, for example color averages, are available 60 | when importing `package:blurhash_dart/blurhash_extensions.dart`. See example for 61 | basic usage. 62 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | implicit-dynamic: false 7 | errors: 8 | missing_required_param: error 9 | missing_return: error 10 | todo: ignore 11 | sdk_version_async_exported_from_core: ignore 12 | 13 | linter: 14 | rules: 15 | prefer_single_quotes: true 16 | lines_longer_than_80_chars: true 17 | omit_local_variable_types: true 18 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:blurhash_dart/blurhash_dart.dart'; 4 | import 'package:blurhash_dart/blurhash_extensions.dart'; 5 | 6 | void main() { 7 | // Decode a BlurHash. 8 | const hash = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; 9 | final blurHash = BlurHash.decode(hash); 10 | 11 | // Generate an image with size 35x20. 12 | final image = blurHash.toImage(35, 20); 13 | print('The BlurHash image has size: ${image.width}x${image.height}.'); 14 | 15 | // Import 'package:blurhash_dart/blurhash_extensions.dart' 16 | // to use the utility extensions. 17 | print('The average color tone is dark: ${blurHash.isDark}'); 18 | print('The left image edge is dark: ${blurHash.isLeftEdgeDark}'); 19 | 20 | // Using the extension methods allows fast retrieval of average colors. For 21 | // example, to get the average color in a rectangular region we first define 22 | // a rectangle that is inset by twenty percent to each edge of the original 23 | // BlurHash and retrieve the average linear RGB within that region. 24 | final topLeftCorner = Point(0.2, 0.2); 25 | final bottomRightCorner = Point(0.8, 0.8); 26 | final triplet = blurHash.linearRgbInRect(topLeftCorner, bottomRightCorner); 27 | 28 | // Convert to sRGB before displaying. Values will be between [0, 255]. 29 | final color = triplet.toRgb(); 30 | print('Color(${color.r}, ${color.g}, ${color.b}'); 31 | } 32 | -------------------------------------------------------------------------------- /lib/blurhash_dart.dart: -------------------------------------------------------------------------------- 1 | export 'src/blurhash.dart'; 2 | export 'src/exception.dart'; 3 | export 'src/foundation.dart' show ColorTriplet; 4 | -------------------------------------------------------------------------------- /lib/blurhash_extensions.dart: -------------------------------------------------------------------------------- 1 | export 'src/extension.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/blurhash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:image/image.dart'; 5 | 6 | import 'encoding.dart'; 7 | import 'exception.dart'; 8 | import 'foundation.dart'; 9 | 10 | class BlurHash { 11 | /// The actual BlurHash string. 12 | final String hash; 13 | 14 | /// The decoded components of the BlurHash. 15 | /// This is mostly useful for e.g. transposing a BlurHash. 16 | final List> components; 17 | 18 | /// The number of horizontal BlurHash components. 19 | final int numCompX; 20 | 21 | /// The number of vertical BlurHash components. 22 | final int numCompY; 23 | 24 | /// Private constructor used in the actual factory constructors. 25 | /// See [BlurHash.decode] and [BlurHash.encode]. 26 | BlurHash._( 27 | this.hash, 28 | this.components, 29 | ) : assert(components.isNotEmpty), 30 | assert(components[0].isNotEmpty), 31 | numCompY = components.length, 32 | numCompX = components[0].length; 33 | 34 | /// Construct a [BlurHash] object from decoded components. 35 | /// This is useful for e.g. transposing a BlurHash. 36 | BlurHash.components(this.components) 37 | : assert(components.isNotEmpty), 38 | assert(components[0].isNotEmpty), 39 | hash = _encodeComponents(components), 40 | numCompX = components[0].length, 41 | numCompY = components.length; 42 | 43 | /// Decode a BlurHash String to a BlurHash object. 44 | /// 45 | /// The [punch] parameter adjusts the contrast on the decoded image. Values 46 | /// less than 1 will make the effect more subtle, larger values will make the 47 | /// effect stronger. This is a design parameter to adjust the look. 48 | /// 49 | /// Throws [BlurHashDecodeException] when an invalid BlurHash is encountered. 50 | factory BlurHash.decode(String blurHash, {double punch = 1.0}) { 51 | if (blurHash.length < 6) { 52 | throw BlurHashDecodeException( 53 | "BlurHash must not be null or '< 6' characters long.", 54 | ); 55 | } 56 | final sizeFlag = decode83(blurHash, 0, 1); 57 | final numCompX = (sizeFlag % 9) + 1; 58 | final numCompY = (sizeFlag ~/ 9) + 1; 59 | 60 | if (blurHash.length != 4 + 2 * numCompX * numCompY) { 61 | throw BlurHashDecodeException( 62 | 'Invalid number of components in BlurHash.', 63 | ); 64 | } 65 | 66 | final maxAcEnc = decode83(blurHash, 1, 2); 67 | final maxAc = (maxAcEnc + 1) / 166.0; 68 | final components = List.generate( 69 | numCompY, 70 | (i) => List.filled(numCompX, ColorTriplet(0, 0, 0)), 71 | ); 72 | 73 | for (var j = 0; j < numCompY; j++) { 74 | for (var i = 0; i < numCompX; i++) { 75 | if (i == 0 && j == 0) { 76 | final value = decodeDc(decode83(blurHash, 2, 6)); 77 | components[j][i] = value; 78 | } else { 79 | final index = i + j * numCompX; 80 | final value = decodeAc( 81 | decode83(blurHash, 4 + index * 2, (4 + index * 2) + 2), 82 | maxAc, 83 | ); 84 | components[j][i] = value; 85 | } 86 | } 87 | } 88 | 89 | return BlurHash._(blurHash, _multiplyPunch(components, punch)); 90 | } 91 | 92 | /// Encodes an image to a BlurHash string. 93 | /// 94 | /// The parameters [numCompX] and [numCompY] are the number of components of 95 | /// the BlurHash. Both parameters must be between 1 and 9. Throws a 96 | /// [BlurHashEncodeException] when [numCompX] and [numCompY] are not in 97 | /// the expected range. 98 | factory BlurHash.encode( 99 | Image image, { 100 | int numCompX = 4, 101 | int numCompY = 3, 102 | }) { 103 | if (numCompX < 1 || numCompX > 9 || numCompY < 1 || numCompY > 9) { 104 | throw BlurHashEncodeException( 105 | 'BlurHash components must be between 1 and 9.', 106 | ); 107 | } 108 | 109 | final img = image.convert(format: Format.uint8); 110 | final components = List.generate( 111 | numCompY, 112 | (i) => List.filled(numCompX, ColorTriplet(0, 0, 0)), 113 | ); 114 | 115 | for (var y = 0; y < numCompY; ++y) { 116 | for (var x = 0; x < numCompX; ++x) { 117 | final normalisation = (x == 0 && y == 0) ? 1.0 : 2.0; 118 | components[y][x] = _multiplyBasisFunction(img, x, y, normalisation); 119 | } 120 | } 121 | 122 | return BlurHash._(_encodeComponents(components), components); 123 | } 124 | 125 | /// Construct a [BlurHash] with a single color. 126 | /// 127 | /// The RGB values must be in range [0, 255]. 128 | factory BlurHash.fromRgb(int red, int green, int blue) { 129 | assert(red >= 0 && red <= 255); 130 | assert(green >= 0 && green <= 255); 131 | assert(blue >= 0 && blue <= 255); 132 | 133 | final color = ColorTriplet( 134 | sRgbToLinear(red), 135 | sRgbToLinear(green), 136 | sRgbToLinear(blue), 137 | ); 138 | 139 | return BlurHash.components([ 140 | [color] 141 | ]); 142 | } 143 | 144 | /// Returns the actual [BlurHash] image with the given [width] and [height]. 145 | /// 146 | /// The [width] and [height] must not be null and greater than 0. It is 147 | /// recommended to keep the [width] and [height] small and let the UI layer 148 | /// handle upscaling for better performance. 149 | Image toImage(int width, int height) { 150 | assert(width > 0); 151 | assert(height > 0); 152 | final data = _transform(width, height, components); 153 | return Image.fromBytes( 154 | width: width, 155 | height: height, 156 | bytes: data.buffer, 157 | numChannels: 4, 158 | ); 159 | } 160 | } 161 | 162 | /// Deprecated. Please use [BlurHash.decode] and [BlurHash.toImage] instead. 163 | /// Decode a BlurHash to raw pixels in RGBA32 format 164 | @Deprecated('Use [BlurHash.decode] instead.') 165 | Uint8List decodeBlurHash( 166 | String blurHash, 167 | int width, 168 | int height, { 169 | double punch = 1.0, 170 | }) { 171 | final hash = BlurHash.decode(blurHash, punch: punch); 172 | return hash.toImage(width, height).getBytes(); 173 | } 174 | 175 | /// Deprecated. Please use [BlurHash.encode] instead. 176 | /// Encodes an image to a BlurHash string 177 | @Deprecated('Use [BlurHash.encode] instead.') 178 | String encodeBlurHash( 179 | Uint8List data, 180 | int width, 181 | int height, { 182 | int numCompX = 4, 183 | int numpCompY = 3, 184 | }) { 185 | final image = Image.fromBytes( 186 | width: width, 187 | height: height, 188 | bytes: data.buffer, 189 | ); 190 | final hash = BlurHash.encode(image, numCompX: numCompX, numCompY: numpCompY); 191 | return hash.hash; 192 | } 193 | 194 | String _encodeComponents(List> components) { 195 | final numCompX = components[0].length; 196 | final numCompY = components.length; 197 | 198 | final factors = List.filled( 199 | numCompX * numCompY, 200 | ColorTriplet(0, 0, 0), 201 | ); 202 | 203 | var count = 0; 204 | for (var i = 0; i < numCompY; i++) { 205 | for (var j = 0; j < numCompX; j++) { 206 | factors[count++] = components[i][j]; 207 | } 208 | } 209 | 210 | return _encodeFactors(factors, numCompX, numCompY); 211 | } 212 | 213 | String _encodeFactors( 214 | List factors, 215 | int numCompX, 216 | int numCompY, 217 | ) { 218 | final dc = factors.first; 219 | final ac = factors.skip(1).toList(); 220 | 221 | final blurHash = StringBuffer(); 222 | final sizeFlag = (numCompX - 1) + (numCompY - 1) * 9; 223 | blurHash.write(encode83(sizeFlag, 1)); 224 | 225 | var maxVal = 1.0; 226 | if (ac.isNotEmpty) { 227 | final actualMax = ac.map(_maxChannelAbs).reduce(max); 228 | final quantisedMax = max(0, min(82, (actualMax * 166.0 - 0.5).floor())); 229 | maxVal = (quantisedMax + 1.0) / 166.0; 230 | blurHash.write(encode83(quantisedMax, 1)); 231 | } else { 232 | blurHash.write(encode83(0, 1)); 233 | } 234 | 235 | blurHash.write(encode83(encodeDc(dc), 4)); 236 | for (final factor in ac) { 237 | blurHash.write(encode83(encodeAc(factor, maxVal), 2)); 238 | } 239 | return blurHash.toString(); 240 | } 241 | 242 | double _maxChannelAbs(ColorTriplet c) { 243 | return max(c.r.abs(), max(c.g.abs(), c.b.abs())); 244 | } 245 | 246 | List> _multiplyPunch( 247 | List> components, 248 | double factor, 249 | ) { 250 | for (var i = 0; i < components.length; i++) { 251 | for (var j = 0; j < components[i].length; j++) { 252 | if (i != 0 && j != 0) { 253 | components[i][j] = components[i][j] * factor; 254 | } 255 | } 256 | } 257 | return components; 258 | } 259 | 260 | Uint8List _transform( 261 | int width, 262 | int height, 263 | List> components, 264 | ) { 265 | final pixels = List.filled(width * height * 4, 0); 266 | 267 | var pixel = 0; 268 | for (var y = 0; y < height; ++y) { 269 | for (var x = 0; x < width; ++x) { 270 | var r = 0.0; 271 | var g = 0.0; 272 | var b = 0.0; 273 | 274 | for (var j = 0; j < components.length; ++j) { 275 | for (var i = 0; i < components[0].length; ++i) { 276 | final basis = cos(pi * x * i / width) * cos(pi * y * j / height); 277 | final color = components[j][i]; 278 | r += color.r * basis; 279 | g += color.g * basis; 280 | b += color.b * basis; 281 | } 282 | } 283 | 284 | pixels[pixel++] = linearTosRgb(r); 285 | pixels[pixel++] = linearTosRgb(g); 286 | pixels[pixel++] = linearTosRgb(b); 287 | pixels[pixel++] = 255; 288 | } 289 | } 290 | 291 | return Uint8List.fromList(pixels); 292 | } 293 | 294 | ColorTriplet _multiplyBasisFunction( 295 | Image image, 296 | int x, 297 | int y, 298 | double normalisation, 299 | ) { 300 | var r = 0.0; 301 | var g = 0.0; 302 | var b = 0.0; 303 | 304 | if (image.numChannels >= 3) { 305 | for (final pixel in image) { 306 | final basis = normalisation * 307 | cos((pi * x * pixel.x) / image.width) * 308 | cos((pi * y * pixel.y) / image.height); 309 | 310 | r += basis * sRgbToLinear(pixel.r as int); 311 | g += basis * sRgbToLinear(pixel.g as int); 312 | b += basis * sRgbToLinear(pixel.b as int); 313 | } 314 | } else { 315 | for (final pixel in image) { 316 | final basis = normalisation * 317 | cos((pi * x * pixel.x) / image.width) * 318 | cos((pi * y * pixel.y) / image.height); 319 | 320 | final value = sRgbToLinear(pixel.r as int); 321 | 322 | r += basis * value; 323 | g += basis * value; 324 | b += basis * value; 325 | } 326 | } 327 | 328 | final scale = 1.0 / (image.width * image.height); 329 | return ColorTriplet(r * scale, g * scale, b * scale); 330 | } 331 | -------------------------------------------------------------------------------- /lib/src/encoding.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'exception.dart'; 4 | 5 | int decode83(String text, int from, int to) { 6 | assert(from >= 0 && to <= text.length); 7 | 8 | var result = 0; 9 | for (var i = from; i < to; ++i) { 10 | final index = _encoding[text[i]]; 11 | if (index == null) { 12 | throw BlurHashDecodeException( 13 | 'Invalid BlurHash encoding: invalid character $index', 14 | ); 15 | } 16 | result = result * 83 + index; 17 | } 18 | return result; 19 | } 20 | 21 | String encode83(int value, int length) { 22 | assert(value >= 0 && length >= 0); 23 | 24 | final buffer = StringBuffer(); 25 | final chars = _encoding.keys.toList().asMap(); 26 | for (var i = 1; i <= length; ++i) { 27 | final digit = (value / pow(83, length - i)) % 83; 28 | buffer.write(chars[digit.toInt()]); 29 | } 30 | return buffer.toString(); 31 | } 32 | 33 | const _encoding = { 34 | '0': 0, 35 | '1': 1, 36 | '2': 2, 37 | '3': 3, 38 | '4': 4, 39 | '5': 5, 40 | '6': 6, 41 | '7': 7, 42 | '8': 8, 43 | '9': 9, 44 | 'A': 10, 45 | 'B': 11, 46 | 'C': 12, 47 | 'D': 13, 48 | 'E': 14, 49 | 'F': 15, 50 | 'G': 16, 51 | 'H': 17, 52 | 'I': 18, 53 | 'J': 19, 54 | 'K': 20, 55 | 'L': 21, 56 | 'M': 22, 57 | 'N': 23, 58 | 'O': 24, 59 | 'P': 25, 60 | 'Q': 26, 61 | 'R': 27, 62 | 'S': 28, 63 | 'T': 29, 64 | 'U': 30, 65 | 'V': 31, 66 | 'W': 32, 67 | 'X': 33, 68 | 'Y': 34, 69 | 'Z': 35, 70 | 'a': 36, 71 | 'b': 37, 72 | 'c': 38, 73 | 'd': 39, 74 | 'e': 40, 75 | 'f': 41, 76 | 'g': 42, 77 | 'h': 43, 78 | 'i': 44, 79 | 'j': 45, 80 | 'k': 46, 81 | 'l': 47, 82 | 'm': 48, 83 | 'n': 49, 84 | 'o': 50, 85 | 'p': 51, 86 | 'q': 52, 87 | 'r': 53, 88 | 's': 54, 89 | 't': 55, 90 | 'u': 56, 91 | 'v': 57, 92 | 'w': 58, 93 | 'x': 59, 94 | 'y': 60, 95 | 'z': 61, 96 | '#': 62, 97 | r'$': 63, 98 | '%': 64, 99 | '*': 65, 100 | '+': 66, 101 | ',': 67, 102 | '-': 68, 103 | '.': 69, 104 | ':': 70, 105 | ';': 71, 106 | '=': 72, 107 | '?': 73, 108 | '@': 74, 109 | '[': 75, 110 | ']': 76, 111 | '^': 77, 112 | '_': 78, 113 | '{': 79, 114 | '|': 80, 115 | '}': 81, 116 | '~': 82 117 | }; 118 | -------------------------------------------------------------------------------- /lib/src/exception.dart: -------------------------------------------------------------------------------- 1 | class BlurHashDecodeException implements Exception { 2 | BlurHashDecodeException([ 3 | String? message, 4 | ]) : message = message ?? ''; 5 | 6 | final String message; 7 | 8 | @override 9 | String toString() => 'Exception: $message'; 10 | } 11 | 12 | class BlurHashEncodeException implements Exception { 13 | BlurHashEncodeException([ 14 | String? message, 15 | ]) : message = message ?? ''; 16 | 17 | final String message; 18 | 19 | @override 20 | String toString() => 'Exception: $message'; 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:blurhash_dart/blurhash_dart.dart'; 4 | import 'package:blurhash_dart/src/foundation.dart'; 5 | 6 | extension BlurHashExtensions on BlurHash { 7 | /// Transposes the [BlurHash]. 8 | BlurHash get transposed { 9 | final numCompX = components[0].length; 10 | final numCompY = components.length; 11 | final transposedComponents = List.generate( 12 | numCompX, 13 | (i) => List.filled(numCompY, ColorTriplet(0, 0, 0)), 14 | ); 15 | for (var j = 0; j < numCompY; j++) { 16 | for (var i = 0; i < numCompX; i++) { 17 | transposedComponents[i][j] = components[j][i]; 18 | } 19 | } 20 | return BlurHash.components(transposedComponents); 21 | } 22 | 23 | /// Mirrors the [BlurHash] horizontally. 24 | BlurHash get mirroredHorizontally { 25 | final numCompX = components[0].length; 26 | final numCompY = components.length; 27 | final mirroredComponents = List.generate( 28 | numCompY, 29 | (i) => List.filled(numCompX, ColorTriplet(0, 0, 0)), 30 | ); 31 | for (var j = 0; j < numCompY; j++) { 32 | for (var i = 0; i < numCompX; i++) { 33 | mirroredComponents[j][i] = components[j][i] * (i % 2 == 0 ? 1 : -1); 34 | } 35 | } 36 | return BlurHash.components(mirroredComponents); 37 | } 38 | 39 | /// Mirrors the [BlurHash] vertically. 40 | BlurHash get mirroredVertically { 41 | final numCompX = components[0].length; 42 | final numCompY = components.length; 43 | final mirroredComponents = List.generate( 44 | numCompY, 45 | (i) => List.filled(numCompX, ColorTriplet(0, 0, 0)), 46 | ); 47 | for (var j = 0; j < numCompY; j++) { 48 | for (var i = 0; i < numCompX; i++) { 49 | mirroredComponents[j][i] = components[j][i] * (j % 2 == 0 ? 1 : -1); 50 | } 51 | } 52 | return BlurHash.components(mirroredComponents); 53 | } 54 | 55 | /// Returns whether the average brightness is considered dark. 56 | /// See [BlurHashExtensions.isAverageDark] to set a custom threshold. 57 | bool get isDark => isAverageDark(); 58 | 59 | /// Returns whether the left edge is considered dark. 60 | /// See [BlurHashExtensions.isDarkAtX] to set a custom threshold. 61 | bool get isLeftEdgeDark => isDarkAtX(0.0); 62 | 63 | /// Returns whether the right edge is considered dark. 64 | /// See [BlurHashExtensions.isDarkAtX] to set a custom threshold. 65 | bool get isRightEdgeDark => isDarkAtX(1.0); 66 | 67 | /// Returns whether the top edge is considered dark. 68 | /// See [BlurHashExtensions.isDarkAtY] to set a custom threshold. 69 | bool get isTopEdgeDark => isDarkAtY(0.0); 70 | 71 | /// Returns whether the bottom edge is considered dark. 72 | /// See [BlurHashExtensions.isDarkAtY] to set a custom threshold. 73 | bool get isBottomEdgeDark => isDarkAtY(1.0); 74 | 75 | /// Returns whether the top-left corner is considered dark. 76 | /// See [BlurHashExtensions.isDarkAtPos] to set a custom threshold. 77 | bool get isTopLeftCornerDark => isDarkAtPos(0.0, 0.0); 78 | 79 | /// Returns whether the top-right corner is considered dark. 80 | /// See [BlurHashExtensions.isDarkAtPos] to set a custom threshold. 81 | bool get isTopRightCornerDark => isDarkAtPos(1.0, 0.0); 82 | 83 | /// Returns whether the bottom-left corner is considered dark. 84 | /// See [BlurHashExtensions.isDarkAtPos] to set a custom threshold. 85 | bool get isBottomLeftCornerDark => isDarkAtPos(0.0, 1.0); 86 | 87 | /// Returns whether the bottom-right corner is considered dark. 88 | /// See [BlurHashExtensions.isDarkAtPos] to set a custom threshold. 89 | bool get isBottomRightCornerDark => isDarkAtPos(1.0, 1.0); 90 | 91 | /// Returns whether the given color is considered dark. 92 | /// The color must be given as a linear RGB color. 93 | bool isColorDark(ColorTriplet color, {double threshold = 0.3}) => 94 | _getDarkness(color, threshold); 95 | 96 | /// Returns whether the average brightness is considered dark. 97 | bool isAverageDark({double? threshold}) => 98 | _getDarkness(averageLinearRgb, threshold); 99 | 100 | /// Returns whether the given row is considered dark. 101 | /// 102 | /// {@template ext_valid_args} 103 | /// Coordinates are given in percent and must be between 0 and 1. 104 | /// Throws [ArgumentError] if the coordinates are out of range. 105 | /// {@endtemplate} 106 | bool isDarkAtX(double x, {double? threshold}) => 107 | _getDarkness(linearRgbAtX(x), threshold); 108 | 109 | /// Returns whether the given row is considered dark. 110 | /// 111 | /// {@macro ext_valid_args} 112 | bool isDarkAtY(double y, {double? threshold}) => 113 | _getDarkness(linearRgbAtY(y), threshold); 114 | 115 | /// Returns whether the given point is considered dark. 116 | /// 117 | /// {@macro ext_valid_args} 118 | bool isDarkAtPos(double x, double y, {double? threshold}) => 119 | _getDarkness(linearRgbAt(x, y), threshold); 120 | 121 | /// Returns whether the given rectangular region is considered dark. 122 | /// 123 | /// {@macro ext_valid_args} 124 | bool isRectDark( 125 | Point topLeftCorner, 126 | Point bottomRightCorner, { 127 | double? threshold, 128 | }) { 129 | return _getDarkness( 130 | linearRgbInRect(topLeftCorner, bottomRightCorner), 131 | threshold, 132 | ); 133 | } 134 | 135 | /// Returns the average linear RGB. 136 | /// 137 | /// {@template linear_vs_srgb} 138 | /// [ColorTriplet] by default is in linear RGB color space. Convert to 139 | /// RGB before using the color. See [ColorTripletExtensions.toRgb]. 140 | /// {@endtemplate} 141 | ColorTriplet get averageLinearRgb => components[0][0]; 142 | 143 | /// Returns linear RGB for the given column. 144 | /// 145 | /// {@macro ext_valid_args} 146 | /// {@macro linear_vs_srgb} 147 | ColorTriplet linearRgbAtX(double x) { 148 | if (x < 0.0 || x > 1.0) { 149 | throw ArgumentError('Coordinates must be between [0, 1].'); 150 | } 151 | 152 | var i = 0; 153 | var sum = ColorTriplet(0, 0, 0); 154 | for (final component in components[0]) { 155 | sum += component * cos(pi * i++ * x); 156 | } 157 | return sum; 158 | } 159 | 160 | /// Returns linear RGB for the given row. 161 | /// 162 | /// {@macro ext_valid_args} 163 | /// {@macro linear_vs_srgb} 164 | ColorTriplet linearRgbAtY(double y) { 165 | if (y < 0.0 || y > 1.0) { 166 | throw ArgumentError('Coordinates must be between [0, 1].'); 167 | } 168 | 169 | var i = 0; 170 | var sum = ColorTriplet(0, 0, 0); 171 | for (final horizontalComponents in components) { 172 | sum += horizontalComponents[0] * cos(pi * i++ * y); 173 | } 174 | return sum; 175 | } 176 | 177 | /// Returns linear RGB for a point. 178 | /// 179 | /// {@macro ext_valid_args} 180 | /// {@macro linear_vs_srgb} 181 | ColorTriplet linearRgbAt(double x, double y) { 182 | if (x < 0.0 || x > 1.0 || y < 0.0 || y > 1.0) { 183 | throw ArgumentError('Coordinates must be between [0, 1].'); 184 | } 185 | 186 | var sum = ColorTriplet(0, 0, 0); 187 | for (var j = 0; j < numCompY; j++) { 188 | for (var i = 0; i < numCompX; i++) { 189 | sum += components[j][i] * cos(pi * i * x) * cos(pi * j * y); 190 | } 191 | } 192 | return sum; 193 | } 194 | 195 | /// Returns linear RGB for a rectangular region. 196 | /// 197 | /// {@macro ext_valid_args} 198 | /// {@macro linear_vs_srgb} 199 | ColorTriplet linearRgbInRect( 200 | Point topLeftCorner, 201 | Point bottomRightCorner, 202 | ) { 203 | if (topLeftCorner.x < 0.0 || 204 | topLeftCorner.x > 1.0 || 205 | topLeftCorner.y < 0.0 || 206 | topLeftCorner.y > 1.0) { 207 | throw ArgumentError('Coordinates must be between [0, 1].'); 208 | } 209 | 210 | if (bottomRightCorner.x < 0.0 || 211 | bottomRightCorner.x > 1.0 || 212 | bottomRightCorner.y < 0.0 || 213 | bottomRightCorner.y > 1.0) { 214 | throw ArgumentError('Coordinates must be between [0, 1].'); 215 | } 216 | 217 | if (topLeftCorner.x >= bottomRightCorner.x || 218 | topLeftCorner.y >= bottomRightCorner.y) { 219 | throw ArgumentError('The bottom-right corner must be right of ' 220 | 'and below to the top-left corner!'); 221 | } 222 | 223 | var sum = ColorTriplet(0, 0, 0); 224 | for (var j = 0; j < numCompY; j++) { 225 | for (var i = 0; i < numCompX; i++) { 226 | final horizontalAverage = i == 0 227 | ? 1.0 228 | : ((sin(pi * i * bottomRightCorner.x) - 229 | sin(pi * i * topLeftCorner.x)) / 230 | (i * pi * (bottomRightCorner.x - topLeftCorner.x))); 231 | final verticalAverage = j == 0 232 | ? 1.0 233 | : ((sin(pi * j * bottomRightCorner.y) - 234 | sin(pi * j * topLeftCorner.y)) / 235 | (j * pi * (bottomRightCorner.y - topLeftCorner.y))); 236 | sum += components[j][i] * horizontalAverage * verticalAverage; 237 | } 238 | } 239 | return sum; 240 | } 241 | 242 | static const _defaultThreshold = 0.3; 243 | 244 | static bool _getDarkness(ColorTriplet color, double? threshold) { 245 | return color.r * 0.299 + color.g * 0.587 + color.b * 0.114 < 246 | (threshold ?? _defaultThreshold); 247 | } 248 | } 249 | 250 | extension ColorTripletExtensions on ColorTriplet { 251 | /// Returns new [ColorTriplet], converted from linear RGB to sRGB. 252 | /// After conversion the color components will be between [0, 255]. 253 | ColorTriplet toRgb() { 254 | return ColorTriplet( 255 | linearTosRgb(r).toDouble(), 256 | linearTosRgb(g).toDouble(), 257 | linearTosRgb(b).toDouble(), 258 | ); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /lib/src/foundation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | /// ColorTriplet by default is used to encode colors in linear space. 4 | /// If you need the color in sRGB see [ColorTripletExtensions.toRgb]. 5 | class ColorTriplet { 6 | /// Construct a new [ColorTriplet]. 7 | const ColorTriplet(this.r, this.g, this.b); 8 | 9 | /// The red component of the color triplet. 10 | final double r; 11 | 12 | /// The green component of the color triplet. 13 | final double g; 14 | 15 | /// The blue component of the color triplet. 16 | final double b; 17 | 18 | /// Adds two [ColorTriplet] objects. 19 | ColorTriplet operator +(ColorTriplet other) => 20 | ColorTriplet(r + other.r, g + other.g, b + other.b); 21 | 22 | /// Subtracts two [ColorTriplet] objects. 23 | ColorTriplet operator -(ColorTriplet other) => 24 | ColorTriplet(r - other.r, g - other.g, b - other.b); 25 | 26 | /// Multiplies two [ColorTriplet] objects. 27 | ColorTriplet operator *(double scalar) => 28 | ColorTriplet(r * scalar, g * scalar, b * scalar); 29 | 30 | /// Divides two [ColorTriplet] objects. 31 | ColorTriplet operator /(double scalar) => 32 | ColorTriplet(r / scalar, g / scalar, b / scalar); 33 | 34 | @override 35 | String toString() => 'ColorTriplet($r, $g, $b)'; 36 | } 37 | 38 | ColorTriplet decodeDc(int value) { 39 | final r = value >> 16; 40 | final g = (value >> 8) & 255; 41 | final b = value & 255; 42 | 43 | return ColorTriplet( 44 | sRgbToLinear(r), 45 | sRgbToLinear(g), 46 | sRgbToLinear(b), 47 | ); 48 | } 49 | 50 | ColorTriplet decodeAc(int value, double maxVal) { 51 | final r = value / (19.0 * 19.0); 52 | final g = (value / 19.0) % 19.0; 53 | final b = value % 19.0; 54 | 55 | return ColorTriplet( 56 | signPow((r - 9.0) / 9.0, 2.0) * maxVal, 57 | signPow((g - 9.0) / 9.0, 2.0) * maxVal, 58 | signPow((b - 9.0) / 9.0, 2.0) * maxVal, 59 | ); 60 | } 61 | 62 | int encodeDc(ColorTriplet color) { 63 | final r = linearTosRgb(color.r); 64 | final g = linearTosRgb(color.g); 65 | final b = linearTosRgb(color.b); 66 | return (r << 16) + (g << 8) + b; 67 | } 68 | 69 | int encodeAc(ColorTriplet color, double maxVal) { 70 | final r = max(0, min(18, signPow(color.r / maxVal, 0.5) * 9 + 9.5)).floor(); 71 | final g = max(0, min(18, signPow(color.g / maxVal, 0.5) * 9 + 9.5)).floor(); 72 | final b = max(0, min(18, signPow(color.b / maxVal, 0.5) * 9 + 9.5)).floor(); 73 | return r * 19 * 19 + g * 19 + b; 74 | } 75 | 76 | double sRgbToLinear(int value) { 77 | final v = value / 255.0; 78 | if (v <= 0.04045) return v / 12.92; 79 | return pow((v + 0.055) / 1.055, 2.4).toDouble(); 80 | } 81 | 82 | int linearTosRgb(double value) { 83 | final v = value.clamp(0.0, 1.0); 84 | if (v <= 0.0031308) return (v * 12.92 * 255.0 + 0.5).toInt(); 85 | return ((1.055 * pow(v, 1.0 / 2.4) - 0.055) * 255.0 + 0.5).toInt(); 86 | } 87 | 88 | double signPow(double value, double exp) { 89 | return pow(value.abs(), exp) * value.sign; 90 | } 91 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: blurhash_dart 2 | version: 1.2.1 3 | homepage: https://github.com/justacid/blurhash-dart 4 | description: > 5 | A pure dart implementation of the BlurHash algorithm. 6 | This package provides both an encoder and a decoder, 7 | as well as some utility extensions. 8 | 9 | environment: 10 | sdk: ">=2.12.0 <3.0.0" 11 | 12 | dependencies: 13 | image: ^4.0.8 14 | 15 | dev_dependencies: 16 | lints: ^1.0.1 17 | test: ^1.16.5 18 | -------------------------------------------------------------------------------- /test/blurhash_extensions_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:blurhash_dart/blurhash_dart.dart'; 4 | import 'package:blurhash_dart/blurhash_extensions.dart'; 5 | import 'package:image/image.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('check if dark picture is dark', () { 10 | final fileData = File('test/images/darkness_test_0.png').readAsBytesSync(); 11 | final image = decodeImage(fileData); 12 | final blurHash = BlurHash.encode(image!, numCompX: 4, numCompY: 3); 13 | 14 | expect(blurHash.isDark, true); 15 | expect(blurHash.isLeftEdgeDark, true); 16 | expect(blurHash.isRightEdgeDark, true); 17 | expect(blurHash.isBottomEdgeDark, true); 18 | expect(blurHash.isTopEdgeDark, true); 19 | expect(blurHash.isTopLeftCornerDark, true); 20 | expect(blurHash.isTopRightCornerDark, true); 21 | expect(blurHash.isBottomLeftCornerDark, true); 22 | expect(blurHash.isBottomRightCornerDark, true); 23 | }); 24 | 25 | test('check if light picture is not dark', () { 26 | final fileData = File('test/images/darkness_test_1.png').readAsBytesSync(); 27 | final image = decodeImage(fileData); 28 | final blurHash = BlurHash.encode(image!, numCompX: 4, numCompY: 3); 29 | 30 | expect(blurHash.isDark, false); 31 | expect(blurHash.isLeftEdgeDark, false); 32 | expect(blurHash.isRightEdgeDark, false); 33 | expect(blurHash.isBottomEdgeDark, false); 34 | expect(blurHash.isTopEdgeDark, false); 35 | expect(blurHash.isTopLeftCornerDark, false); 36 | expect(blurHash.isTopRightCornerDark, false); 37 | expect(blurHash.isBottomLeftCornerDark, false); 38 | expect(blurHash.isBottomRightCornerDark, false); 39 | }); 40 | 41 | test('check if mixed picture is sometimes dark', () { 42 | final fileData = File('test/images/darkness_test_2.png').readAsBytesSync(); 43 | final image = decodeImage(fileData); 44 | final blurHash = BlurHash.encode(image!, numCompX: 4, numCompY: 3); 45 | 46 | expect(blurHash.isDark, false); 47 | expect(blurHash.isLeftEdgeDark, true); 48 | expect(blurHash.isRightEdgeDark, false); 49 | expect(blurHash.isBottomEdgeDark, false); 50 | expect(blurHash.isTopEdgeDark, false); 51 | expect(blurHash.isTopLeftCornerDark, true); 52 | expect(blurHash.isTopRightCornerDark, false); 53 | expect(blurHash.isBottomLeftCornerDark, true); 54 | expect(blurHash.isBottomRightCornerDark, false); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/blurhash_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: lines_longer_than_80_chars 2 | import 'dart:io'; 3 | 4 | import 'package:blurhash_dart/blurhash_dart.dart'; 5 | import 'package:image/image.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('decode a blurhash and check equality', () { 10 | const blurHash = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; 11 | 12 | final image = BlurHash.decode(blurHash).toImage(35, 20); 13 | final data = image.getBytes(); 14 | 15 | var areEqual = true; 16 | for (var i = 0; i < data.length; ++i) { 17 | if (data[i] != _decoded[i]) areEqual = false; 18 | } 19 | 20 | expect(areEqual, true); 21 | }); 22 | 23 | final hashes = [ 24 | 'LNAdApj[00aymkj[TKay9}ay-Sj[', 25 | 'LFE.@D9F01_2%L%MIVD*9Goe-;WB', 26 | 'LlMF%n00%#MwS|WCWEM{R*bbWBbH', 27 | 'LjIY5?00?bIUofWBWBM{WBofWBj[', 28 | ]; 29 | 30 | for (var i = 0; i < hashes.length; ++i) { 31 | test('encode test image "$i" and check equality with blurhash', () { 32 | final fileData = File('test/images/test$i.png').readAsBytesSync(); 33 | final image = decodeImage(fileData); 34 | final blurHash = BlurHash.encode(image!, numCompX: 4, numCompY: 3); 35 | expect(blurHash.hash, hashes[i]); 36 | }); 37 | } 38 | 39 | test('decode throws on invalid blurhashes', () { 40 | expect( 41 | () => BlurHash.decode(''), 42 | throwsA(TypeMatcher()), 43 | ); 44 | expect( 45 | () => BlurHash.decode('abcdef'), 46 | throwsA(TypeMatcher()), 47 | ); 48 | expect( 49 | () => BlurHash.decode('LEHV6nBk8'), 50 | throwsA(TypeMatcher()), 51 | ); 52 | expect( 53 | () => BlurHash.decode('LEHV6nWB2yk8pyo0adR*.7kCMdnä'), 54 | throwsA(TypeMatcher()), 55 | ); 56 | }); 57 | 58 | test('encode throws on invalid number of components', () { 59 | final fileData = File('test/images/test0.png').readAsBytesSync(); 60 | final image = decodeImage(fileData)!; 61 | expect( 62 | () => BlurHash.encode(image, numCompX: 0), 63 | throwsA(TypeMatcher()), 64 | ); 65 | expect( 66 | () => BlurHash.encode(image, numCompX: 10), 67 | throwsA(TypeMatcher()), 68 | ); 69 | expect( 70 | () => BlurHash.encode(image, numCompY: 0), 71 | throwsA(TypeMatcher()), 72 | ); 73 | expect( 74 | () => BlurHash.encode(image, numCompY: 10), 75 | throwsA(TypeMatcher()), 76 | ); 77 | }); 78 | } 79 | 80 | const _decoded = [ 81 | 145, 173, 177, 255, 146, 173, 177, 255, 147, 173, 177, 255, 148, 174, 177, 255, 82 | 151, 174, 177, 255, 153, 175, 177, 255, 156, 176, 177, 255, 159, 176, 177, 255, 83 | 162, 177, 177, 255, 166, 178, 177, 255, 169, 179, 176, 255, 172, 180, 176, 255, 84 | 174, 181, 175, 255, 177, 182, 174, 255, 179, 182, 174, 255, 180, 183, 173, 255, 85 | 181, 183, 172, 255, 182, 183, 172, 255, 182, 183, 171, 255, 181, 183, 171, 255, 86 | 180, 183, 171, 255, 178, 182, 171, 255, 176, 181, 171, 255, 174, 180, 172, 255, 87 | 171, 180, 172, 255, 168, 178, 173, 255, 164, 177, 174, 255, 161, 176, 175, 255, 88 | 157, 175, 176, 255, 153, 174, 177, 255, 150, 173, 178, 255, 147, 172, 179, 255, 89 | 145, 172, 180, 255, 143, 171, 181, 255, 142, 171, 181, 255, 145, 172, 177, 255, 90 | 145, 172, 177, 255, 146, 173, 177, 255, 148, 173, 177, 255, 150, 174, 177, 255, 91 | 153, 174, 177, 255, 155, 175, 177, 255, 159, 176, 177, 255, 162, 177, 176, 255, 92 | 165, 178, 176, 255, 168, 178, 175, 255, 171, 179, 175, 255, 174, 180, 174, 255, 93 | 176, 181, 174, 255, 178, 181, 173, 255, 180, 182, 172, 255, 181, 182, 171, 255, 94 | 181, 182, 171, 255, 181, 182, 170, 255, 181, 182, 170, 255, 179, 182, 170, 255, 95 | 178, 181, 170, 255, 176, 180, 170, 255, 173, 180, 171, 255, 170, 179, 171, 255, 96 | 167, 178, 172, 255, 164, 177, 173, 255, 160, 176, 175, 255, 157, 175, 176, 255, 97 | 153, 174, 177, 255, 150, 173, 178, 255, 147, 172, 179, 255, 144, 171, 180, 255, 98 | 143, 171, 181, 255, 141, 170, 181, 255, 143, 171, 176, 255, 144, 171, 176, 255, 99 | 145, 171, 176, 255, 146, 171, 176, 255, 148, 172, 176, 255, 151, 172, 176, 255, 100 | 154, 173, 176, 255, 157, 174, 175, 255, 160, 174, 175, 255, 164, 175, 174, 255, 101 | 167, 176, 173, 255, 170, 177, 173, 255, 172, 177, 172, 255, 175, 178, 171, 255, 102 | 176, 178, 170, 255, 178, 179, 169, 255, 179, 179, 169, 255, 179, 179, 168, 255, 103 | 179, 179, 167, 255, 179, 179, 167, 255, 178, 179, 167, 255, 176, 178, 167, 255, 104 | 174, 178, 168, 255, 171, 177, 168, 255, 169, 176, 169, 255, 165, 175, 171, 255, 105 | 162, 175, 172, 255, 159, 174, 173, 255, 155, 173, 175, 255, 152, 172, 176, 255, 106 | 148, 171, 177, 255, 146, 171, 178, 255, 143, 170, 179, 255, 142, 170, 180, 255, 107 | 140, 169, 180, 255, 141, 169, 176, 255, 141, 169, 175, 255, 142, 169, 175, 255, 108 | 144, 169, 175, 255, 146, 169, 175, 255, 148, 170, 174, 255, 151, 170, 174, 255, 109 | 155, 170, 173, 255, 158, 171, 172, 255, 161, 171, 171, 255, 164, 172, 170, 255, 110 | 167, 172, 169, 255, 170, 172, 168, 255, 172, 173, 167, 255, 174, 173, 166, 255, 111 | 175, 174, 165, 255, 176, 174, 164, 255, 177, 174, 163, 255, 176, 174, 163, 255, 112 | 176, 174, 163, 255, 175, 174, 163, 255, 173, 174, 163, 255, 171, 173, 164, 255, 113 | 169, 173, 165, 255, 166, 172, 166, 255, 163, 172, 168, 255, 160, 171, 169, 255, 114 | 156, 171, 171, 255, 153, 170, 173, 255, 149, 169, 174, 255, 146, 169, 176, 255, 115 | 144, 168, 177, 255, 141, 168, 178, 255, 140, 168, 179, 255, 139, 167, 180, 255, 116 | 137, 166, 174, 255, 138, 166, 174, 255, 139, 166, 174, 255, 140, 166, 173, 255, 117 | 143, 166, 173, 255, 145, 166, 172, 255, 148, 166, 171, 255, 151, 166, 170, 255, 118 | 155, 166, 169, 255, 158, 166, 167, 255, 161, 166, 166, 255, 164, 166, 164, 255, 119 | 166, 166, 163, 255, 168, 166, 161, 255, 170, 167, 160, 255, 172, 167, 159, 255, 120 | 172, 167, 158, 255, 173, 167, 157, 255, 173, 167, 156, 255, 172, 167, 156, 255, 121 | 171, 168, 157, 255, 169, 168, 157, 255, 167, 168, 159, 255, 165, 167, 160, 255, 122 | 162, 167, 162, 255, 159, 167, 164, 255, 156, 167, 166, 255, 153, 167, 168, 255, 123 | 150, 166, 170, 255, 147, 166, 172, 255, 144, 166, 174, 255, 141, 165, 176, 255, 124 | 139, 165, 177, 255, 138, 165, 178, 255, 137, 165, 179, 255, 134, 162, 173, 255, 125 | 134, 162, 172, 255, 135, 162, 172, 255, 137, 162, 171, 255, 139, 162, 170, 255, 126 | 141, 161, 169, 255, 144, 161, 168, 255, 148, 160, 166, 255, 151, 160, 164, 255, 127 | 154, 160, 162, 255, 157, 159, 160, 255, 160, 159, 158, 255, 162, 159, 156, 255, 128 | 164, 159, 154, 255, 166, 159, 153, 255, 167, 159, 151, 255, 168, 159, 150, 255, 129 | 169, 159, 149, 255, 168, 159, 149, 255, 168, 160, 149, 255, 167, 160, 150, 255, 130 | 165, 160, 151, 255, 163, 161, 152, 255, 161, 161, 154, 255, 158, 161, 156, 255, 131 | 155, 161, 159, 255, 153, 162, 162, 255, 149, 162, 164, 255, 146, 162, 167, 255, 132 | 144, 162, 169, 255, 141, 162, 172, 255, 139, 162, 174, 255, 137, 162, 175, 255, 133 | 135, 162, 176, 255, 134, 162, 177, 255, 130, 159, 171, 255, 130, 159, 171, 255, 134 | 131, 158, 170, 255, 133, 158, 169, 255, 135, 157, 168, 255, 138, 156, 166, 255, 135 | 140, 155, 164, 255, 144, 155, 162, 255, 147, 154, 160, 255, 150, 153, 157, 255, 136 | 153, 152, 155, 255, 156, 151, 152, 255, 158, 150, 149, 255, 160, 150, 147, 255, 137 | 162, 150, 145, 255, 163, 150, 143, 255, 164, 150, 141, 255, 164, 150, 140, 255, 138 | 164, 151, 140, 255, 163, 151, 141, 255, 162, 152, 141, 255, 161, 152, 143, 255, 139 | 159, 153, 145, 255, 157, 154, 148, 255, 154, 155, 151, 255, 151, 155, 154, 255, 140 | 149, 156, 157, 255, 146, 157, 160, 255, 143, 157, 163, 255, 140, 157, 166, 255, 141 | 138, 158, 169, 255, 136, 158, 171, 255, 134, 158, 173, 255, 133, 158, 175, 255, 142 | 132, 159, 175, 255, 126, 155, 169, 255, 126, 155, 168, 255, 127, 154, 168, 255, 143 | 129, 153, 167, 255, 131, 153, 165, 255, 134, 151, 163, 255, 137, 150, 161, 255, 144 | 140, 149, 158, 255, 143, 147, 155, 255, 146, 146, 152, 255, 149, 144, 149, 255, 145 | 152, 143, 145, 255, 154, 142, 142, 255, 156, 141, 139, 255, 158, 141, 136, 255, 146 | 159, 140, 134, 255, 160, 141, 132, 255, 160, 141, 131, 255, 160, 141, 131, 255, 147 | 159, 142, 132, 255, 158, 143, 133, 255, 157, 144, 135, 255, 155, 145, 138, 255, 148 | 153, 147, 141, 255, 150, 148, 144, 255, 148, 149, 148, 255, 145, 150, 152, 255, 149 | 142, 151, 156, 255, 140, 152, 160, 255, 137, 153, 163, 255, 135, 154, 166, 255, 150 | 133, 154, 169, 255, 131, 155, 171, 255, 130, 155, 172, 255, 129, 155, 173, 255, 151 | 123, 151, 166, 255, 123, 151, 166, 255, 124, 150, 165, 255, 126, 149, 164, 255, 152 | 128, 148, 162, 255, 131, 147, 160, 255, 133, 145, 157, 255, 137, 143, 154, 255, 153 | 140, 141, 150, 255, 143, 139, 146, 255, 146, 137, 143, 255, 148, 135, 139, 255, 154 | 151, 134, 135, 255, 153, 133, 131, 255, 154, 132, 128, 255, 156, 132, 125, 255, 155 | 156, 132, 123, 255, 157, 132, 122, 255, 156, 133, 122, 255, 156, 134, 123, 255, 156 | 155, 135, 125, 255, 153, 137, 127, 255, 151, 138, 130, 255, 149, 140, 134, 255, 157 | 147, 142, 138, 255, 145, 143, 143, 255, 142, 145, 147, 255, 140, 146, 152, 255, 158 | 137, 147, 156, 255, 135, 149, 160, 255, 133, 150, 163, 255, 131, 150, 166, 255, 159 | 129, 151, 168, 255, 128, 151, 170, 255, 127, 152, 171, 255, 120, 148, 164, 255, 160 | 121, 148, 164, 255, 122, 147, 163, 255, 123, 146, 161, 255, 126, 144, 159, 255, 161 | 128, 143, 157, 255, 131, 140, 153, 255, 134, 138, 150, 255, 137, 136, 146, 255, 162 | 140, 133, 142, 255, 143, 131, 137, 255, 146, 129, 133, 255, 148, 127, 128, 255, 163 | 150, 126, 124, 255, 152, 125, 121, 255, 153, 125, 118, 255, 154, 125, 115, 255, 164 | 154, 125, 114, 255, 154, 126, 114, 255, 153, 127, 115, 255, 152, 129, 117, 255, 165 | 151, 130, 120, 255, 149, 132, 124, 255, 147, 134, 128, 255, 145, 136, 133, 255, 166 | 143, 138, 138, 255, 140, 140, 143, 255, 138, 142, 148, 255, 135, 143, 152, 255, 167 | 133, 145, 157, 255, 131, 146, 160, 255, 129, 147, 163, 255, 128, 148, 166, 255, 168 | 126, 148, 168, 255, 126, 148, 169, 255, 119, 145, 162, 255, 120, 145, 161, 255, 169 | 121, 144, 160, 255, 122, 143, 159, 255, 124, 141, 156, 255, 127, 139, 154, 255, 170 | 130, 137, 150, 255, 133, 135, 146, 255, 136, 132, 142, 255, 139, 129, 137, 255, 171 | 142, 127, 133, 255, 145, 125, 128, 255, 147, 123, 123, 255, 149, 121, 119, 255, 172 | 151, 120, 115, 255, 152, 120, 111, 255, 153, 120, 109, 255, 153, 120, 108, 255, 173 | 153, 121, 108, 255, 152, 123, 109, 255, 151, 124, 111, 255, 150, 126, 115, 255, 174 | 148, 128, 119, 255, 147, 131, 124, 255, 144, 133, 129, 255, 142, 135, 134, 255, 175 | 140, 137, 139, 255, 137, 139, 144, 255, 135, 140, 149, 255, 132, 142, 153, 255, 176 | 130, 143, 157, 255, 128, 144, 161, 255, 127, 145, 163, 255, 126, 145, 165, 255, 177 | 125, 146, 166, 255, 120, 143, 159, 255, 120, 143, 159, 255, 121, 142, 158, 255, 178 | 122, 141, 156, 255, 125, 139, 154, 255, 127, 137, 151, 255, 130, 135, 147, 255, 179 | 133, 133, 143, 255, 136, 130, 139, 255, 139, 127, 134, 255, 142, 125, 129, 255, 180 | 144, 123, 124, 255, 147, 121, 119, 255, 149, 119, 115, 255, 151, 118, 111, 255, 181 | 152, 118, 107, 255, 153, 118, 105, 255, 153, 118, 104, 255, 153, 119, 104, 255, 182 | 153, 121, 105, 255, 152, 122, 107, 255, 151, 124, 111, 255, 149, 127, 115, 255, 183 | 147, 129, 120, 255, 145, 131, 125, 255, 143, 133, 131, 255, 140, 135, 136, 255, 184 | 138, 137, 141, 255, 135, 138, 146, 255, 133, 140, 151, 255, 131, 141, 155, 255, 185 | 129, 142, 158, 255, 127, 142, 160, 255, 126, 143, 162, 255, 125, 143, 163, 255, 186 | 121, 142, 157, 255, 121, 142, 157, 255, 122, 141, 156, 255, 124, 140, 154, 255, 187 | 126, 138, 152, 255, 128, 136, 149, 255, 131, 134, 145, 255, 134, 132, 141, 255, 188 | 137, 130, 137, 255, 140, 127, 132, 255, 143, 125, 127, 255, 145, 123, 122, 255, 189 | 148, 121, 118, 255, 150, 120, 113, 255, 152, 119, 109, 255, 153, 119, 106, 255, 190 | 154, 119, 103, 255, 155, 119, 102, 255, 155, 120, 102, 255, 155, 122, 104, 255, 191 | 154, 123, 106, 255, 153, 125, 109, 255, 151, 127, 114, 255, 150, 129, 118, 255, 192 | 147, 131, 124, 255, 145, 133, 129, 255, 142, 134, 134, 255, 140, 136, 139, 255, 193 | 137, 137, 144, 255, 134, 138, 148, 255, 132, 139, 152, 255, 130, 140, 155, 255, 194 | 128, 141, 158, 255, 126, 141, 160, 255, 126, 142, 161, 255, 124, 141, 155, 255, 195 | 124, 141, 155, 255, 125, 141, 154, 255, 126, 140, 152, 255, 128, 138, 150, 255, 196 | 131, 137, 147, 255, 133, 135, 144, 255, 136, 133, 140, 255, 139, 131, 136, 255, 197 | 142, 129, 131, 255, 145, 127, 127, 255, 148, 126, 122, 255, 150, 124, 118, 255, 198 | 152, 123, 113, 255, 154, 123, 110, 255, 156, 122, 107, 255, 157, 123, 104, 255, 199 | 158, 123, 103, 255, 158, 124, 103, 255, 158, 125, 105, 255, 157, 127, 107, 255, 200 | 156, 128, 110, 255, 155, 130, 114, 255, 153, 131, 118, 255, 151, 133, 123, 255, 201 | 148, 134, 128, 255, 145, 135, 133, 255, 142, 137, 138, 255, 139, 138, 142, 255, 202 | 137, 138, 146, 255, 134, 139, 150, 255, 131, 140, 153, 255, 129, 140, 155, 255, 203 | 128, 140, 157, 255, 127, 141, 158, 255, 127, 142, 153, 255, 127, 142, 153, 255, 204 | 128, 141, 152, 255, 129, 140, 150, 255, 131, 139, 148, 255, 133, 138, 146, 255, 205 | 136, 137, 143, 255, 139, 135, 139, 255, 142, 134, 135, 255, 145, 132, 131, 255, 206 | 148, 131, 127, 255, 150, 130, 123, 255, 153, 129, 119, 255, 155, 128, 115, 255, 207 | 158, 128, 112, 255, 159, 128, 109, 255, 161, 128, 107, 255, 162, 129, 107, 255, 208 | 162, 129, 107, 255, 162, 130, 108, 255, 161, 131, 109, 255, 160, 133, 112, 255, 209 | 159, 134, 116, 255, 157, 135, 120, 255, 155, 136, 124, 255, 152, 137, 128, 255, 210 | 149, 137, 133, 255, 146, 138, 137, 255, 143, 139, 141, 255, 139, 139, 145, 255, 211 | 136, 139, 148, 255, 134, 140, 151, 255, 132, 140, 153, 255, 130, 140, 154, 255, 212 | 129, 140, 155, 255, 131, 142, 151, 255, 131, 142, 151, 255, 132, 142, 150, 255, 213 | 133, 141, 149, 255, 135, 141, 147, 255, 137, 140, 145, 255, 139, 139, 142, 255, 214 | 142, 138, 139, 255, 145, 137, 136, 255, 148, 136, 132, 255, 151, 135, 129, 255, 215 | 154, 135, 125, 255, 156, 134, 122, 255, 159, 134, 118, 255, 161, 134, 116, 255, 216 | 163, 134, 113, 255, 164, 134, 112, 255, 166, 135, 111, 255, 166, 136, 111, 255, 217 | 166, 136, 112, 255, 166, 137, 113, 255, 165, 138, 116, 255, 163, 139, 118, 255, 218 | 161, 139, 122, 255, 159, 140, 125, 255, 156, 140, 129, 255, 153, 140, 133, 255, 219 | 150, 140, 137, 255, 146, 140, 140, 255, 143, 140, 144, 255, 139, 140, 146, 255, 220 | 136, 140, 149, 255, 134, 140, 151, 255, 132, 140, 152, 255, 131, 140, 153, 255, 221 | 134, 143, 150, 255, 134, 143, 149, 255, 135, 143, 149, 255, 136, 143, 148, 255, 222 | 138, 142, 146, 255, 140, 142, 144, 255, 142, 141, 142, 255, 145, 141, 139, 255, 223 | 148, 140, 136, 255, 151, 140, 133, 255, 154, 140, 130, 255, 157, 140, 127, 255, 224 | 160, 140, 124, 255, 162, 140, 122, 255, 165, 140, 119, 255, 167, 140, 118, 255, 225 | 168, 141, 116, 255, 169, 141, 116, 255, 170, 142, 115, 255, 170, 142, 116, 255, 226 | 170, 143, 117, 255, 169, 143, 119, 255, 167, 143, 122, 255, 165, 144, 124, 255, 227 | 163, 144, 127, 255, 160, 143, 131, 255, 157, 143, 134, 255, 153, 143, 137, 255, 228 | 149, 142, 140, 255, 146, 142, 143, 255, 142, 141, 145, 255, 139, 141, 147, 255, 229 | 136, 141, 149, 255, 134, 140, 150, 255, 133, 140, 151, 255, 137, 144, 149, 255, 230 | 137, 144, 148, 255, 138, 144, 148, 255, 139, 144, 147, 255, 141, 144, 145, 255, 231 | 143, 144, 144, 255, 145, 144, 142, 255, 148, 144, 140, 255, 151, 144, 137, 255, 232 | 154, 144, 135, 255, 157, 144, 132, 255, 160, 144, 130, 255, 162, 144, 127, 255, 233 | 165, 145, 125, 255, 168, 145, 123, 255, 170, 146, 122, 255, 172, 146, 120, 255, 234 | 173, 147, 120, 255, 174, 147, 120, 255, 174, 148, 120, 255, 174, 148, 121, 255, 235 | 173, 148, 123, 255, 171, 148, 125, 255, 169, 148, 127, 255, 166, 147, 129, 255, 236 | 163, 147, 132, 255, 160, 146, 135, 255, 156, 145, 137, 255, 152, 144, 140, 255, 237 | 148, 143, 142, 255, 145, 143, 144, 255, 141, 142, 146, 255, 138, 141, 147, 255, 238 | 136, 141, 149, 255, 135, 140, 149, 255, 140, 145, 148, 255, 140, 145, 147, 255, 239 | 140, 145, 147, 255, 142, 145, 146, 255, 143, 145, 145, 255, 145, 146, 144, 255, 240 | 147, 146, 142, 255, 150, 146, 140, 255, 153, 146, 138, 255, 156, 147, 136, 255, 241 | 159, 147, 134, 255, 162, 148, 132, 255, 165, 148, 130, 255, 168, 149, 128, 255, 242 | 170, 150, 126, 255, 172, 150, 125, 255, 174, 151, 124, 255, 176, 151, 123, 255, 243 | 176, 152, 123, 255, 177, 152, 124, 255, 176, 152, 124, 255, 176, 152, 126, 255, 244 | 174, 151, 127, 255, 172, 151, 129, 255, 169, 150, 131, 255, 166, 149, 133, 255, 245 | 163, 148, 135, 255, 159, 147, 138, 255, 155, 146, 140, 255, 150, 145, 142, 255, 246 | 147, 144, 144, 255, 143, 143, 145, 255, 140, 142, 146, 255, 137, 141, 147, 255, 247 | 136, 141, 148, 255, 141, 146, 147, 255, 141, 146, 147, 255, 142, 146, 147, 255, 248 | 143, 146, 146, 255, 145, 146, 145, 255, 147, 147, 144, 255, 149, 147, 142, 255, 249 | 151, 147, 140, 255, 154, 148, 139, 255, 157, 149, 137, 255, 160, 149, 135, 255, 250 | 163, 150, 133, 255, 166, 151, 131, 255, 169, 151, 129, 255, 172, 152, 128, 255, 251 | 174, 153, 127, 255, 176, 153, 126, 255, 177, 154, 125, 255, 178, 154, 125, 255, 252 | 178, 154, 126, 255, 178, 154, 126, 255, 177, 154, 127, 255, 176, 153, 129, 255, 253 | 174, 153, 130, 255, 171, 152, 132, 255, 168, 151, 134, 255, 164, 150, 136, 255, 254 | 160, 148, 138, 255, 156, 147, 140, 255, 152, 146, 142, 255, 148, 144, 143, 255, 255 | 144, 143, 145, 255, 141, 142, 146, 255, 138, 141, 147, 255, 137, 141, 147, 255 256 | ]; 257 | -------------------------------------------------------------------------------- /test/images/darkness_test_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/darkness_test_0.png -------------------------------------------------------------------------------- /test/images/darkness_test_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/darkness_test_1.png -------------------------------------------------------------------------------- /test/images/darkness_test_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/darkness_test_2.png -------------------------------------------------------------------------------- /test/images/test0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/test0.png -------------------------------------------------------------------------------- /test/images/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/test1.png -------------------------------------------------------------------------------- /test/images/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/test2.png -------------------------------------------------------------------------------- /test/images/test3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justacid/blurhash-dart/c8fde809e88ceb3a8f0ed92f595dd8dc9144ea16/test/images/test3.png --------------------------------------------------------------------------------