Loading...
墨滴

掀乱书页的风

2021/10/15  阅读:63  主题:兰青

Android P heif图片适配

一.什么是HEIF图片

[官方介绍](http://nokiatech.github.io/heif/)

  1. HEIF(High Efficiency Image Format),高效率图片格式,采用 HEVC 编码格 式,由 MPEG 标准组织制定,2015 年 ISO 批准发布
  2. 苹果自 2017.9.20 发布的 iOS11 开始拍照保存支持 HEIF 格式(iPhone 7 及以上 机型)
  3. 谷歌O 版本 MR1 支持 HEIF 静态图的软解码, P 版本,支持 HEIF 软件解码、软件编码
  4. 华为 EMUI 9.0 上全面继承谷歌 P 版本能力,海思 980 芯片支持 HEIF 硬解码

HEIF 图片价值:

  1. 同等图片质量压缩率是 JPEG 的 2.39 倍,可节省约 50%空间,节省网络传输流量
  2. 支持存储多张图片(图片集合、序列图等,如连拍)
  3. 支持动态图片(类似 Gif 动图)
  4. 支持图片深度信息、透明度信息

heif与webp、jpg对比

文件头结构

FileTypeBox在文件中有且仅有一个,它的类型字段值为ftyp,位于文件起始位置,其中的brand定义了文件中所存放的媒体类型,它的定义如下

如果文件为HEIF格式,它的major_brand字段会是以下表格中所示:目标:替代JPEG,成为主流图片类型

那么问题来了,目前我们并不支持这种图片的加载,比如从相册选择图片的时候,会出现白面,无法显示

Google的解决方案

1 通过P版本新增的类ImageDecoder加载图片 参考实现代码:

public static Drawable getHeifImageFromSdcardUseImageDecoder(String path) throws IOException {
    File file = new File(path);
    ImageDecoder.Source source = ImageDecoder.createSource(file); 
    return ImageDecoder.decodeDrawable(source);
}

2 直接使用BitmapFactory来解码

public static Bitmap getHeifImageFromSdcardUseBitmapFactory(String path) 
    return BitmapFactory.decodeFile(path);
}

这两种方式可以实现图片的显示,但是这种原生加载并不会为我们处理图片内存,如果处理不当会造成内存泄漏等问题,这不是我们想要的。

我们项目里面是用的fresco加载,首先我们就会想到,fresco是否支持这种格式呢。

fresco自定义支持新图片格式

我笔记本vpn上不了fresco官网,只能找到中文版的一些介绍,坑爹的是,关于自定义图片格式的这部分,翻译的时候都给忽略掉了。

下面是我根据fresco官网给出的建议,把内容进行了整理,大家可以直接在GitHub源码里面找到详细的英文介绍 fresco-master/docs/_docs/

一般而言,图像显示需要两部分

  1. 解码图像
  2. 渲染解码的图像

fresco支持自定义这两个部分,比如,可以为现有图片格式添加自定义的解码器,也可以用fresco内置的渲染体系支持一种新的图片格式。 或者可以让内置解码器负责解码,然后创建一个自定义drawable来渲染图像。这两部分根据需要可以同时自定义。自定义选项可以设置为全局生效或者本地生效。

简化的解码和渲染过程如下:

  1. 加载从网络下载的或者本地磁盘缓存中编码图像
  2. EncodedImage的ImageFormat是用ImageFormatChecker类来确定的,该类有一个ImageFormat.FormatChecker对象列表,每个可识别的图像格式都有一个ImageFormat对象
  3. EncodedImage使用给定格式的合适ImageDecoder进行解码,并返回一个继承了CloseableImage的对象,这个对象表示解码图像
  4. 从DrawableFactory对象列表中,第一个能够处理CloseableImage对象用于创建Drawable
  5. Drawable在屏幕上显示

