├── .gitignore ├── .gitmodules ├── Algorithm.md ├── C ├── Makefile ├── Readme.md ├── common.h ├── decode.c ├── decode.h ├── decode_stb.c ├── encode.c ├── encode.h ├── encode_stb.c ├── stb_image.h └── stb_writer.h ├── CodeOfConduct.md ├── Kotlin ├── .gitignore ├── Readme.md ├── build.gradle ├── demo │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── wolt │ │ │ └── blurhashapp │ │ │ └── MainActivity.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_blue_rounded_rect_8.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── wolt │ │ │ └── blurhashkt │ │ │ └── BlurHashDecoderTest.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── wolt │ │ └── blurhashkt │ │ └── BlurHashDecoder.kt └── settings.gradle ├── License.md ├── Media ├── BadScreenshot.png ├── GoodScreenshot.png ├── HowItWorks1.jpg ├── HowItWorks2.jpg ├── WhyBlurHash.afphoto └── WhyBlurHash.png ├── Readme.md ├── SECURITY.md ├── Swift ├── BlurHash.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── BlurHash.xcscmblueprint │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── BlurHashKit.xcscheme │ │ ├── BlurHashTest.xcscheme │ │ ├── blurhash.xcscheme │ │ └── libBlurHashKit.xcscheme ├── BlurHashDecode.swift ├── BlurHashEncode.swift ├── BlurHashKit │ ├── BlurHash.swift │ ├── ColourProbes.swift │ ├── ColourSpace.swift │ ├── EscapeSequences.swift │ ├── FromString.swift │ ├── FromUIImage.swift │ ├── Generation.swift │ ├── Info.plist │ ├── StringCoding.swift │ ├── ToString.swift │ ├── ToUIImage.swift │ └── TupleMaths.swift ├── BlurHashTest │ ├── AdvancedViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── GeneratedViewController.swift │ ├── Info.plist │ ├── SimpleViewController.swift │ ├── pic1.png │ ├── pic2.png │ ├── pic3.png │ ├── pic4.png │ ├── pic5.png │ ├── pic6.jpg │ └── pic6.png ├── License.txt └── Readme.md ├── TypeScript ├── .gitignore ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── demo │ └── index.html ├── package.json ├── src │ ├── base83.ts │ ├── decode.ts │ ├── demo.ts │ ├── encode.ts │ ├── error.ts │ ├── index.ts │ └── utils.ts ├── tsconfig.json ├── tsup.config.ts ├── webpack.config.js └── yarn.lock └── Website ├── .babelrc ├── .prettierrc ├── Readme.md ├── assets ├── fonts │ └── averta │ │ ├── 346526_0_0.eot │ │ ├── 346526_0_0.ttf │ │ ├── 346526_0_0.woff │ │ ├── 346526_0_0.woff2 │ │ ├── 346526_1_0.eot │ │ ├── 346526_1_0.ttf │ │ ├── 346526_1_0.woff │ │ ├── 346526_1_0.woff2 │ │ ├── 346526_2_0.eot │ │ ├── 346526_2_0.ttf │ │ ├── 346526_2_0.woff │ │ ├── 346526_2_0.woff2 │ │ ├── 346526_3_0.eot │ │ ├── 346526_3_0.ttf │ │ ├── 346526_3_0.woff │ │ ├── 346526_3_0.woff2 │ │ ├── 346526_4_0.eot │ │ ├── 346526_4_0.ttf │ │ ├── 346526_4_0.woff │ │ ├── 346526_4_0.woff2 │ │ ├── 346526_5_0.eot │ │ ├── 346526_5_0.ttf │ │ ├── 346526_5_0.woff │ │ ├── 346526_5_0.woff2 │ │ ├── 346526_6_0.eot │ │ ├── 346526_6_0.ttf │ │ ├── 346526_6_0.woff │ │ ├── 346526_6_0.woff2 │ │ ├── 346526_7_0.eot │ │ ├── 346526_7_0.ttf │ │ ├── 346526_7_0.woff │ │ ├── 346526_7_0.woff2 │ │ ├── 346526_8_0.eot │ │ ├── 346526_8_0.ttf │ │ ├── 346526_8_0.woff │ │ ├── 346526_8_0.woff2 │ │ ├── 346526_9_0.eot │ │ ├── 346526_9_0.ttf │ │ ├── 346526_9_0.woff │ │ ├── 346526_9_0.woff2 │ │ ├── 346526_A_0.eot │ │ ├── 346526_A_0.ttf │ │ ├── 346526_A_0.woff │ │ ├── 346526_A_0.woff2 │ │ ├── 346526_B_0.eot │ │ ├── 346526_B_0.ttf │ │ ├── 346526_B_0.woff │ │ ├── 346526_B_0.woff2 │ │ ├── 346526_C_0.eot │ │ ├── 346526_C_0.ttf │ │ ├── 346526_C_0.woff │ │ ├── 346526_C_0.woff2 │ │ ├── 346526_D_0.eot │ │ ├── 346526_D_0.ttf │ │ ├── 346526_D_0.woff │ │ ├── 346526_D_0.woff2 │ │ ├── 346526_E_0.eot │ │ ├── 346526_E_0.ttf │ │ ├── 346526_E_0.woff │ │ ├── 346526_E_0.woff2 │ │ ├── 346526_F_0.eot │ │ ├── 346526_F_0.ttf │ │ ├── 346526_F_0.woff │ │ └── 346526_F_0.woff2 ├── images │ ├── Bad_screen@2x.png │ ├── Good_screen@2x.png │ ├── arrow.svg │ ├── get-started-bg.jpg │ ├── iPhone-X-Silver.png │ ├── img1.jpg │ ├── img2.jpg │ ├── img3.jpg │ ├── img4.jpg │ └── img5.jpg └── svg │ └── wolt-logo.svg ├── deploy.sh ├── package-lock.json ├── package.json ├── src ├── constants.js ├── index.ejs ├── index.js ├── index.scss ├── sections │ ├── demo.js │ └── hero.js └── styles │ ├── averta.scss │ ├── base.scss │ ├── resets.scss │ ├── utils.scss │ └── variables.scss └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS X specific 2 | .DS_Store 3 | 4 | ## Various settings 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata/ 14 | C/blurhash_encoder 15 | C/blurhash_decoder 16 | Ruby/.* 17 | Ruby/Makefile 18 | Python/build/ 19 | *.bundle 20 | *.so 21 | *.o 22 | *.pyc 23 | 24 | # Website 25 | Website/node_modules/ 26 | Website/dist/ 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Python"] 2 | path = Python 3 | url = https://github.com/creditornot/blurhash-python.git 4 | -------------------------------------------------------------------------------- /Algorithm.md: -------------------------------------------------------------------------------- 1 | # BlurHash Algorithm 2 | 3 | ## Summary 4 | 5 | BlurHash applies a simple [DCT transform](https://en.wikipedia.org/wiki/Discrete_cosine_transform) to the image data, 6 | keeping only the first few components, and then encodes these components using a base 83 encoding, with a JSON, 7 | HTML and shell-safe character set. The DC component, which represents the average colour of the image, is stored exactly 8 | as an sRGB value, for easy use without implementing the full algorithm. The AC components are encoded lossily. 9 | 10 | ## Reference implementation 11 | 12 | [Simplified Swift decoder implementation.](Swift/BlurHashDecode.swift) 13 | 14 | [Simplified Swift encoder implementation.](Swift/BlurHashEncode.swift) 15 | 16 | ## Structure 17 | 18 | Here follows an example of a BlurHash string, with the different parts labelled: 19 | 20 | Example: LlMF%n00%#MwS|WCWEM{R*bbWBbH 21 | Legend: 12333344.................... 22 | 23 | 1. **Number of components, 1 digit.** 24 | 25 | For a BlurHash with `nx` components along the X axis and `ny` components along the Y axis, this is equal to `(nx - 1) + (ny - 1) * 9`. 26 | 27 | 2. **Maximum AC component value, 1 digit.** 28 | 29 | All AC components are scaled by this value. It represents a floating-point value of `(max + 1) / 166`. 30 | 31 | 3. **Average colour. 4 digits.** 32 | 33 | The average colour of the image in sRGB space, encoded as a 24-bit RGB value, with R in the most significant position. This value can 34 | be used directly if you only want the average colour rather than the full DCT-encoded image. 35 | 36 | 4. **AC components, 2 digits each, `nx * ny - 1` components in total.** 37 | 38 | The AC components of the DCT transform, ordered by increasing X first, then Y. They are encoded as three values for `R`, `G` and `B`, 39 | each between 0 and 18. They are combined together as `R * 19^2 + G * 19 + B`, for a total range of 0 to 6859. 40 | 41 | Each value represents a floating-point value between -1 and 1. 0-8 represent negative values, 9 represents zero, and 10-18 42 | represent positive values. Positive values are encoded as `((X - 9) / 9) ^ 2`, while negative 43 | values are encoded as `-((9 - X) / 9 ) ^ 2`. `^` represents exponentiation. This value is then multiplied by the maximum AC 44 | component value, field 2 above. 45 | 46 | ## Base 83 47 | 48 | A custom base 83 encoding is used. Values are encoded individually, using 1 to 4 digits, and concatenated together. Multiple-digit 49 | values are encoded in big-endian order, with the most significant digit first. 50 | 51 | The character used set is `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~`. 52 | 53 | ## Discrete Cosine Transform 54 | 55 | To decode a single pixel of output, you loop over the DCT components and calculate a weighted sum of cosine functions. In 56 | pseudocode, for a normalised pixel position `x`, `y`, with each coordinate ranging from 0 to 1, and components `Cij` , 57 | you calculate the following for each of R, G and B: 58 | 59 | foreach j in 0 ... ny - 1 60 | foreach i in 0 ... nx - 1 61 | value = value + Cij * cos(x * i * pi) * cos(y * j * pi) 62 | 63 | The `C00` component is the DC component, while the others are the AC components. The DC component must first be converted 64 | from sRGB to linear RGB space. AC components are already linear. 65 | 66 | Once the R, G and B values have been calculated, they must be converted from linear to your output colourspace, usually sRGB. 67 | -------------------------------------------------------------------------------- /C/Makefile: -------------------------------------------------------------------------------- 1 | PROGRAM=blurhash_encoder 2 | DECODER=blurhash_decoder 3 | $(PROGRAM): encode_stb.c encode.c encode.h stb_image.h common.h 4 | $(CC) -o $@ encode_stb.c encode.c -lm -Ofast 5 | 6 | $(DECODER): decode_stb.c decode.c decode.h stb_writer.h common.h 7 | $(CC) -o $(DECODER) decode_stb.c decode.c -lm -Ofast 8 | 9 | .PHONY: clean 10 | clean: 11 | rm -f $(PROGRAM) 12 | rm -f $(DECODER) -------------------------------------------------------------------------------- /C/Readme.md: -------------------------------------------------------------------------------- 1 | # BlurHash encoder in portable C 2 | 3 | This code implements an encoder for the BlurHash algorithm in C. It can be used to integrate into other language 4 | using an FFI interface. Currently the Python integration uses this code. 5 | 6 | ## Usage as a library 7 | 8 | Include the `encode.c` and `encode.h` files in your project. They have no external dependencies. 9 | 10 | A single file function is defined: 11 | 12 | const char *blurHashForPixels(int xComponents, int yComponents, int width, int height, uint8_t *rgb, size_t bytesPerRow) { 13 | 14 | This function returns a string containing the BlurHash. This memory is managed by the function, and you should not free it. 15 | It will be overwritten on the next call into the function, so be careful! 16 | 17 | * `xComponents` - The number of components in the X direction. Must be between 1 and 9. 3 to 5 is usually a good range for this. 18 | * `yComponents` - The number of components in the Y direction. Must be between 1 and 9. 3 to 5 is usually a good range for this. 19 | * `width` - The width in pixels of the supplied image. 20 | * `height` - The height in pixels of the supplied image. 21 | * `rgb` - A pointer to the pixel data. This is supplied in RGB order, with 3 bytes per pixels. 22 | * `bytesPerRow` - The number of bytes per row of the RGB pixel data. 23 | 24 | ## Usage as a command-line tool 25 | 26 | You can also build a command-line version to test the encoder and decoder. However, note that it uses `stb_image` to load images, 27 | which is not really security-hardened, so it is **not** recommended to use this version in production on untrusted data! 28 | Use one of the integrations instead, which use more robust image loading libraries. 29 | 30 | Nevertheless, if you want to try it out quickly, simply run: 31 | 32 | $ make blurhash_encoder 33 | $ ./blurhash_encoder 4 3 ../Swift/BlurHashTest/pic1.png 34 | LaJHjmVu8_~po#smR+a~xaoLWCRj 35 | 36 | If you want to try out the decoder, simply run: 37 | 38 | $ make blurhash_decoder 39 | $ ./blurhash_decoder "LaJHjmVu8_~po#smR+a~xaoLWCRj" 32 32 decoded_output.png 40 | -------------------------------------------------------------------------------- /C/common.h: -------------------------------------------------------------------------------- 1 | #ifndef __BLURHASH_COMMON_H__ 2 | #define __BLURHASH_COMMON_H__ 3 | 4 | #include 5 | 6 | #ifndef M_PI 7 | #define M_PI 3.14159265358979323846 8 | #endif 9 | 10 | static inline int linearTosRGB(float value) { 11 | float v = fmaxf(0, fminf(1, value)); 12 | if(v <= 0.0031308) return v * 12.92 * 255 + 0.5; 13 | else return (1.055 * powf(v, 1 / 2.4) - 0.055) * 255 + 0.5; 14 | } 15 | 16 | static inline float sRGBToLinear(int value) { 17 | float v = (float)value / 255; 18 | if(v <= 0.04045) return v / 12.92; 19 | else return powf((v + 0.055) / 1.055, 2.4); 20 | } 21 | 22 | static inline float signPow(float value, float exp) { 23 | return copysignf(powf(fabsf(value), exp), value); 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /C/decode.c: -------------------------------------------------------------------------------- 1 | #include "decode.h" 2 | #include "common.h" 3 | 4 | static char chars[83] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"; 5 | 6 | static inline uint8_t clampToUByte(int * src) { 7 | if( *src >= 0 && *src <= 255 ) 8 | return *src; 9 | return (*src < 0) ? 0 : 255; 10 | } 11 | 12 | static inline uint8_t * createByteArray(int size) { 13 | return (uint8_t *)malloc(size * sizeof(uint8_t)); 14 | } 15 | 16 | int decodeToInt(const char * string, int start, int end) { 17 | int value = 0, iter1 = 0, iter2 = 0; 18 | for( iter1 = start; iter1 < end; iter1 ++) { 19 | int index = -1; 20 | for(iter2 = 0; iter2 < 83; iter2 ++) { 21 | if (chars[iter2] == string[iter1]) { 22 | index = iter2; 23 | break; 24 | } 25 | } 26 | if (index == -1) return -1; 27 | value = value * 83 + index; 28 | } 29 | return value; 30 | } 31 | 32 | bool isValidBlurhash(const char * blurhash) { 33 | 34 | const int hashLength = strlen(blurhash); 35 | 36 | if ( !blurhash || strlen(blurhash) < 6) return false; 37 | 38 | int sizeFlag = decodeToInt(blurhash, 0, 1); //Get size from first character 39 | int numY = (int)floorf(sizeFlag / 9) + 1; 40 | int numX = (sizeFlag % 9) + 1; 41 | 42 | if (hashLength != 4 + 2 * numX * numY) return false; 43 | return true; 44 | } 45 | 46 | void decodeDC(int value, float * r, float * g, float * b) { 47 | *r = sRGBToLinear(value >> 16); // R-component 48 | *g = sRGBToLinear((value >> 8) & 255); // G-Component 49 | *b = sRGBToLinear(value & 255); // B-Component 50 | } 51 | 52 | void decodeAC(int value, float maximumValue, float * r, float * g, float * b) { 53 | int quantR = (int)floorf(value / (19 * 19)); 54 | int quantG = (int)floorf(value / 19) % 19; 55 | int quantB = (int)value % 19; 56 | 57 | *r = signPow(((float)quantR - 9) / 9, 2.0) * maximumValue; 58 | *g = signPow(((float)quantG - 9) / 9, 2.0) * maximumValue; 59 | *b = signPow(((float)quantB - 9) / 9, 2.0) * maximumValue; 60 | } 61 | 62 | int decodeToArray(const char * blurhash, int width, int height, int punch, int nChannels, uint8_t * pixelArray) { 63 | if (! isValidBlurhash(blurhash)) return -1; 64 | if (punch < 1) punch = 1; 65 | 66 | int sizeFlag = decodeToInt(blurhash, 0, 1); 67 | int numY = (int)floorf(sizeFlag / 9) + 1; 68 | int numX = (sizeFlag % 9) + 1; 69 | int iter = 0; 70 | 71 | float r = 0, g = 0, b = 0; 72 | int quantizedMaxValue = decodeToInt(blurhash, 1, 2); 73 | if (quantizedMaxValue == -1) return -1; 74 | 75 | float maxValue = ((float)(quantizedMaxValue + 1)) / 166; 76 | 77 | int colors_size = numX * numY; 78 | float colors[colors_size][3]; 79 | 80 | for(iter = 0; iter < colors_size; iter ++) { 81 | if (iter == 0) { 82 | int value = decodeToInt(blurhash, 2, 6); 83 | if (value == -1) return -1; 84 | decodeDC(value, &r, &g, &b); 85 | colors[iter][0] = r; 86 | colors[iter][1] = g; 87 | colors[iter][2] = b; 88 | 89 | } else { 90 | int value = decodeToInt(blurhash, 4 + iter * 2, 6 + iter * 2); 91 | if (value == -1) return -1; 92 | decodeAC(value, maxValue * punch, &r, &g, &b); 93 | colors[iter][0] = r; 94 | colors[iter][1] = g; 95 | colors[iter][2] = b; 96 | } 97 | } 98 | 99 | int bytesPerRow = width * nChannels; 100 | int x = 0, y = 0, i = 0, j = 0; 101 | int intR = 0, intG = 0, intB = 0; 102 | 103 | for(y = 0; y < height; y ++) { 104 | for(x = 0; x < width; x ++) { 105 | 106 | float r = 0, g = 0, b = 0; 107 | 108 | for(j = 0; j < numY; j ++) { 109 | for(i = 0; i < numX; i ++) { 110 | float basics = cos((M_PI * x * i) / width) * cos((M_PI * y * j) / height); 111 | int idx = i + j * numX; 112 | r += colors[idx][0] * basics; 113 | g += colors[idx][1] * basics; 114 | b += colors[idx][2] * basics; 115 | } 116 | } 117 | 118 | intR = linearTosRGB(r); 119 | intG = linearTosRGB(g); 120 | intB = linearTosRGB(b); 121 | 122 | pixelArray[nChannels * x + 0 + y * bytesPerRow] = clampToUByte(&intR); 123 | pixelArray[nChannels * x + 1 + y * bytesPerRow] = clampToUByte(&intG); 124 | pixelArray[nChannels * x + 2 + y * bytesPerRow] = clampToUByte(&intB); 125 | 126 | if (nChannels == 4) 127 | pixelArray[nChannels * x + 3 + y * bytesPerRow] = 255; // If nChannels=4, treat each pixel as RGBA instead of RGB 128 | 129 | } 130 | } 131 | 132 | return 0; 133 | } 134 | 135 | uint8_t * decode(const char * blurhash, int width, int height, int punch, int nChannels) { 136 | int bytesPerRow = width * nChannels; 137 | uint8_t * pixelArray = createByteArray(bytesPerRow * height); 138 | 139 | if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) 140 | return NULL; 141 | return pixelArray; 142 | } 143 | 144 | void freePixelArray(uint8_t * pixelArray) { 145 | if (pixelArray) { 146 | free(pixelArray); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /C/decode.h: -------------------------------------------------------------------------------- 1 | #ifndef __BLURHASH_DECODE_H__ 2 | 3 | #define __BLURHASH_DECODE_H__ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* 12 | decode : Returns the pixel array of the result image given the blurhash string, 13 | Parameters : 14 | blurhash : A string representing the blurhash to be decoded. 15 | width : Width of the resulting image 16 | height : Height of the resulting image 17 | punch : The factor to improve the contrast, default = 1 18 | nChannels : Number of channels in the resulting image array, 3 = RGB, 4 = RGBA 19 | Returns : A pointer to memory region where pixels are stored in (H, W, C) format 20 | */ 21 | uint8_t * decode(const char * blurhash, int width, int height, int punch, int nChannels); 22 | 23 | /* 24 | decodeToArray : Decodes the blurhash and copies the pixels to pixelArray, 25 | This method is suggested if you use an external memory allocator for pixelArray. 26 | pixelArray should be of size : width * height * nChannels 27 | Parameters : 28 | blurhash : A string representing the blurhash to be decoded. 29 | width : Width of the resulting image 30 | height : Height of the resulting image 31 | punch : The factor to improve the contrast, default = 1 32 | nChannels : Number of channels in the resulting image array, 3 = RGB, 4 = RGBA 33 | pixelArray : Pointer to memory region where pixels needs to be copied. 34 | Returns : int, -1 if error 0 if successful 35 | */ 36 | int decodeToArray(const char * blurhash, int width, int height, int punch, int nChannels, uint8_t * pixelArray); 37 | 38 | /* 39 | isValidBlurhash : Checks if the Blurhash is valid or not. 40 | Parameters : 41 | blurhash : A string representing the blurhash 42 | Returns : bool (true if it is a valid blurhash, else false) 43 | */ 44 | bool isValidBlurhash(const char * blurhash); 45 | 46 | /* 47 | freePixelArray : Frees the pixel array 48 | Parameters : 49 | pixelArray : Pixel array pointer which will be freed. 50 | Returns : void (None) 51 | */ 52 | void freePixelArray(uint8_t * pixelArray); 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /C/decode_stb.c: -------------------------------------------------------------------------------- 1 | #include "decode.h" 2 | 3 | #define STB_IMAGE_WRITE_IMPLEMENTATION 4 | #include "stb_writer.h" 5 | 6 | int main(int argc, char **argv) { 7 | if(argc < 5) { 8 | fprintf(stderr, "Usage: %s hash width height output_file [punch]\n", argv[0]); 9 | return 1; 10 | } 11 | 12 | int width, height, punch = 1; 13 | char * hash = argv[1]; 14 | width = atoi(argv[2]); 15 | height = atoi(argv[3]); 16 | char * output_file = argv[4]; 17 | 18 | const int nChannels = 4; 19 | 20 | if(argc == 6) 21 | punch = atoi(argv[5]); 22 | 23 | uint8_t * bytes = decode(hash, width, height, punch, nChannels); 24 | 25 | if (!bytes) { 26 | fprintf(stderr, "%s is not a valid blurhash, decoding failed.\n", hash); 27 | return 1; 28 | } 29 | 30 | if (stbi_write_png(output_file, width, height, nChannels, bytes, nChannels * width) == 0) { 31 | fprintf(stderr, "Failed to write PNG file %s\n", output_file); 32 | return 1; 33 | } 34 | 35 | freePixelArray(bytes); 36 | 37 | fprintf(stdout, "Decoded blurhash successfully, wrote PNG file %s\n", output_file); 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /C/encode.c: -------------------------------------------------------------------------------- 1 | #include "encode.h" 2 | #include "common.h" 3 | 4 | #include 5 | 6 | static float *multiplyBasisFunction(int xComponent, int yComponent, int width, int height, uint8_t *rgb, size_t bytesPerRow); 7 | static char *encode_int(int value, int length, char *destination); 8 | 9 | static int encodeDC(float r, float g, float b); 10 | static int encodeAC(float r, float g, float b, float maximumValue); 11 | 12 | const char *blurHashForPixels(int xComponents, int yComponents, int width, int height, uint8_t *rgb, size_t bytesPerRow) { 13 | static char buffer[2 + 4 + (9 * 9 - 1) * 2 + 1]; 14 | 15 | if(xComponents < 1 || xComponents > 9) return NULL; 16 | if(yComponents < 1 || yComponents > 9) return NULL; 17 | 18 | float factors[yComponents][xComponents][3]; 19 | memset(factors, 0, sizeof(factors)); 20 | 21 | for(int y = 0; y < yComponents; y++) { 22 | for(int x = 0; x < xComponents; x++) { 23 | float *factor = multiplyBasisFunction(x, y, width, height, rgb, bytesPerRow); 24 | factors[y][x][0] = factor[0]; 25 | factors[y][x][1] = factor[1]; 26 | factors[y][x][2] = factor[2]; 27 | } 28 | } 29 | 30 | float *dc = factors[0][0]; 31 | float *ac = dc + 3; 32 | int acCount = xComponents * yComponents - 1; 33 | char *ptr = buffer; 34 | 35 | int sizeFlag = (xComponents - 1) + (yComponents - 1) * 9; 36 | ptr = encode_int(sizeFlag, 1, ptr); 37 | 38 | float maximumValue; 39 | if(acCount > 0) { 40 | float actualMaximumValue = 0; 41 | for(int i = 0; i < acCount * 3; i++) { 42 | actualMaximumValue = fmaxf(fabsf(ac[i]), actualMaximumValue); 43 | } 44 | 45 | int quantisedMaximumValue = fmaxf(0, fminf(82, floorf(actualMaximumValue * 166 - 0.5))); 46 | maximumValue = ((float)quantisedMaximumValue + 1) / 166; 47 | ptr = encode_int(quantisedMaximumValue, 1, ptr); 48 | } else { 49 | maximumValue = 1; 50 | ptr = encode_int(0, 1, ptr); 51 | } 52 | 53 | ptr = encode_int(encodeDC(dc[0], dc[1], dc[2]), 4, ptr); 54 | 55 | for(int i = 0; i < acCount; i++) { 56 | ptr = encode_int(encodeAC(ac[i * 3 + 0], ac[i * 3 + 1], ac[i * 3 + 2], maximumValue), 2, ptr); 57 | } 58 | 59 | *ptr = 0; 60 | 61 | return buffer; 62 | } 63 | 64 | static float *multiplyBasisFunction(int xComponent, int yComponent, int width, int height, uint8_t *rgb, size_t bytesPerRow) { 65 | float r = 0, g = 0, b = 0; 66 | float normalisation = (xComponent == 0 && yComponent == 0) ? 1 : 2; 67 | 68 | for(int y = 0; y < height; y++) { 69 | for(int x = 0; x < width; x++) { 70 | float basis = cosf(M_PI * xComponent * x / width) * cosf(M_PI * yComponent * y / height); 71 | r += basis * sRGBToLinear(rgb[3 * x + 0 + y * bytesPerRow]); 72 | g += basis * sRGBToLinear(rgb[3 * x + 1 + y * bytesPerRow]); 73 | b += basis * sRGBToLinear(rgb[3 * x + 2 + y * bytesPerRow]); 74 | } 75 | } 76 | 77 | float scale = normalisation / (width * height); 78 | 79 | static float result[3]; 80 | result[0] = r * scale; 81 | result[1] = g * scale; 82 | result[2] = b * scale; 83 | 84 | return result; 85 | } 86 | 87 | 88 | 89 | static int encodeDC(float r, float g, float b) { 90 | int roundedR = linearTosRGB(r); 91 | int roundedG = linearTosRGB(g); 92 | int roundedB = linearTosRGB(b); 93 | return (roundedR << 16) + (roundedG << 8) + roundedB; 94 | } 95 | 96 | static int encodeAC(float r, float g, float b, float maximumValue) { 97 | int quantR = fmaxf(0, fminf(18, floorf(signPow(r / maximumValue, 0.5) * 9 + 9.5))); 98 | int quantG = fmaxf(0, fminf(18, floorf(signPow(g / maximumValue, 0.5) * 9 + 9.5))); 99 | int quantB = fmaxf(0, fminf(18, floorf(signPow(b / maximumValue, 0.5) * 9 + 9.5))); 100 | 101 | return quantR * 19 * 19 + quantG * 19 + quantB; 102 | } 103 | 104 | static char characters[83]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"; 105 | 106 | static char *encode_int(int value, int length, char *destination) { 107 | int divisor = 1; 108 | for(int i = 0; i < length - 1; i++) divisor *= 83; 109 | 110 | for(int i = 0; i < length; i++) { 111 | int digit = (value / divisor) % 83; 112 | divisor /= 83; 113 | *destination++ = characters[digit]; 114 | } 115 | return destination; 116 | } 117 | -------------------------------------------------------------------------------- /C/encode.h: -------------------------------------------------------------------------------- 1 | #ifndef __BLURHASH_ENCODE_H__ 2 | #define __BLURHASH_ENCODE_H__ 3 | 4 | #include 5 | #include 6 | 7 | const char *blurHashForPixels(int xComponents, int yComponents, int width, int height, uint8_t *rgb, size_t bytesPerRow); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /C/encode_stb.c: -------------------------------------------------------------------------------- 1 | #include "encode.h" 2 | 3 | #define STB_IMAGE_IMPLEMENTATION 4 | #include "stb_image.h" 5 | 6 | #include 7 | 8 | const char *blurHashForFile(int xComponents, int yComponents,const char *filename); 9 | 10 | int main(int argc, const char **argv) { 11 | if(argc != 4) { 12 | fprintf(stderr, "Usage: %s x_components y_components imagefile\n", argv[0]); 13 | return 1; 14 | } 15 | 16 | int xComponents = atoi(argv[1]); 17 | int yComponents = atoi(argv[2]); 18 | if(xComponents < 1 || xComponents > 8 || yComponents < 1 || yComponents > 8) { 19 | fprintf(stderr, "Component counts must be between 1 and 8.\n"); 20 | return 1; 21 | } 22 | 23 | const char *hash = blurHashForFile(xComponents, yComponents, argv[3]); 24 | if(!hash) { 25 | fprintf(stderr, "Failed to load image file \"%s\".\n", argv[3]); 26 | return 1; 27 | } 28 | 29 | printf("%s\n", hash); 30 | 31 | return 0; 32 | } 33 | 34 | const char *blurHashForFile(int xComponents, int yComponents,const char *filename) { 35 | int width, height, channels; 36 | unsigned char *data = stbi_load(filename, &width, &height, &channels, 3); 37 | if(!data) return NULL; 38 | 39 | const char *hash = blurHashForPixels(xComponents, yComponents, width, height, data, width * 3); 40 | 41 | stbi_image_free(data); 42 | 43 | return hash; 44 | } 45 | -------------------------------------------------------------------------------- /CodeOfConduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dag.agren@wolt.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | -------------------------------------------------------------------------------- /Kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | .idea/.workspace 29 | 30 | # http://stackoverflow.com/questions/16736856/what-should-be-in-my-gitignore-for-an-android-studio-project 31 | .gradle 32 | /local.properties 33 | /.idea/workspace.xml 34 | /.idea/libraries 35 | .DS_Store 36 | /build 37 | .idea 38 | **/*.iml 39 | *.hprof 40 | **/*.project 41 | -------------------------------------------------------------------------------- /Kotlin/Readme.md: -------------------------------------------------------------------------------- 1 | # BlurHash in Kotlin, for Android 2 | 3 | 4 | -------------------------------------------------------------------------------- /Kotlin/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | 4 | ext.kotlin_version = '1.3.72' 5 | 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.5.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | 16 | } 17 | 18 | allprojects { 19 | 20 | repositories { 21 | google() 22 | jcenter() 23 | } 24 | 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /Kotlin/demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | 7 | compileSdkVersion 29 8 | 9 | defaultConfig { 10 | applicationId "com.wolt.blurhash" 11 | minSdkVersion 14 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | } 25 | 26 | dependencies { 27 | implementation project(path: ':lib') 28 | implementation 'androidx.appcompat:appcompat:1.1.0' 29 | } 30 | -------------------------------------------------------------------------------- /Kotlin/demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.wolt.blurhashapp 2 | 3 | import android.graphics.Bitmap 4 | import android.os.Bundle 5 | import android.os.SystemClock 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.wolt.blurhashkt.BlurHashDecoder 8 | import kotlinx.android.synthetic.main.activity_main.* 9 | 10 | class MainActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_main) 15 | tvDecode.setOnClickListener { 16 | var bitmap: Bitmap? = null 17 | val time = timed { 18 | bitmap = BlurHashDecoder.decode(etInput.text.toString(), 20, 12) 19 | } 20 | ivResult.setImageBitmap(bitmap) 21 | ivResultTime.text = "Time: $time ms" 22 | } 23 | } 24 | 25 | } 26 | 27 | /** 28 | * Executes a function and return the time spent in milliseconds. 29 | */ 30 | private inline fun timed(function: () -> Unit): Long { 31 | val start = SystemClock.elapsedRealtime() 32 | function() 33 | return SystemClock.elapsedRealtime() - start 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/drawable/bg_blue_rounded_rect_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 36 | 37 | 43 | 44 | 53 | 54 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #29b6f6 4 | #0086c3 5 | #444444 6 | 7 | 8 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BlurHash 3 | BlurHash string 4 | Decode! 5 | 6 | -------------------------------------------------------------------------------- /Kotlin/demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Kotlin/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /Kotlin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Kotlin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Kotlin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 01 10:02:38 EEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /Kotlin/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /Kotlin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /Kotlin/lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | 5 | android { 6 | 7 | compileSdkVersion 29 8 | 9 | defaultConfig { 10 | minSdkVersion 14 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 30 | androidTestImplementation 'junit:junit:4.13' 31 | androidTestImplementation 'androidx.test:runner:1.2.0' 32 | } 33 | -------------------------------------------------------------------------------- /Kotlin/lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /Kotlin/lib/src/androidTest/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt: -------------------------------------------------------------------------------- 1 | package com.wolt.blurhashkt 2 | 3 | import android.graphics.Bitmap 4 | import com.wolt.blurhashkt.BlurHashDecoder.clearCache 5 | import com.wolt.blurhashkt.BlurHashDecoder.decode 6 | import junit.framework.Assert.assertTrue 7 | import org.junit.Before 8 | import org.junit.Test 9 | import java.nio.ByteBuffer 10 | import java.util.* 11 | 12 | 13 | class BlurHashDecoderTest { 14 | @Before 15 | @Throws(Exception::class) 16 | fun setUp() { 17 | clearCache() 18 | } 19 | 20 | @Test 21 | fun decode_smallImage_cacheEnabled_shouldGetTheSameBitmapInManyRequests() { 22 | val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! 23 | val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! 24 | val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! 25 | 26 | bmp1.assertEquals(bmp2) 27 | bmp2.assertEquals(bmp3) 28 | } 29 | 30 | @Test 31 | fun decode_smallImage_differentCache_shouldGetTheSameBitmapInManyRequests() { 32 | val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! 33 | val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! 34 | val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12)!! 35 | 36 | bmp1.assertEquals(bmp2) 37 | bmp2.assertEquals(bmp3) 38 | } 39 | 40 | @Test 41 | fun decode_smallImage_cacheDisabled_shouldGetTheSameBitmapInManyRequests() { 42 | val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! 43 | val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! 44 | val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 20, 12, useCache = false)!! 45 | 46 | bmp1.assertEquals(bmp2) 47 | bmp2.assertEquals(bmp3) 48 | } 49 | 50 | @Test 51 | fun decode_bigImage_cacheEnabled_shouldGetTheSameBitmapInManyRequests() { 52 | val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! 53 | val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! 54 | val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! 55 | 56 | bmp1.assertEquals(bmp2) 57 | bmp2.assertEquals(bmp3) 58 | } 59 | 60 | @Test 61 | fun decode_bigImage_differentCache_shouldGetTheSameBitmapInManyRequests() { 62 | val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! 63 | val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! 64 | val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100)!! 65 | 66 | bmp1.assertEquals(bmp2) 67 | bmp2.assertEquals(bmp3) 68 | } 69 | 70 | @Test 71 | fun decode_bigImage_cacheDisabled_shouldGetTheSameBitmapInManyRequests() { 72 | val bmp1 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! 73 | val bmp2 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! 74 | val bmp3 = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 100, 100, useCache = false)!! 75 | 76 | bmp1.assertEquals(bmp2) 77 | bmp2.assertEquals(bmp3) 78 | } 79 | } 80 | 81 | fun Bitmap.assertEquals(bitmap2: Bitmap) { 82 | val buffer1: ByteBuffer = ByteBuffer.allocate(height * rowBytes) 83 | copyPixelsToBuffer(buffer1) 84 | val buffer2: ByteBuffer = ByteBuffer.allocate(bitmap2.height * bitmap2.rowBytes) 85 | bitmap2.copyPixelsToBuffer(buffer2) 86 | val equals = Arrays.equals(buffer1.array(), buffer2.array()) 87 | assertTrue(equals) 88 | } 89 | -------------------------------------------------------------------------------- /Kotlin/lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Kotlin/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo', ':lib' 2 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wolt Enterprises 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 | -------------------------------------------------------------------------------- /Media/BadScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Media/BadScreenshot.png -------------------------------------------------------------------------------- /Media/GoodScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Media/GoodScreenshot.png -------------------------------------------------------------------------------- /Media/HowItWorks1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Media/HowItWorks1.jpg -------------------------------------------------------------------------------- /Media/HowItWorks2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Media/HowItWorks2.jpg -------------------------------------------------------------------------------- /Media/WhyBlurHash.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Media/WhyBlurHash.afphoto -------------------------------------------------------------------------------- /Media/WhyBlurHash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Media/WhyBlurHash.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report security issues to `security@wolt.com` -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/project.xcworkspace/xcshareddata/BlurHash.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "8D94AD06B7E57997939C2DBED75F327A184CF7B8", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "8D94AD06B7E57997939C2DBED75F327A184CF7B8" : 9223372036854775807 8 | }, 9 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "78AF9E08-7F50-4648-8F5B-F7536E5EB55E", 10 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 11 | "8D94AD06B7E57997939C2DBED75F327A184CF7B8" : "BlurHash\/" 12 | }, 13 | "DVTSourceControlWorkspaceBlueprintNameKey" : "BlurHash", 14 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 15 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "BlurHash.xcodeproj", 16 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 17 | { 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/creditornot\/BlurHash.git", 19 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8D94AD06B7E57997939C2DBED75F327A184CF7B8" 21 | }, 22 | { 23 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/creditornot\/BlurHash.git", 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8D94AD06B7E57997939C2DBED75F327A184CF7B8" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/xcshareddata/xcschemes/BlurHashKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/xcshareddata/xcschemes/BlurHashTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/xcshareddata/xcschemes/blurhash.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Swift/BlurHash.xcodeproj/xcshareddata/xcschemes/libBlurHashKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Swift/BlurHashDecode.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIImage { 4 | public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { 5 | guard blurHash.count >= 6 else { return nil } 6 | 7 | let sizeFlag = String(blurHash[0]).decode83() 8 | let numY = (sizeFlag / 9) + 1 9 | let numX = (sizeFlag % 9) + 1 10 | 11 | let quantisedMaximumValue = String(blurHash[1]).decode83() 12 | let maximumValue = Float(quantisedMaximumValue + 1) / 166 13 | 14 | guard blurHash.count == 4 + 2 * numX * numY else { return nil } 15 | 16 | let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in 17 | if i == 0 { 18 | let value = String(blurHash[2 ..< 6]).decode83() 19 | return decodeDC(value) 20 | } else { 21 | let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() 22 | return decodeAC(value, maximumValue: maximumValue * punch) 23 | } 24 | } 25 | 26 | let width = Int(size.width) 27 | let height = Int(size.height) 28 | let bytesPerRow = width * 3 29 | guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } 30 | CFDataSetLength(data, bytesPerRow * height) 31 | guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } 32 | 33 | for y in 0 ..< height { 34 | for x in 0 ..< width { 35 | var r: Float = 0 36 | var g: Float = 0 37 | var b: Float = 0 38 | 39 | for j in 0 ..< numY { 40 | for i in 0 ..< numX { 41 | let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) 42 | let colour = colours[i + j * numX] 43 | r += colour.0 * basis 44 | g += colour.1 * basis 45 | b += colour.2 * basis 46 | } 47 | } 48 | 49 | let intR = UInt8(linearTosRGB(r)) 50 | let intG = UInt8(linearTosRGB(g)) 51 | let intB = UInt8(linearTosRGB(b)) 52 | 53 | pixels[3 * x + 0 + y * bytesPerRow] = intR 54 | pixels[3 * x + 1 + y * bytesPerRow] = intG 55 | pixels[3 * x + 2 + y * bytesPerRow] = intB 56 | } 57 | } 58 | 59 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) 60 | 61 | guard let provider = CGDataProvider(data: data) else { return nil } 62 | guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, 63 | space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } 64 | 65 | self.init(cgImage: cgImage) 66 | } 67 | } 68 | 69 | private func decodeDC(_ value: Int) -> (Float, Float, Float) { 70 | let intR = value >> 16 71 | let intG = (value >> 8) & 255 72 | let intB = value & 255 73 | return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) 74 | } 75 | 76 | private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { 77 | let quantR = value / (19 * 19) 78 | let quantG = (value / 19) % 19 79 | let quantB = value % 19 80 | 81 | let rgb = ( 82 | signPow((Float(quantR) - 9) / 9, 2) * maximumValue, 83 | signPow((Float(quantG) - 9) / 9, 2) * maximumValue, 84 | signPow((Float(quantB) - 9) / 9, 2) * maximumValue 85 | ) 86 | 87 | return rgb 88 | } 89 | 90 | private func signPow(_ value: Float, _ exp: Float) -> Float { 91 | return copysign(pow(abs(value), exp), value) 92 | } 93 | 94 | private func linearTosRGB(_ value: Float) -> Int { 95 | let v = max(0, min(1, value)) 96 | if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } 97 | else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } 98 | } 99 | 100 | private func sRGBToLinear(_ value: Type) -> Float { 101 | let v = Float(Int64(value)) / 255 102 | if v <= 0.04045 { return v / 12.92 } 103 | else { return pow((v + 0.055) / 1.055, 2.4) } 104 | } 105 | 106 | private let encodeCharacters: [String] = { 107 | return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } 108 | }() 109 | 110 | private let decodeCharacters: [String: Int] = { 111 | var dict: [String: Int] = [:] 112 | for (index, character) in encodeCharacters.enumerated() { 113 | dict[character] = index 114 | } 115 | return dict 116 | }() 117 | 118 | extension String { 119 | func decode83() -> Int { 120 | var value: Int = 0 121 | for character in self { 122 | if let digit = decodeCharacters[String(character)] { 123 | value = value * 83 + digit 124 | } 125 | } 126 | return value 127 | } 128 | } 129 | 130 | private extension String { 131 | subscript (offset: Int) -> Character { 132 | return self[index(startIndex, offsetBy: offset)] 133 | } 134 | 135 | subscript (bounds: CountableClosedRange) -> Substring { 136 | let start = index(startIndex, offsetBy: bounds.lowerBound) 137 | let end = index(startIndex, offsetBy: bounds.upperBound) 138 | return self[start...end] 139 | } 140 | 141 | subscript (bounds: CountableRange) -> Substring { 142 | let start = index(startIndex, offsetBy: bounds.lowerBound) 143 | let end = index(startIndex, offsetBy: bounds.upperBound) 144 | return self[start.. String? { 5 | let pixelWidth = Int(round(size.width * scale)) 6 | let pixelHeight = Int(round(size.height * scale)) 7 | 8 | let context = CGContext( 9 | data: nil, 10 | width: pixelWidth, 11 | height: pixelHeight, 12 | bitsPerComponent: 8, 13 | bytesPerRow: pixelWidth * 4, 14 | space: CGColorSpace(name: CGColorSpace.sRGB)!, 15 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 16 | )! 17 | context.scaleBy(x: scale, y: -scale) 18 | context.translateBy(x: 0, y: -size.height) 19 | 20 | UIGraphicsPushContext(context) 21 | draw(at: .zero) 22 | UIGraphicsPopContext() 23 | 24 | guard let cgImage = context.makeImage(), 25 | let dataProvider = cgImage.dataProvider, 26 | let data = dataProvider.data, 27 | let pixels = CFDataGetBytePtr(data) else { 28 | assertionFailure("Unexpected error!") 29 | return nil 30 | } 31 | 32 | let width = cgImage.width 33 | let height = cgImage.height 34 | let bytesPerRow = cgImage.bytesPerRow 35 | 36 | var factors: [(Float, Float, Float)] = [] 37 | for y in 0 ..< components.1 { 38 | for x in 0 ..< components.0 { 39 | let normalisation: Float = (x == 0 && y == 0) ? 1 : 2 40 | let factor = multiplyBasisFunction(pixels: pixels, width: width, height: height, bytesPerRow: bytesPerRow, bytesPerPixel: cgImage.bitsPerPixel / 8, pixelOffset: 0) { 41 | normalisation * cos(Float.pi * Float(x) * $0 / Float(width)) as Float * cos(Float.pi * Float(y) * $1 / Float(height)) as Float 42 | } 43 | factors.append(factor) 44 | } 45 | } 46 | 47 | let dc = factors.first! 48 | let ac = factors.dropFirst() 49 | 50 | var hash = "" 51 | 52 | let sizeFlag = (components.0 - 1) + (components.1 - 1) * 9 53 | hash += sizeFlag.encode83(length: 1) 54 | 55 | let maximumValue: Float 56 | if ac.count > 0 { 57 | let actualMaximumValue = ac.map({ max(abs($0.0), abs($0.1), abs($0.2)) }).max()! 58 | let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5)))) 59 | maximumValue = Float(quantisedMaximumValue + 1) / 166 60 | hash += quantisedMaximumValue.encode83(length: 1) 61 | } else { 62 | maximumValue = 1 63 | hash += 0.encode83(length: 1) 64 | } 65 | 66 | hash += encodeDC(dc).encode83(length: 4) 67 | 68 | for factor in ac { 69 | hash += encodeAC(factor, maximumValue: maximumValue).encode83(length: 2) 70 | } 71 | 72 | return hash 73 | } 74 | 75 | private func multiplyBasisFunction(pixels: UnsafePointer, width: Int, height: Int, bytesPerRow: Int, bytesPerPixel: Int, pixelOffset: Int, basisFunction: (Float, Float) -> Float) -> (Float, Float, Float) { 76 | var r: Float = 0 77 | var g: Float = 0 78 | var b: Float = 0 79 | 80 | let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow) 81 | 82 | for x in 0 ..< width { 83 | for y in 0 ..< height { 84 | let basis = basisFunction(Float(x), Float(y)) 85 | r += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 0 + y * bytesPerRow]) 86 | g += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 1 + y * bytesPerRow]) 87 | b += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 2 + y * bytesPerRow]) 88 | } 89 | } 90 | 91 | let scale = 1 / Float(width * height) 92 | 93 | return (r * scale, g * scale, b * scale) 94 | } 95 | } 96 | 97 | private func encodeDC(_ value: (Float, Float, Float)) -> Int { 98 | let roundedR = linearTosRGB(value.0) 99 | let roundedG = linearTosRGB(value.1) 100 | let roundedB = linearTosRGB(value.2) 101 | return (roundedR << 16) + (roundedG << 8) + roundedB 102 | } 103 | 104 | private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int { 105 | let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5)))) 106 | let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5)))) 107 | let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5)))) 108 | 109 | return quantR * 19 * 19 + quantG * 19 + quantB 110 | } 111 | 112 | private func signPow(_ value: Float, _ exp: Float) -> Float { 113 | return copysign(pow(abs(value), exp), value) 114 | } 115 | 116 | private func linearTosRGB(_ value: Float) -> Int { 117 | let v = max(0, min(1, value)) 118 | if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } 119 | else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } 120 | } 121 | 122 | private func sRGBToLinear(_ value: Type) -> Float { 123 | let v = Float(Int64(value)) / 255 124 | if v <= 0.04045 { return v / 12.92 } 125 | else { return pow((v + 0.055) / 1.055, 2.4) } 126 | } 127 | 128 | private let encodeCharacters: [String] = { 129 | return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } 130 | }() 131 | 132 | extension BinaryInteger { 133 | func encode83(length: Int) -> String { 134 | var result = "" 135 | for i in 1 ... length { 136 | let digit = (Int(self) / pow(83, length - i)) % 83 137 | result += encodeCharacters[Int(digit)] 138 | } 139 | return result 140 | } 141 | } 142 | 143 | private func pow(_ base: Int, _ exponent: Int) -> Int { 144 | return (0 ..< exponent).reduce(1) { value, _ in value * base } 145 | } 146 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/BlurHash.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct BlurHash { 4 | public let components: [[(Float, Float, Float)]] 5 | 6 | public var numberOfHorizontalComponents: Int { return components.first!.count } 7 | public var numberOfVerticalComponents: Int { return components.count } 8 | 9 | public init(components: [[(Float, Float, Float)]]) { 10 | self.components = components 11 | } 12 | 13 | public func punch(_ factor: Float) -> BlurHash { 14 | return BlurHash(components: components.enumerated().map { (j, horizontalComponents) -> [(Float, Float, Float)] in 15 | return horizontalComponents.enumerated().map { (i, component) -> (Float, Float, Float) in 16 | if i == 0 && j == 0 { 17 | return component 18 | } else { 19 | return component * factor 20 | } 21 | } 22 | }) 23 | } 24 | } 25 | 26 | public func +(lhs: BlurHash, rhs: BlurHash) throws -> BlurHash { 27 | return BlurHash(components: paddedZip(lhs.components, rhs.components, [], []).map { 28 | paddedZip($0.0, $0.1, (0, 0, 0) as (Float, Float, Float), (0, 0, 0) as (Float, Float, Float)).map { ($0.0.0 + $0.1.0, $0.0.1 + $0.1.1, $0.0.2 + $0.1.2) } 29 | }) 30 | } 31 | 32 | public func -(lhs: BlurHash, rhs: BlurHash) throws -> BlurHash { 33 | return BlurHash(components: paddedZip(lhs.components, rhs.components, [], []).map { 34 | paddedZip($0.0, $0.1, (0, 0, 0) as (Float, Float, Float), (0, 0, 0) as (Float, Float, Float)).map { ($0.0.0 - $0.1.0, $0.0.1 - $0.1.1, $0.0.2 - $0.1.2) } 35 | }) 36 | } 37 | 38 | private func paddedZip(_ collection1: Collection1, _ collection2: Collection2, _ padding1: Collection1.Element, _ padding2: Collection2.Element) -> Zip2Sequence<[Collection1.Element], [Collection2.Element]> where Collection1: Collection, Collection2: Collection { 39 | if collection1.count < collection2.count { 40 | let padded = collection1 + Array(repeating: padding1, count: collection2.count - collection1.count) 41 | return zip(padded, Array(collection2)) 42 | } else if collection2.count < collection1.count { 43 | let padded = collection2 + Array(repeating: padding2, count: collection1.count - collection2.count) 44 | return zip(Array(collection1), padded) 45 | } else { 46 | return zip(Array(collection1), Array(collection2)) 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/ColourProbes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension BlurHash { 4 | public func linearRGB(atX x: Float) -> (Float, Float, Float) { 5 | return components[0].enumerated().reduce((0, 0, 0)) { (sum, horizontalEnumerated) -> (Float, Float, Float) in 6 | let (i, component) = horizontalEnumerated 7 | return sum + component * cos(Float.pi * Float(i) * x) 8 | } 9 | } 10 | 11 | public func linearRGB(atY y: Float) -> (Float, Float, Float) { 12 | return components.enumerated().reduce((0, 0, 0)) { (sum, verticalEnumerated) in 13 | let (j, horizontalComponents) = verticalEnumerated 14 | return sum + horizontalComponents[0] * cos(Float.pi * Float(j) * y) 15 | } 16 | } 17 | 18 | public func linearRGB(at position: (Float, Float)) -> (Float, Float, Float) { 19 | return components.enumerated().reduce((0, 0, 0)) { (sum, verticalEnumerated) in 20 | let (j, horizontalComponents) = verticalEnumerated 21 | return horizontalComponents.enumerated().reduce(sum) { (sum, horizontalEnumerated) in 22 | let (i, component) = horizontalEnumerated 23 | return sum + component * cos(Float.pi * Float(i) * position.0) * cos(Float.pi * Float(j) * position.1) 24 | } 25 | } 26 | } 27 | 28 | public func linearRGB(from upperLeft: (Float, Float), to lowerRight: (Float, Float)) -> (Float, Float, Float) { 29 | return components.enumerated().reduce((0, 0, 0)) { (sum, verticalEnumerated) in 30 | let (j, horizontalComponents) = verticalEnumerated 31 | return horizontalComponents.enumerated().reduce(sum) { (sum, horizontalEnumerated) in 32 | let (i, component) = horizontalEnumerated 33 | let horizontalAverage: Float = i == 0 ? 1 : (sin(Float.pi * Float(i) * lowerRight.0) - sin(Float.pi * Float(i) * upperLeft.0)) / (Float(i) * Float.pi * (lowerRight.0 - upperLeft.0)) 34 | let veritcalAverage: Float = j == 0 ? 1 : (sin(Float.pi * Float(j) * lowerRight.1) - sin(Float.pi * Float(j) * upperLeft.1)) / (Float(j) * Float.pi * (lowerRight.1 - upperLeft.1)) 35 | return sum + component * horizontalAverage * veritcalAverage 36 | } 37 | } 38 | } 39 | 40 | public func linearRGB(at upperLeft: (Float, Float), size: (Float, Float)) -> (Float, Float, Float) { 41 | return linearRGB(from: upperLeft, to: (upperLeft.0 + size.0, upperLeft.1 + size.1)) 42 | } 43 | 44 | public var averageLinearRGB: (Float, Float, Float) { 45 | return components[0][0] 46 | } 47 | 48 | public var leftEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atX: 0) } 49 | public var rightEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atX: 1) } 50 | public var topEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atY: 0) } 51 | public var bottomEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atY: 1) } 52 | public var topLeftCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (0, 0)) } 53 | public var topRightCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (1, 0)) } 54 | public var bottomLeftCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (0, 1)) } 55 | public var bottomRightCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (1, 1)) } 56 | } 57 | 58 | extension BlurHash { 59 | public func isDark(linearRGB rgb: (Float, Float, Float), threshold: Float = 0.3) -> Bool { 60 | return rgb.0 * 0.299 + rgb.1 * 0.587 + rgb.2 * 0.114 < threshold 61 | } 62 | 63 | public func isDark(threshold: Float = 0.3) -> Bool { return isDark(linearRGB: averageLinearRGB, threshold: threshold) } 64 | 65 | public func isDark(atX x: Float, threshold: Float = 0.3) -> Bool { return isDark(linearRGB: linearRGB(atX: x), threshold: threshold) } 66 | public func isDark(atY y: Float, threshold: Float = 0.3) -> Bool { return isDark(linearRGB: linearRGB(atY: y), threshold: threshold) } 67 | public func isDark(at position: (Float, Float), threshold: Float = 0.3) -> Bool { return isDark(linearRGB: linearRGB(at: position), threshold: threshold) } 68 | public func isDark(from upperLeft: (Float, Float), to lowerRight: (Float, Float), threshold: Float = 0.3) -> Bool { return isDark(linearRGB: linearRGB(from: upperLeft, to: lowerRight), threshold: threshold) } 69 | public func isDark(at upperLeft: (Float, Float), size: (Float, Float), threshold: Float = 0.3) -> Bool { return isDark(linearRGB: linearRGB(at: upperLeft, size: size), threshold: threshold) } 70 | 71 | public var isLeftEdgeDark: Bool { return isDark(atX: 0) } 72 | public var isRightEdgeDark: Bool { return isDark(atX: 1) } 73 | public var isTopEdgeDark: Bool { return isDark(atY: 0) } 74 | public var isBottomEdgeDark: Bool { return isDark(atY: 1) } 75 | public var isTopLeftCornerDark: Bool { return isDark(at: (0, 0)) } 76 | public var isTopRightCornerDark: Bool { return isDark(at: (1, 0)) } 77 | public var isBottomLeftCornerDark: Bool { return isDark(at: (0, 1)) } 78 | public var isBottomRightCornerDark: Bool { return isDark(at: (1, 1)) } 79 | } 80 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/ColourSpace.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func signPow(_ value: Float, _ exp: Float) -> Float { 4 | return copysign(pow(abs(value), exp), value) 5 | } 6 | 7 | func linearTosRGB(_ value: Float) -> Int { 8 | let v = max(0, min(1, value)) 9 | if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } 10 | else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } 11 | } 12 | 13 | func sRGBToLinear(_ value: Type) -> Float { 14 | let v = Float(Int64(value)) / 255 15 | if v <= 0.04045 { return v / 12.92 } 16 | else { return pow((v + 0.055) / 1.055, 2.4) } 17 | } 18 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/EscapeSequences.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension BlurHash { 4 | var twoByThreeEscapeSequence: String { 5 | let areas: [(from: (Float, Float), to: (Float, Float))] = [ 6 | (from: (0, 0), to: (0.333, 0.5)), 7 | (from: (0, 0.5), to: (0.333, 1.0)), 8 | (from: (0.333, 0), to: (0.666, 0.5)), 9 | (from: (0.333, 0.5), to: (0.666, 1.0)), 10 | (from: (0.666, 0), to: (1.0, 0.5)), 11 | (from: (0.666, 0.5), to: (1.0, 1.0)), 12 | ] 13 | 14 | let rgb: [(Float, Float, Float)] = areas.map { area in 15 | linearRGB(from: area.from, to: area.to) 16 | } 17 | 18 | let maxRgb: (Float, Float, Float) = rgb.reduce((-Float.infinity, -Float.infinity, -Float.infinity), max) 19 | let minRgb: (Float, Float, Float) = rgb.reduce((Float.infinity, Float.infinity, Float.infinity), min) 20 | 21 | let positiveScale: (Float, Float, Float) = ((1, 1, 1) - averageLinearRGB) / (maxRgb - averageLinearRGB) 22 | let negativeScale: (Float, Float, Float) = averageLinearRGB / (averageLinearRGB - minRgb) 23 | let scale: (Float, Float, Float) = min(positiveScale, negativeScale) 24 | 25 | let scaledRgb: [(Float, Float, Float)] = rgb.map { rgb in 26 | return (rgb - averageLinearRGB) * scale + averageLinearRGB 27 | } 28 | 29 | let c = scaledRgb.map { rgb in 30 | return (linearTosRGB(rgb.0) / 51) * 36 + (linearTosRGB(rgb.1) / 51) * 6 + (linearTosRGB(rgb.2) / 51) + 16 31 | } 32 | 33 | return "\u{1b}[38;5;\(c[1]);48;5;\(c[0])m▄\u{1b}[38;5;\(c[3]);48;5;\(c[2])m▄\u{1b}[38;5;\(c[5]);48;5;\(c[4])m▄\u{1b}[m" 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/FromString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension BlurHash { 4 | init?(string: String) { 5 | guard string.count >= 6 else { return nil } 6 | 7 | let sizeFlag = String(string[0]).decode83() 8 | let numberOfHorizontalComponents = (sizeFlag % 9) + 1 9 | let numberOfVerticalComponents = (sizeFlag / 9) + 1 10 | 11 | let quantisedMaximumValue = String(string[1]).decode83() 12 | let maximumValue = Float(quantisedMaximumValue + 1) / 166 13 | 14 | guard string.count == 4 + 2 * numberOfHorizontalComponents * numberOfVerticalComponents else { return nil } 15 | 16 | self.components = (0 ..< numberOfVerticalComponents).map { j in 17 | return (0 ..< numberOfHorizontalComponents).map { i in 18 | if i == 0 && j == 0 { 19 | let value = String(string[2 ..< 6]).decode83() 20 | return BlurHash.decodeDC(value) 21 | } else { 22 | let index = i + j * numberOfHorizontalComponents 23 | let value = String(string[4 + index * 2 ..< 4 + index * 2 + 2]).decode83() 24 | return BlurHash.decodeAC(value, maximumValue: maximumValue) 25 | } 26 | } 27 | } 28 | } 29 | 30 | private static func decodeDC(_ value: Int) -> (Float, Float, Float) { 31 | let intR = value >> 16 32 | let intG = (value >> 8) & 255 33 | let intB = value & 255 34 | return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) 35 | } 36 | 37 | private static func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { 38 | let quantR = value / (19 * 19) 39 | let quantG = (value / 19) % 19 40 | let quantB = value % 19 41 | 42 | let rgb = ( 43 | signPow((Float(quantR) - 9) / 9, 2) * maximumValue, 44 | signPow((Float(quantG) - 9) / 9, 2) * maximumValue, 45 | signPow((Float(quantB) - 9) / 9, 2) * maximumValue 46 | ) 47 | 48 | return rgb 49 | } 50 | } 51 | 52 | private extension String { 53 | subscript (offset: Int) -> Character { 54 | return self[index(startIndex, offsetBy: offset)] 55 | } 56 | 57 | subscript (bounds: CountableClosedRange) -> Substring { 58 | let start = index(startIndex, offsetBy: bounds.lowerBound) 59 | let end = index(startIndex, offsetBy: bounds.upperBound) 60 | return self[start...end] 61 | } 62 | 63 | subscript (bounds: CountableRange) -> Substring { 64 | let start = index(startIndex, offsetBy: bounds.lowerBound) 65 | let end = index(startIndex, offsetBy: bounds.upperBound) 66 | return self[start..= 1, components.0 <= 9, 6 | components.1 >= 1, components.1 <= 9 else { 7 | fatalError("Number of components bust be between 1 and 9 inclusive on each axis") 8 | } 9 | 10 | let pixelWidth = Int(round(image.size.width * image.scale)) 11 | let pixelHeight = Int(round(image.size.height * image.scale)) 12 | 13 | let context = CGContext( 14 | data: nil, 15 | width: pixelWidth, 16 | height: pixelHeight, 17 | bitsPerComponent: 8, 18 | bytesPerRow: pixelWidth * 4, 19 | space: CGColorSpace(name: CGColorSpace.sRGB)!, 20 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 21 | )! 22 | context.scaleBy(x: image.scale, y: -image.scale) 23 | context.translateBy(x: 0, y: -image.size.height) 24 | 25 | UIGraphicsPushContext(context) 26 | image.draw(at: .zero) 27 | UIGraphicsPopContext() 28 | 29 | guard let cgImage = context.makeImage(), 30 | let dataProvider = cgImage.dataProvider, 31 | let data = dataProvider.data, 32 | let pixels = CFDataGetBytePtr(data) else { 33 | assertionFailure("Unexpected error!") 34 | return nil 35 | } 36 | 37 | let width = cgImage.width 38 | let height = cgImage.height 39 | let bytesPerRow = cgImage.bytesPerRow 40 | 41 | self.components = (0 ..< components.1).map { j -> [(Float, Float, Float)] in 42 | return (0 ..< components.0).map { i -> (Float, Float, Float) in 43 | let normalisation: Float = (i == 0 && j == 0) ? 1 : 2 44 | return BlurHash.multiplyBasisFunction(pixels: pixels, width: width, height: height, bytesPerRow: bytesPerRow, bytesPerPixel: cgImage.bitsPerPixel / 8) { x, y in 45 | normalisation * cos(Float.pi * Float(i) * x / Float(width)) as Float * cos(Float.pi * Float(j) * y / Float(height)) as Float 46 | } 47 | } 48 | } 49 | } 50 | 51 | static private func multiplyBasisFunction(pixels: UnsafePointer, width: Int, height: Int, bytesPerRow: Int, bytesPerPixel: Int, basisFunction: (Float, Float) -> Float) -> (Float, Float, Float) { 52 | var c: (Float, Float, Float) = (0, 0, 0) 53 | 54 | let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow) 55 | 56 | for x in 0 ..< width { 57 | for y in 0 ..< height { 58 | c += basisFunction(Float(x), Float(y)) * ( 59 | sRGBToLinear(buffer[bytesPerPixel * x + 0 + y * bytesPerRow]), 60 | sRGBToLinear(buffer[bytesPerPixel * x + 1 + y * bytesPerRow]), 61 | sRGBToLinear(buffer[bytesPerPixel * x + 2 + y * bytesPerRow]) 62 | ) 63 | } 64 | } 65 | 66 | return c / Float(width * height) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/Generation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension BlurHash { 4 | public init(blendingTop top: BlurHash, bottom: BlurHash) { 5 | guard top.components.count == 1, bottom.components.count == 1 else { 6 | fatalError("Blended BlurHashses must have only one vertical component") 7 | } 8 | 9 | let average = zip(top.components[0], bottom.components[0]).map { ($0 + $1) / 2 } 10 | let difference = zip(top.components[0], bottom.components[0]).map { ($0 - $1) / 2 } 11 | self.init(components: [average, difference]) 12 | } 13 | 14 | public init(blendingLeft left: BlurHash, right: BlurHash) { 15 | self = BlurHash(blendingTop: left.transposed, bottom: right.transposed).transposed 16 | } 17 | } 18 | 19 | extension BlurHash { 20 | public init(colour: UIColor) { 21 | self.init(components: [[colour.linear]]) 22 | } 23 | 24 | public init(blendingTop topColour: UIColor, bottom bottomColour: UIColor) { 25 | self = BlurHash(blendingTop: .init(colour: topColour), bottom: .init(colour: bottomColour)) 26 | } 27 | 28 | public init(blendingLeft leftColour: UIColor, right rightColour: UIColor) { 29 | self = BlurHash(blendingLeft: .init(colour: leftColour), right: .init(colour: rightColour)) 30 | } 31 | 32 | public init(blendingTopLeft topLeftColour: UIColor, topRight topRightColour: UIColor, bottomLeft bottomLeftColour: UIColor, bottomRight bottomRightColour: UIColor) { 33 | self = BlurHash( 34 | blendingTop: BlurHash(blendingTop: topLeftColour, bottom: topRightColour).transposed, 35 | bottom: BlurHash(blendingTop: bottomLeftColour, bottom: bottomRightColour).transposed 36 | ) 37 | } 38 | } 39 | 40 | extension BlurHash { 41 | public init(horizontalColours colours: [(Float, Float, Float)], numberOfComponents: Int) { 42 | guard numberOfComponents >= 1, numberOfComponents <= 9 else { 43 | fatalError("Number of components bust be between 1 and 9 inclusive") 44 | } 45 | 46 | self.init(components: [(0 ..< numberOfComponents).map { i in 47 | let normalisation: Float = i == 0 ? 1 : 2 48 | var sum: (Float, Float, Float) = (0, 0, 0) 49 | for x in 0 ..< colours.count { 50 | let basis = normalisation * cos(Float.pi * Float(i) * Float(x) / Float(colours.count - 1)) 51 | sum += basis * colours[x] 52 | } 53 | 54 | return sum / Float(colours.count) 55 | }]) 56 | } 57 | } 58 | 59 | extension BlurHash { 60 | public var mirroredHorizontally: BlurHash { 61 | return .init(components: (0 ..< numberOfVerticalComponents).map { j -> [(Float, Float, Float)] in 62 | (0 ..< numberOfHorizontalComponents).map { i -> (Float, Float, Float) in 63 | components[j][i] * (i % 2 == 0 ? 1 : -1) 64 | } 65 | }) 66 | } 67 | 68 | public var mirroredVertically: BlurHash { 69 | return .init(components: (0 ..< numberOfVerticalComponents).map { j -> [(Float, Float, Float)] in 70 | (0 ..< numberOfHorizontalComponents).map { i -> (Float, Float, Float) in 71 | components[j][i] * (j % 2 == 0 ? 1 : -1) 72 | } 73 | }) 74 | } 75 | 76 | public var transposed: BlurHash { 77 | return .init(components: (0 ..< numberOfHorizontalComponents).map { i in 78 | (0 ..< numberOfVerticalComponents).map { j in 79 | components[j][i] 80 | } 81 | }) 82 | } 83 | } 84 | 85 | extension UIColor { 86 | var linear: (Float, Float, Float) { 87 | guard let c = cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)?.components else { return (0, 0, 0) } 88 | 89 | switch c.count { 90 | case 1, 2: return (sRGBToLinear(c[0]), sRGBToLinear(c[0]), sRGBToLinear(c[0])) 91 | case 3, 4: return (sRGBToLinear(c[0]), sRGBToLinear(c[1]), sRGBToLinear(c[2])) 92 | default: return (0, 0, 0) 93 | } 94 | } 95 | } 96 | 97 | func sRGBToLinear(_ value: CGFloat) -> Float { 98 | let v = Float(value) 99 | if v <= 0.04045 { return v / 12.92 } 100 | else { return pow((v + 0.055) / 1.055, 2.4) } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/StringCoding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let encodeCharacters: [String] = { 4 | return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } 5 | }() 6 | 7 | private let decodeCharacters: [String: Int] = { 8 | var dict: [String: Int] = [:] 9 | for (index, character) in encodeCharacters.enumerated() { 10 | dict[character] = index 11 | } 12 | return dict 13 | }() 14 | 15 | extension BinaryInteger { 16 | func encode83(length: Int) -> String { 17 | var result = "" 18 | for i in 1 ... length { 19 | let digit = (Int(self) / pow(83, length - i)) % 83 20 | result += encodeCharacters[Int(digit)] 21 | } 22 | return result 23 | } 24 | } 25 | 26 | extension String { 27 | func decode83() -> Int { 28 | var value: Int = 0 29 | for character in self { 30 | if let digit = decodeCharacters[String(character)] { 31 | value = value * 83 + digit 32 | } 33 | } 34 | return value 35 | } 36 | } 37 | 38 | private func pow(_ base: Int, _ exponent: Int) -> Int { 39 | return (0 ..< exponent).reduce(1) { value, _ in value * base } 40 | } 41 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/ToString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension BlurHash { 4 | var string: String { 5 | let flatComponents = components.reduce([]) { $0 + $1 } 6 | let dc = flatComponents.first! 7 | let ac = flatComponents.dropFirst() 8 | 9 | var hash = "" 10 | 11 | let sizeFlag = (numberOfHorizontalComponents - 1) + (numberOfVerticalComponents - 1) * 9 12 | hash += sizeFlag.encode83(length: 1) 13 | 14 | let maximumValue: Float 15 | if ac.count > 0 { 16 | let actualMaximumValue = ac.map({ max(abs($0.0), abs($0.1), abs($0.2)) }).max()! 17 | let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5)))) 18 | maximumValue = Float(quantisedMaximumValue + 1) / 166 19 | hash += quantisedMaximumValue.encode83(length: 1) 20 | } else { 21 | maximumValue = 1 22 | hash += 0.encode83(length: 1) 23 | } 24 | 25 | hash += encodeDC(dc).encode83(length: 4) 26 | 27 | for factor in ac { 28 | hash += encodeAC(factor, maximumValue: maximumValue).encode83(length: 2) 29 | } 30 | 31 | return hash 32 | } 33 | 34 | private func encodeDC(_ value: (Float, Float, Float)) -> Int { 35 | let roundedR = linearTosRGB(value.0) 36 | let roundedG = linearTosRGB(value.1) 37 | let roundedB = linearTosRGB(value.2) 38 | return (roundedR << 16) + (roundedG << 8) + roundedB 39 | } 40 | 41 | private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int { 42 | let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5)))) 43 | let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5)))) 44 | let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5)))) 45 | 46 | return quantR * 19 * 19 + quantG * 19 + quantB 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/ToUIImage.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension BlurHash { 4 | public func cgImage(size: CGSize) -> CGImage? { 5 | let width = Int(size.width) 6 | let height = Int(size.height) 7 | let bytesPerRow = width * 3 8 | 9 | guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } 10 | CFDataSetLength(data, bytesPerRow * height) 11 | 12 | guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } 13 | 14 | for y in 0 ..< height { 15 | for x in 0 ..< width { 16 | var c: (Float, Float, Float) = (0, 0, 0) 17 | 18 | for j in 0 ..< numberOfVerticalComponents { 19 | for i in 0 ..< numberOfHorizontalComponents { 20 | let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) 21 | let component = components[j][i] 22 | c += component * basis 23 | } 24 | } 25 | 26 | let intR = UInt8(linearTosRGB(c.0)) 27 | let intG = UInt8(linearTosRGB(c.1)) 28 | let intB = UInt8(linearTosRGB(c.2)) 29 | 30 | pixels[3 * x + 0 + y * bytesPerRow] = intR 31 | pixels[3 * x + 1 + y * bytesPerRow] = intG 32 | pixels[3 * x + 2 + y * bytesPerRow] = intB 33 | } 34 | } 35 | 36 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) 37 | 38 | guard let provider = CGDataProvider(data: data) else { return nil } 39 | guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, 40 | space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } 41 | 42 | return cgImage 43 | } 44 | 45 | public func cgImage(numberOfPixels: Int = 1024, originalSize size: CGSize) -> CGImage? { 46 | let width: CGFloat 47 | let height: CGFloat 48 | if size.width > size.height { 49 | width = floor(sqrt(CGFloat(numberOfPixels) * size.width / size.height) + 0.5) 50 | height = floor(CGFloat(numberOfPixels) / width + 0.5) 51 | } else { 52 | height = floor(sqrt(CGFloat(numberOfPixels) * size.height / size.width) + 0.5) 53 | width = floor(CGFloat(numberOfPixels) / height + 0.5) 54 | } 55 | return cgImage(size: CGSize(width: width, height: height)) 56 | } 57 | 58 | public func image(size: CGSize) -> UIImage? { 59 | guard let cgImage = cgImage(size: size) else { return nil } 60 | return UIImage(cgImage: cgImage) 61 | } 62 | 63 | public func image(numberOfPixels: Int = 1024, originalSize size: CGSize) -> UIImage? { 64 | guard let cgImage = cgImage(numberOfPixels: numberOfPixels, originalSize: size) else { return nil } 65 | return UIImage(cgImage: cgImage) 66 | } 67 | } 68 | 69 | @objc extension UIImage { 70 | public convenience init?(blurHash string: String, size: CGSize, punch: Float = 1) { 71 | guard let blurHash = BlurHash(string: string), 72 | let cgImage = blurHash.punch(punch).cgImage(size: size) else { return nil } 73 | self.init(cgImage: cgImage) 74 | } 75 | 76 | public convenience init?(blurHash string: String, numberOfPixels: Int = 1024, originalSize size: CGSize, punch: Float = 1) { 77 | guard let blurHash = BlurHash(string: string), 78 | let cgImage = blurHash.punch(punch).cgImage(numberOfPixels: numberOfPixels, originalSize: size) else { return nil } 79 | self.init(cgImage: cgImage) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Swift/BlurHashKit/TupleMaths.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func +(lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { 4 | return (lhs.0 + rhs.0, lhs.1 + rhs.1, lhs.2 + rhs.2) 5 | } 6 | 7 | func -(lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { 8 | return (lhs.0 - rhs.0, lhs.1 - rhs.1, lhs.2 - rhs.2) 9 | } 10 | 11 | func *(lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { 12 | return (lhs.0 * rhs.0, lhs.1 * rhs.1, lhs.2 * rhs.2) 13 | } 14 | 15 | func *(lhs: (Float, Float, Float), rhs: Float) -> (Float, Float, Float) { 16 | return (lhs.0 * rhs, lhs.1 * rhs, lhs.2 * rhs) 17 | } 18 | 19 | func *(lhs: Float, rhs: (Float, Float, Float)) -> (Float, Float, Float) { 20 | return (lhs * rhs.0, lhs * rhs.1, lhs * rhs.2) 21 | } 22 | 23 | func /(lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { 24 | return (lhs.0 / rhs.0, lhs.1 / rhs.1, lhs.2 / rhs.2) 25 | } 26 | 27 | func /(lhs: (Float, Float, Float), rhs: Float) -> (Float, Float, Float) { 28 | return (lhs.0 / rhs, lhs.1 / rhs, lhs.2 / rhs) 29 | } 30 | 31 | func +=(lhs: inout (Float, Float, Float), rhs: (Float, Float, Float)) { 32 | lhs = lhs + rhs 33 | } 34 | 35 | func -=(lhs: inout (Float, Float, Float), rhs: (Float, Float, Float)) { 36 | lhs = lhs - rhs 37 | } 38 | 39 | func *=(lhs: inout (Float, Float, Float), rhs: Float) { 40 | lhs = lhs * rhs 41 | } 42 | 43 | func /=(lhs: inout (Float, Float, Float), rhs: Float) { 44 | lhs = lhs / rhs 45 | } 46 | 47 | func min(_ a: (Float, Float, Float), _ b: (Float, Float, Float)) -> (Float, Float, Float) { 48 | return (min(a.0, b.0), min(a.1, b.1), min(a.2, b.2)) 49 | } 50 | 51 | func max(_ a: (Float, Float, Float), _ b: (Float, Float, Float)) -> (Float, Float, Float) { 52 | return (max(a.0, b.0), max(a.1, b.1), max(a.2, b.2)) 53 | } 54 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/AdvancedViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import BlurHashKit 3 | 4 | class AdvancedViewController: UIViewController { 5 | @IBOutlet weak var originalImageView: UIImageView? 6 | @IBOutlet weak var uncompressedBlurImageView: UIImageView? 7 | @IBOutlet weak var hashLabel: UILabel? 8 | @IBOutlet weak var compressedBlurImageView: UIImageView? 9 | 10 | @IBOutlet weak var darknessBlurImageView: UIImageView? 11 | @IBOutlet weak var topLeftCornerLabel: UILabel? 12 | @IBOutlet weak var topEdgeLabel: UILabel? 13 | @IBOutlet weak var topRightCornerLabel: UILabel? 14 | @IBOutlet weak var leftEdgeLabel: UILabel? 15 | @IBOutlet weak var centreLabel: UILabel? 16 | @IBOutlet weak var rightEdgeLabel: UILabel? 17 | @IBOutlet weak var bottomLeftCornerLabel: UILabel? 18 | @IBOutlet weak var bottomEdgeLabel: UILabel? 19 | @IBOutlet weak var bottomRightCornerLabel: UILabel? 20 | 21 | @IBOutlet weak var xComponentsLabel: UILabel? 22 | @IBOutlet weak var yComponentsLabel: UILabel? 23 | 24 | let images: [UIImage] = [ 25 | UIImage(named: "pic2.png")!, 26 | UIImage(named: "pic1.png")!, 27 | UIImage(named: "pic3.png")!, 28 | UIImage(named: "pic6.png")!, 29 | UIImage(named: "pic4.png")!, 30 | UIImage(named: "pic5.png")!, 31 | ] 32 | 33 | var imageIndex: Int = 0 34 | var xComponents: Int = 4 35 | var yComponents: Int = 3 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | update() 41 | } 42 | 43 | @IBAction func imageTapped() { 44 | imageIndex = (imageIndex + 1) % images.count 45 | update() 46 | } 47 | 48 | @IBAction func xPlusTapped() { 49 | if xComponents < 9 { 50 | xComponents += 1 51 | update() 52 | } 53 | } 54 | 55 | @IBAction func xMinusTapped() { 56 | if xComponents > 1 { 57 | xComponents -= 1 58 | update() 59 | } 60 | } 61 | 62 | @IBAction func yPlusTapped() { 63 | if yComponents < 9 { 64 | yComponents += 1 65 | update() 66 | } 67 | } 68 | 69 | @IBAction func yMinusTapped() { 70 | if yComponents > 1 { 71 | yComponents -= 1 72 | update() 73 | } 74 | } 75 | 76 | 77 | func update() { 78 | let image = images[imageIndex] 79 | 80 | originalImageView?.image = image 81 | 82 | let blurHash = BlurHash(image: images[imageIndex], numberOfComponents: (xComponents, yComponents))! 83 | uncompressedBlurImageView?.image = blurHash.image(numberOfPixels: 1024, originalSize: image.size) 84 | 85 | hashLabel?.text = blurHash.string 86 | let decodedBlurHash = BlurHash(string: blurHash.string)! 87 | 88 | compressedBlurImageView?.image = decodedBlurHash.image(numberOfPixels: 1024, originalSize: image.size) 89 | darknessBlurImageView?.image = decodedBlurHash.image(numberOfPixels: 1024, originalSize: image.size) 90 | 91 | setDarkness(label: topLeftCornerLabel, isDark: decodedBlurHash.isTopLeftCornerDark, light: "Ⓛ", dark: "Ⓓ") 92 | setDarkness(label: topEdgeLabel, isDark: decodedBlurHash.isTopEdgeDark, light: "------Light------", dark: "------Dark------") 93 | setDarkness(label: topRightCornerLabel, isDark: decodedBlurHash.isTopRightCornerDark, light: "Ⓛ", dark: "Ⓓ") 94 | setDarkness(label: leftEdgeLabel, isDark: decodedBlurHash.isLeftEdgeDark, light: "|\n|\nLight\n|\n|", dark: "|\n|\nDark\n|\n|") 95 | setDarkness(label: centreLabel, isDark: decodedBlurHash.isDark(), light: "Light", dark: "Dark") 96 | setDarkness(label: rightEdgeLabel, isDark: decodedBlurHash.isRightEdgeDark, light: "|\n|\nLight\n|\n|", dark: "|\n|\nDark\n|\n|") 97 | setDarkness(label: bottomLeftCornerLabel, isDark: decodedBlurHash.isBottomLeftCornerDark, light: "Ⓛ", dark: "Ⓓ") 98 | setDarkness(label: bottomEdgeLabel, isDark: decodedBlurHash.isBottomEdgeDark, light: "------Light------", dark: "------Dark------") 99 | setDarkness(label: bottomRightCornerLabel, isDark: decodedBlurHash.isBottomRightCornerDark, light: "Ⓛ", dark: "Ⓓ") 100 | 101 | xComponentsLabel?.text = String(xComponents) 102 | yComponentsLabel?.text = String(yComponents) 103 | 104 | } 105 | 106 | func setDarkness(label: UILabel?, isDark: Bool, light: String, dark: String) { 107 | if isDark { 108 | label?.textColor = .white 109 | label?.text = dark 110 | } else { 111 | label?.textColor = .black 112 | label?.text = light 113 | } 114 | } 115 | 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 9 | return true 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Swift/BlurHashTest/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/GeneratedViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import BlurHashKit 3 | 4 | class GeneratedViewController: UIViewController { 5 | @IBOutlet weak var horizontalUncompressedImageView: UIImageView? 6 | @IBOutlet weak var horizontalCompressedImageView: UIImageView? 7 | @IBOutlet weak var horizontalLeftView: UIView? 8 | @IBOutlet weak var horizontalRightView: UIView? 9 | @IBOutlet weak var horizontalHashLabel: UILabel? 10 | 11 | @IBOutlet weak var verticalUncompressedImageView: UIImageView? 12 | @IBOutlet weak var verticalCompressedImageView: UIImageView? 13 | @IBOutlet weak var verticalTopView: UIView? 14 | @IBOutlet weak var verticalBottomView: UIView? 15 | @IBOutlet weak var verticalHashLabel: UILabel? 16 | 17 | @IBOutlet weak var cornerUncompressedImageView: UIImageView? 18 | @IBOutlet weak var cornerCompressedImageView: UIImageView? 19 | @IBOutlet weak var cornerTopLeftView: UIView? 20 | @IBOutlet weak var cornerTopRightView: UIView? 21 | @IBOutlet weak var cornerBottomLeftView: UIView? 22 | @IBOutlet weak var cornerBottomRightView: UIView? 23 | @IBOutlet weak var cornerHashLabel: UILabel? 24 | 25 | /* private var horizontalLeftColour = UIColor.red 26 | private var horizontalRightColour = UIColor.green 27 | private var verticalTopColour = UIColor.white 28 | private var verticalBottomColour = UIColor.black 29 | private var cornerTopLeftColour = UIColor.white 30 | private var cornerTopRightColour = UIColor.red 31 | private var cornerBottomLeftColour = UIColor.green 32 | private var cornerBottomRightColour = UIColor.black 33 | */ 34 | private var horizontalLeftColour = UIColor.white 35 | private var horizontalRightColour = UIColor.black 36 | private var verticalTopColour = UIColor.white 37 | private var verticalBottomColour = UIColor.black 38 | private var cornerTopLeftColour = UIColor.white 39 | private var cornerTopRightColour = UIColor.black 40 | private var cornerBottomLeftColour = UIColor.black 41 | private var cornerBottomRightColour = UIColor.white 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | horizontalLeftView?.layer.borderWidth = 1 47 | horizontalLeftView?.layer.borderColor = UIColor.black.cgColor 48 | horizontalRightView?.layer.borderWidth = 1 49 | horizontalRightView?.layer.borderColor = UIColor.black.cgColor 50 | 51 | verticalTopView?.layer.borderWidth = 1 52 | verticalTopView?.layer.borderColor = UIColor.black.cgColor 53 | verticalBottomView?.layer.borderWidth = 1 54 | verticalBottomView?.layer.borderColor = UIColor.black.cgColor 55 | 56 | cornerTopLeftView?.layer.borderWidth = 1 57 | cornerTopLeftView?.layer.borderColor = UIColor.black.cgColor 58 | cornerTopRightView?.layer.borderWidth = 1 59 | cornerTopRightView?.layer.borderColor = UIColor.black.cgColor 60 | cornerBottomLeftView?.layer.borderWidth = 1 61 | cornerBottomLeftView?.layer.borderColor = UIColor.black.cgColor 62 | cornerBottomRightView?.layer.borderWidth = 1 63 | cornerBottomRightView?.layer.borderColor = UIColor.black.cgColor 64 | 65 | update() 66 | } 67 | 68 | @IBAction func randomiseTapped() { 69 | horizontalLeftColour = .random() 70 | horizontalRightColour = .random() 71 | verticalTopColour = .random() 72 | verticalBottomColour = .random() 73 | cornerTopLeftColour = .random() 74 | cornerTopRightColour = .random() 75 | cornerBottomLeftColour = .random() 76 | cornerBottomRightColour = .random() 77 | 78 | update() 79 | } 80 | 81 | 82 | func update() { 83 | horizontalLeftView?.backgroundColor = horizontalLeftColour 84 | horizontalRightView?.backgroundColor = horizontalRightColour 85 | 86 | verticalTopView?.backgroundColor = verticalTopColour 87 | verticalBottomView?.backgroundColor = verticalBottomColour 88 | 89 | cornerTopLeftView?.backgroundColor = cornerTopLeftColour 90 | cornerTopRightView?.backgroundColor = cornerTopRightColour 91 | cornerBottomLeftView?.backgroundColor = cornerBottomLeftColour 92 | cornerBottomRightView?.backgroundColor = cornerBottomRightColour 93 | 94 | let horizontalBlurHash = BlurHash(blendingLeft: horizontalLeftColour, right: horizontalRightColour) 95 | horizontalUncompressedImageView?.image = horizontalBlurHash.image(size: CGSize(width: 32, height: 32)) 96 | horizontalCompressedImageView?.image = BlurHash(string: horizontalBlurHash.string)?.image(size: CGSize(width: 32, height: 32)) 97 | horizontalHashLabel?.text = horizontalBlurHash.string 98 | 99 | let verticalBlurHash = BlurHash(blendingTop: verticalTopColour, bottom: verticalBottomColour) 100 | verticalUncompressedImageView?.image = verticalBlurHash.image(size: CGSize(width: 32, height: 32)) 101 | verticalCompressedImageView?.image = BlurHash(string: verticalBlurHash.string)?.image(size: CGSize(width: 32, height: 32)) 102 | verticalHashLabel?.text = verticalBlurHash.string 103 | 104 | let cornerBlurHash = BlurHash(blendingTopLeft: cornerTopLeftColour, topRight: cornerTopRightColour, bottomLeft: cornerBottomLeftColour, bottomRight: cornerBottomRightColour) 105 | cornerUncompressedImageView?.image = cornerBlurHash.image(size: CGSize(width: 32, height: 32)) 106 | cornerCompressedImageView?.image = BlurHash(string: cornerBlurHash.string)?.image(size: CGSize(width: 32, height: 32)) 107 | cornerHashLabel?.text = cornerBlurHash.string 108 | } 109 | } 110 | 111 | extension UIColor { 112 | static func random() -> UIColor { 113 | let hue = CGFloat(arc4random()) / CGFloat(UInt32.max) 114 | let brightness = CGFloat(arc4random()) / CGFloat(UInt32.max) 115 | if brightness < 0.5 { 116 | return UIColor(hue: hue, saturation: 1, brightness: brightness * 2, alpha: 1) 117 | } else { 118 | return UIColor(hue: hue, saturation: 2 - brightness * 2, brightness: 1, alpha: 1) 119 | } 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/SimpleViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SimpleViewController: UIViewController { 4 | @IBOutlet weak var originalImageView: UIImageView? 5 | @IBOutlet weak var hashLabel: UILabel? 6 | @IBOutlet weak var blurImageView: UIImageView? 7 | @IBOutlet weak var xComponentsLabel: UILabel? 8 | @IBOutlet weak var yComponentsLabel: UILabel? 9 | 10 | let images: [UIImage] = [ 11 | UIImage(named: "pic2.png")!, 12 | UIImage(named: "pic1.png")!, 13 | UIImage(named: "pic3.png")!, 14 | UIImage(named: "pic6.png")!, 15 | ] 16 | 17 | var imageIndex: Int = 0 18 | var xComponents: Int = 4 19 | var yComponents: Int = 3 20 | var blurHash: String = "" 21 | var punch: Float = 1 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | updateEncode() 27 | updateDecode() 28 | } 29 | 30 | @IBAction func imageTapped() { 31 | imageIndex = (imageIndex + 1) % images.count 32 | updateEncode() 33 | updateDecode() 34 | } 35 | 36 | @IBAction func xPlusTapped() { 37 | if xComponents < 8 { 38 | xComponents += 1 39 | updateEncode() 40 | updateDecode() 41 | } 42 | } 43 | 44 | @IBAction func xMinusTapped() { 45 | if xComponents > 1 { 46 | xComponents -= 1 47 | updateEncode() 48 | updateDecode() 49 | } 50 | } 51 | 52 | @IBAction func yPlusTapped() { 53 | if yComponents < 8 { 54 | yComponents += 1 55 | updateEncode() 56 | updateDecode() 57 | } 58 | } 59 | 60 | @IBAction func yMinusTapped() { 61 | if yComponents > 1 { 62 | yComponents -= 1 63 | updateEncode() 64 | updateDecode() 65 | } 66 | } 67 | 68 | 69 | @IBAction func sliderChanged(slider: UISlider) { 70 | punch = slider.value 71 | updateDecode() 72 | } 73 | 74 | func updateEncode() { 75 | originalImageView?.image = images[imageIndex] 76 | blurHash = images[imageIndex].blurHash(numberOfComponents: (xComponents, yComponents))! 77 | hashLabel?.text = blurHash 78 | xComponentsLabel?.text = String(xComponents) 79 | yComponentsLabel?.text = String(yComponents) 80 | } 81 | 82 | func updateDecode() { 83 | let blurImage = UIImage(blurHash: blurHash, size: CGSize(width: 32, height: 32), punch: punch) 84 | 85 | blurImageView?.image = blurImage 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic1.png -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic2.png -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic3.png -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic4.png -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic5.png -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic6.jpg -------------------------------------------------------------------------------- /Swift/BlurHashTest/pic6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Swift/BlurHashTest/pic6.png -------------------------------------------------------------------------------- /Swift/License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Wolt Enterprises 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Swift/Readme.md: -------------------------------------------------------------------------------- 1 | # BlurHash for iOS, in Swift 2 | 3 | ## Standalone decoder and encoder 4 | 5 | [BlurHashDecode.swift](BlurHashDecode.swift) and [BlurHashEncode.swift](BlurHashEncode.swift) contain a decoder 6 | and encoder for BlurHash to and from `UIImage`. Both files are completeiy standalone, and can simply be copied into your 7 | project directly. 8 | 9 | ### Decoding 10 | 11 | [BlurHashDecode.swift](BlurHashDecode.swift) implements the following extension on `UIImage`: 12 | 13 | public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) 14 | 15 | This creates a UIImage containing the placeholder image decoded from the BlurHash string, or returns nil if decoding failed. 16 | The parameters are: 17 | 18 | * `blurHash` - A string containing the BlurHash. 19 | * `size` - The requested output size. You should keep this small, and let UIKit scale it up for you. 32 pixels wide is plenty. 20 | * `punch` - Adjusts the contrast of the output image. Tweak it if you want a different look for your placeholders. 21 | 22 | ### Encoding 23 | 24 | [BlurHashEncode.swift](BlurHashEncode.swift) implements the following extension on `UIImage`: 25 | 26 | public func blurHash(numberOfComponents components: (Int, Int)) -> String? 27 | 28 | This returns a string containing the BlurHash for the image, or nil if the image was in a weird format that is not supported. 29 | The parameters are: 30 | 31 | * `numberOfComponents` - a Tuple of integers specifying the number of components in the X and Y directions. Both must be 32 | between 1 and 9 inclusive, or the function will return nil. 3 to 5 is usually a good range. 33 | 34 | ## BlurHashKit 35 | 36 | This is a more advanced library, currently in development. It will let you do more advanced operations using BlurHashes, 37 | such testing whether various parts of an image are dark and light, or generating BlurHashes as gradients from corner colours. 38 | 39 | It is currently not documented or finalised, but feel free to look into the different files and what they implement, or look at 40 | how it is used by the test app. 41 | 42 | ## BlurHashTest.app 43 | 44 | This is a simple test app that shows how to use the various pieces of BlurHash functionality, and lets you play with the 45 | algorithm. 46 | -------------------------------------------------------------------------------- /TypeScript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /TypeScript/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .prettierrc 3 | tsconfig.json 4 | demo 5 | webpack.config.js 6 | -------------------------------------------------------------------------------- /TypeScript/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "overrides": [ 4 | { 5 | "files": ["*.ts"], 6 | "options": {} 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /TypeScript/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 (September 12, 2022) 4 | 5 | - Modern bundling 6 | - Fix DC encoding 7 | - Drop IE11 support 8 | 9 | ## 1.1.5 (February 15, 2022) 10 | 11 | - added typescript sources to npm package for source map support 12 | 13 | ## 1.1.4 (August 16, 2021) 14 | 15 | - added esm build target 16 | 17 | ## 1.1.2 (June 29, 2019) 18 | 19 | - added `isBlurhashValid()` utility 20 | 21 | ## 1.1.1 (June 29, 2019) 22 | 23 | - fixed incorrect type declaration path in package.json 24 | - improved error handling 25 | -------------------------------------------------------------------------------- /TypeScript/README.md: -------------------------------------------------------------------------------- 1 | # blurhash 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/blurhash.svg?style=flat)](https://npmjs.org/package/blurhash) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/blurhash.svg?style=flat)](https://npmjs.org/package/blurhash) 5 | 6 | > JavaScript encoder and decoder for the [Wolt BlurHash](https://github.com/woltapp/blurhash) algorithm 7 | 8 | ## Install 9 | 10 | ```sh 11 | npm install --save blurhash 12 | ``` 13 | 14 | See [react-blurhash](https://github.com/woltapp/react-blurhash) to use blurhash with React. 15 | 16 | ## API 17 | 18 | ### `decode(blurhash: string, width: number, height: number, punch?: number) => Uint8ClampedArray` 19 | 20 | > Decodes a blurhash string to pixels 21 | 22 | #### Example 23 | 24 | ```js 25 | import { decode } from "blurhash"; 26 | 27 | const pixels = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 32, 32); 28 | 29 | const canvas = document.createElement("canvas"); 30 | const ctx = canvas.getContext("2d"); 31 | const imageData = ctx.createImageData(width, height); 32 | imageData.data.set(pixels); 33 | ctx.putImageData(imageData, 0, 0); 34 | document.body.append(canvas); 35 | ``` 36 | 37 | ### `encode(pixels: Uint8ClampedArray, width: number, height: number, componentX: number, componentY: number) => string` 38 | 39 | > Encodes pixels to a blurhash string 40 | 41 | ```js 42 | import { encode } from "blurhash"; 43 | 44 | const loadImage = async src => 45 | new Promise((resolve, reject) => { 46 | const img = new Image(); 47 | img.onload = () => resolve(img); 48 | img.onerror = (...args) => reject(args); 49 | img.src = src; 50 | }); 51 | 52 | const getImageData = image => { 53 | const canvas = document.createElement("canvas"); 54 | canvas.width = image.width; 55 | canvas.height = image.height; 56 | const context = canvas.getContext("2d"); 57 | context.drawImage(image, 0, 0); 58 | return context.getImageData(0, 0, image.width, image.height); 59 | }; 60 | 61 | const encodeImageToBlurhash = async imageUrl => { 62 | const image = await loadImage(imageUrl); 63 | const imageData = getImageData(image); 64 | return encode(imageData.data, imageData.width, imageData.height, 4, 4); 65 | }; 66 | ``` 67 | 68 | ### `isBlurhashValid(blurhash: string) => { result: boolean; errorReason?: string }` 69 | 70 | ```js 71 | import { isBlurhashValid } from "blurhash"; 72 | 73 | const validRes = isBlurhashValid("LEHV6nWB2yk8pyo0adR*.7kCMdnj"); 74 | // { result: true } 75 | 76 | const invalidRes = isBlurhashValid("???"); 77 | // { result: false, errorReason: "The blurhash string must be at least 6 characters" } 78 | ``` 79 | -------------------------------------------------------------------------------- /TypeScript/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blurhash test file 6 | 43 | 44 | 45 | 46 |

