react-native-fast-image 10.1.1 → 10.3.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 +107 -14
- 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 +11 -0
- package/dist/index.cjs.web.js +67 -14
- package/dist/index.cjs.web.js.flow +11 -0
- package/dist/index.d.ts +77 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +11 -0
- package/dist/index.web.js +68 -15
- package/dist/index.web.js.flow +11 -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 +11 -0
- package/src/index.tsx +166 -31
- package/src/index.web.tsx +166 -33
- 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,241 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/thumbhash.ts
|
|
3
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4
|
+
const CACHE_SIZE = 256;
|
|
5
|
+
/**
|
|
6
|
+
* Makes a source's `blurDataUri` from a ThumbHash, e.g. on your server, so
|
|
7
|
+
* the page shows the preview without any JavaScript. Undefined for an invalid
|
|
8
|
+
* hash. Works in browsers, Node and React Native.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```js
|
|
12
|
+
* // e.g. on your server, so the page shows the preview without any JavaScript
|
|
13
|
+
* const source = { uri: photo.url, blurDataUri: thumbHashToBlurDataUri(photo.thumbhash) }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
function thumbHashToBlurDataUri(thumbhash) {
|
|
17
|
+
if (!thumbhash) return void 0;
|
|
18
|
+
if (cache.has(thumbhash)) {
|
|
19
|
+
const uri = cache.get(thumbhash);
|
|
20
|
+
cache.delete(thumbhash);
|
|
21
|
+
cache.set(thumbhash, uri);
|
|
22
|
+
return uri;
|
|
23
|
+
}
|
|
24
|
+
const hash = base64Bytes(thumbhash);
|
|
25
|
+
const image = hash ? thumbHashToRGBA(hash) : void 0;
|
|
26
|
+
const small = image ? shrink(image.w, image.h, image.rgba, 8) : void 0;
|
|
27
|
+
const uri = small ? rgbaToDataURL(small.w, small.h, small.rgba) : void 0;
|
|
28
|
+
if (cache.size >= CACHE_SIZE) cache.delete(cache.keys().next().value);
|
|
29
|
+
cache.set(thumbhash, uri);
|
|
30
|
+
return uri;
|
|
31
|
+
}
|
|
32
|
+
globalThis[Symbol.for("react-native-fast-image/thumbhash")] = thumbHashToBlurDataUri;
|
|
33
|
+
function shrink(w, h, rgba, max) {
|
|
34
|
+
const scale = Math.max(w, h) / max;
|
|
35
|
+
if (scale <= 1) return {
|
|
36
|
+
w,
|
|
37
|
+
h,
|
|
38
|
+
rgba
|
|
39
|
+
};
|
|
40
|
+
const sw = Math.max(1, Math.round(w / scale));
|
|
41
|
+
const sh = Math.max(1, Math.round(h / scale));
|
|
42
|
+
const out = new Uint8Array(sw * sh * 4);
|
|
43
|
+
for (let y = 0; y < sh; y++) {
|
|
44
|
+
const y0 = Math.floor(y * h / sh);
|
|
45
|
+
const y1 = Math.floor((y + 1) * h / sh);
|
|
46
|
+
for (let x = 0; x < sw; x++) {
|
|
47
|
+
const x0 = Math.floor(x * w / sw);
|
|
48
|
+
const x1 = Math.floor((x + 1) * w / sw);
|
|
49
|
+
for (let c = 0; c < 4; c++) {
|
|
50
|
+
let sum = 0;
|
|
51
|
+
for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) sum += rgba[(yy * w + xx) * 4 + c];
|
|
52
|
+
out[(y * sw + x) * 4 + c] = Math.round(sum / ((y1 - y0) * (x1 - x0)));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
w: sw,
|
|
58
|
+
h: sh,
|
|
59
|
+
rgba: out
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function base64Bytes(base64) {
|
|
63
|
+
const standard = base64.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
64
|
+
if (!/^[A-Za-z0-9+/]*=*$/.test(standard)) return void 0;
|
|
65
|
+
try {
|
|
66
|
+
const binary = atob(standard.replace(/=+$/, "") + "=".repeat((4 - standard.replace(/=+$/, "").length % 4) % 4));
|
|
67
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function count(nx, ny) {
|
|
73
|
+
let n = 0;
|
|
74
|
+
for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) n++;
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
function thumbHashToRGBA(hash) {
|
|
78
|
+
const { PI, min, max, cos, round } = Math;
|
|
79
|
+
if (hash.length < 5) return void 0;
|
|
80
|
+
const header24 = hash[0] | hash[1] << 8 | hash[2] << 16;
|
|
81
|
+
const header16 = hash[3] | hash[4] << 8;
|
|
82
|
+
const l_dc = (header24 & 63) / 63;
|
|
83
|
+
const p_dc = (header24 >> 6 & 63) / 31.5 - 1;
|
|
84
|
+
const q_dc = (header24 >> 12 & 63) / 31.5 - 1;
|
|
85
|
+
const l_scale = (header24 >> 18 & 31) / 31;
|
|
86
|
+
const hasAlpha = header24 >> 23;
|
|
87
|
+
const p_scale = (header16 >> 3 & 63) / 63;
|
|
88
|
+
const q_scale = (header16 >> 9 & 63) / 63;
|
|
89
|
+
const isLandscape = header16 >> 15;
|
|
90
|
+
const lx = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
|
|
91
|
+
const ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
|
|
92
|
+
const ac_start = hasAlpha ? 6 : 5;
|
|
93
|
+
const ac_count = count(lx, ly) + 2 * count(3, 3) + (hasAlpha ? count(5, 5) : 0);
|
|
94
|
+
if (hash.length < ac_start + Math.ceil(ac_count / 2)) return void 0;
|
|
95
|
+
const a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1;
|
|
96
|
+
const a_scale = (hash[5] >> 4) / 15;
|
|
97
|
+
let ac_index = 0;
|
|
98
|
+
const decodeChannel = (nx, ny, scale) => {
|
|
99
|
+
const ac = [];
|
|
100
|
+
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);
|
|
101
|
+
return ac;
|
|
102
|
+
};
|
|
103
|
+
const l_ac = decodeChannel(lx, ly, l_scale);
|
|
104
|
+
const p_ac = decodeChannel(3, 3, p_scale * 1.25);
|
|
105
|
+
const q_ac = decodeChannel(3, 3, q_scale * 1.25);
|
|
106
|
+
const a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : [];
|
|
107
|
+
const ratio = thumbHashToApproximateAspectRatio(hash);
|
|
108
|
+
const w = max(1, round(ratio > 1 ? 32 : 32 * ratio));
|
|
109
|
+
const h = max(1, round(ratio > 1 ? 32 / ratio : 32));
|
|
110
|
+
const rgba = new Uint8Array(w * h * 4);
|
|
111
|
+
const fx = [];
|
|
112
|
+
const fy = [];
|
|
113
|
+
for (let y = 0, i = 0; y < h; y++) for (let x = 0; x < w; x++, i += 4) {
|
|
114
|
+
let l = l_dc, p = p_dc, q = q_dc, a = a_dc;
|
|
115
|
+
for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) fx[cx] = cos(PI / w * (x + .5) * cx);
|
|
116
|
+
for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) fy[cy] = cos(PI / h * (y + .5) * cy);
|
|
117
|
+
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;
|
|
118
|
+
for (let cy = 0, j = 0; cy < 3; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) {
|
|
119
|
+
const f = fx[cx] * fy2;
|
|
120
|
+
p += p_ac[j] * f;
|
|
121
|
+
q += q_ac[j] * f;
|
|
122
|
+
}
|
|
123
|
+
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;
|
|
124
|
+
const b = l - 2 / 3 * p;
|
|
125
|
+
const r = (3 * l - b + q) / 2;
|
|
126
|
+
const g = r - q;
|
|
127
|
+
rgba[i] = max(0, 255 * min(1, r));
|
|
128
|
+
rgba[i + 1] = max(0, 255 * min(1, g));
|
|
129
|
+
rgba[i + 2] = max(0, 255 * min(1, b));
|
|
130
|
+
rgba[i + 3] = max(0, 255 * min(1, a));
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
w,
|
|
134
|
+
h,
|
|
135
|
+
rgba
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function thumbHashToApproximateAspectRatio(hash) {
|
|
139
|
+
const header = hash[3];
|
|
140
|
+
const hasAlpha = hash[2] & 128;
|
|
141
|
+
const isLandscape = hash[4] & 128;
|
|
142
|
+
const lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7;
|
|
143
|
+
const ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7;
|
|
144
|
+
return ly ? lx / ly : 1;
|
|
145
|
+
}
|
|
146
|
+
function rgbaToDataURL(w, h, rgba) {
|
|
147
|
+
const row = w * 4 + 1;
|
|
148
|
+
const idat = 6 + h * (5 + row);
|
|
149
|
+
const bytes = [
|
|
150
|
+
137,
|
|
151
|
+
80,
|
|
152
|
+
78,
|
|
153
|
+
71,
|
|
154
|
+
13,
|
|
155
|
+
10,
|
|
156
|
+
26,
|
|
157
|
+
10,
|
|
158
|
+
0,
|
|
159
|
+
0,
|
|
160
|
+
0,
|
|
161
|
+
13,
|
|
162
|
+
73,
|
|
163
|
+
72,
|
|
164
|
+
68,
|
|
165
|
+
82,
|
|
166
|
+
0,
|
|
167
|
+
0,
|
|
168
|
+
w >> 8,
|
|
169
|
+
w & 255,
|
|
170
|
+
0,
|
|
171
|
+
0,
|
|
172
|
+
h >> 8,
|
|
173
|
+
h & 255,
|
|
174
|
+
8,
|
|
175
|
+
6,
|
|
176
|
+
0,
|
|
177
|
+
0,
|
|
178
|
+
0,
|
|
179
|
+
0,
|
|
180
|
+
0,
|
|
181
|
+
0,
|
|
182
|
+
0,
|
|
183
|
+
idat >>> 24,
|
|
184
|
+
idat >> 16 & 255,
|
|
185
|
+
idat >> 8 & 255,
|
|
186
|
+
idat & 255,
|
|
187
|
+
73,
|
|
188
|
+
68,
|
|
189
|
+
65,
|
|
190
|
+
84,
|
|
191
|
+
120,
|
|
192
|
+
1
|
|
193
|
+
];
|
|
194
|
+
const table = [
|
|
195
|
+
0,
|
|
196
|
+
498536548,
|
|
197
|
+
997073096,
|
|
198
|
+
651767980,
|
|
199
|
+
1994146192,
|
|
200
|
+
1802195444,
|
|
201
|
+
1303535960,
|
|
202
|
+
1342533948,
|
|
203
|
+
-306674912,
|
|
204
|
+
-267414716,
|
|
205
|
+
-690576408,
|
|
206
|
+
-882789492,
|
|
207
|
+
-1687895376,
|
|
208
|
+
-2032938284,
|
|
209
|
+
-1609899400,
|
|
210
|
+
-1111625188
|
|
211
|
+
];
|
|
212
|
+
let a = 1, b = 0;
|
|
213
|
+
for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) {
|
|
214
|
+
bytes.push(y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, row >> 8 ^ 255, 0);
|
|
215
|
+
for (b = (b + a) % 65521; i < end; i++) {
|
|
216
|
+
const u = rgba[i] & 255;
|
|
217
|
+
bytes.push(u);
|
|
218
|
+
a = (a + u) % 65521;
|
|
219
|
+
b = (b + a) % 65521;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
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);
|
|
223
|
+
for (let [start, end] of [[12, 29], [37, 41 + idat]]) {
|
|
224
|
+
let c = -1;
|
|
225
|
+
for (let i = start; i < end; i++) {
|
|
226
|
+
c ^= bytes[i];
|
|
227
|
+
c = c >>> 4 ^ table[c & 15];
|
|
228
|
+
c = c >>> 4 ^ table[c & 15];
|
|
229
|
+
}
|
|
230
|
+
c = ~c;
|
|
231
|
+
bytes[end++] = c >>> 24;
|
|
232
|
+
bytes[end++] = c >> 16 & 255;
|
|
233
|
+
bytes[end++] = c >> 8 & 255;
|
|
234
|
+
bytes[end++] = c & 255;
|
|
235
|
+
}
|
|
236
|
+
let binary = "";
|
|
237
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
238
|
+
return "data:image/png;base64," + btoa(binary);
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
exports.thumbHashToBlurDataUri = thumbHashToBlurDataUri;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes a source's `blurDataUri` from a ThumbHash, e.g. on your server, so
|
|
3
|
+
* the page shows the preview without any JavaScript. Undefined for an invalid
|
|
4
|
+
* hash. Works in browsers, Node and React Native.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```js
|
|
8
|
+
* // e.g. on your server, so the page shows the preview without any JavaScript
|
|
9
|
+
* const source = { uri: photo.url, blurDataUri: thumbHashToBlurDataUri(photo.thumbhash) }
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function thumbHashToBlurDataUri(thumbhash: string | undefined): string | undefined;
|
|
@@ -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
|