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