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

Crop 🪚 #184

Merged
merged 4 commits into from
Dec 11, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ class CropImageEngine : UCropImageEngine {
}
}

class CropEngine(cropOption: UCrop.Options) : CropFileEngine {
private val options: UCrop.Options = cropOption
class CropEngine(cropOption: Options) : CropFileEngine {
private val options: Options = cropOption

override fun onStartCrop(
fragment: Fragment,
srcUri: Uri?,
Expand All @@ -78,6 +79,7 @@ class MediaEditInterceptListener(
val inputUri =
if (PictureMimeType.isContent(currentEditPath)) Uri.parse(currentEditPath)
else Uri.fromFile(File(currentEditPath))

val destinationUri = Uri.fromFile(
File(outputCropPath, DateUtils.getCreateFileName("CROP_") + ".jpeg")
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.margelo.nitro.multipleimagepicker

import com.margelo.nitro.NitroModules
import com.margelo.nitro.multipleimagepicker.HybridMultipleImagePickerSpec
import com.margelo.nitro.multipleimagepicker.NitroConfig
import com.margelo.nitro.multipleimagepicker.Result


class MultipleImagePicker: HybridMultipleImagePickerSpec() {
class MultipleImagePicker : HybridMultipleImagePickerSpec() {
override val memorySize: Long
get() = 5

Expand All @@ -20,7 +17,15 @@ class MultipleImagePicker: HybridMultipleImagePickerSpec() {
pickerModule.openPicker(config, resolved, rejected)
}

override fun openCrop(
image: String,
config: NitroCropConfig,
resolved: (result: CropResult) -> Unit,
rejected: (reject: Double) -> Unit
) {

pickerModule.openCrop(image, config, resolved, rejected)
}


}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.margelo.nitro.multipleimagepicker

import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.ActivityEventListener
import com.facebook.react.bridge.BaseActivityEventListener
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
Expand All @@ -23,8 +29,16 @@ import com.luck.picture.lib.style.PictureSelectorStyle
import com.luck.picture.lib.style.PictureWindowAnimationStyle
import com.luck.picture.lib.style.SelectMainStyle
import com.luck.picture.lib.style.TitleBarStyle
import com.luck.picture.lib.utils.DateUtils
import com.luck.picture.lib.utils.DensityUtil
import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCrop.Options
import com.yalantis.ucrop.UCrop.REQUEST_CROP
import com.yalantis.ucrop.model.AspectRatio
import java.io.File
import java.net.HttpURLConnection
import java.net.URL


class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
ReactContextBaseJavaModule(reactContext), IApp {
Expand Down Expand Up @@ -65,7 +79,6 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
else -> SelectMimeType.ofAll()
}


val maxSelect = config.maxSelect?.toInt() ?: 20
val maxVideo = config.maxVideo?.toInt() ?: 20
val isPreview = config.isPreview ?: true
Expand All @@ -82,13 +95,10 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :

val isCrop = config.crop != null

PictureSelector.create(activity)
.openGallery(chooseMode)
.setImageEngine(imageEngine)
.setSelectedData(dataList)
.setSelectorUIStyle(style).apply {
PictureSelector.create(activity).openGallery(chooseMode).setImageEngine(imageEngine)
.setSelectedData(dataList).setSelectorUIStyle(style).apply {
if (isCrop) {
setCropOption()
setCropOption(config.crop)
// Disabled force crop engine for multiple
if (!isMultiple) setCropEngine(CropEngine(cropOption))
else setEditMediaInterceptListener(setEditMediaEvent())
Expand All @@ -113,28 +123,18 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
if (videoQuality != null && videoQuality != 1.0) {
setVideoQuality(if (videoQuality > 0.5) 1 else 0)
}
}
.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3)
.setMaxSelectNum(maxSelect)
.isDirectReturnSingle(true)
.isSelectZoomAnim(true)
.isPageStrategy(true, 50)
}.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3).setMaxSelectNum(maxSelect)
.isDirectReturnSingle(true).isSelectZoomAnim(true).isPageStrategy(true, 50)
.isWithSelectVideoImage(true)
.setMaxVideoSelectNum(if (maxVideo != 20) maxVideo else maxSelect)
.isMaxSelectEnabledMask(true)
.isAutoVideoPlay(true)
.isFastSlidingSelect(allowSwipeToSelect)
.isPageSyncAlbumCount(true)
.isMaxSelectEnabledMask(true).isAutoVideoPlay(true)
.isFastSlidingSelect(allowSwipeToSelect).isPageSyncAlbumCount(true)
// isPreview
.isPreviewImage(isPreview)
.isPreviewVideo(isPreview)
.isPreviewImage(isPreview).isPreviewVideo(isPreview)
//
.isDisplayCamera(config.allowedCamera ?: true)
.isDisplayTimeAxis(true)
.setSelectionMode(selectMode)
.isOriginalControl(config.isHiddenOriginalButton == false)
.setLanguage(getLanguage())
.isPreviewFullScreenMode(true)
.isDisplayCamera(config.allowedCamera ?: true).isDisplayTimeAxis(true)
.setSelectionMode(selectMode).isOriginalControl(config.isHiddenOriginalButton == false)
.setLanguage(getLanguage()).isPreviewFullScreenMode(true)
.forResult(object : OnResultCallbackListener<LocalMedia?> {
override fun onResult(localMedia: ArrayList<LocalMedia?>?) {
var data: Array<Result> = arrayOf()
Expand All @@ -161,6 +161,112 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
})
}

@ReactMethod
fun openCrop(
image: String,
options: NitroCropConfig,
resolved: (result: CropResult) -> Unit,
rejected: (reject: Double) -> Unit
) {


fun isImage(uri: Uri, contentResolver: ContentResolver): Boolean {
val mimeType: String? = contentResolver.getType(uri)
return mimeType?.startsWith("image/") == true
}

val uri = Uri.parse(image)
val isImageFile = isImage(uri, appContext.contentResolver)

if (!isImageFile) return rejected(0.0)

cropOption = Options()

setCropOption(
PickerCropConfig(
circle = options.circle,
ratio = options.ratio,
defaultRatio = options.defaultRatio,
freeStyle = options.freeStyle
)
)

try {
val uri = when {
// image network
image.startsWith("http://") || image.startsWith("https://") -> {
// Handle remote URL
val url = URL(image)
val connection = url.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()

val inputStream = connection.inputStream
// Create a temp file to store the image
val file = File(appContext.cacheDir, "CROP_")
file.outputStream().use { output ->
inputStream.copyTo(output)
}

Uri.fromFile(file)
}


else -> {
Uri.parse(image)
}
}


val destinationUri = Uri.fromFile(
File(getSandboxPath(appContext), DateUtils.getCreateFileName("CROP_") + ".jpeg")
)

val uCrop = UCrop.of<Any>(uri, destinationUri).withOptions(cropOption)

// set engine
uCrop.setImageEngine(CropImageEngine())
// start edit

val cropActivityEventListener = object : BaseActivityEventListener() {
override fun onActivityResult(
activity: Activity,
requestCode: Int,
resultCode: Int,
data: Intent?
) {
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CROP) {
val resultUri = UCrop.getOutput(data!!)
val width = UCrop.getOutputImageWidth(data).toDouble()
val height = UCrop.getOutputImageHeight(data).toDouble()

resultUri?.let {
val result = CropResult(
path = it.toString(),
width,
height,
)
resolved(result)
}
} else if (resultCode == UCrop.RESULT_ERROR) {
val cropError = UCrop.getError(data!!)
rejected(0.0)
}

// Remove listener after getting result
reactApplicationContext.removeActivityEventListener(this)
}
}

// Add listener before starting UCrop
reactApplicationContext.addActivityEventListener(cropActivityEventListener)

currentActivity?.let { uCrop.start(it, REQUEST_CROP) }
} catch (e: Exception) {
rejected(0.0)
}
}

private fun getLanguage(): Int {
return when (config.language) {
Language.VI -> LanguageConfig.VIETNAM // -> 🇻🇳 My country. Yeahhh
Expand All @@ -177,12 +283,10 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
}
}

private fun setCropOption() {
// val mainStyle: SelectMainStyle = style.selectMainStyle

private fun setCropOption(config: PickerCropConfig?) {
cropOption.setShowCropFrame(true)
cropOption.setShowCropGrid(true)
cropOption.setCircleDimmedLayer(config.crop?.circle ?: false)
cropOption.setCircleDimmedLayer(config?.circle ?: false)
cropOption.setCropOutputPathDir(getSandboxPath(appContext))
cropOption.isCropDragSmoothToCenter(true)
cropOption.isForbidSkipMultipleCrop(true)
Expand All @@ -191,8 +295,48 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
cropOption.setStatusBarColor(Color.WHITE)
cropOption.isDarkStatusBarBlack(true)
cropOption.isDragCropImages(true)
cropOption.setFreeStyleCropEnabled(true)
cropOption.setFreeStyleCropEnabled(config?.freeStyle ?: true)
cropOption.setSkipCropMimeType(*getNotSupportCrop())


val ratioCount = config?.ratio?.size ?: 0

if (config?.defaultRatio != null || ratioCount > 0) {

var ratioList = arrayOf(AspectRatio("Original", 0f, 0f))

if (ratioCount > 0) {
config?.ratio?.take(4)?.toTypedArray()?.forEach { item ->
ratioList += AspectRatio(
item.title, item.width.toFloat(), item.height.toFloat()
)
}
}

// Add default Aspects
ratioList += arrayOf(
AspectRatio(null, 1f, 1f),
AspectRatio(null, 16f, 9f),
AspectRatio(null, 4f, 3f),
AspectRatio(null, 3f, 2f)
)

config?.defaultRatio?.let {
val defaultRatio = AspectRatio(it.title, it.width.toFloat(), it.height.toFloat())
ratioList = arrayOf(defaultRatio) + ratioList

}

cropOption.apply {

setAspectRatioOptions(
0,
*ratioList.take(5).toTypedArray()
)

}

}
}


Expand Down
47 changes: 45 additions & 2 deletions docs/docs/CONFIG.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,59 @@ Maximum number of videos allowed.
- **Required**: No
- **Platform**: iOS, Android

### `crop`
## Crop 🪚

Configuration for image cropping functionality.

- **Type**: object
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android

### `circle`

Enable circular crop mask.

- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android

### `ratio`

Aspect ratios for cropping.
Android: Maximum: 4 items

- **Type**: `array`
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `circle`: boolean - Enable circular crop mask
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
- `width`: number - Width value for the aspect ratio
- `height`: number - Height value for the aspect ratio

### `defaultRatio`

Default ratio to be selected when opening the crop interface.

- **Type**: `object`
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
- `width`: number - Width value for the aspect ratio
- `height`: number - Height value for the aspect ratio

### `freeStyle`

Enable free style cropping.

- **Type**: `boolean`
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android

---

Expand Down
Loading