Skip to content

Commit 0b0a78d

Browse files
author
farfromrefuge
committed
fix(android): prevent null bitmap
1 parent 1fd0912 commit 0b0a78d

File tree

6 files changed

+199
-131
lines changed

6 files changed

+199
-131
lines changed

packages/ui-cameraview/platforms/android/java/com/nativescript/cameraview/BitmapUtils.kt

Lines changed: 145 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.exifinterface.media.ExifInterface
1919
import java.io.ByteArrayOutputStream
2020
import java.io.IOException
2121
import java.lang.Exception
22+
import java.lang.reflect.Method
2223
import java.nio.ByteBuffer
2324

2425
/*
@@ -40,137 +41,170 @@ import java.nio.ByteBuffer
4041

4142

4243
/** Utils functions for bitmap conversions. */
43-
object BitmapUtils {
44-
private const val TAG = "BitmapUtils"
45-
46-
private var yuvToRgbConverter: YuvToRgbConverter? = null
47-
48-
// based on androidx.camera.core.ImageSaver#imageToJpegByteArray(),
49-
// optimized to avoid extracting uncropped image twice and to close ImageProxy sooner
50-
@SuppressLint("RestrictedApi")
51-
@Throws(ImageUtil.CodecFailedException::class)
52-
public fun extractJpegBytes(image: ImageProxy, jpegQuality: Int): ByteArray {
53-
// try {
54-
var cropRect = if (ImageUtil.shouldCropImage(image)) image.cropRect else null
44+
class BitmapUtils {
45+
46+
47+
companion object {
48+
private const val TAG = "BitmapUtils"
49+
private var yuvToRgbConverter: YuvToRgbConverter? = null
50+
51+
// private var post1dot4: Boolean = true
52+
// private var yuvImageToJpegByteArray: Method
53+
54+
// init {
55+
// // we try handle the fact yuvImageToJpegByteArray changed number of parameter from 1.3.2 to later
56+
// yuvImageToJpegByteArray =
57+
// ImageUtil::class.java.declaredMethods.first { it.name == "yuvImageToJpegByteArray" }
58+
// post1dot4 = yuvImageToJpegByteArray.parameterCount == 4
59+
// }
60+
61+
// based on androidx.camera.core.ImageSaver#imageToJpegByteArray(),
62+
// optimized to avoid extracting uncropped image twice and to close ImageProxy sooner
63+
// @SuppressLint("RestrictedApi")
64+
// @Throws(ImageUtil.CodecFailedException::class)
65+
// public fun extractJpegBytes(image: ImageProxy, jpegQuality: Int, rotation: Int): ByteArray {
66+
// var cropRect = if (ImageUtil.shouldCropImage(image)) image.cropRect else null
67+
// val imageFormat = image.format
68+
//
69+
// var origJpegBytes = if (imageFormat == ImageFormat.JPEG) {
70+
// ImageUtil.jpegImageToJpegByteArray(image)
71+
// } else if (imageFormat == ImageFormat.YUV_420_888) {
72+
// if (post1dot4) {
73+
// yuvImageToJpegByteArray!!.invoke(
74+
// null,
75+
// image,
76+
// cropRect,
77+
// jpegQuality,
78+
// rotation
79+
// ) as ByteArray
80+
//// ImageUtil.yuvImageToJpegByteArray(image, cropRect, jpegQuality, rotation)
81+
//
82+
// } else {
83+
// yuvImageToJpegByteArray!!.invoke(
84+
// null,
85+
// image,
86+
// cropRect,
87+
// jpegQuality
88+
// ) as ByteArray
89+
//// ImageUtil.yuvImageToJpegByteArray(image, cropRect, jpegQuality)
90+
// }
91+
// } else {
92+
// throw IllegalStateException("unknown imageFormat $imageFormat")
93+
// }
94+
// return origJpegBytes!!;
95+
// }
96+
97+
// @OptIn(ExperimentalGetImage::class)
98+
// public fun byteArrayFromProxy(image: ImageProxy, jpegQuality: Int): ByteArray? {
99+
// return extractJpegBytes(image, jpegQuality, image.imageInfo.rotationDegrees);
100+
// }
101+
102+
@SuppressLint("RestrictedApi")
103+
@OptIn(ExperimentalGetImage::class)
104+
public fun getBitmap(context: Context, image: ImageProxy, jpegQuality: Int = 95): Bitmap {
55105
val imageFormat = image.format
56-
57-
var origJpegBytes = if (imageFormat == ImageFormat.JPEG) {
58-
ImageUtil.jpegImageToJpegByteArray(image)
59-
} else if (imageFormat == ImageFormat.YUV_420_888) {
60-
ImageUtil.yuvImageToJpegByteArray(image, cropRect, jpegQuality)
106+
if (imageFormat == ImageFormat.YUV_420_888) {
107+
// TODO: for now extractJpegBytes is around 3/4 times slower than yuvToRgbConverter
108+
if (yuvToRgbConverter == null) {
109+
yuvToRgbConverter = YuvToRgbConverter(context)
110+
}
111+
var bm = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
112+
yuvToRgbConverter!!.yuvToRgb(image.image!!, bm)
113+
return bm;
61114
} else {
62-
throw IllegalStateException("unknown imageFormat $imageFormat")
63-
}
64-
return origJpegBytes!!;
65-
// } finally {
66-
/*
67-
from javadoc of the Image class:
68-
Since Images are often directly produced or consumed by hardware components, they are
69-
a limited resource shared across the system, and should be closed as soon as
70-
they are no longer needed.
71-
*/
72-
// image.close()
73-
// }
74-
}
75-
@OptIn(ExperimentalGetImage::class) public fun byteArrayFromProxy(image: ImageProxy, jpegQuality: Int): ByteArray? {
76-
return extractJpegBytes(image,jpegQuality);
77-
}
78-
@OptIn(ExperimentalGetImage::class) public fun getBitmap(context: Context, image: ImageProxy, jpegQuality: Int = 95): Bitmap {
79-
val imageFormat = image.format
80-
if (imageFormat == ImageFormat.YUV_420_888) {
81-
// TODO: for now extractJpegBytes is around 3/4 times slower than yuvToRgbConverter
82-
if (yuvToRgbConverter == null) {
83-
yuvToRgbConverter = YuvToRgbConverter(context)
115+
val byteArray = ImageUtil.jpegImageToJpegByteArray(image);
116+
var bm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size);
117+
return bm;
84118
}
85-
var bm = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
86-
yuvToRgbConverter!!.yuvToRgb(image.image!!, bm)
87-
return bm;
88-
} else {
89-
val byteArray = extractJpegBytes(image, if (jpegQuality > 0) jpegQuality else 95 );
90-
var bm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size);
91-
return bm;
92-
}
93119

94120

95121
// var bm = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
96122
// yuvToRgbConverter!!.yuvToRgb(image.image!!, bm)
97123
// return bm;
98-
}
124+
}
99125

100-
/** Rotates a bitmap if it is converted from a bytebuffer. */
101-
private fun rotateBitmap(
102-
bitmap: Bitmap, rotationDegrees: Int, flipX: Boolean, flipY: Boolean
103-
): Bitmap {
104-
val matrix = Matrix()
126+
/** Rotates a bitmap if it is converted from a bytebuffer. */
127+
private fun rotateBitmap(
128+
bitmap: Bitmap, rotationDegrees: Int, flipX: Boolean, flipY: Boolean
129+
): Bitmap {
130+
val matrix = Matrix()
105131

106-
// Rotate the image back to straight.
107-
matrix.postRotate(rotationDegrees.toFloat())
132+
// Rotate the image back to straight.
133+
matrix.postRotate(rotationDegrees.toFloat())
108134

109-
// Mirror the image along the X or Y axis.
110-
matrix.postScale(if (flipX) -1.0f else 1.0f, if (flipY) -1.0f else 1.0f)
111-
val rotatedBitmap =
112-
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
135+
// Mirror the image along the X or Y axis.
136+
matrix.postScale(if (flipX) -1.0f else 1.0f, if (flipY) -1.0f else 1.0f)
137+
val rotatedBitmap =
138+
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
113139

114-
// Recycle the old bitmap if it has changed.
115-
if (rotatedBitmap != bitmap) {
116-
bitmap.recycle()
140+
// Recycle the old bitmap if it has changed.
141+
if (rotatedBitmap != bitmap) {
142+
bitmap.recycle()
143+
}
144+
return rotatedBitmap
117145
}
118-
return rotatedBitmap
119-
}
120146

121-
@Throws(IOException::class)
122-
fun getBitmapFromContentUri(contentResolver: ContentResolver, imageUri: Uri): Bitmap? {
123-
val decodedBitmap =
124-
MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
125-
?: return null
126-
val orientation = getExifOrientationTag(contentResolver, imageUri)
127-
var rotationDegrees = 0
128-
var flipX = false
129-
var flipY = false
130-
when (orientation) {
131-
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipX = true
132-
ExifInterface.ORIENTATION_ROTATE_90 -> rotationDegrees = 90
133-
ExifInterface.ORIENTATION_TRANSPOSE -> {
134-
rotationDegrees = 90
135-
flipX = true
136-
}
137-
ExifInterface.ORIENTATION_ROTATE_180 -> rotationDegrees = 180
138-
ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipY = true
139-
ExifInterface.ORIENTATION_ROTATE_270 -> rotationDegrees = -90
140-
ExifInterface.ORIENTATION_TRANSVERSE -> {
141-
rotationDegrees = -90
142-
flipX = true
147+
@Throws(IOException::class)
148+
fun getBitmapFromContentUri(contentResolver: ContentResolver, imageUri: Uri): Bitmap? {
149+
val decodedBitmap =
150+
MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
151+
?: return null
152+
val orientation = getExifOrientationTag(contentResolver, imageUri)
153+
var rotationDegrees = 0
154+
var flipX = false
155+
var flipY = false
156+
when (orientation) {
157+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipX = true
158+
ExifInterface.ORIENTATION_ROTATE_90 -> rotationDegrees = 90
159+
ExifInterface.ORIENTATION_TRANSPOSE -> {
160+
rotationDegrees = 90
161+
flipX = true
162+
}
163+
164+
ExifInterface.ORIENTATION_ROTATE_180 -> rotationDegrees = 180
165+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipY = true
166+
ExifInterface.ORIENTATION_ROTATE_270 -> rotationDegrees = -90
167+
ExifInterface.ORIENTATION_TRANSVERSE -> {
168+
rotationDegrees = -90
169+
flipX = true
170+
}
171+
172+
ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_NORMAL -> {}
173+
else -> {}
143174
}
144-
ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_NORMAL -> {}
145-
else -> {}
175+
return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY)
146176
}
147-
return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY)
148-
}
149177

150-
private fun getExifOrientationTag(resolver: ContentResolver, imageUri: Uri): Int {
151-
// We only support parsing EXIF orientation tag from local file on the device.
152-
// See also:
153-
// https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html
154-
if (ContentResolver.SCHEME_CONTENT != imageUri.scheme
155-
&& ContentResolver.SCHEME_FILE != imageUri.scheme
156-
) {
157-
return 0
158-
}
159-
var exif: ExifInterface
160-
try {
161-
resolver.openInputStream(imageUri).use { inputStream ->
162-
if (inputStream == null) {
163-
return 0
178+
private fun getExifOrientationTag(resolver: ContentResolver, imageUri: Uri): Int {
179+
// We only support parsing EXIF orientation tag from local file on the device.
180+
// See also:
181+
// https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html
182+
if (ContentResolver.SCHEME_CONTENT != imageUri.scheme
183+
&& ContentResolver.SCHEME_FILE != imageUri.scheme
184+
) {
185+
return 0
186+
}
187+
var exif: ExifInterface
188+
try {
189+
resolver.openInputStream(imageUri).use { inputStream ->
190+
if (inputStream == null) {
191+
return 0
192+
}
193+
exif = ExifInterface(inputStream)
164194
}
165-
exif = ExifInterface(inputStream)
195+
} catch (e: IOException) {
196+
Log.e(
197+
TAG,
198+
"failed to open file to read rotation meta data: $imageUri", e
199+
)
200+
return 0
166201
}
167-
} catch (e: IOException) {
168-
Log.e(
169-
TAG,
170-
"failed to open file to read rotation meta data: $imageUri", e
202+
return exif.getAttributeInt(
203+
ExifInterface.TAG_ORIENTATION,
204+
ExifInterface.ORIENTATION_NORMAL
171205
)
172-
return 0
173206
}
174-
return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
175207
}
208+
209+
176210
}

packages/ui-cameraview/platforms/android/java/com/nativescript/cameraview/CameraBase.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.nativescript.widgets.GridLayout
3030
abstract class CameraBase @JvmOverloads constructor(
3131
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
3232
) : GridLayout(context) {
33+
// ) : GridLayout(context, attrs, defStyleAttr) {
3334
var enableAudio: Boolean = true
3435
abstract var retrieveLatestImage: Boolean
3536
internal var latestImage: Bitmap? = null

packages/ui-cameraview/platforms/android/java/com/nativescript/cameraview/CameraEventListener.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package com.nativescript.cameraview
1010
import android.media.Image
1111
import android.graphics.Bitmap
1212
import androidx.camera.core.ImageInfo
13+
import androidx.camera.core.ImageProxy
1314
import java.io.File
1415
import java.lang.Exception
1516

@@ -19,6 +20,7 @@ interface CameraEventListener {
1920
fun onCameraClose()
2021
fun onCameraPhoto(file: File?)
2122
fun onCameraPhotoImage(image: Bitmap?, info: ImageInfo/* , processor: ImageAsyncProcessor */)
23+
fun onCameraPhotoImageProxy(image: ImageProxy, processor: ImageAsyncProcessor)
2224
fun onCameraVideo(file: File?)
2325
fun onCameraAnalysis(analysis: ImageAnalysis)
2426
fun onCameraError(message: String, ex: Exception)

0 commit comments

Comments
 (0)