react-native-nitro-image-pipeline 1.5.0 → 1.7.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 +142 -18
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +72 -16
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/PipelineImageLoader.kt +47 -8
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/FitGeometry.kt +104 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/PipelineRoundedCornersTransformation.kt +103 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/ResizeTransformation.kt +69 -16
- package/ios/FitResizeProcessor.swift +127 -0
- package/ios/HybridNitroImagePipeline.swift +88 -24
- package/ios/PipelineImageLoader.swift +108 -18
- package/lib/commonjs/NativePipelineImage.js +20 -4
- package/lib/commonjs/NativePipelineImage.js.map +1 -1
- package/lib/commonjs/PipelineImage.js +13 -1
- package/lib/commonjs/PipelineImage.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/resizeForStyle.js +3 -1
- package/lib/commonjs/resizeForStyle.js.map +1 -1
- package/lib/commonjs/resolveImageSource.js +89 -0
- package/lib/commonjs/resolveImageSource.js.map +1 -0
- package/lib/commonjs/useImage.js +14 -6
- package/lib/commonjs/useImage.js.map +1 -1
- package/lib/commonjs/usePipelineImageLoader.js +114 -11
- package/lib/commonjs/usePipelineImageLoader.js.map +1 -1
- package/lib/module/NativePipelineImage.js +20 -4
- package/lib/module/NativePipelineImage.js.map +1 -1
- package/lib/module/PipelineImage.js +13 -1
- package/lib/module/PipelineImage.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/resizeForStyle.js +3 -1
- package/lib/module/resizeForStyle.js.map +1 -1
- package/lib/module/resolveImageSource.js +83 -0
- package/lib/module/resolveImageSource.js.map +1 -0
- package/lib/module/useImage.js +14 -6
- package/lib/module/useImage.js.map +1 -1
- package/lib/module/usePipelineImageLoader.js +115 -12
- package/lib/module/usePipelineImageLoader.js.map +1 -1
- package/lib/typescript/src/NativePipelineImage.d.ts +51 -5
- package/lib/typescript/src/NativePipelineImage.d.ts.map +1 -1
- package/lib/typescript/src/PipelineImage.d.ts +35 -3
- package/lib/typescript/src/PipelineImage.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/resizeForStyle.d.ts +3 -1
- package/lib/typescript/src/resizeForStyle.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 +95 -8
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
- package/lib/typescript/src/useImage.d.ts +13 -6
- package/lib/typescript/src/useImage.d.ts.map +1 -1
- package/lib/typescript/src/usePipelineImageLoader.d.ts +7 -3
- package/lib/typescript/src/usePipelineImageLoader.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroImagePipelineOnLoad.cpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +8 -0
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -0
- package/nitrogen/generated/android/c++/JResizeFit.hpp +64 -0
- package/nitrogen/generated/android/c++/JResizeOptions.hpp +14 -4
- package/nitrogen/generated/android/c++/JViewOptions.hpp +42 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/Func_void_double_double.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/Func_void_std__string.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ResizeFit.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ResizeOptions.kt +14 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ViewOptions.kt +29 -5
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.cpp +16 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +107 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +4 -0
- package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +4 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/ResizeFit.swift +48 -0
- package/nitrogen/generated/ios/swift/ResizeOptions.swift +31 -2
- package/nitrogen/generated/ios/swift/ViewOptions.swift +82 -1
- package/nitrogen/generated/shared/c++/ResizeFit.hpp +84 -0
- package/nitrogen/generated/shared/c++/ResizeOptions.hpp +14 -4
- package/nitrogen/generated/shared/c++/ViewOptions.hpp +24 -3
- package/package.json +1 -1
- package/src/NativePipelineImage.tsx +75 -6
- package/src/PipelineImage.tsx +46 -3
- package/src/index.ts +6 -0
- package/src/resizeForStyle.ts +3 -1
- package/src/resolveImageSource.ts +83 -0
- package/src/specs/nitro-image-toolkit.nitro.ts +96 -8
- package/src/useImage.ts +38 -17
- package/src/usePipelineImageLoader.ts +126 -11
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ 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
|
-
- Resize (
|
|
13
|
+
- Resize in every `resizeMode` (`cover`, `contain`, `stretch`, `center`) and apply Gaussian blur and rounded corners (uniform or per-corner) at load time
|
|
14
14
|
- Apply Gaussian blur to already-loaded images
|
|
15
15
|
- Clear the image cache on demand
|
|
16
16
|
- `useImage` hook for declarative image loading in components
|
|
@@ -69,7 +69,9 @@ pixels internally, unlike the pixel-based values used everywhere else in this li
|
|
|
69
69
|
so a style that already rounds the view rounds the bitmap too, with no separate prop. Pass
|
|
70
70
|
`cornerRadius` explicitly to override that. `onLoad`/`onError` callbacks are supported, and every
|
|
71
71
|
other prop (`resizeMode`, `recyclingKey`, `testID`, …) is passed straight through to
|
|
72
|
-
`NativeNitroImage`.
|
|
72
|
+
`NativeNitroImage`. `resizeMode` also decides how the bitmap is produced: `resizeMode="contain"`
|
|
73
|
+
decodes an aspect-fitted bitmap (no crop, no padding) rather than a center-cropped one the view
|
|
74
|
+
then letterboxes — see [Resize modes](#resize-modes). Pass `fit` to decouple the two.
|
|
73
75
|
|
|
74
76
|
### `<NativePipelineImage>` component
|
|
75
77
|
|
|
@@ -93,11 +95,25 @@ import { NativePipelineImage } from 'react-native-nitro-image-pipeline';
|
|
|
93
95
|
`blur`/`cornerRadius` are in points and `style`'s `borderRadius` is picked up automatically,
|
|
94
96
|
exactly like `<PipelineImage>`. The trade-offs of going fully native:
|
|
95
97
|
|
|
96
|
-
-
|
|
97
|
-
`useImage` when you need
|
|
98
|
+
- `onLoad` reports the bitmap's size in pixels rather than the `Image` itself, which never crosses
|
|
99
|
+
into JS — use `<PipelineImage>` or `useImage` when you need the `Image`. It means "this view is
|
|
100
|
+
now showing this image" rather than firing exactly once: a recycled cell re-attaching calls it
|
|
101
|
+
again, and a single view may call it more than once for the same image. Make it idempotent.
|
|
102
|
+
- Setting `onLoad`/`onError` means there is per-image JS work again. Leave them unset (the default)
|
|
103
|
+
and nothing crosses into JS at all.
|
|
98
104
|
- The bitmap is loaded once at the size the view first has; if the view resizes later, the bitmap
|
|
99
105
|
scales with it instead of reloading.
|
|
100
106
|
|
|
107
|
+
```tsx
|
|
108
|
+
<NativePipelineImage
|
|
109
|
+
url={url}
|
|
110
|
+
style={styles.photo}
|
|
111
|
+
// Bitmap pixels, not points. Inline arrows are fine — they don't reload it.
|
|
112
|
+
onLoad={(width, height) => console.log(`loaded ${width}×${height}`)}
|
|
113
|
+
onError={(message) => setFailed(message)}
|
|
114
|
+
/>;
|
|
115
|
+
```
|
|
116
|
+
|
|
101
117
|
Under the hood this is `NitroImagePipeline.createImageLoader(url, options)` — an
|
|
102
118
|
[`ImageLoader`](https://github.com/mrousavy/react-native-nitro-image) driven by
|
|
103
119
|
`NativeNitroImage` — so you can also use the `usePipelineImageLoader(url, options)` hook directly
|
|
@@ -177,6 +193,41 @@ function BlurFade({ url, blurred }: { url: string; blurred: boolean }) {
|
|
|
177
193
|
}
|
|
178
194
|
```
|
|
179
195
|
|
|
196
|
+
### Local images and `require()`
|
|
197
|
+
|
|
198
|
+
Every `url` in this library — `<PipelineImage>`, `<NativePipelineImage>`, `useImage`,
|
|
199
|
+
`usePipelineImageLoader` — also takes a `require()`d asset, and any `url` string may point at the
|
|
200
|
+
file system. The image goes through the same pipeline, so a bundled logo or a photo from the camera
|
|
201
|
+
roll gets the same resize-to-layout, blur and rounded corners as a download:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
// A bundled asset — streamed from Metro in debug, read from the app bundle /
|
|
205
|
+
// resources in release, at the scale that matches the screen (like <Image>).
|
|
206
|
+
<PipelineImage url={require('./assets/logo.png')} style={styles.logo} blur={2} />
|
|
207
|
+
|
|
208
|
+
// A file on disk — a `file://` URL or a plain absolute path, e.g. the path
|
|
209
|
+
// react-native-nitro-image's `saveToTemporaryFileAsync` returns.
|
|
210
|
+
<NativePipelineImage url={`file://${photoPath}`} style={styles.thumb} />
|
|
211
|
+
<NativePipelineImage url={photoPath} style={styles.thumb} />
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The direct `NitroImagePipeline.loadImage`/`createImageLoader` calls take a string; resolve a
|
|
215
|
+
`require()` first with `resolveImageUrl`:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { NitroImagePipeline, resolveImageUrl } from 'react-native-nitro-image-pipeline';
|
|
219
|
+
|
|
220
|
+
const logo = await NitroImagePipeline.loadImage(resolveImageUrl(require('./assets/logo.png')), {
|
|
221
|
+
resize: { width: 200, height: 200 },
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Accepted `url` forms: `http(s)://`, `file://`, a plain absolute path, `data:`, and on Android also
|
|
226
|
+
`content://` URIs and bare drawable resource names (what `require()` resolves to in a release build
|
|
227
|
+
there). Local sources are cached **in memory only** — there is nothing to gain from copying a file
|
|
228
|
+
that is already on disk into the disk cache — so `cache: 'disk'` on a local `url` means no caching,
|
|
229
|
+
and `preLoadImage(s)` treats local sources as a no-op.
|
|
230
|
+
|
|
180
231
|
### `useImage` hook
|
|
181
232
|
|
|
182
233
|
The simplest way to load an image in a component:
|
|
@@ -249,24 +300,67 @@ await NitroImagePipeline.clearCache();
|
|
|
249
300
|
|
|
250
301
|
### `loadImage(url, options?)`
|
|
251
302
|
|
|
252
|
-
Loads an image from a URL and returns a `Promise<Image>`.
|
|
303
|
+
Loads an image from a URL and returns a `Promise<Image>`. `url` is a string — `http(s)://`, `file://`,
|
|
304
|
+
a plain absolute path, or the other forms listed under
|
|
305
|
+
[Local images and `require()`](#local-images-and-require); pass a `require()` through
|
|
306
|
+
[`resolveImageUrl`](#resolveimageurlsource) first.
|
|
253
307
|
|
|
254
308
|
| Option | Type | Default | Description |
|
|
255
309
|
|---|---|---|---|
|
|
256
310
|
| `blur` | `number` | `0` | Gaussian blur strength applied at load time — see [Blur units](#blur-units) |
|
|
257
|
-
| `resize` | `{ width, height }` | source size | Target bitmap
|
|
311
|
+
| `resize` | `{ width, height, fit?, allowUpscale? }` | source size | Target bitmap box in pixels. By default scales to fill and center-crops (CSS `object-fit: cover`, upscaling if needed); `fit` selects the other modes and `allowUpscale: false` never enlarges the source — see [Resize modes](#resize-modes). Runs before `blur`/`cornerRadius`, so their pixel units refer to the produced bitmap. Typically your display size in points × `PixelRatio.get()` |
|
|
258
312
|
| `cornerRadius` | `number \| CornerRadii` | `0` | Corner radius in pixels of the produced bitmap — a single number for all four corners, or `{ topLeft?, topRight?, bottomLeft?, bottomRight? }` for independent per-corner radii (omitted corners stay square). Pair with `resize` for radii that match your layout |
|
|
259
313
|
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
260
314
|
|
|
315
|
+
#### Resize modes
|
|
316
|
+
|
|
317
|
+
`resize.fit` takes the same four values as `NativeNitroImage`'s `resizeMode`, so the bitmap can be
|
|
318
|
+
produced the way the view will display it. With a `w × h` source and a `W × H` box:
|
|
319
|
+
|
|
320
|
+
| `fit` | Scale | Produced bitmap | Notes |
|
|
321
|
+
|---|---|---|---|
|
|
322
|
+
| `'cover'` (default) | `max(W/w, H/h)` | exactly `W × H` | Center-cropped. Unchanged from earlier versions, byte for byte |
|
|
323
|
+
| `'contain'` | `min(W/w, H/h)` | `round(w·s) × round(h·s)` | Aspect-fit. The bitmap is **not** padded to the box |
|
|
324
|
+
| `'stretch'` | `W/w` and `H/h` independently | exactly `W × H` | Aspect ratio ignored |
|
|
325
|
+
| `'center'` | `1` | `min(w, W) × min(h, H)` | Never resamples; only center-crops what the box could not show |
|
|
326
|
+
|
|
327
|
+
`allowUpscale: false` clamps the scale to at most 1 for `cover`, `contain` and `stretch`, so a
|
|
328
|
+
source smaller than the box keeps its own size (under `cover` it is only cropped to the box). It
|
|
329
|
+
has no effect on `center`, which never scales. Some examples:
|
|
330
|
+
|
|
331
|
+
| Source → box | `cover` | `contain` | `stretch` | `center` |
|
|
332
|
+
|---|---|---|---|---|
|
|
333
|
+
| 2000×1000 → 400×400 | 400×400 | 400×200 | 400×400 | 400×400 (cropped) |
|
|
334
|
+
| 100×50 → 400×400 | 400×400 | 400×200 | 400×400 | 100×50 |
|
|
335
|
+
| 100×50 → 400×400, `allowUpscale: false` | 100×50 | 100×50 | 100×50 | 100×50 |
|
|
336
|
+
|
|
337
|
+
`blur` and `cornerRadius` apply to the produced bitmap, so under `contain` the corners round the
|
|
338
|
+
image's own edges, not the box's. Displaying a `center` bitmap pixel for pixel needs the view's
|
|
339
|
+
`resizeMode="center"`, which `<NativePipelineImage>` (and `createImageLoader`) draw 1:1 on both
|
|
340
|
+
platforms; an `Image` passed to `<NativeNitroImage>` or `<PipelineImage>` is a scale-1 image that
|
|
341
|
+
iOS's `center` mode draws at 1 pt per pixel (nitro-image's `Image` contract — `PixelRatio.get()`×
|
|
342
|
+
too large on a Retina screen). Both platforms implement the same geometry (down to the
|
|
343
|
+
rounding), and the harness suites check every cell of this table on each. How much is decoded before
|
|
344
|
+
the resize depends on the mode. `contain` decodes aspect-fit, so the decode is always near the
|
|
345
|
+
produced size. `cover` and `stretch` decode aspect-*fill* — the source scaled until it covers the
|
|
346
|
+
box — which is near the produced size when the aspect ratios are similar, but a source whose
|
|
347
|
+
aspect ratio differs sharply from the box (a panorama into a square) is decoded larger, up to its
|
|
348
|
+
full size, before the crop or stretch; `cover` has always worked this way, and `stretch` needs the
|
|
349
|
+
fill scale to keep the axis that is shrunk less from being resampled up from a few pixels.
|
|
350
|
+
`center` decodes the source at full resolution — it has to see the source's own pixels. Prefer
|
|
351
|
+
`contain`, or a box with a similar aspect ratio, for very large images.
|
|
352
|
+
|
|
261
353
|
### `<PipelineImage>`
|
|
262
354
|
|
|
263
355
|
| Prop | Type | Default | Description |
|
|
264
356
|
|---|---|---|---|
|
|
265
|
-
| `url` | `string` | — | Image
|
|
357
|
+
| `url` | `string \| number` | — | Image to load: a URL string (`https://`, `file://`, an absolute path) or a `require()`d asset |
|
|
266
358
|
| `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)) |
|
|
267
359
|
| `blur` | `number` | `0` | Gaussian blur strength, in **points** (converted to bitmap pixels internally) |
|
|
268
360
|
| `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 |
|
|
269
361
|
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
362
|
+
| `fit` | `ResizeFit` | derived from `resizeMode` | How the bitmap is fitted into the display size — see [Resize modes](#resize-modes). When omitted it follows `resizeMode` (`'cover'` if that is unset too); set it to decouple the two. On iOS, `resizeMode="center"` draws the (scale-1) `Image` at 1 pt per pixel — a nitro-image `Image` limitation; use `<NativePipelineImage>` for pixel-exact `center` on both platforms |
|
|
363
|
+
| `allowUpscale` | `boolean` | `true` | `false` never enlarges a source smaller than the display size when the *bitmap* is produced (nothing is decoded or blurred larger than the source). The view still scales it per `resizeMode`; use `resizeMode="center"` to show it at its natural size |
|
|
270
364
|
| `onLoad` | `(image: Image) => void` | — | Called when the image finishes loading |
|
|
271
365
|
| `onError` | `(error: Error) => void` | — | Called if loading fails |
|
|
272
366
|
| `onLayout` | `(event: LayoutChangeEvent) => void` | — | Standard `View` layout callback; also drives the deferred resize for non-numeric sizes |
|
|
@@ -277,33 +371,58 @@ Loads an image from a URL and returns a `Promise<Image>`.
|
|
|
277
371
|
|
|
278
372
|
| Prop | Type | Default | Description |
|
|
279
373
|
|---|---|---|---|
|
|
280
|
-
| `url` | `string` | — | Image
|
|
374
|
+
| `url` | `string \| number` | — | Image to load: a URL string (`https://`, `file://`, an absolute path) or a `require()`d asset |
|
|
281
375
|
| `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 |
|
|
282
376
|
| `blur` | `number` | `0` | Gaussian blur strength, in **points** (screen scale applied natively) |
|
|
283
377
|
| `cornerRadius` | `number \| CornerRadii` | derived from `style` | Corner radius, in **points** (screen scale applied natively) |
|
|
284
378
|
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
285
|
-
| `resize` | `{ width, height }` | measured from the view | Explicit target bitmap
|
|
379
|
+
| `resize` | `{ width, height, fit?, allowUpscale? }` | measured from the view | Explicit target bitmap box in **pixels**, skipping the native measurement. Rarely needed |
|
|
380
|
+
| `fit` | `ResizeFit` | derived from `resizeMode` | How the bitmap is fitted into the measured size — see [Resize modes](#resize-modes). Follows `resizeMode` when omitted; set it to decouple the two |
|
|
381
|
+
| `allowUpscale` | `boolean` | `true` | `false` never enlarges a source smaller than the view when the *bitmap* is produced. The view still scales it per `resizeMode`; `resizeMode="center"` shows it pixel for pixel on both platforms |
|
|
382
|
+
| `onLoad` | `(width: number, height: number) => void` | — | Called when the view has displayed the image, with the bitmap's size in **pixels**. Hands you the size rather than the `Image`, which stays native. Not a one-shot event — a recycled cell calls it again on re-attach, and one view may call it more than once for the same image, so make it idempotent |
|
|
383
|
+
| `onError` | `(message: string) => void` | — | Called when loading fails, with the error's message. A load cancelled by the view detaching is not a failure. Without it, failures on this component are silent |
|
|
286
384
|
| `ref` | `Ref<NativePipelineImageRef>` | — | Forwarded to the underlying `NativeNitroImage` host view |
|
|
287
385
|
| `…NativeNitroImage props` | — | — | Everything else (`resizeMode`, `recyclingKey`, `testID`, …) is passed through; `recyclingKey` defaults to `url` |
|
|
288
386
|
|
|
289
|
-
|
|
387
|
+
`onLoad`/`onError` are the only things that cross into JS here, and only when you pass them: unset,
|
|
388
|
+
loading happens entirely natively. The `Image` itself never crosses — use `<PipelineImage>` or
|
|
389
|
+
`useImage` for that.
|
|
290
390
|
|
|
291
|
-
### `createImageLoader(url, options?)` / `usePipelineImageLoader(
|
|
391
|
+
### `createImageLoader(url, options?)` / `usePipelineImageLoader(source, options?)`
|
|
292
392
|
|
|
293
393
|
Creates the [`ImageLoader`](https://github.com/mrousavy/react-native-nitro-image) that powers
|
|
294
394
|
`<NativePipelineImage>`, for use with your own `<NativeNitroImage image={loader} />`. The view
|
|
295
395
|
calls into it natively when it attaches (load at the view's laid-out size) and detaches
|
|
296
|
-
(cancel + release). `options` takes `blur`/`cornerRadius` in **points
|
|
297
|
-
pixel-based `resize` override
|
|
298
|
-
|
|
299
|
-
the
|
|
396
|
+
(cancel + release). `options` takes `blur`/`cornerRadius` in **points**, an optional
|
|
397
|
+
pixel-based `resize` override, and `fit`/`allowUpscale` for the box the view measures (pass the
|
|
398
|
+
view's `resizeMode` as `fit`; a `resize` that carries its own wins) — see the `ViewOptions` type.
|
|
399
|
+
Unlike the callbacks, `fit` is part of what is loaded, so changing it swaps the loader. It also
|
|
400
|
+
takes the optional
|
|
401
|
+
`onLoad(width, height)`/`onError(message)` callbacks a view reports its loads through; without
|
|
402
|
+
them the loader never calls into JS. The hook memoizes by value, so inline options literals are
|
|
403
|
+
fine — the callbacks are read through a ref, so inline arrows don't reload the image either — and
|
|
404
|
+
its `source` may be a `require()` as well as a URL string. `loader.loadImage()` also works
|
|
405
|
+
imperatively and resolves with the processed `Image`; it reports through its promise, not through
|
|
406
|
+
`onLoad`/`onError`.
|
|
407
|
+
|
|
408
|
+
### `resolveImageUrl(source)`
|
|
409
|
+
|
|
410
|
+
Turns an `ImageSource` (`string | number`) into the URL string the native pipeline loads: strings
|
|
411
|
+
pass through unchanged, a `require()`d asset is resolved with `Image.resolveAssetSource` to the
|
|
412
|
+
scale-matched variant. The components and hooks do this internally; use it when calling
|
|
413
|
+
`loadImage`, `createImageLoader` or `preLoadImage(s)` directly with a `require()`. Throws if the
|
|
414
|
+
number is not a registered asset; the components and hooks never throw for one — `useImage` reports
|
|
415
|
+
it through `error`, and `usePipelineImageLoader`/`<NativePipelineImage>` load a URL no loader can
|
|
416
|
+
resolve, so the request fails at load time like a missing file.
|
|
300
417
|
|
|
301
418
|
### `resizeForStyle(style)` / `resizeForLayout(width, height)`
|
|
302
419
|
|
|
303
420
|
Converts a layout size in points to a bitmap `resize` option in pixels. Returns
|
|
304
421
|
`{ width, height }` in whole pixels via `PixelRatio.getPixelSizeForLayoutSize`, or `undefined` for
|
|
305
422
|
non-numeric sizes (e.g. `'100%'`, `undefined`) — `resizeForStyle` reads `style.width`/`style.height`,
|
|
306
|
-
`resizeForLayout` takes explicit numbers.
|
|
423
|
+
`resizeForLayout` takes explicit numbers. Spread a `fit` into the result for the other
|
|
424
|
+
[resize modes](#resize-modes); under `contain` and `center` the size is then the box the bitmap
|
|
425
|
+
fits in, not necessarily its final size.
|
|
307
426
|
|
|
308
427
|
### `cornerRadiusForStyle(style)`
|
|
309
428
|
|
|
@@ -317,6 +436,7 @@ omitted.
|
|
|
317
436
|
### `preLoadImage(url)`
|
|
318
437
|
|
|
319
438
|
Prefetches a single image into the **disk cache**, without decoding it. Returns `Promise<void>`.
|
|
439
|
+
Local sources (`file://`, paths, resources) have no download to cache and are a no-op.
|
|
320
440
|
|
|
321
441
|
Prefetching only pays the network and disk I/O cost up front — no bitmap is decoded or held in
|
|
322
442
|
memory, so prefetching a long list of URLs doesn't balloon RAM. The image is decoded (at the
|
|
@@ -389,7 +509,11 @@ The pipeline is set up so RAM scales with what you display, not with what you do
|
|
|
389
509
|
- **Pass `resize` (or just use `<PipelineImage>`, which derives it from layout).** With a target
|
|
390
510
|
size known, both platforms decode the source *near that size* instead of at full resolution —
|
|
391
511
|
iOS via a downsampled thumbnail decode, Android via Coil's subsampling. Without `resize`, a
|
|
392
|
-
48 MP photo decompresses to ~190 MB of bitmap no matter how small you display it.
|
|
512
|
+
48 MP photo decompresses to ~190 MB of bitmap no matter how small you display it. "Near that
|
|
513
|
+
size" means the aspect-*fill* decode for `cover` and `stretch` — the source scaled until it
|
|
514
|
+
covers the box, so a panorama into a square box still decodes near its full width — and the
|
|
515
|
+
aspect-fit decode for `contain`, which is always near the produced size. `fit: 'center'` by
|
|
516
|
+
definition needs the source's own pixels and decodes at full resolution before cropping.
|
|
393
517
|
- **Android draws transformed images from hardware bitmaps** (API 26+). When a view loads its
|
|
394
518
|
image natively (`<NativePipelineImage>`, or `<NativeNitroImage image={createImageLoader(...)}>`),
|
|
395
519
|
a resized, blurred or rounded result is uploaded to a `Bitmap.Config.HARDWARE` bitmap once and
|
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt
CHANGED
|
@@ -3,6 +3,7 @@ package com.margelo.nitro.nitroimagepipeline
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
5
|
import androidx.core.graphics.drawable.toBitmap
|
|
6
|
+
import androidx.core.net.toUri
|
|
6
7
|
import coil3.BitmapImage
|
|
7
8
|
import coil3.ColorImage
|
|
8
9
|
import coil3.DrawableImage
|
|
@@ -18,6 +19,7 @@ import coil3.request.ImageRequest
|
|
|
18
19
|
import coil3.request.SuccessResult
|
|
19
20
|
import coil3.request.allowHardware
|
|
20
21
|
import coil3.request.transformations
|
|
22
|
+
import coil3.size.Precision
|
|
21
23
|
import coil3.size.Scale
|
|
22
24
|
import coil3.size.Size
|
|
23
25
|
import coil3.transform.RoundedCornersTransformation
|
|
@@ -28,6 +30,7 @@ import com.margelo.nitro.image.HybridImage
|
|
|
28
30
|
import com.margelo.nitro.image.HybridImageSpec
|
|
29
31
|
import com.margelo.nitro.nitroimagepipeline.transform.BlurTransformation
|
|
30
32
|
import com.margelo.nitro.nitroimagepipeline.transform.HardwareBitmapTransformation
|
|
33
|
+
import com.margelo.nitro.nitroimagepipeline.transform.PipelineRoundedCornersTransformation
|
|
31
34
|
import com.margelo.nitro.nitroimagepipeline.transform.ResizeTransformation
|
|
32
35
|
import kotlin.math.roundToInt
|
|
33
36
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -76,7 +79,7 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
76
79
|
// size — only when a loadImage actually displays it.
|
|
77
80
|
private fun preloadRequest(url: String): ImageRequest =
|
|
78
81
|
ImageRequest.Builder(context)
|
|
79
|
-
.data(url)
|
|
82
|
+
.data(requestData(context, url))
|
|
80
83
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
|
81
84
|
.decoderFactory { _, _, _ -> Decoder { DecodeResult(ColorImage(), false) } }
|
|
82
85
|
.build()
|
|
@@ -151,21 +154,27 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
151
154
|
val height = r.height.roundToInt()
|
|
152
155
|
if (width > 0 && height > 0) width to height else null
|
|
153
156
|
}
|
|
154
|
-
val
|
|
157
|
+
val fit = options?.resize?.fit ?: ResizeFit.COVER
|
|
158
|
+
val allowUpscale = options?.resize?.allowUpscale ?: true
|
|
159
|
+
// The resize as it behaved before `fit` existed — aspect-fill, crop,
|
|
160
|
+
// upscale — which keeps the original transformations and cache keys.
|
|
161
|
+
val isDefaultFit = fit == ResizeFit.COVER && allowUpscale
|
|
162
|
+
// Radii in bitmap pixels as (topLeft, topRight, bottomLeft, bottomRight),
|
|
163
|
+
// or null when no corner is actually rounded. Negative radii are square.
|
|
164
|
+
val cornerRadii: FloatArray? =
|
|
155
165
|
options
|
|
156
166
|
?.cornerRadius
|
|
157
167
|
?.match(
|
|
158
168
|
first = { radius ->
|
|
159
|
-
if (radius > 0.0)
|
|
169
|
+
if (radius > 0.0) FloatArray(4) { radius.toFloat() } else null
|
|
160
170
|
},
|
|
161
171
|
second = { radii ->
|
|
162
|
-
// RoundedCornersTransformation rejects negative radii; treat them as square.
|
|
163
172
|
val topLeft = (radii.topLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
164
173
|
val topRight = (radii.topRight?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
165
174
|
val bottomLeft = (radii.bottomLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
166
175
|
val bottomRight = (radii.bottomRight?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
167
176
|
if (topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f) {
|
|
168
|
-
|
|
177
|
+
floatArrayOf(topLeft, topRight, bottomLeft, bottomRight)
|
|
169
178
|
} else {
|
|
170
179
|
null
|
|
171
180
|
}
|
|
@@ -176,14 +185,25 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
176
185
|
// they run on, so they must see the final size. Coil's
|
|
177
186
|
// RoundedCornersTransformation already scale-fills and center-crops
|
|
178
187
|
// to the request size (`resize`, when set) as part of its own draw,
|
|
179
|
-
// so with rounded corners and no blur the explicit
|
|
180
|
-
// add an intermediate bitmap — and the radii still
|
|
181
|
-
// size, because that is the size its output has.
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
// so with the default fit, rounded corners and no blur the explicit
|
|
189
|
+
// resize would only add an intermediate bitmap — and the radii still
|
|
190
|
+
// apply to the final size, because that is the size its output has.
|
|
191
|
+
// Any other fit produces a bitmap that may be smaller than the box,
|
|
192
|
+
// so it always resizes explicitly and rounds at the produced size.
|
|
193
|
+
if (resize != null && (blur > 0f || cornerRadii == null || !isDefaultFit)) {
|
|
194
|
+
add(ResizeTransformation(resize.first, resize.second, fit, allowUpscale))
|
|
184
195
|
}
|
|
185
196
|
if (blur > 0f) add(BlurTransformation(blur))
|
|
186
|
-
|
|
197
|
+
cornerRadii?.let { (topLeft, topRight, bottomLeft, bottomRight) ->
|
|
198
|
+
if (resize == null || isDefaultFit) {
|
|
199
|
+
// The uniform constructor keeps the original cache key for a single radius.
|
|
200
|
+
val uniform = topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight
|
|
201
|
+
if (uniform) add(RoundedCornersTransformation(topLeft))
|
|
202
|
+
else add(RoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
|
|
203
|
+
} else {
|
|
204
|
+
add(PipelineRoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
187
207
|
// Without transformations Coil decodes straight to a hardware bitmap
|
|
188
208
|
// already (allowHardware below); only a transformed result needs the
|
|
189
209
|
// explicit upload.
|
|
@@ -192,7 +212,7 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
192
212
|
}
|
|
193
213
|
}
|
|
194
214
|
return ImageRequest.Builder(context)
|
|
195
|
-
.data(url)
|
|
215
|
+
.data(requestData(context, url))
|
|
196
216
|
.apply {
|
|
197
217
|
when (options?.cache) {
|
|
198
218
|
CacheOption.MEMORY -> {
|
|
@@ -211,10 +231,22 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
211
231
|
}
|
|
212
232
|
// Ask the decoder for the target size so a large source is
|
|
213
233
|
// subsampled near it instead of decoded at full resolution;
|
|
214
|
-
// ResizeTransformation then makes the size exact.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
234
|
+
// ResizeTransformation then makes the size exact. `contain`
|
|
235
|
+
// decodes to *fit* the box: its output follows the smaller
|
|
236
|
+
// scale, and a fill decode of a wide panorama into a small box
|
|
237
|
+
// would be near the source's full width before the
|
|
238
|
+
// transformation shrinks it (iOS's aspect-fit thumbnail bounds
|
|
239
|
+
// it the same way). `center` never scales, so it must see the
|
|
240
|
+
// source's own pixels — the default Size.ORIGINAL — or it would
|
|
241
|
+
// crop the wrong ones.
|
|
242
|
+
if (resize != null && fit != ResizeFit.CENTER) {
|
|
243
|
+
size(resize.first, resize.second)
|
|
244
|
+
scale(if (fit == ResizeFit.CONTAIN) Scale.FIT else Scale.FILL)
|
|
245
|
+
// With an explicit size Coil also *upscales* the decode to it,
|
|
246
|
+
// which the default fit doesn't mind (the transformation ends
|
|
247
|
+
// at the box anyway) but `allowUpscale: false` and `contain`
|
|
248
|
+
// must not see. Only subsample; the transformation scales.
|
|
249
|
+
if (!isDefaultFit) precision(Precision.INEXACT)
|
|
218
250
|
}
|
|
219
251
|
}
|
|
220
252
|
.allowHardware(true)
|
|
@@ -222,6 +254,30 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
222
254
|
.build()
|
|
223
255
|
}
|
|
224
256
|
|
|
257
|
+
/**
|
|
258
|
+
* What Coil should load for [url]. Coil itself handles `http(s)://`, `file://`, `content://`
|
|
259
|
+
* and plain absolute paths. The one form it doesn't is a bare resource name — a string with no
|
|
260
|
+
* scheme, like `src_assets_logo` — which is what React Native's `require()` resolves to in a
|
|
261
|
+
* release build (assets are packed into `res/drawable-*`). Resolve that to the resource id
|
|
262
|
+
* here; the density-qualified variant is then picked by the resources system, like `<Image>`
|
|
263
|
+
* does. An unknown name is passed through so Coil reports the failure as an [ErrorResult] (the
|
|
264
|
+
* loaders must not throw while building a request).
|
|
265
|
+
*/
|
|
266
|
+
internal fun requestData(context: Context, url: String): Any {
|
|
267
|
+
if (url.startsWith("/") || url.toUri().scheme != null) return url
|
|
268
|
+
// Tolerate `logo.png`: RN's generated names carry no extension, but nitro-image's
|
|
269
|
+
// `{ resource: 'logo.png' }` convention does.
|
|
270
|
+
val name = url.substringBefore('.')
|
|
271
|
+
for (type in RESOURCE_TYPES) {
|
|
272
|
+
val id = context.resources.getIdentifier(name, type, context.packageName)
|
|
273
|
+
if (id != 0) return id
|
|
274
|
+
}
|
|
275
|
+
return url
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Where React Native puts bundled image assets, then the other image resource types. */
|
|
279
|
+
private val RESOURCE_TYPES = arrayOf("drawable", "mipmap", "raw")
|
|
280
|
+
|
|
225
281
|
/** The decoded bitmap of a successful load. */
|
|
226
282
|
internal fun bitmapOf(result: SuccessResult): Bitmap =
|
|
227
283
|
when (val img = result.image) {
|
|
@@ -15,6 +15,7 @@ import com.margelo.nitro.image.HybridImageSpec
|
|
|
15
15
|
import com.margelo.nitro.image.HybridImageView
|
|
16
16
|
import com.margelo.nitro.image.HybridNitroImageViewSpec
|
|
17
17
|
import kotlin.coroutines.resume
|
|
18
|
+
import kotlinx.coroutines.CancellationException
|
|
18
19
|
import kotlinx.coroutines.CoroutineScope
|
|
19
20
|
import kotlinx.coroutines.Dispatchers
|
|
20
21
|
import kotlinx.coroutines.Job
|
|
@@ -36,6 +37,11 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|
|
36
37
|
* the shared request builder using the display density, so cache keys match an equivalent
|
|
37
38
|
* `loadImage` call. View sizes are already physical pixels on Android, so only blur and corner
|
|
38
39
|
* radii need scaling.
|
|
40
|
+
*
|
|
41
|
+
* The optional `onLoad`/`onError` callbacks report a view's load back to JS. They are per request,
|
|
42
|
+
* not per loader: several views (and a recycled cell re-attaching) each call them, and a view that
|
|
43
|
+
* requests twice reports twice — `HybridImageView` requests both when its `image` is set and when
|
|
44
|
+
* it becomes visible. [loadImage] does not call them — it reports through its promise instead.
|
|
39
45
|
*/
|
|
40
46
|
@DoNotStrip
|
|
41
47
|
@Keep
|
|
@@ -56,7 +62,8 @@ class PipelineImageLoader(
|
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* The point-based [ViewOptions] as pixel-based [Options], resolved against the display [scale]
|
|
59
|
-
* and the target size in pixels.
|
|
65
|
+
* and the target size in pixels. The fit comes from the explicit `resize` when it carries one,
|
|
66
|
+
* else from the top-level `fit`/`allowUpscale`, which apply to the measured size.
|
|
60
67
|
*/
|
|
61
68
|
private fun pixelOptions(scale: Float, resize: ResizeOptions?): Options {
|
|
62
69
|
val cornerRadius =
|
|
@@ -79,7 +86,15 @@ class PipelineImageLoader(
|
|
|
79
86
|
blur = options?.blur?.times(scale),
|
|
80
87
|
cache = options?.cache,
|
|
81
88
|
cornerRadius = cornerRadius,
|
|
82
|
-
resize =
|
|
89
|
+
resize =
|
|
90
|
+
resize?.let {
|
|
91
|
+
ResizeOptions(
|
|
92
|
+
width = it.width,
|
|
93
|
+
height = it.height,
|
|
94
|
+
fit = it.fit ?: options?.fit,
|
|
95
|
+
allowUpscale = it.allowUpscale ?: options?.allowUpscale,
|
|
96
|
+
)
|
|
97
|
+
},
|
|
83
98
|
)
|
|
84
99
|
}
|
|
85
100
|
|
|
@@ -123,10 +138,18 @@ class PipelineImageLoader(
|
|
|
123
138
|
// touch the view.
|
|
124
139
|
if (!isActive) return@launch
|
|
125
140
|
when (result) {
|
|
126
|
-
is SuccessResult ->
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
is SuccessResult -> {
|
|
142
|
+
val bitmap = HybridNitroImagePipeline.bitmapOf(result)
|
|
143
|
+
imageView.setImageBitmap(bitmap)
|
|
144
|
+
report { options?.onLoad?.invoke(bitmap.width.toDouble(), bitmap.height.toDouble()) }
|
|
145
|
+
}
|
|
146
|
+
is ErrorResult -> {
|
|
147
|
+
// Deliberately no URL in the message — signed URLs and query
|
|
148
|
+
// tokens must not leak into consuming apps' Logcat.
|
|
149
|
+
val throwable = result.throwable
|
|
150
|
+
Log.w(TAG, "Failed to load image", throwable)
|
|
151
|
+
report { options?.onError?.invoke(throwable.message ?: throwable.toString()) }
|
|
152
|
+
}
|
|
130
153
|
}
|
|
131
154
|
}
|
|
132
155
|
jobs[forView] = job
|
|
@@ -139,10 +162,26 @@ class PipelineImageLoader(
|
|
|
139
162
|
view.imageView.setImageDrawable(null)
|
|
140
163
|
}
|
|
141
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Reports to JS. Calling a JS callback throws if the callback itself throws, or if the runtime is
|
|
167
|
+
* already gone — a load can land after it was torn down or reloaded — and an exception escaping
|
|
168
|
+
* this coroutine would reach the thread's uncaught handler and take the app with it. A report
|
|
169
|
+
* nobody is left to receive is not worth a crash.
|
|
170
|
+
*/
|
|
171
|
+
private fun report(block: () -> Unit) {
|
|
172
|
+
try {
|
|
173
|
+
block()
|
|
174
|
+
} catch (cancellation: CancellationException) {
|
|
175
|
+
throw cancellation
|
|
176
|
+
} catch (throwable: Throwable) {
|
|
177
|
+
Log.w(TAG, "Image callback failed", throwable)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
142
181
|
/** The view's laid-out size in pixels, suspending until it has one. */
|
|
143
182
|
private suspend fun measuredSize(view: ImageView): ResizeOptions {
|
|
144
183
|
if (view.width > 0 && view.height > 0) {
|
|
145
|
-
return ResizeOptions(view.width.toDouble(), view.height.toDouble())
|
|
184
|
+
return ResizeOptions(view.width.toDouble(), view.height.toDouble(), null, null)
|
|
146
185
|
}
|
|
147
186
|
return suspendCancellableCoroutine { continuation ->
|
|
148
187
|
val listener =
|
|
@@ -162,7 +201,7 @@ class PipelineImageLoader(
|
|
|
162
201
|
val height = bottom - top
|
|
163
202
|
if (width > 0 && height > 0) {
|
|
164
203
|
v.removeOnLayoutChangeListener(this)
|
|
165
|
-
continuation.resume(ResizeOptions(width.toDouble(), height.toDouble()))
|
|
204
|
+
continuation.resume(ResizeOptions(width.toDouble(), height.toDouble(), null, null))
|
|
166
205
|
}
|
|
167
206
|
}
|
|
168
207
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroimagepipeline.transform
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroimagepipeline.ResizeFit
|
|
4
|
+
import kotlin.math.max
|
|
5
|
+
import kotlin.math.min
|
|
6
|
+
import kotlin.math.roundToInt
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The resize geometry both platforms implement — a port of `FitGeometry` in
|
|
10
|
+
* `ios/FitResizeProcessor.swift`; `example/__tests__/fit-geometry.ts` is the copy the harness
|
|
11
|
+
* checks both against. With a `w` × `h` source and a `W` × `H` box:
|
|
12
|
+
* ```
|
|
13
|
+
* cover : sx = sy = max(W/w, H/h)
|
|
14
|
+
* contain : sx = sy = min(W/w, H/h)
|
|
15
|
+
* stretch : sx = W/w ; sy = H/h
|
|
16
|
+
* center : sx = sy = 1
|
|
17
|
+
* if !allowUpscale: sx = min(sx, 1) ; sy = min(sy, 1)
|
|
18
|
+
* scaled = (round(w·sx), round(h·sy))
|
|
19
|
+
* output = (min(W, scaled.w), min(H, scaled.h))
|
|
20
|
+
* origin = trunc((output − scaled) / 2) — a centred, whole-pixel crop
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The source is drawn scaled to `scaled` at `origin` on an `output`-sized canvas. Rounding is
|
|
24
|
+
* half-away-from-zero (`roundToInt()` here, `.rounded()` in Swift — identical for the positive
|
|
25
|
+
* values involved), and the divisions are done in the same order in `Double` on both sides so the
|
|
26
|
+
* two agree to the pixel.
|
|
27
|
+
*/
|
|
28
|
+
internal data class FitGeometry(
|
|
29
|
+
val sourceWidth: Int,
|
|
30
|
+
val sourceHeight: Int,
|
|
31
|
+
val scaledWidth: Int,
|
|
32
|
+
val scaledHeight: Int,
|
|
33
|
+
val outputWidth: Int,
|
|
34
|
+
val outputHeight: Int,
|
|
35
|
+
val originX: Int,
|
|
36
|
+
val originY: Int,
|
|
37
|
+
) {
|
|
38
|
+
/**
|
|
39
|
+
* Whether the source is already the output — nothing to draw. Both sizes are compared with the
|
|
40
|
+
* *source*: `scaled == output` alone also holds for every plain downscale (nothing is cropped),
|
|
41
|
+
* which is not an identity.
|
|
42
|
+
*/
|
|
43
|
+
val isIdentity: Boolean
|
|
44
|
+
get() =
|
|
45
|
+
scaledWidth == sourceWidth &&
|
|
46
|
+
scaledHeight == sourceHeight &&
|
|
47
|
+
outputWidth == sourceWidth &&
|
|
48
|
+
outputHeight == sourceHeight
|
|
49
|
+
|
|
50
|
+
companion object {
|
|
51
|
+
fun of(
|
|
52
|
+
sourceWidth: Int,
|
|
53
|
+
sourceHeight: Int,
|
|
54
|
+
boxWidth: Int,
|
|
55
|
+
boxHeight: Int,
|
|
56
|
+
fit: ResizeFit,
|
|
57
|
+
allowUpscale: Boolean,
|
|
58
|
+
): FitGeometry {
|
|
59
|
+
val source = sourceWidth.toDouble() to sourceHeight.toDouble()
|
|
60
|
+
val box = boxWidth.toDouble() to boxHeight.toDouble()
|
|
61
|
+
var scaleX: Double
|
|
62
|
+
var scaleY: Double
|
|
63
|
+
when (fit) {
|
|
64
|
+
ResizeFit.COVER -> {
|
|
65
|
+
val scale = max(box.first / source.first, box.second / source.second)
|
|
66
|
+
scaleX = scale
|
|
67
|
+
scaleY = scale
|
|
68
|
+
}
|
|
69
|
+
ResizeFit.CONTAIN -> {
|
|
70
|
+
val scale = min(box.first / source.first, box.second / source.second)
|
|
71
|
+
scaleX = scale
|
|
72
|
+
scaleY = scale
|
|
73
|
+
}
|
|
74
|
+
ResizeFit.STRETCH -> {
|
|
75
|
+
scaleX = box.first / source.first
|
|
76
|
+
scaleY = box.second / source.second
|
|
77
|
+
}
|
|
78
|
+
ResizeFit.CENTER -> {
|
|
79
|
+
scaleX = 1.0
|
|
80
|
+
scaleY = 1.0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!allowUpscale) {
|
|
84
|
+
scaleX = min(scaleX, 1.0)
|
|
85
|
+
scaleY = min(scaleY, 1.0)
|
|
86
|
+
}
|
|
87
|
+
val scaledWidth = (source.first * scaleX).roundToInt().coerceAtLeast(1)
|
|
88
|
+
val scaledHeight = (source.second * scaleY).roundToInt().coerceAtLeast(1)
|
|
89
|
+
val outputWidth = min(boxWidth, scaledWidth)
|
|
90
|
+
val outputHeight = min(boxHeight, scaledHeight)
|
|
91
|
+
return FitGeometry(
|
|
92
|
+
sourceWidth = sourceWidth,
|
|
93
|
+
sourceHeight = sourceHeight,
|
|
94
|
+
scaledWidth = scaledWidth,
|
|
95
|
+
scaledHeight = scaledHeight,
|
|
96
|
+
outputWidth = outputWidth,
|
|
97
|
+
outputHeight = outputHeight,
|
|
98
|
+
// Integer division truncates toward zero, like `.rounded(.towardZero)` on iOS.
|
|
99
|
+
originX = (outputWidth - scaledWidth) / 2,
|
|
100
|
+
originY = (outputHeight - scaledHeight) / 2,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|