Bitmap / Drawable / ImageDecoder

ImageDecoder – Error Handling, Cropping, & Scaling

The Android P developer preview contains a new ImageDecoder which is quite interesting and can do some useful things. At present it only appears within the Framework so we can only use it on devices running P or later, and I understand that if consists of a chunk of native code and so is unlikely to appear as a support library. However, it is a really nice, powerful yet simple API. In this series we’ll take a look at how to use it, and see some of the cool things that we can achieve with it.

Previously we’ve looked at the basics of how to use ImageDecoder, but often we have to plan for image data which may be outside of our control – and handle error conditions gracefully. The default behaviour is to throw an Exception and therefore we’ll never receive a return value from the decodeBitmap() or decodeDrawable() method. In Java you’ll receive an UnhandledException error at compile-time if you do not correctly handle this case, but Kotlin is less strict so the previous examples just work. However, we can easily add an exception handler if we want to properly detect error conditions:

class ErrorBitmapFragment : ImageDecoderFragment() {

    override val assetName: String = "corrupt.png"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            try {
                ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                    ImageDecoder.decodeBitmap(source).also { bitmap ->
                        launch(UI) {
                            image.setImageBitmap(bitmap)
                        }
                    }
                }
            } catch (e: Exception) {
                println("Exception loading image: $e")
            }
        }
    }
}

This uses a corrupt PNG image which I deliberately corrupted by truncating it, and if we run this we get the following logcat output:

I/System.out: Exception loading image: android.graphics.ImageDecoder$IncompleteException: Incomplete input

While this default behaviour works well in the majority of cases, there may be occasions where we need to tweak things, and we can do that quite easily. First we need to add an ImageDecoder.OnHeaderDecodedListener as we saw previously:

class ErrorBitmapFragment : ImageDecoderFragment() {

    override val assetName: String = "corrupt.png"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            try {
                ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                    ImageDecoder.decodeBitmap(source, headerDecodedListener).also { bitmap ->
                        launch(UI) {
                            image.setImageBitmap(bitmap)
                        }
                    }
                }
            } catch (e: Exception) {
                println("Exception loading image: $e")
            }
        }
    }

    private var headerDecodedListener = ImageDecoder.OnHeaderDecodedListener { imageDecoder, _, _ ->
        //...
    }
}

Within the body of this (which will be called as soon as the image header data has been parsed, but before the image data itself has been ready) we can register an ImageDecoder.OnPartialImageListener:

    .
    .
    .
    private var headerDecodedListener = ImageDecoder.OnHeaderDecodedListener { imageDecoder, _, _ ->
        imageDecoder.setOnPartialImageListener(partialImageListener)
    }

    private var partialImageListener = ImageDecoder.OnPartialImageListener { errorCode, _ ->
        println("An error occurred: $errorCode")
        false
    }
}

ImageDecoder.OnPartialImageListener receives two arguments: An error code Integer, and the ImageDecoder.Source object. The really useful thing is that we return a boolean value indicating whether the decoding should terminate by throwing an exception (i.e. the default behaviour). In this case we return false and so we get this default behaviour:

I/System.out: An error occurred: 2
I/System.out: Exception loading image: android.graphics.ImageDecoder$IncompleteException: Incomplete input

However, if we instead return true, then while the error inside the ImageDecoder.OnPartialImageListener will still be output, it will not longer throw an exception, and will actually return that partial image data that was loaded:

    .
    .
    .
    private var partialImageListener = ImageDecoder.OnPartialImageListener { errorCode, _ ->
        println("An error occurred: $errorCode")
        true
    }
}
I/System.out: An error occurred: 2

Another occurrence may be that an image may be a different size to what we actually need. We’ve already seen how the ImageDecoder.ImageInfo instance that we get inside the ImageDecoder.OnHeaderDecodedListener contains the size of the image in pixels, but we can also scale the image during processing so we get an image of the required dimensions:

class ScaleDrawableFragment : ImageDecoderFragment() {

    override val assetName: String = "StylingAndroid.png"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                ImageDecoder.decodeDrawable(source, listener).also { drawable ->
                    launch(UI) {
                        image.setImageDrawable(drawable)
                    }
                }
            }
        }
    }

    private val listener = ImageDecoder.OnHeaderDecodedListener { imageDecoder, imageInfo, _ ->
        imageDecoder.setResize(imageInfo.size.width / 2, imageInfo.size.height / 2)
    }
}

In this case we halve the width and height to get a smaller image:

The other thing we can do is crop the image

private const val HALF_SIZE = 200

class CropDrawableFragment : ImageDecoderFragment() {

    override val assetName: String = "StylingAndroid.png"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                ImageDecoder.decodeDrawable(source, listener).also { drawable ->
                    launch(UI) {
                        image.setImageDrawable(drawable)
                    }
                }
            }
        }
    }

    private val listener = ImageDecoder.OnHeaderDecodedListener { imageDecoder, imageInfo, _ ->
        imageDecoder.setCrop(
                createCropRect(imageInfo.size.width / 2, imageInfo.size.height / 2)
        )
    }

    private fun createCropRect(centreX: Int, centreY: Int) : Rect =
            Rect(
                    centreX - HALF_SIZE,
                    centreY - HALF_SIZE,
                    centreX + HALF_SIZE,
                    centreY + HALF_SIZE
            )
}

This crops to the middle 400×400 pixels of the image:

So we can do some really quite useful processing during the image decoding to get our images resized to what we need, but there are some even more powerful things that we have. In the final article in this series we’ll dive even deeper in to the nice features offered by ImageDecoder.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. I don’t some things here:
    1. What’s the advantage of using this code over Glide library? Can it decode more files ? VectorDrawable? Animated WEBP/HEIF ?
    2. Does it use some caching? Maybe even in OS scope ?
    3. What happens if I change orientation (making the activity get restarted) ?

    1. It’s doing a different job to Glide. ImageDecoder is purely about converting image data from a stream in to a Bitmap or Drawable. I know that Picasso 3.0 will use ImageDecoder internally, but I have no idea whether Glide will do the same – you would have to ask the authors of Glide that question.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.