├── .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 |
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 |
--------------------------------------------------------------------------------