Skip to content

Commit 07a4eb9

Browse files
authored
Merge pull request #213 from callstack/chore/bump-min-rn-0.77
feat(android): share Fresco disk cache with Image.prefetch, new example app
2 parents a942f5d + b67b021 commit 07a4eb9

65 files changed

Lines changed: 4268 additions & 4878 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Image Editor Native module for React Native.
1010

1111
Originally extracted from React Native [`issue#23313`](https://github.com/facebook/react-native/issues/23313) and maintained by the community.
1212

13+
<img src="./example/screenshots/editor.jpg" width="280" alt="Example app: selecting a crop area on an image" />
14+
15+
_Screenshot from the [example app](/example). The crop UI is example code, not part of this library, which provides only `ImageEditor.cropImage()`._
16+
1317
## Getting started
1418

1519
### Requirements
@@ -18,7 +22,7 @@ Starting from v5, this library supports only the [New Architecture](https://reac
1822

1923
| Library version | React Native version | Architecture |
2024
| --------------- | -------------------- | -------------- |
21-
| 5.x | >= 0.74 | New only |
25+
| 5.x | >= 0.77 | New only |
2226
| 4.x | >= 0.57 | Old and New |
2327

2428
> [!NOTE]
@@ -86,7 +90,12 @@ ImageEditor.cropImage(uri, cropData).then((result) => {
8690
| `type` | `string` | The MIME type of the image (`'image/jpeg'`, `'image/png'`, `'image/webp'`) |
8791
| `base64`<br>_(optional)_ | `string` | The base64-encoded image data example: `'/9j/4AAQSkZJRgABAQAAAQABAAD'`<br>if you need data URI as the `source` for an `Image` element for example, you can use `data:${type};base64,${base64}` |
8892

89-
For more advanced usage check our [example app](/example/src/App.tsx).
93+
## Example app
94+
95+
The [example app](/example) shows every `cropImage` option, plus a cache lab for `Image.prefetch` / `<Image>` cache reuse. See its [README](/example/README.md) for how to run it.
96+
97+
> [!NOTE]
98+
> The example UI (crop box, controls, result view) is **not part of this library**. It's example code built with plain React Native. The library itself only provides `ImageEditor.cropImage()`.
9099
91100
<!-- badges -->
92101

‎android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt‎

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ import android.os.Build
2020
import android.provider.MediaStore
2121
import android.text.TextUtils
2222
import android.util.Base64
23+
import android.util.Log
2324
import androidx.exifinterface.media.ExifInterface
2425
import com.facebook.common.logging.FLog
26+
import com.facebook.drawee.backends.pipeline.Fresco
27+
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
28+
import com.facebook.imagepipeline.request.ImageRequest
2529
import com.facebook.infer.annotation.Assertions
2630
import com.facebook.react.bridge.Arguments
2731
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
@@ -353,13 +357,59 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
353357
} else if (isLocalUri(uri)) {
354358
reactContext.contentResolver.openInputStream(Uri.parse(uri))
355359
} else {
356-
val connection = URL(uri).openConnection()
357-
headers?.forEach { (key, value) ->
358-
if (value is String) {
359-
connection.setRequestProperty(key, value)
360-
}
360+
openFrescoCachedInputStream(uri, headers)?.let {
361+
return it
362+
}
363+
openRemoteInputStream(uri, headers)
364+
}
365+
}
366+
367+
private fun openRemoteInputStream(uri: String, headers: HashMap<String, Any>?): InputStream {
368+
val connection = URL(uri).openConnection()
369+
headers?.forEach { (key, value) ->
370+
if (value is String) {
371+
connection.setRequestProperty(key, value)
361372
}
362-
connection.getInputStream()
373+
}
374+
return connection.getInputStream()
375+
}
376+
377+
/**
378+
* Reads the image through Fresco's disk cache, the one `Image.prefetch(url)` and `<Image>` use.
379+
* On cache miss the image is downloaded into the cache, so a later `<Image>` with the same URL
380+
* loads it from disk. Returns null if the cache can't be used.
381+
*/
382+
private fun openFrescoCachedInputStream(
383+
uri: String,
384+
headers: HashMap<String, Any>?
385+
): InputStream? {
386+
val (fileCache, cacheKey) =
387+
try {
388+
if (!Fresco.hasBeenInitialized()) return null
389+
val request = ImageRequest.fromUri(uri) ?: return null
390+
Pair(
391+
Fresco.getImagePipelineFactory().diskCachesStoreSupplier.get().mainFileCache,
392+
DefaultCacheKeyFactory.getInstance().getEncodedCacheKey(request, null)
393+
)
394+
} catch (e: Exception) {
395+
FLog.w(ReactConstants.TAG, "ImageEditor: Fresco disk cache unavailable", e)
396+
return null
397+
}
398+
399+
fileCache.getResource(cacheKey)?.let {
400+
Log.i("RNCImageEditor", "Fresco disk cache hit for $uri")
401+
return it.openStream()
402+
}
403+
404+
// Network errors propagate to the caller as before; only cache failures fall back
405+
val input = openRemoteInputStream(uri, headers)
406+
return try {
407+
val resource = input.use { fileCache.insert(cacheKey) { out -> it.copyTo(out) } }
408+
Log.i("RNCImageEditor", "Fresco disk cache miss, stored $uri")
409+
resource?.openStream()
410+
} catch (e: Exception) {
411+
FLog.w(ReactConstants.TAG, "ImageEditor: Failed to store $uri in Fresco disk cache", e)
412+
null
363413
}
364414
}
365415

‎example/.gitignore‎

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,75 @@
1-
*.binlog
2-
*.hprof
3-
*.xcworkspace/
4-
*.zip
1+
# OSX
2+
#
53
.DS_Store
6-
.bundle/*
7-
!.bundle/config
8-
.gradle/
9-
.idea/
10-
.vs/
11-
.watchman-*
12-
.xcode.env
13-
android/app/
14-
android/build/
15-
dist/*
16-
!dist/.gitignore
17-
ios/DerivedData/
18-
ios/Pods/
19-
ios/build/
4+
5+
# Xcode
6+
#
7+
build/
8+
*.pbxuser
9+
!default.pbxuser
10+
*.mode1v3
11+
!default.mode1v3
12+
*.mode2v3
13+
!default.mode2v3
14+
*.perspectivev3
15+
!default.perspectivev3
16+
xcuserdata
17+
*.xccheckout
18+
*.moved-aside
19+
DerivedData
20+
*.hmap
21+
*.ipa
22+
*.xcuserstate
23+
**/.xcode.env.local
24+
25+
# Android/IntelliJ
26+
#
27+
build/
28+
.idea
29+
.gradle
2030
local.properties
21-
macos/DerivedData/
22-
macos/Pods/
23-
macos/build/
31+
*.iml
32+
*.hprof
33+
.cxx/
34+
*.keystore
35+
!debug.keystore
36+
.kotlin/
37+
38+
# node.js
39+
#
2440
node_modules/
41+
npm-debug.log
42+
yarn-error.log
43+
44+
# fastlane
45+
#
46+
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47+
# screenshots whenever they are needed.
48+
# For more information about the recommended setup visit:
49+
# https://docs.fastlane.tools/best-practices/source-control/
50+
51+
**/fastlane/report.xml
52+
**/fastlane/Preview.html
53+
**/fastlane/screenshots
54+
**/fastlane/test_output
55+
56+
# Bundle artifact
57+
*.jsbundle
58+
59+
# Ruby / CocoaPods
60+
**/Pods/
61+
/vendor/bundle/
62+
63+
# Temporary files created by Metro to check the health of the file watcher
64+
.metro-health-check*
65+
66+
# testing
67+
/coverage
68+
69+
# Yarn
70+
.yarn/*
71+
!.yarn/patches
72+
!.yarn/plugins
73+
!.yarn/releases
74+
!.yarn/sdks
75+
!.yarn/versions

‎example/Gemfile.lock‎

Lines changed: 0 additions & 114 deletions
This file was deleted.

‎example/README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Image Editor example app
2+
3+
A playground for [`@react-native-community/image-editor`](../README.md) that exercises every option of `ImageEditor.cropImage`.
4+
5+
> [!NOTE]
6+
> The UI in this app (crop box, controls, result inspector, cache lab) is **not part of the library**. The library only provides `ImageEditor.cropImage()`. The UI is plain React Native code you're free to copy.
7+
8+
| Editor | Output options | Result | Cache lab |
9+
| :---: | :---: | :---: | :---: |
10+
| <img src="./screenshots/editor.jpg" width="200" alt="Crop area with a draggable crop box and aspect ratio presets" /> | <img src="./screenshots/options.jpg" width="200" alt="Format, quality, base64 and displaySize options" /> | <img src="./screenshots/result.jpg" width="200" alt="Cropped image with its dimensions, type, size and base64" /> | <img src="./screenshots/cache-lab.jpg" width="200" alt="Cache lab log showing prefetch followed by a fast crop" /> |
11+
12+
## What it covers
13+
14+
- **Sources**: a remote URL, a remote URL with custom `headers`, a 4000×3000 image, a portrait image, the previous result as a `file://` URI, and the previous result's base64 as a `data:` URI.
15+
- **Crop area**: drag the box to move it and its corners to resize it, with aspect ratio presets (Free, 1:1, 4:3, 3:4, 16:9). Offset and size are shown in the original image's pixels, which is what `cropImage` expects.
16+
- **Output options**: `format`, `quality`, `includeBase64`, `displaySize` and `resizeMode`.
17+
- **Result**: preview, dimensions, MIME type, file size, path, base64 preview and the time `cropImage` took.
18+
- **Cache lab**: `Image.queryCache`, `Image.prefetch`, cropping a URL without displaying it, rendering it with `<Image>`, and an invalid URL to check that the promise rejects. Use it to check that `cropImage` reuses the image already cached by `Image.prefetch` / `<Image>`.
19+
20+
Platform differences are labeled in the UI: iOS writes only JPEG or PNG (`webp` falls back to JPEG), and Android ignores `resizeMode`.
21+
22+
## Running
23+
24+
The app links the library from the repository root (`"@react-native-community/image-editor": "link:.."`), so changes to `../src`, `../ios` and `../android` are picked up directly. Metro watches the root folder (see [`metro.config.js`](./metro.config.js)).
25+
26+
```sh
27+
cd example
28+
yarn install
29+
```
30+
31+
### iOS
32+
33+
```sh
34+
cd ios && bundle install && bundle exec pod install && cd ..
35+
yarn ios
36+
```
37+
38+
### Android
39+
40+
```sh
41+
yarn android
42+
```
43+
44+
Metro starts automatically. To start it yourself, run `yarn start`.
45+
46+
## Code map
47+
48+
| File | What it does |
49+
| --- | --- |
50+
| [`src/App.tsx`](./src/App.tsx) | Screen state and the `ImageEditor.cropImage` call |
51+
| [`src/components/CropArea.tsx`](./src/components/CropArea.tsx) | Image with a draggable, resizable crop box (`PanResponder`) |
52+
| [`src/components/ResultCard.tsx`](./src/components/ResultCard.tsx) | Shows the `CropResult` |
53+
| [`src/components/CacheLab.tsx`](./src/components/CacheLab.tsx) | Prefetch / cache checks |
54+
| [`src/components/ui.tsx`](./src/components/ui.tsx) | Small UI kit (section, segmented control, toggle, stepper, button) |
55+
| [`src/sources.ts`](./src/sources.ts) | Sample image URLs |

0 commit comments

Comments
 (0)