談談 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 // 創建源nCGImageSourceCopyProperties // 獲得元數據nCGImageSourceGetCount // 獲得幀數nCGImageSourceCreateImageAtIndex // 獲得某幀 ...n
通過這些方法對 data 進行 decode 之後可以得到 image 的列表和 duration 的列表,這是決定 GIF 播放最重要的兩個因素。
了解內在的原理是非常重要的,但是現實中你不必再造一個輪子。結合我對各個 GIF 庫的使用,我推薦兩個比較好的,一個是 Flipboard 團隊開源的 FLAnimatedImage,一個是 ibireme 大神開源的 YYImage。尤其是 YYImage,他有一個寫的很棒的 Decoder,我們在後面的討論中會用到它。
合成 GIF
所謂合成,其實是將分解的過程反過來,用的框架也是 ImageIO Framework。通常會有兩種需求,一種是將一個視頻文件轉換到 GIF 格式,另一種是將一個編輯過的 GIF 文件重新保存起來。這兩種其實只有一個區別,就是視頻保存的時候,需要用 AVAssetImageGenerator 將視頻幀取出,得到 image 和 duration,這樣問題就歸納成了另一種情況了。GIF 的合成利用的主要是下面的幾個函數:
CGImageDestinationCreateWithURL // 創建文件nCGImageDestinationAddImage // 添加幀nCGImageDestinationSetProperties // 設置元數據nCGImageDestinationFinalize // 保存n
關於這一點可以參考一個叫做 NSGIF 的開源項目,與上述兩個項目不同的是,這個項目的代碼寫的很一般,鑒於代碼也不多,我建議自己重寫。
控制播放速度
你可能在 app 裡面有控制 GIF 播放速度的要求,這一點可以通過修改 duration 列表來實現,這一點用 YYImage 來實現十分簡單。我自己的做法是繼承了 YYImage,提供了一個 durationScale 的方法。然後通過重寫代理方法來實現這個需求:
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {n NSTimeInterval duration = [super animatedImageDurationAtIndex:index] * self.durationScale;n return duration;n}n
當你更改了播放速度的時候,durationScale 發生變化,隨之播放速度也發生了變化。
GIF 裁剪
對於圖片的裁剪,這裡必須要提到一個在這方面做得最好的一個庫:TOCropViewController,這個庫在體驗方面非常接近原生相冊的裁剪體驗,值得推薦。不過要讓這個庫支持 GIF 的話,要做兩個改動:
- 將 TOCropView.m 裡面的 backgroundImageView 和 foregroundImageView 都改成你用的支持 GIF 播放的 ImageView 實現。
- 實現裁剪得到矩形的代理方法,逐幀對 GIF 進行裁剪,然後使用上述的方法進行合成
實時濾鏡
對 GIF 進行實施的濾鏡效果,可以通過 CoreImage Framework 或者 GPUImage 來實現,這裡介紹 GPUImage 的方案。首先要知道到,GPUImage 如何處理一張圖片的濾鏡效果:
UIImage *input = ...;nGPUImageFilter *filter = [[GPUImageFilter alloc] init];n[filter forceProcessingAtSize:input.size];nGPUImagePicture *picture = [[GPUImagePicture alloc] initWithImage:input];n[picture addTarget:filter];n[picture processImage];n[filter useNextFrameForImageCapture];nUIImage *output = filter.imageFromCurrentFramebuffer;n
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).n// If PHImageRequestOptionsVersionCurrent is requested and the asset has adjustments then the largest rendered image data is returnedn// In all other cases then the original image data is returnedn// resultHandler for asynchronous requests, always called on main threadn- (PHImageRequestID)requestImageDataForAsset:(PHAsset *)asset options:(nullable PHImageRequestOptions *)options resultHandler:(void(^)(NSData *__nullable imageData, NSString *__nullable dataUTI, UIImageOrientation orientation, NSDictionary *__nullable info))resultHandler;n
拿到的 data 就是 GIF 文件,而如果使用讀取 image 的話,將會得到一幀。
GIF 的保存對於 iOS 8 和 iOS 9 要使用兩種不同的方法,iOS 9 的比較簡單,使用 PHAssetCreationRequest 的 addResourceWithType 即可:
PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAsset];n[(PHAssetCreationRequest *)request addResourceWithType:PHAssetResourceTypePhoton data:datan options:nil];n
而 iOS 8 就比較麻煩了,需要構造一個文件然後再使用 PHAssetChangeRequest 的 creationRequestForAssetFromImageAtFileURL:
NSString *temporaryFileName = [NSProcessInfo processInfo].globallyUniqueString;nNSString *temporaryFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:temporaryFileName];nNSURL *temporaryFileURL = [NSURL fileURLWithPath:temporaryFilePath];nNSError *error = nil;n[data writeToURL:temporaryFileURL options:NSDataWritingAtomic error:&error];nif (error == nil) {n request = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:temporaryFileURL];n [[NSFileManager defaultManager] removeItemAtURL:temporaryFileURL error:nil];n} else {n ...n}n
不管怎麼樣都不能使用 UIImageWriteToSavedPhotosAlbum 這個方法,這個方法存到相冊就只有一幀。
GIF 與剪貼板
這是本文最後一個話題,如何在剪貼板裡面獲取 GIF 文件,以及把 GIF 放進剪貼板。
從剪貼板裡面獲取 GIF,要分幾個步驟走。
首先,通過 com.compuserve.gif 直接拿:
NSData *data = [[UIPasteboard generalPasteboard] dataForPasteboardType:@"com.compuserve.gif"];n
但是有些被複制的 GIF 這樣是拿不到的,比如在 WebView 上面複製得到的 GIF,會被存在剪貼板的 Apple Web Archive pasteboard type 裡面:
NSArray<NSDictionary *> *items = [[UIPasteboard generalPasteboard] items];nnfor (NSDictionary *item in items) {nn NSData *data = item[@"Apple Web Archive pasteboard type"];n if (data) {n NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:datan options:NSPropertyListImmutablen format:NULLn error:nil];n if (archive) {n NSArray<NSDictionary *> *resources = archive[@"WebSubresources"];n for (NSDictionary *resource in resources) {n NSData *resData = resource[@"WebResourceData"];n }n }n }n}n
通過枚舉的方法可以把它給找出來,當然還有一些複製來源的 GIF 這樣也找不到,這裡就不一一展開了。
將 GIF 設置到剪貼板則非常簡單,只要用正確的 UTI 就可以了:
[[UIPasteboard generalPasteboard] setData:data forPasteboardType:@"com.compuserve.gif"];n
結語
以上就是我個人對於 GIF 的了解,以及開發過程中的一點點經驗,感謝大家的觀看,再見。
- EOF -
推薦閱讀:
※iOS 開發中,設計師和工程師怎樣有效地進行溝通協作,流程如何?
※仙劍奇俠傳 5 將登錄 iPad 平台,能取得怎樣的成績?
※一個 iOS Universal 的 App 代碼結構要怎麼樣寫才算是一個好的代碼?