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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Image Editor Native module for React Native.

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

<img src="./example/screenshots/editor.jpg" width="280" alt="Example app: selecting a crop area on an image" />

_Screenshot from the [example app](/example). The crop UI is example code, not part of this library, which provides only `ImageEditor.cropImage()`._

## Getting started

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

| Library version | React Native version | Architecture |
| --------------- | -------------------- | -------------- |
| 5.x | >= 0.74 | New only |
| 5.x | >= 0.77 | New only |
| 4.x | >= 0.57 | Old and New |

> [!NOTE]
Expand Down Expand Up @@ -86,7 +90,12 @@ ImageEditor.cropImage(uri, cropData).then((result) => {
| `type` | `string` | The MIME type of the image (`'image/jpeg'`, `'image/png'`, `'image/webp'`) |
| `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}` |

For more advanced usage check our [example app](/example/src/App.tsx).
## Example app

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.

> [!NOTE]
> 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()`.

<!-- badges -->

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ import android.os.Build
import android.provider.MediaStore
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.facebook.common.logging.FLog
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.infer.annotation.Assertions
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
Expand Down Expand Up @@ -353,13 +357,59 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
} else if (isLocalUri(uri)) {
reactContext.contentResolver.openInputStream(Uri.parse(uri))
} else {
val connection = URL(uri).openConnection()
headers?.forEach { (key, value) ->
if (value is String) {
connection.setRequestProperty(key, value)
}
openFrescoCachedInputStream(uri, headers)?.let {
return it
}
openRemoteInputStream(uri, headers)
}
}

private fun openRemoteInputStream(uri: String, headers: HashMap<String, Any>?): InputStream {
val connection = URL(uri).openConnection()
headers?.forEach { (key, value) ->
if (value is String) {
connection.setRequestProperty(key, value)
}
connection.getInputStream()
}
return connection.getInputStream()
}

/**
* Reads the image through Fresco's disk cache, the one `Image.prefetch(url)` and `<Image>` use.
* On cache miss the image is downloaded into the cache, so a later `<Image>` with the same URL
* loads it from disk. Returns null if the cache can't be used.
*/
private fun openFrescoCachedInputStream(
uri: String,
headers: HashMap<String, Any>?
): InputStream? {
val (fileCache, cacheKey) =
try {
if (!Fresco.hasBeenInitialized()) return null
val request = ImageRequest.fromUri(uri) ?: return null
Pair(
Fresco.getImagePipelineFactory().diskCachesStoreSupplier.get().mainFileCache,
DefaultCacheKeyFactory.getInstance().getEncodedCacheKey(request, null)
)
} catch (e: Exception) {
FLog.w(ReactConstants.TAG, "ImageEditor: Fresco disk cache unavailable", e)
return null
}

fileCache.getResource(cacheKey)?.let {
Log.i("RNCImageEditor", "Fresco disk cache hit for $uri")
return it.openStream()
}

// Network errors propagate to the caller as before; only cache failures fall back
val input = openRemoteInputStream(uri, headers)
return try {
val resource = input.use { fileCache.insert(cacheKey) { out -> it.copyTo(out) } }
Log.i("RNCImageEditor", "Fresco disk cache miss, stored $uri")
resource?.openStream()
} catch (e: Exception) {
FLog.w(ReactConstants.TAG, "ImageEditor: Failed to store $uri in Fresco disk cache", e)
null
}
}

Expand Down
93 changes: 72 additions & 21 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,75 @@
*.binlog
*.hprof
*.xcworkspace/
*.zip
# OSX
#
.DS_Store
.bundle/*
!.bundle/config
.gradle/
.idea/
.vs/
.watchman-*
.xcode.env
android/app/
android/build/
dist/*
!dist/.gitignore
ios/DerivedData/
ios/Pods/
ios/build/

# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
**/.xcode.env.local

# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
macos/DerivedData/
macos/Pods/
macos/build/
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore
.kotlin/

# node.js
#
node_modules/
npm-debug.log
yarn-error.log

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/

**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output

# Bundle artifact
*.jsbundle

# Ruby / CocoaPods
**/Pods/
/vendor/bundle/

# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

# testing
/coverage

# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
114 changes: 0 additions & 114 deletions example/Gemfile.lock

This file was deleted.

55 changes: 55 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Image Editor example app

A playground for [`@react-native-community/image-editor`](../README.md) that exercises every option of `ImageEditor.cropImage`.

> [!NOTE]
> 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.

| Editor | Output options | Result | Cache lab |
| :---: | :---: | :---: | :---: |
| <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" /> |

## What it covers

- **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.
- **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.
- **Output options**: `format`, `quality`, `includeBase64`, `displaySize` and `resizeMode`.
- **Result**: preview, dimensions, MIME type, file size, path, base64 preview and the time `cropImage` took.
- **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>`.

Platform differences are labeled in the UI: iOS writes only JPEG or PNG (`webp` falls back to JPEG), and Android ignores `resizeMode`.

## Running

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)).

```sh
cd example
yarn install
```

### iOS

```sh
cd ios && bundle install && bundle exec pod install && cd ..
yarn ios
```

### Android

```sh
yarn android
```

Metro starts automatically. To start it yourself, run `yarn start`.

## Code map

| File | What it does |
| --- | --- |
| [`src/App.tsx`](./src/App.tsx) | Screen state and the `ImageEditor.cropImage` call |
| [`src/components/CropArea.tsx`](./src/components/CropArea.tsx) | Image with a draggable, resizable crop box (`PanResponder`) |
| [`src/components/ResultCard.tsx`](./src/components/ResultCard.tsx) | Shows the `CropResult` |
| [`src/components/CacheLab.tsx`](./src/components/CacheLab.tsx) | Prefetch / cache checks |
| [`src/components/ui.tsx`](./src/components/ui.tsx) | Small UI kit (section, segmented control, toggle, stepper, button) |
| [`src/sources.ts`](./src/sources.ts) | Sample image URLs |
Loading
Loading