├── .gitignore ├── LICENSE.md ├── README.md ├── examples ├── browser │ └── index.html ├── flower.jpg ├── ios │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Example.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift ├── macos │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── MainMenu.xib │ └── Example.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── node │ ├── index.js │ ├── package-lock.json │ └── package.json └── rust │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── main.rs ├── java └── com │ └── madebyevan │ └── thumbhash │ └── ThumbHash.java ├── js ├── README.md ├── package.json ├── thumbhash.d.ts └── thumbhash.js ├── rust ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── swift └── ThumbHash.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | xcuserdata/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Evan Wallace 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThumbHash 2 | 3 | A very compact representation of a placeholder for an image. Store it inline with your data and show it while the real image is loading for a smoother loading experience. It's similar to [BlurHash](https://github.com/woltapp/blurhash) but with the following advantages: 4 | 5 | * Encodes more detail in the same space 6 | * Also encodes the aspect ratio 7 | * Gives more accurate colors 8 | * Supports images with alpha 9 | 10 | Despite doing all of these additional things, the code for ThumbHash is still similar in complexity to the code for BlurHash. One potential drawback compared to BlurHash is that the parameters of the algorithm are not configurable (everything is automatically configured). 11 | 12 | A demo and more information is available here: https://evanw.github.io/thumbhash/. 13 | 14 | ## Implementations 15 | 16 | This repo contains implementations for the following languages: 17 | 18 | * [JavaScript](./js) 19 | * [Rust](./rust) 20 | * [Swift](./swift) 21 | * [Java](./java) 22 | 23 | These additional implementations also exist outside of this repo: 24 | 25 | * Go: https://github.com/galdor/go-thumbhash 26 | * Perl: https://github.com/mauke/Image-ThumbHash 27 | * PHP: https://github.com/SRWieZ/thumbhash 28 | * Ruby: https://github.com/daibhin/thumbhash 29 | 30 | _If you want to add your own implementation here, you can send a PR that puts a link to your implementation in this README._ 31 | -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Browser Example

5 |

6 | To run this demo, serve the root repository directory using a local web server and then visit 7 | http://127.0.0.1:8000/examples/browser/ 8 | (assuming port 8000). 9 |

