Unity

Unity WebGLでJSZipを使ってzip圧縮してみる

Unity WegGLでJSZipを使ってzip圧縮してみたのでやったことをメモしておく。

やったこと

WebGLのカスタムテンプレートを設定してJSZipを配置

Using WebGL Templates - Unity マニュアル を主に参照

  • 以下を場所からdefaultテンプレートを取得
Both minimal and default templates can be found in the Unity installation folder under Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\WebGLTemplates on Windows or /PlaybackEngines/WebGLSupport/BuildTools/WebGLTemplates on Mac.
  • Assets/WebGLTemplates に取得したDefaultテンプレート配置
  • player settingsで配置したDefaultテンプレートを選択
  • Assets/WebGLTemplates/Default/TemplateData にJSZipのdownload JSZipから取得したjszip.min.jsを配置
  • Assets/WebGLTemplates/Default/index.html に以下を追加
<script src="TemplateData/jszip.min.js"></script>

JSZipを使うnative pluginを作成

以下の記事がかなり参考になった。
Unity(WebGL)でC#の関数からブラウザー側のJavaScript関数を呼び出すまたはその逆(JS⇒C#)に関する知見(プラグイン形式[.jslib]) - Qiita

Assets/Plugins/WebGL/ZipArchiver.jslib

var ZipArchiver = {
    $member: {
      zipData: null
    },

    $convertUrlToBlob: function(url, func) {
      var xhr = new XMLHttpRequest();
      xhr.responseType = "blob";
      xhr.open("GET", url, true);
      xhr.onload = function(oEvent) {
        func(xhr.status === 200 ? xhr.response : null);
      }
      xhr.send();
    },

    $getPtrFromString: function(str) {
      var size = lengthBytesUTF8(str) + 1;
      var buffer = _malloc(size);
      stringToUTF8(str, buffer, size);
      return buffer;
    },

    InitZip: function()
    {
      member.zipData = new JSZip();
    },

    AddZipData: function(filename, data) 
    {
      member.zipData.file(Pointer_stringify(filename), Pointer_stringify(data));
    },

    AddZipBase64Data: function(filename, data)
    {
      member.zipData.file(Pointer_stringify(filename), Pointer_stringify(data), {base64: true});
    },

    AddZipBlobURL: function(filename, data, callback)
    {
      var fname = Pointer_stringify(filename);
      var blob = convertUrlToBlob(Pointer_stringify(data), function(blob) {
        console.log(fname);
        member.zipData.file(fname, blob);
        Runtime.dynCall('v', callback, []);
      });
    },

    GenerateZipBlobURL: function(callback)
    {
      member.zipData.generateAsync({type: "blob"}).then(function(content) {
        var buffer = getPtrFromString(URL.createObjectURL(content));
        Runtime.dynCall('vi', callback, [buffer]);
      });
    }
};

autoAddDeps(ZipArchiver, '$member');
autoAddDeps(ZipArchiver, '$convertUrlToBlob');
autoAddDeps(ZipArchiver, '$getPtrFromString');

mergeInto(LibraryManager.library, ZipArchiver);
  • はまったところ
    • letが使えない
    • XMLHttpRequestを非同期で実行しないとblobが正しくとれずバイナリが壊れる(コードが間違っていた可能性もあり)
    • $memberの中にfunctionのメンバを入れられない

ZipArchiver.cs

using AOT;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.Events;

public class ZipArchiver {

    private delegate void GenerateBlobURLCallback(System.IntPtr ptr);
    private delegate void AddZipBlobURLCallback();
    private static UnityAction<string> generateBlobURLCallback;
    private static UnityAction addZipBlobURLCallback;

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void InitZip();

    [DllImport("__Internal")]
    private static extern void AddZipData(string filename, string data);

    [DllImport("__Internal")]
    private static extern void AddZipBlobURL(string filename, string bloburl, AddZipBlobURLCallback callback);

    [DllImport("__Internal")]
    private static extern void GenerateZipBlobURL(GenerateBlobURLCallback callback);

    [DllImport("__Internal")]
    private static extern void AddZipBase64Data(string filename, string data);
#endif

    public ZipArchiver()
    {
        Init();
    }

    public void Init()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        InitZip();
#endif
    }

    public void AddData(string filename, byte[] data)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        AddZipBase64Data(filename, Convert.ToBase64String(data));
#endif
    }

    public void AddData(string filename, string data)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        AddZipData(filename, data);
#endif
    }

    public void AddBlobURL(string filename, string bloburl, UnityAction callback)
    {
        addZipBlobURLCallback = callback;
#if UNITY_WEBGL && !UNITY_EDITOR
        AddZipBlobURL(filename, bloburl, Callback);
#endif
    }

    public void GenerateBlobURL(UnityAction<string> callback)
    {
        generateBlobURLCallback = callback;
#if UNITY_WEBGL && !UNITY_EDITOR
        GenerateZipBlobURL(Callback);
#endif
    }

    [MonoPInvokeCallback(typeof(GenerateBlobURLCallback))]
    private static void Callback(System.IntPtr ptr)
    {
        string value = Marshal.PtrToStringAuto(ptr);
        generateBlobURLCallback.Invoke(value);
    }

    [MonoPInvokeCallback(typeof(AddZipBlobURLCallback))]
    private static void Callback()
    {
        addZipBlobURLCallback.Invoke();
    }
}

試しに使ってみる

以下のスクリプトをカメラに設定してWebGLビルドしてブラウザで実行しコンソールを確認する。
コンソールに出力されたblob URLをブラウザにコピペするとzipがダウンロードできて中身を確認可能。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ZipArchiverTest : MonoBehaviour {

    private bool isCapture = true;

    void OnPostRender()
    {
        if (isCapture)
        {
            isCapture = false;
            ZipArchiver archiver = new ZipArchiver();
            archiver.AddData("test.txt", "test");

            var texture = new Texture2D(Screen.width, Screen.height);
            texture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
            texture.Apply();

            archiver.AddData("test.jpg", texture.EncodeToJPG());
            archiver.GenerateBlobURL((string url) =>
            {
                //Debug.Log("bloburl:" + url);
                ZipArchiver a = new ZipArchiver();
                a.AddData("hoge.txt", "hoge");
                a.AddBlobURL("test.zip", url, () =>
                {
                    a.GenerateBlobURL((string u) =>
                    {
                        Debug.Log("bloburl:" + u);
                    });
                });
            });
        }
    }
}