react-native-nitro-web-image 0.9.1 → 0.10.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 ADDED
@@ -0,0 +1,374 @@
1
+ <a href="https://margelo.com">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./img/banner-dark.png" />
4
+ <source media="(prefers-color-scheme: light)" srcset="./img/banner-light.png" />
5
+ <img alt="react-native-nitro-image" src="./img/banner-light.png" />
6
+ </picture>
7
+ </a>
8
+
9
+ <br />
10
+
11
+ **Nitro Image** is a superfast Image core type and view component for React Native, built with Nitro!
12
+
13
+ - Powered by [Nitro Modules](https://nitro.margelo.com) for highly efficient native bindings! 🔥
14
+ - Instance-based `Image` type with byte-buffer pixel data access 🔗
15
+ - Supports in-memory image operations like resizing and cropping without saving to file 📐
16
+ - Supports deferred `ImageLoader` types to optimize for displaying large lists of Images ⏳
17
+ - Fast Web Image loading and caching using [SDWebImage](https://github.com/SDWebImage/SDWebImage) (iOS) and [Coil](https://github.com/coil-kt/coil) (Android) 🌎
18
+ - [ThumbHash](https://github.com/evanw/thumbhash) support for elegant placeholders 🖼️
19
+
20
+ ```tsx
21
+ function App() {
22
+ return (
23
+ <NitroImage
24
+ image={{ filePath: '/tmp/image.jpg' }}
25
+ style={{ width: 400, height: 400 }}
26
+ />
27
+ )
28
+ }
29
+ ```
30
+
31
+ ## Installation
32
+
33
+ Install [react-native-nitro-image](https://www.npmjs.com/package/react-native-nitro-image) from npm:
34
+
35
+ ```sh
36
+ npm i react-native-nitro-image
37
+ npm i react-native-nitro-modules
38
+ cd ios && pod install
39
+ ```
40
+
41
+ > [!NOTE]
42
+ > Since NitroImage is built with [Nitro Views](https://nitro.margelo.com/docs/hybrid-views), it requires the [new architecture](https://reactnative.dev/architecture/landing-page) to be enabled.
43
+
44
+ ### Web Images
45
+
46
+ To keep NitroImage super lightweight, it does not ship a web image loader and caching system.
47
+ If you want to load images from the web, install [react-native-nitro-web-image](https://www.npmjs.com/package/react-native-nitro-web-image) as well:
48
+
49
+ ```sh
50
+ npm i react-native-nitro-web-image
51
+ cd ios && pod install
52
+ ```
53
+
54
+ Then, since [SDWebImage does not enable modular headers](https://github.com/SDWebImage/SDWebImage?tab=readme-ov-file#swift-and-static-framework) for static linkage, you need to enable those yourself **in your app's `Podfile`**:
55
+
56
+ ```rb
57
+ target '…' do
58
+ config = use_native_modules!
59
+
60
+ # Add this line:
61
+ pod 'SDWebImage', :modular_headers => true
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Creating `Image`s
67
+
68
+ The simplest way to load an Image is to use the exported `loadImage(…)` method:
69
+
70
+ ```ts
71
+ const webImage = await loadImage({ url: 'https://picsum.photos/seed/123/400' })
72
+ const fileImage = await loadImage({ filePath: 'file://my-image.jpg' })
73
+ const resourceImage = await loadImage({ resource: 'my-image.jpg' })
74
+ const symbolImage = await loadImage({ symbol: 'star' })
75
+ const requireImage = await loadImage(require('./my-image.jpg'))
76
+ ```
77
+
78
+ Under the hood, this uses the native methods from `Images` or `WebImages`:
79
+
80
+ ```ts
81
+ const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
82
+ const fileImage = await Images.loadFromFileAsync('file://my-image.jpg')
83
+ const resourceImage = Images.loadFromResources('my-resource.jpg')
84
+ const symbolImage = Images.loadFromSymbol('star')
85
+ ```
86
+
87
+ #### Creating a blank Image
88
+
89
+ Additionally, you can also create a new blank Image:
90
+
91
+ ```ts
92
+ const blank = Images.createBlankImage(100, 100, true)
93
+ ```
94
+
95
+ If you want to fill the blank image with a specific background color, pass the color in RGB:
96
+
97
+ ```ts
98
+ const blankRedImage = Images.createBlankImage(100, 100, true, { r: 1, g: 0, b: 0 })
99
+ ```
100
+
101
+ #### Load with Options
102
+
103
+ When loading from a remote URL, you can tweak options such as `priority`:
104
+
105
+ ```ts
106
+ const image1 = await WebImages.loadFromURLAsync(URL1, { priority: 'low' })
107
+ const image2 = await WebImages.loadFromURLAsync(URL2, { priority: 'high' })
108
+ ```
109
+
110
+ #### Preloading
111
+
112
+ If you know what Images are going to be rendered soon, you can pre-load them using the `preload(...)` API:
113
+
114
+ ```ts
115
+ WebImages.preload(profilePictureLargeUrl)
116
+ ```
117
+
118
+ #### `require(…)`
119
+
120
+ A React Native `require(…)` returns a resource-ID. In debug, resources are streamed over Metro (`localhost://…`), while in release, they are embedded in the resources bundle.
121
+ NitroImage wraps those APIs so you can just pass a `require(…)` to `useImage(…)`, `useImageLoader(…)`, or `<NitroImage />` directly:
122
+
123
+ ```ts
124
+ const image = useImage(require('./image.png'))
125
+ ```
126
+
127
+ #### `RawPixelData` (`ArrayBuffer`)
128
+
129
+ The `Image` type can be converted to- and from- an `ArrayBuffer`, which gives you access to the raw pixel data in an RGB format:
130
+
131
+ ```ts
132
+ const image = ...
133
+ const pixelData = await image.toRawPixelData()
134
+ const sameImageCopied = await Images.loadFromRawPixelData(pixelData)
135
+ ```
136
+
137
+ #### `EncodedImageData` (`ArrayBuffer`)
138
+
139
+ The `Image` type can be encoded to- and decoded from- an `ArrayBuffer` using a container format like `jpg`, `png` or `heic`:
140
+
141
+ ```ts
142
+ const image = ...
143
+ const imageData = await image.toEncodedImageData('jpg', 90)
144
+ const sameImageCopied = await Images.loadFromEncodedImageData(imageData)
145
+ ```
146
+
147
+ #### Resizing
148
+
149
+ An `Image` can be resized entirely in-memory, without ever writing to- or reading from- a file:
150
+
151
+ ```ts
152
+ const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
153
+ const smaller = await webImage.resizeAsync(200, 200)
154
+ ```
155
+
156
+ #### Cropping
157
+
158
+ An `Image` can be cropped entirely in-memory, without ever writing to- or reading from- a file:
159
+
160
+ ```ts
161
+ const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
162
+ const smaller = await webImage.cropAsync(100, 100, 50, 50)
163
+ ```
164
+
165
+ #### Rotating
166
+
167
+ An `Image` can be rotated entirely in-memory, without ever writing to- or reading from- a file:
168
+
169
+ ```ts
170
+ const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
171
+ const upsideDown = await webImage.rotateAsync(180)
172
+ ```
173
+
174
+ #### Render into another Image
175
+
176
+ An `Image` can be rendered into another `Image` entirely in-memory. This creates a third image (the result):
177
+
178
+ ```ts
179
+ const image1 = ...
180
+ const image2 = ...
181
+ const result = await image1.renderIntoAsync(image2, 10, 10, 80, 80)
182
+ ```
183
+
184
+ #### Saving
185
+
186
+ An in-memory `Image` object can also be written/saved to a file:
187
+
188
+ ```ts
189
+ const image = ...
190
+ const path = await image.saveToTemporaryFileAsync('jpg', 90)
191
+ ```
192
+
193
+ #### Compressing
194
+
195
+ Images can be compressed using the `jpg` container format - either in-memory or when writing to a file:
196
+
197
+ ```ts
198
+ const image = ...
199
+ const path = await image.saveToTemporaryFileAsync('jpg', 50) // 50% compression
200
+ const compressed = await image.toEncodedImageData('jpg', 50) // 50% compression
201
+ ```
202
+
203
+ #### HEIC/HEIF
204
+
205
+ NitroImage supports `HEIC`/`HEIF` format if the host OS natively supports it.
206
+
207
+ | | iOS | Android |
208
+ |--------------|----------------|----------------|
209
+ | Loading HEIC | ✅ | ✅ (>= SDK 28) |
210
+ | Writing HEIC | ✅ (>= iOS 17) | ❌ |
211
+
212
+ You can check whether your OS supports `HEIC` via NitroImage:
213
+
214
+ ```ts
215
+ import { supportsHeicWriting } from 'react-native-nitro-modules'
216
+
217
+ const image = ...
218
+ const format = supportsHeicWriting ? 'heic' : 'jpg'
219
+ const path = await image.saveToTemporaryFileAsync(format, 100)
220
+ ```
221
+
222
+ ### Hooks
223
+
224
+ #### The `useImage()` hook
225
+
226
+ The `useImage()` hook asynchronously loads an `Image` from the given source and returns it as a React state:
227
+
228
+ ```tsx
229
+ function App() {
230
+ const image = useImage({ filePath: '/tmp/image.jpg' })
231
+ return …
232
+ }
233
+ ```
234
+
235
+ #### The `useImageLoader()` hook
236
+
237
+ The `useImageLoader()` hook creates an asynchronous `ImageLoader` which can be passed to a `<NitroImage />` view to defer image loading:
238
+
239
+ ```tsx
240
+ function App() {
241
+ const loader = useImageLoader({ filePath: '/tmp/image.jpg' })
242
+ return (
243
+ <NitroImage
244
+ image={loader}
245
+ style={{ width: 400, height: 400 }}
246
+ />
247
+ )
248
+ }
249
+ ```
250
+
251
+ ### The `<NitroImage />` view
252
+
253
+ The `<NitroImage />` view is a React Native view that allows you to render `Image` - either asynchronously (by wrapping `ImageLoader`s), or synchronously (by passing `Image` instances directly):
254
+
255
+ ```tsx
256
+ function App() {
257
+ return (
258
+ <NitroImage
259
+ image={{ filePath: '/tmp/image.jpg' }}
260
+ style={{ width: 400, height: 400 }}
261
+ />
262
+ )
263
+ }
264
+ ```
265
+
266
+ ### The `<NativeNitroImage />` view
267
+
268
+ The `<NativeNitroImage />` view is the actual native Nitro View component for rendering an `Image` instance. It is recommended to use abstractions like [`<NitroImage />`](#the-nitroimage--view) instead of the actual native component. However if you need to use the native component instead, it is still exposed:
269
+
270
+ ```tsx
271
+ function App() {
272
+ const image = …
273
+ return (
274
+ <NativeNitroImage
275
+ image={image}
276
+ style={{ width: 400, height: 400 }}
277
+ />
278
+ )
279
+ }
280
+ ```
281
+
282
+ #### Dynamic width or height
283
+
284
+ To achieve a dynamic width or height calculation, you can use the `image`'s dimensions:
285
+
286
+ ```tsx
287
+ function App() {
288
+ const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
289
+ const aspect = (image?.width ?? 1) / (image?.height ?? 1)
290
+ return (
291
+ <NitroImage
292
+ image={image}
293
+ style={{ width: '100%', aspectRatio: aspect }}
294
+ />
295
+ )
296
+ }
297
+ ```
298
+
299
+ This will now resize the `height` dimension to match the same aspect ratio as the `image` - in this case it will be 1:1 since the image is 400x400.
300
+
301
+ If the `image` is 400x200, the `height` of the view will be **half** of the `width` of the view, i.e. a 0.5 aspect ratio.
302
+
303
+ ### ThumbHash
304
+
305
+ A ThumbHash is a short binary (or base64 string) representation of a blurry image.
306
+ Since it is a very small buffer (or base64 string), it can be added to a payload (like a `user` object in your database) to immediately display an image placeholder while the actual image loads.
307
+
308
+ <details>
309
+ <summary>Usage Example</summary>
310
+
311
+
312
+ For example, your `users` database could have a `users.profile_picture_url` field which you use to asynchronously load the web Image, and a `users.profile_picture_thumbhash` field which contains the ThumbHash buffer (or base64 string) which you can display on-device immediately.
313
+
314
+ - `users`
315
+ - `users.profile_picture_url`: Load asynchronously
316
+ - `users.profile_picture_thumbhash`: Decode & Display immediately
317
+
318
+ 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.
319
+ </details>
320
+
321
+ #### ThumbHash (`ArrayBuffer`) <> Image
322
+
323
+ NitroImage supports conversion from- and to- [ThumbHash](https://github.com/evanw/thumbhash) representations out of the box.
324
+
325
+ For performance reasons, a ThumbHash is represented as an `ArrayBuffer`.
326
+
327
+ ```ts
328
+ const thumbHash = ...from server
329
+ const image = Images.loadFromThumbHash(thumbHash)
330
+ const thumbHashAgain = image.toThumbHash()
331
+ ```
332
+
333
+ ##### ThumbHash (`ArrayBuffer`) <> Base64 String
334
+
335
+ If your ThumbHash is a `string`, convert it to an `ArrayBuffer` first, since this is more efficient:
336
+
337
+ ```ts
338
+ const thumbHashBase64 = ...from server
339
+ const thumbHashArrayBuffer = thumbHashFromBase64String(thumbHashBase64)
340
+ const thumbHashBase64Again = thumbHashToBase64String(thumbHashArrayBuffer)
341
+ ```
342
+
343
+ ##### Async ThumbHash
344
+
345
+ Since ThumbHash decoding or encoding can be a slow process, you should consider using the async methods instead:
346
+
347
+ ```ts
348
+ const thumbHash = ...from server
349
+ const image = await Images.loadFromThumbHashAsync(thumbHash)
350
+ const thumbHashAgain = await image.toThumbHash()
351
+ ```
352
+
353
+ ## Using the native `Image` type in a third-party library
354
+
355
+ To use the native `Image` type in your library (e.g. in a Camera library), you need to follow these steps:
356
+
357
+ 1. Add the dependency on `react-native-nitro-image`
358
+ - JS: Add `react-native-nitro-image` to `peerDependencies` and `devDependencies`
359
+ - Android: Add `:react-native-nitro-image` to your `build.gradle`'s `dependencies`, and `react-native-nitro-image::NitroImage` to your CMake's dependencies (it's a prefab)
360
+ - iOS: Add `NitroImage` to your `*.podspec`'s dependencies
361
+ 2. In your Nitro specs (`*.nitro.ts`), just import `Image` from `'react-native-nitro-image'` and use it as a type
362
+ 3. In your native implementation, you can either;
363
+ - Implement `HybridImageSpec`, `HybridImageLoaderSpec` or `HybridImageViewSpec` with your custom implementation, e.g. to create a `Image` implementation that doesn't use `UIImage` but instead uses `CGImage`, or an `AVPhoto`
364
+ - Use the `HybridImageSpec`, `HybridImageLoaderSpec` or `HybridImageViewSpec` types. You can either use them abstract (with all the methods that are also exposed to JS), or by downcasting them to a specific type - all of them follow a protocol like `NativeImage`:
365
+ ```swift
366
+ class HybridCustom: HybridCustomSpec {
367
+ func doSomething(image: any HybridImageSpec) throws {
368
+ guard let image = image as? NativeImage else { return }
369
+ let uiImage = image.uiImage
370
+ // ...
371
+ }
372
+ }
373
+ ```
374
+ 4. Done! 🎉 Now you can benefit from a common, shared `Image` type - e.g. your Camera library can directly return an `Image` instance in `takePhoto()`, which can be instantly rendered using `<NitroImage />` - no more file I/O!
@@ -24,7 +24,7 @@
24
24
  HybridObjectRegistry::registerHybridObjectConstructor(
25
25
  "WebImageFactory",
26
26
  []() -> std::shared_ptr<HybridObject> {
27
- std::shared_ptr<HybridWebImageFactorySpec> hybridObject = NitroWebImage::NitroWebImageAutolinking::createWebImageFactory();
27
+ std::shared_ptr<HybridWebImageFactorySpec> hybridObject = NitroWebImage::NitroWebImageAutolinking::WebImageFactory::create();
28
28
  return hybridObject;
29
29
  }
30
30
  );
@@ -5,21 +5,40 @@
5
5
  /// Copyright © Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
+ import NitroModules
9
+
10
+ // TODO: Use empty enums once Swift supports exporting them as namespaces
11
+ // See: https://github.com/swiftlang/swift/pull/83616
8
12
  public final class NitroWebImageAutolinking {
9
13
  public typealias bridge = margelo.nitro.web.image.bridge.swift
10
14
 
11
- /**
12
- * Creates an instance of a Swift class that implements `HybridWebImageFactorySpec`,
13
- * and wraps it in a Swift class that can directly interop with C++ (`HybridWebImageFactorySpec_cxx`)
14
- *
15
- * This is generated by Nitrogen and will initialize the class specified
16
- * in the `"autolinking"` property of `nitro.json` (in this case, `HybridWebImageFactory`).
17
- */
18
- public static func createWebImageFactory() -> bridge.std__shared_ptr_HybridWebImageFactorySpec_ {
19
- let hybridObject = HybridWebImageFactory()
20
- return { () -> bridge.std__shared_ptr_HybridWebImageFactorySpec_ in
21
- let __cxxWrapped = hybridObject.getCxxWrapper()
22
- return __cxxWrapped.getCxxPart()
23
- }()
15
+ private protocol AutolinkedClass {
16
+ associatedtype T
17
+ /**
18
+ * Creates an instance of the Swift class that implements the HybridObject's spec,
19
+ * and wraps it in a Swift class that can directly interop with C++.
20
+ *
21
+ * This is generated by Nitrogen and will initialize the class specified
22
+ * in the `"autolinking"` property of `nitro.json`.
23
+ */
24
+ static func create() -> T
25
+ /**
26
+ * Returns whether this concrete implementation is also
27
+ * conforming to the `RecyclableView` protocol, or not.
28
+ */
29
+ static var isRecyclableHybridView: Bool { get }
30
+ }
31
+
32
+ public final class WebImageFactory: AutolinkedClass {
33
+ public static func create() -> bridge.std__shared_ptr_HybridWebImageFactorySpec_ {
34
+ let hybridObject = HybridWebImageFactory()
35
+ return { () -> bridge.std__shared_ptr_HybridWebImageFactorySpec_ in
36
+ let __cxxWrapped = hybridObject.getCxxWrapper()
37
+ return __cxxWrapped.getCxxPart()
38
+ }()
39
+ }
40
+ public static var isRecyclableHybridView: Bool {
41
+ return HybridWebImageFactory.self is any RecyclableView.Type
42
+ }
24
43
  }
25
44
  }
@@ -33,14 +33,14 @@ open class HybridWebImageFactorySpec_base {
33
33
  public init() { }
34
34
  public func getCxxWrapper() -> HybridWebImageFactorySpec_cxx {
35
35
  #if DEBUG
36
- guard self is HybridWebImageFactorySpec else {
36
+ guard self is any HybridWebImageFactorySpec else {
37
37
  fatalError("`self` is not a `HybridWebImageFactorySpec`! Did you accidentally inherit from `HybridWebImageFactorySpec_base` instead of `HybridWebImageFactorySpec`?")
38
38
  }
39
39
  #endif
40
40
  if let cxxWrapper = self.cxxWrapper {
41
41
  return cxxWrapper
42
42
  } else {
43
- let cxxWrapper = HybridWebImageFactorySpec_cxx(self as! HybridWebImageFactorySpec)
43
+ let cxxWrapper = HybridWebImageFactorySpec_cxx(self as! any HybridWebImageFactorySpec)
44
44
  self.cxxWrapper = cxxWrapper
45
45
  return cxxWrapper
46
46
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-nitro-web-image",
3
- "version": "0.9.1",
4
- "description": "react-native-nitro-web-image",
3
+ "version": "0.10.0",
4
+ "description": "A superfast in-memory Image type and view component for React Native Web, built with Nitro!",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
7
7
  "types": "lib/typescript/index.d.ts",
@@ -46,24 +46,25 @@
46
46
  ],
47
47
  "repository": {
48
48
  "type": "git",
49
- "url": "git+https://github.com/mrousavy/react-native-nitro-web-image.git"
49
+ "url": "git+https://github.com/mrousavy/react-native-nitro-image.git",
50
+ "directory": "packages/react-native-nitro-web-image"
50
51
  },
51
52
  "author": "Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)",
52
53
  "license": "MIT",
53
54
  "bugs": {
54
- "url": "https://github.com/mrousavy/react-native-nitro-web-image/issues"
55
+ "url": "https://github.com/mrousavy/react-native-nitro-image/issues"
55
56
  },
56
- "homepage": "https://github.com/mrousavy/react-native-nitro-web-image#readme",
57
+ "homepage": "https://github.com/mrousavy/react-native-nitro-image#readme",
57
58
  "publishConfig": {
58
59
  "registry": "https://registry.npmjs.org/"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@biomejs/biome": "2.2.6",
62
63
  "@types/react": "^19.0.6",
63
- "nitrogen": "0.32.1",
64
+ "nitrogen": "0.33.0",
64
65
  "react": "19.1.0",
65
66
  "react-native": "0.81.0",
66
- "react-native-nitro-modules": "0.32.1",
67
+ "react-native-nitro-modules": "0.33.0",
67
68
  "react-native-nitro-image": "../react-native-nitro-image",
68
69
  "typescript": "5.8.3"
69
70
  },