1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.camera.integration.extensions.utils
18 
19 import android.graphics.ImageFormat
20 import android.graphics.Rect
21 import android.graphics.YuvImage
22 import android.media.Image
23 import androidx.annotation.IntRange
24 import androidx.camera.core.ImageProxy
25 import java.io.ByteArrayOutputStream
26 
27 /** Image util functions */
28 object ImageUtil {
29 
30     /** Converts JPEG [Image] to [ByteArray] */
31     @JvmStatic
jpegImageToJpegByteArraynull32     fun jpegImageToJpegByteArray(image: Image): ByteArray {
33         require(image.format == ImageFormat.JPEG) {
34             "Incorrect image format of the input image proxy: ${image.format}"
35         }
36         val planes = image.planes
37         val buffer = planes[0].buffer
38         val data = ByteArray(buffer.capacity())
39         buffer.rewind()
40         buffer[data]
41         return data
42     }
43 
44     /**
45      * Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image will be
46      * cropped if a non-null crop rectangle is specified. The output JPEG byte array will be
47      * compressed by the specified quality value.
48      */
49     @JvmStatic
yuvImageToJpegByteArraynull50     fun yuvImageToJpegByteArray(
51         image: Image,
52         @IntRange(from = 1, to = 100) jpegQuality: Int
53     ): ByteArray {
54         require(image.format == ImageFormat.YUV_420_888) {
55             "Incorrect image format of the input image proxy: ${image.format}"
56         }
57         return nv21ToJpeg(yuv_420_888toNv21(image), image.width, image.height, jpegQuality)
58     }
59 
60     /** Converts nv21 byte array to JPEG format. */
61     @JvmStatic
nv21ToJpegnull62     private fun nv21ToJpeg(
63         nv21: ByteArray,
64         width: Int,
65         height: Int,
66         @IntRange(from = 1, to = 100) jpegQuality: Int
67     ): ByteArray {
68         val out = ByteArrayOutputStream()
69         val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
70         val success = yuv.compressToJpeg(Rect(0, 0, width, height), jpegQuality, out)
71 
72         if (!success) {
73             throw RuntimeException("YuvImage failed to encode jpeg.")
74         }
75         return out.toByteArray()
76     }
77 
78     /** Converts a YUV [Image] to NV21 byte array. */
79     @JvmStatic
yuv_420_888toNv21null80     private fun yuv_420_888toNv21(image: Image): ByteArray {
81         require(image.format == ImageFormat.YUV_420_888) {
82             "Incorrect image format of the input image proxy: ${image.format}"
83         }
84 
85         val yPlane = image.planes[0]
86         val uPlane = image.planes[1]
87         val vPlane = image.planes[2]
88         val yBuffer = yPlane.buffer
89         val uBuffer = uPlane.buffer
90         val vBuffer = vPlane.buffer
91         yBuffer.rewind()
92         uBuffer.rewind()
93         vBuffer.rewind()
94         val ySize = yBuffer.remaining()
95         var position = 0
96         // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
97         val nv21 = ByteArray(ySize + image.width * image.height / 2)
98 
99         // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
100         for (row in 0 until image.height) {
101             yBuffer[nv21, position, image.width]
102             position += image.width
103             yBuffer.position(Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride))
104         }
105         val chromaHeight = image.height / 2
106         val chromaWidth = image.width / 2
107         val vRowStride = vPlane.rowStride
108         val uRowStride = uPlane.rowStride
109         val vPixelStride = vPlane.pixelStride
110         val uPixelStride = uPlane.pixelStride
111 
112         // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
113         // perform faster bulk gets from the byte buffers.
114         val vLineBuffer = ByteArray(vRowStride)
115         val uLineBuffer = ByteArray(uRowStride)
116         for (row in 0 until chromaHeight) {
117             vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())]
118             uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
119             var vLineBufferPosition = 0
120             var uLineBufferPosition = 0
121             for (col in 0 until chromaWidth) {
122                 nv21[position++] = vLineBuffer[vLineBufferPosition]
123                 nv21[position++] = uLineBuffer[uLineBufferPosition]
124                 vLineBufferPosition += vPixelStride
125                 uLineBufferPosition += uPixelStride
126             }
127         }
128         return nv21
129     }
130 }
131