HEIC and AVIF decoding on Android

更新时间:
复制 MD 格式

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

  1. Add the Alibaba Cloud Maven repository to the repositories node of build.gradle:

       maven { url 'https://maven.aliyun.com/repository/public' }
  2. Add the HEIC library to the dependencies node:

       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:

MethodDescription
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.

  1. 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'
  2. Implement a custom ResourceDecoder that 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)
           }
       }
  3. 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)
           }
       }
  4. 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.

  1. Add the dependencies to build.gradle:

       implementation 'com.aliyun:libheif:1.0.0'
       implementation 'com.facebook.fresco:fresco:2.6.0'
  2. Implement a custom ImageDecoder that uses the Alibaba Cloud library. All examples use ByteBufferUtil to convert file data into a byte array before calling HeifNative.

       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 ByteBufferUtil helper class reads a file into a memory-mapped ByteBuffer and 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)
       }
  3. 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:

ParameterDescription
encodedThe encoded AVIF image. encoded.position() must be 0.
lengthLength of the encoded buffer.
bitmapThe decoded pixels are written into this Bitmap. The Bitmap must be large enough to hold the decoded image.
threadsNumber 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:

Note

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.