可以通过在步骤2中添加ImageFormat.FormatChecker来添加自定义图像格式,然后自定义ImageDecoder来为新图像格式添加解码支持或覆盖内置解码。 最后,可以自定义DrawableFactory使用自定义Drawable来渲染图像。

所有默认的图像格式都可以在DefaultImageFormats和DefaultImageFormatChecker找到,默认的drawablefactory在PipelineDraweeController中。fresco在Showcase里有自定义demo

自定义解码器

自定义解码器需要实现ImageDecoder这个接口

public class CustomDecoder implements ImageDecoder {

  @Override
  public CloseableImage decode(
      EncodedImage encodedImage,
      int length,
      QualityInfo qualityInfo,
      ImageDecodeOptions options)
 
{
    // Decode the given encodedImage and return a
    // corresponding (decoded) CloseableImage.
    CloseableImage closeableImage = ...;
    return closeableImage;
  }
}

给定的encodedImage可用于返回一个扩展CloseableImage的类, CloseableImage表示已解码的图像,然后将自动缓存。 可以返回一个现有的CloseableImage类型,比如bitmap的CloseableStaticBitmap ,或自定义的CloseableImage类

自定义解码器可以设置全局生效或者局部生效,如果是本地生效,可以这么设置:

ImageDecoder customDecoder = ...;
Uri uri = ...;
draweeView.setController(
  Fresco.newDraweeControllerBuilder()
        .setImageRequest(
          ImageRequestBuilder.newBuilderWithSource(uri)
              .setImageDecodeOptions(
                  ImageDecodeOptions.newBuilder()
                      .setCustomImageDecoder(customDecoder)
                      .build())
              .build())
        .build());

注意:如果设置了自定义解码器,它会用于所有的图片,默认的解码器会被覆盖

自定义图像格式

在代码中创建一个ImageFormat对象:

private static final ImageFormat CUSTOM_FORMAT = new ImageFormat("format name""format file extension");

默认支持的图片格式都可以在DefaultImageFormats类中找到,然后,我们需要创建一个用于检测新图像格式的自定义ImageFormat.FormatChecker 。 FormatChecker有两个方法,一个是确定所需的头字节数(由于该操作是针对所有图像执行的,所以尽可能减小该数字)还有一个方法是检测图像格式(应该返回相同的ImageFormat实例) ,在此示例中为CUSTOM_FORMAT ;如果图像格式不同,返回 null 。 下面是一个简单的FormatChecker:

public static class ColorFormatChecker implements ImageFormat.FormatChecker {

  private static final byte[] HEADER = ImageFormatCheckerUtils.asciiBytes("my_header");

  @Override
  public int getHeaderSize() {
    return HEADER.length;
  }

  @Nullable
  @Override
  public ImageFormat determineFormat(byte[] headerBytes, int headerSize) {
    if (headerSize < getHeaderSize()) {
      return null;
    }
    if (ImageFormatCheckerUtils.startsWithPattern(headerBytes, HEADER)) {
      return CUSTOM_FORMAT;
    }
    return null;
  }
}

自定义图像格式,第三步就是自定义解码器,上面已经提到过了。

如果想要fresco支持自定义的图片格式,需要在fresco初始化的时候通过mageDecoderConfig进行注册,同样可以用自定义解码器覆盖内置的图像格式:

ImageFormat myFormat = ...;
ImageFormat.FormatChecker myFormatChecker = ...;
ImageDecoder myDecoder = ...;
ImageDecoderConfig imageDecoderConfig = new ImageDecoderConfig.Builder()
  .addDecodingCapability(
    myFormat,
    myFormatChecker,
    myDecoder)
  .build();

ImagePipelineConfig config = ImagePipelineConfig.newBuilder()
  .setImageDecoderConfig(imageDecoderConfig)
  .build();

Fresco.initialize(context, config);

自定义drawables

如果使用DraweeController加载图像(例如,如果使用DraweeView ),则使用相应的DrawableFactory创建基于CloseableImage渲染解码图像的CloseableImage 。 如果使用imagepipeline,必须处理CloseableImage本身。

