好玩系列:让项目中的相册支持Heif格式图片
前言
目前市面上的成熟的APP,其用户体系中均存在 设置头像
的功能,考虑到尺寸规范问题,一般会加入 图片裁剪
功能;
考虑到页面UI统一度问题,甚至会在应用内实现 相册
功能。据此推断:各位的项目中,会遇到 Heif格式图片
需要兼容的需求。
笔者目前参与的商业项目,也被市场要求对Heif图片进行适配。这篇文章,记录了我在这件事情上 折腾
的过程。
好玩系列是我进行
新事物实践
、尝试创造
的记录,了解更多
背景
HEIF格式的全名为 High Efficiency Image File Format(高效率图档格式),是由动态图像专家组(MPEG)在2013年推出的新格式,了解更多
笔者注:印象中,iOS系统大约在16年就全面支持这一类型的文件了,而Android大约是三年前,在Android P推出的时候,宣布原生支持Heif文件
随着市场上的Android机器已经大面积过渡到 Android Q
,从这一点看,确实到了该适配的阶段了。
目标,至少实现Android P及其以上的适配,尝试向更低版本适配
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的方式保存。
作者:金山视频云 链接:https://www.jianshu.com/p/b016d10a087d 来源:简书 著作权归作者所有
通过ContentResolver查询Heif格式文件
系统通过ContentProvider向其他应用暴露图片等内容信息。目前 尚未查询相关文档
,未确定 Android相册向其他应用提供了Heif文件查询支持
通过查询我们得到Heif文件的 主要的
扩展名为 heic
、 heif
.
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在受支持的平台上被正确处理图片方向。
// 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作为一款优秀的图片加载框架,并且有庞大的社区支持,盲目推测其亦实现了内部支持。
接下来展开向更低版本适配的尝试。当然,这 仅限于
解码、呈现环节,并不考虑 ContentProvider
, ContentResolver
在低版本上对于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上使用,还有很多事情要处理。
看下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,就 可以在低版本上适配
了。当然还需要完成:适配所有可能的解码路径
,头信息处理
工作。
这次尝试, 以失败告终
。
居然翻车了,
遐想
力大砖飞?集成ImageMagick之类的库,直接实现图片转码,成本有点过大了,先不折腾。
本次实践,我们实现了基本目标,高级目标因为初步调研不充分以失败告终,但是也增长了知识。