谈谈 GIF 格式

谈谈 GIF 格式

GIF 的起源

GIF 的全称是 Graphics Interchange Format,它是由 CompuServe 公司在 1987 年开发的图像格式,年龄比我还大。在 iOS 平台写过 GIF 相关程序的人可能会对 CompuServe 有点眼熟,没错,GIF 的 UTI 是 com.compuserve.gif

GIF 有三个特点:256 色、LZW 压缩、支持多张图像

所以长期以来,GIF 在社交网络上扮演着一个不可或缺的角色,我们称之为动图,AKA 表情包。

iOS 平台的历史问题

长期以来,iOS 被 Android 歧视的一个点就是不支持 GIF,甚至每一代 iPhone 推出都要有人谣传这一代 iPhone 要支持 GIF 了。可是真相真的是这样吗?

并不是,iPhone 一开始就支持 GIF,一个在 Safari 里面保存到相册的 GIF,导出到 macOS 上面,他还是一个 GIF,原有的格式并没有被丢掉。

导致这个问题的罪魁祸首,是 iOS 平台的照片应用,不支持播放 GIF,他只会展示 GIF 的第一帧。所以有趣的事情就来了,第三方应用们开始集体认为 iOS 不支持 GIF,所以他们在保存 GIF 的时候不约而同的丢掉了 GIF 原有的格式。这样,被保存到相册的图片已经不是 GIF 了(关于这一点我在一个回答中有讨论到:为什么 iPhone iPad 在设计的时候不能播放 GIF 图片? - 钟颖Cyan 的回答)。

我说的是 QQ、微信、微博 的早期版本,鉴于这三个 app 占领了国内社交网络绝大部分的份额,所以 iPhone 对 GIF 的支持,就成了一个历史悬案。所幸的是,在最近的几个版本里面,这三个 app 又陆陆续续支持了 GIF 的正确保存和读取。所以到了 2016 年的今天,iPhone 在社交网络上看到的 GIF 终于可以流通,相册可以成为这样一个容器了(这里还是要吐槽一下微信的封闭,流进微信的 GIF 是无法再流出的)。

GIF 在 iOS 平台的一些技术

上面谈到了一些 GIF 的历史和现状,下面讲讲 iOS 平台上面 GIF 的一些技术。主要包括显示、合成、裁剪、滤镜等等,不会讲的很详细。

显示 GIF

对于 iOS 平台而言,二进制文件都是一个 NSData,对于 GIF 而言,这个 data 里面就包括了 GIF 的一些元数据和每张图片的数据。UIImage 是并不直接支持 GIF 的展示的,所以要对 GIF data 进行 decode。最原始的方法是使用 ImageIO Framework 里面的方法:

CGImageSourceCreateWithData // 创建源
CGImageSourceCopyProperties // 获得元数据
CGImageSourceGetCount // 获得帧数
CGImageSourceCreateImageAtIndex // 获得某帧 ...

通过这些方法对 data 进行 decode 之后可以得到 image 的列表和 duration 的列表,这是决定 GIF 播放最重要的两个因素。

了解内在的原理是非常重要的,但是现实中你不必再造一个轮子。结合我对各个 GIF 库的使用,我推荐两个比较好的,一个是 Flipboard 团队开源的 FLAnimatedImage,一个是 ibireme 大神开源的 YYImage。尤其是 YYImage,他有一个写的很棒的 Decoder,我们在后面的讨论中会用到它。

合成 GIF

所谓合成,其实是将分解的过程反过来,用的框架也是 ImageIO Framework。通常会有两种需求,一种是将一个视频文件转换到 GIF 格式,另一种是将一个编辑过的 GIF 文件重新保存起来。这两种其实只有一个区别,就是视频保存的时候,需要用 AVAssetImageGenerator 将视频帧取出,得到 image 和 duration,这样问题就归纳成了另一种情况了。GIF 的合成利用的主要是下面的几个函数:

CGImageDestinationCreateWithURL // 创建文件
CGImageDestinationAddImage // 添加帧
CGImageDestinationSetProperties // 设置元数据
CGImageDestinationFinalize // 保存

关于这一点可以参考一个叫做 NSGIF 的开源项目,与上述两个项目不同的是,这个项目的代码写的很一般,鉴于代码也不多,我建议自己重写。

控制播放速度

你可能在 app 里面有控制 GIF 播放速度的要求,这一点可以通过修改 duration 列表来实现,这一点用 YYImage 来实现十分简单。我自己的做法是继承了 YYImage,提供了一个 durationScale 的方法。然后通过重写代理方法来实现这个需求:

- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
    NSTimeInterval duration = [super animatedImageDurationAtIndex:index] * self.durationScale;
    return duration;
}

当你更改了播放速度的时候,durationScale 发生变化,随之播放速度也发生了变化。

GIF 裁剪

对于图片的裁剪,这里必须要提到一个在这方面做得最好的一个库:TOCropViewController,这个库在体验方面非常接近原生相册的裁剪体验,值得推荐。不过要让这个库支持 GIF 的话,要做两个改动:

  • TOCropView.m 里面的 backgroundImageViewforegroundImageView 都改成你用的支持 GIF 播放的 ImageView 实现。
  • 实现裁剪得到矩形的代理方法,逐帧对 GIF 进行裁剪,然后使用上述的方法进行合成

