Skip to content

Commit e0e8e20

Browse files
committed
fix(android): fixed a crash on takePicture
1 parent a6ba601 commit e0e8e20

File tree

6 files changed

+343
-21
lines changed

6 files changed

+343
-21
lines changed

demo-snippets/platforms/android/java/com/nativescript/cameraviewdemo/CustomImageAnalysisCallback.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import com.nativescript.cameraviewdemo.models.Quad
1212
import java.util.Vector
1313

1414
class CustomImageAnalysisCallback @JvmOverloads constructor(
15-
context: Context, private val cropView: CropView
15+
private val context: Context, private val cropView: CropView
1616
) : ImageAnalysisCallback {
1717
/**
1818
* @property cropperOffsetWhenCornersNotFound if we can't find document corners, we set
@@ -148,7 +148,7 @@ class CustomImageAnalysisCallback @JvmOverloads constructor(
148148
// }
149149
try {
150150

151-
var previewBitmap = BitmapUtils.getBitmap(image, 100 )
151+
var previewBitmap = BitmapUtils.getBitmap(context, image )
152152
var pointsList: List<List<Point>>?;
153153

154154
pointsList = getDocumentCorners(

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.nativescript.cameraview
33
import android.annotation.SuppressLint
44
import android.annotation.TargetApi
55
import android.content.ContentResolver
6+
import android.content.Context
67
import android.graphics.*
78
import android.media.Image
89
import android.net.Uri
@@ -42,12 +43,14 @@ import java.nio.ByteBuffer
4243
object BitmapUtils {
4344
private const val TAG = "BitmapUtils"
4445

46+
private var yuvToRgbConverter: YuvToRgbConverter? = null
47+
4548
// based on androidx.camera.core.ImageSaver#imageToJpegByteArray(),
4649
// optimized to avoid extracting uncropped image twice and to close ImageProxy sooner
4750
@SuppressLint("RestrictedApi")
4851
@Throws(ImageUtil.CodecFailedException::class)
4952
public fun extractJpegBytes(image: ImageProxy, jpegQuality: Int): ByteArray {
50-
try {
53+
// try {
5154
var cropRect = if (ImageUtil.shouldCropImage(image)) image.cropRect else null
5255
val imageFormat = image.format
5356

@@ -59,23 +62,39 @@ object BitmapUtils {
5962
throw IllegalStateException("unknown imageFormat $imageFormat")
6063
}
6164
return origJpegBytes!!;
62-
} finally {
65+
// } finally {
6366
/*
6467
from javadoc of the Image class:
6568
Since Images are often directly produced or consumed by hardware components, they are
6669
a limited resource shared across the system, and should be closed as soon as
6770
they are no longer needed.
6871
*/
69-
image.close()
70-
}
72+
// image.close()
73+
// }
7174
}
7275
@OptIn(ExperimentalGetImage::class) public fun byteArrayFromProxy(image: ImageProxy, jpegQuality: Int): ByteArray? {
7376
return extractJpegBytes(image,jpegQuality);
7477
}
75-
@OptIn(ExperimentalGetImage::class) public fun getBitmap(image: ImageProxy, jpegQuality: Int): Bitmap {
76-
val byteArray = extractJpegBytes(image, jpegQuality);
77-
var bm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size);
78-
return bm;
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)
84+
}
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+
}
93+
94+
95+
// var bm = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
96+
// yuvToRgbConverter!!.yuvToRgb(image.image!!, bm)
97+
// return bm;
7998
}
8099

81100
/** Rotates a bitmap if it is converted from a bytebuffer. */

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import org.nativescript.widgets.GridLayout
2929

3030
abstract class CameraBase @JvmOverloads constructor(
3131
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
32-
) : GridLayout(context) {
32+
) : GridLayout(context, attrs) {
3333
var enableAudio: Boolean = true
3434
abstract var retrieveLatestImage: Boolean
3535
internal var latestImage: Bitmap? = null

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
509509
}
510510

511511
if (retrieveLatestImage) {
512-
latestImage = BitmapUtils.getBitmap(it, jpegQuality)
512+
latestImage = BitmapUtils.getBitmap(context, it, jpegQuality)
513513
}
514514

