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
package/dist/index.d.ts
CHANGED
|
@@ -111,6 +111,32 @@ export type Source = {
|
|
|
111
111
|
* @default true
|
|
112
112
|
*/
|
|
113
113
|
diskCache?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* A [ThumbHash](https://evanw.github.io/thumbhash/) of the image, shown
|
|
116
|
+
* blurred while the image loads, and if it fails to load. It's a short
|
|
117
|
+
* string you make when the image is uploaded (e.g. with the `thumbhash`
|
|
118
|
+
* package) and send with the image's url.
|
|
119
|
+
*
|
|
120
|
+
* On the web, it needs `import 'react-native-fast-image/thumbhash'` (see
|
|
121
|
+
* [ThumbHashes on the web](#thumbhashes-on-the-web)).
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```jsx
|
|
125
|
+
* <FastImage source={{ uri: photo.url, thumbhash: photo.thumbhash }} />
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
thumbhash?: string;
|
|
129
|
+
/**
|
|
130
|
+
* A tiny version of the image (e.g. 8 to 20 px) as a data uri, shown
|
|
131
|
+
* blurred while the image loads, and if it fails to load. On the web it
|
|
132
|
+
* shows without any JavaScript, e.g. in a server-rendered page.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```jsx
|
|
136
|
+
* <FastImage source={{ uri: photo.url, blurDataUri: photo.lqip }} />
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
blurDataUri?: string;
|
|
114
140
|
/**
|
|
115
141
|
* With several sources (`source` as an array), the image's width at this
|
|
116
142
|
* uri, in pixels (times `scale`, if given).
|
|
@@ -124,6 +150,8 @@ export type Source = {
|
|
|
124
150
|
/** With several sources, the scale `width` and `height` are multiplied by. */
|
|
125
151
|
scale?: number;
|
|
126
152
|
};
|
|
153
|
+
/** A `thumbhash` or `blurDataUri` as `placeholder`. */
|
|
154
|
+
export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>;
|
|
127
155
|
/** How the image fades in: see [`transition`](#transition). */
|
|
128
156
|
export type Transition = {
|
|
129
157
|
/**
|
|
@@ -135,7 +163,7 @@ export type Transition = {
|
|
|
135
163
|
/**
|
|
136
164
|
* Also fades between images: a new `source` replacing an image that's
|
|
137
165
|
* showing cross-dissolves from it. Otherwise only an image that appears
|
|
138
|
-
* over nothing (or over
|
|
166
|
+
* over nothing (or over a placeholder) fades in, and a new source
|
|
139
167
|
* replaces the image showing at once, once it has loaded.
|
|
140
168
|
* @default false
|
|
141
169
|
*/
|
|
@@ -303,14 +331,29 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
303
331
|
*/
|
|
304
332
|
source?: Source | Source[] | ImageRequireSource;
|
|
305
333
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* `
|
|
334
|
+
* A placeholder shown while the image loads, and if it fails to load:
|
|
335
|
+
*
|
|
336
|
+
* - An image loaded with `require()`: `placeholder={require('./gray.png')}`.
|
|
337
|
+
* - A data uri: `placeholder="data:image/png;base64,…"`.
|
|
338
|
+
* - The name of an image bundled with the app (an iOS asset catalog image
|
|
339
|
+
* or an Android drawable): `placeholder="gray"`.
|
|
340
|
+
* - A [ThumbHash](#sourcethumbhash), shown blurred:
|
|
341
|
+
* `placeholder={{ thumbhash }}`.
|
|
342
|
+
* - A tiny image as a data uri, shown blurred:
|
|
343
|
+
* `placeholder={{ blurDataUri }}`.
|
|
310
344
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
345
|
+
* A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
|
|
346
|
+
* one.
|
|
347
|
+
*
|
|
348
|
+
* Notes:
|
|
349
|
+
*
|
|
350
|
+
* - A url isn't loaded: it's ignored, with a warning in development.
|
|
351
|
+
* - On Android, a `require()`d image doesn't show in debug builds, where
|
|
352
|
+
* the dev server serves it. Release builds show it.
|
|
353
|
+
*/
|
|
354
|
+
placeholder?: ImageRequireSource | string | SourcePlaceholder;
|
|
355
|
+
/**
|
|
356
|
+
* @deprecated Use `placeholder`, which does the same.
|
|
314
357
|
*/
|
|
315
358
|
defaultSource?: ImageRequireSource;
|
|
316
359
|
/**
|
|
@@ -353,8 +396,8 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
353
396
|
* `{ right: 10, bottom: '20%' }`. A missing axis is centered.
|
|
354
397
|
*
|
|
355
398
|
* Points are dp on Android. An invalid value is ignored (the image is
|
|
356
|
-
* centered), with a warning in development.
|
|
357
|
-
*
|
|
399
|
+
* centered), with a warning in development. Placeholders are placed the
|
|
400
|
+
* same way.
|
|
358
401
|
*
|
|
359
402
|
* @default '50% 50%'
|
|
360
403
|
* @example
|
|
@@ -367,7 +410,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
367
410
|
* For views that get reused for other content, such as rows in FlashList
|
|
368
411
|
* or recyclerlistview. Set it to something that identifies the content,
|
|
369
412
|
* e.g. the item's id. When it changes, the image is cleared right away (to
|
|
370
|
-
* `
|
|
413
|
+
* `placeholder`, or blank) instead of staying until the new one has
|
|
371
414
|
* loaded. Unlike changing `key`, this keeps the view, which is what list
|
|
372
415
|
* recycling is for.
|
|
373
416
|
*
|
|
@@ -477,7 +520,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
477
520
|
* blurring a photo behind a sheet). Each change blurs the image again on
|
|
478
521
|
* the CPU, so don't animate it.
|
|
479
522
|
*
|
|
480
|
-
* - Only the loaded image is blurred, not
|
|
523
|
+
* - Only the loaded image is blurred, not a placeholder.
|
|
481
524
|
* - An animated image (GIF, animated WebP, APNG or AVIF) shows its first frame,
|
|
482
525
|
* blurred, and doesn't animate.
|
|
483
526
|
* - With `tintColor`, the blurred image is tinted.
|
|
@@ -559,7 +602,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
559
602
|
style?: StyleProp<ImageStyle>;
|
|
560
603
|
/**
|
|
561
604
|
* If supplied, changes the color of all the non-transparent pixels to the
|
|
562
|
-
* given color.
|
|
605
|
+
* given color, including the placeholder.
|
|
563
606
|
*/
|
|
564
607
|
tintColor?: ColorValue;
|
|
565
608
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Children, forwardRef, memo } from "react";
|
|
1
|
+
import React, { Children, forwardRef, memo, useMemo } from "react";
|
|
2
2
|
import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeComponent } from "react-native";
|
|
3
3
|
//#region src/constants.ts
|
|
4
4
|
const resizeMode = {
|
|
@@ -233,10 +233,39 @@ var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView
|
|
|
233
233
|
//#region src/specs/NativeFastImageModule.ts
|
|
234
234
|
var NativeFastImageModule_default = TurboModuleRegistry.get("FastImageModule");
|
|
235
235
|
//#endregion
|
|
236
|
+
//#region src/placeholders.ts
|
|
237
|
+
function isImageName(value) {
|
|
238
|
+
return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
|
|
239
|
+
}
|
|
240
|
+
function hasSourcePlaceholder(value) {
|
|
241
|
+
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
242
|
+
}
|
|
243
|
+
function dataUri(value) {
|
|
244
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
245
|
+
if (/^data:/i.test(value)) return value;
|
|
246
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
236
249
|
//#region src/index.tsx
|
|
237
|
-
|
|
238
|
-
if (!
|
|
239
|
-
const
|
|
250
|
+
function withoutPlaceholders(source) {
|
|
251
|
+
if (!hasSourcePlaceholder(source)) return source;
|
|
252
|
+
const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
|
|
253
|
+
return rest;
|
|
254
|
+
}
|
|
255
|
+
function sourcePlaceholder(source) {
|
|
256
|
+
if (!source || typeof source !== "object") return "";
|
|
257
|
+
const { thumbhash, blurDataUri } = source;
|
|
258
|
+
return thumbhash || dataUri(blurDataUri) || "";
|
|
259
|
+
}
|
|
260
|
+
const resolvePlaceholder = (placeholder) => {
|
|
261
|
+
if (typeof placeholder === "string") {
|
|
262
|
+
if (isImageName(placeholder)) return Platform.OS === "android" ? placeholder : { uri: placeholder };
|
|
263
|
+
const uri = dataUri(placeholder);
|
|
264
|
+
if (!uri) return null;
|
|
265
|
+
return Platform.OS === "android" ? uri : { uri };
|
|
266
|
+
}
|
|
267
|
+
if (!placeholder) return null;
|
|
268
|
+
const resolved = Image.resolveAssetSource(placeholder);
|
|
240
269
|
if (!resolved) return null;
|
|
241
270
|
return Platform.OS === "android" ? resolved.uri : resolved;
|
|
242
271
|
};
|
|
@@ -291,7 +320,7 @@ function builtInTransition() {
|
|
|
291
320
|
return resolveTransition(Platform.OS === "android", NO_TRANSITION);
|
|
292
321
|
}
|
|
293
322
|
let defaultTransition;
|
|
294
|
-
function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
|
|
323
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
|
|
295
324
|
var _props$recyclingKey, _defaultTransition;
|
|
296
325
|
const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
|
|
297
326
|
if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
|
|
@@ -299,10 +328,18 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
299
328
|
const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
|
|
300
329
|
const fit = resolveObjectFit(objectFit, style);
|
|
301
330
|
const position = objectPositionOrCentered(objectPosition);
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
331
|
+
const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
|
|
332
|
+
const { resolvedSource, sources, sourcePlaceholders } = useMemo(() => {
|
|
333
|
+
const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
|
|
334
|
+
const single = Array.isArray(source) ? source[0] : source;
|
|
335
|
+
const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
|
|
336
|
+
return {
|
|
337
|
+
resolvedSource: sources ? void 0 : Image.resolveAssetSource(withoutPlaceholders(single)),
|
|
338
|
+
sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
|
|
339
|
+
sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
|
|
340
|
+
};
|
|
341
|
+
}, [source, shared]);
|
|
342
|
+
const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
306
343
|
return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
|
|
307
344
|
...props,
|
|
308
345
|
ref: forwardedRef,
|
|
@@ -312,7 +349,8 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
|
|
|
312
349
|
...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
|
|
313
350
|
source: resolvedSource,
|
|
314
351
|
sources,
|
|
315
|
-
|
|
352
|
+
sourcePlaceholders,
|
|
353
|
+
placeholder: resolvedPlaceholder,
|
|
316
354
|
onFastImageLoadStart: onLoadStart,
|
|
317
355
|
onFastImageProgress: onProgress,
|
|
318
356
|
handledEvents: handledEvents({
|
package/dist/index.js.flow
CHANGED
|
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
|
110
110
|
export type PreloadFn = (
|
|
111
111
|
sources: Array<FastImageSource>,
|
|
112
112
|
) => Promise<Array<PreloadResult>>
|
|
113
|
+
export type SourcePlaceholder = {
|
|
114
|
+
thumbhash?: string,
|
|
115
|
+
blurDataUri?: string,
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
export type FastImageSource = {
|
|
114
119
|
uri?: string,
|
|
115
120
|
headers?: Object,
|
|
@@ -118,6 +123,8 @@ export type FastImageSource = {
|
|
|
118
123
|
cacheKey?: string,
|
|
119
124
|
memoryCache?: boolean,
|
|
120
125
|
diskCache?: boolean,
|
|
126
|
+
thumbhash?: string,
|
|
127
|
+
blurDataUri?: string,
|
|
121
128
|
width?: number,
|
|
122
129
|
height?: number,
|
|
123
130
|
scale?: number,
|
|
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
132
139
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
133
140
|
|
|
134
141
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
+
placeholder?: ?(number | string | SourcePlaceholder),
|
|
143
|
+
// Deprecated: use placeholder, which is the same.
|
|
135
144
|
defaultSource?: ?number,
|
|
136
145
|
|
|
137
146
|
tintColor?: number | string,
|
package/dist/index.web.js
CHANGED
|
@@ -240,6 +240,16 @@ function cssPosition({ x, y }) {
|
|
|
240
240
|
return `${axis(x)} ${axis(y)}`;
|
|
241
241
|
}
|
|
242
242
|
//#endregion
|
|
243
|
+
//#region src/placeholders.ts
|
|
244
|
+
function hasSourcePlaceholder(value) {
|
|
245
|
+
return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
|
|
246
|
+
}
|
|
247
|
+
function dataUri(value) {
|
|
248
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
249
|
+
if (/^data:/i.test(value)) return value;
|
|
250
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
243
253
|
//#region src/index.web.tsx
|
|
244
254
|
var _useId;
|
|
245
255
|
const notSupported = {
|
|
@@ -250,7 +260,8 @@ let ids = 0;
|
|
|
250
260
|
const useCounterId = () => useState(() => `${++ids}`)[0];
|
|
251
261
|
const useSvgId = (_useId = React.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
|
|
252
262
|
const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
|
|
253
|
-
function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
263
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
264
|
+
var _resolveUri;
|
|
254
265
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
|
|
255
266
|
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
|
|
256
267
|
const label = ariaLabel || accessibilityLabel || void 0;
|
|
@@ -259,10 +270,14 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
|
|
|
259
270
|
const src = sized ? sized.src : resolveUri(source);
|
|
260
271
|
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
|
|
261
272
|
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
262
|
-
const
|
|
273
|
+
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
274
|
+
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
275
|
+
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
263
276
|
const [result, setResult] = useState();
|
|
264
277
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
265
|
-
const
|
|
278
|
+
const svgId = useSvgId().replace(/[^\w-]/g, "");
|
|
279
|
+
const tintId = `fast-image-tint-${svgId}`;
|
|
280
|
+
const blurId = `fast-image-blur-${svgId}`;
|
|
266
281
|
const fit = resolveObjectFit(objectFit, style);
|
|
267
282
|
const position = cssPosition(objectPositionOrCentered(objectPosition));
|
|
268
283
|
const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
|
|
@@ -280,9 +295,9 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
|
|
|
280
295
|
}, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
|
|
281
296
|
key: `default ${defaultUri}`,
|
|
282
297
|
src: defaultUri,
|
|
283
|
-
fit,
|
|
298
|
+
fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
|
|
284
299
|
position,
|
|
285
|
-
filter,
|
|
300
|
+
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
286
301
|
alt: key ? "" : label !== null && label !== void 0 ? label : ""
|
|
287
302
|
}) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
|
|
288
303
|
key: several ? "sizes" : key,
|
|
@@ -325,7 +340,21 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
|
|
|
325
340
|
}) : null, tintColor ? /* @__PURE__ */ React.createElement(TintFilter, {
|
|
326
341
|
id: tintId,
|
|
327
342
|
color: tintColor
|
|
328
|
-
}) : null);
|
|
343
|
+
}) : null, blurUri && showDefault ? /* @__PURE__ */ React.createElement(PreviewBlur, { id: blurId }) : null);
|
|
344
|
+
}
|
|
345
|
+
function thumbhashUri(hash) {
|
|
346
|
+
if (!hash) return void 0;
|
|
347
|
+
const decode = globalThis[Symbol.for("react-native-fast-image/thumbhash")];
|
|
348
|
+
if (decode) return decode(hash);
|
|
349
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.", "warn");
|
|
350
|
+
}
|
|
351
|
+
function shared(source, key, preview) {
|
|
352
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) => {
|
|
353
|
+
var _ref;
|
|
354
|
+
const own = entry && typeof entry === "object" ? entry : void 0;
|
|
355
|
+
return (_ref = (own === null || own === void 0 ? void 0 : own.thumbhash) || dataUri(own === null || own === void 0 ? void 0 : own.blurDataUri) ? own : preview) === null || _ref === void 0 ? void 0 : _ref[key];
|
|
356
|
+
});
|
|
357
|
+
return values.every((value) => value && value === values[0]) ? values[0] : void 0;
|
|
329
358
|
}
|
|
330
359
|
function resolveUri(source) {
|
|
331
360
|
let uri;
|
|
@@ -437,6 +466,20 @@ function TintFilter({ id, color }) {
|
|
|
437
466
|
operator: "in"
|
|
438
467
|
}))));
|
|
439
468
|
}
|
|
469
|
+
function PreviewBlur({ id }) {
|
|
470
|
+
return /* @__PURE__ */ React.createElement("svg", { style: tintStyle }, /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", {
|
|
471
|
+
id,
|
|
472
|
+
x: "0",
|
|
473
|
+
y: "0",
|
|
474
|
+
width: "1",
|
|
475
|
+
height: "1",
|
|
476
|
+
primitiveUnits: "objectBoundingBox",
|
|
477
|
+
colorInterpolationFilters: "sRGB"
|
|
478
|
+
}, /* @__PURE__ */ React.createElement("feGaussianBlur", { stdDeviation: "0.06" }), /* @__PURE__ */ React.createElement("feComponentTransfer", null, /* @__PURE__ */ React.createElement("feFuncA", {
|
|
479
|
+
type: "table",
|
|
480
|
+
tableValues: "0 1 1 1 1"
|
|
481
|
+
})))));
|
|
482
|
+
}
|
|
440
483
|
const tintStyle = {
|
|
441
484
|
position: "absolute",
|
|
442
485
|
width: 0,
|
package/dist/index.web.js.flow
CHANGED
|
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
|
110
110
|
export type PreloadFn = (
|
|
111
111
|
sources: Array<FastImageSource>,
|
|
112
112
|
) => Promise<Array<PreloadResult>>
|
|
113
|
+
export type SourcePlaceholder = {
|
|
114
|
+
thumbhash?: string,
|
|
115
|
+
blurDataUri?: string,
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
export type FastImageSource = {
|
|
114
119
|
uri?: string,
|
|
115
120
|
headers?: Object,
|
|
@@ -118,6 +123,8 @@ export type FastImageSource = {
|
|
|
118
123
|
cacheKey?: string,
|
|
119
124
|
memoryCache?: boolean,
|
|
120
125
|
diskCache?: boolean,
|
|
126
|
+
thumbhash?: string,
|
|
127
|
+
blurDataUri?: string,
|
|
121
128
|
width?: number,
|
|
122
129
|
height?: number,
|
|
123
130
|
scale?: number,
|
|
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
132
139
|
onProgress?: ?(event: OnProgressEvent) => void,
|
|
133
140
|
|
|
134
141
|
source?: ?(FastImageSource | Array<FastImageSource> | number),
|
|
142
|
+
placeholder?: ?(number | string | SourcePlaceholder),
|
|
143
|
+
// Deprecated: use placeholder, which is the same.
|
|
135
144
|
defaultSource?: ?number,
|
|
136
145
|
|
|
137
146
|
tintColor?: number | string,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SourcePlaceholder } from './index';
|
|
2
|
+
export declare function isImageName(value: string): boolean;
|
|
3
|
+
export declare function hasSourcePlaceholder(value: unknown): value is SourcePlaceholder;
|
|
4
|
+
export declare function dataUri(value: unknown): string | undefined;
|
|
@@ -21,7 +21,8 @@ type LoadEndEvent = Readonly<{
|
|
|
21
21
|
export interface NativeProps extends ViewProps {
|
|
22
22
|
source?: CodegenTypes.UnsafeMixed;
|
|
23
23
|
sources?: CodegenTypes.UnsafeMixed;
|
|
24
|
-
|
|
24
|
+
placeholder?: CodegenTypes.UnsafeMixed;
|
|
25
|
+
sourcePlaceholders?: ReadonlyArray<string>;
|
|
25
26
|
objectFit?: CodegenTypes.WithDefault<string, 'cover'>;
|
|
26
27
|
objectPositionX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
|
|
27
28
|
objectPositionXOffset?: CodegenTypes.Float;
|
|
@@ -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;
|