react-native-nitro-image-pipeline 1.4.0 → 1.6.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 +124 -8
- package/android/CMakeLists.txt +4 -0
- package/android/src/main/cpp/GaussianBlur.cpp +165 -0
- package/android/src/main/cpp/GaussianBlur.hpp +52 -0
- package/android/src/main/cpp/GaussianBlurJni.cpp +50 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +167 -75
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/PipelineImageLoader.kt +177 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt +36 -81
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/HardwareBitmapTransformation.kt +45 -0
- package/ios/GaussianBlur.swift +48 -9
- package/ios/HybridNitroImagePipeline.swift +109 -41
- package/ios/PipelineImageLoader.swift +195 -0
- package/ios/RoundedCornersProcessor.swift +5 -0
- package/lib/commonjs/NativePipelineImage.js +72 -0
- package/lib/commonjs/NativePipelineImage.js.map +1 -0
- package/lib/commonjs/PipelineImage.js.map +1 -1
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/resolveImageSource.js +89 -0
- package/lib/commonjs/resolveImageSource.js.map +1 -0
- package/lib/commonjs/useImage.js +8 -4
- package/lib/commonjs/useImage.js.map +1 -1
- package/lib/commonjs/usePipelineImageLoader.js +74 -0
- package/lib/commonjs/usePipelineImageLoader.js.map +1 -0
- package/lib/module/NativePipelineImage.js +68 -0
- package/lib/module/NativePipelineImage.js.map +1 -0
- package/lib/module/PipelineImage.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/resolveImageSource.js +83 -0
- package/lib/module/resolveImageSource.js.map +1 -0
- package/lib/module/useImage.js +8 -4
- package/lib/module/useImage.js.map +1 -1
- package/lib/module/usePipelineImageLoader.js +70 -0
- package/lib/module/usePipelineImageLoader.js.map +1 -0
- package/lib/typescript/src/NativePipelineImage.d.ts +71 -0
- package/lib/typescript/src/NativePipelineImage.d.ts.map +1 -0
- package/lib/typescript/src/PipelineImage.d.ts +7 -2
- package/lib/typescript/src/PipelineImage.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/resolveImageSource.d.ts +56 -0
- package/lib/typescript/src/resolveImageSource.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +56 -1
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
- package/lib/typescript/src/useImage.d.ts +10 -4
- package/lib/typescript/src/useImage.d.ts.map +1 -1
- package/lib/typescript/src/usePipelineImageLoader.d.ts +17 -0
- package/lib/typescript/src/usePipelineImageLoader.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JViewOptions.hpp +77 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipelineSpec.kt +5 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ViewOptions.kt +66 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.cpp +10 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +44 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +8 -0
- package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec_cxx.swift +15 -0
- package/nitrogen/generated/ios/swift/ViewOptions.swift +101 -0
- package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.hpp +7 -0
- package/nitrogen/generated/shared/c++/ViewOptions.hpp +104 -0
- package/package.json +4 -3
- package/src/NativePipelineImage.tsx +108 -0
- package/src/PipelineImage.tsx +7 -2
- package/src/index.ts +12 -0
- package/src/resolveImageSource.ts +83 -0
- package/src/specs/nitro-image-toolkit.nitro.ts +57 -1
- package/src/useImage.ts +26 -15
- package/src/usePipelineImageLoader.ts +90 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A high-performance image loading, caching, and processing library for React Nati
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- Load
|
|
11
|
+
- Load images from the network (with built-in memory and disk caching), the file system, or bundled `require()` assets
|
|
12
12
|
- Prefetch single or multiple images in the background
|
|
13
13
|
- Resize (aspect-fill, center-crop) and apply Gaussian blur and rounded corners (uniform or per-corner) at load time
|
|
14
14
|
- Apply Gaussian blur to already-loaded images
|
|
@@ -71,6 +71,38 @@ so a style that already rounds the view rounds the bitmap too, with no separate
|
|
|
71
71
|
other prop (`resizeMode`, `recyclingKey`, `testID`, …) is passed straight through to
|
|
72
72
|
`NativeNitroImage`.
|
|
73
73
|
|
|
74
|
+
### `<NativePipelineImage>` component
|
|
75
|
+
|
|
76
|
+
The fully native-driven variant of `<PipelineImage>`, for when per-image JS work matters (long,
|
|
77
|
+
fast-scrolling lists): after the first render there are **zero JS round trips per image**. The
|
|
78
|
+
native view starts the request the moment it attaches to the window — at its own laid-out size,
|
|
79
|
+
so nothing waits for an `onLayout` event to reach JS — and cancels it (releasing the bitmap) when
|
|
80
|
+
it detaches, which makes off-screen list cells free. Re-attaching hits the shared memory cache,
|
|
81
|
+
so recycled cells re-display instantly.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { NativePipelineImage } from 'react-native-nitro-image-pipeline';
|
|
85
|
+
|
|
86
|
+
<NativePipelineImage
|
|
87
|
+
url="https://example.com/photo.jpg"
|
|
88
|
+
style={styles.photo} // size measured natively; borderRadius baked into the bitmap
|
|
89
|
+
blur={2} // points, like PipelineImage
|
|
90
|
+
/>;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`blur`/`cornerRadius` are in points and `style`'s `borderRadius` is picked up automatically,
|
|
94
|
+
exactly like `<PipelineImage>`. The trade-offs of going fully native:
|
|
95
|
+
|
|
96
|
+
- No `onLoad`/`onError` — the loaded `Image` never crosses into JS. Use `<PipelineImage>` or
|
|
97
|
+
`useImage` when you need them.
|
|
98
|
+
- The bitmap is loaded once at the size the view first has; if the view resizes later, the bitmap
|
|
99
|
+
scales with it instead of reloading.
|
|
100
|
+
|
|
101
|
+
Under the hood this is `NitroImagePipeline.createImageLoader(url, options)` — an
|
|
102
|
+
[`ImageLoader`](https://github.com/mrousavy/react-native-nitro-image) driven by
|
|
103
|
+
`NativeNitroImage` — so you can also use the `usePipelineImageLoader(url, options)` hook directly
|
|
104
|
+
with your own `<NativeNitroImage image={loader} />`.
|
|
105
|
+
|
|
74
106
|
### Animating with `react-native-reanimated`
|
|
75
107
|
|
|
76
108
|
`<PipelineImage>` forwards its `ref` to the underlying `NativeNitroImage` host view, so it can be
|
|
@@ -145,6 +177,41 @@ function BlurFade({ url, blurred }: { url: string; blurred: boolean }) {
|
|
|
145
177
|
}
|
|
146
178
|
```
|
|
147
179
|
|
|
180
|
+
### Local images and `require()`
|
|
181
|
+
|
|
182
|
+
Every `url` in this library — `<PipelineImage>`, `<NativePipelineImage>`, `useImage`,
|
|
183
|
+
`usePipelineImageLoader` — also takes a `require()`d asset, and any `url` string may point at the
|
|
184
|
+
file system. The image goes through the same pipeline, so a bundled logo or a photo from the camera
|
|
185
|
+
roll gets the same resize-to-layout, blur and rounded corners as a download:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// A bundled asset — streamed from Metro in debug, read from the app bundle /
|
|
189
|
+
// resources in release, at the scale that matches the screen (like <Image>).
|
|
190
|
+
<PipelineImage url={require('./assets/logo.png')} style={styles.logo} blur={2} />
|
|
191
|
+
|
|
192
|
+
// A file on disk — a `file://` URL or a plain absolute path, e.g. the path
|
|
193
|
+
// react-native-nitro-image's `saveToTemporaryFileAsync` returns.
|
|
194
|
+
<NativePipelineImage url={`file://${photoPath}`} style={styles.thumb} />
|
|
195
|
+
<NativePipelineImage url={photoPath} style={styles.thumb} />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The direct `NitroImagePipeline.loadImage`/`createImageLoader` calls take a string; resolve a
|
|
199
|
+
`require()` first with `resolveImageUrl`:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { NitroImagePipeline, resolveImageUrl } from 'react-native-nitro-image-pipeline';
|
|
203
|
+
|
|
204
|
+
const logo = await NitroImagePipeline.loadImage(resolveImageUrl(require('./assets/logo.png')), {
|
|
205
|
+
resize: { width: 200, height: 200 },
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Accepted `url` forms: `http(s)://`, `file://`, a plain absolute path, `data:`, and on Android also
|
|
210
|
+
`content://` URIs and bare drawable resource names (what `require()` resolves to in a release build
|
|
211
|
+
there). Local sources are cached **in memory only** — there is nothing to gain from copying a file
|
|
212
|
+
that is already on disk into the disk cache — so `cache: 'disk'` on a local `url` means no caching,
|
|
213
|
+
and `preLoadImage(s)` treats local sources as a no-op.
|
|
214
|
+
|
|
148
215
|
### `useImage` hook
|
|
149
216
|
|
|
150
217
|
The simplest way to load an image in a component:
|
|
@@ -217,7 +284,10 @@ await NitroImagePipeline.clearCache();
|
|
|
217
284
|
|
|
218
285
|
### `loadImage(url, options?)`
|
|
219
286
|
|
|
220
|
-
Loads an image from a URL and returns a `Promise<Image>`.
|
|
287
|
+
Loads an image from a URL and returns a `Promise<Image>`. `url` is a string — `http(s)://`, `file://`,
|
|
288
|
+
a plain absolute path, or the other forms listed under
|
|
289
|
+
[Local images and `require()`](#local-images-and-require); pass a `require()` through
|
|
290
|
+
[`resolveImageUrl`](#resolveimageurlsource) first.
|
|
221
291
|
|
|
222
292
|
| Option | Type | Default | Description |
|
|
223
293
|
|---|---|---|---|
|
|
@@ -230,7 +300,7 @@ Loads an image from a URL and returns a `Promise<Image>`.
|
|
|
230
300
|
|
|
231
301
|
| Prop | Type | Default | Description |
|
|
232
302
|
|---|---|---|---|
|
|
233
|
-
| `url` | `string` | — | Image
|
|
303
|
+
| `url` | `string \| number` | — | Image to load: a URL string (`https://`, `file://`, an absolute path) or a `require()`d asset |
|
|
234
304
|
| `style` | `StyleProp<ViewStyle>` | — | Layout style; also determines the resize target (see [`resizeForStyle`](#resizeforstyle-style--resizeforlayoutwidth-height)) and, if `cornerRadius` is omitted, the corner radius (see [`cornerRadiusForStyle`](#cornerradiusforstylestyle)) |
|
|
235
305
|
| `blur` | `number` | `0` | Gaussian blur strength, in **points** (converted to bitmap pixels internally) |
|
|
236
306
|
| `cornerRadius` | `number \| CornerRadii` | derived from `style` | Corner radius, in **points** (converted to bitmap pixels internally). When omitted, derived from `style`'s `borderRadius`/`borderTopLeftRadius`/etc.; square if neither is set |
|
|
@@ -241,6 +311,41 @@ Loads an image from a URL and returns a `Promise<Image>`.
|
|
|
241
311
|
| `ref` | `Ref<PipelineImageRef>` | — | Forwarded to the underlying `NativeNitroImage` host view — gives access to native-view methods (`measure`, …) and makes the component work with `Animated.createAnimatedComponent` (see [Animating](#animating-with-react-native-reanimated)) |
|
|
242
312
|
| `…NativeNitroImage props` | — | — | Everything else (`resizeMode`, `recyclingKey`, `testID`, …) is passed through to `NativeNitroImage` |
|
|
243
313
|
|
|
314
|
+
### `<NativePipelineImage>`
|
|
315
|
+
|
|
316
|
+
| Prop | Type | Default | Description |
|
|
317
|
+
|---|---|---|---|
|
|
318
|
+
| `url` | `string \| number` | — | Image to load: a URL string (`https://`, `file://`, an absolute path) or a `require()`d asset |
|
|
319
|
+
| `style` | `StyleProp<ViewStyle>` | — | Layout style. The native side measures the view and loads at that size; `borderRadius`-family properties drive `cornerRadius` when it's omitted |
|
|
320
|
+
| `blur` | `number` | `0` | Gaussian blur strength, in **points** (screen scale applied natively) |
|
|
321
|
+
| `cornerRadius` | `number \| CornerRadii` | derived from `style` | Corner radius, in **points** (screen scale applied natively) |
|
|
322
|
+
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
323
|
+
| `resize` | `{ width, height }` | measured from the view | Explicit target bitmap size in **pixels**, skipping the native measurement. Rarely needed |
|
|
324
|
+
| `ref` | `Ref<NativePipelineImageRef>` | — | Forwarded to the underlying `NativeNitroImage` host view |
|
|
325
|
+
| `…NativeNitroImage props` | — | — | Everything else (`resizeMode`, `recyclingKey`, `testID`, …) is passed through; `recyclingKey` defaults to `url` |
|
|
326
|
+
|
|
327
|
+
No `onLoad`/`onError`: loading happens entirely natively and the result never crosses into JS.
|
|
328
|
+
|
|
329
|
+
### `createImageLoader(url, options?)` / `usePipelineImageLoader(source, options?)`
|
|
330
|
+
|
|
331
|
+
Creates the [`ImageLoader`](https://github.com/mrousavy/react-native-nitro-image) that powers
|
|
332
|
+
`<NativePipelineImage>`, for use with your own `<NativeNitroImage image={loader} />`. The view
|
|
333
|
+
calls into it natively when it attaches (load at the view's laid-out size) and detaches
|
|
334
|
+
(cancel + release). `options` takes `blur`/`cornerRadius` in **points** and an optional
|
|
335
|
+
pixel-based `resize` override — see the `ViewOptions` type. The hook memoizes by value, so
|
|
336
|
+
inline options literals are fine, and its `source` may be a `require()` as well as a URL string.
|
|
337
|
+
`loader.loadImage()` also works imperatively and resolves with the processed `Image`.
|
|
338
|
+
|
|
339
|
+
### `resolveImageUrl(source)`
|
|
340
|
+
|
|
341
|
+
Turns an `ImageSource` (`string | number`) into the URL string the native pipeline loads: strings
|
|
342
|
+
pass through unchanged, a `require()`d asset is resolved with `Image.resolveAssetSource` to the
|
|
343
|
+
scale-matched variant. The components and hooks do this internally; use it when calling
|
|
344
|
+
`loadImage`, `createImageLoader` or `preLoadImage(s)` directly with a `require()`. Throws if the
|
|
345
|
+
number is not a registered asset; the components and hooks never throw for one — `useImage` reports
|
|
346
|
+
it through `error`, and `usePipelineImageLoader`/`<NativePipelineImage>` load a URL no loader can
|
|
347
|
+
resolve, so the request fails at load time like a missing file.
|
|
348
|
+
|
|
244
349
|
### `resizeForStyle(style)` / `resizeForLayout(width, height)`
|
|
245
350
|
|
|
246
351
|
Converts a layout size in points to a bitmap `resize` option in pixels. Returns
|
|
@@ -260,6 +365,7 @@ omitted.
|
|
|
260
365
|
### `preLoadImage(url)`
|
|
261
366
|
|
|
262
367
|
Prefetches a single image into the **disk cache**, without decoding it. Returns `Promise<void>`.
|
|
368
|
+
Local sources (`file://`, paths, resources) have no download to cache and are a no-op.
|
|
263
369
|
|
|
264
370
|
Prefetching only pays the network and disk I/O cost up front — no bitmap is decoded or held in
|
|
265
371
|
memory, so prefetching a long list of URLs doesn't balloon RAM. The image is decoded (at the
|
|
@@ -300,11 +406,12 @@ Two things follow from the unit being *source* pixels:
|
|
|
300
406
|
Values below ~1 are smaller than the smallest kernel either backend can build and are effectively a
|
|
301
407
|
no-op. There is no upper bound.
|
|
302
408
|
|
|
303
|
-
Implementation:
|
|
304
|
-
(the standard three-box Gaussian approximation, accurate to a few percent)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
409
|
+
Implementation: both platforms run the same three box-convolution passes sized to hit the requested
|
|
410
|
+
sigma (the standard three-box Gaussian approximation, accurate to a few percent) — iOS through
|
|
411
|
+
Accelerate's `vImageBoxConvolve_ARGB8888`, Android through a C++ port of that kernel working directly
|
|
412
|
+
on the bitmap's pixels. The two are checked against each other on every CI run and produce
|
|
413
|
+
byte-identical output for the same input. Both clamp at the edges, so blurred images keep their
|
|
414
|
+
borders instead of fading out.
|
|
308
415
|
|
|
309
416
|
### `setMemoryCacheLimit(bytes)`
|
|
310
417
|
|
|
@@ -332,6 +439,15 @@ The pipeline is set up so RAM scales with what you display, not with what you do
|
|
|
332
439
|
size known, both platforms decode the source *near that size* instead of at full resolution —
|
|
333
440
|
iOS via a downsampled thumbnail decode, Android via Coil's subsampling. Without `resize`, a
|
|
334
441
|
48 MP photo decompresses to ~190 MB of bitmap no matter how small you display it.
|
|
442
|
+
- **Android draws transformed images from hardware bitmaps** (API 26+). When a view loads its
|
|
443
|
+
image natively (`<NativePipelineImage>`, or `<NativeNitroImage image={createImageLoader(...)}>`),
|
|
444
|
+
a resized, blurred or rounded result is uploaded to a `Bitmap.Config.HARDWARE` bitmap once and
|
|
445
|
+
cached like that, so its pixels live in GPU memory instead of the native heap and each view
|
|
446
|
+
drawing it skips a texture upload. In a 200-cell list of rounded thumbnails this cut the app's
|
|
447
|
+
PSS by ~100 MB on a Pixel 6a. Images returned to JavaScript — `loadImage` and
|
|
448
|
+
`createImageLoader(...).loadImage()` — skip that step, so their transformed results stay
|
|
449
|
+
software bitmaps with readable pixels (`toArrayBuffer`/`toBase64`); the two paths cache under
|
|
450
|
+
different keys only when that upload step is present.
|
|
335
451
|
- **Prefetching stores bytes, not bitmaps.** `preLoadImage(s)` writes the download to the disk
|
|
336
452
|
cache and skips decoding entirely.
|
|
337
453
|
- **The in-memory cache is capped and tunable** (defaults: 128 MB on iOS, 25% of the app's memory
|
package/android/CMakeLists.txt
CHANGED
|
@@ -11,6 +11,8 @@ add_compile_options(-DRN_SERIALIZABLE_STATE=1)
|
|
|
11
11
|
# Define C++ library and add all sources
|
|
12
12
|
add_library(${PACKAGE_NAME} SHARED
|
|
13
13
|
src/main/cpp/cpp-adapter.cpp
|
|
14
|
+
src/main/cpp/GaussianBlur.cpp
|
|
15
|
+
src/main/cpp/GaussianBlurJni.cpp
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
# Add Nitrogen specs :)
|
|
@@ -23,12 +25,14 @@ include_directories(
|
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
find_library(LOG_LIB log)
|
|
28
|
+
find_library(JNIGRAPHICS_LIB jnigraphics) # <-- AndroidBitmap_lockPixels for the blur
|
|
26
29
|
find_package(react-native-nitro-image REQUIRED) # <-- for the HybridImage type
|
|
27
30
|
|
|
28
31
|
# Link all libraries together
|
|
29
32
|
target_link_libraries(
|
|
30
33
|
${PACKAGE_NAME}
|
|
31
34
|
${LOG_LIB}
|
|
35
|
+
${JNIGRAPHICS_LIB}
|
|
32
36
|
android # <-- Android core
|
|
33
37
|
react-native-nitro-image::NitroImage # <-- NitroImage
|
|
34
38
|
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
//
|
|
2
|
+
// GaussianBlur.cpp
|
|
3
|
+
// NitroImagePipeline
|
|
4
|
+
//
|
|
5
|
+
// See GaussianBlur.hpp. Three box-blur passes approximate the Gaussian;
|
|
6
|
+
// each pass is a separable 2D box (horizontal running sum, then vertical
|
|
7
|
+
// running sum) that rounds to 8 bits only once, so it matches vImage's
|
|
8
|
+
// single 2D `vImageBoxConvolve_ARGB8888` pass on iOS rather than
|
|
9
|
+
// accumulating an extra rounding error per direction.
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
#include "GaussianBlur.hpp"
|
|
13
|
+
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <cmath>
|
|
16
|
+
#include <cstring>
|
|
17
|
+
#include <vector>
|
|
18
|
+
|
|
19
|
+
namespace {
|
|
20
|
+
|
|
21
|
+
constexpr int kChannels = 4;
|
|
22
|
+
|
|
23
|
+
/// The standard deviation three box blurs of the given widths add up to.
|
|
24
|
+
double standardDeviation(const uint32_t boxes[NITRO_BLUR_PASSES]) {
|
|
25
|
+
double sum = 0;
|
|
26
|
+
for (int i = 0; i < NITRO_BLUR_PASSES; i++) {
|
|
27
|
+
const double width = boxes[i];
|
|
28
|
+
sum += width * width - 1;
|
|
29
|
+
}
|
|
30
|
+
return std::sqrt(sum / 12);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// One box pass of width `box` over the whole image, in place.
|
|
34
|
+
///
|
|
35
|
+
/// `rowSums` caches the horizontal running sums of the rows the vertical
|
|
36
|
+
/// window currently covers (a ring of `ring` rows), so each row's horizontal
|
|
37
|
+
/// pass runs once. The image edges are clamped: sampling outside the image
|
|
38
|
+
/// returns the nearest border pixel, exactly like vImage's kvImageEdgeExtend.
|
|
39
|
+
void boxPass(
|
|
40
|
+
uint8_t* pixels,
|
|
41
|
+
uint32_t width,
|
|
42
|
+
uint32_t height,
|
|
43
|
+
size_t stride,
|
|
44
|
+
uint32_t box,
|
|
45
|
+
std::vector<uint32_t>& rowSums,
|
|
46
|
+
std::vector<int64_t>& rowIndex,
|
|
47
|
+
std::vector<uint64_t>& column) {
|
|
48
|
+
const int64_t radius = (static_cast<int64_t>(box) - 1) / 2;
|
|
49
|
+
const int64_t w = width;
|
|
50
|
+
const int64_t h = height;
|
|
51
|
+
const size_t rowLength = static_cast<size_t>(width) * kChannels;
|
|
52
|
+
const int64_t ring = std::min<int64_t>(box, h) + 1;
|
|
53
|
+
const uint64_t area = static_cast<uint64_t>(box) * box;
|
|
54
|
+
const uint64_t half = area / 2;
|
|
55
|
+
|
|
56
|
+
std::fill(rowIndex.begin(), rowIndex.end(), -1);
|
|
57
|
+
|
|
58
|
+
auto clampRow = [h](int64_t y) { return std::max<int64_t>(0, std::min(h - 1, y)); };
|
|
59
|
+
|
|
60
|
+
// Horizontal running sum of image row `y` into the ring slot for it.
|
|
61
|
+
auto horizontal = [&](int64_t y) -> const uint32_t* {
|
|
62
|
+
const int64_t slot = y % ring;
|
|
63
|
+
uint32_t* out = rowSums.data() + static_cast<size_t>(slot) * rowLength;
|
|
64
|
+
if (rowIndex[static_cast<size_t>(slot)] == y) return out;
|
|
65
|
+
rowIndex[static_cast<size_t>(slot)] = y;
|
|
66
|
+
|
|
67
|
+
const uint8_t* row = pixels + static_cast<size_t>(y) * stride;
|
|
68
|
+
auto px = [&](int64_t x) {
|
|
69
|
+
return row + static_cast<size_t>(std::max<int64_t>(0, std::min(w - 1, x))) * kChannels;
|
|
70
|
+
};
|
|
71
|
+
uint32_t sum[kChannels] = {0, 0, 0, 0};
|
|
72
|
+
for (int64_t dx = -radius; dx <= radius; dx++) {
|
|
73
|
+
const uint8_t* p = px(dx);
|
|
74
|
+
for (int c = 0; c < kChannels; c++) sum[c] += p[c];
|
|
75
|
+
}
|
|
76
|
+
for (int64_t x = 0; x < w; x++) {
|
|
77
|
+
for (int c = 0; c < kChannels; c++) out[x * kChannels + c] = sum[c];
|
|
78
|
+
const uint8_t* leaving = px(x - radius);
|
|
79
|
+
const uint8_t* entering = px(x + radius + 1);
|
|
80
|
+
for (int c = 0; c < kChannels; c++) sum[c] += entering[c] - leaving[c];
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Vertical running sum over the clamped window [y - radius, y + radius].
|
|
86
|
+
std::fill(column.begin(), column.end(), 0);
|
|
87
|
+
for (int64_t dy = -radius; dy <= radius; dy++) {
|
|
88
|
+
const uint32_t* r = horizontal(clampRow(dy));
|
|
89
|
+
for (size_t i = 0; i < rowLength; i++) column[i] += r[i];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (int64_t y = 0; y < h; y++) {
|
|
93
|
+
uint8_t* out = pixels + static_cast<size_t>(y) * stride;
|
|
94
|
+
for (size_t i = 0; i < rowLength; i++) {
|
|
95
|
+
out[i] = static_cast<uint8_t>((column[i] + half) / area);
|
|
96
|
+
}
|
|
97
|
+
if (y + 1 == h) break;
|
|
98
|
+
// The row leaving the window is still cached: it is inside the window,
|
|
99
|
+
// and the ring holds one row more than the window can span.
|
|
100
|
+
const uint32_t* leaving = horizontal(clampRow(y - radius));
|
|
101
|
+
const uint32_t* entering = horizontal(clampRow(y + radius + 1));
|
|
102
|
+
// Two steps: a uint32 `entering - leaving` would wrap before it reached
|
|
103
|
+
// the 64-bit accumulator.
|
|
104
|
+
for (size_t i = 0; i < rowLength; i++) column[i] += entering[i];
|
|
105
|
+
for (size_t i = 0; i < rowLength; i++) column[i] -= leaving[i];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
} // namespace
|
|
110
|
+
|
|
111
|
+
extern "C" void nitro_blur_box_sizes(double sigma, uint32_t boxes[NITRO_BLUR_PASSES]) {
|
|
112
|
+
// Three boxes of widths w1…w3 produce a standard deviation of
|
|
113
|
+
// sqrt((w1² + w2² + w3² - 3) / 12). Box widths have to be odd, so the
|
|
114
|
+
// passes are split between the two odd integers straddling the ideal width
|
|
115
|
+
// and the split landing closest to `sigma` wins — the same search as
|
|
116
|
+
// `GaussianBlur.boxSizes(forSigma:)` on iOS, so both platforms pick the
|
|
117
|
+
// same widths.
|
|
118
|
+
const int passes = NITRO_BLUR_PASSES;
|
|
119
|
+
// A NaN or infinite sigma would make every candidate's error NaN and
|
|
120
|
+
// leave `boxes` unwritten; treat it, and sigma <= 0, as "no blur".
|
|
121
|
+
if (!std::isfinite(sigma) || sigma <= 0) {
|
|
122
|
+
for (int i = 0; i < passes; i++) boxes[i] = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
int64_t lower = static_cast<int64_t>(std::sqrt((12 * sigma * sigma / passes) + 1));
|
|
126
|
+
if (lower % 2 == 0) lower -= 1;
|
|
127
|
+
lower = std::min<int64_t>(std::max<int64_t>(lower, 1), static_cast<int64_t>(UINT32_MAX) - 2);
|
|
128
|
+
const int64_t upper = lower + 2;
|
|
129
|
+
|
|
130
|
+
double bestError = INFINITY;
|
|
131
|
+
for (int lowerCount = 0; lowerCount <= passes; lowerCount++) {
|
|
132
|
+
uint32_t candidate[NITRO_BLUR_PASSES];
|
|
133
|
+
for (int pass = 0; pass < passes; pass++) {
|
|
134
|
+
candidate[pass] = static_cast<uint32_t>(pass < lowerCount ? lower : upper);
|
|
135
|
+
}
|
|
136
|
+
const double error = std::fabs(standardDeviation(candidate) - sigma);
|
|
137
|
+
if (error < bestError) {
|
|
138
|
+
bestError = error;
|
|
139
|
+
std::memcpy(boxes, candidate, sizeof(candidate));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
extern "C" int nitro_blur_premultiplied_8888(
|
|
145
|
+
uint8_t* pixels, uint32_t width, uint32_t height, size_t stride, double sigma) {
|
|
146
|
+
if (width == 0 || height == 0) return 0;
|
|
147
|
+
uint32_t boxes[NITRO_BLUR_PASSES];
|
|
148
|
+
nitro_blur_box_sizes(sigma, boxes);
|
|
149
|
+
|
|
150
|
+
// Size the scratch buffers for the widest pass so every pass fits in them.
|
|
151
|
+
const uint32_t widest = *std::max_element(boxes, boxes + NITRO_BLUR_PASSES);
|
|
152
|
+
const size_t rowLength = static_cast<size_t>(width) * kChannels;
|
|
153
|
+
const size_t ring = std::min<size_t>(widest, height) + 1;
|
|
154
|
+
try {
|
|
155
|
+
std::vector<uint32_t> rowSums(ring * rowLength);
|
|
156
|
+
std::vector<int64_t> rowIndex(ring);
|
|
157
|
+
std::vector<uint64_t> column(rowLength);
|
|
158
|
+
for (int i = 0; i < NITRO_BLUR_PASSES; i++) {
|
|
159
|
+
boxPass(pixels, width, height, stride, boxes[i], rowSums, rowIndex, column);
|
|
160
|
+
}
|
|
161
|
+
} catch (const std::bad_alloc&) {
|
|
162
|
+
return -1;
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// GaussianBlur.hpp
|
|
3
|
+
// NitroImagePipeline
|
|
4
|
+
//
|
|
5
|
+
// Cross-platform Gaussian blur — the Android kernel, a port of
|
|
6
|
+
// ios/GaussianBlur.swift.
|
|
7
|
+
//
|
|
8
|
+
// This file deliberately depends on nothing but the C and C++ standard
|
|
9
|
+
// libraries (no JNI, no Android headers) so `scripts/verify-blur.swift` can
|
|
10
|
+
// compile it on the host and measure it against the iOS kernel. The JNI
|
|
11
|
+
// glue lives in GaussianBlurJni.cpp.
|
|
12
|
+
//
|
|
13
|
+
// `sigma` is the standard deviation of the Gaussian, measured in *source
|
|
14
|
+
// image pixels* — the same sigma applied to the same source file produces
|
|
15
|
+
// the same result on iOS and Android. See the Swift file for why it is a
|
|
16
|
+
// sigma and not a "radius".
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
#pragma once
|
|
20
|
+
|
|
21
|
+
#include <stddef.h>
|
|
22
|
+
#include <stdint.h>
|
|
23
|
+
|
|
24
|
+
#ifdef __cplusplus
|
|
25
|
+
extern "C" {
|
|
26
|
+
#endif
|
|
27
|
+
|
|
28
|
+
/// Number of box-blur passes; both platforms run three.
|
|
29
|
+
#define NITRO_BLUR_PASSES 3
|
|
30
|
+
|
|
31
|
+
/// Widths for the three box-blur passes that approximate a Gaussian of
|
|
32
|
+
/// `sigma` — the same numbers `GaussianBlur.boxSizes(forSigma:)` returns on
|
|
33
|
+
/// iOS. Every width is odd and at least 1.
|
|
34
|
+
void nitro_blur_box_sizes(double sigma, uint32_t boxes[NITRO_BLUR_PASSES]);
|
|
35
|
+
|
|
36
|
+
/// Blurs `pixels` in place with the box widths for `sigma`.
|
|
37
|
+
///
|
|
38
|
+
/// The buffer is `height` rows of `width` 4-byte pixels, rows `stride` bytes
|
|
39
|
+
/// apart. All four channels are convolved identically, so the byte order
|
|
40
|
+
/// (ARGB on iOS, RGBA on Android) does not matter — but alpha must be
|
|
41
|
+
/// premultiplied, otherwise transparent edges bleed dark halos into the blur.
|
|
42
|
+
/// The image edges are clamped (each border pixel extends outward), matching
|
|
43
|
+
/// vImage's kvImageEdgeExtend, so blurred images keep their borders instead
|
|
44
|
+
/// of fading out into transparent black.
|
|
45
|
+
///
|
|
46
|
+
/// Returns 0 on success, or -1 if the scratch memory could not be allocated.
|
|
47
|
+
int nitro_blur_premultiplied_8888(
|
|
48
|
+
uint8_t* pixels, uint32_t width, uint32_t height, size_t stride, double sigma);
|
|
49
|
+
|
|
50
|
+
#ifdef __cplusplus
|
|
51
|
+
}
|
|
52
|
+
#endif
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//
|
|
2
|
+
// GaussianBlurJni.cpp
|
|
3
|
+
// NitroImagePipeline
|
|
4
|
+
//
|
|
5
|
+
// JNI entry point for the blur kernel, called from
|
|
6
|
+
// transform/BlurTransformation.kt with a mutable ARGB_8888 bitmap.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#include <android/bitmap.h>
|
|
10
|
+
#include <android/log.h>
|
|
11
|
+
#include <jni.h>
|
|
12
|
+
|
|
13
|
+
#include "GaussianBlur.hpp"
|
|
14
|
+
|
|
15
|
+
namespace {
|
|
16
|
+
constexpr const char* kTag = "NitroImagePipeline";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
20
|
+
Java_com_margelo_nitro_nitroimagepipeline_transform_BlurTransformation_nativeBlur(
|
|
21
|
+
JNIEnv* env, jclass, jobject bitmap, jfloat sigma) {
|
|
22
|
+
AndroidBitmapInfo info;
|
|
23
|
+
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
|
|
24
|
+
__android_log_print(ANDROID_LOG_ERROR, kTag, "blur: AndroidBitmap_getInfo failed");
|
|
25
|
+
return JNI_FALSE;
|
|
26
|
+
}
|
|
27
|
+
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
|
|
28
|
+
__android_log_print(
|
|
29
|
+
ANDROID_LOG_ERROR,
|
|
30
|
+
kTag,
|
|
31
|
+
"blur: expected an ARGB_8888 bitmap (RGBA_8888 to the NDK), got NDK format %d",
|
|
32
|
+
info.format);
|
|
33
|
+
return JNI_FALSE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
void* pixels = nullptr;
|
|
37
|
+
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) != ANDROID_BITMAP_RESULT_SUCCESS ||
|
|
38
|
+
pixels == nullptr) {
|
|
39
|
+
__android_log_print(ANDROID_LOG_ERROR, kTag, "blur: AndroidBitmap_lockPixels failed");
|
|
40
|
+
return JNI_FALSE;
|
|
41
|
+
}
|
|
42
|
+
const int result = nitro_blur_premultiplied_8888(
|
|
43
|
+
static_cast<uint8_t*>(pixels), info.width, info.height, info.stride, sigma);
|
|
44
|
+
AndroidBitmap_unlockPixels(env, bitmap);
|
|
45
|
+
if (result != 0) {
|
|
46
|
+
__android_log_print(ANDROID_LOG_ERROR, kTag, "blur: out of memory for the scratch buffers");
|
|
47
|
+
return JNI_FALSE;
|
|
48
|
+
}
|
|
49
|
+
return JNI_TRUE;
|
|
50
|
+
}
|