10 | 11 | 12 | 13 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /examples/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanw/thumbhash/a652ce6ed691242f459f468f0a8756cda3b90a82/examples/flower.jpg -------------------------------------------------------------------------------- /examples/ios/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | } 6 | -------------------------------------------------------------------------------- /examples/ios/Base.lproj/Main.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 | -------------------------------------------------------------------------------- /examples/ios/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 20796A0A29CD03A4003EBA91 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20796A0929CD03A4003EBA91 /* AppDelegate.swift */; }; 11 | 20796A0C29CD03A4003EBA91 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20796A0B29CD03A4003EBA91 /* SceneDelegate.swift */; }; 12 | 20796A0E29CD03A4003EBA91 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20796A0D29CD03A4003EBA91 /* ViewController.swift */; }; 13 | 20796A1129CD03A4003EBA91 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 20796A0F29CD03A4003EBA91 /* Main.storyboard */; }; 14 | 20796A1E29CD0432003EBA91 /* flower.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 20796A1D29CD0432003EBA91 /* flower.jpg */; }; 15 | 20796A2029CD0482003EBA91 /* ThumbHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20796A1F29CD0482003EBA91 /* ThumbHash.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 20796A0629CD03A4003EBA91 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 20796A0929CD03A4003EBA91 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 20796A0B29CD03A4003EBA91 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 22 | 20796A0D29CD03A4003EBA91 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 20796A1029CD03A4003EBA91 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 20796A1729CD03A5003EBA91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | 20796A1D29CD0432003EBA91 /* flower.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = flower.jpg; path = ../flower.jpg; sourceTree = ""; }; 26 | 20796A1F29CD0482003EBA91 /* ThumbHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ThumbHash.swift; path = ../../swift/ThumbHash.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 20796A0329CD03A4003EBA91 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 207969FD29CD03A4003EBA91 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 20796A0929CD03A4003EBA91 /* AppDelegate.swift */, 44 | 20796A0B29CD03A4003EBA91 /* SceneDelegate.swift */, 45 | 20796A0D29CD03A4003EBA91 /* ViewController.swift */, 46 | 20796A1F29CD0482003EBA91 /* ThumbHash.swift */, 47 | 20796A0F29CD03A4003EBA91 /* Main.storyboard */, 48 | 20796A1729CD03A5003EBA91 /* Info.plist */, 49 | 20796A1D29CD0432003EBA91 /* flower.jpg */, 50 | 20796A0729CD03A4003EBA91 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 20796A0729CD03A4003EBA91 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 20796A0629CD03A4003EBA91 /* Example.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | /* End PBXGroup section */ 63 | 64 | /* Begin PBXNativeTarget section */ 65 | 20796A0529CD03A4003EBA91 /* Example */ = { 66 | isa = PBXNativeTarget; 67 | buildConfigurationList = 20796A1A29CD03A5003EBA91 /* Build configuration list for PBXNativeTarget "Example" */; 68 | buildPhases = ( 69 | 20796A0229CD03A4003EBA91 /* Sources */, 70 | 20796A0329CD03A4003EBA91 /* Frameworks */, 71 | 20796A0429CD03A4003EBA91 /* Resources */, 72 | ); 73 | buildRules = ( 74 | ); 75 | dependencies = ( 76 | ); 77 | name = Example; 78 | productName = Example; 79 | productReference = 20796A0629CD03A4003EBA91 /* Example.app */; 80 | productType = "com.apple.product-type.application"; 81 | }; 82 | /* End PBXNativeTarget section */ 83 | 84 | /* Begin PBXProject section */ 85 | 207969FE29CD03A4003EBA91 /* Project object */ = { 86 | isa = PBXProject; 87 | attributes = { 88 | BuildIndependentTargetsInParallel = 1; 89 | LastSwiftUpdateCheck = 1420; 90 | LastUpgradeCheck = 1420; 91 | TargetAttributes = { 92 | 20796A0529CD03A4003EBA91 = { 93 | CreatedOnToolsVersion = 14.2; 94 | }; 95 | }; 96 | }; 97 | buildConfigurationList = 20796A0129CD03A4003EBA91 /* Build configuration list for PBXProject "Example" */; 98 | compatibilityVersion = "Xcode 14.0"; 99 | developmentRegion = en; 100 | hasScannedForEncodings = 0; 101 | knownRegions = ( 102 | en, 103 | Base, 104 | ); 105 | mainGroup = 207969FD29CD03A4003EBA91; 106 | productRefGroup = 20796A0729CD03A4003EBA91 /* Products */; 107 | projectDirPath = ""; 108 | projectRoot = ""; 109 | targets = ( 110 | 20796A0529CD03A4003EBA91 /* Example */, 111 | ); 112 | }; 113 | /* End PBXProject section */ 114 | 115 | /* Begin PBXResourcesBuildPhase section */ 116 | 20796A0429CD03A4003EBA91 /* Resources */ = { 117 | isa = PBXResourcesBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | 20796A1E29CD0432003EBA91 /* flower.jpg in Resources */, 121 | 20796A1129CD03A4003EBA91 /* Main.storyboard in Resources */, 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXResourcesBuildPhase section */ 126 | 127 | /* Begin PBXSourcesBuildPhase section */ 128 | 20796A0229CD03A4003EBA91 /* Sources */ = { 129 | isa = PBXSourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | 20796A0E29CD03A4003EBA91 /* ViewController.swift in Sources */, 133 | 20796A0A29CD03A4003EBA91 /* AppDelegate.swift in Sources */, 134 | 20796A2029CD0482003EBA91 /* ThumbHash.swift in Sources */, 135 | 20796A0C29CD03A4003EBA91 /* SceneDelegate.swift in Sources */, 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXSourcesBuildPhase section */ 140 | 141 | /* Begin PBXVariantGroup section */ 142 | 20796A0F29CD03A4003EBA91 /* Main.storyboard */ = { 143 | isa = PBXVariantGroup; 144 | children = ( 145 | 20796A1029CD03A4003EBA91 /* Base */, 146 | ); 147 | name = Main.storyboard; 148 | sourceTree = ""; 149 | }; 150 | /* End PBXVariantGroup section */ 151 | 152 | /* Begin XCBuildConfiguration section */ 153 | 20796A1829CD03A5003EBA91 /* Debug */ = { 154 | isa = XCBuildConfiguration; 155 | buildSettings = { 156 | ALWAYS_SEARCH_USER_PATHS = NO; 157 | CLANG_ANALYZER_NONNULL = YES; 158 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 159 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 160 | CLANG_ENABLE_MODULES = YES; 161 | CLANG_ENABLE_OBJC_ARC = YES; 162 | CLANG_ENABLE_OBJC_WEAK = YES; 163 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 164 | CLANG_WARN_BOOL_CONVERSION = YES; 165 | CLANG_WARN_COMMA = YES; 166 | CLANG_WARN_CONSTANT_CONVERSION = YES; 167 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 168 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 169 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 170 | CLANG_WARN_EMPTY_BODY = YES; 171 | CLANG_WARN_ENUM_CONVERSION = YES; 172 | CLANG_WARN_INFINITE_RECURSION = YES; 173 | CLANG_WARN_INT_CONVERSION = YES; 174 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 175 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 176 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 177 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 178 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 179 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 180 | CLANG_WARN_STRICT_PROTOTYPES = YES; 181 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 182 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 183 | CLANG_WARN_UNREACHABLE_CODE = YES; 184 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 185 | COPY_PHASE_STRIP = NO; 186 | DEBUG_INFORMATION_FORMAT = dwarf; 187 | ENABLE_STRICT_OBJC_MSGSEND = YES; 188 | ENABLE_TESTABILITY = YES; 189 | GCC_C_LANGUAGE_STANDARD = gnu11; 190 | GCC_DYNAMIC_NO_PIC = NO; 191 | GCC_NO_COMMON_BLOCKS = YES; 192 | GCC_OPTIMIZATION_LEVEL = 0; 193 | GCC_PREPROCESSOR_DEFINITIONS = ( 194 | "DEBUG=1", 195 | "$(inherited)", 196 | ); 197 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 198 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 199 | GCC_WARN_UNDECLARED_SELECTOR = YES; 200 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 201 | GCC_WARN_UNUSED_FUNCTION = YES; 202 | GCC_WARN_UNUSED_VARIABLE = YES; 203 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 204 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 205 | MTL_FAST_MATH = YES; 206 | ONLY_ACTIVE_ARCH = YES; 207 | SDKROOT = iphoneos; 208 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 209 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 210 | }; 211 | name = Debug; 212 | }; 213 | 20796A1929CD03A5003EBA91 /* Release */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | ALWAYS_SEARCH_USER_PATHS = NO; 217 | CLANG_ANALYZER_NONNULL = YES; 218 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 220 | CLANG_ENABLE_MODULES = YES; 221 | CLANG_ENABLE_OBJC_ARC = YES; 222 | CLANG_ENABLE_OBJC_WEAK = YES; 223 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_COMMA = YES; 226 | CLANG_WARN_CONSTANT_CONVERSION = YES; 227 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 230 | CLANG_WARN_EMPTY_BODY = YES; 231 | CLANG_WARN_ENUM_CONVERSION = YES; 232 | CLANG_WARN_INFINITE_RECURSION = YES; 233 | CLANG_WARN_INT_CONVERSION = YES; 234 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 235 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 236 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 239 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 240 | CLANG_WARN_STRICT_PROTOTYPES = YES; 241 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 242 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | COPY_PHASE_STRIP = NO; 246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 247 | ENABLE_NS_ASSERTIONS = NO; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu11; 250 | GCC_NO_COMMON_BLOCKS = YES; 251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 253 | GCC_WARN_UNDECLARED_SELECTOR = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 255 | GCC_WARN_UNUSED_FUNCTION = YES; 256 | GCC_WARN_UNUSED_VARIABLE = YES; 257 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 258 | MTL_ENABLE_DEBUG_INFO = NO; 259 | MTL_FAST_MATH = YES; 260 | SDKROOT = iphoneos; 261 | SWIFT_COMPILATION_MODE = wholemodule; 262 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 263 | VALIDATE_PRODUCT = YES; 264 | }; 265 | name = Release; 266 | }; 267 | 20796A1B29CD03A5003EBA91 /* Debug */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 271 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 272 | CODE_SIGN_STYLE = Automatic; 273 | CURRENT_PROJECT_VERSION = 1; 274 | DEVELOPMENT_TEAM = PG9AGL8FNE; 275 | GENERATE_INFOPLIST_FILE = YES; 276 | INFOPLIST_FILE = Info.plist; 277 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 278 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 279 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 280 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 281 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 282 | LD_RUNPATH_SEARCH_PATHS = ( 283 | "$(inherited)", 284 | "@executable_path/Frameworks", 285 | ); 286 | MARKETING_VERSION = 1.0; 287 | PRODUCT_BUNDLE_IDENTIFIER = com.madebyevan.Example; 288 | PRODUCT_NAME = "$(TARGET_NAME)"; 289 | SWIFT_EMIT_LOC_STRINGS = YES; 290 | SWIFT_VERSION = 5.0; 291 | TARGETED_DEVICE_FAMILY = "1,2"; 292 | }; 293 | name = Debug; 294 | }; 295 | 20796A1C29CD03A5003EBA91 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEVELOPMENT_TEAM = PG9AGL8FNE; 303 | GENERATE_INFOPLIST_FILE = YES; 304 | INFOPLIST_FILE = Info.plist; 305 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 306 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 307 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 308 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 310 | LD_RUNPATH_SEARCH_PATHS = ( 311 | "$(inherited)", 312 | "@executable_path/Frameworks", 313 | ); 314 | MARKETING_VERSION = 1.0; 315 | PRODUCT_BUNDLE_IDENTIFIER = com.madebyevan.Example; 316 | PRODUCT_NAME = "$(TARGET_NAME)"; 317 | SWIFT_EMIT_LOC_STRINGS = YES; 318 | SWIFT_VERSION = 5.0; 319 | TARGETED_DEVICE_FAMILY = "1,2"; 320 | }; 321 | name = Release; 322 | }; 323 | /* End XCBuildConfiguration section */ 324 | 325 | /* Begin XCConfigurationList section */ 326 | 20796A0129CD03A4003EBA91 /* Build configuration list for PBXProject "Example" */ = { 327 | isa = XCConfigurationList; 328 | buildConfigurations = ( 329 | 20796A1829CD03A5003EBA91 /* Debug */, 330 | 20796A1929CD03A5003EBA91 /* Release */, 331 | ); 332 | defaultConfigurationIsVisible = 0; 333 | defaultConfigurationName = Release; 334 | }; 335 | 20796A1A29CD03A5003EBA91 /* Build configuration list for PBXNativeTarget "Example" */ = { 336 | isa = XCConfigurationList; 337 | buildConfigurations = ( 338 | 20796A1B29CD03A5003EBA91 /* Debug */, 339 | 20796A1C29CD03A5003EBA91 /* Release */, 340 | ); 341 | defaultConfigurationIsVisible = 0; 342 | defaultConfigurationName = Release; 343 | }; 344 | /* End XCConfigurationList section */ 345 | }; 346 | rootObject = 207969FE29CD03A4003EBA91 /* Project object */; 347 | } 348 | -------------------------------------------------------------------------------- /examples/ios/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/ios/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/ios/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | var window: UIWindow? 5 | } 6 | -------------------------------------------------------------------------------- /examples/ios/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController { 4 | override func viewDidAppear(_ animated: Bool) { 5 | super.viewDidAppear(animated) 6 | 7 | let image = UIImage(imageLiteralResourceName: "flower.jpg") 8 | 9 | // Image to ThumbHash 10 | let thumbHash = imageToThumbHash(image: image) 11 | 12 | // ThumbHash to image 13 | let placeholder = thumbHashToImage(hash: thumbHash) 14 | 15 | // Simulate setting the placeholder first, then the full image loading later on 16 | let view = UIImageView(image: placeholder) 17 | view.contentMode = .scaleAspectFill 18 | view.clipsToBounds = true 19 | view.frame = CGRect(x: 20, y: self.view.safeAreaInsets.top + 20, width: 150, height: 200) 20 | self.view.addSubview(view) 21 | 22 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 23 | view.image = image 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/macos/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @main 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | @IBOutlet var window: NSWindow! 6 | 7 | func applicationDidFinishLaunching(_ aNotification: Notification) { 8 | let image = Bundle.main.image(forResource: "flower.jpg")! 9 | 10 | // Image to ThumbHash 11 | let thumbHash = imageToThumbHash(image: image) 12 | 13 | // ThumbHash to image 14 | let placeholder = thumbHashToImage(hash: thumbHash) 15 | 16 | // Simulate setting the placeholder first, then the full image loading later on 17 | let view = NSImageView(image: placeholder) 18 | view.imageScaling = .scaleProportionallyUpOrDown 19 | view.frame = NSRect(x: 20, y: 20, width: 150, height: 200) 20 | window.contentView = FlippedView() 21 | window.contentView!.addSubview(view) 22 | 23 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 24 | view.image = image 25 | } 26 | } 27 | } 28 | 29 | private final class FlippedView : NSView { 30 | override var isFlipped: Bool { true } 31 | } 32 | -------------------------------------------------------------------------------- /examples/macos/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | Default 539 | 540 | 541 | 542 | 543 | 544 | 545 | Left to Right 546 | 547 | 548 | 549 | 550 | 551 | 552 | Right to Left 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | Default 564 | 565 | 566 | 567 | 568 | 569 | 570 | Left to Right 571 | 572 | 573 | 574 | 575 | 576 | 577 | Right to Left 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | -------------------------------------------------------------------------------- /examples/macos/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 207969ED29CCFA80003EBA91 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207969EC29CCFA80003EBA91 /* AppDelegate.swift */; }; 11 | 207969F229CCFA81003EBA91 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 207969F029CCFA81003EBA91 /* MainMenu.xib */; }; 12 | 207969FA29CCFB23003EBA91 /* ThumbHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207969F929CCFB23003EBA91 /* ThumbHash.swift */; }; 13 | 207969FC29CCFB53003EBA91 /* flower.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 207969FB29CCFB53003EBA91 /* flower.jpg */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 207969E929CCFA80003EBA91 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | 207969EC29CCFA80003EBA91 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 19 | 207969F129CCFA81003EBA91 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 20 | 207969F929CCFB23003EBA91 /* ThumbHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ThumbHash.swift; path = ../../swift/ThumbHash.swift; sourceTree = ""; }; 21 | 207969FB29CCFB53003EBA91 /* flower.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = flower.jpg; path = ../flower.jpg; sourceTree = ""; }; 22 | /* End PBXFileReference section */ 23 | 24 | /* Begin PBXFrameworksBuildPhase section */ 25 | 207969E629CCFA80003EBA91 /* Frameworks */ = { 26 | isa = PBXFrameworksBuildPhase; 27 | buildActionMask = 2147483647; 28 | files = ( 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | 207969E029CCFA80003EBA91 = { 36 | isa = PBXGroup; 37 | children = ( 38 | 207969EC29CCFA80003EBA91 /* AppDelegate.swift */, 39 | 207969F929CCFB23003EBA91 /* ThumbHash.swift */, 40 | 207969F029CCFA81003EBA91 /* MainMenu.xib */, 41 | 207969FB29CCFB53003EBA91 /* flower.jpg */, 42 | 207969EA29CCFA80003EBA91 /* Products */, 43 | ); 44 | sourceTree = ""; 45 | }; 46 | 207969EA29CCFA80003EBA91 /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 207969E929CCFA80003EBA91 /* Example.app */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | /* End PBXGroup section */ 55 | 56 | /* Begin PBXNativeTarget section */ 57 | 207969E829CCFA80003EBA91 /* Example */ = { 58 | isa = PBXNativeTarget; 59 | buildConfigurationList = 207969F629CCFA81003EBA91 /* Build configuration list for PBXNativeTarget "Example" */; 60 | buildPhases = ( 61 | 207969E529CCFA80003EBA91 /* Sources */, 62 | 207969E629CCFA80003EBA91 /* Frameworks */, 63 | 207969E729CCFA80003EBA91 /* Resources */, 64 | ); 65 | buildRules = ( 66 | ); 67 | dependencies = ( 68 | ); 69 | name = Example; 70 | productName = Example; 71 | productReference = 207969E929CCFA80003EBA91 /* Example.app */; 72 | productType = "com.apple.product-type.application"; 73 | }; 74 | /* End PBXNativeTarget section */ 75 | 76 | /* Begin PBXProject section */ 77 | 207969E129CCFA80003EBA91 /* Project object */ = { 78 | isa = PBXProject; 79 | attributes = { 80 | BuildIndependentTargetsInParallel = 1; 81 | LastSwiftUpdateCheck = 1420; 82 | LastUpgradeCheck = 1420; 83 | TargetAttributes = { 84 | 207969E829CCFA80003EBA91 = { 85 | CreatedOnToolsVersion = 14.2; 86 | }; 87 | }; 88 | }; 89 | buildConfigurationList = 207969E429CCFA80003EBA91 /* Build configuration list for PBXProject "Example" */; 90 | compatibilityVersion = "Xcode 14.0"; 91 | developmentRegion = en; 92 | hasScannedForEncodings = 0; 93 | knownRegions = ( 94 | en, 95 | Base, 96 | ); 97 | mainGroup = 207969E029CCFA80003EBA91; 98 | productRefGroup = 207969EA29CCFA80003EBA91 /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | 207969E829CCFA80003EBA91 /* Example */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | 207969E729CCFA80003EBA91 /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | 207969FC29CCFB53003EBA91 /* flower.jpg in Resources */, 113 | 207969F229CCFA81003EBA91 /* MainMenu.xib in Resources */, 114 | ); 115 | runOnlyForDeploymentPostprocessing = 0; 116 | }; 117 | /* End PBXResourcesBuildPhase section */ 118 | 119 | /* Begin PBXSourcesBuildPhase section */ 120 | 207969E529CCFA80003EBA91 /* Sources */ = { 121 | isa = PBXSourcesBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | 207969FA29CCFB23003EBA91 /* ThumbHash.swift in Sources */, 125 | 207969ED29CCFA80003EBA91 /* AppDelegate.swift in Sources */, 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | /* End PBXSourcesBuildPhase section */ 130 | 131 | /* Begin PBXVariantGroup section */ 132 | 207969F029CCFA81003EBA91 /* MainMenu.xib */ = { 133 | isa = PBXVariantGroup; 134 | children = ( 135 | 207969F129CCFA81003EBA91 /* Base */, 136 | ); 137 | name = MainMenu.xib; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXVariantGroup section */ 141 | 142 | /* Begin XCBuildConfiguration section */ 143 | 207969F429CCFA81003EBA91 /* Debug */ = { 144 | isa = XCBuildConfiguration; 145 | buildSettings = { 146 | ALWAYS_SEARCH_USER_PATHS = NO; 147 | CLANG_ANALYZER_NONNULL = YES; 148 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 149 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 150 | CLANG_ENABLE_MODULES = YES; 151 | CLANG_ENABLE_OBJC_ARC = YES; 152 | CLANG_ENABLE_OBJC_WEAK = YES; 153 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 154 | CLANG_WARN_BOOL_CONVERSION = YES; 155 | CLANG_WARN_COMMA = YES; 156 | CLANG_WARN_CONSTANT_CONVERSION = YES; 157 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 158 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 159 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 160 | CLANG_WARN_EMPTY_BODY = YES; 161 | CLANG_WARN_ENUM_CONVERSION = YES; 162 | CLANG_WARN_INFINITE_RECURSION = YES; 163 | CLANG_WARN_INT_CONVERSION = YES; 164 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 165 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 166 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 167 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 168 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 169 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 170 | CLANG_WARN_STRICT_PROTOTYPES = YES; 171 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 172 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 173 | CLANG_WARN_UNREACHABLE_CODE = YES; 174 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 175 | COPY_PHASE_STRIP = NO; 176 | DEBUG_INFORMATION_FORMAT = dwarf; 177 | ENABLE_STRICT_OBJC_MSGSEND = YES; 178 | ENABLE_TESTABILITY = YES; 179 | GCC_C_LANGUAGE_STANDARD = gnu11; 180 | GCC_DYNAMIC_NO_PIC = NO; 181 | GCC_NO_COMMON_BLOCKS = YES; 182 | GCC_OPTIMIZATION_LEVEL = 0; 183 | GCC_PREPROCESSOR_DEFINITIONS = ( 184 | "DEBUG=1", 185 | "$(inherited)", 186 | ); 187 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 188 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 189 | GCC_WARN_UNDECLARED_SELECTOR = YES; 190 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 191 | GCC_WARN_UNUSED_FUNCTION = YES; 192 | GCC_WARN_UNUSED_VARIABLE = YES; 193 | MACOSX_DEPLOYMENT_TARGET = 13.1; 194 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 195 | MTL_FAST_MATH = YES; 196 | ONLY_ACTIVE_ARCH = YES; 197 | SDKROOT = macosx; 198 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 199 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 200 | }; 201 | name = Debug; 202 | }; 203 | 207969F529CCFA81003EBA91 /* Release */ = { 204 | isa = XCBuildConfiguration; 205 | buildSettings = { 206 | ALWAYS_SEARCH_USER_PATHS = NO; 207 | CLANG_ANALYZER_NONNULL = YES; 208 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 209 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 210 | CLANG_ENABLE_MODULES = YES; 211 | CLANG_ENABLE_OBJC_ARC = YES; 212 | CLANG_ENABLE_OBJC_WEAK = YES; 213 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 214 | CLANG_WARN_BOOL_CONVERSION = YES; 215 | CLANG_WARN_COMMA = YES; 216 | CLANG_WARN_CONSTANT_CONVERSION = YES; 217 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 218 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 219 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 220 | CLANG_WARN_EMPTY_BODY = YES; 221 | CLANG_WARN_ENUM_CONVERSION = YES; 222 | CLANG_WARN_INFINITE_RECURSION = YES; 223 | CLANG_WARN_INT_CONVERSION = YES; 224 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 225 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 226 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 227 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 228 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 229 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 230 | CLANG_WARN_STRICT_PROTOTYPES = YES; 231 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 232 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 233 | CLANG_WARN_UNREACHABLE_CODE = YES; 234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 235 | COPY_PHASE_STRIP = NO; 236 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 237 | ENABLE_NS_ASSERTIONS = NO; 238 | ENABLE_STRICT_OBJC_MSGSEND = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu11; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 242 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 243 | GCC_WARN_UNDECLARED_SELECTOR = YES; 244 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 245 | GCC_WARN_UNUSED_FUNCTION = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | MACOSX_DEPLOYMENT_TARGET = 13.1; 248 | MTL_ENABLE_DEBUG_INFO = NO; 249 | MTL_FAST_MATH = YES; 250 | SDKROOT = macosx; 251 | SWIFT_COMPILATION_MODE = wholemodule; 252 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 253 | }; 254 | name = Release; 255 | }; 256 | 207969F729CCFA81003EBA91 /* Debug */ = { 257 | isa = XCBuildConfiguration; 258 | buildSettings = { 259 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 260 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 261 | CODE_SIGN_STYLE = Automatic; 262 | COMBINE_HIDPI_IMAGES = YES; 263 | CURRENT_PROJECT_VERSION = 1; 264 | DEVELOPMENT_TEAM = PG9AGL8FNE; 265 | ENABLE_HARDENED_RUNTIME = NO; 266 | GENERATE_INFOPLIST_FILE = YES; 267 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 268 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 269 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 270 | LD_RUNPATH_SEARCH_PATHS = ( 271 | "$(inherited)", 272 | "@executable_path/../Frameworks", 273 | ); 274 | MARKETING_VERSION = 1.0; 275 | PRODUCT_BUNDLE_IDENTIFIER = com.madebyevan.Example; 276 | PRODUCT_NAME = "$(TARGET_NAME)"; 277 | SWIFT_EMIT_LOC_STRINGS = YES; 278 | SWIFT_VERSION = 5.0; 279 | }; 280 | name = Debug; 281 | }; 282 | 207969F829CCFA81003EBA91 /* Release */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 286 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 287 | CODE_SIGN_STYLE = Automatic; 288 | COMBINE_HIDPI_IMAGES = YES; 289 | CURRENT_PROJECT_VERSION = 1; 290 | DEVELOPMENT_TEAM = PG9AGL8FNE; 291 | ENABLE_HARDENED_RUNTIME = NO; 292 | GENERATE_INFOPLIST_FILE = YES; 293 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 294 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 295 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 296 | LD_RUNPATH_SEARCH_PATHS = ( 297 | "$(inherited)", 298 | "@executable_path/../Frameworks", 299 | ); 300 | MARKETING_VERSION = 1.0; 301 | PRODUCT_BUNDLE_IDENTIFIER = com.madebyevan.Example; 302 | PRODUCT_NAME = "$(TARGET_NAME)"; 303 | SWIFT_EMIT_LOC_STRINGS = YES; 304 | SWIFT_VERSION = 5.0; 305 | }; 306 | name = Release; 307 | }; 308 | /* End XCBuildConfiguration section */ 309 | 310 | /* Begin XCConfigurationList section */ 311 | 207969E429CCFA80003EBA91 /* Build configuration list for PBXProject "Example" */ = { 312 | isa = XCConfigurationList; 313 | buildConfigurations = ( 314 | 207969F429CCFA81003EBA91 /* Debug */, 315 | 207969F529CCFA81003EBA91 /* Release */, 316 | ); 317 | defaultConfigurationIsVisible = 0; 318 | defaultConfigurationName = Release; 319 | }; 320 | 207969F629CCFA81003EBA91 /* Build configuration list for PBXNativeTarget "Example" */ = { 321 | isa = XCConfigurationList; 322 | buildConfigurations = ( 323 | 207969F729CCFA81003EBA91 /* Debug */, 324 | 207969F829CCFA81003EBA91 /* Release */, 325 | ); 326 | defaultConfigurationIsVisible = 0; 327 | defaultConfigurationName = Release; 328 | }; 329 | /* End XCConfigurationList section */ 330 | }; 331 | rootObject = 207969E129CCFA80003EBA91 /* Project object */; 332 | } 333 | -------------------------------------------------------------------------------- /examples/macos/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/macos/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/node/index.js: -------------------------------------------------------------------------------- 1 | import * as ThumbHash from '../../js/thumbhash.js' 2 | import sharp from 'sharp' 3 | 4 | // Image to ThumbHash 5 | const image = sharp('../flower.jpg').resize(100, 100, { fit: 'inside' }) 6 | const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true }) 7 | const binaryThumbHash = ThumbHash.rgbaToThumbHash(info.width, info.height, data) 8 | console.log('binaryThumbHash:', Buffer.from(binaryThumbHash)) 9 | 10 | // If you want to use base64 instead of binary... 11 | const thumbHashToBase64 = Buffer.from(binaryThumbHash).toString('base64') 12 | const thumbHashFromBase64 = Buffer.from(thumbHashToBase64, 'base64') 13 | console.log('thumbHashToBase64:', thumbHashToBase64) 14 | 15 | // ThumbHash to data URL (can be done on the client, not the server) 16 | const placeholderURL = ThumbHash.thumbHashToDataURL(binaryThumbHash) 17 | console.log('placeholderURL:', placeholderURL) 18 | -------------------------------------------------------------------------------- /examples/node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "sharp": "0.33.1" 9 | } 10 | }, 11 | "node_modules/@emnapi/runtime": { 12 | "version": "0.44.0", 13 | "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz", 14 | "integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==", 15 | "optional": true, 16 | "dependencies": { 17 | "tslib": "^2.4.0" 18 | } 19 | }, 20 | "node_modules/@img/sharp-darwin-arm64": { 21 | "version": "0.33.1", 22 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz", 23 | "integrity": "sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==", 24 | "cpu": [ 25 | "arm64" 26 | ], 27 | "optional": true, 28 | "os": [ 29 | "darwin" 30 | ], 31 | "engines": { 32 | "glibc": ">=2.26", 33 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 34 | "npm": ">=9.6.5", 35 | "pnpm": ">=7.1.0", 36 | "yarn": ">=3.2.0" 37 | }, 38 | "funding": { 39 | "url": "https://opencollective.com/libvips" 40 | }, 41 | "optionalDependencies": { 42 | "@img/sharp-libvips-darwin-arm64": "1.0.0" 43 | } 44 | }, 45 | "node_modules/@img/sharp-darwin-x64": { 46 | "version": "0.33.1", 47 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz", 48 | "integrity": "sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==", 49 | "cpu": [ 50 | "x64" 51 | ], 52 | "optional": true, 53 | "os": [ 54 | "darwin" 55 | ], 56 | "engines": { 57 | "glibc": ">=2.26", 58 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 59 | "npm": ">=9.6.5", 60 | "pnpm": ">=7.1.0", 61 | "yarn": ">=3.2.0" 62 | }, 63 | "funding": { 64 | "url": "https://opencollective.com/libvips" 65 | }, 66 | "optionalDependencies": { 67 | "@img/sharp-libvips-darwin-x64": "1.0.0" 68 | } 69 | }, 70 | "node_modules/@img/sharp-libvips-darwin-arm64": { 71 | "version": "1.0.0", 72 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz", 73 | "integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==", 74 | "cpu": [ 75 | "arm64" 76 | ], 77 | "optional": true, 78 | "os": [ 79 | "darwin" 80 | ], 81 | "engines": { 82 | "macos": ">=11", 83 | "npm": ">=9.6.5", 84 | "pnpm": ">=7.1.0", 85 | "yarn": ">=3.2.0" 86 | }, 87 | "funding": { 88 | "url": "https://opencollective.com/libvips" 89 | } 90 | }, 91 | "node_modules/@img/sharp-libvips-darwin-x64": { 92 | "version": "1.0.0", 93 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz", 94 | "integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==", 95 | "cpu": [ 96 | "x64" 97 | ], 98 | "optional": true, 99 | "os": [ 100 | "darwin" 101 | ], 102 | "engines": { 103 | "macos": ">=10.13", 104 | "npm": ">=9.6.5", 105 | "pnpm": ">=7.1.0", 106 | "yarn": ">=3.2.0" 107 | }, 108 | "funding": { 109 | "url": "https://opencollective.com/libvips" 110 | } 111 | }, 112 | "node_modules/@img/sharp-libvips-linux-arm": { 113 | "version": "1.0.0", 114 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz", 115 | "integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==", 116 | "cpu": [ 117 | "arm" 118 | ], 119 | "optional": true, 120 | "os": [ 121 | "linux" 122 | ], 123 | "engines": { 124 | "glibc": ">=2.28", 125 | "npm": ">=9.6.5", 126 | "pnpm": ">=7.1.0", 127 | "yarn": ">=3.2.0" 128 | }, 129 | "funding": { 130 | "url": "https://opencollective.com/libvips" 131 | } 132 | }, 133 | "node_modules/@img/sharp-libvips-linux-arm64": { 134 | "version": "1.0.0", 135 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz", 136 | "integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==", 137 | "cpu": [ 138 | "arm64" 139 | ], 140 | "optional": true, 141 | "os": [ 142 | "linux" 143 | ], 144 | "engines": { 145 | "glibc": ">=2.26", 146 | "npm": ">=9.6.5", 147 | "pnpm": ">=7.1.0", 148 | "yarn": ">=3.2.0" 149 | }, 150 | "funding": { 151 | "url": "https://opencollective.com/libvips" 152 | } 153 | }, 154 | "node_modules/@img/sharp-libvips-linux-s390x": { 155 | "version": "1.0.0", 156 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz", 157 | "integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==", 158 | "cpu": [ 159 | "s390x" 160 | ], 161 | "optional": true, 162 | "os": [ 163 | "linux" 164 | ], 165 | "engines": { 166 | "glibc": ">=2.28", 167 | "npm": ">=9.6.5", 168 | "pnpm": ">=7.1.0", 169 | "yarn": ">=3.2.0" 170 | }, 171 | "funding": { 172 | "url": "https://opencollective.com/libvips" 173 | } 174 | }, 175 | "node_modules/@img/sharp-libvips-linux-x64": { 176 | "version": "1.0.0", 177 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz", 178 | "integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==", 179 | "cpu": [ 180 | "x64" 181 | ], 182 | "optional": true, 183 | "os": [ 184 | "linux" 185 | ], 186 | "engines": { 187 | "glibc": ">=2.26", 188 | "npm": ">=9.6.5", 189 | "pnpm": ">=7.1.0", 190 | "yarn": ">=3.2.0" 191 | }, 192 | "funding": { 193 | "url": "https://opencollective.com/libvips" 194 | } 195 | }, 196 | "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 197 | "version": "1.0.0", 198 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz", 199 | "integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==", 200 | "cpu": [ 201 | "arm64" 202 | ], 203 | "optional": true, 204 | "os": [ 205 | "linux" 206 | ], 207 | "engines": { 208 | "musl": ">=1.2.2", 209 | "npm": ">=9.6.5", 210 | "pnpm": ">=7.1.0", 211 | "yarn": ">=3.2.0" 212 | }, 213 | "funding": { 214 | "url": "https://opencollective.com/libvips" 215 | } 216 | }, 217 | "node_modules/@img/sharp-libvips-linuxmusl-x64": { 218 | "version": "1.0.0", 219 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz", 220 | "integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==", 221 | "cpu": [ 222 | "x64" 223 | ], 224 | "optional": true, 225 | "os": [ 226 | "linux" 227 | ], 228 | "engines": { 229 | "musl": ">=1.2.2", 230 | "npm": ">=9.6.5", 231 | "pnpm": ">=7.1.0", 232 | "yarn": ">=3.2.0" 233 | }, 234 | "funding": { 235 | "url": "https://opencollective.com/libvips" 236 | } 237 | }, 238 | "node_modules/@img/sharp-linux-arm": { 239 | "version": "0.33.1", 240 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz", 241 | "integrity": "sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==", 242 | "cpu": [ 243 | "arm" 244 | ], 245 | "optional": true, 246 | "os": [ 247 | "linux" 248 | ], 249 | "engines": { 250 | "glibc": ">=2.28", 251 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 252 | "npm": ">=9.6.5", 253 | "pnpm": ">=7.1.0", 254 | "yarn": ">=3.2.0" 255 | }, 256 | "funding": { 257 | "url": "https://opencollective.com/libvips" 258 | }, 259 | "optionalDependencies": { 260 | "@img/sharp-libvips-linux-arm": "1.0.0" 261 | } 262 | }, 263 | "node_modules/@img/sharp-linux-arm64": { 264 | "version": "0.33.1", 265 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz", 266 | "integrity": "sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==", 267 | "cpu": [ 268 | "arm64" 269 | ], 270 | "optional": true, 271 | "os": [ 272 | "linux" 273 | ], 274 | "engines": { 275 | "glibc": ">=2.26", 276 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 277 | "npm": ">=9.6.5", 278 | "pnpm": ">=7.1.0", 279 | "yarn": ">=3.2.0" 280 | }, 281 | "funding": { 282 | "url": "https://opencollective.com/libvips" 283 | }, 284 | "optionalDependencies": { 285 | "@img/sharp-libvips-linux-arm64": "1.0.0" 286 | } 287 | }, 288 | "node_modules/@img/sharp-linux-s390x": { 289 | "version": "0.33.1", 290 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz", 291 | "integrity": "sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==", 292 | "cpu": [ 293 | "s390x" 294 | ], 295 | "optional": true, 296 | "os": [ 297 | "linux" 298 | ], 299 | "engines": { 300 | "glibc": ">=2.28", 301 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 302 | "npm": ">=9.6.5", 303 | "pnpm": ">=7.1.0", 304 | "yarn": ">=3.2.0" 305 | }, 306 | "funding": { 307 | "url": "https://opencollective.com/libvips" 308 | }, 309 | "optionalDependencies": { 310 | "@img/sharp-libvips-linux-s390x": "1.0.0" 311 | } 312 | }, 313 | "node_modules/@img/sharp-linux-x64": { 314 | "version": "0.33.1", 315 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz", 316 | "integrity": "sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==", 317 | "cpu": [ 318 | "x64" 319 | ], 320 | "optional": true, 321 | "os": [ 322 | "linux" 323 | ], 324 | "engines": { 325 | "glibc": ">=2.26", 326 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 327 | "npm": ">=9.6.5", 328 | "pnpm": ">=7.1.0", 329 | "yarn": ">=3.2.0" 330 | }, 331 | "funding": { 332 | "url": "https://opencollective.com/libvips" 333 | }, 334 | "optionalDependencies": { 335 | "@img/sharp-libvips-linux-x64": "1.0.0" 336 | } 337 | }, 338 | "node_modules/@img/sharp-linuxmusl-arm64": { 339 | "version": "0.33.1", 340 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz", 341 | "integrity": "sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==", 342 | "cpu": [ 343 | "arm64" 344 | ], 345 | "optional": true, 346 | "os": [ 347 | "linux" 348 | ], 349 | "engines": { 350 | "musl": ">=1.2.2", 351 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 352 | "npm": ">=9.6.5", 353 | "pnpm": ">=7.1.0", 354 | "yarn": ">=3.2.0" 355 | }, 356 | "funding": { 357 | "url": "https://opencollective.com/libvips" 358 | }, 359 | "optionalDependencies": { 360 | "@img/sharp-libvips-linuxmusl-arm64": "1.0.0" 361 | } 362 | }, 363 | "node_modules/@img/sharp-linuxmusl-x64": { 364 | "version": "0.33.1", 365 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz", 366 | "integrity": "sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==", 367 | "cpu": [ 368 | "x64" 369 | ], 370 | "optional": true, 371 | "os": [ 372 | "linux" 373 | ], 374 | "engines": { 375 | "musl": ">=1.2.2", 376 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 377 | "npm": ">=9.6.5", 378 | "pnpm": ">=7.1.0", 379 | "yarn": ">=3.2.0" 380 | }, 381 | "funding": { 382 | "url": "https://opencollective.com/libvips" 383 | }, 384 | "optionalDependencies": { 385 | "@img/sharp-libvips-linuxmusl-x64": "1.0.0" 386 | } 387 | }, 388 | "node_modules/@img/sharp-wasm32": { 389 | "version": "0.33.1", 390 | "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz", 391 | "integrity": "sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==", 392 | "cpu": [ 393 | "wasm32" 394 | ], 395 | "optional": true, 396 | "dependencies": { 397 | "@emnapi/runtime": "^0.44.0" 398 | }, 399 | "engines": { 400 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 401 | "npm": ">=9.6.5", 402 | "pnpm": ">=7.1.0", 403 | "yarn": ">=3.2.0" 404 | }, 405 | "funding": { 406 | "url": "https://opencollective.com/libvips" 407 | } 408 | }, 409 | "node_modules/@img/sharp-win32-ia32": { 410 | "version": "0.33.1", 411 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz", 412 | "integrity": "sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==", 413 | "cpu": [ 414 | "ia32" 415 | ], 416 | "optional": true, 417 | "os": [ 418 | "win32" 419 | ], 420 | "engines": { 421 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 422 | "npm": ">=9.6.5", 423 | "pnpm": ">=7.1.0", 424 | "yarn": ">=3.2.0" 425 | }, 426 | "funding": { 427 | "url": "https://opencollective.com/libvips" 428 | } 429 | }, 430 | "node_modules/@img/sharp-win32-x64": { 431 | "version": "0.33.1", 432 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz", 433 | "integrity": "sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==", 434 | "cpu": [ 435 | "x64" 436 | ], 437 | "optional": true, 438 | "os": [ 439 | "win32" 440 | ], 441 | "engines": { 442 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0", 443 | "npm": ">=9.6.5", 444 | "pnpm": ">=7.1.0", 445 | "yarn": ">=3.2.0" 446 | }, 447 | "funding": { 448 | "url": "https://opencollective.com/libvips" 449 | } 450 | }, 451 | "node_modules/color": { 452 | "version": "4.2.3", 453 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 454 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 455 | "dependencies": { 456 | "color-convert": "^2.0.1", 457 | "color-string": "^1.9.0" 458 | }, 459 | "engines": { 460 | "node": ">=12.5.0" 461 | } 462 | }, 463 | "node_modules/color-convert": { 464 | "version": "2.0.1", 465 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 466 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 467 | "dependencies": { 468 | "color-name": "~1.1.4" 469 | }, 470 | "engines": { 471 | "node": ">=7.0.0" 472 | } 473 | }, 474 | "node_modules/color-name": { 475 | "version": "1.1.4", 476 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 477 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 478 | }, 479 | "node_modules/color-string": { 480 | "version": "1.9.1", 481 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 482 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 483 | "dependencies": { 484 | "color-name": "^1.0.0", 485 | "simple-swizzle": "^0.2.2" 486 | } 487 | }, 488 | "node_modules/detect-libc": { 489 | "version": "2.0.2", 490 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", 491 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", 492 | "engines": { 493 | "node": ">=8" 494 | } 495 | }, 496 | "node_modules/is-arrayish": { 497 | "version": "0.3.2", 498 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 499 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 500 | }, 501 | "node_modules/lru-cache": { 502 | "version": "6.0.0", 503 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 504 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 505 | "dependencies": { 506 | "yallist": "^4.0.0" 507 | }, 508 | "engines": { 509 | "node": ">=10" 510 | } 511 | }, 512 | "node_modules/semver": { 513 | "version": "7.5.4", 514 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 515 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 516 | "dependencies": { 517 | "lru-cache": "^6.0.0" 518 | }, 519 | "bin": { 520 | "semver": "bin/semver.js" 521 | }, 522 | "engines": { 523 | "node": ">=10" 524 | } 525 | }, 526 | "node_modules/sharp": { 527 | "version": "0.33.1", 528 | "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", 529 | "integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==", 530 | "hasInstallScript": true, 531 | "dependencies": { 532 | "color": "^4.2.3", 533 | "detect-libc": "^2.0.2", 534 | "semver": "^7.5.4" 535 | }, 536 | "engines": { 537 | "libvips": ">=8.15.0", 538 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 539 | }, 540 | "funding": { 541 | "url": "https://opencollective.com/libvips" 542 | }, 543 | "optionalDependencies": { 544 | "@img/sharp-darwin-arm64": "0.33.1", 545 | "@img/sharp-darwin-x64": "0.33.1", 546 | "@img/sharp-libvips-darwin-arm64": "1.0.0", 547 | "@img/sharp-libvips-darwin-x64": "1.0.0", 548 | "@img/sharp-libvips-linux-arm": "1.0.0", 549 | "@img/sharp-libvips-linux-arm64": "1.0.0", 550 | "@img/sharp-libvips-linux-s390x": "1.0.0", 551 | "@img/sharp-libvips-linux-x64": "1.0.0", 552 | "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", 553 | "@img/sharp-libvips-linuxmusl-x64": "1.0.0", 554 | "@img/sharp-linux-arm": "0.33.1", 555 | "@img/sharp-linux-arm64": "0.33.1", 556 | "@img/sharp-linux-s390x": "0.33.1", 557 | "@img/sharp-linux-x64": "0.33.1", 558 | "@img/sharp-linuxmusl-arm64": "0.33.1", 559 | "@img/sharp-linuxmusl-x64": "0.33.1", 560 | "@img/sharp-wasm32": "0.33.1", 561 | "@img/sharp-win32-ia32": "0.33.1", 562 | "@img/sharp-win32-x64": "0.33.1" 563 | } 564 | }, 565 | "node_modules/simple-swizzle": { 566 | "version": "0.2.2", 567 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 568 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 569 | "dependencies": { 570 | "is-arrayish": "^0.3.1" 571 | } 572 | }, 573 | "node_modules/tslib": { 574 | "version": "2.6.2", 575 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 576 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", 577 | "optional": true 578 | }, 579 | "node_modules/yallist": { 580 | "version": "4.0.0", 581 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 582 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 583 | } 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /examples/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "sharp": "0.33.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/rust/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "thumbhashdemo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | image = "0.24.5" 10 | thumbhash = "0.1.0" 11 | -------------------------------------------------------------------------------- /examples/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use image::{ImageEncoder}; 2 | use image::codecs::png::{PngEncoder}; 3 | use thumbhash::{rgba_to_thumb_hash, thumb_hash_to_rgba}; 4 | 5 | fn main() -> Result<(), Box> { 6 | // Load the input image from a file 7 | let image = image::open("../flower.jpg").unwrap(); 8 | 9 | // Convert the input image to RgbaImage format and retrieve its raw data, width, and height 10 | let rgba = image.to_rgba8().into_raw(); 11 | let width = image.width() as usize; 12 | let height = image.height() as usize; 13 | 14 | // Compute the ThumbHash of the input image 15 | let thumb_hash = rgba_to_thumb_hash(width, height, &rgba); 16 | 17 | // Convert the ThumbHash back to RgbaImage format 18 | let (_w, _h, rgba2) = thumb_hash_to_rgba(&thumb_hash).unwrap(); 19 | 20 | // Create a new file to store the output image 21 | let output_file = "output.png"; 22 | let file = std::fs::File::create(output_file)?; 23 | 24 | // Initialize a PNG encoder and write the output image to the file 25 | let encoder = PngEncoder::new(file); 26 | encoder 27 | .write_image( 28 | &rgba2, 29 | _w as u32, 30 | _h as u32, 31 | image::ColorType::Rgba8) 32 | ?; 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /java/com/madebyevan/thumbhash/ThumbHash.java: -------------------------------------------------------------------------------- 1 | package com.madebyevan.thumbhash; 2 | 3 | public final class ThumbHash { 4 | /** 5 | * Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A. 6 | * 7 | * @param w The width of the input image. Must be ≤100px. 8 | * @param h The height of the input image. Must be ≤100px. 9 | * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements. 10 | * @return The ThumbHash as a byte array. 11 | */ 12 | public static byte[] rgbaToThumbHash(int w, int h, byte[] rgba) { 13 | // Encoding an image larger than 100x100 is slow with no benefit 14 | if (w > 100 || h > 100) throw new IllegalArgumentException(w + "x" + h + " doesn't fit in 100x100"); 15 | 16 | // Determine the average color 17 | float avg_r = 0, avg_g = 0, avg_b = 0, avg_a = 0; 18 | for (int i = 0, j = 0; i < w * h; i++, j += 4) { 19 | float alpha = (rgba[j + 3] & 255) / 255.0f; 20 | avg_r += alpha / 255.0f * (rgba[j] & 255); 21 | avg_g += alpha / 255.0f * (rgba[j + 1] & 255); 22 | avg_b += alpha / 255.0f * (rgba[j + 2] & 255); 23 | avg_a += alpha; 24 | } 25 | if (avg_a > 0) { 26 | avg_r /= avg_a; 27 | avg_g /= avg_a; 28 | avg_b /= avg_a; 29 | } 30 | 31 | boolean hasAlpha = avg_a < w * h; 32 | int l_limit = hasAlpha ? 5 : 7; // Use fewer luminance bits if there's alpha 33 | int lx = Math.max(1, Math.round((float) (l_limit * w) / (float) Math.max(w, h))); 34 | int ly = Math.max(1, Math.round((float) (l_limit * h) / (float) Math.max(w, h))); 35 | float[] l = new float[w * h]; // luminance 36 | float[] p = new float[w * h]; // yellow - blue 37 | float[] q = new float[w * h]; // red - green 38 | float[] a = new float[w * h]; // alpha 39 | 40 | // Convert the image from RGBA to LPQA (composite atop the average color) 41 | for (int i = 0, j = 0; i < w * h; i++, j += 4) { 42 | float alpha = (rgba[j + 3] & 255) / 255.0f; 43 | float r = avg_r * (1.0f - alpha) + alpha / 255.0f * (rgba[j] & 255); 44 | float g = avg_g * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 1] & 255); 45 | float b = avg_b * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 2] & 255); 46 | l[i] = (r + g + b) / 3.0f; 47 | p[i] = (r + g) / 2.0f - b; 48 | q[i] = r - g; 49 | a[i] = alpha; 50 | } 51 | 52 | // Encode using the DCT into DC (constant) and normalized AC (varying) terms 53 | Channel l_channel = new Channel(Math.max(3, lx), Math.max(3, ly)).encode(w, h, l); 54 | Channel p_channel = new Channel(3, 3).encode(w, h, p); 55 | Channel q_channel = new Channel(3, 3).encode(w, h, q); 56 | Channel a_channel = hasAlpha ? new Channel(5, 5).encode(w, h, a) : null; 57 | 58 | // Write the constants 59 | boolean isLandscape = w > h; 60 | int header24 = Math.round(63.0f * l_channel.dc) 61 | | (Math.round(31.5f + 31.5f * p_channel.dc) << 6) 62 | | (Math.round(31.5f + 31.5f * q_channel.dc) << 12) 63 | | (Math.round(31.0f * l_channel.scale) << 18) 64 | | (hasAlpha ? 1 << 23 : 0); 65 | int header16 = (isLandscape ? ly : lx) 66 | | (Math.round(63.0f * p_channel.scale) << 3) 67 | | (Math.round(63.0f * q_channel.scale) << 9) 68 | | (isLandscape ? 1 << 15 : 0); 69 | int ac_start = hasAlpha ? 6 : 5; 70 | int ac_count = l_channel.ac.length + p_channel.ac.length + q_channel.ac.length 71 | + (hasAlpha ? a_channel.ac.length : 0); 72 | byte[] hash = new byte[ac_start + (ac_count + 1) / 2]; 73 | hash[0] = (byte) header24; 74 | hash[1] = (byte) (header24 >> 8); 75 | hash[2] = (byte) (header24 >> 16); 76 | hash[3] = (byte) header16; 77 | hash[4] = (byte) (header16 >> 8); 78 | if (hasAlpha) hash[5] = (byte) (Math.round(15.0f * a_channel.dc) 79 | | (Math.round(15.0f * a_channel.scale) << 4)); 80 | 81 | // Write the varying factors 82 | int ac_index = 0; 83 | ac_index = l_channel.writeTo(hash, ac_start, ac_index); 84 | ac_index = p_channel.writeTo(hash, ac_start, ac_index); 85 | ac_index = q_channel.writeTo(hash, ac_start, ac_index); 86 | if (hasAlpha) a_channel.writeTo(hash, ac_start, ac_index); 87 | return hash; 88 | } 89 | 90 | /** 91 | * Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A. 92 | * 93 | * @param hash The bytes of the ThumbHash. 94 | * @return The width, height, and pixels of the rendered placeholder image. 95 | */ 96 | public static Image thumbHashToRGBA(byte[] hash) { 97 | // Read the constants 98 | int header24 = (hash[0] & 255) | ((hash[1] & 255) << 8) | ((hash[2] & 255) << 16); 99 | int header16 = (hash[3] & 255) | ((hash[4] & 255) << 8); 100 | float l_dc = (float) (header24 & 63) / 63.0f; 101 | float p_dc = (float) ((header24 >> 6) & 63) / 31.5f - 1.0f; 102 | float q_dc = (float) ((header24 >> 12) & 63) / 31.5f - 1.0f; 103 | float l_scale = (float) ((header24 >> 18) & 31) / 31.0f; 104 | boolean hasAlpha = (header24 >> 23) != 0; 105 | float p_scale = (float) ((header16 >> 3) & 63) / 63.0f; 106 | float q_scale = (float) ((header16 >> 9) & 63) / 63.0f; 107 | boolean isLandscape = (header16 >> 15) != 0; 108 | int lx = Math.max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7); 109 | int ly = Math.max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7); 110 | float a_dc = hasAlpha ? (float) (hash[5] & 15) / 15.0f : 1.0f; 111 | float a_scale = (float) ((hash[5] >> 4) & 15) / 15.0f; 112 | 113 | // Read the varying factors (boost saturation by 1.25x to compensate for quantization) 114 | int ac_start = hasAlpha ? 6 : 5; 115 | int ac_index = 0; 116 | Channel l_channel = new Channel(lx, ly); 117 | Channel p_channel = new Channel(3, 3); 118 | Channel q_channel = new Channel(3, 3); 119 | Channel a_channel = null; 120 | ac_index = l_channel.decode(hash, ac_start, ac_index, l_scale); 121 | ac_index = p_channel.decode(hash, ac_start, ac_index, p_scale * 1.25f); 122 | ac_index = q_channel.decode(hash, ac_start, ac_index, q_scale * 1.25f); 123 | if (hasAlpha) { 124 | a_channel = new Channel(5, 5); 125 | a_channel.decode(hash, ac_start, ac_index, a_scale); 126 | } 127 | float[] l_ac = l_channel.ac; 128 | float[] p_ac = p_channel.ac; 129 | float[] q_ac = q_channel.ac; 130 | float[] a_ac = hasAlpha ? a_channel.ac : null; 131 | 132 | // Decode using the DCT into RGB 133 | float ratio = thumbHashToApproximateAspectRatio(hash); 134 | int w = Math.round(ratio > 1.0f ? 32.0f : 32.0f * ratio); 135 | int h = Math.round(ratio > 1.0f ? 32.0f / ratio : 32.0f); 136 | byte[] rgba = new byte[w * h * 4]; 137 | int cx_stop = Math.max(lx, hasAlpha ? 5 : 3); 138 | int cy_stop = Math.max(ly, hasAlpha ? 5 : 3); 139 | float[] fx = new float[cx_stop]; 140 | float[] fy = new float[cy_stop]; 141 | for (int y = 0, i = 0; y < h; y++) { 142 | for (int x = 0; x < w; x++, i += 4) { 143 | float l = l_dc, p = p_dc, q = q_dc, a = a_dc; 144 | 145 | // Precompute the coefficients 146 | for (int cx = 0; cx < cx_stop; cx++) 147 | fx[cx] = (float) Math.cos(Math.PI / w * (x + 0.5f) * cx); 148 | for (int cy = 0; cy < cy_stop; cy++) 149 | fy[cy] = (float) Math.cos(Math.PI / h * (y + 0.5f) * cy); 150 | 151 | // Decode L 152 | for (int cy = 0, j = 0; cy < ly; cy++) { 153 | float fy2 = fy[cy] * 2.0f; 154 | for (int cx = cy > 0 ? 0 : 1; cx * ly < lx * (ly - cy); cx++, j++) 155 | l += l_ac[j] * fx[cx] * fy2; 156 | } 157 | 158 | // Decode P and Q 159 | for (int cy = 0, j = 0; cy < 3; cy++) { 160 | float fy2 = fy[cy] * 2.0f; 161 | for (int cx = cy > 0 ? 0 : 1; cx < 3 - cy; cx++, j++) { 162 | float f = fx[cx] * fy2; 163 | p += p_ac[j] * f; 164 | q += q_ac[j] * f; 165 | } 166 | } 167 | 168 | // Decode A 169 | if (hasAlpha) 170 | for (int cy = 0, j = 0; cy < 5; cy++) { 171 | float fy2 = fy[cy] * 2.0f; 172 | for (int cx = cy > 0 ? 0 : 1; cx < 5 - cy; cx++, j++) 173 | a += a_ac[j] * fx[cx] * fy2; 174 | } 175 | 176 | // Convert to RGB 177 | float b = l - 2.0f / 3.0f * p; 178 | float r = (3.0f * l - b + q) / 2.0f; 179 | float g = r - q; 180 | rgba[i] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, r))); 181 | rgba[i + 1] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, g))); 182 | rgba[i + 2] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, b))); 183 | rgba[i + 3] = (byte) Math.max(0, Math.round(255.0f * Math.min(1, a))); 184 | } 185 | } 186 | return new Image(w, h, rgba); 187 | } 188 | 189 | /** 190 | * Extracts the average color from a ThumbHash. RGB is not be premultiplied by A. 191 | * 192 | * @param hash The bytes of the ThumbHash. 193 | * @return The RGBA values for the average color. Each value ranges from 0 to 1. 194 | */ 195 | public static RGBA thumbHashToAverageRGBA(byte[] hash) { 196 | int header = (hash[0] & 255) | ((hash[1] & 255) << 8) | ((hash[2] & 255) << 16); 197 | float l = (float) (header & 63) / 63.0f; 198 | float p = (float) ((header >> 6) & 63) / 31.5f - 1.0f; 199 | float q = (float) ((header >> 12) & 63) / 31.5f - 1.0f; 200 | boolean hasAlpha = (header >> 23) != 0; 201 | float a = hasAlpha ? (float) (hash[5] & 15) / 15.0f : 1.0f; 202 | float b = l - 2.0f / 3.0f * p; 203 | float r = (3.0f * l - b + q) / 2.0f; 204 | float g = r - q; 205 | return new RGBA( 206 | Math.max(0, Math.min(1, r)), 207 | Math.max(0, Math.min(1, g)), 208 | Math.max(0, Math.min(1, b)), 209 | a); 210 | } 211 | 212 | /** 213 | * Extracts the approximate aspect ratio of the original image. 214 | * 215 | * @param hash The bytes of the ThumbHash. 216 | * @return The approximate aspect ratio (i.e. width / height). 217 | */ 218 | public static float thumbHashToApproximateAspectRatio(byte[] hash) { 219 | byte header = hash[3]; 220 | boolean hasAlpha = (hash[2] & 0x80) != 0; 221 | boolean isLandscape = (hash[4] & 0x80) != 0; 222 | int lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7; 223 | int ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7; 224 | return (float) lx / (float) ly; 225 | } 226 | 227 | public static final class Image { 228 | public int width; 229 | public int height; 230 | public byte[] rgba; 231 | 232 | public Image(int width, int height, byte[] rgba) { 233 | this.width = width; 234 | this.height = height; 235 | this.rgba = rgba; 236 | } 237 | } 238 | 239 | public static final class RGBA { 240 | public float r; 241 | public float g; 242 | public float b; 243 | public float a; 244 | 245 | public RGBA(float r, float g, float b, float a) { 246 | this.r = r; 247 | this.g = g; 248 | this.b = b; 249 | this.a = a; 250 | } 251 | } 252 | 253 | private static final class Channel { 254 | int nx; 255 | int ny; 256 | float dc; 257 | float[] ac; 258 | float scale; 259 | 260 | Channel(int nx, int ny) { 261 | this.nx = nx; 262 | this.ny = ny; 263 | int n = 0; 264 | for (int cy = 0; cy < ny; cy++) 265 | for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++) 266 | n++; 267 | ac = new float[n]; 268 | } 269 | 270 | Channel encode(int w, int h, float[] channel) { 271 | int n = 0; 272 | float[] fx = new float[w]; 273 | for (int cy = 0; cy < ny; cy++) { 274 | for (int cx = 0; cx * ny < nx * (ny - cy); cx++) { 275 | float f = 0; 276 | for (int x = 0; x < w; x++) 277 | fx[x] = (float) Math.cos(Math.PI / w * cx * (x + 0.5f)); 278 | for (int y = 0; y < h; y++) { 279 | float fy = (float) Math.cos(Math.PI / h * cy * (y + 0.5f)); 280 | for (int x = 0; x < w; x++) 281 | f += channel[x + y * w] * fx[x] * fy; 282 | } 283 | f /= w * h; 284 | if (cx > 0 || cy > 0) { 285 | ac[n++] = f; 286 | scale = Math.max(scale, Math.abs(f)); 287 | } else { 288 | dc = f; 289 | } 290 | } 291 | } 292 | if (scale > 0) 293 | for (int i = 0; i < ac.length; i++) 294 | ac[i] = 0.5f + 0.5f / scale * ac[i]; 295 | return this; 296 | } 297 | 298 | int decode(byte[] hash, int start, int index, float scale) { 299 | for (int i = 0; i < ac.length; i++) { 300 | int data = hash[start + (index >> 1)] >> ((index & 1) << 2); 301 | ac[i] = ((float) (data & 15) / 7.5f - 1.0f) * scale; 302 | index++; 303 | } 304 | return index; 305 | } 306 | 307 | int writeTo(byte[] hash, int start, int index) { 308 | for (float v : ac) { 309 | hash[start + (index >> 1)] |= Math.round(15.0f * v) << ((index & 1) << 2); 310 | index++; 311 | } 312 | return index; 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | See https://evanw.github.io/thumbhash/ for details. 2 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thumbhash", 3 | "version": "0.1.1", 4 | "description": "A very compact representation of an image placeholder", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/evanw/thumbhash" 8 | }, 9 | "type": "module", 10 | "main": "thumbhash.js", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /js/thumbhash.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A. 3 | * 4 | * @param w The width of the input image. Must be ≤100px. 5 | * @param h The height of the input image. Must be ≤100px. 6 | * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements. 7 | * @returns The ThumbHash as a Uint8Array. 8 | */ 9 | export function rgbaToThumbHash(w: number, h: number, rgba: ArrayLike): Uint8Array 10 | 11 | /** 12 | * Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A. 13 | * 14 | * @param hash The bytes of the ThumbHash. 15 | * @returns The width, height, and pixels of the rendered placeholder image. 16 | */ 17 | export function thumbHashToRGBA(hash: ArrayLike): { w: number, h: number, rgba: Uint8Array } 18 | 19 | /** 20 | * Extracts the average color from a ThumbHash. RGB is not be premultiplied by A. 21 | * 22 | * @param hash The bytes of the ThumbHash. 23 | * @returns The RGBA values for the average color. Each value ranges from 0 to 1. 24 | */ 25 | export function thumbHashToAverageRGBA(hash: ArrayLike): { r: number, g: number, b: number, a: number } 26 | 27 | /** 28 | * Extracts the approximate aspect ratio of the original image. 29 | * 30 | * @param hash The bytes of the ThumbHash. 31 | * @returns The approximate aspect ratio (i.e. width / height). 32 | */ 33 | export function thumbHashToApproximateAspectRatio(hash: ArrayLike): number 34 | 35 | /** 36 | * Encodes an RGBA image to a PNG data URL. RGB should not be premultiplied by 37 | * A. This is optimized for speed and simplicity and does not optimize for size 38 | * at all. This doesn't do any compression (all values are stored uncompressed). 39 | * 40 | * @param w The width of the input image. Must be ≤100px. 41 | * @param h The height of the input image. Must be ≤100px. 42 | * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements. 43 | * @returns A data URL containing a PNG for the input image. 44 | */ 45 | export function rgbaToDataURL(w: number, h: number, rgba: ArrayLike): string 46 | 47 | /** 48 | * Decodes a ThumbHash to a PNG data URL. This is a convenience function that 49 | * just calls "thumbHashToRGBA" followed by "rgbaToDataURL". 50 | * 51 | * @param hash The bytes of the ThumbHash. 52 | * @returns A data URL containing a PNG for the rendered ThumbHash. 53 | */ 54 | export function thumbHashToDataURL(hash: ArrayLike): string 55 | -------------------------------------------------------------------------------- /js/thumbhash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A. 3 | * 4 | * @param w The width of the input image. Must be ≤100px. 5 | * @param h The height of the input image. Must be ≤100px. 6 | * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements. 7 | * @returns The ThumbHash as a Uint8Array. 8 | */ 9 | export function rgbaToThumbHash(w, h, rgba) { 10 | // Encoding an image larger than 100x100 is slow with no benefit 11 | if (w > 100 || h > 100) throw new Error(`${w}x${h} doesn't fit in 100x100`) 12 | let { PI, round, max, cos, abs } = Math 13 | 14 | // Determine the average color 15 | let avg_r = 0, avg_g = 0, avg_b = 0, avg_a = 0 16 | for (let i = 0, j = 0; i < w * h; i++, j += 4) { 17 | let alpha = rgba[j + 3] / 255 18 | avg_r += alpha / 255 * rgba[j] 19 | avg_g += alpha / 255 * rgba[j + 1] 20 | avg_b += alpha / 255 * rgba[j + 2] 21 | avg_a += alpha 22 | } 23 | if (avg_a) { 24 | avg_r /= avg_a 25 | avg_g /= avg_a 26 | avg_b /= avg_a 27 | } 28 | 29 | let hasAlpha = avg_a < w * h 30 | let l_limit = hasAlpha ? 5 : 7 // Use fewer luminance bits if there's alpha 31 | let lx = max(1, round(l_limit * w / max(w, h))) 32 | let ly = max(1, round(l_limit * h / max(w, h))) 33 | let l = [] // luminance 34 | let p = [] // yellow - blue 35 | let q = [] // red - green 36 | let a = [] // alpha 37 | 38 | // Convert the image from RGBA to LPQA (composite atop the average color) 39 | for (let i = 0, j = 0; i < w * h; i++, j += 4) { 40 | let alpha = rgba[j + 3] / 255 41 | let r = avg_r * (1 - alpha) + alpha / 255 * rgba[j] 42 | let g = avg_g * (1 - alpha) + alpha / 255 * rgba[j + 1] 43 | let b = avg_b * (1 - alpha) + alpha / 255 * rgba[j + 2] 44 | l[i] = (r + g + b) / 3 45 | p[i] = (r + g) / 2 - b 46 | q[i] = r - g 47 | a[i] = alpha 48 | } 49 | 50 | // Encode using the DCT into DC (constant) and normalized AC (varying) terms 51 | let encodeChannel = (channel, nx, ny) => { 52 | let dc = 0, ac = [], scale = 0, fx = [] 53 | for (let cy = 0; cy < ny; cy++) { 54 | for (let cx = 0; cx * ny < nx * (ny - cy); cx++) { 55 | let f = 0 56 | for (let x = 0; x < w; x++) 57 | fx[x] = cos(PI / w * cx * (x + 0.5)) 58 | for (let y = 0; y < h; y++) 59 | for (let x = 0, fy = cos(PI / h * cy * (y + 0.5)); x < w; x++) 60 | f += channel[x + y * w] * fx[x] * fy 61 | f /= w * h 62 | if (cx || cy) { 63 | ac.push(f) 64 | scale = max(scale, abs(f)) 65 | } else { 66 | dc = f 67 | } 68 | } 69 | } 70 | if (scale) 71 | for (let i = 0; i < ac.length; i++) 72 | ac[i] = 0.5 + 0.5 / scale * ac[i] 73 | return [dc, ac, scale] 74 | } 75 | let [l_dc, l_ac, l_scale] = encodeChannel(l, max(3, lx), max(3, ly)) 76 | let [p_dc, p_ac, p_scale] = encodeChannel(p, 3, 3) 77 | let [q_dc, q_ac, q_scale] = encodeChannel(q, 3, 3) 78 | let [a_dc, a_ac, a_scale] = hasAlpha ? encodeChannel(a, 5, 5) : [] 79 | 80 | // Write the constants 81 | let isLandscape = w > h 82 | let header24 = round(63 * l_dc) | (round(31.5 + 31.5 * p_dc) << 6) | (round(31.5 + 31.5 * q_dc) << 12) | (round(31 * l_scale) << 18) | (hasAlpha << 23) 83 | let header16 = (isLandscape ? ly : lx) | (round(63 * p_scale) << 3) | (round(63 * q_scale) << 9) | (isLandscape << 15) 84 | let hash = [header24 & 255, (header24 >> 8) & 255, header24 >> 16, header16 & 255, header16 >> 8] 85 | let ac_start = hasAlpha ? 6 : 5 86 | let ac_index = 0 87 | if (hasAlpha) hash.push(round(15 * a_dc) | (round(15 * a_scale) << 4)) 88 | 89 | // Write the varying factors 90 | for (let ac of hasAlpha ? [l_ac, p_ac, q_ac, a_ac] : [l_ac, p_ac, q_ac]) 91 | for (let f of ac) 92 | hash[ac_start + (ac_index >> 1)] |= round(15 * f) << ((ac_index++ & 1) << 2) 93 | return new Uint8Array(hash) 94 | } 95 | 96 | /** 97 | * Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A. 98 | * 99 | * @param hash The bytes of the ThumbHash. 100 | * @returns The width, height, and pixels of the rendered placeholder image. 101 | */ 102 | export function thumbHashToRGBA(hash) { 103 | let { PI, min, max, cos, round } = Math 104 | 105 | // Read the constants 106 | let header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16) 107 | let header16 = hash[3] | (hash[4] << 8) 108 | let l_dc = (header24 & 63) / 63 109 | let p_dc = ((header24 >> 6) & 63) / 31.5 - 1 110 | let q_dc = ((header24 >> 12) & 63) / 31.5 - 1 111 | let l_scale = ((header24 >> 18) & 31) / 31 112 | let hasAlpha = header24 >> 23 113 | let p_scale = ((header16 >> 3) & 63) / 63 114 | let q_scale = ((header16 >> 9) & 63) / 63 115 | let isLandscape = header16 >> 15 116 | let lx = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7) 117 | let ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7) 118 | let a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1 119 | let a_scale = (hash[5] >> 4) / 15 120 | 121 | // Read the varying factors (boost saturation by 1.25x to compensate for quantization) 122 | let ac_start = hasAlpha ? 6 : 5 123 | let ac_index = 0 124 | let decodeChannel = (nx, ny, scale) => { 125 | let ac = [] 126 | for (let cy = 0; cy < ny; cy++) 127 | for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) 128 | ac.push((((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2)) & 15) / 7.5 - 1) * scale) 129 | return ac 130 | } 131 | let l_ac = decodeChannel(lx, ly, l_scale) 132 | let p_ac = decodeChannel(3, 3, p_scale * 1.25) 133 | let q_ac = decodeChannel(3, 3, q_scale * 1.25) 134 | let a_ac = hasAlpha && decodeChannel(5, 5, a_scale) 135 | 136 | // Decode using the DCT into RGB 137 | let ratio = thumbHashToApproximateAspectRatio(hash) 138 | let w = round(ratio > 1 ? 32 : 32 * ratio) 139 | let h = round(ratio > 1 ? 32 / ratio : 32) 140 | let rgba = new Uint8Array(w * h * 4), fx = [], fy = [] 141 | for (let y = 0, i = 0; y < h; y++) { 142 | for (let x = 0; x < w; x++, i += 4) { 143 | let l = l_dc, p = p_dc, q = q_dc, a = a_dc 144 | 145 | // Precompute the coefficients 146 | for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) 147 | fx[cx] = cos(PI / w * (x + 0.5) * cx) 148 | for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) 149 | fy[cy] = cos(PI / h * (y + 0.5) * cy) 150 | 151 | // Decode L 152 | for (let cy = 0, j = 0; cy < ly; cy++) 153 | for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) 154 | l += l_ac[j] * fx[cx] * fy2 155 | 156 | // Decode P and Q 157 | for (let cy = 0, j = 0; cy < 3; cy++) { 158 | for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) { 159 | let f = fx[cx] * fy2 160 | p += p_ac[j] * f 161 | q += q_ac[j] * f 162 | } 163 | } 164 | 165 | // Decode A 166 | if (hasAlpha) 167 | for (let cy = 0, j = 0; cy < 5; cy++) 168 | for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) 169 | a += a_ac[j] * fx[cx] * fy2 170 | 171 | // Convert to RGB 172 | let b = l - 2 / 3 * p 173 | let r = (3 * l - b + q) / 2 174 | let g = r - q 175 | rgba[i] = max(0, 255 * min(1, r)) 176 | rgba[i + 1] = max(0, 255 * min(1, g)) 177 | rgba[i + 2] = max(0, 255 * min(1, b)) 178 | rgba[i + 3] = max(0, 255 * min(1, a)) 179 | } 180 | } 181 | return { w, h, rgba } 182 | } 183 | 184 | /** 185 | * Extracts the average color from a ThumbHash. RGB is not be premultiplied by A. 186 | * 187 | * @param hash The bytes of the ThumbHash. 188 | * @returns The RGBA values for the average color. Each value ranges from 0 to 1. 189 | */ 190 | export function thumbHashToAverageRGBA(hash) { 191 | let { min, max } = Math 192 | let header = hash[0] | (hash[1] << 8) | (hash[2] << 16) 193 | let l = (header & 63) / 63 194 | let p = ((header >> 6) & 63) / 31.5 - 1 195 | let q = ((header >> 12) & 63) / 31.5 - 1 196 | let hasAlpha = header >> 23 197 | let a = hasAlpha ? (hash[5] & 15) / 15 : 1 198 | let b = l - 2 / 3 * p 199 | let r = (3 * l - b + q) / 2 200 | let g = r - q 201 | return { 202 | r: max(0, min(1, r)), 203 | g: max(0, min(1, g)), 204 | b: max(0, min(1, b)), 205 | a 206 | } 207 | } 208 | 209 | /** 210 | * Extracts the approximate aspect ratio of the original image. 211 | * 212 | * @param hash The bytes of the ThumbHash. 213 | * @returns The approximate aspect ratio (i.e. width / height). 214 | */ 215 | export function thumbHashToApproximateAspectRatio(hash) { 216 | let header = hash[3] 217 | let hasAlpha = hash[2] & 0x80 218 | let isLandscape = hash[4] & 0x80 219 | let lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7 220 | let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7 221 | return lx / ly 222 | } 223 | 224 | /** 225 | * Encodes an RGBA image to a PNG data URL. RGB should not be premultiplied by 226 | * A. This is optimized for speed and simplicity and does not optimize for size 227 | * at all. This doesn't do any compression (all values are stored uncompressed). 228 | * 229 | * @param w The width of the input image. Must be ≤100px. 230 | * @param h The height of the input image. Must be ≤100px. 231 | * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements. 232 | * @returns A data URL containing a PNG for the input image. 233 | */ 234 | export function rgbaToDataURL(w, h, rgba) { 235 | let row = w * 4 + 1 236 | let idat = 6 + h * (5 + row) 237 | let bytes = [ 238 | 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 239 | w >> 8, w & 255, 0, 0, h >> 8, h & 255, 8, 6, 0, 0, 0, 0, 0, 0, 0, 240 | idat >>> 24, (idat >> 16) & 255, (idat >> 8) & 255, idat & 255, 241 | 73, 68, 65, 84, 120, 1 242 | ] 243 | let table = [ 244 | 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960, 245 | 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376, 246 | -2032938284, -1609899400, -1111625188 247 | ] 248 | let a = 1, b = 0 249 | for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) { 250 | bytes.push(y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, (row >> 8) ^ 255, 0) 251 | for (b = (b + a) % 65521; i < end; i++) { 252 | let u = rgba[i] & 255 253 | bytes.push(u) 254 | a = (a + u) % 65521 255 | b = (b + a) % 65521 256 | } 257 | } 258 | bytes.push( 259 | b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 260 | 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 261 | ) 262 | for (let [start, end] of [[12, 29], [37, 41 + idat]]) { 263 | let c = ~0 264 | for (let i = start; i < end; i++) { 265 | c ^= bytes[i] 266 | c = (c >>> 4) ^ table[c & 15] 267 | c = (c >>> 4) ^ table[c & 15] 268 | } 269 | c = ~c 270 | bytes[end++] = c >>> 24 271 | bytes[end++] = (c >> 16) & 255 272 | bytes[end++] = (c >> 8) & 255 273 | bytes[end++] = c & 255 274 | } 275 | return 'data:image/png;base64,' + btoa(String.fromCharCode(...bytes)) 276 | } 277 | 278 | /** 279 | * Decodes a ThumbHash to a PNG data URL. This is a convenience function that 280 | * just calls "thumbHashToRGBA" followed by "rgbaToDataURL". 281 | * 282 | * @param hash The bytes of the ThumbHash. 283 | * @returns A data URL containing a PNG for the rendered ThumbHash. 284 | */ 285 | export function thumbHashToDataURL(hash) { 286 | let image = thumbHashToRGBA(hash) 287 | return rgbaToDataURL(image.w, image.h, image.rgba) 288 | } 289 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /target/ 3 | -------------------------------------------------------------------------------- /rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "thumbhash" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "thumbhash" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "A very compact representation of an image placeholder" 6 | license = "MIT" 7 | repository = "https://github.com/evanw/thumbhash" 8 | -------------------------------------------------------------------------------- /rust/README.md: -------------------------------------------------------------------------------- 1 | See https://evanw.github.io/thumbhash/ for details. 2 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::PI; 2 | use std::io::Read; 3 | 4 | /// Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A. 5 | /// 6 | /// * `w`: The width of the input image. Must be ≤100px. 7 | /// * `h`: The height of the input image. Must be ≤100px. 8 | /// * `rgba`: The pixels in the input image, row-by-row. Must have `w*h*4` elements. 9 | pub fn rgba_to_thumb_hash(w: usize, h: usize, rgba: &[u8]) -> Vec { 10 | // Encoding an image larger than 100x100 is slow with no benefit 11 | assert!(w <= 100 && h <= 100); 12 | assert_eq!(rgba.len(), w * h * 4); 13 | 14 | // Determine the average color 15 | let mut avg_r = 0.0; 16 | let mut avg_g = 0.0; 17 | let mut avg_b = 0.0; 18 | let mut avg_a = 0.0; 19 | for rgba in rgba.chunks_exact(4) { 20 | let alpha = rgba[3] as f32 / 255.0; 21 | avg_r += alpha / 255.0 * rgba[0] as f32; 22 | avg_g += alpha / 255.0 * rgba[1] as f32; 23 | avg_b += alpha / 255.0 * rgba[2] as f32; 24 | avg_a += alpha; 25 | } 26 | if avg_a > 0.0 { 27 | avg_r /= avg_a; 28 | avg_g /= avg_a; 29 | avg_b /= avg_a; 30 | } 31 | 32 | let has_alpha = avg_a < (w * h) as f32; 33 | let l_limit = if has_alpha { 5 } else { 7 }; // Use fewer luminance bits if there's alpha 34 | let lx = (((l_limit * w) as f32 / w.max(h) as f32).round() as usize).max(1); 35 | let ly = (((l_limit * h) as f32 / w.max(h) as f32).round() as usize).max(1); 36 | let mut l = Vec::with_capacity(w * h); // luminance 37 | let mut p = Vec::with_capacity(w * h); // yellow - blue 38 | let mut q = Vec::with_capacity(w * h); // red - green 39 | let mut a = Vec::with_capacity(w * h); // alpha 40 | 41 | // Convert the image from RGBA to LPQA (composite atop the average color) 42 | for rgba in rgba.chunks_exact(4) { 43 | let alpha = rgba[3] as f32 / 255.0; 44 | let r = avg_r * (1.0 - alpha) + alpha / 255.0 * rgba[0] as f32; 45 | let g = avg_g * (1.0 - alpha) + alpha / 255.0 * rgba[1] as f32; 46 | let b = avg_b * (1.0 - alpha) + alpha / 255.0 * rgba[2] as f32; 47 | l.push((r + g + b) / 3.0); 48 | p.push((r + g) / 2.0 - b); 49 | q.push(r - g); 50 | a.push(alpha); 51 | } 52 | 53 | // Encode using the DCT into DC (constant) and normalized AC (varying) terms 54 | let encode_channel = |channel: &[f32], nx: usize, ny: usize| -> (f32, Vec, f32) { 55 | let mut dc = 0.0; 56 | let mut ac = Vec::with_capacity(nx * ny / 2); 57 | let mut scale = 0.0; 58 | let mut fx = [0.0].repeat(w); 59 | for cy in 0..ny { 60 | let mut cx = 0; 61 | while cx * ny < nx * (ny - cy) { 62 | let mut f = 0.0; 63 | for x in 0..w { 64 | fx[x] = (PI / w as f32 * cx as f32 * (x as f32 + 0.5)).cos(); 65 | } 66 | for y in 0..h { 67 | let fy = (PI / h as f32 * cy as f32 * (y as f32 + 0.5)).cos(); 68 | for x in 0..w { 69 | f += channel[x + y * w] * fx[x] * fy; 70 | } 71 | } 72 | f /= (w * h) as f32; 73 | if cx > 0 || cy > 0 { 74 | ac.push(f); 75 | scale = f.abs().max(scale); 76 | } else { 77 | dc = f; 78 | } 79 | cx += 1; 80 | } 81 | } 82 | if scale > 0.0 { 83 | for ac in &mut ac { 84 | *ac = 0.5 + 0.5 / scale * *ac; 85 | } 86 | } 87 | (dc, ac, scale) 88 | }; 89 | let (l_dc, l_ac, l_scale) = encode_channel(&l, lx.max(3), ly.max(3)); 90 | let (p_dc, p_ac, p_scale) = encode_channel(&p, 3, 3); 91 | let (q_dc, q_ac, q_scale) = encode_channel(&q, 3, 3); 92 | let (a_dc, a_ac, a_scale) = if has_alpha { 93 | encode_channel(&a, 5, 5) 94 | } else { 95 | (1.0, Vec::new(), 1.0) 96 | }; 97 | 98 | // Write the constants 99 | let is_landscape = w > h; 100 | let header24 = (63.0 * l_dc).round() as u32 101 | | (((31.5 + 31.5 * p_dc).round() as u32) << 6) 102 | | (((31.5 + 31.5 * q_dc).round() as u32) << 12) 103 | | (((31.0 * l_scale).round() as u32) << 18) 104 | | if has_alpha { 1 << 23 } else { 0 }; 105 | let header16 = (if is_landscape { ly } else { lx }) as u16 106 | | (((63.0 * p_scale).round() as u16) << 3) 107 | | (((63.0 * q_scale).round() as u16) << 9) 108 | | if is_landscape { 1 << 15 } else { 0 }; 109 | let mut hash = Vec::with_capacity(25); 110 | hash.extend_from_slice(&[ 111 | (header24 & 255) as u8, 112 | ((header24 >> 8) & 255) as u8, 113 | (header24 >> 16) as u8, 114 | (header16 & 255) as u8, 115 | (header16 >> 8) as u8, 116 | ]); 117 | let mut is_odd = false; 118 | if has_alpha { 119 | hash.push((15.0 * a_dc).round() as u8 | (((15.0 * a_scale).round() as u8) << 4)); 120 | } 121 | 122 | // Write the varying factors 123 | for ac in [l_ac, p_ac, q_ac] { 124 | for f in ac { 125 | let u = (15.0 * f).round() as u8; 126 | if is_odd { 127 | *hash.last_mut().unwrap() |= u << 4; 128 | } else { 129 | hash.push(u); 130 | } 131 | is_odd = !is_odd; 132 | } 133 | } 134 | if has_alpha { 135 | for f in a_ac { 136 | let u = (15.0 * f).round() as u8; 137 | if is_odd { 138 | *hash.last_mut().unwrap() |= u << 4; 139 | } else { 140 | hash.push(u); 141 | } 142 | is_odd = !is_odd; 143 | } 144 | } 145 | hash 146 | } 147 | 148 | fn read_byte(bytes: &mut &[u8]) -> Result { 149 | let mut byte = [0; 1]; 150 | bytes.read_exact(&mut byte).map_err(|_| ())?; 151 | Ok(byte[0]) 152 | } 153 | 154 | /// Decodes a ThumbHash to an RGBA image. 155 | /// 156 | /// RGB is not be premultiplied by A. Returns the width, height, and pixels of 157 | /// the rendered placeholder image. An error will be returned if the input is 158 | /// too short. 159 | pub fn thumb_hash_to_rgba(mut hash: &[u8]) -> Result<(usize, usize, Vec), ()> { 160 | let ratio = thumb_hash_to_approximate_aspect_ratio(hash)?; 161 | 162 | // Read the constants 163 | let header24 = read_byte(&mut hash)? as u32 164 | | ((read_byte(&mut hash)? as u32) << 8) 165 | | ((read_byte(&mut hash)? as u32) << 16); 166 | let header16 = read_byte(&mut hash)? as u16 | ((read_byte(&mut hash)? as u16) << 8); 167 | let l_dc = (header24 & 63) as f32 / 63.0; 168 | let p_dc = ((header24 >> 6) & 63) as f32 / 31.5 - 1.0; 169 | let q_dc = ((header24 >> 12) & 63) as f32 / 31.5 - 1.0; 170 | let l_scale = ((header24 >> 18) & 31) as f32 / 31.0; 171 | let has_alpha = (header24 >> 23) != 0; 172 | let p_scale = ((header16 >> 3) & 63) as f32 / 63.0; 173 | let q_scale = ((header16 >> 9) & 63) as f32 / 63.0; 174 | let is_landscape = (header16 >> 15) != 0; 175 | let l_max = if has_alpha { 5 } else { 7 }; 176 | let lx = 3.max(if is_landscape { l_max } else { header16 & 7 }) as usize; 177 | let ly = 3.max(if is_landscape { header16 & 7 } else { l_max }) as usize; 178 | let (a_dc, a_scale) = if has_alpha { 179 | let header8 = read_byte(&mut hash)?; 180 | ((header8 & 15) as f32 / 15.0, (header8 >> 4) as f32 / 15.0) 181 | } else { 182 | (1.0, 1.0) 183 | }; 184 | 185 | // Read the varying factors (boost saturation by 1.25x to compensate for quantization) 186 | let mut prev_bits = None; 187 | let mut decode_channel = |nx: usize, ny: usize, scale: f32| -> Result, ()> { 188 | let mut ac = Vec::with_capacity(nx * ny); 189 | for cy in 0..ny { 190 | let mut cx = if cy > 0 { 0 } else { 1 }; 191 | while cx * ny < nx * (ny - cy) { 192 | let bits = if let Some(bits) = prev_bits { 193 | prev_bits = None; 194 | bits 195 | } else { 196 | let bits = read_byte(&mut hash)?; 197 | prev_bits = Some(bits >> 4); 198 | bits & 15 199 | }; 200 | ac.push((bits as f32 / 7.5 - 1.0) * scale); 201 | cx += 1; 202 | } 203 | } 204 | Ok(ac) 205 | }; 206 | let l_ac = decode_channel(lx, ly, l_scale)?; 207 | let p_ac = decode_channel(3, 3, p_scale * 1.25)?; 208 | let q_ac = decode_channel(3, 3, q_scale * 1.25)?; 209 | let a_ac = if has_alpha { 210 | decode_channel(5, 5, a_scale)? 211 | } else { 212 | Vec::new() 213 | }; 214 | 215 | // Decode using the DCT into RGB 216 | let (w, h) = if ratio > 1.0 { 217 | (32, (32.0 / ratio).round() as usize) 218 | } else { 219 | ((32.0 * ratio).round() as usize, 32) 220 | }; 221 | let mut rgba = Vec::with_capacity(w * h * 4); 222 | let mut fx = [0.0].repeat(7); 223 | let mut fy = [0.0].repeat(7); 224 | for y in 0..h { 225 | for x in 0..w { 226 | let mut l = l_dc; 227 | let mut p = p_dc; 228 | let mut q = q_dc; 229 | let mut a = a_dc; 230 | 231 | // Precompute the coefficients 232 | for cx in 0..lx.max(if has_alpha { 5 } else { 3 }) { 233 | fx[cx] = (PI / w as f32 * (x as f32 + 0.5) * cx as f32).cos(); 234 | } 235 | for cy in 0..ly.max(if has_alpha { 5 } else { 3 }) { 236 | fy[cy] = (PI / h as f32 * (y as f32 + 0.5) * cy as f32).cos(); 237 | } 238 | 239 | // Decode L 240 | let mut j = 0; 241 | for cy in 0..ly { 242 | let mut cx = if cy > 0 { 0 } else { 1 }; 243 | let fy2 = fy[cy] * 2.0; 244 | while cx * ly < lx * (ly - cy) { 245 | l += l_ac[j] * fx[cx] * fy2; 246 | j += 1; 247 | cx += 1; 248 | } 249 | } 250 | 251 | // Decode P and Q 252 | let mut j = 0; 253 | for cy in 0..3 { 254 | let mut cx = if cy > 0 { 0 } else { 1 }; 255 | let fy2 = fy[cy] * 2.0; 256 | while cx < 3 - cy { 257 | let f = fx[cx] * fy2; 258 | p += p_ac[j] * f; 259 | q += q_ac[j] * f; 260 | j += 1; 261 | cx += 1; 262 | } 263 | } 264 | 265 | // Decode A 266 | if has_alpha { 267 | let mut j = 0; 268 | for cy in 0..5 { 269 | let mut cx = if cy > 0 { 0 } else { 1 }; 270 | let fy2 = fy[cy] * 2.0; 271 | while cx < 5 - cy { 272 | a += a_ac[j] * fx[cx] * fy2; 273 | j += 1; 274 | cx += 1; 275 | } 276 | } 277 | } 278 | 279 | // Convert to RGB 280 | let b = l - 2.0 / 3.0 * p; 281 | let r = (3.0 * l - b + q) / 2.0; 282 | let g = r - q; 283 | rgba.extend_from_slice(&[ 284 | (r.clamp(0.0, 1.0) * 255.0) as u8, 285 | (g.clamp(0.0, 1.0) * 255.0) as u8, 286 | (b.clamp(0.0, 1.0) * 255.0) as u8, 287 | (a.clamp(0.0, 1.0) * 255.0) as u8, 288 | ]); 289 | } 290 | } 291 | Ok((w, h, rgba)) 292 | } 293 | 294 | /// Extracts the average color from a ThumbHash. 295 | /// 296 | /// Returns the RGBA values where each value ranges from 0 to 1. RGB is not be 297 | /// premultiplied by A. An error will be returned if the input is too short. 298 | pub fn thumb_hash_to_average_rgba(hash: &[u8]) -> Result<(f32, f32, f32, f32), ()> { 299 | if hash.len() < 5 { 300 | return Err(()); 301 | } 302 | let header = hash[0] as u32 | ((hash[1] as u32) << 8) | ((hash[2] as u32) << 16); 303 | let l = (header & 63) as f32 / 63.0; 304 | let p = ((header >> 6) & 63) as f32 / 31.5 - 1.0; 305 | let q = ((header >> 12) & 63) as f32 / 31.5 - 1.0; 306 | let has_alpha = (header >> 23) != 0; 307 | let a = if has_alpha { 308 | (hash[5] & 15) as f32 / 15.0 309 | } else { 310 | 1.0 311 | }; 312 | let b = l - 2.0 / 3.0 * p; 313 | let r = (3.0 * l - b + q) / 2.0; 314 | let g = r - q; 315 | Ok((r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0), a)) 316 | } 317 | 318 | /// Extracts the approximate aspect ratio of the original image. 319 | /// 320 | /// An error will be returned if the input is too short. 321 | pub fn thumb_hash_to_approximate_aspect_ratio(hash: &[u8]) -> Result { 322 | if hash.len() < 5 { 323 | return Err(()); 324 | } 325 | let has_alpha = (hash[2] & 0x80) != 0; 326 | let l_max = if has_alpha { 5 } else { 7 }; 327 | let l_min = hash[3] & 7; 328 | let is_landscape = (hash[4] & 0x80) != 0; 329 | let lx = if is_landscape { l_max } else { l_min }; 330 | let ly = if is_landscape { l_min } else { l_max }; 331 | Ok(lx as f32 / ly as f32) 332 | } 333 | -------------------------------------------------------------------------------- /swift/ThumbHash.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // NOTE: Swift has an exponential-time type checker and compiling very simple 4 | // expressions can easily take many seconds, especially when expressions involve 5 | // numeric type constructors. 6 | // 7 | // This file deliberately breaks compound expressions up into separate variables 8 | // to improve compile time even though this comes at the expense of readability. 9 | // This is a known workaround for this deficiency in the Swift compiler. 10 | // 11 | // The following command is helpful when debugging Swift compile time issues: 12 | // 13 | // swiftc ThumbHash.swift -Xfrontend -debug-time-function-bodies 14 | // 15 | // These optimizations brought the compile time for this file from around 2.5 16 | // seconds to around 250ms (10x faster). 17 | 18 | // NOTE: Swift's debug-build performance of for-in loops over numeric ranges is 19 | // really awful. Debug builds compile a very generic indexing iterator thing 20 | // that makes many nested calls for every iteration, which makes debug-build 21 | // performance crawl. 22 | // 23 | // This file deliberately avoids for-in loops that loop for more than a few 24 | // times to improve debug-build run time even though this comes at the expense 25 | // of readability. Similarly unsafe pointers are used instead of array getters 26 | // to avoid unnecessary bounds checks, which have extra overhead in debug builds. 27 | // 28 | // These optimizations brought the run time to encode and decode 10 ThumbHashes 29 | // in debug mode from 700ms to 70ms (10x faster). 30 | 31 | func rgbaToThumbHash(w: Int, h: Int, rgba: Data) -> Data { 32 | // Encoding an image larger than 100x100 is slow with no benefit 33 | assert(w <= 100 && h <= 100) 34 | assert(rgba.count == w * h * 4) 35 | 36 | // Determine the average color 37 | var avg_r: Float32 = 0 38 | var avg_g: Float32 = 0 39 | var avg_b: Float32 = 0 40 | var avg_a: Float32 = 0 41 | rgba.withUnsafeBytes { rgba in 42 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 43 | let n = w * h 44 | var i = 0 45 | while i < n { 46 | let alpha = Float32(rgba[3]) / 255 47 | avg_r += alpha / 255 * Float32(rgba[0]) 48 | avg_g += alpha / 255 * Float32(rgba[1]) 49 | avg_b += alpha / 255 * Float32(rgba[2]) 50 | avg_a += alpha 51 | rgba = rgba.advanced(by: 4) 52 | i += 1 53 | } 54 | } 55 | if avg_a > 0 { 56 | avg_r /= avg_a 57 | avg_g /= avg_a 58 | avg_b /= avg_a 59 | } 60 | 61 | let hasAlpha = avg_a < Float32(w * h) 62 | let l_limit = hasAlpha ? 5 : 7 // Use fewer luminance bits if there's alpha 63 | let imax_wh = max(w, h) 64 | let iwl_limit = l_limit * w 65 | let ihl_limit = l_limit * h 66 | let fmax_wh = Float32(imax_wh) 67 | let fwl_limit = Float32(iwl_limit) 68 | let fhl_limit = Float32(ihl_limit) 69 | let flx = round(fwl_limit / fmax_wh) 70 | let fly = round(fhl_limit / fmax_wh) 71 | var lx = Int(flx) 72 | var ly = Int(fly) 73 | lx = max(1, lx) 74 | ly = max(1, ly) 75 | var lpqa = [Float32](repeating: 0, count: w * h * 4) 76 | 77 | // Convert the image from RGBA to LPQA (composite atop the average color) 78 | rgba.withUnsafeBytes { rgba in 79 | lpqa.withUnsafeMutableBytes { lpqa in 80 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 81 | var lpqa = lpqa.baseAddress!.bindMemory(to: Float32.self, capacity: lpqa.count) 82 | let n = w * h 83 | var i = 0 84 | while i < n { 85 | let alpha = Float32(rgba[3]) / 255 86 | let r = avg_r * (1 - alpha) + alpha / 255 * Float32(rgba[0]) 87 | let g = avg_g * (1 - alpha) + alpha / 255 * Float32(rgba[1]) 88 | let b = avg_b * (1 - alpha) + alpha / 255 * Float32(rgba[2]) 89 | lpqa[0] = (r + g + b) / 3 90 | lpqa[1] = (r + g) / 2 - b 91 | lpqa[2] = r - g 92 | lpqa[3] = alpha 93 | rgba = rgba.advanced(by: 4) 94 | lpqa = lpqa.advanced(by: 4) 95 | i += 1 96 | } 97 | } 98 | } 99 | 100 | // Encode using the DCT into DC (constant) and normalized AC (varying) terms 101 | let encodeChannel = { (channel: UnsafePointer, nx: Int, ny: Int) -> (Float32, [Float32], Float32) in 102 | var dc: Float32 = 0 103 | var ac: [Float32] = [] 104 | var scale: Float32 = 0 105 | var fx = [Float32](repeating: 0, count: w) 106 | fx.withUnsafeMutableBytes { fx in 107 | let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count) 108 | var cy = 0 109 | while cy < ny { 110 | var cx = 0 111 | while cx * ny < nx * (ny - cy) { 112 | var ptr = channel 113 | var f: Float32 = 0 114 | var x = 0 115 | while x < w { 116 | let fw = Float32(w) 117 | let fxx = Float32(x) 118 | let fcx = Float32(cx) 119 | fx[x] = cos(Float32.pi / fw * fcx * (fxx + 0.5)) 120 | x += 1 121 | } 122 | var y = 0 123 | while y < h { 124 | let fh = Float32(h) 125 | let fyy = Float32(y) 126 | let fcy = Float32(cy) 127 | let fy = cos(Float32.pi / fh * fcy * (fyy + 0.5)) 128 | var x = 0 129 | while x < w { 130 | f += ptr.pointee * fx[x] * fy 131 | x += 1 132 | ptr = ptr.advanced(by: 4) 133 | } 134 | y += 1 135 | } 136 | f /= Float32(w * h) 137 | if cx > 0 || cy > 0 { 138 | ac.append(f) 139 | scale = max(scale, abs(f)) 140 | } else { 141 | dc = f 142 | } 143 | cx += 1 144 | } 145 | cy += 1 146 | } 147 | } 148 | if scale > 0 { 149 | let n = ac.count 150 | var i = 0 151 | while i < n { 152 | ac[i] = 0.5 + 0.5 / scale * ac[i] 153 | i += 1 154 | } 155 | } 156 | return (dc, ac, scale) 157 | } 158 | let ( 159 | (l_dc, l_ac, l_scale), 160 | (p_dc, p_ac, p_scale), 161 | (q_dc, q_ac, q_scale), 162 | (a_dc, a_ac, a_scale) 163 | ) = lpqa.withUnsafeBytes { lpqa in 164 | let lpqa = lpqa.baseAddress!.bindMemory(to: Float32.self, capacity: lpqa.count) 165 | return ( 166 | encodeChannel(lpqa, max(3, lx), max(3, ly)), 167 | encodeChannel(lpqa.advanced(by: 1), 3, 3), 168 | encodeChannel(lpqa.advanced(by: 2), 3, 3), 169 | hasAlpha ? encodeChannel(lpqa.advanced(by: 3), 5, 5) : (1, [], 1) 170 | ) 171 | } 172 | 173 | // Write the constants 174 | let isLandscape = w > h 175 | let fl_dc = round(63.0 * l_dc) 176 | let fp_dc = round(31.5 + 31.5 * p_dc) 177 | let fq_dc = round(31.5 + 31.5 * q_dc) 178 | let fl_scale = round(31.0 * l_scale) 179 | let il_dc = UInt32(fl_dc) 180 | let ip_dc = UInt32(fp_dc) 181 | let iq_dc = UInt32(fq_dc) 182 | let il_scale = UInt32(fl_scale) 183 | let ihasAlpha = UInt32(hasAlpha ? 1 : 0) 184 | let header24 = il_dc | (ip_dc << 6) | (iq_dc << 12) | (il_scale << 18) | (ihasAlpha << 23) 185 | let fp_scale = round(63.0 * p_scale) 186 | let fq_scale = round(63.0 * q_scale) 187 | let ilxy = UInt16(isLandscape ? ly : lx) 188 | let ip_scale = UInt16(fp_scale) 189 | let iq_scale = UInt16(fq_scale) 190 | let iisLandscape = UInt16(isLandscape ? 1 : 0) 191 | let header16 = ilxy | (ip_scale << 3) | (iq_scale << 9) | (iisLandscape << 15) 192 | var hash = Data(capacity: 25) 193 | hash.append(UInt8(header24 & 255)) 194 | hash.append(UInt8((header24 >> 8) & 255)) 195 | hash.append(UInt8(header24 >> 16)) 196 | hash.append(UInt8(header16 & 255)) 197 | hash.append(UInt8(header16 >> 8)) 198 | var isOdd = false 199 | if hasAlpha { 200 | let fa_dc = round(15.0 * a_dc) 201 | let fa_scale = round(15.0 * a_scale) 202 | let ia_dc = UInt8(fa_dc) 203 | let ia_scale = UInt8(fa_scale) 204 | hash.append(ia_dc | (ia_scale << 4)) 205 | } 206 | 207 | // Write the varying factors 208 | for ac in [l_ac, p_ac, q_ac] { 209 | for f in ac { 210 | let f15 = round(15.0 * f) 211 | let i15 = UInt8(f15) 212 | if isOdd { 213 | hash[hash.count - 1] |= i15 << 4 214 | } else { 215 | hash.append(i15) 216 | } 217 | isOdd = !isOdd 218 | } 219 | } 220 | if hasAlpha { 221 | for f in a_ac { 222 | let f15 = round(15.0 * f) 223 | let i15 = UInt8(f15) 224 | if isOdd { 225 | hash[hash.count - 1] |= i15 << 4 226 | } else { 227 | hash.append(i15) 228 | } 229 | isOdd = !isOdd 230 | } 231 | } 232 | return hash 233 | } 234 | 235 | func thumbHashToRGBA(hash: Data) -> (Int, Int, Data) { 236 | // Read the constants 237 | let h0 = UInt32(hash[0]) 238 | let h1 = UInt32(hash[1]) 239 | let h2 = UInt32(hash[2]) 240 | let h3 = UInt16(hash[3]) 241 | let h4 = UInt16(hash[4]) 242 | let header24 = h0 | (h1 << 8) | (h2 << 16) 243 | let header16 = h3 | (h4 << 8) 244 | let il_dc = header24 & 63 245 | let ip_dc = (header24 >> 6) & 63 246 | let iq_dc = (header24 >> 12) & 63 247 | var l_dc = Float32(il_dc) 248 | var p_dc = Float32(ip_dc) 249 | var q_dc = Float32(iq_dc) 250 | l_dc = l_dc / 63 251 | p_dc = p_dc / 31.5 - 1 252 | q_dc = q_dc / 31.5 - 1 253 | let il_scale = (header24 >> 18) & 31 254 | var l_scale = Float32(il_scale) 255 | l_scale = l_scale / 31 256 | let hasAlpha = (header24 >> 23) != 0 257 | let ip_scale = (header16 >> 3) & 63 258 | let iq_scale = (header16 >> 9) & 63 259 | var p_scale = Float32(ip_scale) 260 | var q_scale = Float32(iq_scale) 261 | p_scale = p_scale / 63 262 | q_scale = q_scale / 63 263 | let isLandscape = (header16 >> 15) != 0 264 | let lx16 = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7) 265 | let ly16 = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7) 266 | let lx = Int(lx16) 267 | let ly = Int(ly16) 268 | var a_dc = Float32(1) 269 | var a_scale = Float32(1) 270 | if hasAlpha { 271 | let ia_dc = hash[5] & 15 272 | let ia_scale = hash[5] >> 4 273 | a_dc = Float32(ia_dc) 274 | a_scale = Float32(ia_scale) 275 | a_dc /= 15 276 | a_scale /= 15 277 | } 278 | 279 | // Read the varying factors (boost saturation by 1.25x to compensate for quantization) 280 | let ac_start = hasAlpha ? 6 : 5 281 | var ac_index = 0 282 | let decodeChannel = { (nx: Int, ny: Int, scale: Float32) -> [Float32] in 283 | var ac: [Float32] = [] 284 | for cy in 0 ..< ny { 285 | var cx = cy > 0 ? 0 : 1 286 | while cx * ny < nx * (ny - cy) { 287 | let iac = (hash[ac_start + (ac_index >> 1)] >> ((ac_index & 1) << 2)) & 15; 288 | var fac = Float32(iac) 289 | fac = (fac / 7.5 - 1) * scale 290 | ac.append(fac) 291 | ac_index += 1 292 | cx += 1 293 | } 294 | } 295 | return ac 296 | } 297 | let l_ac = decodeChannel(lx, ly, l_scale) 298 | let p_ac = decodeChannel(3, 3, p_scale * 1.25) 299 | let q_ac = decodeChannel(3, 3, q_scale * 1.25) 300 | let a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : [] 301 | 302 | // Decode using the DCT into RGB 303 | let ratio = thumbHashToApproximateAspectRatio(hash: hash) 304 | let fw = round(ratio > 1 ? 32 : 32 * ratio) 305 | let fh = round(ratio > 1 ? 32 / ratio : 32) 306 | let w = Int(fw) 307 | let h = Int(fh) 308 | var rgba = Data(count: w * h * 4) 309 | let cx_stop = max(lx, hasAlpha ? 5 : 3) 310 | let cy_stop = max(ly, hasAlpha ? 5 : 3) 311 | var fx = [Float32](repeating: 0, count: cx_stop) 312 | var fy = [Float32](repeating: 0, count: cy_stop) 313 | fx.withUnsafeMutableBytes { fx in 314 | let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count) 315 | fy.withUnsafeMutableBytes { fy in 316 | let fy = fy.baseAddress!.bindMemory(to: Float32.self, capacity: fy.count) 317 | rgba.withUnsafeMutableBytes { rgba in 318 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 319 | var y = 0 320 | while y < h { 321 | var x = 0 322 | while x < w { 323 | var l = l_dc 324 | var p = p_dc 325 | var q = q_dc 326 | var a = a_dc 327 | 328 | // Precompute the coefficients 329 | var cx = 0 330 | while cx < cx_stop { 331 | let fw = Float32(w) 332 | let fxx = Float32(x) 333 | let fcx = Float32(cx) 334 | fx[cx] = cos(Float32.pi / fw * (fxx + 0.5) * fcx) 335 | cx += 1 336 | } 337 | var cy = 0 338 | while cy < cy_stop { 339 | let fh = Float32(h) 340 | let fyy = Float32(y) 341 | let fcy = Float32(cy) 342 | fy[cy] = cos(Float32.pi / fh * (fyy + 0.5) * fcy) 343 | cy += 1 344 | } 345 | 346 | // Decode L 347 | var j = 0 348 | cy = 0 349 | while cy < ly { 350 | var cx = cy > 0 ? 0 : 1 351 | let fy2 = fy[cy] * 2 352 | while cx * ly < lx * (ly - cy) { 353 | l += l_ac[j] * fx[cx] * fy2 354 | j += 1 355 | cx += 1 356 | } 357 | cy += 1 358 | } 359 | 360 | // Decode P and Q 361 | j = 0 362 | cy = 0 363 | while cy < 3 { 364 | var cx = cy > 0 ? 0 : 1 365 | let fy2 = fy[cy] * 2 366 | while cx < 3 - cy { 367 | let f = fx[cx] * fy2 368 | p += p_ac[j] * f 369 | q += q_ac[j] * f 370 | j += 1 371 | cx += 1 372 | } 373 | cy += 1 374 | } 375 | 376 | // Decode A 377 | if hasAlpha { 378 | j = 0 379 | cy = 0 380 | while cy < 5 { 381 | var cx = cy > 0 ? 0 : 1 382 | let fy2 = fy[cy] * 2 383 | while cx < 5 - cy { 384 | a += a_ac[j] * fx[cx] * fy2 385 | j += 1 386 | cx += 1 387 | } 388 | cy += 1 389 | } 390 | } 391 | 392 | // Convert to RGB 393 | var b = l - 2 / 3 * p 394 | var r = (3 * l - b + q) / 2 395 | var g = r - q 396 | r = max(0, 255 * min(1, r)) 397 | g = max(0, 255 * min(1, g)) 398 | b = max(0, 255 * min(1, b)) 399 | a = max(0, 255 * min(1, a)) 400 | rgba[0] = UInt8(r) 401 | rgba[1] = UInt8(g) 402 | rgba[2] = UInt8(b) 403 | rgba[3] = UInt8(a) 404 | rgba = rgba.advanced(by: 4) 405 | x += 1 406 | } 407 | y += 1 408 | } 409 | } 410 | } 411 | } 412 | return (w, h, rgba) 413 | } 414 | 415 | func thumbHashToAverageRGBA(hash: Data) -> (Float32, Float32, Float32, Float32) { 416 | let h0 = UInt32(hash[0]) 417 | let h1 = UInt32(hash[1]) 418 | let h2 = UInt32(hash[2]) 419 | let header = h0 | (h1 << 8) | (h2 << 16) 420 | let il = header & 63 421 | let ip = (header >> 6) & 63 422 | let iq = (header >> 12) & 63 423 | var l = Float32(il) 424 | var p = Float32(ip) 425 | var q = Float32(iq) 426 | l = l / 63 427 | p = p / 31.5 - 1 428 | q = q / 31.5 - 1 429 | let hasAlpha = (header >> 23) != 0 430 | var a = Float32(1) 431 | if hasAlpha { 432 | let ia = hash[5] & 15 433 | a = Float32(ia) 434 | a = a / 15 435 | } 436 | let b = l - 2 / 3 * p 437 | let r = (3 * l - b + q) / 2 438 | let g = r - q 439 | return ( 440 | max(0, min(1, r)), 441 | max(0, min(1, g)), 442 | max(0, min(1, b)), 443 | a 444 | ) 445 | } 446 | 447 | func thumbHashToApproximateAspectRatio(hash: Data) -> Float32 { 448 | let header = hash[3] 449 | let hasAlpha = (hash[2] & 0x80) != 0 450 | let isLandscape = (hash[4] & 0x80) != 0 451 | let lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7 452 | let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7 453 | return Float32(lx) / Float32(ly) 454 | } 455 | 456 | #if os(macOS) 457 | import Cocoa 458 | 459 | func imageToThumbHash(image: NSImage) -> Data { 460 | let size = image.size 461 | let fw = round(100 * size.width / max(size.width, size.height)) 462 | let fh = round(100 * size.height / max(size.width, size.height)) 463 | let w = Int(fw) 464 | let h = Int(fh) 465 | var rgba = Data(count: w * h * 4) 466 | rgba.withUnsafeMutableBytes { rgba in 467 | var rect = NSRect(x: 0, y: 0, width: w, height: h) 468 | if 469 | let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil), 470 | let space = (image.representations[0] as? NSBitmapImageRep)?.colorSpace.cgColorSpace, 471 | let context = CGContext( 472 | data: rgba.baseAddress, 473 | width: w, 474 | height: h, 475 | bitsPerComponent: 8, 476 | bytesPerRow: w * 4, 477 | space: space, 478 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 479 | ) 480 | { 481 | context.draw(cgImage, in: rect) 482 | 483 | // Convert from premultiplied alpha to unpremultiplied alpha 484 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 485 | let n = w * h 486 | var i = 0 487 | while i < n { 488 | let a = UInt16(rgba[3]) 489 | if a > 0 && a < 255 { 490 | var r = UInt16(rgba[0]) 491 | var g = UInt16(rgba[1]) 492 | var b = UInt16(rgba[2]) 493 | r = min(255, r * 255 / a) 494 | g = min(255, g * 255 / a) 495 | b = min(255, b * 255 / a) 496 | rgba[0] = UInt8(r) 497 | rgba[1] = UInt8(g) 498 | rgba[2] = UInt8(b) 499 | } 500 | rgba = rgba.advanced(by: 4) 501 | i += 1 502 | } 503 | } 504 | } 505 | return rgbaToThumbHash(w: w, h: h, rgba: rgba) 506 | } 507 | 508 | func thumbHashToImage(hash: Data) -> NSImage { 509 | let (w, h, rgba) = thumbHashToRGBA(hash: hash) 510 | let bitmap = NSBitmapImageRep( 511 | bitmapDataPlanes: nil, 512 | pixelsWide: w, 513 | pixelsHigh: h, 514 | bitsPerSample: 8, 515 | samplesPerPixel: 4, 516 | hasAlpha: true, 517 | isPlanar: false, 518 | colorSpaceName: .deviceRGB, 519 | bytesPerRow: w * 4, 520 | bitsPerPixel: 32 521 | )! 522 | rgba.withUnsafeBytes { rgba in 523 | // Convert from unpremultiplied alpha to premultiplied alpha 524 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 525 | var to = bitmap.bitmapData! 526 | let n = w * h 527 | var i = 0 528 | while i < n { 529 | let a = rgba[3] 530 | if a == 255 { 531 | to[0] = rgba[0] 532 | to[1] = rgba[1] 533 | to[2] = rgba[2] 534 | } else { 535 | var r = UInt16(rgba[0]) 536 | var g = UInt16(rgba[1]) 537 | var b = UInt16(rgba[2]) 538 | let a = UInt16(a) 539 | r = min(255, r * a / 255) 540 | g = min(255, g * a / 255) 541 | b = min(255, b * a / 255) 542 | to[0] = UInt8(r) 543 | to[1] = UInt8(g) 544 | to[2] = UInt8(b) 545 | } 546 | to[3] = a 547 | rgba = rgba.advanced(by: 4) 548 | to = to.advanced(by: 4) 549 | i += 1 550 | } 551 | } 552 | let image = NSImage(size: NSSize(width: w, height: h)) 553 | image.addRepresentation(bitmap) 554 | return image 555 | } 556 | #endif 557 | 558 | #if os(iOS) 559 | import UIKit 560 | 561 | func imageToThumbHash(image: UIImage) -> Data { 562 | let size = image.size 563 | let w = Int(round(100 * size.width / max(size.width, size.height))) 564 | let h = Int(round(100 * size.height / max(size.width, size.height))) 565 | var rgba = Data(count: w * h * 4) 566 | rgba.withUnsafeMutableBytes { rgba in 567 | if 568 | let space = image.cgImage?.colorSpace, 569 | let context = CGContext( 570 | data: rgba.baseAddress, 571 | width: w, 572 | height: h, 573 | bitsPerComponent: 8, 574 | bytesPerRow: w * 4, 575 | space: space, 576 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 577 | ) 578 | { 579 | // EXIF orientation only works if you draw the UIImage, not the CGImage 580 | context.concatenate(CGAffineTransform(1, 0, 0, -1, 0, CGFloat(h))) 581 | UIGraphicsPushContext(context) 582 | image.draw(in: CGRect(x: 0, y: 0, width: w, height: h)) 583 | UIGraphicsPopContext() 584 | 585 | // Convert from premultiplied alpha to unpremultiplied alpha 586 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 587 | let n = w * h 588 | var i = 0 589 | while i < n { 590 | let a = UInt16(rgba[3]) 591 | if a > 0 && a < 255 { 592 | var r = UInt16(rgba[0]) 593 | var g = UInt16(rgba[1]) 594 | var b = UInt16(rgba[2]) 595 | r = min(255, r * 255 / a) 596 | g = min(255, g * 255 / a) 597 | b = min(255, b * 255 / a) 598 | rgba[0] = UInt8(r) 599 | rgba[1] = UInt8(g) 600 | rgba[2] = UInt8(b) 601 | } 602 | rgba = rgba.advanced(by: 4) 603 | i += 1 604 | } 605 | } 606 | } 607 | return rgbaToThumbHash(w: w, h: h, rgba: rgba) 608 | } 609 | 610 | func thumbHashToImage(hash: Data) -> UIImage { 611 | var (w, h, rgba) = thumbHashToRGBA(hash: hash) 612 | rgba.withUnsafeMutableBytes { rgba in 613 | // Convert from unpremultiplied alpha to premultiplied alpha 614 | var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count) 615 | let n = w * h 616 | var i = 0 617 | while i < n { 618 | let a = UInt16(rgba[3]) 619 | if a < 255 { 620 | var r = UInt16(rgba[0]) 621 | var g = UInt16(rgba[1]) 622 | var b = UInt16(rgba[2]) 623 | r = min(255, r * a / 255) 624 | g = min(255, g * a / 255) 625 | b = min(255, b * a / 255) 626 | rgba[0] = UInt8(r) 627 | rgba[1] = UInt8(g) 628 | rgba[2] = UInt8(b) 629 | } 630 | rgba = rgba.advanced(by: 4) 631 | i += 1 632 | } 633 | } 634 | let image = CGImage( 635 | width: w, 636 | height: h, 637 | bitsPerComponent: 8, 638 | bitsPerPixel: 32, 639 | bytesPerRow: w * 4, 640 | space: CGColorSpaceCreateDeviceRGB(), 641 | bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue), 642 | provider: CGDataProvider(data: rgba as CFData)!, 643 | decode: nil, 644 | shouldInterpolate: true, 645 | intent: .perceptual 646 | ) 647 | return UIImage(cgImage: image!) 648 | } 649 | #endif 650 | --------------------------------------------------------------------------------