1 /*
2  * Copyright 2014 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 /* Original code copied from NDK Native-media sample code */
18 
19 //#define LOG_NDEBUG 0
20 #define TAG "CodecUtilsJNI"
21 #include <log/log.h>
22 
23 #include <stdint.h>
24 #include <sys/types.h>
25 #include <jni.h>
26 
27 #include <ScopedLocalRef.h>
28 #include <JNIHelp.h>
29 
30 #include <math.h>
31 
32 #include "md5_utils.h"
33 
34 typedef ssize_t offs_t;
35 
36 struct NativeImage {
37     struct crop {
38         int left;
39         int top;
40         int right;
41         int bottom;
42     } crop;
43     struct plane {
44         const uint8_t *buffer;
45         size_t size;
46         ssize_t colInc;
47         ssize_t rowInc;
48         offs_t cropOffs;
49         size_t cropWidth;
50         size_t cropHeight;
51     } plane[3];
52     int width;
53     int height;
54     int format;
55     long timestamp;
56     size_t numPlanes;
57 };
58 
59 struct ChecksumAlg {
60     virtual void init() = 0;
61     virtual void update(uint8_t c) = 0;
62     virtual uint32_t checksum() = 0;
63     virtual size_t length() = 0;
64 protected:
~ChecksumAlgChecksumAlg65     virtual ~ChecksumAlg() {}
66 };
67 
68 struct Adler32 : ChecksumAlg {
Adler32Adler3269     Adler32() {
70         init();
71     }
initAdler3272     void init() {
73         a = 1;
74         len = b = 0;
75     }
updateAdler3276     void update(uint8_t c) {
77         a += c;
78         b += a;
79         ++len;
80     }
checksumAdler3281     uint32_t checksum() {
82         return (a % 65521) + ((b % 65521) << 16);
83     }
lengthAdler3284     size_t length() {
85         return len;
86     }
87 private:
88     uint32_t a, b;
89     size_t len;
90 };
91 
92 static struct ImageFieldsAndMethods {
93     // android.graphics.ImageFormat
94     int YUV_420_888;
95     // android.media.Image
96     jmethodID methodWidth;
97     jmethodID methodHeight;
98     jmethodID methodFormat;
99     jmethodID methodTimestamp;
100     jmethodID methodPlanes;
101     jmethodID methodCrop;
102     // android.media.Image.Plane
103     jmethodID methodBuffer;
104     jmethodID methodPixelStride;
105     jmethodID methodRowStride;
106     // android.graphics.Rect
107     jfieldID fieldLeft;
108     jfieldID fieldTop;
109     jfieldID fieldRight;
110     jfieldID fieldBottom;
111 } gFields;
112 static bool gFieldsInitialized = false;
113 
initializeGlobalFields(JNIEnv * env)114 void initializeGlobalFields(JNIEnv *env) {
115     if (gFieldsInitialized) {
116         return;
117     }
118     {   // ImageFormat
119         jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat");
120         const jfieldID fieldYUV420888 = env->GetStaticFieldID(imageFormatClazz, "YUV_420_888", "I");
121         gFields.YUV_420_888 = env->GetStaticIntField(imageFormatClazz, fieldYUV420888);
122         env->DeleteLocalRef(imageFormatClazz);
123         imageFormatClazz = NULL;
124     }
125 
126     {   // Image
127         jclass imageClazz = env->FindClass("android/media/cts/CodecImage");
128         gFields.methodWidth  = env->GetMethodID(imageClazz, "getWidth", "()I");
129         gFields.methodHeight = env->GetMethodID(imageClazz, "getHeight", "()I");
130         gFields.methodFormat = env->GetMethodID(imageClazz, "getFormat", "()I");
131         gFields.methodTimestamp = env->GetMethodID(imageClazz, "getTimestamp", "()J");
132         gFields.methodPlanes = env->GetMethodID(
133                 imageClazz, "getPlanes", "()[Landroid/media/cts/CodecImage$Plane;");
134         gFields.methodCrop   = env->GetMethodID(
135                 imageClazz, "getCropRect", "()Landroid/graphics/Rect;");
136         env->DeleteLocalRef(imageClazz);
137         imageClazz = NULL;
138     }
139 
140     {   // Image.Plane
141         jclass planeClazz = env->FindClass("android/media/cts/CodecImage$Plane");
142         gFields.methodBuffer = env->GetMethodID(planeClazz, "getBuffer", "()Ljava/nio/ByteBuffer;");
143         gFields.methodPixelStride = env->GetMethodID(planeClazz, "getPixelStride", "()I");
144         gFields.methodRowStride = env->GetMethodID(planeClazz, "getRowStride", "()I");
145         env->DeleteLocalRef(planeClazz);
146         planeClazz = NULL;
147     }
148 
149     {   // Rect
150         jclass rectClazz = env->FindClass("android/graphics/Rect");
151         gFields.fieldLeft   = env->GetFieldID(rectClazz, "left", "I");
152         gFields.fieldTop    = env->GetFieldID(rectClazz, "top", "I");
153         gFields.fieldRight  = env->GetFieldID(rectClazz, "right", "I");
154         gFields.fieldBottom = env->GetFieldID(rectClazz, "bottom", "I");
155         env->DeleteLocalRef(rectClazz);
156         rectClazz = NULL;
157     }
158     gFieldsInitialized = true;
159 }
160 
getNativeImage(JNIEnv * env,jobject image,jobject area=NULL)161 NativeImage *getNativeImage(JNIEnv *env, jobject image, jobject area = NULL) {
162     if (image == NULL) {
163         jniThrowNullPointerException(env, "image is null");
164         return NULL;
165     }
166 
167     initializeGlobalFields(env);
168 
169     NativeImage *img = new NativeImage;
170     img->format = env->CallIntMethod(image, gFields.methodFormat);
171     img->width  = env->CallIntMethod(image, gFields.methodWidth);
172     img->height = env->CallIntMethod(image, gFields.methodHeight);
173     img->timestamp = env->CallLongMethod(image, gFields.methodTimestamp);
174 
175     jobject cropRect = NULL;
176     if (area == NULL) {
177         cropRect = env->CallObjectMethod(image, gFields.methodCrop);
178         area = cropRect;
179     }
180 
181     img->crop.left   = env->GetIntField(area, gFields.fieldLeft);
182     img->crop.top    = env->GetIntField(area, gFields.fieldTop);
183     img->crop.right  = env->GetIntField(area, gFields.fieldRight);
184     img->crop.bottom = env->GetIntField(area, gFields.fieldBottom);
185     if (img->crop.right == 0 && img->crop.bottom == 0) {
186         img->crop.right  = img->width;
187         img->crop.bottom = img->height;
188     }
189 
190     if (cropRect != NULL) {
191         env->DeleteLocalRef(cropRect);
192         cropRect = NULL;
193     }
194 
195     if (img->format != gFields.YUV_420_888) {
196         jniThrowException(
197                 env, "java/lang/UnsupportedOperationException",
198                 "only support YUV_420_888 images");
199         delete img;
200         img = NULL;
201         return NULL;
202     }
203     img->numPlanes = 3;
204 
205     ScopedLocalRef<jobjectArray> planesArray(
206             env, (jobjectArray)env->CallObjectMethod(image, gFields.methodPlanes));
207     int xDecim = 0;
208     int yDecim = 0;
209     for (size_t ix = 0; ix < img->numPlanes; ++ix) {
210         ScopedLocalRef<jobject> plane(
211                 env, env->GetObjectArrayElement(planesArray.get(), (jsize)ix));
212         img->plane[ix].colInc = env->CallIntMethod(plane.get(), gFields.methodPixelStride);
213         img->plane[ix].rowInc = env->CallIntMethod(plane.get(), gFields.methodRowStride);
214         ScopedLocalRef<jobject> buffer(
215                 env, env->CallObjectMethod(plane.get(), gFields.methodBuffer));
216 
217         img->plane[ix].buffer = (const uint8_t *)env->GetDirectBufferAddress(buffer.get());
218         img->plane[ix].size = env->GetDirectBufferCapacity(buffer.get());
219 
220         img->plane[ix].cropOffs =
221             (img->crop.left >> xDecim) * img->plane[ix].colInc
222                     + (img->crop.top >> yDecim) * img->plane[ix].rowInc;
223         img->plane[ix].cropHeight =
224             ((img->crop.bottom + (1 << yDecim) - 1) >> yDecim) - (img->crop.top >> yDecim);
225         img->plane[ix].cropWidth =
226             ((img->crop.right + (1 << xDecim) - 1) >> xDecim) - (img->crop.left >> xDecim);
227 
228         // sanity check on increments
229         ssize_t widthOffs =
230             (((img->width + (1 << xDecim) - 1) >> xDecim) - 1) * img->plane[ix].colInc;
231         ssize_t heightOffs =
232             (((img->height + (1 << yDecim) - 1) >> yDecim) - 1) * img->plane[ix].rowInc;
233         if (widthOffs < 0 || heightOffs < 0
234                 || widthOffs + heightOffs >= (ssize_t)img->plane[ix].size) {
235             jniThrowException(
236                     env, "java/lang/IndexOutOfBoundsException", "plane exceeds bytearray");
237             delete img;
238             img = NULL;
239             return NULL;
240         }
241         xDecim = yDecim = 1;
242     }
243     return img;
244 }
245 
Java_android_media_cts_CodecUtils_getImageChecksumAlder32(JNIEnv * env,jclass,jobject image)246 extern "C" jint Java_android_media_cts_CodecUtils_getImageChecksumAlder32(JNIEnv *env,
247         jclass /*clazz*/, jobject image)
248 {
249     NativeImage *img = getNativeImage(env, image);
250     if (img == NULL) {
251         return 0;
252     }
253 
254     Adler32 adler;
255     for (size_t ix = 0; ix < img->numPlanes; ++ix) {
256         const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs;
257         for (size_t y = img->plane[ix].cropHeight; y > 0; --y) {
258             const uint8_t *col = row;
259             ssize_t colInc = img->plane[ix].colInc;
260             for (size_t x = img->plane[ix].cropWidth; x > 0; --x) {
261                 adler.update(*col);
262                 col += colInc;
263             }
264             row += img->plane[ix].rowInc;
265         }
266     }
267     ALOGV("adler %zu/%u", adler.length(), adler.checksum());
268     return adler.checksum();
269 }
270 
Java_android_media_cts_CodecUtils_getImageChecksumMD5(JNIEnv * env,jclass,jobject image)271 extern "C" jstring Java_android_media_cts_CodecUtils_getImageChecksumMD5(JNIEnv *env,
272         jclass /*clazz*/, jobject image)
273 {
274     NativeImage *img = getNativeImage(env, image);
275     if (img == NULL) {
276         return 0;
277     }
278 
279     MD5Context md5;
280     char res[33];
281     MD5Init(&md5);
282 
283     for (size_t ix = 0; ix < img->numPlanes; ++ix) {
284         const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs;
285         for (size_t y = img->plane[ix].cropHeight; y > 0; --y) {
286             const uint8_t *col = row;
287             ssize_t colInc = img->plane[ix].colInc;
288             for (size_t x = img->plane[ix].cropWidth; x > 0; --x) {
289                 MD5Update(&md5, col, 1);
290                 col += colInc;
291             }
292             row += img->plane[ix].rowInc;
293         }
294     }
295 
296     static const char hex[16] = {
297         '0', '1', '2', '3', '4', '5', '6', '7',
298         '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
299     };
300     uint8_t tmp[16];
301 
302     MD5Final(tmp, &md5);
303     for (int i = 0; i < 16; i++) {
304         res[i * 2 + 0] = hex[tmp[i] >> 4];
305         res[i * 2 + 1] = hex[tmp[i] & 0xf];
306     }
307     res[32] = 0;
308 
309     return env->NewStringUTF(res);
310 }
311 
312 /* tiled copy that loops around source image boundary */
Java_android_media_cts_CodecUtils_copyFlexYUVImage(JNIEnv * env,jclass,jobject target,jobject source)313 extern "C" void Java_android_media_cts_CodecUtils_copyFlexYUVImage(JNIEnv *env,
314         jclass /*clazz*/, jobject target, jobject source)
315 {
316     NativeImage *tgt = getNativeImage(env, target);
317     NativeImage *src = getNativeImage(env, source);
318     if (tgt != NULL && src != NULL) {
319         ALOGV("copyFlexYUVImage %dx%d (%d,%d..%d,%d) (%zux%zu) %+zd%+zd %+zd%+zd %+zd%+zd <= "
320                 "%dx%d (%d, %d..%d, %d) (%zux%zu) %+zd%+zd %+zd%+zd %+zd%+zd",
321                 tgt->width, tgt->height,
322                 tgt->crop.left, tgt->crop.top, tgt->crop.right, tgt->crop.bottom,
323                 tgt->plane[0].cropWidth, tgt->plane[0].cropHeight,
324                 tgt->plane[0].rowInc, tgt->plane[0].colInc,
325                 tgt->plane[1].rowInc, tgt->plane[1].colInc,
326                 tgt->plane[2].rowInc, tgt->plane[2].colInc,
327                 src->width, src->height,
328                 src->crop.left, src->crop.top, src->crop.right, src->crop.bottom,
329                 src->plane[0].cropWidth, src->plane[0].cropHeight,
330                 src->plane[0].rowInc, src->plane[0].colInc,
331                 src->plane[1].rowInc, src->plane[1].colInc,
332                 src->plane[2].rowInc, src->plane[2].colInc);
333         for (size_t ix = 0; ix < tgt->numPlanes; ++ix) {
334             uint8_t *row = const_cast<uint8_t *>(tgt->plane[ix].buffer) + tgt->plane[ix].cropOffs;
335             for (size_t y = 0; y < tgt->plane[ix].cropHeight; ++y) {
336                 uint8_t *col = row;
337                 ssize_t colInc = tgt->plane[ix].colInc;
338                 const uint8_t *srcRow = (src->plane[ix].buffer + src->plane[ix].cropOffs
339                         + src->plane[ix].rowInc * (y % src->plane[ix].cropHeight));
340                 for (size_t x = 0; x < tgt->plane[ix].cropWidth; ++x) {
341                     *col = srcRow[src->plane[ix].colInc * (x % src->plane[ix].cropWidth)];
342                     col += colInc;
343                 }
344                 row += tgt->plane[ix].rowInc;
345             }
346         }
347     }
348 }
349 
Java_android_media_cts_CodecUtils_fillImageRectWithYUV(JNIEnv * env,jclass,jobject image,jobject area,jint y,jint u,jint v)350 extern "C" void Java_android_media_cts_CodecUtils_fillImageRectWithYUV(JNIEnv *env,
351         jclass /*clazz*/, jobject image, jobject area, jint y, jint u, jint v)
352 {
353     NativeImage *img = getNativeImage(env, image, area);
354     if (img == NULL) {
355         return;
356     }
357 
358     for (size_t ix = 0; ix < img->numPlanes; ++ix) {
359         const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs;
360         uint8_t val = ix == 0 ? y : ix == 1 ? u : v;
361         for (size_t y = img->plane[ix].cropHeight; y > 0; --y) {
362             uint8_t *col = (uint8_t *)row;
363             ssize_t colInc = img->plane[ix].colInc;
364             for (size_t x = img->plane[ix].cropWidth; x > 0; --x) {
365                 *col = val;
366                 col += colInc;
367             }
368             row += img->plane[ix].rowInc;
369         }
370     }
371 }
372 
getRawStats(NativeImage * img,jlong rawStats[10])373 void getRawStats(NativeImage *img, jlong rawStats[10])
374 {
375     // this works best if crop area is even
376 
377     uint64_t sum_x[3]  = { 0, 0, 0 }; // Y, U, V
378     uint64_t sum_xx[3] = { 0, 0, 0 }; // YY, UU, VV
379     uint64_t sum_xy[3] = { 0, 0, 0 }; // YU, YV, UV
380 
381     const uint8_t *yrow = img->plane[0].buffer + img->plane[0].cropOffs;
382     const uint8_t *urow = img->plane[1].buffer + img->plane[1].cropOffs;
383     const uint8_t *vrow = img->plane[2].buffer + img->plane[2].cropOffs;
384 
385     ssize_t ycolInc = img->plane[0].colInc;
386     ssize_t ucolInc = img->plane[1].colInc;
387     ssize_t vcolInc = img->plane[2].colInc;
388 
389     ssize_t yrowInc = img->plane[0].rowInc;
390     ssize_t urowInc = img->plane[1].rowInc;
391     ssize_t vrowInc = img->plane[2].rowInc;
392 
393     size_t rightOdd = img->crop.right & 1;
394     size_t bottomOdd = img->crop.bottom & 1;
395 
396     for (size_t y = img->plane[0].cropHeight; y; --y) {
397         uint8_t *ycol = (uint8_t *)yrow;
398         uint8_t *ucol = (uint8_t *)urow;
399         uint8_t *vcol = (uint8_t *)vrow;
400 
401         for (size_t x = img->plane[0].cropWidth; x; --x) {
402             uint64_t Y = *ycol;
403             uint64_t U = *ucol;
404             uint64_t V = *vcol;
405 
406             sum_x[0] += Y;
407             sum_x[1] += U;
408             sum_x[2] += V;
409             sum_xx[0] += Y * Y;
410             sum_xx[1] += U * U;
411             sum_xx[2] += V * V;
412             sum_xy[0] += Y * U;
413             sum_xy[1] += Y * V;
414             sum_xy[2] += U * V;
415 
416             ycol += ycolInc;
417             if (rightOdd ^ (x & 1)) {
418                 ucol += ucolInc;
419                 vcol += vcolInc;
420             }
421         }
422 
423         yrow += yrowInc;
424         if (bottomOdd ^ (y & 1)) {
425             urow += urowInc;
426             vrow += vrowInc;
427         }
428     }
429 
430     rawStats[0] = img->plane[0].cropWidth * (uint64_t)img->plane[0].cropHeight;
431     for (size_t i = 0; i < 3; i++) {
432         rawStats[i + 1] = sum_x[i];
433         rawStats[i + 4] = sum_xx[i];
434         rawStats[i + 7] = sum_xy[i];
435     }
436 }
437 
Raw2YUVStats(jlong rawStats[10],jfloat stats[9])438 bool Raw2YUVStats(jlong rawStats[10], jfloat stats[9]) {
439     int64_t sum_x[3], sum_xx[3]; // Y, U, V
440     int64_t sum_xy[3];           // YU, YV, UV
441 
442     int64_t num = rawStats[0];   // #Y,U,V
443     for (size_t i = 0; i < 3; i++) {
444         sum_x[i] = rawStats[i + 1];
445         sum_xx[i] = rawStats[i + 4];
446         sum_xy[i] = rawStats[i + 7];
447     }
448 
449     if (num > 0) {
450         stats[0] = sum_x[0] / (float)num;  // y average
451         stats[1] = sum_x[1] / (float)num;  // u average
452         stats[2] = sum_x[2] / (float)num;  // v average
453 
454         // 60 bits for 4Mpixel image
455         // adding 1 to avoid degenerate case when deviation is 0
456         stats[3] = sqrtf((sum_xx[0] + 1) * num - sum_x[0] * sum_x[0]) / num; // y stdev
457         stats[4] = sqrtf((sum_xx[1] + 1) * num - sum_x[1] * sum_x[1]) / num; // u stdev
458         stats[5] = sqrtf((sum_xx[2] + 1) * num - sum_x[2] * sum_x[2]) / num; // v stdev
459 
460         // yu covar
461         stats[6] = (float)(sum_xy[0] + 1 - sum_x[0] * sum_x[1] / num) / num / stats[3] / stats[4];
462         // yv covar
463         stats[7] = (float)(sum_xy[1] + 1 - sum_x[0] * sum_x[2] / num) / num / stats[3] / stats[5];
464         // uv covar
465         stats[8] = (float)(sum_xy[2] + 1 - sum_x[1] * sum_x[2] / num) / num / stats[4] / stats[5];
466         return true;
467     } else {
468         return false;
469     }
470 }
471 
Java_android_media_cts_CodecUtils_getRawStats(JNIEnv * env,jclass,jobject image,jobject area)472 extern "C" jobject Java_android_media_cts_CodecUtils_getRawStats(JNIEnv *env,
473         jclass /*clazz*/, jobject image, jobject area)
474 {
475     NativeImage *img = getNativeImage(env, image, area);
476     if (img == NULL) {
477         return NULL;
478     }
479 
480     jlong rawStats[10];
481     getRawStats(img, rawStats);
482     jlongArray jstats = env->NewLongArray(10);
483     if (jstats != NULL) {
484         env->SetLongArrayRegion(jstats, 0, 10, rawStats);
485     }
486     return jstats;
487 }
488 
Java_android_media_cts_CodecUtils_getYUVStats(JNIEnv * env,jclass,jobject image,jobject area)489 extern "C" jobject Java_android_media_cts_CodecUtils_getYUVStats(JNIEnv *env,
490         jclass /*clazz*/, jobject image, jobject area)
491 {
492     NativeImage *img = getNativeImage(env, image, area);
493     if (img == NULL) {
494         return NULL;
495     }
496 
497     jlong rawStats[10];
498     getRawStats(img, rawStats);
499     jfloat stats[9];
500     jfloatArray jstats = NULL;
501     if (Raw2YUVStats(rawStats, stats)) {
502         jstats = env->NewFloatArray(9);
503         if (jstats != NULL) {
504             env->SetFloatArrayRegion(jstats, 0, 9, stats);
505         }
506     } else {
507         jniThrowRuntimeException(env, "empty area");
508     }
509 
510     return jstats;
511 }
512 
Java_android_media_cts_CodecUtils_Raw2YUVStats(JNIEnv * env,jclass,jobject jrawStats)513 extern "C" jobject Java_android_media_cts_CodecUtils_Raw2YUVStats(JNIEnv *env,
514         jclass /*clazz*/, jobject jrawStats)
515 {
516     jfloatArray jstats = NULL;
517     jlong rawStats[10];
518     env->GetLongArrayRegion((jlongArray)jrawStats, 0, 10, rawStats);
519     if (!env->ExceptionCheck()) {
520         jfloat stats[9];
521         if (Raw2YUVStats(rawStats, stats)) {
522             jstats = env->NewFloatArray(9);
523             if (jstats != NULL) {
524                 env->SetFloatArrayRegion(jstats, 0, 9, stats);
525             }
526         } else {
527             jniThrowRuntimeException(env, "no raw statistics");
528         }
529     }
530     return jstats;
531 }
532