xref: /aosp_15_r20/cts/tests/tests/uirendering/src/android/uirendering/cts/testclasses/AImageDecoderTest.kt (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
<lambda>null2  * Copyright 2020 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 android.uirendering.cts.testclasses
18 
19 import android.content.res.AssetManager
20 import android.graphics.Bitmap
21 import android.graphics.BitmapFactory
22 import android.graphics.Color
23 import android.graphics.ImageDecoder
24 import android.graphics.Matrix
25 import android.graphics.Rect
26 import android.media.ExifInterface
27 import android.uirendering.cts.bitmapcomparers.MSSIMComparer
28 import android.uirendering.cts.bitmapverifiers.BitmapVerifier
29 import android.uirendering.cts.bitmapverifiers.ColorVerifier
30 import android.uirendering.cts.bitmapverifiers.GoldenImageVerifier
31 import android.uirendering.cts.bitmapverifiers.RectVerifier
32 import android.uirendering.cts.bitmapverifiers.RegionVerifier
33 import android.uirendering.cts.differencevisualizers.PassFailVisualizer
34 import android.uirendering.cts.util.BitmapDumper
35 import androidx.test.InstrumentationRegistry
36 import junitparams.JUnitParamsRunner
37 import junitparams.Parameters
38 import kotlin.test.assertEquals
39 import kotlin.test.assertTrue
40 import kotlin.test.fail
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 
44 @RunWith(JUnitParamsRunner::class)
45 class AImageDecoderTest {
46     init {
47         System.loadLibrary("ctsuirendering_jni")
48     }
49 
50     private val ANDROID_IMAGE_DECODER_SUCCESS = 0
51     private val ANDROID_IMAGE_DECODER_INVALID_CONVERSION = -3
52     private val ANDROID_IMAGE_DECODER_INVALID_SCALE = -4
53     private val ANDROID_IMAGE_DECODER_BAD_PARAMETER = -5
54     private val ANDROID_IMAGE_DECODER_FINISHED = -10
55     private val ANDROID_IMAGE_DECODER_INVALID_STATE = -11
56 
57     private val DEBUG_CAPTURE_IMAGES = false
58 
59     private fun getAssets(): AssetManager {
60         return InstrumentationRegistry.getTargetContext().getAssets()
61     }
62 
63     @Test
64     fun testNullDecoder() = nTestNullDecoder()
65 
66     @Test
67     fun testToString() = nTestToString()
68 
69     private enum class Crop {
70         Top, // Crop a section of the image that contains the top
71         Left, // Crop a section of the image that contains the left
72         None,
73     }
74 
75     /**
76      * Helper class to decode a scaled, cropped image to compare to AImageDecoder.
77      *
78      * Includes properties for getting the right scale and crop values to use in
79      * AImageDecoder.
80      */
81     private inner class DecodeAndCropper constructor(
82         image: String,
83         scale: Float,
84         crop: Crop
85     ) {
86         val bitmap: Bitmap
87         var targetWidth: Int = 0
88             private set
89         var targetHeight: Int = 0
90             private set
91         val cropRect: Rect?
92 
93         init {
94             val source = ImageDecoder.createSource(getAssets(), image)
95             val tmpBm = ImageDecoder.decodeBitmap(source) {
96                 decoder, info, _ ->
97                     decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
98                     if (scale == 1.0f) {
99                         targetWidth = info.size.width
100                         targetHeight = info.size.height
101                     } else {
102                         targetWidth = (info.size.width * scale).toInt()
103                         targetHeight = (info.size.height * scale).toInt()
104                         decoder.setTargetSize(targetWidth, targetHeight)
105                     }
106             }
107             cropRect = when (crop) {
108                 Crop.Top -> Rect((targetWidth / 3.0f).toInt(), 0,
109                         (targetWidth * 2 / 3.0f).toInt(),
110                         (targetHeight / 2.0f).toInt())
111                 Crop.Left -> Rect(0, (targetHeight / 3.0f).toInt(),
112                         (targetWidth / 2.0f).toInt(),
113                         (targetHeight * 2 / 3.0f).toInt())
114                 Crop.None -> null
115             }
116             if (cropRect == null) {
117                 bitmap = tmpBm
118             } else {
119                 // Crop using Bitmap, rather than ImageDecoder, because it uses
120                 // the same code as AImageDecoder for cropping.
121                 bitmap = Bitmap.createBitmap(tmpBm, cropRect.left, cropRect.top,
122                         cropRect.width(), cropRect.height())
123                 if (bitmap !== tmpBm) {
124                     tmpBm.recycle()
125                 }
126             }
127         }
128     }
129 
130     // Create a Bitmap with the same size and colorspace as bitmap.
131     private fun makeEmptyBitmap(bitmap: Bitmap) = Bitmap.createBitmap(
132         bitmap.width,
133         bitmap.height,
134                 bitmap.config!!,
135         true,
136         bitmap.colorSpace!!
137     )
138 
139     private fun setCrop(decoder: Long, rect: Rect): Int = with(rect) {
140         nSetCrop(decoder, left, top, right, bottom)
141     }
142 
143     /**
144      * Test that all frames in the image look as expected.
145      *
146      * @param image Name of the animated image file.
147      * @param frameName Template for creating the name of the expected image
148      *                  file for the i'th frame.
149      * @param numFrames Total number of frames in the animated image.
150      * @param scaleFactor The factor by which to scale the image.
151      * @param crop The crop setting to use.
152      * @param mssimThreshold The minimum MSSIM value to accept as similar. Some
153      *                       images do not match exactly, but they've been
154      *                       manually verified to look the same.
155      * @param testName Optional name of the calling test for BitmapDumper.
156      */
157     private fun decodeAndCropFrames(
158         image: String,
159         frameName: String,
160         numFrames: Int,
161         scaleFactor: Float,
162         crop: Crop,
163         mssimThreshold: Double,
164         testName: String = ""
165     ) {
166         val decodeAndCropper = DecodeAndCropper(image, scaleFactor, crop)
167         var expectedBm = decodeAndCropper.bitmap
168 
169         val asset = nOpenAsset(getAssets(), image)
170         val decoder = nCreateFromAsset(asset)
171         if (scaleFactor != 1.0f) {
172             with(decodeAndCropper) {
173                 assertEquals(
174                     nSetTargetSize(decoder, targetWidth, targetHeight),
175                         ANDROID_IMAGE_DECODER_SUCCESS
176                 )
177             }
178         }
179         with(decodeAndCropper.cropRect) {
180             this?.let {
181                 assertEquals(setCrop(decoder, this), ANDROID_IMAGE_DECODER_SUCCESS)
182             }
183         }
184 
185         val testBm = makeEmptyBitmap(decodeAndCropper.bitmap)
186 
187         var i = 0
188         while (true) {
189             nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
190             val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(mssimThreshold))
191             if (!verifier.verify(testBm)) {
192                 if (DEBUG_CAPTURE_IMAGES) {
193                     BitmapDumper.dumpBitmaps(expectedBm, testBm, PassFailVisualizer())
194                 }
195                 fail("$image has mismatch in frame $i")
196             }
197             expectedBm.recycle()
198 
199             i++
200             when (val result = nAdvanceFrame(decoder)) {
201                 ANDROID_IMAGE_DECODER_SUCCESS -> {
202                     assertTrue(i < numFrames, "Unexpected frame $i in $image")
203                     expectedBm = DecodeAndCropper(frameName.format(i), scaleFactor, crop).bitmap
204                 }
205                 ANDROID_IMAGE_DECODER_FINISHED -> {
206                     assertEquals(i, numFrames, "Expected $numFrames frames in $image; found $i")
207                     break
208                 }
209                 else -> fail("Unexpected error $result when advancing $image to frame $i")
210             }
211         }
212 
213         nDeleteDecoder(decoder)
214         nCloseAsset(asset)
215     }
216 
217     fun animationsAndFrames() = arrayOf(
218         arrayOf<Any>("animated.gif", "animated_%03d.gif", 4),
219         arrayOf<Any>("animated_webp.webp", "animated_%03d.gif", 4),
220         arrayOf<Any>("required_gif.gif", "required_%03d.png", 7),
221         arrayOf<Any>("required_webp.webp", "required_%03d.png", 7),
222         arrayOf<Any>("alphabetAnim.gif", "alphabetAnim_%03d.png", 13),
223         arrayOf<Any>("blendBG.webp", "blendBG_%03d.png", 7),
224         arrayOf<Any>("stoplight.webp", "stoplight_%03d.png", 3)
225     )
226 
227     @Test
228     @Parameters(method = "animationsAndFrames")
229     fun testDecodeFrames(image: String, frameName: String, numFrames: Int) {
230         decodeAndCropFrames(image, frameName, numFrames, 1.0f, Crop.None, .955)
231     }
232 
233     @Test
234     @Parameters(method = "animationsAndFrames")
235     fun testDecodeFramesScaleDown(image: String, frameName: String, numFrames: Int) {
236         // Perceptually, this image looks reasonable, but the MSSIM is low enough to be
237         // meaningless. It has been manually verified.
238         if (image == "alphabetAnim.gif") return
239         decodeAndCropFrames(
240             image,
241             frameName,
242             numFrames,
243             .5f,
244             Crop.None,
245             .749,
246                 "testDecodeFramesScaleDown"
247         )
248     }
249 
250     @Test
251     @Parameters(method = "animationsAndFrames")
252     fun testDecodeFramesScaleDown2(image: String, frameName: String, numFrames: Int) {
253         decodeAndCropFrames(image, frameName, numFrames, .75f, Crop.None, .749)
254     }
255 
256     @Test
257     @Parameters(method = "animationsAndFrames")
258     fun testDecodeFramesScaleUp(image: String, frameName: String, numFrames: Int) {
259         decodeAndCropFrames(image, frameName, numFrames, 2.0f, Crop.None, .875)
260     }
261 
262     @Test
263     @Parameters(method = "animationsAndFrames")
264     fun testDecodeFramesAndCropTop(image: String, frameName: String, numFrames: Int) {
265         decodeAndCropFrames(image, frameName, numFrames, 1.0f, Crop.Top, .934)
266     }
267 
268     @Test
269     @Parameters(method = "animationsAndFrames")
270     fun testDecodeFramesAndCropTopScaleDown(image: String, frameName: String, numFrames: Int) {
271         // Perceptually, this image looks reasonable, but the MSSIM is low enough to be
272         // meaningless. It has been manually verified.
273         if (image == "alphabetAnim.gif") return
274         decodeAndCropFrames(
275             image,
276             frameName,
277             numFrames,
278             .5f,
279             Crop.Top,
280             .749,
281                 "testDecodeFramesAndCropTopScaleDown"
282         )
283     }
284 
285     @Test
286     @Parameters(method = "animationsAndFrames")
287     fun testDecodeFramesAndCropTopScaleDown2(image: String, frameName: String, numFrames: Int) {
288         decodeAndCropFrames(image, frameName, numFrames, .75f, Crop.Top, .749)
289     }
290 
291     @Test
292     @Parameters(method = "animationsAndFrames")
293     fun testDecodeFramesAndCropTopScaleUp(image: String, frameName: String, numFrames: Int) {
294         decodeAndCropFrames(image, frameName, numFrames, 3.0f, Crop.Top, .908)
295     }
296 
297     @Test
298     @Parameters(method = "animationsAndFrames")
299     fun testDecodeFramesAndCropLeft(image: String, frameName: String, numFrames: Int) {
300         decodeAndCropFrames(image, frameName, numFrames, 1.0f, Crop.Left, .924)
301     }
302 
303     @Test
304     @Parameters(method = "animationsAndFrames")
305     fun testDecodeFramesAndCropLeftScaleDown(image: String, frameName: String, numFrames: Int) {
306         // Perceptually, this image looks reasonable, but the MSSIM is low enough to be
307         // meaningless. It has been manually verified.
308         if (image == "alphabetAnim.gif") return
309         decodeAndCropFrames(
310             image,
311             frameName,
312             numFrames,
313             .5f,
314             Crop.Left,
315             .596,
316                 "testDecodeFramesAndCropLeftScaleDown"
317         )
318     }
319 
320     @Test
321     @Parameters(method = "animationsAndFrames")
322     fun testDecodeFramesAndCropLeftScaleDown2(image: String, frameName: String, numFrames: Int) {
323         decodeAndCropFrames(image, frameName, numFrames, .75f, Crop.Left, .596)
324     }
325 
326     @Test
327     @Parameters(method = "animationsAndFrames")
328     fun testDecodeFramesAndCropLeftScaleUp(image: String, frameName: String, numFrames: Int) {
329         decodeAndCropFrames(image, frameName, numFrames, 3.0f, Crop.Left, .894)
330     }
331 
332     @Test
333     @Parameters(method = "animationsAndFrames")
334     fun testRewind(image: String, unused: String, numFrames: Int) {
335         val frame0 = with(ImageDecoder.createSource(getAssets(), image)) {
336             ImageDecoder.decodeBitmap(this) {
337                 decoder, _, _ ->
338                     decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
339             }
340         }
341 
342         // Regardless of the current frame, calling rewind and decoding should
343         // look like frame_0.
344         for (framesBeforeReset in 0 until numFrames) {
345             val asset = nOpenAsset(getAssets(), image)
346             val decoder = nCreateFromAsset(asset)
347             val testBm = makeEmptyBitmap(frame0)
348             for (i in 1..framesBeforeReset) {
349                 nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
350                 assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nAdvanceFrame(decoder))
351             }
352 
353             assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
354             nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
355 
356             val verifier = GoldenImageVerifier(frame0, MSSIMComparer(1.0))
357             assertTrue(
358                 verifier.verify(testBm),
359                 "Mismatch in $image after " +
360                         "decoding $framesBeforeReset and then rewinding!"
361             )
362 
363             nDeleteDecoder(decoder)
364             nCloseAsset(asset)
365         }
366     }
367 
368     @Test
369     @Parameters(method = "animationsAndFrames")
370     fun testDecodeReturnsFinishedAtEnd(image: String, unused: String, numFrames: Int) {
371         val asset = nOpenAsset(getAssets(), image)
372         val decoder = nCreateFromAsset(asset)
373         for (i in 0 until (numFrames - 1)) {
374             assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_SUCCESS)
375         }
376 
377         assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_FINISHED)
378 
379         // Create a Bitmap to decode into and verify that no decoding occurred.
380         val width = nGetWidth(decoder)
381         val height = nGetHeight(decoder)
382         val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true)
383         nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_FINISHED)
384 
385         nDeleteDecoder(decoder)
386         nCloseAsset(asset)
387 
388         // Every pixel should be transparent black, as no decoding happened.
389         assertTrue(ColorVerifier(0, 0).verify(bitmap))
390         bitmap.recycle()
391     }
392 
393     @Test
394     @Parameters(method = "animationsAndFrames")
395     fun testAdvanceReturnsFinishedAtEnd(image: String, unused: String, numFrames: Int) {
396         val asset = nOpenAsset(getAssets(), image)
397         val decoder = nCreateFromAsset(asset)
398         for (i in 0 until (numFrames - 1)) {
399             assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_SUCCESS)
400         }
401 
402         for (i in 0..1000) {
403             assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_FINISHED)
404         }
405 
406         nDeleteDecoder(decoder)
407         nCloseAsset(asset)
408     }
409 
410     fun nonAnimatedAssets() = arrayOf(
411         "blue-16bit-prophoto.png",
412         "green-p3.png",
413         "linear-rgba16f.png",
414         "orange-prophotorgb.png",
415         "animated_001.gif",
416         "animated_002.gif",
417         "sunset1.jpg"
418     )
419 
420     @Test
421     @Parameters(method = "nonAnimatedAssets")
422     fun testAdvanceFrameFailsNonAnimated(image: String) {
423         val asset = nOpenAsset(getAssets(), image)
424         val decoder = nCreateFromAsset(asset)
425         assertEquals(ANDROID_IMAGE_DECODER_BAD_PARAMETER, nAdvanceFrame(decoder))
426         nDeleteDecoder(decoder)
427         nCloseAsset(asset)
428     }
429 
430     @Test
431     @Parameters(method = "nonAnimatedAssets")
432     fun testRewindFailsNonAnimated(image: String) {
433         val asset = nOpenAsset(getAssets(), image)
434         val decoder = nCreateFromAsset(asset)
435         assertEquals(ANDROID_IMAGE_DECODER_BAD_PARAMETER, nRewind(decoder))
436         nDeleteDecoder(decoder)
437         nCloseAsset(asset)
438     }
439 
440     fun imagesAndSetters(): ArrayList<Any> {
441         val setters = arrayOf<(Long) -> Int>(
442             { decoder -> nSetUnpremultipliedRequired(decoder, true) },
443             { decoder ->
444                 val rect = Rect(0, 0, nGetWidth(decoder) / 2, nGetHeight(decoder) / 2)
445                 setCrop(decoder, rect)
446             },
447             { decoder ->
448                 val ANDROID_BITMAP_FORMAT_RGBA_F16 = 9
449                 nSetAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_F16)
450             },
451             { decoder ->
452                 nSetTargetSize(decoder, nGetWidth(decoder) / 2, nGetHeight(decoder) / 2)
453             },
454             { decoder ->
455                 val ADATASPACE_DISPLAY_P3 = 143261696
456                 nSetDataSpace(decoder, ADATASPACE_DISPLAY_P3)
457             }
458         )
459         val list = ArrayList<Any>()
460         for (animations in animationsAndFrames()) {
461             for (setter in setters) {
462                 list.add(arrayOf(animations[0], animations[2], setter))
463             }
464         }
465         return list
466     }
467 
468     @Test
469     @Parameters(method = "imagesAndSetters")
470     fun testSettersFailOnLatterFrames(image: String, numFrames: Int, setter: (Long) -> Int) {
471         // Verify that the setter succeeds on the first frame.
472         with(nOpenAsset(getAssets(), image)) {
473             val decoder = nCreateFromAsset(this)
474             assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, setter(decoder))
475             nDeleteDecoder(decoder)
476             nCloseAsset(this)
477         }
478 
479         for (framesBeforeSet in 1 until numFrames) {
480             val asset = nOpenAsset(getAssets(), image)
481             val decoder = nCreateFromAsset(asset)
482             for (i in 1..framesBeforeSet) {
483                 assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nAdvanceFrame(decoder))
484             }
485 
486             // Not on the first frame, so the setter fails.
487             assertEquals(ANDROID_IMAGE_DECODER_INVALID_STATE, setter(decoder))
488 
489             // Rewind to the beginning. Now the setter can succeed.
490             assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
491             assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, setter(decoder))
492 
493             nDeleteDecoder(decoder)
494             nCloseAsset(asset)
495         }
496     }
497 
498     fun unpremulTestFiles() = arrayOf(
499         "alphabetAnim.gif",
500         "animated_webp.webp",
501         "stoplight.webp"
502     )
503 
504     @Test
505     @Parameters(method = "unpremulTestFiles")
506     fun testUnpremul(image: String) {
507         val expectedBm = with(ImageDecoder.createSource(getAssets(), image)) {
508             ImageDecoder.decodeBitmap(this) {
509                 decoder, _, _ ->
510                     decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
511                     decoder.setUnpremultipliedRequired(true)
512             }
513         }
514 
515         val testBm = makeEmptyBitmap(expectedBm)
516 
517         val asset = nOpenAsset(getAssets(), image)
518         val decoder = nCreateFromAsset(asset)
519         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetUnpremultipliedRequired(decoder, true))
520         nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
521 
522         val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(1.0))
523         assertTrue(verifier.verify(testBm), "$image did not match in unpremul")
524 
525         nDeleteDecoder(decoder)
526         nCloseAsset(asset)
527     }
528 
529     fun imagesWithAlpha() = arrayOf(
530         "alphabetAnim.gif",
531         "animated_webp.webp",
532         "animated.gif"
533     )
534 
535     @Test
536     @Parameters(method = "imagesWithAlpha")
537     fun testUnpremulThenScaleFailsWithAlpha(image: String) {
538         val asset = nOpenAsset(getAssets(), image)
539         val decoder = nCreateFromAsset(asset)
540         val width = nGetWidth(decoder)
541         val height = nGetHeight(decoder)
542 
543         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetUnpremultipliedRequired(decoder, true))
544         assertEquals(
545             ANDROID_IMAGE_DECODER_INVALID_SCALE,
546                 nSetTargetSize(decoder, width * 2, height * 2)
547         )
548         nDeleteDecoder(decoder)
549         nCloseAsset(asset)
550     }
551 
552     @Test
553     @Parameters(method = "imagesWithAlpha")
554     fun testScaleThenUnpremulFailsWithAlpha(image: String) {
555         val asset = nOpenAsset(getAssets(), image)
556         val decoder = nCreateFromAsset(asset)
557         val width = nGetWidth(decoder)
558         val height = nGetHeight(decoder)
559 
560         assertEquals(
561             ANDROID_IMAGE_DECODER_SUCCESS,
562                 nSetTargetSize(decoder, width * 2, height * 2)
563         )
564         assertEquals(
565             ANDROID_IMAGE_DECODER_INVALID_CONVERSION,
566                 nSetUnpremultipliedRequired(decoder, true)
567         )
568         nDeleteDecoder(decoder)
569         nCloseAsset(asset)
570     }
571 
572     fun opaquePlusScale(): ArrayList<Any> {
573         val opaqueImages = arrayOf("sunset1.jpg", "blendBG.webp", "stoplight.webp")
574         val scales = arrayOf(.5f, .75f, 2.0f)
575         val list = ArrayList<Any>()
576         for (image in opaqueImages) {
577             for (scale in scales) {
578                 list.add(arrayOf(image, scale))
579             }
580         }
581         return list
582     }
583 
584     @Test
585     @Parameters(method = "opaquePlusScale")
586     fun testUnpremulPlusScaleOpaque(image: String, scale: Float) {
587         val expectedBm = with(ImageDecoder.createSource(getAssets(), image)) {
588             ImageDecoder.decodeBitmap(this) {
589                 decoder, info, _ ->
590                     decoder.isUnpremultipliedRequired = true
591                     decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
592                     val width = (info.size.width * scale).toInt()
593                     val height = (info.size.height * scale).toInt()
594                     decoder.setTargetSize(width, height)
595             }
596         }
597         val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(1.0))
598 
599         // Flipping the order of setting unpremul and scaling results in taking
600         // a different code path. Ensure both succeed.
601         val ops = listOf(
602             { decoder: Long -> nSetUnpremultipliedRequired(decoder, true) },
603             { decoder: Long -> nSetTargetSize(decoder, expectedBm.width, expectedBm.height) }
604         )
605 
606         for (order in setOf(ops, ops.asReversed())) {
607             val testBm = makeEmptyBitmap(expectedBm)
608             val asset = nOpenAsset(getAssets(), image)
609             val decoder = nCreateFromAsset(asset)
610             for (op in order) {
611                 assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, op(decoder))
612             }
613             nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
614             assertTrue(verifier.verify(testBm))
615 
616             nDeleteDecoder(decoder)
617             nCloseAsset(asset)
618             testBm.recycle()
619         }
620         expectedBm.recycle()
621     }
622 
623     @Test
624     fun testUnpremulPlusScaleWithFrameWithAlpha() {
625         // The first frame of this image is opaque, so unpremul + scale succeeds.
626         // But frame 3 has alpha, so decoding it with unpremul + scale fails.
627         val image = "blendBG.webp"
628         val scale = 2.0f
629         val asset = nOpenAsset(getAssets(), image)
630         val decoder = nCreateFromAsset(asset)
631         val width = (nGetWidth(decoder) * scale).toInt()
632         val height = (nGetHeight(decoder) * scale).toInt()
633 
634         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetUnpremultipliedRequired(decoder, true))
635         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nSetTargetSize(decoder, width, height))
636 
637         val testBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true)
638         for (i in 0 until 3) {
639             nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
640             assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nAdvanceFrame(decoder))
641         }
642         nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_INVALID_SCALE)
643 
644         nDeleteDecoder(decoder)
645         nCloseAsset(asset)
646     }
647 
648     @Test
649     @Parameters(method = "nonAnimatedAssets")
650     fun testGetFrameInfoSucceedsNonAnimated(image: String) {
651         val asset = nOpenAsset(getAssets(), image)
652         val decoder = nCreateFromAsset(asset)
653         val frameInfo = nCreateFrameInfo()
654         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nGetFrameInfo(decoder, frameInfo))
655 
656         if (image.startsWith("animated")) {
657             // Although these images have only one frame, they still contain encoded frame info.
658             val ANDROID_IMAGE_DECODER_INFINITE = Integer.MAX_VALUE
659             assertEquals(ANDROID_IMAGE_DECODER_INFINITE, nGetRepeatCount(decoder))
660             assertEquals(250_000_000L, nGetDuration(frameInfo))
661             assertEquals(ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, nGetDisposeOp(frameInfo))
662         } else {
663             // Since these are not animated and have no encoded frame info, they should use
664             // defaults.
665             assertEquals(0, nGetRepeatCount(decoder))
666             assertEquals(0L, nGetDuration(frameInfo))
667             assertEquals(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, nGetDisposeOp(frameInfo))
668         }
669 
670         nTestGetFrameRect(frameInfo, 0, 0, nGetWidth(decoder), nGetHeight(decoder))
671         if (image.endsWith("gif")) {
672             // GIFs do not support SRC, so they always report SRC_OVER.
673             assertEquals(ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER, nGetBlendOp(frameInfo))
674         } else {
675             assertEquals(ANDROID_IMAGE_DECODER_BLEND_OP_SRC, nGetBlendOp(frameInfo))
676         }
677         assertEquals(nGetAlpha(decoder), nGetFrameAlpha(frameInfo))
678 
679         nDeleteFrameInfo(frameInfo)
680         nDeleteDecoder(decoder)
681         nCloseAsset(asset)
682     }
683 
684     @Test
685     fun testNullFrameInfo() = nTestNullFrameInfo(getAssets(), "animated.gif")
686 
687     @Test
688     @Parameters(method = "animationsAndFrames")
689     fun testGetFrameInfo(image: String, frameName: String, numFrames: Int) {
690         val asset = nOpenAsset(getAssets(), image)
691         val decoder = nCreateFromAsset(asset)
692         val frameInfo = nCreateFrameInfo()
693         for (i in 0 until numFrames) {
694             assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nGetFrameInfo(decoder, frameInfo))
695             val result = nAdvanceFrame(decoder)
696             val expectedResult = if (i == numFrames - 1) {
697                 ANDROID_IMAGE_DECODER_FINISHED
698             } else {
699                 ANDROID_IMAGE_DECODER_SUCCESS
700             }
701             assertEquals(expectedResult, result)
702         }
703 
704         assertEquals(ANDROID_IMAGE_DECODER_FINISHED, nGetFrameInfo(decoder, frameInfo))
705 
706         nDeleteFrameInfo(frameInfo)
707         nDeleteDecoder(decoder)
708         nCloseAsset(asset)
709     }
710 
711     fun animationsAndDurations() = arrayOf(
712         arrayOf<Any>("animated.gif", LongArray(4) { 250_000_000 }),
713         arrayOf<Any>("animated_webp.webp", LongArray(4) { 250_000_000 }),
714         arrayOf<Any>("required_gif.gif", LongArray(7) { 100_000_000 }),
715         arrayOf<Any>("required_webp.webp", LongArray(7) { 100_000_000 }),
716         arrayOf<Any>("alphabetAnim.gif", LongArray(13) { 100_000_000 }),
717         arrayOf<Any>("blendBG.webp", longArrayOf(
718             525_000_000,
719             500_000_000,
720                 525_000_000,
721             437_000_000,
722             609_000_000,
723             729_000_000,
724             444_000_000
725         )),
726         arrayOf<Any>("stoplight.webp", longArrayOf(
727             1_000_000_000,
728             500_000_000,
729                                                     1_000_000_000
730         ))
731     )
732 
733     @Test
734     @Parameters(method = "animationsAndDurations")
735     fun testDurations(image: String, durations: LongArray) = testFrameInfo(image) {
736         frameInfo, i ->
737             assertEquals(durations[i], nGetDuration(frameInfo))
738     }
739 
740     /**
741      * Iterate through all frames and call a lambda that tests an individual frame's info.
742      *
743      * @param image Name of the image asset to test
744      * @param test Lambda with two parameters: A pointer to the native decoder, and the
745      *             current frame number.
746      */
747     private fun testFrameInfo(image: String, test: (Long, Int) -> Unit) {
748         val asset = nOpenAsset(getAssets(), image)
749         val decoder = nCreateFromAsset(asset)
750         val frameInfo = nCreateFrameInfo()
751         var frame = 0
752         do {
753             assertEquals(
754                 ANDROID_IMAGE_DECODER_SUCCESS,
755                 nGetFrameInfo(decoder, frameInfo),
756                 "Failed to getFrameInfo for frame $frame of $image!"
757             )
758             test(frameInfo, frame)
759             frame++
760         } while (ANDROID_IMAGE_DECODER_SUCCESS == nAdvanceFrame(decoder))
761 
762         nDeleteFrameInfo(frameInfo)
763         nDeleteDecoder(decoder)
764         nCloseAsset(asset)
765     }
766 
767     fun animationsAndRects() = arrayOf(
768         // Each group of four Ints represents a frame's rectangle
769         arrayOf<Any>("animated.gif", intArrayOf(0, 0, 278, 183,
770                                                 0, 0, 278, 183,
771                                                 0, 0, 278, 183,
772                                                 0, 0, 278, 183)),
773         arrayOf<Any>("animated_webp.webp", intArrayOf(0, 0, 278, 183,
774                                                       0, 0, 278, 183,
775                                                       0, 0, 278, 183,
776                                                       0, 0, 278, 183)),
777         arrayOf<Any>("required_gif.gif", intArrayOf(0, 0, 100, 100,
778                                                     0, 0, 75, 75,
779                                                     0, 0, 50, 50,
780                                                     0, 0, 60, 60,
781                                                     0, 0, 100, 100,
782                                                     0, 0, 50, 50,
783                                                     0, 0, 75, 75)),
784         arrayOf<Any>("required_webp.webp", intArrayOf(0, 0, 100, 100,
785                                                       0, 0, 75, 75,
786                                                       0, 0, 50, 50,
787                                                       0, 0, 60, 60,
788                                                       0, 0, 100, 100,
789                                                       0, 0, 50, 50,
790                                                       0, 0, 75, 75)),
791         arrayOf<Any>("alphabetAnim.gif", intArrayOf(25, 25, 75, 75,
792                                                     25, 25, 75, 75,
793                                                     25, 25, 75, 75,
794                                                     37, 37, 62, 62,
795                                                     37, 37, 62, 62,
796                                                     25, 25, 75, 75,
797                                                     0, 0, 50, 50,
798                                                     0, 0, 100, 100,
799                                                     25, 25, 75, 75,
800                                                     25, 25, 75, 75,
801                                                     0, 0, 100, 100,
802                                                     25, 25, 75, 75,
803                                                     37, 37, 62, 62)),
804 
805         arrayOf<Any>("blendBG.webp", intArrayOf(0, 0, 200, 200,
806                                                 0, 0, 200, 200,
807                                                 0, 0, 200, 200,
808                                                 0, 0, 200, 200,
809                                                 0, 0, 200, 200,
810                                                 100, 100, 200, 200,
811                                                 100, 100, 200, 200)),
812         arrayOf<Any>("stoplight.webp", intArrayOf(0, 0, 145, 55,
813                                                   0, 0, 145, 55,
814                                                   0, 0, 145, 55))
815     )
816 
817     @Test
818     @Parameters(method = "animationsAndRects")
819     fun testFrameRects(image: String, rects: IntArray) = testFrameInfo(image) {
820         frameInfo, i ->
821             val left = rects[i * 4]
822             val top = rects[i * 4 + 1]
823             val right = rects[i * 4 + 2]
824             val bottom = rects[i * 4 + 3]
825             try {
826                 nTestGetFrameRect(frameInfo, left, top, right, bottom)
827             } catch (t: Throwable) {
828                 throw AssertionError("$image, frame $i: ${t.message}", t)
829             }
830     }
831 
832     fun animationsAndAlphas() = arrayOf(
833         arrayOf<Any>("animated.gif", BooleanArray(4) { true }),
834         arrayOf<Any>("animated_webp.webp", BooleanArray(4) { true }),
835         arrayOf<Any>("required_gif.gif", booleanArrayOf(
836             false,
837             true,
838             true,
839             true,
840                 true,
841             true,
842             true,
843             true
844         )),
845         arrayOf<Any>("required_webp.webp", BooleanArray(7) { false }),
846         arrayOf<Any>("alphabetAnim.gif", booleanArrayOf(true, false, true, false,
847                 true, true, true, true, true, true, true, true, true)),
848         arrayOf<Any>("blendBG.webp", booleanArrayOf(
849             false,
850             true,
851             false,
852             true,
853                                                  false,
854             true,
855             true
856         )),
857         arrayOf<Any>("stoplight.webp", BooleanArray(3) { false })
858     )
859 
860     @Test
861     @Parameters(method = "animationsAndAlphas")
862     fun testAlphas(image: String, alphas: BooleanArray) = testFrameInfo(image) {
863         frameInfo, i ->
864             assertEquals(
865                 alphas[i],
866                 nGetFrameAlpha(frameInfo),
867                 "Mismatch in alpha for $image frame $i " +
868                     "expected ${alphas[i]}"
869             )
870     }
871 
872     private val ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE = 1
873     private val ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND = 2
874     private val ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS = 3
875 
876     fun animationsAndDisposeOps() = arrayOf(
877         arrayOf<Any>("animated.gif", IntArray(4) { ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND }),
878         arrayOf<Any>("animated_webp.webp", IntArray(4) { ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE }),
879         arrayOf<Any>("required_gif.gif", intArrayOf(
880             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
881                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
882             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
883                 ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
884             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
885                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
886                 ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE
887         )),
888         arrayOf<Any>("required_webp.webp", intArrayOf(
889             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
890                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
891             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
892                 ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
893             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
894                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
895                 ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE
896         )),
897         arrayOf<Any>("alphabetAnim.gif", intArrayOf(ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
898                 ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
899                 ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
900                 ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
901                 ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
902                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
903                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
904                 ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND, ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
905                 ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, ANDROID_IMAGE_DECODER_DISPOSE_OP_BACKGROUND,
906                 ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE)),
907         arrayOf<Any>("blendBG.webp", IntArray(7) { ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE }),
908         arrayOf<Any>("stoplight.webp", IntArray(4) { ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE })
909     )
910 
911     @Test
912     @Parameters(method = "animationsAndDisposeOps")
913     fun testDisposeOps(image: String, disposeOps: IntArray) = testFrameInfo(image) {
914         frameInfo, i ->
915             assertEquals(disposeOps[i], nGetDisposeOp(frameInfo))
916     }
917 
918     private val ANDROID_IMAGE_DECODER_BLEND_OP_SRC = 1
919     private val ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER = 2
920 
921     fun animationsAndBlendOps() = arrayOf(
922         arrayOf<Any>("animated.gif", IntArray(4) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER }),
923         arrayOf<Any>("animated_webp.webp", IntArray(4) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC }),
924         arrayOf<Any>("required_gif.gif", IntArray(7) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER }),
925         arrayOf<Any>("required_webp.webp", intArrayOf(
926             ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
927                 ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER,
928             ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER,
929                 ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER,
930             ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
931                 ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER,
932             ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER
933         )),
934         arrayOf<Any>("alphabetAnim.gif", IntArray(13) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER }),
935         arrayOf<Any>("blendBG.webp", intArrayOf(
936             ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
937                 ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER,
938             ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
939                 ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
940             ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
941                 ANDROID_IMAGE_DECODER_BLEND_OP_SRC,
942             ANDROID_IMAGE_DECODER_BLEND_OP_SRC
943         )),
944         arrayOf<Any>("stoplight.webp", IntArray(4) { ANDROID_IMAGE_DECODER_BLEND_OP_SRC_OVER })
945     )
946 
947     @Test
948     @Parameters(method = "animationsAndBlendOps")
949     fun testBlendOps(image: String, blendOps: IntArray) = testFrameInfo(image) {
950         frameInfo, i ->
951             assertEquals(
952                 blendOps[i],
953                 nGetBlendOp(frameInfo),
954                 "Mismatch in blend op for $image " +
955                         "frame $i, expected: ${blendOps[i]}"
956             )
957     }
958 
959     @Test
960     fun testHandleDisposePrevious() {
961         // The first frame is ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE, followed by a single
962         // ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS frame. The third frame looks different
963         // depending on whether that is respected.
964         val image = "RestorePrevious.gif"
965         val disposeOps = intArrayOf(
966             ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE,
967                                     ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS,
968                                     ANDROID_IMAGE_DECODER_DISPOSE_OP_NONE
969         )
970         val asset = nOpenAsset(getAssets(), image)
971         val decoder = nCreateFromAsset(asset)
972 
973         val width = nGetWidth(decoder)
974         val height = nGetHeight(decoder)
975         val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true)
976 
977         val verifiers = arrayOf<BitmapVerifier>(
978             ColorVerifier(Color.BLACK, 0),
979             RectVerifier(Color.BLACK, Color.RED, Rect(0, 0, 100, 80), 0),
980             RectVerifier(Color.BLACK, Color.GREEN, Rect(0, 0, 100, 50), 0)
981         )
982 
983         with(nCreateFrameInfo()) {
984             for (i in 0..2) {
985                 nGetFrameInfo(decoder, this)
986                 assertEquals(disposeOps[i], nGetDisposeOp(this))
987 
988                 nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
989                 assertTrue(verifiers[i].verify(bitmap))
990                 nAdvanceFrame(decoder)
991             }
992             nDeleteFrameInfo(this)
993         }
994 
995         // Now redecode without letting AImageDecoder handle
996         // ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS.
997         bitmap.eraseColor(Color.TRANSPARENT)
998         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
999         nSetHandleDisposePrevious(decoder, false)
1000 
1001         // If the client does not handle ANDROID_IMAGE_DECODER_DISPOSE_OP_PREVIOUS
1002         // the final frame does not match.
1003         for (i in 0..2) {
1004             nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
1005             assertEquals(i != 2, verifiers[i].verify(bitmap))
1006 
1007             if (i == 2) {
1008                 // Not only can we verify that frame 2 does not look as expected, but it
1009                 // should look as if we decoded frame 1 and did not revert it.
1010                 val verifier = RegionVerifier()
1011                 verifier.addVerifier(Rect(0, 0, 100, 50), ColorVerifier(Color.GREEN, 0))
1012                 verifier.addVerifier(Rect(0, 50, 100, 80), ColorVerifier(Color.RED, 0))
1013                 verifier.addVerifier(Rect(0, 80, 100, 100), ColorVerifier(Color.BLACK, 0))
1014                 assertTrue(verifier.verify(bitmap))
1015             }
1016             nAdvanceFrame(decoder)
1017         }
1018 
1019         // Now redecode and manually store/restore the first frame.
1020         bitmap.eraseColor(Color.TRANSPARENT)
1021         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
1022         nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
1023         val storedFrame = bitmap
1024         for (i in 1..2) {
1025             assertEquals(nAdvanceFrame(decoder), ANDROID_IMAGE_DECODER_SUCCESS)
1026             val frame = storedFrame.copy(storedFrame.config!!, true)
1027             nDecode(decoder, frame, ANDROID_IMAGE_DECODER_SUCCESS)
1028             assertTrue(verifiers[i].verify(frame))
1029             frame.recycle()
1030         }
1031 
1032         // This setting can be switched back, so that AImageDecoder handles it.
1033         bitmap.eraseColor(Color.TRANSPARENT)
1034         assertEquals(ANDROID_IMAGE_DECODER_SUCCESS, nRewind(decoder))
1035         nSetHandleDisposePrevious(decoder, true)
1036 
1037         for (i in 0..2) {
1038             nDecode(decoder, bitmap, ANDROID_IMAGE_DECODER_SUCCESS)
1039             assertTrue(verifiers[i].verify(bitmap))
1040             nAdvanceFrame(decoder)
1041         }
1042 
1043         bitmap.recycle()
1044         nDeleteDecoder(decoder)
1045         nCloseAsset(asset)
1046     }
1047 
1048     @Test
1049     @Parameters(method = "animationsAndAlphas")
1050     fun test565NoAnimation(image: String, alphas: BooleanArray) {
1051         val asset = nOpenAsset(getAssets(), image)
1052         val decoder = nCreateFromAsset(asset)
1053         val ANDROID_BITMAP_FORMAT_RGB_565 = 4
1054         if (alphas[0]) {
1055             assertEquals(
1056                 ANDROID_IMAGE_DECODER_INVALID_CONVERSION,
1057                     nSetAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGB_565)
1058             )
1059         } else {
1060             assertEquals(
1061                 ANDROID_IMAGE_DECODER_SUCCESS,
1062                     nSetAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGB_565)
1063             )
1064             assertEquals(
1065                 ANDROID_IMAGE_DECODER_INVALID_STATE,
1066                     nAdvanceFrame(decoder)
1067             )
1068         }
1069 
1070         nDeleteDecoder(decoder)
1071         nCloseAsset(asset)
1072     }
1073 
1074     private fun handleRotation(original: Bitmap, image: String): Bitmap {
1075         // ExifInterface does not support GIF.
1076         if (image.endsWith("gif")) return original
1077 
1078         val inputStream = getAssets().open(image)
1079         val exifInterface = ExifInterface(inputStream)
1080         var rotation = 0
1081         when (exifInterface.getAttributeInt(
1082             ExifInterface.TAG_ORIENTATION,
1083                 ExifInterface.ORIENTATION_NORMAL
1084         )) {
1085             ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_UNDEFINED -> return original
1086             ExifInterface.ORIENTATION_ROTATE_90 -> rotation = 90
1087             ExifInterface.ORIENTATION_ROTATE_180 -> rotation = 180
1088             ExifInterface.ORIENTATION_ROTATE_270 -> rotation = 270
1089             else -> fail("Unexpected orientation for $image!")
1090         }
1091 
1092         val m = Matrix()
1093         m.setRotate(rotation.toFloat(), original.width / 2.0f, original.height / 2.0f)
1094         return Bitmap.createBitmap(original, 0, 0, original.width, original.height, m, false)
1095     }
1096 
1097     private fun decodeF16(image: String): Bitmap {
1098         val options = BitmapFactory.Options()
1099         options.inPreferredConfig = Bitmap.Config.RGBA_F16
1100         val inputStream = getAssets().open(image)
1101         val bm = BitmapFactory.decodeStream(inputStream, null, options)
1102         if (bm == null) {
1103             fail("Failed to decode $image to RGBA_F16!")
1104         }
1105         return bm
1106     }
1107 
1108     @Test
1109     @Parameters(method = "animationsAndFrames")
1110     fun testDecodeFramesF16(image: String, frameName: String, numFrames: Int) {
1111         var expectedBm = handleRotation(decodeF16(image), image)
1112 
1113         val asset = nOpenAsset(getAssets(), image)
1114         val decoder = nCreateFromAsset(asset)
1115         val ANDROID_BITMAP_FORMAT_RGBA_F16 = 9
1116         nSetAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_F16)
1117 
1118         val testBm = makeEmptyBitmap(expectedBm)
1119 
1120         val mssimThreshold = .95
1121         var i = 0
1122         while (true) {
1123             nDecode(decoder, testBm, ANDROID_IMAGE_DECODER_SUCCESS)
1124             val verifier = GoldenImageVerifier(expectedBm, MSSIMComparer(mssimThreshold))
1125             assertTrue(verifier.verify(testBm), "$image has mismatch in frame $i")
1126             expectedBm.recycle()
1127 
1128             i++
1129             when (val result = nAdvanceFrame(decoder)) {
1130                 ANDROID_IMAGE_DECODER_SUCCESS -> {
1131                     assertTrue(i < numFrames, "Unexpected frame $i in $image")
1132                     expectedBm = decodeF16(frameName.format(i))
1133                 }
1134                 ANDROID_IMAGE_DECODER_FINISHED -> {
1135                     assertEquals(i, numFrames, "Expected $numFrames frames in $image; found $i")
1136                     break
1137                 }
1138                 else -> fail("Unexpected error $result when advancing $image to frame $i")
1139             }
1140         }
1141 
1142         nDeleteDecoder(decoder)
1143         nCloseAsset(asset)
1144     }
1145 
1146     private external fun nTestNullDecoder()
1147     private external fun nTestToString()
1148     private external fun nOpenAsset(assets: AssetManager, name: String): Long
1149     private external fun nCloseAsset(asset: Long)
1150     private external fun nCreateFromAsset(asset: Long): Long
1151     private external fun nGetWidth(decoder: Long): Int
1152     private external fun nGetHeight(decoder: Long): Int
1153     private external fun nDeleteDecoder(decoder: Long)
1154     private external fun nSetTargetSize(decoder: Long, width: Int, height: Int): Int
1155     private external fun nSetCrop(decoder: Long, left: Int, top: Int, right: Int, bottom: Int): Int
1156     private external fun nDecode(decoder: Long, dst: Bitmap, expectedResult: Int)
1157     private external fun nAdvanceFrame(decoder: Long): Int
1158     private external fun nRewind(decoder: Long): Int
1159     private external fun nSetUnpremultipliedRequired(decoder: Long, required: Boolean): Int
1160     private external fun nSetAndroidBitmapFormat(decoder: Long, format: Int): Int
1161     private external fun nSetDataSpace(decoder: Long, format: Int): Int
1162     private external fun nCreateFrameInfo(): Long
1163     private external fun nDeleteFrameInfo(frameInfo: Long)
1164     private external fun nGetFrameInfo(decoder: Long, frameInfo: Long): Int
1165     private external fun nTestNullFrameInfo(assets: AssetManager, name: String)
1166     private external fun nGetDuration(frameInfo: Long): Long
1167     private external fun nTestGetFrameRect(
1168         frameInfo: Long,
1169         expectedLeft: Int,
1170         expectedTop: Int,
1171         expectedRight: Int,
1172         expectedBottom: Int
1173     )
1174     private external fun nGetFrameAlpha(frameInfo: Long): Boolean
1175     private external fun nGetAlpha(decoder: Long): Boolean
1176     private external fun nGetDisposeOp(frameInfo: Long): Int
1177     private external fun nGetBlendOp(frameInfo: Long): Int
1178     private external fun nGetRepeatCount(decoder: Long): Int
1179     private external fun nSetHandleDisposePrevious(decoder: Long, handle: Boolean)
1180 }
1181