调用Skia内置位图编解码器的坑点

Posted on November 2, 2016

Skia内置了对大部分图片格式的编解码支持,几乎所有格式都可以解码,编码主要支持:JPEG,PNG,WEBP等。然而想直接调用Skia去编解码图片,还有一些注意事项,这方面几乎没搜索到多少相关文档,一开始走了不少弯路,通过研究源码才一点一点走通了,这里总结一下方便遇到同样问题的人。

先来看最常用的位图解码:

    bool decode(const void* bytes, size_t length, SkBitmap* image) {
        sk_sp<SkData> data = SkData::MakeWithoutCopy(bytes, length);
        SkAutoTDelete<SkCodec> codec(SkCodec::NewFromData(data.get()));
        if (!codec) {
            return false;
        }

        SkImageInfo info = codec->getInfo();
        auto alphaType = imageInfo.isOpaque() ? kOpaque_SkAlphaType : kPremul_SkAlphaType;
        auto info = SkImageInfo::Make(imageInfo.width(), imageInfo.height(), kN32_SkColorType, alphaType);
        auto result = codec->getPixels(info, image->getPixels(), image->rowBytes());
        return result == SkCodec::kSuccess;
    }

上面这个函数就能实现调用Skia对字节流数据进行位图解码。解释一下几个参数: bytes是位图文件的字节流首地址,这个很简单,通过各种标准文件系统接口就能读取。length就是这个字节流的长度,单位字节。image参数是一个SkBitmap对象,用于存储解码后的位图结果。在外部实例化一个空的SkBitmap,并把指针传进来就行。

这里主要的坑点是对包含透明度的图片,Skia只支持预乘透明度格式(Premultiplied Alpha)的图片渲染。具体什么是 Premultiplied Alpha,大家可以自行谷歌一下,是一种常见的渲染上的性能优化方式。我们的位图文件通常都不是按 Premultiplied Alpha 方式存储的,所以这里 codec 直接分析出来的 SkImageInfo 一般也是不能用的。所以要判断一下,图片是否含有透明通道,如果有的话,强制把 alphaType 改为 kPremul_SkAlphaType 的,也就是这句:auto alphaType = imageInfo.isOpaque() ? kOpaque_SkAlphaType : kPremul_SkAlphaType; 这样解码出来的图片就能正确渲染了。

再来看一下位图编码:

    void encode(const SkBitmap& image) {
        auto encodeType = SkImageEncoder::kJPEG_Type;
        auto compressionQuality = 92;
        SkDynamicMemoryWStream stream;
        auto result = SkImageEncoder::EncodeStream(&stream, image, encodeType, compressionQuality);
        if (result) {
            auto length = stream.bytesWritten();
            auto bytes = new uint8_t[length];
            stream.copyTo(bytes);
            // do something with the bytes ... 
        }
    }

上面这个简单的示例展示了如何把一个 SkBitmap 对象编码为 JPEG 格式的图片文件字节流。当然你也可以把 encodeType 改为其他的格式,比如:SkImageEncoder::kPNG_TypeSkImageEncoder::kWEBP_TypecompressionQuality 表示图片的压缩质量,这个参数对PNG格式无效,因为PNG是无损的。最后我们会得到一个 bytes 字节流首地址,以及一个 length 字节流长度。需要存储文件就直接保存这个字节流即可。

这里的坑点是你直接这么调用的话,结果会总是编码失败。断点进去看运行过程就会发现因为缺少对应类型的编码器,实际上一个编码器都不存在。这是因为 SkImageEncoder 采用的是注册机制,这是被动调用的(你也可以注册自己的编码器进去)。虽然Skia默认提供了各种格式的编码器,也写好了注册代码,但是你自己的最终项目里因为没直接调用过那些编码器,只调用了 SkImageEncoder, C++在链接你的程序的时候就会抛弃库里的实际编码器,因为你没调用过。所以解决方案是显式调用一下。好在Skia给我们写了一个专门用来欺骗编译器的宏。

#include <SkForceLinking.h>

__SK_FORCE_IMAGE_DECODER_LINKING;

你只需要像上面那样,包含 SkForceLinking.h 的头文件,然后在工程里任何一个地方加上这句宏的调用即可:__SK_FORCE_IMAGE_DECODER_LINKING;。这样之前的代码就可以正常编码位图了。