Bitmap / Drawable / ImageDecoder

Image Decoder – Post Processing and Masks

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.

Before we dive in to the final article in this series, it is worth pointing out that it is being published on Star Wars Day – May 4th 2018. It feels fitting to mark this by using a Star Wars themed image and I am delighted that the exceptionally talented Virginia Poltrack gave me permission to use this image for this post. I’m sure that you will agree that it fits the bill perfectly. Thanks Virginia!

There are a couple more aspects of ImageDecoder that are worthy of our attention, and we’ll use Artoo to demonstrate them. First we’ll load the main image in the normal way with an OnHeaderDecodedListener:

class MaskDrawableDecoder : ImageDecoderFragment() {

    override val assetName: String = "artoo.png"

    override fun updateAsset(context: Context) {
        launch(CommonPool) {
            ImageDecoder.createSource(cacheAsset.file(assetName)).also { source ->
                ImageDecoder.decodeBitmap(source, headerDecodedListener).also { bitmap ->
                    launch(UI) {
                        image.setImageBitmap(bitmap)
                    }
                }
            }
        }
    }

    private val headerDecodedListener = ImageDecoder.OnHeaderDecodedListener { imageDecoder, imageInfo, _ ->
        imageDecoder.setPostProcessor(MaskProcessor(imageInfo.size.width, imageInfo.size.height))
    }
    .
    .
    .
}

The OnHeaderDecoderListener sets a PostProcessor on the image. A PostProcessor, as its name suggests, will run after the image has been loaded and will allow us to do some nice things. In this case we’re going to apply a mask to feather the edges of the image. The PostProcessor to do this is as follows:

private inner class MaskProcessor(val width: Int, val height: Int) : PostProcessor {

    private val maskName: String = "feather.png"

    override fun onPostProcess(canvas: Canvas): Int {
        loadMask().also { mask ->
            canvas.drawBitmap(
                    mask,
                    Rect(0, 0, mask.width, mask.height),
                    Rect(0, 0, width, height),
                    maskPaint)
        }

        return PixelFormat.TRANSLUCENT
    }

    private fun loadMask(): Bitmap =
            ImageDecoder.createSource(cacheAsset.file(maskName)).let { maskSource ->
                ImageDecoder.decodeBitmap(maskSource) { maskDecoder, _, _ ->
                    maskDecoder.setAsAlphaMask(true)
                }
            }

    private val maskPaint = Paint().apply {
        xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
    }
}

The PostProcessor interface has a single method named onPostProcess() which takes a Canvas instance as an argument which has the image that we’ve just loaded already rendered – so we can draw other things on top. In this case we load a mask and draw this on top using a PorterDuff mode of DST_OUT. This will combine the two images, and use the mask image to alter the transparency of the main image. The mask image that we’re using looks like this:

This image does not contain any transparency information itself, it is a simple, monochrome image which has a gradient at the edge. The actual dimension of this are different to the main image. I have done this deliberately to show that it’s easy to scale the mask to match the dimension of the image we’re applying the mask to. The mask size is specified on line 30, and the main image size is on line 31 and this will scale the mask to match the main image. The other important thing is that when we load the mask, wee need to tell ImageDecoder that it’s a mask (line 42) and it will only load it as a single channel image (which is far smaller) containing only transparency data. Although the mask image does not contain any transparency information, setting this flag means that it will be loaded and converted on the fly.

The final thing that is important here is the return value of the onPostProcess() method. In this case we know for sure that the composite image will contain transparency because that is precisely what we’re applying, so we return PixelFormat.TRANSLUCENT. If our post processing would definitely remove any transparency data, then we would return PixelFormat.OPAQUE, but if our post processing is not affecting the transparency then we should return PixelFormat.UNKNOWN and the transparency settings of the main image will be preserved.

If we run this we get Artoo with a feathered edge:

It is worth remembering that we can do much more than applying a mask using a PostProcessor, having control of the Canvas provides us with many options for different things that we can do with the image. Also, for production code, it may be better to take the loading of the mask outside of this pipeline – particularly if the same mask will be used for different images. However, this does serve as a good example of the types of effects that can be achieved by using a PostProcessor.

That concludes our look at ImageDecoder. Not only is it a really clean API, but it is also hugely powerful and provides a framework for creating some really interesting effects.

Once again: my profound thanks to Virginia Poltrack for allowing me to use such a wonderful image.

Many thanks also to Jake Wharton who, in the absence of any source code to peruse, provided me with some useful insights in to the internals of 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.

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.