diff --git a/README.md b/README.md index 534b304..90afe5b 100644 --- a/README.md +++ b/README.md @@ -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" } @@ -203,6 +203,54 @@ Button(
+## Image Resizing Options +`peekaboo` offers customizable resizing options for both single and multiple image selections.
+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. + +
+ ## ByteArray to ImageBitmap Conversion We've added a new extension function `toImageBitmap()` to convert a `ByteArray` into an `ImageBitmap`.
This function simplifies the process of converting image data into a displayable format, enhancing the app's capability to handle image processing efficiently. diff --git a/convention-plugins/src/main/kotlin/root.publication.gradle.kts b/convention-plugins/src/main/kotlin/root.publication.gradle.kts index a3995a9..d3f3b04 100644 --- a/convention-plugins/src/main/kotlin/root.publication.gradle.kts +++ b/convention-plugins/src/main/kotlin/root.publication.gradle.kts @@ -19,7 +19,7 @@ plugins { allprojects { group = "io.github.team-preat" - version = "0.3.0" + version = "0.3.1" } nexusPublishing { diff --git a/peekaboo-image-picker/src/androidMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.android.kt b/peekaboo-image-picker/src/androidMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.android.kt index 66b38c2..735fd3d 100644 --- a/peekaboo-image-picker/src/androidMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.android.kt +++ b/peekaboo-image-picker/src/androidMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.android.kt @@ -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 @@ -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) -> Unit, ): ImagePickerLauncher { return when (selectionMode) { SelectionMode.Single -> pickSingleImage( selectionMode = selectionMode, + resizeOptions = resizeOptions, onResult = onResult, ) is SelectionMode.Multiple -> pickMultipleImages( selectionMode = selectionMode, + resizeOptions = resizeOptions, onResult = onResult, ) } @@ -48,6 +56,7 @@ actual fun rememberImagePickerLauncher( @Composable private fun pickSingleImage( selectionMode: SelectionMode, + resizeOptions: ResizeOptions, onResult: (List) -> Unit, ): ImagePickerLauncher { val context = LocalContext.current @@ -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)) } } }, @@ -81,6 +95,7 @@ private fun pickSingleImage( @Composable fun pickMultipleImages( selectionMode: SelectionMode.Multiple, + resizeOptions: ResizeOptions, onResult: (List) -> Unit, ): ImagePickerLauncher { val context = LocalContext.current @@ -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) @@ -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 +} diff --git a/peekaboo-image-picker/src/commonMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.kt b/peekaboo-image-picker/src/commonMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.kt index 84a7096..d41f2a7 100644 --- a/peekaboo-image-picker/src/commonMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.kt +++ b/peekaboo-image-picker/src/commonMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope expect fun rememberImagePickerLauncher( selectionMode: SelectionMode = SelectionMode.Single, scope: CoroutineScope?, + resizeOptions: ResizeOptions = ResizeOptions(), onResult: (List) -> Unit, ): ImagePickerLauncher @@ -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, diff --git a/peekaboo-image-picker/src/iosMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.ios.kt b/peekaboo-image-picker/src/iosMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.ios.kt index 1382ced..67e711f 100644 --- a/peekaboo-image-picker/src/iosMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.ios.kt +++ b/peekaboo-image-picker/src/iosMain/kotlin/com/preat/peekaboo/image/picker/ImagePickerLauncher.ios.kt @@ -17,11 +17,16 @@ 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 @@ -29,6 +34,11 @@ 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 @@ -40,9 +50,9 @@ import platform.posix.memcpy actual fun rememberImagePickerLauncher( selectionMode: SelectionMode, scope: CoroutineScope?, + resizeOptions: ResizeOptions, onResult: (List) -> Unit, ): ImagePickerLauncher { - @OptIn(ExperimentalForeignApi::class) val delegate = object : NSObject(), PHPickerViewControllerDelegateProtocol { override fun picker( @@ -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) } } } @@ -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): 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, diff --git a/sample/common/src/commonMain/kotlin/com/preat/peekaboo/common/App.kt b/sample/common/src/commonMain/kotlin/com/preat/peekaboo/common/App.kt index 2e00f00..ccb0c6b 100644 --- a/sample/common/src/commonMain/kotlin/com/preat/peekaboo/common/App.kt +++ b/sample/common/src/commonMain/kotlin/com/preat/peekaboo/common/App.kt @@ -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 @@ -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 ->