iOSでAnimation GIFを実装してみる(Swift)

iOSでAnimation GIFを作る機会があったのでメモを残す。

[前回はAnimation PNGを作った](https://qiita.com/mbotsu/items/99f21a0b244fbc048547
)のだが無駄にファイルサイズがでかいので色数的にはGIFで足りたのでGIFにした。
ファイルサイズは10分の1程度になった。


## SwiftでOpenCVの導入

今回はSwiftでOpenCVも導入して遊んでみた。

以前ImageProcが使える程度のを作ったのがあったのでこちらからダウンロードした。
https://github.com/mbotsu/KeypointDecoder/releases/download/0.0.1/opencv2.xcframework.zip

opencv2.xcframework.zipの作り方はこちら
https://github.com/mbotsu/KeypointDecoder/blob/main/minimum_opencv.md

### OpenCVをSwiftで利用する設定

- `libc++.tbd`を追加
- `opencv2.xcframework`をDo Not Embedで追加

![スクリーンショット 2023-02-11 15.54.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/3482f9ee-3205-d6b5-ef9f-95ffd491c1a9.png)

Other Linker Flagsに`-all_load`を設定

![スクリーンショット 2023-02-11 15.55.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/d17bf68a-f511-bba8-7809-fa5f001cd7e4.png)


## カラーテーブルを手動で登録する理由

多くの画像フレームをAnimation GIFの終了処理`CGImageDestinationFinalize`にかけると大量のメモリを消費してハングする。
どうやら色の自動抽出処理が原因みたいなので、カラーテーブルを先に抽出するかして手動で作る必要がある。
それをするには`kCGImagePropertyGIFHasGlobalColorMap`をfalseにして、`kCGImagePropertyGIFImageColorMap`にbinary Arrayを放り込めば良い。

動作確認にはコードの`colorTable`を削ると色が欠けることから動作していることは確認できた。
それでも処理には大量のメモリを消費するのでフレームが増える度にメモリが急激に使われるんだけどね。
許容量の調査はできるのでいきなりハングしないようになるだけマシかな。

```swift: ContentView.swift
import SwiftUI
import opencv2
import UniformTypeIdentifiers

struct ContentView: View {
  var body: some View {
    VStack {
      Image(systemName: "globe")
        .imageScale(.large)
        .foregroundColor(.accentColor)
      Text("Hello, world!")
    }
    .padding()
    .onAppear {
      test()
    }
  }
  
  func test(){
    
    var images = [UIImage]()
    for p:Int32 in 0..<100 {
      let img = Mat(size: Size(width: 100, height: 100),
                    type: CvType.CV_8UC3, scalar: Scalar(255,255,255))
      Imgproc.circle(img: img, center: Point2i(x: p, y: 50), radius: 5, color: Scalar(255,0,0), thickness: -1)
      Imgproc.circle(img: img, center: Point2i(x: 50, y: p), radius: 5, color: Scalar(0,0,255), thickness: -1)
      let uiImage = img.toUIImage()
      images.append(uiImage)
    }
    
    let fileUrl: URL = FileManager.default.temporaryDirectory.appendingPathComponent("test.gif")
    
    let colorTable = Data([
      255,  0,  0,
        0,  0,255,
      255,255,255,
    ]);
    
    do {
      try Animation(images, toUrl: fileUrl, colorTable: colorTable, fps: 10)
    } catch {
      print(error.localizedDescription)
    }
    
    print(fileUrl.path)
  }
}

enum AnimationError: Error {
  case create
  case finalize
}

func Animation(_ images: [UIImage], toUrl: URL, colorTable: Data, fps: Float = 30, loopCount: Int = 0) throws {
  let frameCount = images.count
  
  let fileDict = NSMutableDictionary()
  fileDict.setObject(false, forKey: kCGImagePropertyGIFHasGlobalColorMap as NSString)
  fileDict.setObject(loopCount, forKey: kCGImagePropertyGIFLoopCount as NSString)
  let fileProperties = NSDictionary(object: fileDict, forKey: kCGImagePropertyGIFDictionary as NSString)
  
  let frameDict = NSMutableDictionary()
  frameDict.setObject( 1.0 / fps, forKey: kCGImagePropertyGIFDelayTime as NSString)
  frameDict.setObject(colorTable, forKey: kCGImagePropertyGIFImageColorMap as NSString)
  let frameProperties = NSDictionary(object: frameDict, forKey: kCGImagePropertyGIFDictionary as NSString)
  
  guard let destination = CGImageDestinationCreateWithURL(toUrl as CFURL, UTType.gif.identifier as CFString , frameCount, nil) else {
    throw AnimationError.create
  }
  
  CGImageDestinationSetProperties(destination, fileProperties.copy() as? NSDictionary)
  
  for image in images {
    autoreleasepool {
      CGImageDestinationAddImage(destination, image.cgImage!, frameProperties)
    }
  }
  
  if !CGImageDestinationFinalize(destination) {
    throw AnimationError.finalize
  }
}
```

## できたもの

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/ce428db3-7188-21d5-4d7a-29c1e24fa629.gif)

## カラーテーブルから青だけ削る

```
let colorTable = Data([
  255,  0,  0,
  255,255,255,
]);
```

![test2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/eb759de3-5b33-10e5-a19b-0d97185b8d48.gif)