簡單實現 GIF 圖的倒序播放

前言

常常在網上看到一些有意思的 GIF 圖,有些 GIF 圖倒放以後,甚至變得更有意思,簡直是每日的快樂源泉;html

好比下面這個java

正放的時候很搞笑,很悲催;倒放的時候竟然很炫酷,簡直比段譽的凌波微步還牛逼,有沒有一種蓋世神功已練成的感受 😎😎😎😎😎,是否是能夠和綠巨人一戰 😀😀。python

再來一個git

小男生的快樂與悲傷竟然如此簡單,嚶嚶嬰 😊😊😊😊github

🤣🤣🤣🤣🤣🤣🤣🤣,真是能讓人笑上三天三夜。下面就來看看如何實現 GIF 圖的倒放。編程

如下全部實現細節源碼已同步至 GitHub 倉庫,可參考最後源碼緩存

GIF 是怎麼播放的,如何把GIF倒序?

想要倒放 GIF 圖,首先了解一下 GIF 的原理;這裏建議看看這篇來自騰訊手Q團隊的文章濃縮的纔是精華:淺析GIF格式圖片的存儲和壓縮。總的來講,GIF 和圖通圖片最大的不一樣點就是它是由許多幀組成的。既然如此咱們很容易想到,從 GIF 裏把全部幀拿出來,而後倒序組合這些幀,而後在合成一張 GIF 不就能夠了嗎?bash

是的,道理就是就麼簡單。若是你如今去 Google GIF 倒序的實現,會看到不少 Python 的實現版本,相似以下:網絡

import os
import sys
from PIL import Image, ImageSequence

path = sys.path[0]                          # 設置路徑 -- 系統當前路徑
dirs = os.listdir(path)                     # 獲取該路徑下的文件
for i in dirs:                              # 循環讀取全部文件
    if os.path.splitext(i)[1] == ".gif":    # 篩選gif文件
        print(i)                            # 輸出全部的gif文件名
        #將gif倒放保存
        with Image.open(i) as im:
            if im.is_animated:
                frames = [f.copy() for f in ImageSequence.Iterator(im)]
                frames.reverse()  # 內置列表倒序
                frames[0].save('./save/reverse_'+i+'.gif',save_all=True, append_images=frames[1:])# 保存
複製代碼

不得不說,Python 的各類三方庫的確很強大,幾行代碼就實現了 GIF 倒序的功能。可是做爲一個稍微有點追求的人,難道就到此爲止了嗎?下次若是有個好玩的 GIF 圖片,若是想看倒序圖,難道還要打開電腦用用上述腳本轉一次嗎?app

尤爲是做爲一個 Android 開發者,這種事情用手機不也能作嗎?爲了每日的快樂源泉,就算天崩地裂,海枯石爛也要作出來(其實好像也不是很難o(╯□╰)o)

好了,不吹牛逼了,下面來看看怎麼實現。

GIF 倒放的實現

上面已經說過了,要實現 GIF 的倒序須要作三件事

  • 從 GIF 圖裏把每一幀摘出來,組成序列
  • 把序列逆序
  • 用逆序後的每一幀再從新生成一張新的 GIF 圖

上面兩步,對集合逆序不是什麼難事,主要看看如何實現第一步和第三步。

從 GIF 圖裏把每一幀摳出來

