好玩系列:讓項目中的相冊支持Heif格式圖片

前言

目前市面上的成熟的APP,其用戶體系中均存在 設置頭像 的功能,考慮到尺寸規範問題,通常會加入 圖片裁剪 功能; 考慮到頁面UI統一度問題,甚至會在應用內實現 相冊功能。據此推斷:各位的項目中,會遇到 Heif格式圖片 須要兼容的需求。java

筆者目前參與的商業項目,也被市場要求對Heif圖片進行適配。這篇文章,記錄了我在這件事情上 折騰 的過程。android

好玩系列是我進行 新事物實踐嘗試創造 的記錄,瞭解更多git

背景

HEIF格式的全名爲 High Efficiency Image File Format(高效率圖檔格式),是由動態圖像專家組(MPEG)在2013年推出的新格式,瞭解更多github

瞭解Heif整個項目markdown

測試文件app

筆者注:印象中,iOS系統大約在16年就全面支持這一類型的文件了,而Android大約是三年前,在Android P推出的時候,宣佈原生支持Heif文件框架

隨着市場上的Android機器已經大面積過渡到 Android Q,從這一點看,確實到了該適配的階段了。ide

目標,至少實現Android P及其以上的適配,嘗試向更低版本適配oop

ISO Base Media File Format

HEIF格式是基於 ISO Base Media File Format格式衍生出來的圖像封裝格式,因此它的文件格式一樣符合ISO Base Media File Format (ISO/IEC 14496-12)中的定義( ISOBMFF)。測試

文件中全部的數據都存儲在稱爲Box的數據塊結構中,每一個文件由若干個Box組成,每一個Box有本身的類型和長度。在一個Box中還能夠包含子Box,最終由一系列的Box組成完整的文件內容,結構以下圖所示,圖中每一個方塊即表明一個Box。

咱們常見的MP4文件一樣是ISOBMFF結構,因此HEIF文件結構和MP4文件結構基本一致,只是用到的Box類型有區別。

HEIF文件若是是單幅的靜態圖片的話,使用item的形式保存數據,全部item單獨解碼;若是保存的爲圖片序列的話,使用track的方式保存。

做者:金山視頻雲 連接:www.jianshu.com/p/b016d10a0… 來源:簡書 著做權歸做者全部

經過ContentResolver查詢Heif格式文件

系統經過ContentProvider向其餘應用暴露圖片等內容信息。目前 還沒有查詢相關文檔 ,未肯定 Android相冊向其餘應用提供了Heif文件查詢支持

經過查詢咱們獲得Heif文件的 主要的 擴展名爲 heicheif.

ContentResolver contentResolver = context.getContentResolver();
String sort = MediaStore.Images.Media.DATE_MODIFIED + " desc ";
String selection = MediaStore.Images.Media.MIME_TYPE + "=?";
String[] selectionArgs = new String[]{"image/heic"};

String[] projection = {MediaStore.Images.Media._ID, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
        MediaStore.Images.ImageColumns.DATE_MODIFIED};

Cursor cursor = contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        selectionArgs,
        sort
);
複製代碼

咱們在測試文件中只找到了 heic, 就先只測一種。

咱們發現,系統支持的狀況下,是能夠查詢到數據的。PS,導入數據到手機後,最好重啓下

解碼與圖片顯示

咱們忽略掉Android版本相應的適配問題,假定已經獲得了相應文件的 Uri, 項目中Glide-4.12.0版本已經處理了適配。

咱們去探索一下,是自行添加的解碼器,仍是依賴於系統API

ExifInterfaceImageHeaderParser 說起內容

/**
 * Uses {@link ExifInterface} to parse orientation data.
 *
 * <p>ExifInterface supports the HEIF format on OMR1+. Glide's {@link DefaultImageHeaderParser}
 * doesn't currently support HEIF. In the future we should reconcile these two classes, but for now
 * this is a simple way to ensure that HEIF files are oriented correctly on platforms where they're
 * supported.
 */
複製代碼

文檔中提到,系統版本 O_MR1+ 中已經支持了 HEIF,可是目前的 DefaultImageHeaderParser 還不支持,將來會綜合考慮這兩個類(系統Exif相關類和DefaultImageHeaderParser),但目前,這是一個簡單的方式,確保HEIF在受支持的平臺上被正確處理圖片方向。

Glide 類中說起的內容

// Right now we're only using this parser for HEIF images, which are only supported on OMR1+.
// If we need this for other file types, we should consider removing this restriction.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
  registry.register(new ExifInterfaceImageHeaderParser());
}
複製代碼

目前僅用於解析HEIF文件的頭信息。

咱們知道,Glide加載是先獲取流,解析頭信息,利用對應的解碼器處理。而加載此類性質的圖片時,是先解碼爲Bitmap,在進行 裝飾 , 而Bitmap的解碼是利用了系統API,見BitmapFactory.

因此,若是項目中使用了Glide(彷佛高於4.10.0即具備功能,沒有仔細查閱),而手機也支持了HEIF,那麼應用就能夠支持Heif顯示了。

Glide官方對於 自定義解碼器 仍是持保守態度的。可是咱們要試一下,嘗試在Glide中接入Heif解碼器。


至此,咱們已經完成了基本目標:

  • 借用平臺自身兼容性(固然也能夠本身根據版本適配查詢語句),利用 ContentResolver 獲取 Heif格式的圖片
  • 藉助Glide已有的實現,直接在支持的平臺版本上解碼、構建Bitmap、構建相應Drawable、呈現。

