欢迎光临
我们一直在努力

好玩系列:让项目中的相册支持Heif格式图片

前言

目前市面上的成熟的APP,其用户体系中均存在 设置头像 的功能,考虑到尺寸规范问题,一般会加入 图片裁剪 功能;
考虑到页面UI统一度问题,甚至会在应用内实现 相册功能。据此推断:各位的项目中,会遇到 Heif格式图片 需要兼容的需求。

笔者目前参与的商业项目,也被市场要求对Heif图片进行适配。这篇文章,记录了我在这件事情上 折腾 的过程。

好玩系列是我进行 新事物实践 、 尝试创造 的记录,了解更多

背景

HEIF格式的全名为 High Efficiency Image File Format(高效率图档格式),是由动态图像专家组(MPEG)在2013年推出的新格式,了解更多

了解Heif整个项目

测试文件

笔者注:印象中,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。

便宜美国vps

我们常见的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在受支持的平台上被正确处理图片方向。

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作为一款优秀的图片加载框架,并且有庞大的社区支持,盲目推测其亦实现了内部支持。

接下来展开向更低版本适配的尝试。当然,这 仅限于 解码、呈现环节,并不考虑 ContentProvider, ContentResolver 在低版本上对于Heif格式文件的适配。

尝试向Glide接入Heif解码器

将官方测试数据集导入 支持Heif 的小米、华为部分机型后,我发现部分图片未被系统支持,提示文件损毁或者不受支持。

另外,冲着好玩,我值得折腾一下。

必须申明:下面的实践只是从好玩角度出发的,并未考虑 健壮性和全场景覆盖。

我计划将Heif文件放入Assets资源,按照我们对Glide的了解,其解码路径起始点是:android.content.res.AssetManager$AssetInputStream

@GlideModuleclass 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之类的库,直接实现图片转码,成本有点过大了,先不折腾。

本次实践,我们实现了基本目标,高级目标因为初步调研不充分以失败告终,但是也增长了知识。

73624242

赞(0)
【声明】:本博客不参与任何交易,也非中介,仅记录个人感兴趣的主机测评结果和优惠活动,内容均不作直接、间接、法定、约定的保证。访问本博客请务必遵守有关互联网的相关法律、规定与规则。一旦您访问本博客,即表示您已经知晓并接受了此声明通告。