Skip to content

Commit

Permalink
feature: Use cache for images from network
Browse files Browse the repository at this point in the history
  • Loading branch information
retyui committed Apr 12, 2024
1 parent 9813b2c commit 13afa2c
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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<CloseableReference<PooledByteBuffer>> =
getImagePipeline().fetchEncodedImage(imageRequest, getCallerContext())

try {
val ref: CloseableReference<PooledByteBuffer>? =
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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")
}
Expand Down Expand Up @@ -196,7 +263,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
y: Int,
width: Int,
height: Int,
headers: HashMap<String, Any?>?
headers: ReadableMap?
): Bitmap? {
return openBitmapInputStream(uri, headers)?.use {
// Efficiently crops image without loading full resolution into memory
Expand Down Expand Up @@ -258,7 +325,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
rectHeight: Int,
outputWidth: Int,
outputHeight: Int,
headers: HashMap<String, Any?>?
headers: ReadableMap?
): Bitmap? {
Assertions.assertNotNull(outOptions)

Expand Down Expand Up @@ -337,20 +404,14 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
}
}

private fun openBitmapInputStream(uri: String, headers: HashMap<String, Any?>?): 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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 13afa2c

Please sign in to comment.