如果使用fresco其中一种内置类型,比如CloseableStaticBitmap ,则PipelineDraweeController已经知道如何处理该格式,并且会自动创建一个BitmapDrawable。 如果想覆写这种处理方式或者添加对自定义CloseableImage的支持,就必须实现一个drawablefactory:

public static class CustomDrawableFactory implements DrawableFactory {

  @Override
  public boolean supportsImageType(CloseableImage image) {
    // You can either override a built-in format, like `CloseableStaticBitmap`
    // or your own implementations.
    return image instanceof CustomCloseableImage;
  }

  @Nullable
  @Override
  public Drawable createDrawable(CloseableImage image) {
    // Create and return your custom drawable for the given CloseableImage.
    // It is guaranteed that the `CloseableImage` is an instance of the
    // declared classes in `supportsImageType` above.
    CustomCloseableImage myCloseableImage = (CustomCloseableImage) image;
    Drawable myDrawable = ...; //e.g. new CustomDrawable(myCloseableImage)
    return myDrawable;
  }- 
  
  
  
  
  
}

如果想用自定义的DrawableFactory,就必须进行全局或者本地设置

全局覆写 自定义drawable

在fresco初始化的时候注册

DrawableFactory myDrawableFactory = ...;

DraweeConfig draweeConfig = DraweeConfig.newBuilder()
  .addCustomDrawableFactory(myDrawableFactory)
  .build();

Fresco.initialize(this, imagePipelineConfig, draweeConfig);

###本地覆写 自定义drawable PipelineDraweeControllerBuilder提供了一个方法来进行设置

DrawableFactory myDrawableFactory = ...;
Uri uri = ...;

simpleDraweeView.setController(Fresco.newDraweeControllerBuilder()
  .setUri(uri)
  .setCustomDrawableFactory(factory)
  .build());

了解了fresco的介绍,下面我们要根据自己的需要来自定义支持

考虑一个问题: 新的图片格式是动态图还是静态图?

  1. 如果是静态图可以直接使用CloseableStaticBitmap,也可以自己定义(个人认为没有必要,除非情况特殊)
  2. 如果是动态图片比较复杂,理论上可以实现,成本较高,可参考gif和webp

虽然在HEIF官方介绍中宣称支持动态图片 但是heif格式目前用到的都是静态图片,包括Nokia官网的GitHub提供的所有示例图片也是静态图片,所以可以暂时当成静态图片来处理,fresco后续会支持heif格式,我们还是要采用fresco官方代码来加载heif图片。

自定义图片格式和实现ImageFormat.FormatChecker接口

public class HeifFormatChecker implements ImageFormat.FormatChecker {
    //第一步,定义图像格式
    public static final ImageFormat HEIF = new ImageFormat("HEIF""heif");
    /**
   * Every HEIF image starts with a specific signature. It's guaranteed to include "ftyp". The 4th
   * byte of the header gives us the size of the box, then we have "ftyp" followed by the exact
   * image format which can be one of: heic, heix, hevc, hevx.
   */

    private static final String HEIF_HEADER_PREFIX = "ftyp";
 //需要添加"mif1","msf1"格式  fresco官网有bug 参考http://nokiatech.github.io/heif/technical.html

  private static final String[] HEIF_HEADER_SUFFIXES = {"heic""heix""hevc""hevx""mif1","msf1"};
    private static final int HEIF_HEADER_LENGTH =
            ImageFormatCheckerUtils.asciiBytes(HEIF_HEADER_PREFIX + HEIF_HEADER_SUFFIXES[0]).length;
    //第二步实现ImageFormat.FormatChecker接口
    @Override
    public int getHeaderSize() {
        return HEIF_HEADER_LENGTH;
    }