实时滤镜

对 GIF 进行实施的滤镜效果,可以通过 CoreImage Framework 或者 GPUImage 来实现,这里介绍 GPUImage 的方案。首先要知道到,GPUImage 如何处理一张图片的滤镜效果:

UIImage *input = ...;
GPUImageFilter *filter = [[GPUImageFilter alloc] init];
[filter forceProcessingAtSize:input.size];
GPUImagePicture *picture = [[GPUImagePicture alloc] initWithImage:input];
[picture addTarget:filter];
[picture processImage];
[filter useNextFrameForImageCapture];
UIImage *output = filter.imageFromCurrentFramebuffer;

GPUImageFilter 这个东西是个基类,在现实中把它替换成加载了不同 shader 的实现类。
现在要做的就是在对 GIF 实时应用滤镜效果,方法是在播放 GIF 的组件返回某一帧的时候对其使用上述的方法。
出于 GPUImage 强大的性能,这样并不会有太大的性能问题,但你还是应该对这个过程进行缓存。
所幸的是 YYImage 的实现里面,已经包含了一个缓存,也就是说 animatedImageFrameAtIndex 拿到的是缓存后的结果。
所以在切换滤镜的时候,有必要清除掉这个缓存,让滤镜效果可以重新应用。这个缓存在 YYAnimatedImageView_buffer 里面。

Photos Framework 与 GIF

Photos Framework 是 iOS 8 之后用于代替 ALAssetLibrary 的一个框架,这里讲的是如何用它来读取和保存 GIF 文件到相册。

读取使用的是 PHAsset,每个 Photos 对象都是一个 PHAsset,读取 GIF 的话,只要使用读取 data 的方法就可以了:

// Request largest represented image as data bytes, resultHandler is called exactly once (deliveryMode is ignored).
//     If PHImageRequestOptionsVersionCurrent is requested and the asset has adjustments then the largest rendered image data is returned
//     In all other cases then the original image data is returned
// resultHandler for asynchronous requests, always called on main thread
- (PHImageRequestID)requestImageDataForAsset:(PHAsset *)asset options:(nullable PHImageRequestOptions *)options resultHandler:(void(^)(NSData *__nullable imageData, NSString *__nullable dataUTI, UIImageOrientation orientation, NSDictionary *__nullable info))resultHandler;

拿到的 data 就是 GIF 文件,而如果使用读取 image 的话,将会得到一帧。

GIF 的保存对于 iOS 8 和 iOS 9 要使用两种不同的方法,iOS 9 的比较简单,使用 PHAssetCreationRequestaddResourceWithType 即可:

PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAsset];
[(PHAssetCreationRequest *)request addResourceWithType:PHAssetResourceTypePhoto
                                                  data:data
                                               options:nil];

而 iOS 8 就比较麻烦了,需要构造一个文件然后再使用 PHAssetChangeRequestcreationRequestForAssetFromImageAtFileURL

NSString *temporaryFileName = [NSProcessInfo processInfo].globallyUniqueString;
NSString *temporaryFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:temporaryFileName];
NSURL *temporaryFileURL = [NSURL fileURLWithPath:temporaryFilePath];
NSError *error = nil;
[data writeToURL:temporaryFileURL options:NSDataWritingAtomic error:&error];
if (error == nil) {
    request = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:temporaryFileURL];
    [[NSFileManager defaultManager] removeItemAtURL:temporaryFileURL error:nil];
} else {
    ...
}

不管怎么样都不能使用 UIImageWriteToSavedPhotosAlbum 这个方法,这个方法存到相册就只有一帧。

GIF 与剪贴板

这是本文最后一个话题,如何在剪贴板里面获取 GIF 文件,以及把 GIF 放进剪贴板。

从剪贴板里面获取 GIF,要分几个步骤走。

首先,通过 com.compuserve.gif 直接拿:

NSData *data = [[UIPasteboard generalPasteboard] dataForPasteboardType:@"com.compuserve.gif"];

但是有些被复制的 GIF 这样是拿不到的,比如在 WebView 上面复制得到的 GIF,会被存在剪贴板的 Apple Web Archive pasteboard type 里面:

NSArray<NSDictionary *> *items = [[UIPasteboard generalPasteboard] items];

for (NSDictionary *item in items) {

    NSData *data = item[@"Apple Web Archive pasteboard type"];
    if (data) {
        NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data
                                                                          options:NSPropertyListImmutable
                                                                           format:NULL
                                                                            error:nil];
        if (archive) {
            NSArray<NSDictionary *> *resources = archive[@"WebSubresources"];
            for (NSDictionary *resource in resources) {
                NSData *resData = resource[@"WebResourceData"];
            }
        }
    }
}

通过枚举的方法可以把它给找出来,当然还有一些复制来源的 GIF 这样也找不到,这里就不一一展开了。

将 GIF 设置到剪贴板则非常简单,只要用正确的 UTI 就可以了:

[[UIPasteboard generalPasteboard] setData:data forPasteboardType:@"com.compuserve.gif"];

结语

以上就是我个人对于 GIF 的了解,以及开发过程中的一点点经验,感谢大家的观看,再见。

- EOF -


「真诚赞赏,手留余香」
4 人赞赏
龙小龙
微信用户
Ren Roger
李龑
21 条评论