Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Use cache for images from network #147

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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
Loading