Glide官方提供了支持,是一件值得慶幸的事情。

由於項目中僅使用了Glide,筆者沒有繼續對Fresco展開調研。而Fresco做爲一款優秀的圖片加載框架,而且有龐大的社區支持,盲目推測其亦實現了內部支持。

接下來展開向更低版本適配的嘗試。固然,這 僅限於 解碼、呈現環節,並不考慮 ContentProviderContentResolver 在低版本上對於Heif格式文件的適配。


嘗試向Glide接入Heif解碼器

將官方測試數據集導入 支持Heif 的小米、華爲部分機型後,我發現部分圖片未被系統支持,提示文件損毀或者不受支持。

另外,衝着好玩,我值得折騰一下。

必須申明:下面的實踐只是從好玩角度出發的,並未考慮 健壯性和全場景覆蓋。

我計劃將Heif文件放入Assets資源,按照咱們對Glide的瞭解,其解碼路徑起始點是:android.content.res.AssetManager$AssetInputStream

@GlideModule
class CustomGlideModule : AppGlideModule() {
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
            registry.register(object : ImageHeaderParser {
                override fun getType(`is`: InputStream): ImageHeaderParser.ImageType {
                    return ImageHeaderParser.ImageType.UNKNOWN
                }

                override fun getType(byteBuffer: ByteBuffer): ImageHeaderParser.ImageType {
                    return ImageHeaderParser.ImageType.UNKNOWN
                }

                override fun getOrientation(`is`: InputStream, byteArrayPool: ArrayPool): Int {
                    return ImageHeaderParser.UNKNOWN_ORIENTATION
                }

                override fun getOrientation(byteBuffer: ByteBuffer, byteArrayPool: ArrayPool): Int {
                    return ImageHeaderParser.UNKNOWN_ORIENTATION
                }

            })
        }

        registry.prepend(
                Registry.BUCKET_BITMAP,
                InputStream::class.java, Bitmap::class.java, CustomBitmapDecoder(context, glide.bitmapPool)
        )
    }

}
複製代碼

這樣,咱們會獲得這樣一條解碼路徑:

DecodePath{ 
	dataClass=class android.content.res.AssetManager$AssetInputStream, 
	decoders=[
		osp.leobert.android.heifdemo.CustomBitmapDecoder@5c4ee9e,
		com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder@1c1ed7f
	],
	transcoder=com.bumptech.glide.load.resource.transcode.BitmapDrawableTranscoder@529014c
}
複製代碼

接下來咱們須要考慮解碼器的接入。

Nokia的SDK

Nokia的Heif庫:連接

草率了,通過一番源碼研讀,發現只有讀寫過程封裝,至關於只有 最基礎 的協議封包、拆包,想要真正在Android上使用,還有不少事情要處理。

a1.jpeg

看下Android P

咱們知道,Android P原生支持了Heif,查一下資料,其底層支持以下:


一番思考後,發現 成本過大

再附上 Glide 適用的Decoder:

class CustomBitmapDecoder(val context: Context, val bitmapPool: BitmapPool) : ResourceDecoder<InputStream, Bitmap> {
    override fun handles(source: InputStream, options: Options): Boolean {
        return true
    }

    @Throws(IOException::class)
    fun toByteArray(input: InputStream): ByteArray? {
        val output = ByteArrayOutputStream()
        copy(input, output)
        return output.toByteArray()
    }

    @Throws(IOException::class)
    fun copy(input: InputStream, output: OutputStream): Int {
        val count = copyLarge(input, output)
        return if (count > 2147483647L) {
            -1
        } else count.toInt()
    }

    @Throws(IOException::class)
    fun copyLarge(input: InputStream, output: OutputStream): Long {
        val buffer = ByteArray(4096)
        var count = 0L
        var n = 0
        while (-1 != input.read(buffer).also { n = it }) {
            output.write(buffer, 0, n)
            count += n.toLong()
        }
        return count
    }

    override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<Bitmap>? {

        val heif = HEIF()
        try {
            val byteArray = toByteArray(source)
            // Load the file
            heif.load(ByteArrayInputStream(byteArray))

            // Get the primary image
            val primaryImage = heif.primaryImage

            // Check the type, assuming that it's a HEVC image
            if (primaryImage is HEVCImageItem) {
// val decoderConfig = primaryImage.decoderConfig.config

                val imageData = primaryImage.itemDataAsArray
                // Feed the data to a decoder

                // FIXME: 2021/3/23 find a decoder to generate Bitmap when not upon Android P
                return BitmapResource.obtain(
                        BitmapFactory.decodeByteArray(imageData, 0, imageData.size),
                        bitmapPool
                )
            }
        } // All exceptions thrown by the HEIF library are of the same type
        // Check the error code to see what happened
        catch (e: Exception) {
            e.printStackTrace()
        } finally {
            heif.release()
        }
        return null
    }
}
複製代碼

若是找到了一個解碼器,在Android P如下支持解碼或轉碼,封裝爲Bitmap,就 能夠在低版本上適配 了。固然還須要完成:適配全部可能的解碼路徑頭信息處理 工做。

此次嘗試, 以失敗了結

竟然翻車了,壓壓驚

jljt.gif

遐想

力大磚飛?集成ImageMagick之類的庫,直接實現圖片轉碼,成本有點過大了,先不折騰。

本次實踐,咱們實現了基本目標,高級目標由於初步調研不充分以失敗了結,可是也增加了知識。

相關文章
相關標籤/搜索