diff --git a/composeApp/src/iosMain/kotlin/com/github/familyvault/services/ImagePickerService.kt b/composeApp/src/iosMain/kotlin/com/github/familyvault/services/ImagePickerService.kt index 84ab2889..82111547 100644 --- a/composeApp/src/iosMain/kotlin/com/github/familyvault/services/ImagePickerService.kt +++ b/composeApp/src/iosMain/kotlin/com/github/familyvault/services/ImagePickerService.kt @@ -2,68 +2,176 @@ package com.github.familyvault.services import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.graphics.ImageBitmap -import com.github.familyvault.AppConfig +import androidx.compose.ui.graphics.toComposeImageBitmap import com.github.familyvault.models.ImageSize -import org.jetbrains.skia.Bitmap +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.runBlocking +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Image +import org.jetbrains.skia.impl.use +import platform.Foundation.NSData +import platform.Foundation.NSError +import platform.Foundation.NSItemProvider +import platform.PhotosUI.PHPickerConfiguration +import platform.PhotosUI.PHPickerFilter +import platform.PhotosUI.PHPickerResult +import platform.PhotosUI.PHPickerViewController +import platform.PhotosUI.PHPickerViewControllerDelegateProtocol +import platform.UIKit.UIApplication import kotlin.coroutines.Continuation -import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import platform.UniformTypeIdentifiers.UTTypeImage +import platform.darwin.NSObject +import platform.posix.memcpy +import kotlin.coroutines.resume + class ImagePickerService : IImagePickerService { private var continuation: Continuation>? = null - private val selectedImageUrls = mutableStateListOf() - fun initializeWithActivity() { - TODO("Method not yet implemented") + private val selectedImages = mutableStateListOf() + private val pickerController = PickerController(selectedImages) + private val config = PHPickerConfiguration().apply { + selectionLimit = 0 + filter = PHPickerFilter.imagesFilter } + private val scope = CoroutineScope(Dispatchers.Default) + private var cont: PHPickerViewController? = null - override fun openMediaPickerForSelectingImages() { - TODO("Method not yet implemented") + init { + clearSelectedImages() } - override suspend fun pickImagesAndReturnByteArrays(): List = suspendCoroutine { cont -> - TODO("Method not yet implemented") + override fun openMediaPickerForSelectingImages() { + if (cont == null) cont = PHPickerViewController(config) + cont!!.setDelegate(pickerController) + UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController( + cont!!, + true, + null + ) } + override suspend fun pickImagesAndReturnByteArrays(): List = + suspendCoroutine { cont -> + continuation = cont + openMediaPickerForSelectingImages() + } + override fun getBytesFromUri(uriString: String): ByteArray? { - TODO("Method not yet implemented") + return selectedImages.firstOrNull { phResult -> + phResult.ID == uriString + }?.let { phResult -> + return@let runBlocking(Dispatchers.IO) { + suspendCoroutine { cont -> + phResult.itemProvider.loadDataRepresentationForTypeIdentifier(UTTypeImage.identifier){ data, error -> + cont.resume(data!!.toByteArray()) + } + } + } + } } + @OptIn(ExperimentalForeignApi::class) override fun getSelectedImageAsByteArrays(): List { - TODO("Method not yet implemented") + return selectedImages.map { phResult -> + runBlocking { + suspendCoroutine { cont -> + phResult.itemProvider.loadDataRepresentationForTypeIdentifier( + UTTypeImage.identifier, + ) { p1: NSData?, + p2: NSError? -> + val image = p1!! + cont.resume(image.toByteArray()) + } + } + } + } } - override fun getSelectedImageUrls(): List = selectedImageUrls + override fun getSelectedImageUrls(): List = selectedImages.map { phResult -> + phResult.ID + } override fun getBitmapFromBytes(imageBytes: ByteArray): ImageBitmap { - TODO("Method not yet implemented") + return Image.makeFromEncoded(imageBytes).use { + it.toComposeImageBitmap() + } } override fun clearSelectedImages() { - TODO("Method not yet implemented") + selectedImages.clear() + cont = null } override fun removeSelectedImage(uri: String) { - TODO("Method not yet implemented") + selectedImages.removeAll { + it.ID == uri + } } override fun compressAndRotateImage( imageByteArray: ByteArray, compressionQuality: Int? ): ByteArray { - TODO("Method not yet implemented") + return Image.makeFromEncoded(imageByteArray).use { + it.compress(quality = compressionQuality ?: 92) + } } override fun getImageAsByteArraySize(image: ByteArray): ImageSize { - TODO("Method not yet implemented") + val imageBitmap = getBitmapFromBytes(image) + return ImageSize(imageBitmap.height, imageBitmap.width) } - private fun compressImage(bitmap: Bitmap, quality: Int): ByteArray { - TODO("Method not yet implemented") + private fun Image.compress(quality: Int): ByteArray { + return encodeToData(EncodedImageFormat.JPEG, quality)!!.bytes } - private fun fixImageRotation(imageBytes: ByteArray): Bitmap { - TODO("Method not yet implemented") + //TODO: Check if this rotation method is needed +// private fun Image.fixRotation(): Image = this.apply{ +// +// Bitmap.makeFromImage(Image.makeFromEncoded(imageBytes)) +// } + +} + +@OptIn(BetaInteropApi::class) +class PickerController( + val selectedUris: MutableList +) : NSObject(), + PHPickerViewControllerDelegateProtocol { + + override fun picker( + picker: PHPickerViewController, + didFinishPicking: List<*> + ) { + @Suppress("UNCHECKED_CAST") + didFinishPicking as List + val itemsToAdd = didFinishPicking.filter { + it.itemProvider.hasItemConformingToTypeIdentifier(UTTypeImage.identifier) + } + selectedUris.clear() + selectedUris.addAll(itemsToAdd) + picker.dismissViewControllerAnimated(true,null) + } +} + +@OptIn(ExperimentalForeignApi::class) +fun NSData.toByteArray(): ByteArray = memScoped { + ByteArray(length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), bytes, length) + } } +} -} \ No newline at end of file +val NSItemProvider.ID get() = "$suggestedName/$hash" +val PHPickerResult.ID get() = this.assetIdentifier ?: itemProvider.ID \ No newline at end of file