Android接入HEIC和AVIF解码说明

本文为您介绍如何配置Android客户端的相关依赖,以便支持AVIF和HEIC图片解码。

Android接入HEIC解码说明

高效率图像格式HEIF(High Efficiency Image Format )是一种压缩图片格式,通过使用更现代的压缩算法,在相同质量的前提下,HEIF文件大小是JPEG文件的40%左右。

阿里云高性能HEIC解码库在开源的libheif和libde265基础上进行二次开发,针对ARM平台做了大量优化,参考了其它同类开源库优点,在解码链路上的各个环节进行优化,显著地提升了运行速度。相比原始的开源版本,解码的效率有数倍的提升。阿里云高性能HEIC解码库代码链接,请参见Github

Android客户端接入

通过Android客户端接入HEIC解码步骤如下:

  1. build.gradle的repositories节点中添加阿里云Maven仓库配置。

    maven { url 'https://maven.aliyun.com/repository/public' }
  2. build.gradle的dependencies节点添加依赖。

    implementation 'com.aliyun:libheif:0.0.4'

混淆配置

如果开启混淆需配置,需要添加如下代码以保证功能正常使用。

-keep class com.aliyun.libheif.**

解码HEIC图片

  • 场景一:直接使用API接口解码

        /**
         * judge file is heic format or not
         * @param length  the length of effective file memory
         * @param filebuf file pointer in memory
         * @return rgba byte, convenient to create a {@link android.graphics.Bitmap}
         */
        public static native boolean toRgba(long length, byte[] fileBuf, Bitmap bitmap);
    
        /**
         * judge file is heic format or not
         * @param length  the length of effective file memory
         * @param filebuf file pointer in memory
         * @return bool, if true, the format is heif;
         *               if false, the fromat is not heif
         */
        public static native boolean isHeic(long length, byte[] fileBuf);
    
        /**
         * get info from heic picture
         * @param HeifInfo info
         * @param length  the length of effective file memory
         * @param filebuf file pointer in memory
         * @return bool, if true, outSize is valid;
         *               if false, outSize is not valid
         */
        public static native boolean getInfo(HeifInfo info, long length, byte[] fileBuf);                

    以下以将asset目录下的test.heic解码成Bitmap为例为您进行介绍。

    val image = findViewById<ImageView>(R.id.image);
    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 heifInfo = HeifInfo()
    val fileBuffer: ByteArray = output.toByteArray()
    
    HeifNative.getInfo(heifInfo, fileBuffer.size.toLong(), fileBuffer)
    // 单帧图片,获取frameList的第一个元素即图片高宽。
    val heifSize = heifInfo.frameList.first()
    // 根据高宽创建好图片。
    val bitmap = Bitmap.createBitmap(
                heifSize.width,
                heifSize.height,
                Bitmap.Config.ARGB_8888)
    // 解码。
    val heifBuffer = HeifNative.toRgba(fileBuffer.size.toLong(), fileBuffer, bitmap)
    image.setImageBitmap(bitmap)
  • 场景二:使用Glide集成解码库解码

    Glide是一个知名开源的图片缓存库。更多信息,请参见Glide官方文档

    1. build.gradle中接入Glide。

      implementation 'com.aliyun:libheif:0.0.4'
      implementation 'com.github.bumptech.glide:glide:4.13.2'
      implementation 'com.github.bumptech.glide:compiler:4.13.2'
    2. 自定义HEIF解码器,在decode方法中接入阿里云高性能HEIC解码库,具体可以参考Glide官方文档中的自定义组件模块。

      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)
              var 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. 注册解码器Module。

      @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. 加载显示HEIF图片。

      fun loadHeif(context: Context, imageView: ImageView, file: File) {
           Glide.with(context).asBitmap().load(file).into(imageView)
       }
  • 场景三:使用Fresco集成解码库解码

    Fresco是Facebook开源的一个知名Android图片缓存库。更多信息,请参见Fresco官方文档

    1. build.gradle中接入Fresco。

      implementation 'com.aliyun:libheif:0.0.4'
      implementation 'com.facebook.fresco:fresco:2.6.0'
    2. 创建自定义HEIF解码器,在decode方法中接入阿里云高性能HEIC解码库。

      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)
          }
      }

      接入阿里云高性能HEIC解码库时,调用了ByteBufferUtil参考类,具体如下:

      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 {
                  if (channel != null) {
                      try {
                          channel.close()
                      } catch (e: IOException) {
                      }
                  }
                  if (raf != null) {
                      try {
                          raf.close()
                      } catch (e: IOException) {
                      }
                  }
              }
          }
      
          fun toBytes(byteBuffer: ByteBuffer): ByteArray {
              val result: ByteArray
              val safeArray = getSafeArray(byteBuffer)
              if (safeArray != null && safeArray.offset == 0 && safeArray.limit == safeArray.data.size) {
                  result = byteBuffer.array()
              } else {
                  val toCopy = byteBuffer.asReadOnlyBuffer()
                  result = ByteArray(toCopy.limit())
                  rewind(toCopy)
                  toCopy[result]
              }
              return result
          }
      
          private fun rewind(buffer: ByteBuffer): ByteBuffer {
              return 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. 加载HEIF图片。

      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
          }

Android接入AVIF解码说明

AVIF是一种基于AV1视频编码的新图像格式,相对于JPEG,Wep等图片格式压缩率更高,并且画面细节更好。AVIF通过使用更现代的压缩算法,在相同质量的前提下,AVIF文件大小是JPEG文件的35%左右。

AVIF的解码可以用采用官方解码库libavif。libavif已经封装好了Android的相关接口,相关代码在android_jni目录下。关于libavif代码的更多信息,请参见Github

AVIF集成解码库

在build.gradle添加如下依赖。

implementation'org.aomedia.avif.android:avif:0.11.1.3c786d2'

直接解码

直接调用解码方法即可将ByteBuffer进行解码。

/**
   * Decodes the AVIF image into the bitmap.
   *
   * @param encoded The encoded AVIF image. encoded.position() must be 0.
   * @param length Length of the encoded buffer.
   * @param bitmap The decoded pixels will be copied into the bitmap.
   * @param threads Number of threads to be used for the AVIF decode. Zero means use number of CPU
   *     cores as the thread count. Negative values are invalid. When this value is > 0, it is
   *     simply mapped to the maxThreads parameter in libavif. For more details, see the
   *     documentation for maxThreads variable in avif.h.
   * @return true on success and false on failure. A few possible reasons for failure are: 1) Input
   *     was not valid AVIF. 2) Bitmap was not large enough to store the decoded image. 3) Negative
   *     value was passed for the threads parameter.
   */
  public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads);

其中,threads参数建议指定为1,即单线程解码。如果你的图片编码使用了AV1 tiles特性,开启多线程才能提升效率。关于接口的更多信息,请参见Github

Glide集成解码库

Glide自定义Module已经支持AVIF,源码参考其代码仓库Github

从4.13.0版本开始支持,只需要添加如下依赖即可使Glide支持AVIF解码。

说明

请确保版本号为4.13.0及以上版本。

implementation "com.github.bumptech.glide:avif-integration:4.13.2"       

Fresco集成解码库

关于Fresco如何集成解码库,请参见自定义解码器相关文档