BlurHash demo

47 |
48 |
49 |

Encode

50 | 51 | 55 | 61 |
62 |
63 |

Blurhash

64 | 65 |
66 |
67 |

Decode

68 | 69 |
70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /TypeScript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blurhash", 3 | "version": "2.0.5", 4 | "description": "Encoder and decoder for the Wolt BlurHash algorithm.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "module": "dist/esm/index.js", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "browser": { 12 | "import":"./dist/esm/index.js", 13 | "require": "./dist/index.js" 14 | }, 15 | "import": "./dist/index.mjs", 16 | "module": "./dist/esm/index.js", 17 | "node": "./dist/index.js", 18 | "require": "./dist/index.js", 19 | "types": "./dist/index.d.ts", 20 | "default": "./dist/index.js" 21 | } 22 | }, 23 | "sideEffects": false, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/woltapp/blurhash/tree/master/TypeScript" 27 | }, 28 | "homepage": "https://blurha.sh", 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "prepublishOnly": "npm run build", 34 | "build": "tsup", 35 | "demo": "webpack-dev-server --mode development", 36 | "prettier": "prettier src/**/*.ts", 37 | "prettier-fix": "npm run prettier -- --write", 38 | "start": "tsup --watch" 39 | }, 40 | "keywords": [ 41 | "blurhash", 42 | "blur", 43 | "hash", 44 | "image" 45 | ], 46 | "author": "omahlama", 47 | "license": "MIT", 48 | "devDependencies": { 49 | "prettier": "2.7.1", 50 | "ts-loader": "9.3.1", 51 | "tsup": "^6.1.3", 52 | "typescript": "4.7.4", 53 | "webpack": "5.73.0", 54 | "webpack-cli": "4.10.0", 55 | "webpack-dev-server": "4.9.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /TypeScript/src/base83.ts: -------------------------------------------------------------------------------- 1 | const digitCharacters = [ 2 | "0", 3 | "1", 4 | "2", 5 | "3", 6 | "4", 7 | "5", 8 | "6", 9 | "7", 10 | "8", 11 | "9", 12 | "A", 13 | "B", 14 | "C", 15 | "D", 16 | "E", 17 | "F", 18 | "G", 19 | "H", 20 | "I", 21 | "J", 22 | "K", 23 | "L", 24 | "M", 25 | "N", 26 | "O", 27 | "P", 28 | "Q", 29 | "R", 30 | "S", 31 | "T", 32 | "U", 33 | "V", 34 | "W", 35 | "X", 36 | "Y", 37 | "Z", 38 | "a", 39 | "b", 40 | "c", 41 | "d", 42 | "e", 43 | "f", 44 | "g", 45 | "h", 46 | "i", 47 | "j", 48 | "k", 49 | "l", 50 | "m", 51 | "n", 52 | "o", 53 | "p", 54 | "q", 55 | "r", 56 | "s", 57 | "t", 58 | "u", 59 | "v", 60 | "w", 61 | "x", 62 | "y", 63 | "z", 64 | "#", 65 | "$", 66 | "%", 67 | "*", 68 | "+", 69 | ",", 70 | "-", 71 | ".", 72 | ":", 73 | ";", 74 | "=", 75 | "?", 76 | "@", 77 | "[", 78 | "]", 79 | "^", 80 | "_", 81 | "{", 82 | "|", 83 | "}", 84 | "~", 85 | ]; 86 | 87 | export const decode83 = (str: String) => { 88 | let value = 0; 89 | for (let i = 0; i < str.length; i++) { 90 | const c = str[i]; 91 | const digit = digitCharacters.indexOf(c); 92 | value = value * 83 + digit; 93 | } 94 | return value; 95 | }; 96 | 97 | export const encode83 = (n: number, length: number): string => { 98 | var result = ""; 99 | for (let i = 1; i <= length; i++) { 100 | let digit = (Math.floor(n) / Math.pow(83, length - i)) % 83; 101 | result += digitCharacters[Math.floor(digit)]; 102 | } 103 | return result; 104 | }; 105 | -------------------------------------------------------------------------------- /TypeScript/src/decode.ts: -------------------------------------------------------------------------------- 1 | import { decode83 } from "./base83"; 2 | import { sRGBToLinear, signPow, linearTosRGB } from "./utils"; 3 | import { ValidationError } from "./error"; 4 | 5 | /** 6 | * Returns an error message if invalid or undefined if valid 7 | * @param blurhash 8 | */ 9 | const validateBlurhash = (blurhash: string) => { 10 | if (!blurhash || blurhash.length < 6) { 11 | throw new ValidationError( 12 | "The blurhash string must be at least 6 characters" 13 | ); 14 | } 15 | 16 | const sizeFlag = decode83(blurhash[0]); 17 | const numY = Math.floor(sizeFlag / 9) + 1; 18 | const numX = (sizeFlag % 9) + 1; 19 | 20 | if (blurhash.length !== 4 + 2 * numX * numY) { 21 | throw new ValidationError( 22 | `blurhash length mismatch: length is ${ 23 | blurhash.length 24 | } but it should be ${4 + 2 * numX * numY}` 25 | ); 26 | } 27 | }; 28 | 29 | export const isBlurhashValid = ( 30 | blurhash: string 31 | ): { result: boolean; errorReason?: string } => { 32 | try { 33 | validateBlurhash(blurhash); 34 | } catch (error) { 35 | return { result: false, errorReason: error.message }; 36 | } 37 | 38 | return { result: true }; 39 | }; 40 | 41 | const decodeDC = (value: number) => { 42 | const intR = value >> 16; 43 | const intG = (value >> 8) & 255; 44 | const intB = value & 255; 45 | return [sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)]; 46 | }; 47 | 48 | const decodeAC = (value: number, maximumValue: number) => { 49 | const quantR = Math.floor(value / (19 * 19)); 50 | const quantG = Math.floor(value / 19) % 19; 51 | const quantB = value % 19; 52 | 53 | const rgb = [ 54 | signPow((quantR - 9) / 9, 2.0) * maximumValue, 55 | signPow((quantG - 9) / 9, 2.0) * maximumValue, 56 | signPow((quantB - 9) / 9, 2.0) * maximumValue, 57 | ]; 58 | 59 | return rgb; 60 | }; 61 | 62 | const decode = ( 63 | blurhash: string, 64 | width: number, 65 | height: number, 66 | punch?: number 67 | ) => { 68 | validateBlurhash(blurhash); 69 | 70 | punch = punch | 1; 71 | 72 | const sizeFlag = decode83(blurhash[0]); 73 | const numY = Math.floor(sizeFlag / 9) + 1; 74 | const numX = (sizeFlag % 9) + 1; 75 | 76 | const quantisedMaximumValue = decode83(blurhash[1]); 77 | const maximumValue = (quantisedMaximumValue + 1) / 166; 78 | 79 | const colors = new Array(numX * numY); 80 | 81 | for (let i = 0; i < colors.length; i++) { 82 | if (i === 0) { 83 | const value = decode83(blurhash.substring(2, 6)); 84 | colors[i] = decodeDC(value); 85 | } else { 86 | const value = decode83(blurhash.substring(4 + i * 2, 6 + i * 2)); 87 | colors[i] = decodeAC(value, maximumValue * punch); 88 | } 89 | } 90 | 91 | const bytesPerRow = width * 4; 92 | const pixels = new Uint8ClampedArray(bytesPerRow * height); 93 | 94 | for (let y = 0; y < height; y++) { 95 | for (let x = 0; x < width; x++) { 96 | let r = 0; 97 | let g = 0; 98 | let b = 0; 99 | 100 | for (let j = 0; j < numY; j++) { 101 | const basisY = Math.cos((Math.PI * y * j) / height); 102 | for (let i = 0; i < numX; i++) { 103 | const basis = Math.cos((Math.PI * x * i) / width) * basisY; 104 | const color = colors[i + j * numX]; 105 | r += color[0] * basis; 106 | g += color[1] * basis; 107 | b += color[2] * basis; 108 | } 109 | } 110 | 111 | let intR = linearTosRGB(r); 112 | let intG = linearTosRGB(g); 113 | let intB = linearTosRGB(b); 114 | 115 | pixels[4 * x + 0 + y * bytesPerRow] = intR; 116 | pixels[4 * x + 1 + y * bytesPerRow] = intG; 117 | pixels[4 * x + 2 + y * bytesPerRow] = intB; 118 | pixels[4 * x + 3 + y * bytesPerRow] = 255; // alpha 119 | } 120 | } 121 | return pixels; 122 | }; 123 | 124 | export default decode; 125 | -------------------------------------------------------------------------------- /TypeScript/src/demo.ts: -------------------------------------------------------------------------------- 1 | import decode from "./decode"; 2 | import encode from "./encode"; 3 | 4 | const blurhashElement = document.getElementById("blurhash") as HTMLInputElement; 5 | const canvas = document.getElementById("canvas") as HTMLCanvasElement; 6 | const originalCanvas = document.getElementById("original") as HTMLCanvasElement; 7 | const fileInput = document.getElementById("fileinput") as HTMLInputElement; 8 | const componentXElement = document.getElementById("x") as HTMLInputElement; 9 | const componentYElement = document.getElementById("y") as HTMLInputElement; 10 | 11 | function render() { 12 | const blurhash = blurhashElement.value; 13 | if (blurhash) { 14 | const pixels = decode(blurhash, 32, 32); 15 | if (pixels) { 16 | const ctx = canvas.getContext("2d"); 17 | 18 | const imageData = new ImageData(pixels, 32, 32); 19 | ctx.putImageData(imageData, 0, 0); 20 | } 21 | } 22 | } 23 | 24 | function clamp(n: number) { 25 | return Math.min(9, Math.max(1, n)); 26 | } 27 | 28 | function doEncode() { 29 | const file = fileInput.files[0]; 30 | const componentX = clamp(+componentXElement.value); 31 | const componentY = clamp(+componentYElement.value); 32 | if (file) { 33 | const ctx = originalCanvas.getContext("2d"); 34 | var img = new Image(); 35 | img.onload = function () { 36 | ctx.drawImage(img, 0, 0, originalCanvas.width, originalCanvas.height); 37 | URL.revokeObjectURL(img.src); 38 | 39 | setTimeout(() => { 40 | const imageData = ctx.getImageData( 41 | 0, 42 | 0, 43 | originalCanvas.width, 44 | originalCanvas.height 45 | ); 46 | const blurhash = encode( 47 | imageData.data, 48 | imageData.width, 49 | imageData.height, 50 | componentX, 51 | componentY 52 | ); 53 | blurhashElement.value = blurhash; 54 | render(); 55 | }, 0); 56 | }; 57 | img.src = URL.createObjectURL(fileInput.files[0]); 58 | } 59 | } 60 | 61 | blurhashElement.addEventListener("keyup", render); 62 | fileInput.addEventListener("change", doEncode); 63 | componentXElement.addEventListener("change", doEncode); 64 | componentYElement.addEventListener("change", doEncode); 65 | 66 | render(); 67 | -------------------------------------------------------------------------------- /TypeScript/src/encode.ts: -------------------------------------------------------------------------------- 1 | import { encode83 } from "./base83"; 2 | import { sRGBToLinear, signPow, linearTosRGB } from "./utils"; 3 | import { ValidationError } from "./error"; 4 | 5 | type NumberTriplet = [number, number, number]; 6 | 7 | const bytesPerPixel = 4; 8 | 9 | const multiplyBasisFunction = ( 10 | pixels: Uint8ClampedArray, 11 | width: number, 12 | height: number, 13 | basisFunction: (i: number, j: number) => number 14 | ): NumberTriplet => { 15 | let r = 0; 16 | let g = 0; 17 | let b = 0; 18 | const bytesPerRow = width * bytesPerPixel; 19 | 20 | for (let x = 0; x < width; x++) { 21 | const bytesPerPixelX = bytesPerPixel * x; 22 | 23 | for (let y = 0; y < height; y++) { 24 | const basePixelIndex = bytesPerPixelX + y * bytesPerRow; 25 | const basis = basisFunction(x, y); 26 | r += 27 | basis * sRGBToLinear(pixels[basePixelIndex]); 28 | g += 29 | basis * sRGBToLinear(pixels[basePixelIndex + 1]); 30 | b += 31 | basis * sRGBToLinear(pixels[basePixelIndex + 2]); 32 | } 33 | } 34 | 35 | let scale = 1 / (width * height); 36 | 37 | return [r * scale, g * scale, b * scale]; 38 | }; 39 | 40 | const encodeDC = (value: NumberTriplet): number => { 41 | const roundedR = linearTosRGB(value[0]); 42 | const roundedG = linearTosRGB(value[1]); 43 | const roundedB = linearTosRGB(value[2]); 44 | return (roundedR << 16) + (roundedG << 8) + roundedB; 45 | }; 46 | 47 | const encodeAC = (value: NumberTriplet, maximumValue: number): number => { 48 | let quantR = Math.floor( 49 | Math.max( 50 | 0, 51 | Math.min(18, Math.floor(signPow(value[0] / maximumValue, 0.5) * 9 + 9.5)) 52 | ) 53 | ); 54 | let quantG = Math.floor( 55 | Math.max( 56 | 0, 57 | Math.min(18, Math.floor(signPow(value[1] / maximumValue, 0.5) * 9 + 9.5)) 58 | ) 59 | ); 60 | let quantB = Math.floor( 61 | Math.max( 62 | 0, 63 | Math.min(18, Math.floor(signPow(value[2] / maximumValue, 0.5) * 9 + 9.5)) 64 | ) 65 | ); 66 | 67 | return quantR * 19 * 19 + quantG * 19 + quantB; 68 | }; 69 | 70 | const encode = ( 71 | pixels: Uint8ClampedArray, 72 | width: number, 73 | height: number, 74 | componentX: number, 75 | componentY: number 76 | ): string => { 77 | if (componentX < 1 || componentX > 9 || componentY < 1 || componentY > 9) { 78 | throw new ValidationError("BlurHash must have between 1 and 9 components"); 79 | } 80 | if (width * height * 4 !== pixels.length) { 81 | throw new ValidationError("Width and height must match the pixels array"); 82 | } 83 | 84 | let factors: Array<[number, number, number]> = []; 85 | for (let y = 0; y < componentY; y++) { 86 | for (let x = 0; x < componentX; x++) { 87 | const normalisation = x == 0 && y == 0 ? 1 : 2; 88 | const factor = multiplyBasisFunction( 89 | pixels, 90 | width, 91 | height, 92 | (i: number, j: number) => 93 | normalisation * 94 | Math.cos((Math.PI * x * i) / width) * 95 | Math.cos((Math.PI * y * j) / height) 96 | ); 97 | factors.push(factor); 98 | } 99 | } 100 | 101 | const dc = factors[0]; 102 | const ac = factors.slice(1); 103 | 104 | let hash = ""; 105 | 106 | let sizeFlag = componentX - 1 + (componentY - 1) * 9; 107 | hash += encode83(sizeFlag, 1); 108 | 109 | let maximumValue: number; 110 | if (ac.length > 0) { 111 | let actualMaximumValue = Math.max(...ac.map((val) => Math.max(...val))); 112 | let quantisedMaximumValue = Math.floor( 113 | Math.max(0, Math.min(82, Math.floor(actualMaximumValue * 166 - 0.5))) 114 | ); 115 | maximumValue = (quantisedMaximumValue + 1) / 166; 116 | hash += encode83(quantisedMaximumValue, 1); 117 | } else { 118 | maximumValue = 1; 119 | hash += encode83(0, 1); 120 | } 121 | 122 | hash += encode83(encodeDC(dc), 4); 123 | 124 | ac.forEach((factor) => { 125 | hash += encode83(encodeAC(factor, maximumValue), 2); 126 | }); 127 | 128 | return hash; 129 | }; 130 | 131 | export default encode; 132 | -------------------------------------------------------------------------------- /TypeScript/src/error.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = "ValidationError"; 5 | this.message = message; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TypeScript/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as decode, isBlurhashValid } from "./decode"; 2 | export { default as encode } from "./encode"; 3 | export * from "./error"; 4 | -------------------------------------------------------------------------------- /TypeScript/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const sRGBToLinear = (value: number) => { 2 | let v = value / 255; 3 | if (v <= 0.04045) { 4 | return v / 12.92; 5 | } else { 6 | return Math.pow((v + 0.055) / 1.055, 2.4); 7 | } 8 | }; 9 | 10 | export const linearTosRGB = (value: number) => { 11 | let v = Math.max(0, Math.min(1, value)); 12 | if (v <= 0.0031308) { 13 | return Math.trunc(v * 12.92 * 255 + 0.5); 14 | } else { 15 | return Math.trunc((1.055 * Math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5); 16 | } 17 | }; 18 | 19 | export const sign = (n: number) => (n < 0 ? -1 : 1); 20 | 21 | export const signPow = (val: number, exp: number) => 22 | sign(val) * Math.pow(Math.abs(val), exp); 23 | -------------------------------------------------------------------------------- /TypeScript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es5", "dom"], 6 | "declaration": true, 7 | "outDir": "./dist/", 8 | "sourceMap": true, 9 | "noImplicitAny": true 10 | }, 11 | "include": ["src/index.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /TypeScript/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | name: "main", 6 | entry: ["./src/index.ts"], 7 | outDir: "./dist", 8 | format: ["cjs", "esm"], 9 | legacyOutput: true, 10 | sourcemap: true, 11 | clean: true, 12 | splitting: false, 13 | dts: false, 14 | minify: true, 15 | }, 16 | { 17 | name: "esm", 18 | entry: ["./src/index.ts"], 19 | outDir: "./dist", 20 | format: ["esm"], 21 | legacyOutput: false, 22 | sourcemap: true, 23 | clean: true, 24 | splitting: false, 25 | dts: false, 26 | minify: true, 27 | }, 28 | { 29 | name: "typedefs", 30 | entry: ["./src/index.ts"], 31 | outDir: "./dist", 32 | clean: false, 33 | dts: { 34 | only: true, 35 | }, 36 | }, 37 | ]); 38 | -------------------------------------------------------------------------------- /TypeScript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/demo.ts', 5 | devtool: 'inline-source-map', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: 'ts-loader', 11 | exclude: /node_modules/ 12 | } 13 | ] 14 | }, 15 | resolve: { 16 | extensions: [ '.tsx', '.ts', '.js' ] 17 | }, 18 | output: { 19 | filename: 'demo.js', 20 | path: path.resolve(__dirname, 'dist') 21 | }, 22 | devServer: { 23 | static: path.join(__dirname, "demo"), 24 | compress: true, 25 | port: 9000 26 | } 27 | }; -------------------------------------------------------------------------------- /Website/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "> 0.25%, not dead", 7 | "modules": false 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "overrides": [ 4 | { 5 | "files": ["*.js"], 6 | "options": { 7 | "singleQuote": true, 8 | "trailingComma": "all" 9 | } 10 | }, 11 | { 12 | "files": ["webpack.config.js"], 13 | "options": { 14 | "trailingComma": "es5" 15 | } 16 | }, 17 | { 18 | "files": ["*.css", "*.scss"], 19 | "options": { 20 | "parser": "scss", 21 | "singleQuote": true 22 | } 23 | }, 24 | { 25 | "files": "*.json", 26 | "options": { 27 | "parser": "json" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /Website/Readme.md: -------------------------------------------------------------------------------- 1 | # blurha.sh 2 | 3 | Website for blurhash lives here. 4 | 5 | ## Developing 6 | 7 | `npm start` runs the development server 8 | 9 | `npm deploy` builds the site, moves the contents into gh-pages branch and pushes it. -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_0_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_0_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_0_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_0_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_0_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_0_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_0_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_0_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_1_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_1_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_1_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_1_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_1_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_1_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_1_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_1_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_2_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_2_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_2_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_2_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_2_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_2_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_2_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_2_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_3_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_3_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_3_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_3_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_3_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_3_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_3_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_3_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_4_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_4_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_4_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_4_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_4_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_4_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_4_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_4_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_5_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_5_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_5_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_5_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_5_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_5_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_5_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_5_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_6_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_6_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_6_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_6_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_6_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_6_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_6_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_6_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_7_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_7_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_7_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_7_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_7_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_7_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_7_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_7_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_8_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_8_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_8_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_8_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_8_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_8_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_8_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_8_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_9_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_9_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_9_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_9_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_9_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_9_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_9_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_9_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_A_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_A_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_A_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_A_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_A_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_A_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_A_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_A_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_B_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_B_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_B_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_B_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_B_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_B_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_B_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_B_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_C_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_C_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_C_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_C_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_C_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_C_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_C_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_C_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_D_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_D_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_D_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_D_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_D_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_D_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_D_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_D_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_E_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_E_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_E_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_E_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_E_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_E_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_E_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_E_0.woff2 -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_F_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_F_0.eot -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_F_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_F_0.ttf -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_F_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_F_0.woff -------------------------------------------------------------------------------- /Website/assets/fonts/averta/346526_F_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/fonts/averta/346526_F_0.woff2 -------------------------------------------------------------------------------- /Website/assets/images/Bad_screen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/Bad_screen@2x.png -------------------------------------------------------------------------------- /Website/assets/images/Good_screen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/Good_screen@2x.png -------------------------------------------------------------------------------- /Website/assets/images/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Website/assets/images/get-started-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/get-started-bg.jpg -------------------------------------------------------------------------------- /Website/assets/images/iPhone-X-Silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/iPhone-X-Silver.png -------------------------------------------------------------------------------- /Website/assets/images/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/img1.jpg -------------------------------------------------------------------------------- /Website/assets/images/img2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/img2.jpg -------------------------------------------------------------------------------- /Website/assets/images/img3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/img3.jpg -------------------------------------------------------------------------------- /Website/assets/images/img4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/img4.jpg -------------------------------------------------------------------------------- /Website/assets/images/img5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/blurhash/712a47f946b98c30097eb1ada086ea00b18681ec/Website/assets/images/img5.jpg -------------------------------------------------------------------------------- /Website/assets/svg/wolt-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /Website/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit #abort if any command fails 3 | me=$(basename "$0") 4 | 5 | help_message="\ 6 | Usage: $me [-c FILE] [] 7 | Deploy generated files to a git branch. 8 | 9 | Options: 10 | 11 | -h, --help Show this help information. 12 | -v, --verbose Increase verbosity. Useful for debugging. 13 | -e, --allow-empty Allow deployment of an empty directory. 14 | -m, --message MESSAGE Specify the message used when committing on the 15 | deploy branch. 16 | -n, --no-hash Don't append the source commit's hash to the deploy 17 | commit's message. 18 | -c, --config-file PATH Override default & environment variables' values 19 | with those in set in the file at 'PATH'. Must be the 20 | first option specified. 21 | 22 | Variables: 23 | 24 | GIT_DEPLOY_DIR Folder path containing the files to deploy. 25 | GIT_DEPLOY_BRANCH Commit deployable files to this branch. 26 | GIT_DEPLOY_REPO Push the deploy branch to this repository. 27 | 28 | These variables have default values defined in the script. The defaults can be 29 | overridden by environment variables. Any environment variables are overridden 30 | by values set in a '.env' file (if it exists), and in turn by those set in a 31 | file specified by the '--config-file' option." 32 | 33 | parse_args() { 34 | # Set args from a local environment file. 35 | if [ -e ".env" ]; then 36 | source .env 37 | fi 38 | 39 | # Set args from file specified on the command-line. 40 | if [[ $1 = "-c" || $1 = "--config-file" ]]; then 41 | source "$2" 42 | shift 2 43 | fi 44 | 45 | # Parse arg flags 46 | # If something is exposed as an environment variable, set/overwrite it 47 | # here. Otherwise, set/overwrite the internal variable instead. 48 | while : ; do 49 | if [[ $1 = "-h" || $1 = "--help" ]]; then 50 | echo "$help_message" 51 | return 0 52 | elif [[ $1 = "-v" || $1 = "--verbose" ]]; then 53 | verbose=true 54 | shift 55 | elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then 56 | allow_empty=true 57 | shift 58 | elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then 59 | commit_message=$2 60 | shift 2 61 | elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then 62 | GIT_DEPLOY_APPEND_HASH=false 63 | shift 64 | else 65 | break 66 | fi 67 | done 68 | 69 | # Set internal option vars from the environment and arg flags. All internal 70 | # vars should be declared here, with sane defaults if applicable. 71 | 72 | # Source directory & target branch. 73 | deploy_directory=${GIT_DEPLOY_DIR:-dist} 74 | deploy_branch=${GIT_DEPLOY_BRANCH:-gh-pages} 75 | 76 | #if no user identity is already set in the current git environment, use this: 77 | default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} 78 | default_email=${GIT_DEPLOY_EMAIL:-} 79 | 80 | #repository to deploy to. must be readable and writable. 81 | repo=${GIT_DEPLOY_REPO:-origin} 82 | 83 | #append commit hash to the end of message by default 84 | append_hash=${GIT_DEPLOY_APPEND_HASH:-true} 85 | } 86 | 87 | main() { 88 | parse_args "$@" 89 | 90 | enable_expanded_output 91 | 92 | if ! git diff --exit-code --quiet --cached; then 93 | echo Aborting due to uncommitted changes in the index >&2 94 | return 1 95 | fi 96 | 97 | commit_title=`git log -n 1 --format="%s" HEAD` 98 | commit_hash=` git log -n 1 --format="%H" HEAD` 99 | 100 | #default commit message uses last title if a custom one is not supplied 101 | if [[ -z $commit_message ]]; then 102 | commit_message="publish: $commit_title" 103 | fi 104 | 105 | #append hash to commit message unless no hash flag was found 106 | if [ $append_hash = true ]; then 107 | commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" 108 | fi 109 | 110 | previous_branch=`git rev-parse --abbrev-ref HEAD` 111 | 112 | if [ ! -d "$deploy_directory" ]; then 113 | echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 114 | return 1 115 | fi 116 | 117 | # must use short form of flag in ls for compatibility with OS X and BSD 118 | if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then 119 | echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 120 | return 1 121 | fi 122 | 123 | if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then 124 | # deploy_branch exists in $repo; make sure we have the latest version 125 | 126 | disable_expanded_output 127 | git fetch --force $repo $deploy_branch:$deploy_branch 128 | enable_expanded_output 129 | fi 130 | 131 | # check if deploy_branch exists locally 132 | if git show-ref --verify --quiet "refs/heads/$deploy_branch" 133 | then incremental_deploy 134 | else initial_deploy 135 | fi 136 | 137 | restore_head 138 | } 139 | 140 | initial_deploy() { 141 | git --work-tree "$deploy_directory" checkout --orphan $deploy_branch 142 | git --work-tree "$deploy_directory" add --all 143 | commit+push 144 | } 145 | 146 | incremental_deploy() { 147 | #make deploy_branch the current branch 148 | git symbolic-ref HEAD refs/heads/$deploy_branch 149 | #put the previously committed contents of deploy_branch into the index 150 | git --work-tree "$deploy_directory" reset --mixed --quiet 151 | git --work-tree "$deploy_directory" add --all 152 | 153 | set +o errexit 154 | diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? 155 | set -o errexit 156 | case $diff in 157 | 0) echo No changes to files in $deploy_directory. Skipping commit.;; 158 | 1) commit+push;; 159 | *) 160 | echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to master, use: git symbolic-ref HEAD refs/heads/master && git reset --mixed >&2 161 | return $diff 162 | ;; 163 | esac 164 | } 165 | 166 | commit+push() { 167 | set_user_id 168 | git --work-tree "$deploy_directory" commit -m "$commit_message" 169 | 170 | disable_expanded_output 171 | #--quiet is important here to avoid outputting the repo URL, which may contain a secret token 172 | git push --quiet $repo $deploy_branch 173 | enable_expanded_output 174 | } 175 | 176 | #echo expanded commands as they are executed (for debugging) 177 | enable_expanded_output() { 178 | if [ $verbose ]; then 179 | set -o xtrace 180 | set +o verbose 181 | fi 182 | } 183 | 184 | #this is used to avoid outputting the repo URL, which may contain a secret token 185 | disable_expanded_output() { 186 | if [ $verbose ]; then 187 | set +o xtrace 188 | set -o verbose 189 | fi 190 | } 191 | 192 | set_user_id() { 193 | if [[ -z `git config user.name` ]]; then 194 | git config user.name "$default_username" 195 | fi 196 | if [[ -z `git config user.email` ]]; then 197 | git config user.email "$default_email" 198 | fi 199 | } 200 | 201 | restore_head() { 202 | if [[ $previous_branch = "HEAD" ]]; then 203 | #we weren't on any branch before, so just set HEAD back to the commit it was on 204 | git update-ref --no-deref HEAD $commit_hash $deploy_branch 205 | else 206 | git symbolic-ref HEAD refs/heads/$previous_branch 207 | fi 208 | 209 | git reset --mixed 210 | } 211 | 212 | filter() { 213 | sed -e "s|$repo|\$repo|g" 214 | } 215 | 216 | sanitize() { 217 | "$@" 2> >(filter 1>&2) | filter 218 | } 219 | 220 | [[ $1 = --source-only ]] || main "$@" -------------------------------------------------------------------------------- /Website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "website", 4 | "version": "1.0.0", 5 | "description": "Website for BlurHash promo", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "start": "webpack-dev-server --mode development", 9 | "build": "webpack --mode production", 10 | "clean": "rm -rf dist/*", 11 | "deploy": "npm run clean && npm run build && echo 'blurha.sh' > dist/CNAME && ./deploy.sh" 12 | }, 13 | "author": { 14 | "name": "woltapp", 15 | "url": "https://github.com/woltapp" 16 | }, 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@babel/cli": "^7.18.6", 20 | "@babel/core": "^7.18.6", 21 | "@babel/preset-env": "^7.18.6", 22 | "babel-loader": "^8.2.5", 23 | "css-loader": "^6.7.1", 24 | "file-loader": "^6.2.0", 25 | "html-webpack-plugin": "5.5.0", 26 | "mini-css-extract-plugin": "2.6.1", 27 | "node-sass": "7.0.3", 28 | "prettier": "2.7.1", 29 | "sass-loader": "13.0.2", 30 | "style-loader": "3.3.1", 31 | "webpack": "5.73.0", 32 | "webpack-cli": "4.10.0", 33 | "webpack-dev-server": "4.9.3" 34 | }, 35 | "dependencies": { 36 | "blurhash": "file:../TypeScript", 37 | "@babel/runtime": "7.18.6", 38 | "smoothscroll-polyfill": "0.4.4", 39 | "velocity-animate": "1.5.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Website/src/constants.js: -------------------------------------------------------------------------------- 1 | const imageHashes = [ 2 | 'LEHV6njZ2ykUpyoKadR*.8kCMdnj', 3 | 'LHF5]+c[^6#M@-5b,1J5@[or[kA{', 4 | 'L6Pj0^xZ.A.S_Nt7t7R+*0o}DgQ-', 5 | 'LKO2?U%2Tw=_]~VeVZRi};RPxuwH', 6 | 'LPPGdFog?wt7?HofM|R+OGRjr;xu', 7 | ]; 8 | 9 | export { imageHashes }; 10 | -------------------------------------------------------------------------------- /Website/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | 3 | import Hero from './sections/hero'; 4 | import Demo from './sections/demo'; 5 | 6 | Hero(); 7 | Demo(); -------------------------------------------------------------------------------- /Website/src/sections/demo.js: -------------------------------------------------------------------------------- 1 | import { encode, decode } from 'blurhash'; 2 | 3 | const blurhashElement = document.getElementById('demo-blurhash'); 4 | const canvas = document.getElementById('demo-canvas'); 5 | const originalCanvas = document.getElementById('original-canvas'); 6 | const fileInput = document.getElementById('file-upload'); 7 | const componentXElement = document.getElementById('component-x'); 8 | const componentYElement = document.getElementById('component-y'); 9 | const predefined = document.querySelector('.predefined'); 10 | 11 | function render() { 12 | const blurhash = blurhashElement.textContent; 13 | if (blurhash) { 14 | const pixels = decode(blurhash, 32, 32); 15 | if (pixels) { 16 | blurhashElement.classList.remove('error'); 17 | const ctx = canvas.getContext('2d'); 18 | 19 | const imageData = new ImageData(pixels, 32, 32); 20 | ctx.putImageData(imageData, 0, 0); 21 | } else { 22 | blurhashElement.classList.add('error'); 23 | } 24 | } 25 | } 26 | 27 | function clamp(n) { 28 | return isNaN(n) ? 1 : Math.min(9, Math.max(1, n)); 29 | } 30 | 31 | function renderSelectedFile() { 32 | const file = fileInput.files[0]; 33 | if (file) { 34 | var img = new Image(); 35 | originalCanvas.classList.add('visible'); 36 | img.onload = function () { 37 | renderImage(img); 38 | }; 39 | img.src = URL.createObjectURL(fileInput.files[0]); 40 | } 41 | } 42 | 43 | function renderImage(img) { 44 | const ctx = originalCanvas.getContext('2d'); 45 | 46 | ctx.drawImage(img, 0, 0, originalCanvas.width, originalCanvas.height); 47 | URL.revokeObjectURL(img.src); 48 | 49 | setTimeout(renderBlurhash, 0); 50 | } 51 | 52 | function renderBlurhash() { 53 | const ctx = originalCanvas.getContext('2d'); 54 | const componentX = clamp(+componentXElement.value); 55 | const componentY = clamp(+componentYElement.value); 56 | 57 | const imageData = ctx.getImageData(0, 0, originalCanvas.width, originalCanvas.height); 58 | const blurhash = encode( 59 | imageData.data, 60 | imageData.width, 61 | imageData.height, 62 | componentX, 63 | componentY, 64 | ); 65 | blurhashElement.textContent = blurhash; 66 | render(); 67 | } 68 | 69 | function renderSelectedImage() { 70 | console.log('renderSelectedImage'); 71 | const firstPredefinedImage = document.querySelector('.predefined input:checked + img'); 72 | originalCanvas.classList.remove('visible'); 73 | fileInput.value = ''; 74 | requestAnimationFrame(() => renderImage(firstPredefinedImage)); 75 | } 76 | 77 | blurhashElement.addEventListener('change', render); 78 | blurhashElement.addEventListener('keyup', render); 79 | fileInput.addEventListener('change', renderSelectedFile); 80 | componentXElement.addEventListener('keyup', renderBlurhash); 81 | componentYElement.addEventListener('keyup', renderBlurhash); 82 | predefined.addEventListener('change', renderSelectedImage); 83 | originalCanvas.addEventListener('click', renderSelectedImage); 84 | 85 | export default function () { 86 | renderSelectedImage(); 87 | } 88 | -------------------------------------------------------------------------------- /Website/src/sections/hero.js: -------------------------------------------------------------------------------- 1 | import { imageHashes } from '../constants'; 2 | import { decode } from 'blurhash'; 3 | import Velocity from 'velocity-animate'; 4 | import smoothscroll from 'smoothscroll-polyfill'; 5 | 6 | // kick off the polyfill! 7 | smoothscroll.polyfill(); 8 | 9 | function hero() { 10 | const images = document.getElementsByClassName('image-bg'); 11 | const imageContainer = document.getElementsByClassName('imagesContainer'); 12 | const content = document.getElementsByClassName('content'); 13 | 14 | function render(canvas, blurhash) { 15 | if (blurhash) { 16 | const pixels = decode(blurhash, 32, 32); 17 | if (pixels) { 18 | const ctx = canvas.getContext('2d'); 19 | 20 | const imageData = new ImageData(pixels, 32, 32); 21 | ctx.putImageData(imageData, 0, 0); 22 | } 23 | } 24 | } 25 | drawBlurHash(); 26 | function drawBlurHash() { 27 | if (!document.readyState === 'complete') { 28 | return; 29 | } 30 | init(); 31 | const canvases = document.getElementsByClassName('image-canvas-bg'); 32 | 33 | if (canvases && canvases.length) { 34 | for (let i = 0; i < canvases.length; i++) { 35 | render(canvases[i], imageHashes[i]); 36 | } 37 | } 38 | startAnimation(); 39 | } 40 | 41 | function init() { 42 | Velocity({ 43 | elements: imageContainer, 44 | properties: { translateY: '25%' }, 45 | options: { 46 | duration: 0, 47 | }, 48 | }); 49 | Velocity({ 50 | elements: content, 51 | properties: { opacity: 0 }, 52 | options: { 53 | duration: 0, 54 | }, 55 | }); 56 | } 57 | 58 | function startAnimation() { 59 | for (let i = 0; i < images.length; i++) { 60 | images[i].classList.add('animateImages'); 61 | } 62 | Velocity({ 63 | elements: imageContainer, 64 | properties: { translateY: '0%' }, 65 | options: { 66 | duration: 750, 67 | delay: 500, 68 | easing: 'easeInOutCubic', 69 | }, 70 | }); 71 | Velocity({ 72 | elements: content, 73 | properties: { opacity: 1 }, 74 | options: { 75 | duration: 250, 76 | delay: 1000, 77 | complete: startParallax, 78 | }, 79 | }); 80 | } 81 | 82 | const translateY = (amount) => `translate3d(0%, ${amount}px, 0)`; 83 | 84 | function startParallax() { 85 | window.addEventListener('scroll', scrollImages); 86 | } 87 | 88 | function scrollImages(e) { 89 | const transform = window.pageYOffset * 0.5; 90 | imageContainer[0].style.transform = translateY(transform); 91 | imageContainer[0].style.webkitTransform = translateY(transform); 92 | } 93 | 94 | document.getElementById('get-started').addEventListener('click', () => { 95 | document.querySelector('.why-blurhash').scrollIntoView({ behavior: 'smooth' }); 96 | }); 97 | } 98 | 99 | export default hero; 100 | -------------------------------------------------------------------------------- /Website/src/styles/base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | @include fluid-type(font-size, 20rem, 100rem, 0.8rem, 1rem); 4 | } 5 | 6 | body { 7 | width: 100%; 8 | min-height: 100vh; 9 | background: #fff; 10 | min-width: 320px; 11 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 12 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 13 | -webkit-tap-highlight-color: transparent; 14 | } 15 | 16 | strong, 17 | label, 18 | input, 19 | textarea, 20 | input, 21 | button { 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 23 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 24 | } 25 | ::-webkit-input-placeholder, 26 | ::-moz-placeholder, 27 | :-ms-input-placeholder, 28 | input:-moz-placeholder { 29 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 30 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 31 | color: $text-grey-light; 32 | } 33 | 34 | body, 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5, 40 | h6, 41 | p, 42 | a, 43 | li, 44 | strong, 45 | label, 46 | input, 47 | textarea { 48 | color: $dark-grey; 49 | line-height: 1.4; 50 | text-rendering: optimizeLegibility; 51 | } 52 | 53 | h1 { 54 | font-size: 2rem; 55 | font-weight: 700; 56 | } 57 | h2 { 58 | font-size: 1.8rem; 59 | font-weight: 700; 60 | } 61 | h3 { 62 | font-size: 1.6rem; 63 | font-weight: 700; 64 | } 65 | h4 { 66 | font-size: 1.4rem; 67 | font-weight: 700; 68 | } 69 | h5 { 70 | font-size: 1.2rem; 71 | font-weight: 700; 72 | } 73 | h6 { 74 | font-size: 1rem; 75 | font-weight: 700; 76 | } 77 | 78 | body > svg { 79 | display: none; 80 | } 81 | 82 | p, 83 | label, 84 | textarea, 85 | input, 86 | label, 87 | select, 88 | button, 89 | textarea { 90 | font-size: 1rem; 91 | } 92 | -------------------------------------------------------------------------------- /Website/src/styles/resets.scss: -------------------------------------------------------------------------------- 1 | /* ---------------------------------------------------------------------------------------------------- 2 | 3 | Super Form Reset 4 | 5 | A couple of things to watch out for: 6 | 7 | - IE8: If a text input doesn't have padding on all sides or none the text won't be centered. 8 | - The default border sizes on text inputs in all UAs seem to be slightly different. You're better off using custom borders. 9 | - You NEED to set the font-size and family on all form elements 10 | - Search inputs need to have their appearance reset and the box-sizing set to content-box to match other UAs 11 | - You can style the upload button in webkit using ::-webkit-file-upload-button 12 | - ::-webkit-file-upload-button selectors can't be used in the same selector as normal ones. FF and IE freak out. 13 | - IE: You don't need to fake inline-block with labels and form controls in IE. They function as inline-block. 14 | - By turning off ::-webkit-search-decoration, it removes the extra whitespace on the left on search inputs 15 | 16 | ----------------------------------------------------------------------------------------------------*/ 17 | 18 | input, 19 | label, 20 | select, 21 | button, 22 | textarea { 23 | margin: 0; 24 | border: 0; 25 | padding: 0; 26 | display: inline-block; 27 | vertical-align: middle; 28 | white-space: normal; 29 | background: none; 30 | line-height: 1; 31 | 32 | /* Browsers have different default form fonts */ 33 | font-size: 13px; 34 | font-family: Arial; 35 | } 36 | 37 | /* Remove the stupid outer glow in Webkit */ 38 | input:focus { 39 | outline: 0; 40 | } 41 | 42 | /* Box Sizing Reset 43 | -----------------------------------------------*/ 44 | 45 | /* All of our custom controls should be what we expect them to be */ 46 | input, 47 | textarea { 48 | -webkit-box-sizing: content-box; 49 | -moz-box-sizing: content-box; 50 | box-sizing: content-box; 51 | } 52 | 53 | /* These elements are usually rendered a certain way by the browser */ 54 | button, 55 | input[type='reset'], 56 | input[type='button'], 57 | input[type='submit'], 58 | input[type='checkbox'], 59 | input[type='radio'], 60 | select { 61 | -webkit-box-sizing: border-box; 62 | -moz-box-sizing: border-box; 63 | box-sizing: border-box; 64 | } 65 | 66 | /* Text Inputs 67 | -----------------------------------------------*/ 68 | 69 | input[type='date'], 70 | input[type='datetime'], 71 | input[type='datetime-local'], 72 | input[type='email'], 73 | input[type='month'], 74 | input[type='number'], 75 | input[type='password'], 76 | input[type='range'], 77 | input[type='search'], 78 | input[type='tel'], 79 | input[type='text'], 80 | input[type='time'], 81 | input[type='url'], 82 | input[type='week'] { 83 | } 84 | 85 | /* Button Controls 86 | -----------------------------------------------*/ 87 | 88 | input[type='checkbox'], 89 | input[type='radio'] { 90 | width: 13px; 91 | height: 13px; 92 | } 93 | 94 | /* File Uploads 95 | -----------------------------------------------*/ 96 | 97 | input[type='file'] { 98 | } 99 | 100 | /* Search Input 101 | -----------------------------------------------*/ 102 | 103 | /* Make webkit render the search input like a normal text field */ 104 | input[type='search'] { 105 | -webkit-appearance: textfield; 106 | -webkit-box-sizing: content-box; 107 | } 108 | 109 | /* Turn off the recent search for webkit. It adds about 15px padding on the left */ 110 | ::-webkit-search-decoration { 111 | display: none; 112 | } 113 | 114 | /* Buttons 115 | -----------------------------------------------*/ 116 | 117 | button, 118 | input[type='reset'], 119 | input[type='button'], 120 | input[type='submit'] { 121 | /* Fix IE7 display bug */ 122 | overflow: visible; 123 | width: auto; 124 | } 125 | 126 | /* IE8 and FF freak out if this rule is within another selector */ 127 | ::-webkit-file-upload-button { 128 | padding: 0; 129 | border: 0; 130 | background: none; 131 | } 132 | 133 | /* Textarea 134 | -----------------------------------------------*/ 135 | 136 | textarea { 137 | /* Move the label to the top */ 138 | vertical-align: top; 139 | 140 | /* Turn off scroll bars in IE unless needed */ 141 | overflow: auto; 142 | } 143 | 144 | /* Selects 145 | -----------------------------------------------*/ 146 | 147 | select { 148 | } 149 | 150 | select[multiple] { 151 | /* Move the label to the top */ 152 | vertical-align: top; 153 | } 154 | 155 | /* selected Foundation resets */ 156 | /* copied from node_modules/foundation-sites/scss/_global.scss */ 157 | 158 | html { 159 | box-sizing: border-box; 160 | } 161 | 162 | // Set box-sizing globally to handle padding and border widths 163 | *, 164 | *::before, 165 | *::after { 166 | box-sizing: inherit; 167 | } 168 | 169 | // Default body styles 170 | body { 171 | margin: 0; 172 | padding: 0; 173 | 174 | -webkit-font-smoothing: antialiased; 175 | -moz-osx-font-smoothing: grayscale; 176 | } 177 | 178 | img { 179 | // Get rid of gap under images by making them display: inline-block; by default 180 | display: inline-block; 181 | vertical-align: middle; 182 | 183 | // Grid defaults to get images and embeds to work properly 184 | max-width: 100%; 185 | height: auto; 186 | -ms-interpolation-mode: bicubic; 187 | } 188 | -------------------------------------------------------------------------------- /Website/src/styles/utils.scss: -------------------------------------------------------------------------------- 1 | @mixin fluid-type($properties, $min-vw, $max-vw, $min-value, $max-value) { 2 | & { 3 | @each $property in $properties { 4 | #{$property}: $min-value; 5 | } 6 | 7 | @media screen and (min-width: $min-vw) { 8 | @each $property in $properties { 9 | #{$property}: calc( 10 | #{$min-value} + 11 | #{strip-unit($max-value - $min-value)} * 12 | (100vw - #{$min-vw}) / 13 | #{strip-unit($max-vw - $min-vw)} 14 | ); 15 | } 16 | } 17 | 18 | @media screen and (min-width: $max-vw) { 19 | @each $property in $properties { 20 | #{$property}: $max-value; 21 | } 22 | } 23 | } 24 | } 25 | 26 | @function strip-unit($value) { 27 | @return $value / ($value * 0 + 1); 28 | } 29 | 30 | @function top($px) { 31 | @return $px / 768 * 100%; 32 | } 33 | 34 | @function left($px) { 35 | @return $px / 1440 * 100%; 36 | } 37 | -------------------------------------------------------------------------------- /Website/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | @function rem($pixels, $context: 16px) { 2 | $pixels: strip-units($pixels); 3 | $context: strip-units($context); 4 | @return ($pixels / $context) * 1rem; 5 | } 6 | 7 | $wolt-id-blue: #0019ff; 8 | $error-color-id: #ff1900; 9 | $header-height: 70px; 10 | // Brand 11 | $wolt-blue: #0065aa; 12 | $wolt-blue-light: #4a90e2; 13 | $wolt-blue-lighter: rgba(74, 144, 226, 0.5); 14 | $wolt-blue-gradient: linear-gradient(-180deg, #0077c8 0%, #0065aa 100%); 15 | $wolt-black: #141414; 16 | 17 | // Text 18 | $offwhite: #eff1f3; 19 | $offwhite-light: #f7f8f9; 20 | $dark-grey: #202125; 21 | $text-color: #404040; 22 | $text-grey: #838383; 23 | $text-grey-light: #acacac; 24 | -------------------------------------------------------------------------------- /Website/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | 6 | // Is the current build a development build 7 | const env = process.env.NODE_ENV; 8 | 9 | const IS_DEV = !env ? true : env; 10 | const dirNode = 'node_modules'; 11 | const dirApp = path.join(__dirname, 'src'); 12 | const dirAssets = path.join(__dirname, 'assets'); 13 | 14 | /** 15 | * Webpack Configuration 16 | */ 17 | module.exports = { 18 | entry: { 19 | blurhash: path.join(dirApp, 'index'), 20 | }, 21 | resolve: { 22 | modules: [dirNode, dirApp, dirAssets], 23 | }, 24 | output: { 25 | filename: '[name].[hash].js', 26 | }, 27 | devtool: IS_DEV ? 'cheap-module-source-map' : 'source-map', 28 | plugins: [ 29 | new MiniCssExtractPlugin({ 30 | filename: '[name].[hash].css', 31 | }), 32 | new webpack.DefinePlugin({ 33 | IS_DEV: IS_DEV, 34 | }), 35 | new webpack.HotModuleReplacementPlugin(), 36 | new HtmlWebpackPlugin({ 37 | template: path.join(dirApp, 'index.ejs'), 38 | title: 'BlurHash', 39 | }), 40 | ], 41 | module: { 42 | rules: [ 43 | // BABEL 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | exclude: /(node_modules)/, 48 | options: { 49 | compact: true, 50 | }, 51 | }, 52 | 53 | // CSS / SASS 54 | { 55 | test: /\.scss/, 56 | use: [ 57 | IS_DEV && MiniCssExtractPlugin.loader, 58 | { 59 | loader: 'css-loader', 60 | options: { 61 | sourceMap: IS_DEV, 62 | }, 63 | }, 64 | { 65 | loader: 'sass-loader', 66 | options: { 67 | sourceMap: IS_DEV, 68 | sassOptions: { 69 | includePaths: [dirAssets], 70 | }, 71 | }, 72 | }, 73 | ], 74 | }, 75 | // IMAGES 76 | { 77 | test: /\.(jpe?g|png|gif|svg)$/, 78 | type: 'asset/resource', 79 | }, 80 | 81 | // FONTS 82 | { 83 | test: /\.(eot|otf|woff2?|ttf)[\?]?.*$/, // eslint-disable-line 84 | type: 'asset/resource', 85 | }, 86 | ], 87 | }, 88 | devServer: { 89 | host: '0.0.0.0', 90 | historyApiFallback: true, 91 | hot: true, 92 | }, 93 | }; 94 | --------------------------------------------------------------------------------