HEIC and AVIF are modern image formats that deliver significantly smaller file sizes than JPEG at equivalent quality — HEIC images are approximately 40% the size of a JPEG, and AVIF images are approximately 35%. This document explains how to add HEIC and AVIF decoding to an Android app using Alibaba Cloud's high-performance libraries and, optionally, integrate them with Glide or Fresco.
Prerequisites
Before you begin, ensure that you have:
An Android project with Gradle build support
Access to the Alibaba Cloud Maven repository
HEIC decoding on Android
Alibaba Cloud's HEIC decoding library is built on the open-source libheif and libde265 libraries, optimized for ARM. The library is available on GitHub at aliyun/heif-decoder-lib.
Add the dependency
Add the Alibaba Cloud Maven repository to the
repositoriesnode ofbuild.gradle:maven { url 'https://maven.aliyun.com/repository/public' }Add the HEIC library to the
dependenciesnode:implementation 'com.aliyun:libheif:1.0.0'
Configure obfuscation
If your build uses ProGuard or R8, add the following keep rule to prevent the library from being stripped:
-keep class com.aliyun.libheif.**Decode HEIC images
Choose the integration approach that fits your app's image loading architecture.
Direct API decoding
The library exposes three native methods:
| Method | Description |
|---|---|
isHeic(long length, byte[] fileBuf) | Returns true if the buffer contains a valid HEIC file |
getInfo(HeifInfo info, long length, byte[] fileBuf) | Populates a HeifInfo object with frame dimensions |
toRgba(long length, byte[] fileBuf, Bitmap bitmap) | Decodes the image into an ARGB_8888 Bitmap |
The following example reads test.heic from the assets directory and displays it in an ImageView:
val image = findViewById<ImageView>(R.id.image)
// Step 1: Read the file into a byte array
val inputStream = assets.open("test.heic")
val buffer = ByteArray(8192)
var bytesRead: Int
val output = ByteArrayOutputStream()
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
}
val fileBuffer: ByteArray = output.toByteArray()
// Step 2: Get image dimensions
val heifInfo = HeifInfo()
HeifNative.getInfo(heifInfo, fileBuffer.size.toLong(), fileBuffer)
val heifSize = heifInfo.frameList.first()
// Step 3: Create a Bitmap and decode
val bitmap = Bitmap.createBitmap(heifSize.width, heifSize.height, Bitmap.Config.ARGB_8888)
HeifNative.toRgba(fileBuffer.size.toLong(), fileBuffer, bitmap)
// Step 4: Display
image.setImageBitmap(bitmap)Integrate with Glide
Glide is an open-source Android image loading and caching library.
Add the dependencies to
build.gradle:implementation 'com.aliyun:libheif:1.0.0' implementation 'com.github.bumptech.glide:glide:4.13.2' implementation 'com.github.bumptech.glide:compiler:4.13.2'Implement a custom
ResourceDecoderthat routes HEIC buffers to the Alibaba Cloud library:class HeifByteBufferBitmapDecoder(bitmapPool: BitmapPool) : ResourceDecoder<ByteBuffer, Bitmap> { private val bitmapPool: BitmapPool init { this.bitmapPool = Preconditions.checkNotNull(bitmapPool) } override fun handles(source: ByteBuffer, options: Options): Boolean { val buffer = ByteBufferUtil.toBytes(source) return HeifNative.isHeic(buffer.size.toLong(), buffer) } override fun decode( source: ByteBuffer, width: Int, height: Int, options: Options ): Resource<Bitmap>? { val buffer = ByteBufferUtil.toBytes(source) val heifInfo = HeifInfo() HeifNative.getInfo(heifInfo, buffer.size.toLong(), buffer) val heifSize = heifInfo.frameList[0] val bitmap = Bitmap.createBitmap(heifSize.width, heifSize.height, Bitmap.Config.ARGB_8888) HeifNative.toRgba(buffer.size.toLong(), buffer, bitmap) return BitmapResource.obtain(bitmap, bitmapPool) } }Register the decoder in a Glide module. For details on creating a custom Glide module, see Custom module of Glide.
@GlideModule(glideName = "HeifGlide") open class HeifGlideModule : LibraryGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { val byteBufferBitmapDecoder = HeifByteBufferBitmapDecoder(glide.bitmapPool) registry.prepend(ByteBuffer::class.java, Bitmap::class.java, byteBufferBitmapDecoder) } }Load and display the HEIC image:
fun loadHeif(context: Context, imageView: ImageView, file: File) { Glide.with(context).asBitmap().load(file).into(imageView) }
Integrate with Fresco
Fresco is an open-source Android image caching library developed by Facebook.
Add the dependencies to
build.gradle:implementation 'com.aliyun:libheif:1.0.0' implementation 'com.facebook.fresco:fresco:2.6.0'Implement a custom
ImageDecoderthat uses the Alibaba Cloud library. All examples useByteBufferUtilto convert file data into a byte array before callingHeifNative.class FrescoHeifDecoder : ImageDecoder { private var mFile: File? = null private var mUri: Uri? = null constructor(file: File) { mFile = file } constructor(uri: Uri) { mUri = uri } override fun decode( encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions ): CloseableImage? { var imageRequest: ImageRequest? = null if (mFile != null) imageRequest = ImageRequest.fromFile(mFile) if (mUri != null) imageRequest = ImageRequest.fromUri(mUri) try { val cacheKey = DefaultCacheKeyFactory.getInstance() .getEncodedCacheKey(imageRequest, null) val fileCache = ImagePipelineFactory.getInstance().mainFileCache val resource = fileCache.getResource(cacheKey) val file: File = if (resource == null) mFile!! else (resource as FileBinaryResource).file val bufferFromFile = ByteBufferUtil.fromFile(file) val bytes = ByteBufferUtil.toBytes(bufferFromFile) val heifInfo = HeifInfo() HeifNative.getInfo(heifInfo, file.length(), bytes) val heifSize = heifInfo.frameList[0] val bitmap = Bitmap.createBitmap(heifSize.width, heifSize.height, Bitmap.Config.ARGB_8888) HeifNative.toRgba(file.length(), bytes, bitmap) return CloseableStaticBitmap( pinBitmap(bitmap), qualityInfo, encodedImage.rotationAngle, encodedImage.exifOrientation ) } catch (e: Exception) { } return null } private fun pinBitmap(bitmap: Bitmap?): CloseableReference<Bitmap>? { return CloseableReference.of( Preconditions.checkNotNull(bitmap), BitmapCounterProvider.get().releaser ) } }The
ByteBufferUtilhelper class reads a file into a memory-mappedByteBufferand converts it to a byte array:object ByteBufferUtil { @Throws(IOException::class) fun fromFile(file: File): ByteBuffer { var raf: RandomAccessFile? = null var channel: FileChannel? = null return try { val fileLength = file.length() if (fileLength > Int.MAX_VALUE) throw IOException("File too large to map into memory") if (fileLength == 0L) throw IOException("File unsuitable for memory mapping") raf = RandomAccessFile(file, "r") channel = raf.channel channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength).load() } finally { try { channel?.close() } catch (e: IOException) {} try { raf?.close() } catch (e: IOException) {} } } fun toBytes(byteBuffer: ByteBuffer): ByteArray { val safeArray = getSafeArray(byteBuffer) return if (safeArray != null && safeArray.offset == 0 && safeArray.limit == safeArray.data.size) { byteBuffer.array() } else { val toCopy = byteBuffer.asReadOnlyBuffer() val result = ByteArray(toCopy.limit()) rewind(toCopy) toCopy[result] result } } private fun rewind(buffer: ByteBuffer): ByteBuffer = buffer.position(0) as ByteBuffer private fun getSafeArray(byteBuffer: ByteBuffer): SafeArray? { return if (!byteBuffer.isReadOnly && byteBuffer.hasArray()) { SafeArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit()) } else null } internal class SafeArray(val data: ByteArray, val offset: Int, val limit: Int) }Load the HEIC image using Fresco:
fun loadHeif(simpleDraweeView: SimpleDraweeView, file: File) { val request = ImageRequestBuilder.newBuilderWithSource(Uri.fromFile(file)) .setImageDecodeOptions( ImageDecodeOptions.newBuilder() .setCustomImageDecoder(FrescoHeifDecoder(file)) .build() ).build() val controller = Fresco.newDraweeControllerBuilder() .setImageRequest(request).build() simpleDraweeView.controller = controller }
AVIF decoding on Android
AVIF is based on AV1 video encoding and provides higher compression efficiency and image fidelity than JPEG and WebP. An AVIF image is approximately 35% the size of an equivalent-quality JPEG.
Use the libavif library to decode AVIF images on Android. The library wraps the relevant Android JNI (Java Native Interface) operations; the source code is in the android_jni directory.
Add the dependency
Add the following dependency to build.gradle:
implementation 'org.aomedia.avif.android:avif:0.11.1.3c786d2'Direct decoding
Use the static decode() method to decode a still AVIF image from a ByteBuffer:
public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads);Parameters:
| Parameter | Description |
|---|---|
encoded | The encoded AVIF image. encoded.position() must be 0. |
length | Length of the encoded buffer. |
bitmap | The decoded pixels are written into this Bitmap. The Bitmap must be large enough to hold the decoded image. |
threads | Number of threads for decoding. 0 uses the device's CPU core count. 1 uses single-threaded decoding. Negative values are invalid. |
Return value: true on success. Returns false if the input is not valid AVIF, the Bitmap is too small, or a negative thread count is passed.
We recommend that you set the threads parameter to 1 for single-threaded decoding. If your image encoding method uses the AV1 tiles feature, you can enable multi-threading to improve efficiency.
For the full AvifDecoder API reference, see AvifDecoder.java on GitHub.
Integrate with Glide
Glide 4.13.0 and later includes built-in AVIF support through the avif-integration module. Add the dependency to build.gradle:
Glide 4.13.0 or later is required.
implementation "com.github.bumptech.glide:avif-integration:4.13.2"For source code and customization options, see the Glide AVIF integration on GitHub.
Integrate with Fresco
For AVIF decoding with Fresco, follow the Fresco custom image format guide: Customizing image formats.