Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- Supports in-memory image operations like resizing and cropping without saving to file 📐
- Supports deferred `ImageLoader` types to optimize for displaying large lists of Images ⏳
- Fast Web Image loading and caching using [SDWebImage](https://github.com/SDWebImage/SDWebImage) (iOS) and [Coil](https://github.com/coil-kt/coil) (Android) 🌎
- [ThumbHash](https://github.com/evanw/thumbhash) support for elegant placeholders 🖼️
- [ThumbHash](https://github.com/evanw/thumbhash) support for elegant placeholders (via the separate [`react-native-nitro-image-thumbhash`](./packages/react-native-nitro-image-thumbhash) package) 🖼️

```tsx
function App() {
Expand Down Expand Up @@ -346,36 +346,46 @@ Since it is a very small buffer (or base64 string), it can be added to a payload
Everytime you upload a new profile picture for the user, you should encode the image to a new ThumbHash again and update the `users.profile_picture_thumbhash` field. This should ideally happen on your backend, but can also be performed on-device if needed.
</details>

#### ThumbHash (`ArrayBuffer`) <> Image
ThumbHash support lives in a separate package, [`react-native-nitro-image-thumbhash`](./packages/react-native-nitro-image-thumbhash), so apps that don't need it don't pay for it.

```sh
bun add react-native-nitro-image-thumbhash
```

NitroImage supports conversion from- and to- [ThumbHash](https://github.com/evanw/thumbhash) representations out of the box.
#### ThumbHash (`ArrayBuffer`) <> Image

For performance reasons, a ThumbHash is represented as an `ArrayBuffer`.

```ts
import { ThumbHash } from 'react-native-nitro-image-thumbhash'

const thumbHash = ...from server
const image = Images.loadFromThumbHash(thumbHash)
const thumbHashAgain = image.toThumbHash()
const image = ThumbHash.decode(thumbHash)
const thumbHashAgain = ThumbHash.encode(image)
```

##### ThumbHash (`ArrayBuffer`) <> Base64 String

If your ThumbHash is a `string`, convert it to an `ArrayBuffer` first, since this is more efficient:

```ts
import { ThumbHash } from 'react-native-nitro-image-thumbhash'

const thumbHashBase64 = ...from server
const thumbHashArrayBuffer = thumbHashFromBase64String(thumbHashBase64)
const thumbHashBase64Again = thumbHashToBase64String(thumbHashArrayBuffer)
const thumbHashArrayBuffer = ThumbHash.fromBase64String(thumbHashBase64)
const thumbHashBase64Again = ThumbHash.toBase64String(thumbHashArrayBuffer)
```

##### Async ThumbHash

Since ThumbHash decoding or encoding can be a slow process, you should consider using the async methods instead:

```ts
import { ThumbHash } from 'react-native-nitro-image-thumbhash'

const thumbHash = ...from server
const image = await Images.loadFromThumbHashAsync(thumbHash)
const thumbHashAgain = await image.toThumbHash()
const image = await ThumbHash.decodeAsync(thumbHash)
const thumbHashAgain = await ThumbHash.encodeAsync(image)
```

## Using the native `Image` type in a third-party library
Expand Down
476 changes: 449 additions & 27 deletions bun.lock

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions example/__tests__/image-loaders.harness.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Platform } from 'react-native'
import { describe, expect, it } from 'react-native-harness'
import { Images, loadImage } from 'react-native-nitro-image'
import { ThumbHash } from 'react-native-nitro-image-thumbhash'
import { WebImages } from 'react-native-nitro-web-image'

const RED = { r: 1, g: 0, b: 0, a: 1 }
Expand Down Expand Up @@ -96,19 +97,19 @@ describe('Images.loadFromFile', () => {
})
})

describe('Images.loadFromThumbHash', () => {
describe('ThumbHash.decode', () => {
it('decodes a ThumbHash buffer back into an Image', () => {
const source = Images.createBlankImage(64, 64, true, BLUE).resize(32, 32)
const hash = source.toThumbHash()
const decoded = Images.loadFromThumbHash(hash)
const hash = ThumbHash.encode(source)
const decoded = ThumbHash.decode(hash)
expect(decoded.width).toBeGreaterThan(0)
expect(decoded.height).toBeGreaterThan(0)
})

it('async variant decodes the ThumbHash buffer', async () => {
const source = Images.createBlankImage(64, 64, true, GREEN).resize(32, 32)
const hash = await source.toThumbHashAsync()
const decoded = await Images.loadFromThumbHashAsync(hash)
const hash = await ThumbHash.encodeAsync(source)
const decoded = await ThumbHash.decodeAsync(hash)
expect(decoded.width).toBeGreaterThan(0)
expect(decoded.height).toBeGreaterThan(0)
})
Expand Down
15 changes: 7 additions & 8 deletions example/__tests__/image-utils.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import {
Images,
supportsHeicLoading,
supportsHeicWriting,
thumbHashFromBase64String,
thumbHashToBase64String,
} from 'react-native-nitro-image'
import { ThumbHash } from 'react-native-nitro-image-thumbhash'

const expectTemporaryHeicPath = (path: string) => {
expect(path.length).toBeGreaterThan(0)
Expand Down Expand Up @@ -58,7 +57,7 @@ describe('ImageUtils - HEIC round-trip', () => {
})
})

describe('ImageUtils - thumbHash round-trip', () => {
describe('ThumbHash - round-trip', () => {
it('encodes a small image to a thumbHash and converts to base64', () => {
const image = Images.createBlankImage(64, 64, true, {
r: 0.5,
Expand All @@ -67,14 +66,14 @@ describe('ImageUtils - thumbHash round-trip', () => {
a: 1,
})
const small = image.resize(32, 32)
const hash = small.toThumbHash()
const hash = ThumbHash.encode(small)
expect(hash.byteLength).toBeGreaterThan(0)

const base64 = thumbHashToBase64String(hash)
const base64 = ThumbHash.toBase64String(hash)
expect(base64.length).toBeGreaterThan(0)
expect(base64).toMatch(/^[A-Za-z0-9+/=]+$/)

const restored = thumbHashFromBase64String(base64)
const restored = ThumbHash.fromBase64String(base64)
expect(restored.byteLength).toBe(hash.byteLength)
})

Expand All @@ -85,8 +84,8 @@ describe('ImageUtils - thumbHash round-trip', () => {
b: 0,
a: 1,
})
const hash = image.toThumbHash()
const decoded = Images.loadFromThumbHash(hash)
const hash = ThumbHash.encode(image)
const decoded = ThumbHash.decode(hash)
expect(decoded.width).toBeGreaterThan(0)
expect(decoded.height).toBeGreaterThan(0)
})
Expand Down
169 changes: 102 additions & 67 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ PODS:
- DoubleConversion (1.1.6)
- fast_float (8.0.0)
- FBLazyVector (0.81.0)
- fmt (11.0.2)
- fmt (12.1.0)
- glog (0.3.5)
- HarnessUI (1.1.0):
- boost
Expand Down Expand Up @@ -78,6 +78,37 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroImageThumbHash (0.0.1):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- NitroImage
- NitroModules
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-callinvoker
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- NitroModules (0.35.5):
- boost
- DoubleConversion
Expand Down Expand Up @@ -143,20 +174,20 @@ PODS:
- boost
- DoubleConversion
- fast_float (= 8.0.0)
- fmt (= 11.0.2)
- fmt (= 12.1.0)
- glog
- RCT-Folly/Default (= 2024.11.18.00)
- RCT-Folly/Default (2024.11.18.00):
- boost
- DoubleConversion
- fast_float (= 8.0.0)
- fmt (= 11.0.2)
- fmt (= 12.1.0)
- glog
- RCT-Folly/Fabric (2024.11.18.00):
- boost
- DoubleConversion
- fast_float (= 8.0.0)
- fmt (= 11.0.2)
- fmt (= 12.1.0)
- glog
- RCTDeprecation (0.81.0)
- RCTRequired (0.81.0)
Expand Down Expand Up @@ -2523,6 +2554,7 @@ DEPENDENCIES:
- "HarnessUI (from `../../node_modules/@react-native-harness/ui`)"
- hermes-engine (from `../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- NitroImage (from `../../node_modules/react-native-nitro-image`)
- NitroImageThumbHash (from `../../node_modules/react-native-nitro-image-thumbhash`)
- NitroModules (from `../../node_modules/react-native-nitro-modules`)
- NitroWebImage (from `../../node_modules/react-native-nitro-web-image`)
- RCT-Folly (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
Expand Down Expand Up @@ -2623,6 +2655,8 @@ EXTERNAL SOURCES:
:tag: hermes-2025-07-07-RNv0.81.0-e0fc67142ec0763c6b6153ca2bf96df815539782
NitroImage:
:path: "../../node_modules/react-native-nitro-image"
NitroImageThumbHash:
:path: "../../node_modules/react-native-nitro-image-thumbhash"
NitroModules:
:path: "../../node_modules/react-native-nitro-modules"
NitroWebImage:
Expand Down Expand Up @@ -2767,85 +2801,86 @@ SPEC CHECKSUMS:
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
FBLazyVector: a867936a67af0d09c37935a1b900a1a3c795b6d1
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
HarnessUI: 89b0197f0c33f741f001a9113d4979f17e8004bf
HarnessUI: 01740b858c62c55d42995d4ca459ead036b96c9a
hermes-engine: e7491a2038f2618c8cd444ed411a6deb350a3742
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
NitroImage: 358f7e81941d911eeb27ac37526731eb9672bc19
NitroModules: 6e73058747e2da022cb4fca5654f39eaf61e3c4f
NitroWebImage: 2e181d7eae11fad17d689eb054ab803f9d60a5e3
RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
NitroImage: cef8d9c91d6d9247ec0569bb68148f7fba717026
NitroImageThumbHash: 992d4d9529f1770664b62e79f53e111381653bca
NitroModules: ea5dc6c43666f4a75e71e372eaca6d3605856e51
NitroWebImage: 3b45bf7868d02a3ea2ba20c9c9f955288384e31a
RCT-Folly: b29feb752b08042c62badaef7d453f3bb5e6ae23
RCTDeprecation: 0735ab4f6b3ec93a7f98187b5da74d7916e2cf4c
RCTRequired: 8fcc7801bfc433072287b0f24a662e2816e89d0c
RCTTypeSafety: 2b2be515d6b968bcba7a68c4179d8199bd8c9b58
React: 1000c0e96d8fb9fbdaf13f7d31d0b09db3cbb4ac
React-callinvoker: 7e52661bfaf5d8881a9cee049792627a00001fbe
React-Core: a9128dd77ec52432727bfbec8c55d17189f6c039
React-CoreModules: 4597116bd78ae2b183547e3700be0dc9537918e9
React-cxxreact: e3a02f535cc1f1b547ac1baafe6ac25552352362
React-Core: 949b436ddfe76cf47ac96375152de2f3506a8421
React-CoreModules: 0f27580d0d82d430fa4f2cf4d970b6ad1120d63a
React-cxxreact: 48754f11f47a29ea4800cbdd694c10f874a26b9b
React-debug: 7a23d96f709f437c5e08973d6e06d0a54dd180a1
React-defaultsnativemodule: f01b6e58a23efe4fc8d74db7dadeea112908f5d5
React-domnativemodule: 2d9796d40ab675e0f91ae8aae26c796b6e9a7499
React-Fabric: f4344b3a882292783de9a5404852023b6c4fdd2d
React-FabricComponents: 7c51eb1619473ae3ed92d8bbf5d5dd3be0c5ef9d
React-FabricImage: 9e743575e67a9c14242bec3ae0e26663eed641bb
React-featureflags: 5188951cc2fc81f4d249dc37e8f96dca7ef50e96
React-featureflagsnativemodule: 0fa7473065377ca4e5651c75614796326ef57aa8
React-graphics: f65ecd0a8c70f9c7dcdae322851c19b21c83ec27
React-hermes: 8418dae38a0513aa66aaa0a1b0904e55c4448644
React-idlecallbacksnativemodule: 540d6f743fcb595b26da8b182b28c878a1176a96
React-ImageManager: 5f9f1e33611a852d21a63e1de76d211fb04ac935
React-jserrorhandler: 9c0a7d69cd07c9ae08fab3a61150d526c0174c83
React-jsi: b711b7a11d77357beb95fa2eabd30c1ae34dcf40
React-jsiexecutor: 0d1c78e666c5be71ff7c0ff5ea7fb043e5b1f14c
React-jsinspector: 5fabd9f0be9390d5b5eb5fc88a8965d97e0c14ac
React-jsinspectorcdp: e78c65e25253999c0efd5e23c99e649e02fd0244
React-jsinspectornetwork: b02c6f7fe00e12b575a7faea0ed9ec9ddbc1c20f
React-jsinspectortracing: c6d8da3c8bcd939b8dcfd5113e247d56af932e1b
React-jsitooling: 4ca9b158d65909590daf6bf30a345b663eb71964
React-jsitracing: d9e9378d5a3e05febea2164a5d0c5fab06492872
React-logger: 839abfd18a3fbdf88132824de584b226d0c5cbce
React-Mapbuffer: bd5b1120c9bbaac6203eb288735e239f04e03009
React-microtasksnativemodule: 10892b00e612d79436022a11e5bc8bdf468a284f
react-native-safe-area-context: 0f4986a88ec555aff660503b483d6e4bd6980a9a
React-NativeModulesApple: 3f9e97a4a90eeec1ceade511f973b277632650bb
React-defaultsnativemodule: 569d9222a701ed3dc60a60b2ce066b5bd88da059
React-domnativemodule: 34474bda3973bfd0ca2ea9f1b3db20db5d504cc7
React-Fabric: 45c3e9b112075451e592f0e008cabd4b82575355
React-FabricComponents: a428f23938c27a073baacc069d484b3478df85f3
React-FabricImage: 4375129ba8a26e8a7074af1c2468870fb8aab723
React-featureflags: ed973a134993f3be204d0b2d385d386603c9a0af
React-featureflagsnativemodule: aa3e1dc86bc185344d4875e7cb40cce0bd28de76
React-graphics: b5b8709a8216075bb6a5f9e7bb68881212d924ee
React-hermes: c543ffa2866304c582bdcb135c184e0f776f0d0b
React-idlecallbacksnativemodule: f19c4060b12fffc3ad33ce5de190338751b462ef
React-ImageManager: ecaf317aa5dff5eebba178b0813ef998c62547ea
React-jserrorhandler: 92eea1ee4f8c56b466b34e0065def59805e5d3a9
React-jsi: 7336786a4a14c473d104e6b37df935620d218fcd
React-jsiexecutor: 7c750f5b63fbc071d0f0e56e86f1a1589914f7b1
React-jsinspector: da5f336c1aa174a05885d061559a92e1d07b8a80
React-jsinspectorcdp: 0e807e4c2dc8ae8a07f0a6bfe50377f442079ba3
React-jsinspectornetwork: 3399384f2b6b70b287d8b9675452af4cec21dc65
React-jsinspectortracing: 030af0e9dca9a4eaa1d0ba258c7bd859fb90f61d
React-jsitooling: f8ed67814b17ebb124c48fccdf587ee1e02f16f4
React-jsitracing: 5cf6b84d46a4653895e30956a0ce3a315244c10a
React-logger: 04ce9229cb57db2c2a8164eaec1105f89da7fb22
React-Mapbuffer: e402e7a0535b2213c50727553621480fe8cd8ade
React-microtasksnativemodule: a63ce5595016996a9bac1f10c70a7a7fe6506649
react-native-safe-area-context: befb5404eb8a16fdc07fa2bebab3568ecabcbb8a
React-NativeModulesApple: b3766e1f87b08064ebc459b9e1538da2447ca874
React-oscompat: 34f3d3c06cadcbc470bc4509c717fb9b919eaa8b
React-perflogger: 95dff8cc9901777360716cbdcb2998849f133a4f
React-performancetimeline: 2937a27399b52ca8baf46f22c39087f617e626b5
React-perflogger: a1edb025fd5d44f61bf09307e248f7608d7b2dcf
React-performancetimeline: 1f86dc9782e3fe78727c5fbb3e2178b9fd1aa6fd
React-RCTActionSheet: 550c9c6c2e7dcd85a51954dc08e2f3837a148e7c
React-RCTAnimation: 0008bfe273566acd3128da13598073383325ac7a
React-RCTAppDelegate: 8b9452baef5548856a22f4710d4135cf68746cf5
React-RCTBlob: 60006ab743e5fd807aaf536092f5ce86e87df526
React-RCTFabric: 8d5d1006b3812c35fd0f37c117ff7bcf6449e20d
React-RCTFBReactNativeSpec: 3cb4265fa9a4e4f8250ae89feb345edc542731da
React-RCTImage: f40a2ee0f79c1666e8b81da4ea2d9d1182c94962
React-RCTLinking: cfe6995bdd8d08d0bb0df12771f4d28fd5fd54ff
React-RCTNetwork: 565c0cd46313f2cad0e4db70a44958b2842c372b
React-RCTRuntime: 971a71a42d8979475a380e5179083302e5506cdd
React-RCTSettings: afcec6060d916e9c0410004ad8419d45f9dbcd36
React-RCTText: 952f2a1b618d3f3872e7e5a82aefc5e5082c59aa
React-RCTVibration: 2a7e7497ffefa135c7f0fee8ee10e3505ab5cc61
React-RCTAnimation: 19d4bb6d2190983d1354b096b7b65dbd591924da
React-RCTAppDelegate: 6c71d16eef920831a312ff363355fc3b99c02a98
React-RCTBlob: b81a0cffe1a083bcf9d8aa9f27f4d37864579e90
React-RCTFabric: 01005d2fa799bba6e21aae18820498f56fe0be5f
React-RCTFBReactNativeSpec: 5adb84a81c4ed7a1f2661835d166e4b2c4320cd4
React-RCTImage: 607e5e373fb56d72417464bd82e8046af81ab502
React-RCTLinking: 301434c7bf1100458be5a3866326ba33491e3687
React-RCTNetwork: a118a47bd123ac96c9877e04f5731a1d6545aba5
React-RCTRuntime: 85fdbf469fe8a12c4db6c836731b190efc33d11d
React-RCTSettings: 5a5aa2cf9ac40f7a8897cc0f9d945ac803886604
React-RCTText: e6e00bee9847a8af1218079b73c8bfed16c75b8d
React-RCTVibration: 5a05fa0ef05ee73d074a3314e57586afc969f1ba
React-rendererconsistency: c2cb23365f4a7b511893748fe8cad1830bbae637
React-renderercss: 621b2b85af14694e93c2bcd63986fb57bcceab2e
React-rendererdebug: 4ba0769131e20347b900757fcac3c7919b27080c
React-RuntimeApple: c1a211351c14d35805d45a94094cfb3e5649552c
React-RuntimeCore: b7c7d8dffa3728a9e9616e0e8b5b6b41037ebcca
React-runtimeexecutor: e931e48afc888fe459f6ffb481971e23bb34f7ee
React-RuntimeHermes: 5763230801ee57d9f414818f48e44b874f3ce1be
React-runtimescheduler: b2e99f9702705fc8c11cf3c51f9911f478ee2210
React-renderercss: 0c1472d6572c05e493aee476598c3ed6234b6c33
React-rendererdebug: d6335da9730fa5a151537aa976a16d48de6135e2
React-RuntimeApple: 5684c2a5d8768e5728a5817c21e5dba798d54c58
React-RuntimeCore: 52428a1b48fb3c50ddf4dd5eee494486e4ecffc6
React-runtimeexecutor: 1b4e99e5c27d2cb8bdeca9773ff5f1a8eac7709c
React-RuntimeHermes: a688639233a3ea44b4f8e4d448f51943d7e00815
React-runtimescheduler: b833f0fc8c788329a497e93f55ce30508f56307a
React-timing: 25e8229ad1cf6874e9f0711515213cb2bc322215
React-utils: 7ea6e4d300c43a763e4e08091413aec962588f93
ReactAppDependencyProvider: 562d731311d0524a577cf8a01faa97874bacbdfe
ReactCodegen: 0fc801cfa34581b2acfb9568ef6180042043826a
ReactCommon: c235ebd26d63fde9a2dfa72cee9f8294b910fee1
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
RNScreens: 7179cc1ba31b4e18ed29f10abf20c24a7961cf4c
React-utils: 068cec677032ba78ca0700f2dcbe6d08a0939647
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
ReactCodegen: c3a2e945d68bcf8839624acaf1b276acbb41e9ba
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
RNScreens: 7f643ee0fd1407dc5085c7795460bd93da113b8f
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: b01392348aeea02064c21a2762a42893d82b60a7
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782

PODFILE CHECKSUM: 8c90c25c7a6bc16ec7b3ed7968df16467ab0fc35

COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"react-native-fast-image": "^8.6.3",
"react-native-harness": "^1.0.0-alpha.25",
"react-native-nitro-image": "workspace:*",
"react-native-nitro-image-thumbhash": "workspace:*",
"react-native-nitro-web-image": "workspace:*",
"react-native-nitro-modules": "0.35.5",
"react-native-safe-area-context": "^5.6.0",
Expand Down
Loading