這個聽起來很複雜,作起來好像也挺難,Android 沒有提供相似的 API 能夠作這件事,平時加載圖片用的三方庫 Glide,Fresco 等貌似也沒有提供能夠作相似事情的接口。但其實咱們稍微深刻看一下三方庫是實現 GIF 播放的代碼,就會找到突破口,這裏以 Glide 爲例,假設你研究過 Glide 的源碼(若是沒有看過,也沒關係,能夠略過這段,直接看實現

GifFrameLoader.loadNextFrame

在 GifFrameLoader 的 loadNextFrame 實現中(咱們能夠猜想到這就是 Glide 加載每一幀圖片的實現)

private void loadNextFrame() {
    if (!isRunning || isLoadPending) {
      return;
    }
    if (startFromFirstFrame) {
      Preconditions.checkArgument(
          pendingTarget == null, "Pending target must be null when starting from the first frame");
      gifDecoder.resetFrameIndex();
      startFromFirstFrame = false;
    }
    if (pendingTarget != null) {
      DelayTarget temp = pendingTarget;
      pendingTarget = null;
      onFrameReady(temp);
      return;
    }
    isLoadPending = true;
    // Get the delay before incrementing the pointer because the delay indicates the amount of time
    // we want to spend on the current frame.
    int delay = gifDecoder.getNextDelay();
    long targetTime = SystemClock.uptimeMillis() + delay;

    gifDecoder.advance();
    next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
    requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
  }
複製代碼

能夠看到具體的實現是由 gifDecoder 這個對象實現的。這裏最關鍵的一句就是

gifDecoder.advance();
複製代碼

咱們能夠看看這個方法的定義

/** * Move the animation frame counter forward. */
  void advance();
複製代碼

就是跳轉到下一幀的意思。

好了,至此咱們知道若是能夠獲取到 GifDeCoder 和 GifFrameLoader 的實例,那麼就能夠手動控制和獲取 GIF 圖裏每一幀了。可是,咱們回過去看 Glide 提供的 API 發現,咱們沒有辦法直接獲取 GifFrameLoader 和 GifDeCoder,由於在源碼裏這些變量都是 private 的。🤦‍🤦‍🤦‍ ,難道這就走到了死衚衕嗎?否則,前人曾說過,編程領域的任何問題均可以經過添加一箇中間層實現。咱們這裏的中間層就是 反射。使用反射能夠獲取就能夠訪問 GifFrameLoader 和 GifDeCoder 了;那麼後續的實現就變得簡單了。

獲取每一幀圖片並保存在集合中

Glide.with(mContext).asGif().listener(object :RequestListener<GifDrawable>{
            
            ...fail stuff...

            override fun onResourceReady(resource: GifDrawable, model: Any?, target: Target<GifDrawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                val frames = ArrayList<ResFrame>()
                val decoder = getGifDecoder(resource)
                if (decoder != null) {

                    for (i in 0..resource.frameCount) {
                        val bitmap = decoder.nextFrame
                        val path = IOTool.saveBitmap2Box(context, bitmap, "pic_$i")
                        val frame = ResFrame(decoder.getDelay(i), path)
                        frames.add(frame)
                        decoder.advance()
                    }
                }
                return false
            }

        }).load(originalUrl).into(original)
複製代碼

這裏的實現很簡單,監聽 GIF 的加載過程,加載成功後獲得一個 GifDrawable 的實例 resource ,經過這個實例用反射的方式(具體實現可參考源碼,很是簡單)獲取到了 GifDecode 的實例,有了這個實例就能夠獲取每一幀了,這裏還須要記錄一下每一幀播放時間間隔,返回的每個幀就是一個 Bitmap ,咱們把這些 Bitmap 保存在應用的安裝目錄下,而後用一個列表記錄下全部幀的信息,包含當前幀的延遲時間和當前幀對應的 Bitmap 的存儲路徑。

每一幀的集合序列有了,序列反轉一行代碼的事情,剩下的就是用這個序列生成新的 GIF 圖片了。

用幀序列再次生成圖片

用已有的圖片組成和一個新的圖片,這個並非什麼難事,網上已經有不少實現了。甚至包括 GIF 的再次生成,也能夠藉助 GifMaker 這樣的三方庫完成。

private fun genGifByFrames(context: Context, frames: List<ResFrame>): String {
        
        val os = ByteArrayOutputStream()
        val encoder = AnimatedGifEncoder()
        encoder.start(os)
        encoder.setRepeat(0)
        for (value in frames) {
            val bitmap = BitmapFactory.decodeFile(value.path)
            encoder.setDelay(value.delay)
            encoder.addFrame(bitmap)
        }
        encoder.finish()

        val path = IOTool.saveStreamToSDCard("test", os)
        IOTool.notifySystemGallay(context, path)
    
        return path
    }
複製代碼

藉助 AnimatedGifEncoder 很是簡單把以前保存的序列再次拼接成了一張新的 GIF 圖。

GIF 倒放

把上述三個步驟簡單整理一下

private fun reverseRes(context: Context, resource: GifDrawable?): String {
        if (resource == null) {
            return ""
        }
        // 獲取全部幀信息集合
        val frames = getResourceFrames(resource, context)

        // 逆序集合
        reverse(frames)

        // 生成新的 GIF 圖片
        return genGifByFrames(context, frames)
    }
複製代碼

須要注意的是,這三步操做都是涉及到 UI 的耗時操做,所以這裏簡單用 RxJava 作了一次封裝。而後就能夠愉快的使用了。

demo

GifFactory.getReverseRes(mContext,source)
                .subscribe { 
                    Glide.with(mContext).load(it).into(reversed)
                }
複製代碼

掘金 GIF 圖上傳限制爲 5MB,圖有點被壓縮了,感謝的能夠拉源碼嘗試一下效果

是的,就是這麼簡單,提供原始 GIF 資源的路徑,便可返回實現倒序的 GIF 圖。

總結

不得不說,Glide 的內部實現很是強大,對移動端圖片加載的各類場景作了很是複雜的考慮和設計,所以也致使它的源碼很是的難於閱讀。可是,若是僅僅從某個的出發,好比緩存、網絡、圖片解碼和編碼的角度出發,脫離整個流程,去看局部仍是有收穫的。

回到上述 GIF 倒序的步驟,總的來講有如下幾個關鍵步驟

  1. Glide 根據 URL 加載 GIF 圖片,同時監聽加載過程
  2. 經過 GifDrawable 反射獲取到 GifDecoder
  3. 經過 GifDecoder 獲取全部幀(包含保存這些幀 Bitmap)
  4. 反轉幀序列 frames
  5. 經過 frame 再次生成 GIF 圖片

上述步驟中 1 和 4 的執行速度是基本上是線性的,也是沒法再過多幹預的。而步驟 2,3,5 也是 GIF 反轉實現的核心,所以對方法耗時簡單作了下記錄。

GIF 圖 size = 8.9M

E/GifFactory: 方法: getGifDecoder        耗時 0.001000 second
E/GifFactory: 方法: getResourceFrames    耗時 1.489000 second
E/GifFactory: 方法: genGifByFrames       耗時 9.397000 second

GIF 圖 size = 11.9M

E/GifFactory: 方法: getGifDecoder        耗時 0.000000 second
E/GifFactory: 方法: getResourceFrames    耗時 1.074000 second
E/GifFactory: 方法: genGifByFrames       耗時 9.559000 second

GIF 圖 size = 3.3M

E/GifFactory: 方法: getGifDecoder        耗時 0.001000 second
E/GifFactory: 方法: getResourceFrames    耗時 0.437000 second
E/GifFactory: 方法: genGifByFrames       耗時 2.907000 second

GIF 圖 size = 8.1M

E/GifFactory: 方法: getGifDecoder        耗時 0.000000 second
E/GifFactory: 方法: getResourceFrames    耗時 0.854000 second
E/GifFactory: 方法: genGifByFrames       耗時 6.416000 second
複製代碼

能夠看到,雖然咱們獲取 GifDecoder 的過程使用了反射,但其實這比不是性能瓶頸;獲取全部幀信息的方法 getResourceFrames 耗時,也是和 GIF 圖的大小有關,基本上是一個可接受的值。可是經過幀序列再次生成 GIF 圖的方法執行時間就有點恐怖了,即使個人測試機是 kirin(麒麟)960 ,運行內存有 6G 😳😳。

可是一樣的圖片在 PC 上用 Python 腳本基本上是毫秒級完成。因此純粹用 java 實現(AnimatedGifEncoder 是 java 寫的,不算 kotlin 👀)圖片二次編碼仍是有些性能差距的。

雖然,這次的實現轉換較慢,但也算是一次不錯的嘗試吧。若是對最後一個步驟,有什麼更優雅的方式,能夠縮短 GIF 合成時間,能夠提 PR 到 GitHub ,全部的觀點討論都是很是歡迎的。

源碼

本文全部實現細節源碼已同步至 GitHub 倉庫 AndroidAnimationExercise, 本節入口能夠參考 ReverseGifActivity

參考文檔

濃縮的纔是精華:淺析GIF格式圖片的存儲和壓縮

相關文章
相關標籤/搜索