diff --git a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt index 8fd6014..e76a014 100644 --- a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt +++ b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt @@ -22,6 +22,15 @@ import android.text.TextUtils import android.util.Base64 import androidx.exifinterface.media.ExifInterface import com.facebook.common.logging.FLog +import com.facebook.common.memory.PooledByteBuffer +import com.facebook.common.memory.PooledByteBufferInputStream +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.datasource.DataSources +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.core.ImagePipeline +import com.facebook.imagepipeline.request.ImageRequest +import com.facebook.imagepipeline.request.ImageRequestBuilder import com.facebook.infer.annotation.Assertions import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.JSApplicationIllegalArgumentException @@ -31,6 +40,9 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.WritableMap import com.facebook.react.common.ReactConstants +import com.facebook.react.modules.fresco.ReactNetworkImageRequest +import com.facebook.react.views.image.ReactCallerContextFactory +import com.facebook.react.views.imagehelper.ImageSource import java.io.ByteArrayInputStream import java.io.File import java.io.FileInputStream @@ -51,7 +63,13 @@ object MimeType { const val WEBP = "image/webp" } -class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { +class ImageEditorModuleImpl( + private val reactContext: ReactApplicationContext, + private val callerContext: Any?, + private val callerContextFactory: ReactCallerContextFactory?, + private val imagePipeline: ImagePipeline? +) { + private val moduleCoroutineScope = CoroutineScope(Dispatchers.Default) init { @@ -65,6 +83,56 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { cleanTask() } + private fun getCallerContext(): Any? { + return callerContextFactory?.getOrCreateCallerContext("", "") ?: callerContext + } + + private fun getImagePipeline(): ImagePipeline { + return imagePipeline ?: Fresco.getImagePipeline() + } + + private fun fetchAndCacheImage( + uri: String, + headers: ReadableMap?, + ): InputStream? { + try { + val source = ImageSource(reactContext, uri) + val imageRequest: ImageRequest = + if (headers != null) { + val imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(source.uri) + ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers) + } else ImageRequestBuilder.newBuilderWithSource(source.uri).build() + + val dataSource: DataSource> = + getImagePipeline().fetchEncodedImage(imageRequest, getCallerContext()) + + try { + val ref: CloseableReference? = + DataSources.waitForFinalResult(dataSource) + if (ref != null) { + try { + val result = ref.get() + return PooledByteBufferInputStream(result) + } finally { + CloseableReference.closeSafely(ref) + } + } + return null + } finally { + dataSource.close() + } + } catch (e: Exception) { + // Fallback to default network requests + val connection = URL(uri).openConnection() + headers?.toHashMap()?.forEach { (key, value) -> + if (value is kotlin.String) { + connection.setRequestProperty(key, value) + } + } + return connection.getInputStream() + } + } + /** * Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped * image files. This is run when the module is invalidated (i.e. app is shutting down) and when @@ -102,7 +170,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { fun cropImage(uri: String?, options: ReadableMap, promise: Promise) { val headers = if (options.hasKey("headers") && options.getType("headers") == ReadableType.Map) - options.getMap("headers")?.toHashMap() + options.getMap("headers") else null val format = if (options.hasKey("format")) options.getString("format") else null val offset = if (options.hasKey("offset")) options.getMap("offset") else null @@ -148,7 +216,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { // memory val hasTargetSize = targetWidth > 0 && targetHeight > 0 val cropped: Bitmap? = - if (hasTargetSize) { + if (hasTargetSize) cropAndResizeTask( outOptions, uri, @@ -160,9 +228,8 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { targetHeight, headers ) - } else { - cropTask(outOptions, uri, x, y, width, height, headers) - } + else cropTask(outOptions, uri, x, y, width, height, headers) + if (cropped == null) { throw IOException("Cannot decode bitmap: $uri") } @@ -196,7 +263,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { y: Int, width: Int, height: Int, - headers: HashMap? + headers: ReadableMap? ): Bitmap? { return openBitmapInputStream(uri, headers)?.use { // Efficiently crops image without loading full resolution into memory @@ -258,7 +325,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { rectHeight: Int, outputWidth: Int, outputHeight: Int, - headers: HashMap? + headers: ReadableMap? ): Bitmap? { Assertions.assertNotNull(outOptions) @@ -337,20 +404,14 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { } } - private fun openBitmapInputStream(uri: String, headers: HashMap?): InputStream? { + private fun openBitmapInputStream(uri: String, headers: ReadableMap?): InputStream? { return if (uri.startsWith("data:")) { val src = uri.substring(uri.indexOf(",") + 1) ByteArrayInputStream(Base64.decode(src, Base64.DEFAULT)) } 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) - } - } - connection.getInputStream() + fetchAndCacheImage(uri, headers) } } diff --git a/android/src/newarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt b/android/src/newarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt index e42d86b..c276ddf 100644 --- a/android/src/newarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt +++ b/android/src/newarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt @@ -1,17 +1,30 @@ package com.reactnativecommunity.imageeditor +import com.facebook.imagepipeline.core.ImagePipeline import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.views.image.ReactCallerContextFactory @ReactModule(name = ImageEditorModule.NAME) -class ImageEditorModule(reactContext: ReactApplicationContext) : - NativeRNCImageEditorSpec(reactContext) { +class ImageEditorModule : NativeRNCImageEditorSpec { private val moduleImpl: ImageEditorModuleImpl - init { - moduleImpl = ImageEditorModuleImpl(reactContext) + constructor(reactContext: ReactApplicationContext) : super(reactContext) { + moduleImpl = ImageEditorModuleImpl(reactContext, this, null, null) + } + + constructor(reactContext: ReactApplicationContext, callerContext: Any?) : super(reactContext) { + moduleImpl = ImageEditorModuleImpl(reactContext, callerContext, null, null) + } + + constructor( + reactContext: ReactApplicationContext, + imagePipeline: ImagePipeline?, + callerContextFactory: ReactCallerContextFactory? + ) : super(reactContext) { + moduleImpl = ImageEditorModuleImpl(reactContext, null, callerContextFactory, imagePipeline) } override fun getName(): String { diff --git a/android/src/oldarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt b/android/src/oldarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt index e27e4d8..561b942 100644 --- a/android/src/oldarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt +++ b/android/src/oldarch/com/reactnativecommunity/imageeditor/ImageEditorModule.kt @@ -1,19 +1,32 @@ package com.reactnativecommunity.imageeditor +import com.facebook.imagepipeline.core.ImagePipeline import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.views.image.ReactCallerContextFactory @ReactModule(name = ImageEditorModule.NAME) -class ImageEditorModule(reactContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactContext) { +class ImageEditorModule : ReactContextBaseJavaModule { private val moduleImpl: ImageEditorModuleImpl - init { - moduleImpl = ImageEditorModuleImpl(reactContext) + constructor(reactContext: ReactApplicationContext) : super(reactContext) { + moduleImpl = ImageEditorModuleImpl(reactContext, this, null, null) + } + + constructor(reactContext: ReactApplicationContext, callerContext: Any?) : super(reactContext) { + moduleImpl = ImageEditorModuleImpl(reactContext, callerContext, null, null) + } + + constructor( + reactContext: ReactApplicationContext, + imagePipeline: ImagePipeline?, + callerContextFactory: ReactCallerContextFactory? + ) : super(reactContext) { + moduleImpl = ImageEditorModuleImpl(reactContext, null, callerContextFactory, imagePipeline) } override fun getName(): String {