    @android.support.annotation.Nullable
    @Override
    public ImageFormat determineFormat(byte[] headerBytes, int headerSize) {
        if (headerSize < HEIF_HEADER_LENGTH) {
            return null;
        }

        final byte boxLength = headerBytes[3];
        if (boxLength < 8) {
            return null;
        }

        for (final String heifFtype : HEIF_HEADER_SUFFIXES) {
            final int indexOfHeaderPattern =
                    indexOfPattern(
                            headerBytes,
                            headerBytes.length,
                            ImageFormatCheckerUtils.asciiBytes(HEIF_HEADER_PREFIX + heifFtype),
                            HEIF_HEADER_LENGTH);
            if (indexOfHeaderPattern > -1) {
                return HEIF;
            }
        }
        return null;
    }

    /**
     * Checks if byteArray interpreted as sequence of bytes contains the pattern.
     * @param byteArray the byte array to be checked
     * @param pattern the pattern to check
     * @return index of beginning of pattern, if found; otherwise -1
     */

    private static int indexOfPattern(
            final byte[] byteArray,
            final int byteArrayLen,
            final byte[] pattern,
            final int patternLen)
 
{
        Preconditions.checkNotNull(byteArray);
        Preconditions.checkNotNull(pattern);
        if (patternLen > byteArrayLen) {
            return -1;
        }

        byte first = pattern[0];
        int max = byteArrayLen - patternLen;

        for (int i = 0; i <= max; i++) {
            // Look for first byte
            if (byteArray[i] != first) {
                while (++i <= max && byteArray[i] != first) {
                }
            }

            // Found first byte, now look for the rest
            if (i <= max) {
                int j = i + 1;
                int end = j + patternLen - 1;
                for (int k = 1; j < end && byteArray[j] == pattern[k]; j++, k++) {
                }

                if (j == end) {
                    // found whole pattern
                    return i;
                }
            }
        }
        return -1;
    }
}

自定义HeifDecoder

因为jpg格式也是静态图片,所以参考jpg格式解码方式来实现

public class HeifDecoder implements ImageDecoder {

    @Override
    public CloseableImage decode(EncodedImage encodedImage, int length, QualityInfo qualityInfo,
                                 ImageDecodeOptions options)
 
{

        return decodeStaticImage(encodedImage, options);
    }
    /**
    *返回fresco封装好的一个CloseableStaticBitmap
    */

    private CloseableStaticBitmap decodeStaticImage(
            final EncodedImage encodedImage,
            ImageDecodeOptions options)
 
{
        CloseableReference<Bitmap> bitmapReference =
                FrescoWubaCore.getImagePipelineFactory().getPlatformDecoder()
                        .decodeFromEncodedImage(encodedImage, options.bitmapConfig);
        try {
            return new CloseableStaticBitmap(
                    bitmapReference,
                    ImmutableQualityInfo.FULL_QUALITY,
                    encodedImage.getRotationAngle());
        } finally {
            bitmapReference.close();
        }
    }
}

全局配置

//自定义heif格式图片设置
ImageFormat heifFormat = HeifFormatChecker.HEIF;
ImageFormat.FormatChecker heifChecker = new HeifFormatChecker();
ImageDecoder heifDecoder = new HeifDecoder();
ImageDecoderConfig heifConfig = new ImageDecoderConfig.Builder()
                .addDecodingCapability(
                        heifFormat,
                        heifChecker,
                        heifDecoder)
                .build();

ImagePipelineConfig pipelineConfig = ImagePipelineConfig.newBuilder(mContext)
                .setImageDecoderConfig(heifConfig) //添加自定义heif解码器配置
                .build();
FrescoWubaCore.init(mContext, pipelineConfig);

到这里fresco已经可以像加载普通图片一样为我们加载heif格式的图片了。

如果是动态图片,可以参考下 GifImage和WebImage,理论上可以实现,成本较高

自定义图片格式和解码器是如何生效的

