mirror of
https://github.com/SimpleMobileTools/Simple-Camera.git
synced 2025-06-27 09:02:59 +02:00
232 lines
8.1 KiB
Kotlin
232 lines
8.1 KiB
Kotlin
package com.simplemobiletools.camera.helpers
|
|
|
|
import android.graphics.*
|
|
import androidx.annotation.IntRange
|
|
import androidx.camera.core.ImageProxy
|
|
import java.io.ByteArrayOutputStream
|
|
import java.io.IOException
|
|
|
|
/**
|
|
* Utility class for image related operations.
|
|
* @see androidx.camera.core.internal.utils.ImageUtil
|
|
*/
|
|
object ImageUtil {
|
|
|
|
@Throws(CodecFailedException::class)
|
|
fun imageToJpegByteArray(image: ImageProxy, jpegQuality: Int): ByteArray {
|
|
val shouldCropImage = shouldCropImage(image)
|
|
return when (image.format) {
|
|
ImageFormat.JPEG -> {
|
|
if (!shouldCropImage) {
|
|
// When cropping is unnecessary, the byte array doesn't need to be decoded and
|
|
// re-encoded again. Therefore, jpegQuality is unnecessary in this case.
|
|
jpegImageToJpegByteArray(image)
|
|
} else {
|
|
jpegImageToJpegByteArray(image, image.cropRect, jpegQuality)
|
|
}
|
|
}
|
|
ImageFormat.YUV_420_888 -> {
|
|
yuvImageToJpegByteArray(image, if (shouldCropImage) image.cropRect else null, jpegQuality)
|
|
}
|
|
else -> {
|
|
// Unrecognized image format
|
|
byteArrayOf()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts JPEG [ImageProxy] to JPEG byte array.
|
|
*/
|
|
fun jpegImageToJpegByteArray(image: ImageProxy): ByteArray {
|
|
require(image.format == ImageFormat.JPEG) { "Incorrect image format of the input image proxy: " + image.format }
|
|
val planes = image.planes
|
|
val buffer = planes[0].buffer
|
|
val data = ByteArray(buffer.capacity())
|
|
buffer.rewind()
|
|
buffer[data]
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Converts JPEG [ImageProxy] to JPEG byte array. The input JPEG image will be cropped
|
|
* by the specified crop rectangle and compressed by the specified quality value.
|
|
*/
|
|
@Throws(CodecFailedException::class)
|
|
private fun jpegImageToJpegByteArray(
|
|
image: ImageProxy,
|
|
cropRect: Rect,
|
|
@IntRange(from = 1, to = 100) jpegQuality: Int,
|
|
): ByteArray {
|
|
require(image.format == ImageFormat.JPEG) { "Incorrect image format of the input image proxy: " + image.format }
|
|
var data = jpegImageToJpegByteArray(image)
|
|
data = cropJpegByteArray(data, cropRect, jpegQuality)
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image
|
|
* will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
|
|
* be compressed by the specified quality value.
|
|
*/
|
|
@Throws(CodecFailedException::class)
|
|
private fun yuvImageToJpegByteArray(
|
|
image: ImageProxy,
|
|
cropRect: Rect?,
|
|
@IntRange(from = 1, to = 100) jpegQuality: Int,
|
|
): ByteArray {
|
|
require(image.format == ImageFormat.YUV_420_888) { "Incorrect image format of the input image proxy: " + image.format }
|
|
return nv21ToJpeg(
|
|
yuv_420_888toNv21(image),
|
|
image.width,
|
|
image.height,
|
|
cropRect,
|
|
jpegQuality
|
|
)
|
|
}
|
|
|
|
/**
|
|
* [android.media.Image] to NV21 byte array.
|
|
*/
|
|
private fun yuv_420_888toNv21(image: ImageProxy): ByteArray {
|
|
val yPlane = image.planes[0]
|
|
val uPlane = image.planes[1]
|
|
val vPlane = image.planes[2]
|
|
val yBuffer = yPlane.buffer
|
|
val uBuffer = uPlane.buffer
|
|
val vBuffer = vPlane.buffer
|
|
yBuffer.rewind()
|
|
uBuffer.rewind()
|
|
vBuffer.rewind()
|
|
val ySize = yBuffer.remaining()
|
|
var position = 0
|
|
val nv21 = ByteArray(ySize + image.width * image.height / 2)
|
|
|
|
// Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
|
|
for (row in 0 until image.height) {
|
|
yBuffer[nv21, position, image.width]
|
|
position += image.width
|
|
yBuffer.position(
|
|
Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride)
|
|
)
|
|
}
|
|
val chromaHeight = image.height / 2
|
|
val chromaWidth = image.width / 2
|
|
val vRowStride = vPlane.rowStride
|
|
val uRowStride = uPlane.rowStride
|
|
val vPixelStride = vPlane.pixelStride
|
|
val uPixelStride = uPlane.pixelStride
|
|
|
|
// Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
|
|
// perform faster bulk gets from the byte buffers.
|
|
val vLineBuffer = ByteArray(vRowStride)
|
|
val uLineBuffer = ByteArray(uRowStride)
|
|
for (row in 0 until chromaHeight) {
|
|
vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())]
|
|
uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
|
|
var vLineBufferPosition = 0
|
|
var uLineBufferPosition = 0
|
|
for (col in 0 until chromaWidth) {
|
|
nv21[position++] = vLineBuffer[vLineBufferPosition]
|
|
nv21[position++] = uLineBuffer[uLineBufferPosition]
|
|
vLineBufferPosition += vPixelStride
|
|
uLineBufferPosition += uPixelStride
|
|
}
|
|
}
|
|
return nv21
|
|
}
|
|
|
|
/**
|
|
* Crops JPEG byte array with given [android.graphics.Rect].
|
|
*/
|
|
@Throws(CodecFailedException::class)
|
|
private fun cropJpegByteArray(
|
|
data: ByteArray,
|
|
cropRect: Rect,
|
|
@IntRange(from = 1, to = 100) jpegQuality: Int,
|
|
): ByteArray {
|
|
val bitmap: Bitmap
|
|
try {
|
|
val decoder = BitmapRegionDecoder.newInstance(
|
|
data, 0, data.size,
|
|
false
|
|
)
|
|
bitmap = decoder.decodeRegion(cropRect, BitmapFactory.Options())
|
|
decoder.recycle()
|
|
} catch (e: IllegalArgumentException) {
|
|
throw CodecFailedException(
|
|
"Decode byte array failed with illegal argument.$e",
|
|
CodecFailedException.FailureType.DECODE_FAILED
|
|
)
|
|
} catch (e: IOException) {
|
|
throw CodecFailedException(
|
|
"Decode byte array failed.",
|
|
CodecFailedException.FailureType.DECODE_FAILED
|
|
)
|
|
}
|
|
|
|
val out = ByteArrayOutputStream()
|
|
val success = bitmap.compress(Bitmap.CompressFormat.JPEG, jpegQuality, out)
|
|
if (!success) {
|
|
throw CodecFailedException(
|
|
"Encode bitmap failed.",
|
|
CodecFailedException.FailureType.ENCODE_FAILED
|
|
)
|
|
}
|
|
bitmap.recycle()
|
|
return out.toByteArray()
|
|
}
|
|
|
|
@Throws(CodecFailedException::class)
|
|
private fun nv21ToJpeg(
|
|
nv21: ByteArray,
|
|
width: Int,
|
|
height: Int,
|
|
cropRect: Rect?,
|
|
@IntRange(from = 1, to = 100) jpegQuality: Int,
|
|
): ByteArray {
|
|
val out = ByteArrayOutputStream()
|
|
val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
|
|
val success = yuv.compressToJpeg(
|
|
cropRect ?: Rect(0, 0, width, height),
|
|
jpegQuality, out
|
|
)
|
|
if (!success) {
|
|
throw CodecFailedException(
|
|
"YuvImage failed to encode jpeg.",
|
|
CodecFailedException.FailureType.ENCODE_FAILED
|
|
)
|
|
}
|
|
return out.toByteArray()
|
|
}
|
|
|
|
/**
|
|
* Checks whether the image's crop rectangle is the same as the source image size.
|
|
*/
|
|
private fun shouldCropImage(image: ImageProxy): Boolean {
|
|
return shouldCropImage(
|
|
image.width, image.height, image.cropRect.width(),
|
|
image.cropRect.height()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Checks whether the image's crop rectangle is the same as the source image size.
|
|
*/
|
|
private fun shouldCropImage(
|
|
sourceWidth: Int, sourceHeight: Int, cropRectWidth: Int,
|
|
cropRectHeight: Int
|
|
): Boolean {
|
|
return sourceWidth != cropRectWidth || sourceHeight != cropRectHeight
|
|
}
|
|
|
|
/**
|
|
* Exception for error during transcoding image.
|
|
*/
|
|
class CodecFailedException internal constructor(message: String, val failureType: FailureType) : Exception(message) {
|
|
enum class FailureType {
|
|
ENCODE_FAILED, DECODE_FAILED, UNKNOWN
|
|
}
|
|
}
|
|
}
|