react-native-fast-image 10.1.1 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -10
- package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
- package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
- package/dist/index.cjs.js +47 -9
- package/dist/index.cjs.js.flow +9 -0
- package/dist/index.cjs.web.js +49 -6
- package/dist/index.cjs.web.js.flow +9 -0
- package/dist/index.d.ts +56 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +9 -0
- package/dist/index.web.js +49 -6
- package/dist/index.web.js.flow +9 -0
- package/dist/placeholders.d.ts +4 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
- package/dist/thumbhash.cjs.js +241 -0
- package/dist/thumbhash.d.ts +12 -0
- package/dist/thumbhash.js +240 -0
- package/ios/FastImage/FFFBoxBlur.h +7 -0
- package/ios/FastImage/FFFBoxBlur.m +29 -0
- package/ios/FastImage/FFFThumbHash.h +24 -0
- package/ios/FastImage/FFFThumbHash.m +269 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +93 -46
- package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
- package/package.json +4 -2
- package/src/index.js.flow +9 -0
- package/src/index.tsx +142 -31
- package/src/index.web.tsx +119 -11
- package/src/placeholders.ts +45 -0
- package/src/specs/FastImageViewNativeComponent.ts +8 -2
- package/src/thumbhash.ts +363 -0
- package/thumbhash/package.json +8 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//#region src/thumbhash.ts
|
|
2
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3
|
+
const CACHE_SIZE = 256;
|
|
4
|
+
/**
|
|
5
|
+
* Makes a source's `blurDataUri` from a ThumbHash, e.g. on your server, so
|
|
6
|
+
* the page shows the preview without any JavaScript. Undefined for an invalid
|
|
7
|
+
* hash. Works in browsers, Node and React Native.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```js
|
|
11
|
+
* // e.g. on your server, so the page shows the preview without any JavaScript
|
|
12
|
+
* const source = { uri: photo.url, blurDataUri: thumbHashToBlurDataUri(photo.thumbhash) }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
function thumbHashToBlurDataUri(thumbhash) {
|
|
16
|
+
if (!thumbhash) return void 0;
|
|
17
|
+
if (cache.has(thumbhash)) {
|
|
18
|
+
const uri = cache.get(thumbhash);
|
|
19
|
+
cache.delete(thumbhash);
|
|
20
|
+
cache.set(thumbhash, uri);
|
|
21
|
+
return uri;
|
|
22
|
+
}
|
|
23
|
+
const hash = base64Bytes(thumbhash);
|
|
24
|
+
const image = hash ? thumbHashToRGBA(hash) : void 0;
|
|
25
|
+
const small = image ? shrink(image.w, image.h, image.rgba, 8) : void 0;
|
|
26
|
+
const uri = small ? rgbaToDataURL(small.w, small.h, small.rgba) : void 0;
|
|
27
|
+
if (cache.size >= CACHE_SIZE) cache.delete(cache.keys().next().value);
|
|
28
|
+
cache.set(thumbhash, uri);
|
|
29
|
+
return uri;
|
|
30
|
+
}
|
|
31
|
+
globalThis[Symbol.for("react-native-fast-image/thumbhash")] = thumbHashToBlurDataUri;
|
|
32
|
+
function shrink(w, h, rgba, max) {
|
|
33
|
+
const scale = Math.max(w, h) / max;
|
|
34
|
+
if (scale <= 1) return {
|
|
35
|
+
w,
|
|
36
|
+
h,
|
|
37
|
+
rgba
|
|
38
|
+
};
|
|
39
|
+
const sw = Math.max(1, Math.round(w / scale));
|
|
40
|
+
const sh = Math.max(1, Math.round(h / scale));
|
|
41
|
+
const out = new Uint8Array(sw * sh * 4);
|
|
42
|
+
for (let y = 0; y < sh; y++) {
|
|
43
|
+
const y0 = Math.floor(y * h / sh);
|
|
44
|
+
const y1 = Math.floor((y + 1) * h / sh);
|
|
45
|
+
for (let x = 0; x < sw; x++) {
|
|
46
|
+
const x0 = Math.floor(x * w / sw);
|
|
47
|
+
const x1 = Math.floor((x + 1) * w / sw);
|
|
48
|
+
for (let c = 0; c < 4; c++) {
|
|
49
|
+
let sum = 0;
|
|
50
|
+
for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) sum += rgba[(yy * w + xx) * 4 + c];
|
|
51
|
+
out[(y * sw + x) * 4 + c] = Math.round(sum / ((y1 - y0) * (x1 - x0)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
w: sw,
|
|
57
|
+
h: sh,
|
|
58
|
+
rgba: out
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function base64Bytes(base64) {
|
|
62
|
+
const standard = base64.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
63
|
+
if (!/^[A-Za-z0-9+/]*=*$/.test(standard)) return void 0;
|
|
64
|
+
try {
|
|
65
|
+
const binary = atob(standard.replace(/=+$/, "") + "=".repeat((4 - standard.replace(/=+$/, "").length % 4) % 4));
|
|
66
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function count(nx, ny) {
|
|
72
|
+
let n = 0;
|
|
73
|
+
for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) n++;
|
|
74
|
+
return n;
|
|
75
|
+
}
|
|
76
|
+
function thumbHashToRGBA(hash) {
|
|
77
|
+
const { PI, min, max, cos, round } = Math;
|
|
78
|
+
if (hash.length < 5) return void 0;
|
|
79
|
+
const header24 = hash[0] | hash[1] << 8 | hash[2] << 16;
|
|
80
|
+
const header16 = hash[3] | hash[4] << 8;
|
|
81
|
+
const l_dc = (header24 & 63) / 63;
|
|
82
|
+
const p_dc = (header24 >> 6 & 63) / 31.5 - 1;
|
|
83
|
+
const q_dc = (header24 >> 12 & 63) / 31.5 - 1;
|
|
84
|
+
const l_scale = (header24 >> 18 & 31) / 31;
|
|
85
|
+
const hasAlpha = header24 >> 23;
|
|
86
|
+
const p_scale = (header16 >> 3 & 63) / 63;
|
|
87
|
+
const q_scale = (header16 >> 9 & 63) / 63;
|
|
88
|
+
const isLandscape = header16 >> 15;
|
|
89
|
+
const lx = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
|
|
90
|
+
const ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
|
|
91
|
+
const ac_start = hasAlpha ? 6 : 5;
|
|
92
|
+
const ac_count = count(lx, ly) + 2 * count(3, 3) + (hasAlpha ? count(5, 5) : 0);
|
|
93
|
+
if (hash.length < ac_start + Math.ceil(ac_count / 2)) return void 0;
|
|
94
|
+
const a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1;
|
|
95
|
+
const a_scale = (hash[5] >> 4) / 15;
|
|
96
|
+
let ac_index = 0;
|
|
97
|
+
const decodeChannel = (nx, ny, scale) => {
|
|
98
|
+
const ac = [];
|
|
99
|
+
for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) ac.push(((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2) & 15) / 7.5 - 1) * scale);
|
|
100
|
+
return ac;
|
|
101
|
+
};
|
|
102
|
+
const l_ac = decodeChannel(lx, ly, l_scale);
|
|
103
|
+
const p_ac = decodeChannel(3, 3, p_scale * 1.25);
|
|
104
|
+
const q_ac = decodeChannel(3, 3, q_scale * 1.25);
|
|
105
|
+
const a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : [];
|
|
106
|
+
const ratio = thumbHashToApproximateAspectRatio(hash);
|
|
107
|
+
const w = max(1, round(ratio > 1 ? 32 : 32 * ratio));
|
|
108
|
+
const h = max(1, round(ratio > 1 ? 32 / ratio : 32));
|
|
109
|
+
const rgba = new Uint8Array(w * h * 4);
|
|
110
|
+
const fx = [];
|
|
111
|
+
const fy = [];
|
|
112
|
+
for (let y = 0, i = 0; y < h; y++) for (let x = 0; x < w; x++, i += 4) {
|
|
113
|
+
let l = l_dc, p = p_dc, q = q_dc, a = a_dc;
|
|
114
|
+
for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) fx[cx] = cos(PI / w * (x + .5) * cx);
|
|
115
|
+
for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) fy[cy] = cos(PI / h * (y + .5) * cy);
|
|
116
|
+
for (let cy = 0, j = 0; cy < ly; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) l += l_ac[j] * fx[cx] * fy2;
|
|
117
|
+
for (let cy = 0, j = 0; cy < 3; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) {
|
|
118
|
+
const f = fx[cx] * fy2;
|
|
119
|
+
p += p_ac[j] * f;
|
|
120
|
+
q += q_ac[j] * f;
|
|
121
|
+
}
|
|
122
|
+
if (hasAlpha) for (let cy = 0, j = 0; cy < 5; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) a += a_ac[j] * fx[cx] * fy2;
|
|
123
|
+
const b = l - 2 / 3 * p;
|
|
124
|
+
const r = (3 * l - b + q) / 2;
|
|
125
|
+
const g = r - q;
|
|
126
|
+
rgba[i] = max(0, 255 * min(1, r));
|
|
127
|
+
rgba[i + 1] = max(0, 255 * min(1, g));
|
|
128
|
+
rgba[i + 2] = max(0, 255 * min(1, b));
|
|
129
|
+
rgba[i + 3] = max(0, 255 * min(1, a));
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
w,
|
|
133
|
+
h,
|
|
134
|
+
rgba
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function thumbHashToApproximateAspectRatio(hash) {
|
|
138
|
+
const header = hash[3];
|
|
139
|
+
const hasAlpha = hash[2] & 128;
|
|
140
|
+
const isLandscape = hash[4] & 128;
|
|
141
|
+
const lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7;
|
|
142
|
+
const ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7;
|
|
143
|
+
return ly ? lx / ly : 1;
|
|
144
|
+
}
|
|
145
|
+
function rgbaToDataURL(w, h, rgba) {
|
|
146
|
+
const row = w * 4 + 1;
|
|
147
|
+
const idat = 6 + h * (5 + row);
|
|
148
|
+
const bytes = [
|
|
149
|
+
137,
|
|
150
|
+
80,
|
|
151
|
+
78,
|
|
152
|
+
71,
|
|
153
|
+
13,
|
|
154
|
+
10,
|
|
155
|
+
26,
|
|
156
|
+
10,
|
|
157
|
+
0,
|
|
158
|
+
0,
|
|
159
|
+
0,
|
|
160
|
+
13,
|
|
161
|
+
73,
|
|
162
|
+
72,
|
|
163
|
+
68,
|
|
164
|
+
82,
|
|
165
|
+
0,
|
|
166
|
+
0,
|
|
167
|
+
w >> 8,
|
|
168
|
+
w & 255,
|
|
169
|
+
0,
|
|
170
|
+
0,
|
|
171
|
+
h >> 8,
|
|
172
|
+
h & 255,
|
|
173
|
+
8,
|
|
174
|
+
6,
|
|
175
|
+
0,
|
|
176
|
+
0,
|
|
177
|
+
0,
|
|
178
|
+
0,
|
|
179
|
+
0,
|
|
180
|
+
0,
|
|
181
|
+
0,
|
|
182
|
+
idat >>> 24,
|
|
183
|
+
idat >> 16 & 255,
|
|
184
|
+
idat >> 8 & 255,
|
|
185
|
+
idat & 255,
|
|
186
|
+
73,
|
|
187
|
+
68,
|
|
188
|
+
65,
|
|
189
|
+
84,
|
|
190
|
+
120,
|
|
191
|
+
1
|
|
192
|
+
];
|
|
193
|
+
const table = [
|
|
194
|
+
0,
|
|
195
|
+
498536548,
|
|
196
|
+
997073096,
|
|
197
|
+
651767980,
|
|
198
|
+
1994146192,
|
|
199
|
+
1802195444,
|
|
200
|
+
1303535960,
|
|
201
|
+
1342533948,
|
|
202
|
+
-306674912,
|
|
203
|
+
-267414716,
|
|
204
|
+
-690576408,
|
|
205
|
+
-882789492,
|
|
206
|
+
-1687895376,
|
|
207
|
+
-2032938284,
|
|
208
|
+
-1609899400,
|
|
209
|
+
-1111625188
|
|
210
|
+
];
|
|
211
|
+
let a = 1, b = 0;
|
|
212
|
+
for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) {
|
|
213
|
+
bytes.push(y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, row >> 8 ^ 255, 0);
|
|
214
|
+
for (b = (b + a) % 65521; i < end; i++) {
|
|
215
|
+
const u = rgba[i] & 255;
|
|
216
|
+
bytes.push(u);
|
|
217
|
+
a = (a + u) % 65521;
|
|
218
|
+
b = (b + a) % 65521;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
bytes.push(b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130);
|
|
222
|
+
for (let [start, end] of [[12, 29], [37, 41 + idat]]) {
|
|
223
|
+
let c = -1;
|
|
224
|
+
for (let i = start; i < end; i++) {
|
|
225
|
+
c ^= bytes[i];
|
|
226
|
+
c = c >>> 4 ^ table[c & 15];
|
|
227
|
+
c = c >>> 4 ^ table[c & 15];
|
|
228
|
+
}
|
|
229
|
+
c = ~c;
|
|
230
|
+
bytes[end++] = c >>> 24;
|
|
231
|
+
bytes[end++] = c >> 16 & 255;
|
|
232
|
+
bytes[end++] = c >> 8 & 255;
|
|
233
|
+
bytes[end++] = c & 255;
|
|
234
|
+
}
|
|
235
|
+
let binary = "";
|
|
236
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
237
|
+
return "data:image/png;base64," + btoa(binary);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
export { thumbHashToBlurDataUri };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
2
|
+
|
|
3
|
+
// Blurs a bitmap context's pixels in place (8 bits per channel, 4 channels in
|
|
4
|
+
// any order, premultiplied if it has alpha): a box blur of boxSize (odd)
|
|
5
|
+
// three times, close to a Gaussian blur, with the edges extended outwards.
|
|
6
|
+
// For blurRadius and for a source's blurDataUri.
|
|
7
|
+
void FFFBoxBlur(CGContextRef context, uint32_t boxSize);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#import "FFFBoxBlur.h"
|
|
2
|
+
#import <Accelerate/Accelerate.h>
|
|
3
|
+
|
|
4
|
+
void FFFBoxBlur(CGContextRef context, uint32_t boxSize)
|
|
5
|
+
{
|
|
6
|
+
if (boxSize <= 1) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
size_t height = CGBitmapContextGetHeight(context);
|
|
10
|
+
vImage_Buffer buffer = {
|
|
11
|
+
.data = CGBitmapContextGetData(context),
|
|
12
|
+
.height = height,
|
|
13
|
+
.width = CGBitmapContextGetWidth(context),
|
|
14
|
+
.rowBytes = CGBitmapContextGetBytesPerRow(context),
|
|
15
|
+
};
|
|
16
|
+
vImage_Buffer other = buffer;
|
|
17
|
+
other.data = malloc(buffer.rowBytes * height);
|
|
18
|
+
vImage_Error tempSize = vImageBoxConvolve_ARGB8888(&buffer, &other, NULL, 0, 0, boxSize, boxSize, NULL,
|
|
19
|
+
kvImageGetTempBufferSize | kvImageEdgeExtend);
|
|
20
|
+
void* temp = tempSize > 0 ? malloc(tempSize) : NULL;
|
|
21
|
+
if (other.data && temp) {
|
|
22
|
+
vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
23
|
+
vImageBoxConvolve_ARGB8888(&other, &buffer, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
24
|
+
vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
25
|
+
memcpy(buffer.data, other.data, buffer.rowBytes * height);
|
|
26
|
+
}
|
|
27
|
+
free(other.data);
|
|
28
|
+
free(temp);
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
// A source's `thumbhash` (https://evanw.github.io/thumbhash/) decoded into the
|
|
6
|
+
// placeholder it shows while its image loads: at most 32 × 32 pixels, which
|
|
7
|
+
// the view scales up smoothly. Decoding takes well under a millisecond, so it
|
|
8
|
+
// happens as the view picks its source; recent ones are cached by their hash.
|
|
9
|
+
// Also a source's `blurDataUri`: a data uri's few pixels, scaled up to the
|
|
10
|
+
// same size and blurred.
|
|
11
|
+
@interface FFFThumbHash : NSObject
|
|
12
|
+
|
|
13
|
+
// A source's placeholder: its blurDataUri's (PNG, JPEG, WebP, a GIF's first
|
|
14
|
+
// frame; nil for one iOS doesn't decode, e.g. an SVG), or its ThumbHash's.
|
|
15
|
+
// Main thread.
|
|
16
|
+
+ (nullable UIImage*) imageForPlaceholder: (nullable NSString*)placeholder;
|
|
17
|
+
|
|
18
|
+
// The placeholder for a base64 ThumbHash (standard or url-safe, with or
|
|
19
|
+
// without padding), or nil for an empty or invalid one. Main thread.
|
|
20
|
+
+ (nullable UIImage*) imageForThumbHash: (nullable NSString*)thumbhash;
|
|
21
|
+
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#import "FFFThumbHash.h"
|
|
2
|
+
#import "FFFBoxBlur.h"
|
|
3
|
+
#include <math.h>
|
|
4
|
+
|
|
5
|
+
// FFFThumbHashToRGBA is Evan Wallace's reference decoder
|
|
6
|
+
// (https://github.com/evanw/thumbhash, thumbHashToRGBA in java/ and js/) in C,
|
|
7
|
+
// with a check that the hash is long enough for what its header says it
|
|
8
|
+
// holds:
|
|
9
|
+
//
|
|
10
|
+
// Copyright (c) 2023 Evan Wallace
|
|
11
|
+
//
|
|
12
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
// of this software and associated documentation files (the "Software"), to
|
|
14
|
+
// deal in the Software without restriction, including without limitation the
|
|
15
|
+
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
16
|
+
// sell copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
// furnished to do so, subject to the following conditions:
|
|
18
|
+
//
|
|
19
|
+
// The above copyright notice and this permission notice shall be included in
|
|
20
|
+
// all copies or substantial portions of the Software.
|
|
21
|
+
//
|
|
22
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
27
|
+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
28
|
+
// IN THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
// How many AC factors a channel of nx × ny has.
|
|
31
|
+
static int FFFThumbHashCount(int nx, int ny) {
|
|
32
|
+
int n = 0;
|
|
33
|
+
for (int cy = 0; cy < ny; cy++)
|
|
34
|
+
for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
|
|
35
|
+
n++;
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static void FFFThumbHashDecodeChannel(const uint8_t* hash, int start, int* index, int nx, int ny, float scale, float* ac) {
|
|
40
|
+
int n = FFFThumbHashCount(nx, ny);
|
|
41
|
+
for (int i = 0; i < n; i++) {
|
|
42
|
+
int data = hash[start + (*index >> 1)] >> ((*index & 1) << 2);
|
|
43
|
+
ac[i] = ((float) (data & 15) / 7.5f - 1.0f) * scale;
|
|
44
|
+
(*index)++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static uint8_t FFFThumbHashChannel(float value) {
|
|
49
|
+
return (uint8_t) fmaxf(0, roundf(255.0f * fminf(1, value)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The hash decoded as unpremultiplied RGBA (malloc'd, the caller frees it),
|
|
53
|
+
// with its width and height, or NULL if it's too short.
|
|
54
|
+
static uint8_t* FFFThumbHashToRGBA(const uint8_t* hash, NSUInteger length, int* width, int* height) {
|
|
55
|
+
if (length < 5) {
|
|
56
|
+
return NULL;
|
|
57
|
+
}
|
|
58
|
+
// Read the constants
|
|
59
|
+
int header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16);
|
|
60
|
+
int header16 = hash[3] | (hash[4] << 8);
|
|
61
|
+
float l_dc = (float) (header24 & 63) / 63.0f;
|
|
62
|
+
float p_dc = (float) ((header24 >> 6) & 63) / 31.5f - 1.0f;
|
|
63
|
+
float q_dc = (float) ((header24 >> 12) & 63) / 31.5f - 1.0f;
|
|
64
|
+
float l_scale = (float) ((header24 >> 18) & 31) / 31.0f;
|
|
65
|
+
BOOL hasAlpha = (header24 >> 23) != 0;
|
|
66
|
+
float p_scale = (float) ((header16 >> 3) & 63) / 63.0f;
|
|
67
|
+
float q_scale = (float) ((header16 >> 9) & 63) / 63.0f;
|
|
68
|
+
BOOL isLandscape = (header16 >> 15) != 0;
|
|
69
|
+
int lx = MAX(3, isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7);
|
|
70
|
+
int ly = MAX(3, isLandscape ? header16 & 7 : (hasAlpha ? 5 : 7));
|
|
71
|
+
int ac_start = hasAlpha ? 6 : 5;
|
|
72
|
+
int l_count = FFFThumbHashCount(lx, ly);
|
|
73
|
+
int pq_count = FFFThumbHashCount(3, 3);
|
|
74
|
+
int a_count = hasAlpha ? FFFThumbHashCount(5, 5) : 0;
|
|
75
|
+
if (length < (NSUInteger) (ac_start + (l_count + 2 * pq_count + a_count + 1) / 2)) {
|
|
76
|
+
return NULL;
|
|
77
|
+
}
|
|
78
|
+
float a_dc = hasAlpha ? (float) (hash[5] & 15) / 15.0f : 1.0f;
|
|
79
|
+
float a_scale = hasAlpha ? (float) ((hash[5] >> 4) & 15) / 15.0f : 0;
|
|
80
|
+
|
|
81
|
+
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
|
82
|
+
float l_ac[64], p_ac[16], q_ac[16], a_ac[32];
|
|
83
|
+
int ac_index = 0;
|
|
84
|
+
FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, lx, ly, l_scale, l_ac);
|
|
85
|
+
FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, 3, 3, p_scale * 1.25f, p_ac);
|
|
86
|
+
FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, 3, 3, q_scale * 1.25f, q_ac);
|
|
87
|
+
if (hasAlpha) {
|
|
88
|
+
FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, 5, 5, a_scale, a_ac);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Decode using the DCT into RGB
|
|
92
|
+
int ratio_lx = isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7;
|
|
93
|
+
int ratio_ly = isLandscape ? header16 & 7 : (hasAlpha ? 5 : 7);
|
|
94
|
+
float ratio = ratio_ly > 0 ? (float) ratio_lx / (float) ratio_ly : 1;
|
|
95
|
+
int w = MAX(1, (int) roundf(ratio > 1.0f ? 32.0f : 32.0f * ratio));
|
|
96
|
+
int h = MAX(1, (int) roundf(ratio > 1.0f ? 32.0f / ratio : 32.0f));
|
|
97
|
+
uint8_t* rgba = malloc((size_t) w * h * 4);
|
|
98
|
+
if (!rgba) {
|
|
99
|
+
return NULL;
|
|
100
|
+
}
|
|
101
|
+
int cx_stop = MAX(lx, hasAlpha ? 5 : 3);
|
|
102
|
+
int cy_stop = MAX(ly, hasAlpha ? 5 : 3);
|
|
103
|
+
float fx[8], fy[8];
|
|
104
|
+
for (int y = 0, i = 0; y < h; y++) {
|
|
105
|
+
for (int x = 0; x < w; x++, i += 4) {
|
|
106
|
+
float l = l_dc, p = p_dc, q = q_dc, a = a_dc;
|
|
107
|
+
|
|
108
|
+
// Precompute the coefficients
|
|
109
|
+
for (int cx = 0; cx < cx_stop; cx++)
|
|
110
|
+
fx[cx] = (float) cos(M_PI / w * (x + 0.5f) * cx);
|
|
111
|
+
for (int cy = 0; cy < cy_stop; cy++)
|
|
112
|
+
fy[cy] = (float) cos(M_PI / h * (y + 0.5f) * cy);
|
|
113
|
+
|
|
114
|
+
// Decode L
|
|
115
|
+
for (int cy = 0, j = 0; cy < ly; cy++) {
|
|
116
|
+
float fy2 = fy[cy] * 2.0f;
|
|
117
|
+
for (int cx = cy > 0 ? 0 : 1; cx * ly < lx * (ly - cy); cx++, j++)
|
|
118
|
+
l += l_ac[j] * fx[cx] * fy2;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Decode P and Q
|
|
122
|
+
for (int cy = 0, j = 0; cy < 3; cy++) {
|
|
123
|
+
float fy2 = fy[cy] * 2.0f;
|
|
124
|
+
for (int cx = cy > 0 ? 0 : 1; cx < 3 - cy; cx++, j++) {
|
|
125
|
+
float f = fx[cx] * fy2;
|
|
126
|
+
p += p_ac[j] * f;
|
|
127
|
+
q += q_ac[j] * f;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Decode A
|
|
132
|
+
if (hasAlpha)
|
|
133
|
+
for (int cy = 0, j = 0; cy < 5; cy++) {
|
|
134
|
+
float fy2 = fy[cy] * 2.0f;
|
|
135
|
+
for (int cx = cy > 0 ? 0 : 1; cx < 5 - cy; cx++, j++)
|
|
136
|
+
a += a_ac[j] * fx[cx] * fy2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Convert to RGB
|
|
140
|
+
float b = l - 2.0f / 3.0f * p;
|
|
141
|
+
float r = (3.0f * l - b + q) / 2.0f;
|
|
142
|
+
float g = r - q;
|
|
143
|
+
rgba[i] = FFFThumbHashChannel(r);
|
|
144
|
+
rgba[i + 1] = FFFThumbHashChannel(g);
|
|
145
|
+
rgba[i + 2] = FFFThumbHashChannel(b);
|
|
146
|
+
rgba[i + 3] = FFFThumbHashChannel(a);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
*width = w;
|
|
150
|
+
*height = h;
|
|
151
|
+
return rgba;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// A blurDataUri's image scaled so its longer side is 32 px (a ThumbHash's
|
|
155
|
+
// size) and blurred by about half of one of its pixels, so that a few pixels
|
|
156
|
+
// scale up smoothly instead of in steps: a box blur three times each way
|
|
157
|
+
// (about a Gaussian), on premultiplied colors, the edges extended outwards.
|
|
158
|
+
static UIImage* FFFBlurredPreview(UIImage* image) {
|
|
159
|
+
CGImageRef source = image.CGImage;
|
|
160
|
+
if (!source) {
|
|
161
|
+
return nil;
|
|
162
|
+
}
|
|
163
|
+
size_t width = CGImageGetWidth(source), height = CGImageGetHeight(source);
|
|
164
|
+
if (width == 0 || height == 0) {
|
|
165
|
+
return nil;
|
|
166
|
+
}
|
|
167
|
+
float scale = 32.0f / MAX(width, height);
|
|
168
|
+
int w = MAX(1, (int) lroundf(width * scale)), h = MAX(1, (int) lroundf(height * scale));
|
|
169
|
+
int radius = MAX(1, (int) lroundf(scale / 2));
|
|
170
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
171
|
+
CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, w * 4, colorSpace,
|
|
172
|
+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
|
173
|
+
CGColorSpaceRelease(colorSpace);
|
|
174
|
+
if (!context) {
|
|
175
|
+
return nil;
|
|
176
|
+
}
|
|
177
|
+
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
|
178
|
+
CGContextDrawImage(context, CGRectMake(0, 0, w, h), source);
|
|
179
|
+
FFFBoxBlur(context, 2 * radius + 1);
|
|
180
|
+
CGImageRef blurred = CGBitmapContextCreateImage(context);
|
|
181
|
+
CGContextRelease(context);
|
|
182
|
+
if (!blurred) {
|
|
183
|
+
return nil;
|
|
184
|
+
}
|
|
185
|
+
UIImage* result = [UIImage imageWithCGImage: blurred scale: 1 orientation: UIImageOrientationUp];
|
|
186
|
+
CGImageRelease(blurred);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@implementation FFFThumbHash
|
|
191
|
+
|
|
192
|
+
+ (NSCache<NSString*, UIImage*>*) cache {
|
|
193
|
+
static NSCache* cache;
|
|
194
|
+
static dispatch_once_t once;
|
|
195
|
+
dispatch_once(&once, ^{
|
|
196
|
+
cache = [NSCache new];
|
|
197
|
+
// Recent placeholders, at most 4 KB each.
|
|
198
|
+
cache.countLimit = 256;
|
|
199
|
+
});
|
|
200
|
+
return cache;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
+ (UIImage*) imageForPlaceholder: (NSString*)placeholder {
|
|
204
|
+
if ([placeholder rangeOfString: @"data:" options: NSCaseInsensitiveSearch | NSAnchoredSearch].location == NSNotFound) {
|
|
205
|
+
return [self imageForThumbHash: placeholder];
|
|
206
|
+
}
|
|
207
|
+
UIImage* cached = [[self cache] objectForKey: placeholder];
|
|
208
|
+
if (cached) {
|
|
209
|
+
return cached;
|
|
210
|
+
}
|
|
211
|
+
NSURL* url = [NSURL URLWithString: placeholder];
|
|
212
|
+
NSData* data = url ? [NSData dataWithContentsOfURL: url] : nil;
|
|
213
|
+
UIImage* decoded = data ? [UIImage imageWithData: data scale: 1] : nil;
|
|
214
|
+
UIImage* image = decoded ? FFFBlurredPreview(decoded) : nil;
|
|
215
|
+
if (image) {
|
|
216
|
+
[[self cache] setObject: image forKey: placeholder];
|
|
217
|
+
}
|
|
218
|
+
return image;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
+ (UIImage*) imageForThumbHash: (NSString*)thumbhash {
|
|
222
|
+
if (thumbhash.length == 0) {
|
|
223
|
+
return nil;
|
|
224
|
+
}
|
|
225
|
+
UIImage* cached = [[self cache] objectForKey: thumbhash];
|
|
226
|
+
if (cached) {
|
|
227
|
+
return cached;
|
|
228
|
+
}
|
|
229
|
+
NSMutableString* base64 = [[thumbhash stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
|
|
230
|
+
[base64 replaceOccurrencesOfString: @"-" withString: @"+" options: 0 range: NSMakeRange(0, base64.length)];
|
|
231
|
+
[base64 replaceOccurrencesOfString: @"_" withString: @"/" options: 0 range: NSMakeRange(0, base64.length)];
|
|
232
|
+
while (base64.length % 4 != 0) {
|
|
233
|
+
[base64 appendString: @"="];
|
|
234
|
+
}
|
|
235
|
+
NSData* hash = [[NSData alloc] initWithBase64EncodedString: base64 options: 0];
|
|
236
|
+
if (!hash) {
|
|
237
|
+
return nil;
|
|
238
|
+
}
|
|
239
|
+
int width = 0, height = 0;
|
|
240
|
+
uint8_t* rgba = FFFThumbHashToRGBA(hash.bytes, hash.length, &width, &height);
|
|
241
|
+
if (!rgba) {
|
|
242
|
+
return nil;
|
|
243
|
+
}
|
|
244
|
+
// Premultiplied, as Core Graphics draws it.
|
|
245
|
+
for (int i = 0; i < width * height * 4; i += 4) {
|
|
246
|
+
int alpha = rgba[i + 3];
|
|
247
|
+
rgba[i] = (uint8_t) ((rgba[i] * alpha + 127) / 255);
|
|
248
|
+
rgba[i + 1] = (uint8_t) ((rgba[i + 1] * alpha + 127) / 255);
|
|
249
|
+
rgba[i + 2] = (uint8_t) ((rgba[i + 2] * alpha + 127) / 255);
|
|
250
|
+
}
|
|
251
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
252
|
+
CGContextRef context = CGBitmapContextCreate(rgba, width, height, 8, width * 4, colorSpace,
|
|
253
|
+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
|
254
|
+
CGImageRef cgImage = context ? CGBitmapContextCreateImage(context) : NULL;
|
|
255
|
+
if (context) {
|
|
256
|
+
CGContextRelease(context);
|
|
257
|
+
}
|
|
258
|
+
CGColorSpaceRelease(colorSpace);
|
|
259
|
+
free(rgba);
|
|
260
|
+
if (!cgImage) {
|
|
261
|
+
return nil;
|
|
262
|
+
}
|
|
263
|
+
UIImage* image = [UIImage imageWithCGImage: cgImage scale: 1 orientation: UIImageOrientationUp];
|
|
264
|
+
CGImageRelease(cgImage);
|
|
265
|
+
[[self cache] setObject: image forKey: thumbhash];
|
|
266
|
+
return image;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@end
|
|
@@ -32,7 +32,13 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
|
|
|
32
32
|
// Several sources (2 or more) of the same image at different sizes: the view
|
|
33
33
|
// loads the one whose size is closest to its own.
|
|
34
34
|
@property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
|
|
35
|
-
|
|
35
|
+
// The `placeholder` prop.
|
|
36
|
+
@property (nonatomic, strong) UIImage *placeholder;
|
|
37
|
+
// The `sourcePlaceholders` prop, by the source's index (an empty string for
|
|
38
|
+
// none; one for a single source): the picked source's ThumbHash or
|
|
39
|
+
// blurDataUri (a data uri) shows instead of placeholder. JS sends them apart
|
|
40
|
+
// from the sources, so a new one for the same image doesn't load it again.
|
|
41
|
+
@property (nonatomic, copy) NSArray<NSString *> *sourcePlaceholders;
|
|
36
42
|
@property (nonatomic, strong) UIColor *imageColor;
|
|
37
43
|
// When it changes, the next image doesn't replace the current one: the view
|
|
38
44
|
// clears first (for views reused for other content, like list rows).
|