/**
     * Add a new decoding cabability for a new image format.
     *
     * @param imageFormat the new image format
     * @param imageFormatChecker the format checker that can determine the new image format
     * @param decoder the decoder that can decode the new image format
     * @return the builder
     */

    public Builder addDecodingCapability(
        ImageFormat imageFormat,
        ImageFormat.FormatChecker imageFormatChecker,
        ImageDecoder decoder)
 
{
      if (mCustomImageFormats == null) {
        mCustomImageFormats = new ArrayList<>();
      }
      //将imageFormatChecker加入到自定义列表中
      mCustomImageFormats.add(imageFormatChecker);
      overrideDecoder(imageFormat, decoder);
      return this;
    }
/**
     * Use a different decoder for an existing image format.
     * This can be used for example to set a custom decoder for any of the
     * {@link com.facebook.imageformat.DefaultImageFormats}
     *
     * @param imageFormat the existing image format
     * @param decoder the decoder to use
     * @return the builder
     */

    public Builder overrideDecoder(ImageFormat imageFormat, ImageDecoder decoder) {
      if (mCustomImageDecoders == null) {
        mCustomImageDecoders  = new HashMap<>();
      }
      //将imageFormat和decoder以map的形式存储
      mCustomImageDecoders.put(imageFormat, decoder);
      return this;
    }

DefaultImageDecoder中

private final Map<ImageFormat, ImageDecoder> mCustomDecoders;

@Override
  public CloseableImage decode(
      final EncodedImage encodedImage,
      final int length,
      final QualityInfo qualityInfo,
      final ImageDecodeOptions options)
 
{
    ImageFormat imageFormat = encodedImage.getImageFormat();
    if (imageFormat == null || imageFormat == ImageFormat.UNKNOWN) {
      imageFormat = ImageFormatChecker.getImageFormat_WrapIOException(
          encodedImage.getInputStream());
      encodedImage.setImageFormat(imageFormat);
    }
    //如果自定义解码器不为空,某个格式的图片有自定义的解码器,就使用自定义的解码器进行解码
    if (mCustomDecoders != null) {
      ImageDecoder decoder = mCustomDecoders.get(imageFormat);
      if (decoder != null) {
        return decoder.decode(encodedImage, length, qualityInfo, options);
      }
    }
    return mDefaultDecoder.decode(encodedImage, length, qualityInfo, options);
  }

番外

heif格式扫描(推荐使用ContentProvider方式)

实现上传、分享或者是发送手机本地图片功能,肯定需要用到手机本地图片的扫描,如果自己扫描的话,还需要根据手机版本判断手机支持的图片编解码格式,所以推荐使用ContentProvider扫描手机中的图片,系统会把支持的所有解码格式的图片文件返回给应用,不需要应用自己再去做格式判断:

参考实现:

public static void getAllImagesLocal(final Context context) new Thread(new Runnable() {
@Override
public void run() {
Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, nullnullnullnull); while (cursor.moveToNext()) {
String path = cursor.getString(cursor .getColumnIndex(MediaStore.Images.Media.DATA));
Log.e(TAG, "image path:" + path); }
} }).start();
}

如果是在支持Heif编解码的手机执行,系统会把Heif格式的图片的扫描结果返回给应用,当然如果是在不支持Heif格式的手机执行的结果也不会有Heif格式图片返回

Heif格式转成JPG格式

Heif格式转成JPG格式主要是考虑向下兼容,支持Heif的手机向不支持Heif手机之前发送Heif格式图片,需要考虑兼容性,直接发送原始的Heif格式图片到不支持Heif格式的手机,将会出现,发送过去的图片无法使用的情况,为了避免这种情况,可以考虑在发送之前将Heif格式的图片转换成JPG格式再发送

动态图片格式的自定义支持

主要涉及以下几个类。 DefaultImageDecoder.java

AnimatedImageFactoryImpl.java

DefaultDrawableFactory.java

可以参考GifImage WebPImage

参考资料:heif官网

                  heif格式解析

掀乱书页的风

2021/10/15  阅读:63  主题:兰青

作者介绍

掀乱书页的风