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