Skip to content

Commit

Permalink
Merge pull request #29 from TEAM-PREAT/feature/add-image-resizing-option
Browse files Browse the repository at this point in the history
[feature/add-image-resizing-option] Add Image Resizing �Options to Image Picker
  • Loading branch information
onseok authored Dec 27, 2023
2 parents 504453c + 0bdef29 commit 6e84271
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 14 deletions.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ First, define the version in `libs.versions.toml`:

```toml
[versions]
peekaboo = "0.3.0"
peekaboo = "0.3.1"

[libraries]
peekaboo-ui = { module = "io.github.team-preat:peekaboo-ui", version.ref = "peekaboo" }
Expand Down Expand Up @@ -203,6 +203,54 @@ Button(

<br/>

## Image Resizing Options
`peekaboo` offers customizable resizing options for both single and multiple image selections. <br/>
This feature allows you to resize the selected images to specific dimensions, optimizing them for your application's requirements and enhancing performance.

- The default resizing dimensions are set to `800 x 800` pixels.
- You can customize the resizing dimensions according to your needs.

### Usage
Set the `resizeOptions` parameter in `rememberImagePickerLauncher` with your desired dimensions:

```kotlin
val resizeOptions = ResizeOptions(width = 1200, height = 1200) // Custom dimensions
```

#### Single Image Selection with Resizing
```kotlin
val singleImagePicker = rememberImagePickerLauncher(
selectionMode = SelectionMode.Single,
scope = rememberCoroutineScope(),
resizeOptions = resizeOptions,
onResult = { byteArrays ->
byteArrays.firstOrNull()?.let {
// Process the resized image's ByteArray
println(it)
}
}
)
```

#### Multiple Images Selection with Resizing
```kotlin
val multipleImagePicker = rememberImagePickerLauncher(
selectionMode = SelectionMode.Multiple(maxSelection = 5),
scope = rememberCoroutineScope(),
resizeOptions = resizeOptions,
onResult = { byteArrays ->
byteArrays.forEach {
// Process the resized images' ByteArrays
println(it)
}
}
)
```

>💡 Note: While resizing, the aspect ratio of the original images is preserved. The final dimensions may slightly vary to maintain the original proportions.
<br/>

## ByteArray to ImageBitmap Conversion
We've added a new extension function `toImageBitmap()` to convert a `ByteArray` into an `ImageBitmap`. <br/>
This function simplifies the process of converting image data into a displayable format, enhancing the app's capability to handle image processing efficiently.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ plugins {

allprojects {
group = "io.github.team-preat"
version = "0.3.0"
version = "0.3.1"
}

nexusPublishing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package com.preat.peekaboo.image.picker

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
Expand All @@ -23,23 +27,27 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.preat.peekaboo.image.picker.SelectionMode.Companion.INFINITY
import kotlinx.coroutines.CoroutineScope
import java.io.ByteArrayOutputStream

@Composable
actual fun rememberImagePickerLauncher(
selectionMode: SelectionMode,
scope: CoroutineScope?,
resizeOptions: ResizeOptions,
onResult: (List<ByteArray>) -> Unit,
): ImagePickerLauncher {
return when (selectionMode) {
SelectionMode.Single ->
pickSingleImage(
selectionMode = selectionMode,
resizeOptions = resizeOptions,
onResult = onResult,
)

is SelectionMode.Multiple ->
pickMultipleImages(
selectionMode = selectionMode,
resizeOptions = resizeOptions,
onResult = onResult,
)
}
Expand All @@ -48,6 +56,7 @@ actual fun rememberImagePickerLauncher(
@Composable
private fun pickSingleImage(
selectionMode: SelectionMode,
resizeOptions: ResizeOptions,
onResult: (List<ByteArray>) -> Unit,
): ImagePickerLauncher {
val context = LocalContext.current
Expand All @@ -57,10 +66,15 @@ private fun pickSingleImage(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
uri?.let {
with(context.contentResolver) {
openInputStream(uri)?.use {
onResult(listOf(it.readBytes()))
}
val resizedImage =
resizeImage(
context = context,
uri = uri,
width = resizeOptions.width,
height = resizeOptions.height,
)
if (resizedImage != null) {
onResult(listOf(resizedImage))
}
}
},
Expand All @@ -81,6 +95,7 @@ private fun pickSingleImage(
@Composable
fun pickMultipleImages(
selectionMode: SelectionMode.Multiple,
resizeOptions: ResizeOptions,
onResult: (List<ByteArray>) -> Unit,
): ImagePickerLauncher {
val context = LocalContext.current
Expand All @@ -97,9 +112,12 @@ fun pickMultipleImages(
onResult = { uriList ->
val imageBytesList =
uriList.mapNotNull { uri ->
context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.readBytes()
}
resizeImage(
context = context,
uri = uri,
width = resizeOptions.width,
height = resizeOptions.height,
)
}
if (imageBytesList.isNotEmpty()) {
onResult(imageBytesList)
Expand Down Expand Up @@ -127,3 +145,37 @@ actual class ImagePickerLauncher actual constructor(
onLaunch()
}
}

fun resizeImage(
context: Context,
uri: Uri,
width: Int,
height: Int,
): ByteArray? {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val options =
BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(inputStream, null, options)

var inSampleSize = 1
while (options.outWidth / inSampleSize > width || options.outHeight / inSampleSize > height) {
inSampleSize *= 2
}

options.inJustDecodeBounds = false
options.inSampleSize = inSampleSize

context.contentResolver.openInputStream(uri)?.use { scaledInputStream ->
val scaledBitmap = BitmapFactory.decodeStream(scaledInputStream, null, options)
if (scaledBitmap != null) {
ByteArrayOutputStream().use { byteArrayOutputStream ->
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
return byteArrayOutputStream.toByteArray()
}
}
}
}
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope
expect fun rememberImagePickerLauncher(
selectionMode: SelectionMode = SelectionMode.Single,
scope: CoroutineScope?,
resizeOptions: ResizeOptions = ResizeOptions(),
onResult: (List<ByteArray>) -> Unit,
): ImagePickerLauncher

Expand All @@ -35,6 +36,11 @@ sealed class SelectionMode {
}
}

data class ResizeOptions(
val width: Int = 800,
val height: Int = 800,
)

expect class ImagePickerLauncher(
selectionMode: SelectionMode,
onLaunch: () -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,28 @@ package com.preat.peekaboo.image.picker

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.refTo
import kotlinx.cinterop.useContents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGSize
import platform.CoreGraphics.CGSizeMake
import platform.PhotosUI.PHPickerConfiguration
import platform.PhotosUI.PHPickerConfigurationSelectionOrdered
import platform.PhotosUI.PHPickerFilter
import platform.PhotosUI.PHPickerResult
import platform.PhotosUI.PHPickerViewController
import platform.PhotosUI.PHPickerViewControllerDelegateProtocol
import platform.UIKit.UIApplication
import platform.UIKit.UIGraphicsBeginImageContextWithOptions
import platform.UIKit.UIGraphicsEndImageContext
import platform.UIKit.UIGraphicsGetImageFromCurrentImageContext
import platform.UIKit.UIImage
import platform.UIKit.UIImageJPEGRepresentation
import platform.darwin.NSObject
import platform.darwin.dispatch_group_create
import platform.darwin.dispatch_group_enter
Expand All @@ -40,9 +50,9 @@ import platform.posix.memcpy
actual fun rememberImagePickerLauncher(
selectionMode: SelectionMode,
scope: CoroutineScope?,
resizeOptions: ResizeOptions,
onResult: (List<ByteArray>) -> Unit,
): ImagePickerLauncher {
@OptIn(ExperimentalForeignApi::class)
val delegate =
object : NSObject(), PHPickerViewControllerDelegateProtocol {
override fun picker(
Expand All @@ -63,11 +73,14 @@ actual fun rememberImagePickerLauncher(
) { nsData, _ ->
scope?.launch(Dispatchers.Main) {
nsData?.let {
val bytes = ByteArray(it.length.toInt())
memcpy(bytes.refTo(0), it.bytes, it.length)
imageData.add(bytes)
val image = UIImage.imageWithData(it)
val resizedImage = image?.fitInto(resizeOptions.width, resizeOptions.height)
val bytes = resizedImage?.toByteArray()
if (bytes != null) {
imageData.add(bytes)
}
dispatch_group_leave(dispatchGroup)
}
dispatch_group_leave(dispatchGroup)
}
}
}
Expand Down Expand Up @@ -95,6 +108,33 @@ actual fun rememberImagePickerLauncher(
}
}

@OptIn(ExperimentalForeignApi::class)
private fun UIImage.toByteArray(): ByteArray {
val jpegData = UIImageJPEGRepresentation(this, 1.0)!!
return ByteArray(jpegData.length.toInt()).apply {
memcpy(this.refTo(0), jpegData.bytes, jpegData.length)
}
}

@OptIn(ExperimentalForeignApi::class)
private fun UIImage.fitInto(
width: Int,
height: Int,
): UIImage {
val targetSize = CGSizeMake(width.toDouble(), height.toDouble())
return this.resize(targetSize)
}

@OptIn(ExperimentalForeignApi::class)
private fun UIImage.resize(targetSize: CValue<CGSize>): UIImage {
UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0)
this.drawInRect(CGRectMake(0.0, 0.0, targetSize.useContents { width }, targetSize.useContents { height }))
val newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

return newImage!!
}

private fun createPHPickerViewController(
delegate: PHPickerViewControllerDelegateProtocol,
selection: SelectionMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.preat.peekaboo.common.icon.IconCached
import com.preat.peekaboo.common.icon.IconClose
import com.preat.peekaboo.common.icon.IconWarning
import com.preat.peekaboo.common.style.PeekabooTheme
import com.preat.peekaboo.image.picker.ResizeOptions
import com.preat.peekaboo.image.picker.SelectionMode
import com.preat.peekaboo.image.picker.rememberImagePickerLauncher
import com.preat.peekaboo.image.picker.toImageBitmap
Expand Down Expand Up @@ -90,6 +91,8 @@ fun App() {
// Default: No limit, depends on system's maximum capacity.
selectionMode = SelectionMode.Multiple(maxSelection = 5),
scope = scope,
// Resize options are customizable. Default is set to 800 x 800 pixels.
resizeOptions = ResizeOptions(width = 1200, height = 1200),
onResult = { byteArrays ->
images =
byteArrays.map { byteArray ->
Expand Down

0 comments on commit 6e84271

Please sign in to comment.