515515
if (it.image != null) {
@@ -1137,14 +1137,18 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
11371137
object : ImageCapture.OnImageCapturedCallback() {
11381138
@SuppressLint("UnsafeOptInUsageError")
11391139
override fun onCaptureSuccess(image: ImageProxy) {
1140-
if (!savePhotoToDisk) {
1141-
listener?.onCameraPhotoImage(
1142-
bitmapFromProxy(image),
1143-
image.imageInfo
1144-
)
1145-
image.close()
1146-
} else {
1147-
processImageProxy(image, fileName, autoSquareCrop, saveToGallery)
1140+
try {
1141+
if (!savePhotoToDisk) {
1142+
listener?.onCameraPhotoImage(
1143+
bitmapFromProxy(image),
1144+
image.imageInfo
1145+
)
1146+
image.close()
1147+
} else {
1148+
processImageProxy(image, fileName, autoSquareCrop, saveToGallery)
1149+
}
1150+
} catch (exception: java.lang.Exception) {
1151+
listener?.onCameraError("Failed to take photo image", exception)
11481152
}
11491153
}
11501154

@@ -1184,7 +1188,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
11841188

11851189

11861190
@OptIn(ExperimentalGetImage::class) private fun bitmapFromProxy(image: ImageProxy): Bitmap {
1187-
var bm = BitmapUtils.getBitmap(image, jpegQuality);
1191+
var bm = BitmapUtils.getBitmap(context, image, jpegQuality);
11881192
// var bm = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
11891193
// yuvToRgbConverter.yuvToRgb(image.image!!, bm)
11901194
val matrix = Matrix()
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.nativescript.cameraview
2+
3+
import android.graphics.ImageFormat
4+
import android.media.Image
5+
import androidx.annotation.IntDef
6+
import java.nio.ByteBuffer
7+
8+
/*
9+
This file is converted from part of https://github.com/gordinmitya/yuv2buf.
10+
Follow the link to find demo app, performance benchmarks and unit tests.
11+
12+
Intro to YUV image formats:
13+
YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12.
14+
420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V.
15+
16+
* I420 format represents an image as Y plane followed by U then followed by V plane
17+
without chroma channels interleaving.
18+
For example:
19+
Y Y Y Y
20+
Y Y Y Y
21+
U U V V
22+
23+
* NV21 format represents an image as Y plane followed by V and U interleaved. First V then U.
24+
For example:
25+
Y Y Y Y
26+
Y Y Y Y
27+
V U V U
28+
29+
* YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V)
30+
31+
Visualization of these 4 formats:
32+
https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg
33+
34+
It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888.
35+
https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
36+
37+
Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN)
38+
the conversion is done into these formats.
39+
40+
More about each format: https://www.fourcc.org/yuv.php
41+
*/
42+
43+
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
44+
@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888)
45+
annotation class YuvType
46+
47+
class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) {
48+
@YuvType
49+
val type: Int
50+
val buffer: ByteBuffer
51+
52+
init {
53+
val wrappedImage = ImageWrapper(image)
54+
55+
type = if (wrappedImage.u.pixelStride == 1) {
56+
ImageFormat.YUV_420_888
57+
} else {
58+
ImageFormat.NV21
59+
}
60+
val size = image.width * image.height * 3 / 2
61+
buffer = if (
62+
dstBuffer == null || dstBuffer.capacity() < size ||
63+
dstBuffer.isReadOnly || !dstBuffer.isDirect
64+
) {
65+
ByteBuffer.allocateDirect(size) }
66+
else {
67+
dstBuffer
68+
}
69+
buffer.rewind()
70+
71+
removePadding(wrappedImage)
72+
}
73+
74+
// Input buffers are always direct as described in
75+
// https://developer.android.com/reference/android/media/Image.Plane#getBuffer()
76+
private fun removePadding(image: ImageWrapper) {
77+
val sizeLuma = image.y.width * image.y.height
78+
val sizeChroma = image.u.width * image.u.height
79+
if (image.y.rowStride > image.y.width) {
80+
removePaddingCompact(image.y, buffer, 0)
81+
} else {
82+
buffer.position(0)
83+
buffer.put(image.y.buffer)
84+
}
85+
if (type == ImageFormat.YUV_420_888) {
86+
if (image.u.rowStride > image.u.width) {
87+
removePaddingCompact(image.u, buffer, sizeLuma)
88+
removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma)
89+
} else {
90+
buffer.position(sizeLuma)
91+
buffer.put(image.u.buffer)
92+
buffer.position(sizeLuma + sizeChroma)
93+
buffer.put(image.v.buffer)
94+
}
95+
} else {
96+
if (image.u.rowStride > image.u.width * 2) {
97+
removePaddingNotCompact(image, buffer, sizeLuma)
98+
} else {
99+
buffer.position(sizeLuma)
100+
var uv = image.v.buffer
101+
val properUVSize = image.v.height * image.v.rowStride - 1
102+
if (uv.capacity() > properUVSize) {
103+
uv = clipBuffer(image.v.buffer, 0, properUVSize)
104+
}
105+
buffer.put(uv)
106+
val lastOne = image.u.buffer[image.u.buffer.capacity() - 1]
107+
buffer.put(buffer.capacity() - 1, lastOne)
108+
}
109+
}
110+
buffer.rewind()
111+
}
112+
113+
private fun removePaddingCompact(
114+
plane: PlaneWrapper,
115+
dst: ByteBuffer,
116+
offset: Int
117+
) {
118+
require(plane.pixelStride == 1) {
119+
"use removePaddingCompact with pixelStride == 1"
120+
}
121+
122+
val src = plane.buffer
123+
val rowStride = plane.rowStride
124+
var row: ByteBuffer
125+
dst.position(offset)
126+
for (i in 0 until plane.height) {
127+
row = clipBuffer(src, i * rowStride, plane.width)
128+
dst.put(row)
129+
}
130+
}
131+
132+
private fun removePaddingNotCompact(
133+
image: ImageWrapper,
134+
dst: ByteBuffer,
135+
offset: Int
136+
) {
137+
require(image.u.pixelStride == 2) {
138+
"use removePaddingNotCompact pixelStride == 2"
139+
}
140+
val width = image.u.width
141+
val height = image.u.height
142+
val rowStride = image.u.rowStride
143+
var row: ByteBuffer
144+
dst.position(offset)
145+
for (i in 0 until height - 1) {
146+
row = clipBuffer(image.v.buffer, i * rowStride, width * 2)
147+
dst.put(row)
148+
}
149+
row = clipBuffer(image.u.buffer, (height - 1) * rowStride - 1, width * 2)
150+
dst.put(row)
151+
}
152+
153+
private fun clipBuffer(buffer: ByteBuffer, start: Int, size: Int): ByteBuffer {
154+
val duplicate = buffer.duplicate()
155+
duplicate.position(start)
156+
duplicate.limit(start + size)
157+
return duplicate.slice()
158+
}
159+
160+
private class ImageWrapper(image:Image) {
161+
val width= image.width
162+
val height = image.height
163+
val y = PlaneWrapper(width, height, image.planes[0])
164+
val u = PlaneWrapper(width / 2, height / 2, image.planes[1])
165+
val v = PlaneWrapper(width / 2, height / 2, image.planes[2])
166+
167+
// Check this is a supported image format
168+
// https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
169+
init {
170+
require(y.pixelStride == 1) {
171+
"Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead."
172+
}
173+
require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) {
174+
"U and V planes must have the same pixel and row strides " +
175+
"but got pixel=${u.pixelStride} row=${u.rowStride} for U " +
176+
"and pixel=${v.pixelStride} and row=${v.rowStride} for V"
177+
}
178+
require(u.pixelStride == 1 || u.pixelStride == 2) {
179+
"Supported" + " pixel strides for U and V planes are 1 and 2"
180+
}
181+
}
182+
}
183+
184+
private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) {
185+
val width = width
186+
val height = height
187+
val buffer: ByteBuffer = plane.buffer
188+
val rowStride = plane.rowStride
189+
val pixelStride = plane.pixelStride
190+
}
191+
}

0 commit comments

Comments
 (0)