icerockdev / moko-media

Media selection & presenting for mobile (android & ios) Kotlin Multiplatform development
https://moko.icerock.dev/
Apache License 2.0
120 stars 12 forks source link

Photo capture logic with CameraView #17

Open Alex009 opened 4 years ago

Alex009 commented 4 years ago

Now library contains simple picker for selection from photo gallery or create photo from camera with system UI. but for case when we add cameraview on own screen we not have any complete solution. With this reason on one project we already implement logic of photo capturing with:

  1. Android use library https://github.com/natario1/CameraView for capture photo
  2. after photo capturing we save photo to file and pass between screens only path to temporary file with photo, with this class: expect:
    
    import dev.icerock.moko.media.Bitmap

expect class ImageFile { fun toBitmap(maxWidthPx: Int? = null, maxHeightPx: Int? = null): Bitmap }

fun ImageFile.toBitmap(maxDimensionPx: Int): Bitmap = toBitmap( maxDimensionPx, maxDimensionPx )


android actual:
```kotlin
import android.graphics.BitmapFactory
import android.graphics.Matrix
import androidx.exifinterface.media.ExifInterface
import dev.icerock.moko.media.Bitmap
import dev.icerock.moko.parcelize.Parcelable
import dev.icerock.moko.parcelize.Parcelize
import java.io.File
import android.graphics.Bitmap as AndroidBitmap

@Parcelize
actual class ImageFile(
    private val path: String
) : Parcelable {

    actual fun toBitmap(maxWidthPx: Int?, maxHeightPx: Int?): Bitmap {
        val file = File(this.path)

        val bitmapOptions: BitmapFactory.Options = readImageSize(file)
        val rotationDegrees: Int = readImageRotation(file)
        val sampleSize: Int? = calculateSampleSize(maxWidthPx, maxHeightPx, bitmapOptions)
        val sampledBitmap: AndroidBitmap = readSampledBitmap(file, sampleSize)
        val rotatedBitmap: AndroidBitmap = getRotatedBitmap(rotationDegrees, sampledBitmap)

        return Bitmap(rotatedBitmap)
    }

    private fun getRotatedBitmap(
        rotationDegrees: Int,
        sampledBitmap: AndroidBitmap
    ): AndroidBitmap {
        return if (rotationDegrees != 0) {
            val matrix = Matrix()
            matrix.postRotate(rotationDegrees.toFloat())

            AndroidBitmap.createBitmap(
                sampledBitmap,
                0, 0,
                sampledBitmap.width, sampledBitmap.height,
                matrix,
                true
            )
        } else {
            sampledBitmap
        }
    }

    private fun readSampledBitmap(
        file: File,
        sampleSize: Int?
    ): AndroidBitmap {
        val readingBitmapOptions = BitmapFactory.Options().apply {
            inJustDecodeBounds = false
            inSampleSize = sampleSize ?: 1
        }
        return file.inputStream().use { inputStream ->
            BitmapFactory.decodeStream(inputStream, null, readingBitmapOptions)
        } ?: throw IllegalArgumentException("can't decode file $file with $readingBitmapOptions")
    }

    private fun calculateSampleSize(
        maxWidthPx: Int?,
        maxHeightPx: Int?,
        bitmapOptions: BitmapFactory.Options
    ): Int? {
        return if (maxWidthPx != null || maxHeightPx != null) {
            val widthLimit = maxWidthPx ?: bitmapOptions.outWidth
            val heightLimit = maxHeightPx ?: bitmapOptions.outHeight

            calculateInSampleSize(
                options = bitmapOptions,
                maxWidth = widthLimit,
                maxHeight = heightLimit
            )
        } else {
            null
        }
    }

    private fun readImageRotation(file: File): Int {
        return file.inputStream().use { inputStream ->
            ExifInterface(inputStream)
        }.rotationDegrees
    }

    private fun readImageSize(file: File): BitmapFactory.Options {
        return file.inputStream().use { inputStream ->
            val bitmapOptions = BitmapFactory.Options().apply {
                inJustDecodeBounds = true
            }

            BitmapFactory.decodeStream(inputStream, null, bitmapOptions)

            bitmapOptions
        }
    }

    private fun calculateInSampleSize(
        options: BitmapFactory.Options,
        maxWidth: Int,
        maxHeight: Int
    ): Int {
        val (height: Int, width: Int) = options.run { outHeight to outWidth }
        var inSampleSize = 1

        if (height > maxHeight || width > maxWidth) {

            val halfHeight: Int = height / 2
            val halfWidth: Int = width / 2

            while (halfHeight / inSampleSize >= maxHeight && halfWidth / inSampleSize >= maxWidth) {
                inSampleSize *= 2
            }
        }

        return inSampleSize
    }
}

ios actual:

import dev.icerock.moko.media.Bitmap

actual class ImageFile(
    private val bitmap: Bitmap
) {
    actual fun toBitmap(maxWidthPx: Int?, maxHeightPx: Int?): Bitmap {
        return bitmap
    }
}
  1. in application we can use multiple ImageFile without OutOfMemory on android, because all images just on storage, not in RAM. on iOS now logic with files not implemented (here more available memory for images)...

i think we should have in moko-media out of box solution for this case and allow devs to not think about photo capturing problems.

also for android i think we can try https://developer.android.com/training/camerax