react-native-fast-image 10.1.0 → 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.
Files changed (35) hide show
  1. package/README.md +84 -10
  2. package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
  3. package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
  4. package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
  5. package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
  6. package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
  7. package/dist/index.cjs.js +47 -9
  8. package/dist/index.cjs.js.flow +9 -0
  9. package/dist/index.cjs.web.js +49 -6
  10. package/dist/index.cjs.web.js.flow +9 -0
  11. package/dist/index.d.ts +56 -13
  12. package/dist/index.js +48 -10
  13. package/dist/index.js.flow +9 -0
  14. package/dist/index.web.js +49 -6
  15. package/dist/index.web.js.flow +9 -0
  16. package/dist/placeholders.d.ts +4 -0
  17. package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
  18. package/dist/thumbhash.cjs.js +241 -0
  19. package/dist/thumbhash.d.ts +12 -0
  20. package/dist/thumbhash.js +240 -0
  21. package/ios/FastImage/FFFBoxBlur.h +7 -0
  22. package/ios/FastImage/FFFBoxBlur.m +29 -0
  23. package/ios/FastImage/FFFThumbHash.h +24 -0
  24. package/ios/FastImage/FFFThumbHash.m +269 -0
  25. package/ios/FastImage/FFFastImageView.h +7 -1
  26. package/ios/FastImage/FFFastImageView.m +100 -46
  27. package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
  28. package/package.json +4 -2
  29. package/src/index.js.flow +9 -0
  30. package/src/index.tsx +142 -31
  31. package/src/index.web.tsx +119 -11
  32. package/src/placeholders.ts +45 -0
  33. package/src/specs/FastImageViewNativeComponent.ts +8 -2
  34. package/src/thumbhash.ts +363 -0
  35. 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 `defaultSource`) fades in, and a new source
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
- * An asset loaded with `require(...)`, shown while the first image loads,
307
- * and if an image fails to load. When `source` changes, the previous image
308
- * shows while the new one loads instead (see `source` and
309
- * `recyclingKey`).
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
- * On Android, `defaultSource` doesn't show in debug builds: there the dev
312
- * server serves `require()`d images, and `defaultSource` is only loaded
313
- * from the app's resources.
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. `defaultSource` is placed
357
- * the same way.
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
- * `defaultSource`, or blank) instead of staying until the new one has
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 `defaultSource`.
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
- const resolveDefaultSource = (defaultSource) => {
238
- if (!defaultSource) return null;
239
- const resolved = Image.resolveAssetSource(defaultSource);
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 sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
303
- const single = Array.isArray(source) ? source[0] : source;
304
- const resolvedSource = sources ? void 0 : Image.resolveAssetSource(single);
305
- const resolvedDefaultSource = resolveDefaultSource(defaultSource);
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
- defaultSource: resolvedDefaultSource,
352
+ sourcePlaceholders,
353
+ placeholder: resolvedPlaceholder,
316
354
  onFastImageLoadStart: onLoadStart,
317
355
  onFastImageProgress: onProgress,
318
356
  handledEvents: handledEvents({
@@ -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 defaultUri = resolveUri(defaultSource);
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 tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, "")}`;
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,
@@ -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
- defaultSource?: CodegenTypes.UnsafeMixed;
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;