GAS の Blob とファイル変換まとめ

　外出自粛を要請されたので引きこもって勉強。Google Apps Script で Blob を操作したり、ファイルをダウンロードさせたりする方法をまとめました。

## 注意
　この記事の内容は V8 ランタイムで検証しています。
　またこの記事では Google Apps Script のオブジェクトである Blob について扱いますが、これは GAS の Base Service に定義されている GAS 独自のオブジェクトで、**JavaScript (ECMA) に定義されている Blob とは似て非なるものです**。こちらの Blob は GAS では定義すらされていません。たとえば以下は [MDN に掲載されている Blob コンストラクタの使用例](https://developer.mozilla.org/ja/docs/Web/API/Blob)ですが、これを GAS で実行するとエラーを吐きます。

```javascript:GAS
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});
// > ReferenceError: Blob is not defined
```

　JS Blob の参考

- [Blob | MDN](https://developer.mozilla.org/ja/docs/Web/API/Blob)
- [Blob | Javascript.info](https://ja.javascript.info/blob)
- [File API #Blob | W3C 仕様書](https://w3c.github.io/FileAPI/#blob) ／ [邦訳](https://triple-underscore.github.io/File_API-ja.html)

# Blob を扱う
　Blob 型 (blob: binary large object) は、**ファイル名と MIME 型の指定がついた万能なデータ型**です。MIME 型は拡張子みたいなもんなので、素朴な感覚では「ファイル名と拡張子が付いたファイル」です。テキストや画像など様々な内実のデータを十把一絡げにただのバイト列とみなして、Blob オブジェクトとして持つことができます。様々なファイル形式をやり取りするときにはいったん Blob オブジェクトを介すると便利です。

### Blob の作成、Blob への変換
　`Utilities` サービスに用意されているメソッド `newBlob(data[, contentType[, name]])` で Blob を作成できます。このメソッドは、String のような非ファイル的なオブジェクトを Blob 化するのに用いたり、ファイルをインプットして MIME 型やファイル名を付けなおすのに用います。ファイルの形式変換には対応していません。ファイルの形式変換には後述する `getAs(contentType)` を使います。

```javascript
// 非ファイルから Blob を新規作成することができる
var myStr  = "ねこです。ねこはいます。";                         // String
var myBlob = Utilities.newBlob(myStr, MimeType.PDF, "myText"); // String => PDF Blob
DriveApp.getFolderById(folderId).createFile(myBlob);           // PDF Blob => PDF File

// ファイルを Blob 化するついでに異なる MIME 型に変換することはできない
// 以下のコードではエラーが出て myBlob を生成できない
var myText = DriveApp.getFileById(fileId); // PLAIN_TEXT を読み込む
var myBlob = Utilities.newBlob(myText, MimeType.PDF, "filename");

// 以下のコードではエラーこそ出ないが JPEG に変換されず単なる PNG Blob として処理される
var myPng  = DriveApp.getFileById(fileId); // PNG を読み込む
var myBlob = Utilities.newBlob(myPng, MimeType.JPEG, "filename");
```

### MIME 型の指定
　スラッシュ記法で正式に書いてもいいですが、GAS に定義されている `MimeType` 列挙型を用いて簡易に指定することもできます。たとえば JPEG 画像なら `"image/jpeg"` と書いてもいいし `MimeType.JPEG` でもよいです。特に G Suite や Microsoft Office 系のファイルはこの列挙型で表現するほうが圧倒的に楽で、たとえば Google Spreadsheet ファイルの MIME 型は `"application/vnd.google-apps.spreadsheet"` と書くよりも `MimeType.GOOGLE_SPREADSHEET` と書けば、コード補完も利用できるし、視認性も良いでしょう。
　公式レファレンスの [MimeType 列挙型](https://developers.google.com/apps-script/reference/base/mime-type)のページは、GAS で扱えるファイル形式の一覧としても有用です[^gz]。見ればわかる通り、Microsoft Office の各種ファイルにも `MimeType` が定義されています。

[^gz]: `Utilities.gzip()` というメソッドを用意しているのに、ここに GZIP ファイル（.gz）が挙げられていないのは不思議です。

### getBlob() と getAs(contentType)
　`getBlob()` は何らかのデータを Blob として取得するメソッドで、`getAs(contentType)` はそれに MIME 型を**明示的に指定して**[^mime]変換する機能がついたものです。これらのメソッドは Blob だけでなく Document, Spreadsheet, Chart など多くのクラスに実装されていて、特に `getAs()` は非常に使い勝手がよいものです（`getBlob()` で取得できるクラスをまとめて `BlobSource` と呼びます）。

[^mime]: 後述するように、GOOGLE_DOCS, GOOGLE_SPREADSHEET などのオブジェクトは `getBlob()` しただけで自動的に PDF に変換されます。つまり `getBlob()` は対象に何の変更も加えずただ取得するメソッドではありません。`getAs(contentType)` は変換先を明示的に指定することができ、かつ変換元と変換先の組合せが可能であるときだけ正しく実行されます。

```javascript
// Google Docs, Google Spreadsheet, Webpage などを PDF に変換する
var myPdf = mySpreadSheet.getAs("application/pdf").setName("myPdf.pdf");
var myPdf = UrlFetchApp.fetch(url).getAs(MimeType.PDF); 
```

　画像ファイルはいったん Blob にすることで形式の相互変換が可能です。たとえば Drive 上にある PNG 画像を JPEG に変換するには  `getAs()` を使えば十分です。

```javascript
var pngImg = DriveApp.getFileById(fileId);           // PNG File
var jpgImg = pngImg.getAs(MimeType.JPEG);            // PNG File => JPEG Blob
DriveApp.getFolderById(folderId).createFile(jpgImg); // JPEG Blob => JPEG File

// Utilities.newBlob には画像の変換機能はないので、これだとうまくいかない
var jpgImg = Utilities.newBlob(pngImg.getBlob().getBytes(), MimeType.JPEG, "myImg");
```

　`getAs()` では決められた組合せの変換しかできません。画像４種（BMP, GIF, JPEG, PNG）は相互変換可能ですが、たとえば PNG => PLAIN_TEXT のようなナンセンスな変換を行おうとするとエラーが出ます。

```javascript
var myBlob = pngImg.getAs(MimeType.PLAIN_TEXT);
// Excption: image/png から text/plain への変換はサポートされていません。
```

　PDF への変換は、画像からは無理ですが、各種テキストデータ (PLAIN_TEXT, etc.) や GOOGLE_DOCS, GOOGLE_SPREADSHEET といったアプリ専用形式からの変換に対応しています。たとえば以下のようにすると、スプレッドシートを PDF に変換して保存できます。

```javascript
var ss   = SpreadsheetApp.openById(spreadsheetId);    // Spreadsheet
// OR:
var ss   = DriveApp.getFileById(spreadsheetId);       // File(Spreadsheet)

var pdfFile  = ss.getAs(MimeType.PDF);                // Spreadsheet/File => Blob(pdf)
DriveApp.getFolderById(folderId).createFile(pdfFile); // Blob(pdf) => File(pdf)
```
### G Suite Apps 専用のデータ型
　GOOGLE_DOCUMENT, GOOGLE_SPREADSHEET などの G Suite アプリ専用ファイルは基本的にそのアプリ内でしか扱えず、アプリ外に持ち出すことができません。データ自体 Drive 上には保存されず、ユーザーはそれらがどのような形式で保存されているのかすら知ることができません（多分）。アプリ外で扱うには、それ以外の汎用的なオブジェクトに変換してエクスポートすることになります。
　たとえば、こうした G Suite 系ファイルはそのまま Blob として持つことができず（MICROSOFT_EXCEL など Office 系ファイルは可能）、`getBlob()` した時点で自動的に PDF に変換されてしまいます。`getAs()` で MIME 型を指定しても無駄です。

```javascript
var myBlob = SpreadsheetApp.openById(ssid).getAs(MimeType.GOOGLE_SHEETS);
// Exception: application/pdf から application/vnd.google-apps.spreadsheet への変換はサポートされていません。
```

### その他の Blob 操作
　`BaseService` に [Blob クラス](https://developers.google.com/apps-script/reference/base/blob)があります。Blob の構成要素であるファイル名、MIME 型、バイナリ部分を操作するメソッドとしては以下のようなものがあります。

- `getName()` 
- `setName(name)`
 - Blob の名前を取得・設定する。設定されていなければ null を取得します。
- `getContentType()`
- `setContentType(contentType)`
- `setContentTypeFromExtention()`
 - Blob の MIME 型を取得したり設定したりする（設定するだけで変換機能はない）。設定されていなければ null を取得します。Extention とは拡張子のことで、ファイル名から勝手に MIME 型を判断してくれます。ただ GAS の側で認識できるファイル形式なら Blob にした時点で勝手に MIME 型が指定されるっぽいので、この設定メソッドを使うのは GAS で扱えないようなファイル形式に限られ（ると思い）ます。
- `getBytes()`
- `setBytes(data)`
 - Blob のバイナリデータ部分をそのままバイト列として取得・設定する。一括のみで、範囲を指定して扱うことはできません。
- `getDataAsString([charset])`
- `setDataFromString(string[, charset])`
 - Blob のバイナリデータ部分を文字列として取得したり、設定したりする。文字コードの指定が可能です。文字列データが Blob として存在する場合に使うメソッドで、たとえば zip 圧縮されている文書データを解凍して、それを文字列型に変換するときなどに使えます。

　それ以外のメソッドは~~あまり使いどころがありません~~。

- `copyBlob()`
 - Blob のコピーを作成して返す。GMail の添付ファイルを取ってくるときによく使われているのを見かけます。
- `isGoogleType()`
 - Blob が G Suite 系ファイルかどうか判定する。ただし先述のとおり G Suite 系ファイルは Blob として持てず、`getBlob()` した時点で PDF に変換されるので、このメソッドは常に false を返すはずで、使いどころがありません（`isGoogleType()` メソッドは `GmailAttachment` クラスにも実装されており、こちらは有用です）。

　GAS の Blob には上掲のメソッドしか用意されていません。`size` 属性もありませんし、バイト列の一部を切り出すようなメソッドもないので、組合せで実現します。

```javascript
var myBlobSize = myBlob.getBytes().length; // Blob のサイズを取得する
var myBlobPart = myBlob.getBytes().slice(firstByte, lastByte); // Blob の一部を取得する
```

　このほかに `Utilities` クラスにもいくつか Blob の操作に役立つメソッドが用意されています。

- `Utilities.base64Encode(Bytes[]/String)`
- `Utilities.base64Decode(encodedBase64String)`
 - Blob の中身はバイナリなので、このメソッドで [base64 エンコード](https://ja.wikipedia.org/wiki/Base64)すると `HtmlService` に渡せたりして便利です。処理に必要ならヘッダは別途付けます。どの部分がデータの本体なのかに注意してエンコード・デコードを行ないます。

```javascript
// blob -> base64 url
var contentType = myBlob.getContentType();
var base64Data = Utilities.base64Encode(myBlob.getBytes());
var myBase64Url = "data:" + contentType + ";base64," + base64Data;

// base64 url -> blob
var contentType = myBase64Url.split(":")[1].split(";")[0];
var base64Data = myBase64Url.split(",")[1];
var blob = Utilities.newBlob(Utilities.base64Decode(base64Data), contentType, filename);
```

- `Utilities.gzip(blob[, name])` 
- `Utilities.ungzip(blob)` 
 - Blob を gzip (.gz) 圧縮したり解凍したりして Blob として返す。MIME 型は `application/x-gzip` らしいです。File オブジェクトを引数に入れてもエラーは出ませんが、内部的には `getBlob()` が実行されて Blob に変換されているっぽいです[^gzip]。
- `Utilities.zip(blobs[, name])` 
- `Utilities.unzip(blobs)` 
 - Blob を（まとめて）zip 圧縮したり解凍したりする。Blob のリストをやりとりするので、ファイルひとつだけを圧縮したければ `[blob]` のように臨時的に配列にします。パスワードのかかった zip は扱えません。こちらも同様に File オブジェクトを直接渡せますが、やはり内部的には `getBlob()` を噛ませているっぽいです。

[^gzip]: たとえばスプレッドシート File を直接 gzip に渡すと、gzip 圧縮された PDF が出来ます。PDF に変換されない「生の」スプレッドシートが google のサーバ内でどのような形で保存されているのかは結局わかりません。

### 文字列の処理
　文字列については、JavaScript で処理できる部分は変わらないのですが、スクレイピングなどに便利な機能として `XmlService` クラスが、また独自機能とのインターフェースとして `ContentService` クラスが用意されています。前者の解説はここでは割愛します。

- [Class XmlService | Google Developers](https://developers.google.com/apps-script/reference/xml-service/xml-service)
- [Class ContentService | Google Developers](https://developers.google.com/apps-script/reference/content/content-service)

　`ContentService` は文字列データを扱うクラスで、文字列データにファイル名と MIME 型の情報がついた `TextOutput` という専用型を持っています。Blob のテキスト版と捉えてよいでしょう。以下のようなメソッドが用意されています。

- `ContentService.createTextOutput([content])`
 - 新たに `TextOutput` 型オブジェクトを生成する。内容はこの時点では空でもいいです。
 
- `append(content)`
- `setContent(content)`
- `clear()`
 - TextOutput の内容を追加・設定・全消去する。

- `getMimeType(contentType)` 
- `setMimeType(contentType)`
 - TextOutput の MIME 型を取得・設定する。設定されていなければ null を取得します。なお **上で紹介した `MimeType` 列挙型を利用することはできません** 。`ContentService.MimeType` という `ContentService` 独自の列挙型があり、これが利用できます。両者は非互換でメンバーも違います。参考：[Enum MimeType | Google Developers](https://developers.google.com/apps-script/reference/content/mime-type)

- `getFilename(name)` 
 - TextOutput の名前を取得する。設定されていなければ null を取得します。

- `downloadAsFile(name)`
 - TextOutput の名前を設定して、ブラウザにダウンロードの指示を行ないます。詳しい使い方は後述。


　また、CSV を配列にパースするメソッド `Utilities.parseCsv(csv[, delimiter])` が用意されています。配列から CSV への変換は、万全を期すならば既存の JS ライブラリを適当に引っ張ってくるのが楽でいいと思います[^arrayjoin]。

```javascript
var arr = Utilities.parseCsv(csv);  // csv String => Array
var csv = arr.join("\n");           // Array => csv String
```

[^arrayjoin]: 多次元配列に `join` をかけると、最上層だけ指定した区切り文字でつながれ、それより下の層はカンマ (,) でつながれます。これは `Array.prototype.join` の仕様上、結合前に配列の各要素に `toString` がかけられる（そして `toString` では区切り文字に (,) が用いられる）ためです。したがって CSV を作りたければ２次元配列を単に `join("\n")` するだけで事足ります。



### それ以上の処理
　ファイルのフォーマットを理解してライブラリを自作するか、あるいは既存のライブラリを利用することになります。たとえば以下のリンクでは、tar ファイルの仕様に踏み込んで GAS で tar ファイルを展開するスクリプトを実装しておられます。

- [Google Apps Scriptを使ってtarファイルを展開したい | Qiita](https://qiita.com/tanaike/items/9d36070f95bdcf12aa2a)

　また GAS ライブラリ自体の数は少ないですが、JavaScript のライブラリはたくさんあるので、JS ライブラリへのラッパーを作ることで随分省力化できそうです。たとえば、パス付 zip を扱える GAS スクリプトを公開している方がいらっしゃいますが、これはパス付 zip を扱える JS ライブラリ zlib.js のラッパーを作ることでその機能を実現しているようです。

- [tanaikech/UnzipGs | GitHub](https://github.com/tanaikech/UnzipGs)

# データの保存
　Google Drive に保存するか、ローカルに保存するか、など。

## DriveApp
　`DriveApp` を利用して Google Drive 上に保存するのは簡単です。データを File 型に変換し、Drive 上に保存します（File 型はざっくり言うと、Drive 上のパス情報やメタ情報などが付与された Blob ）。普通は `createFile(name, data[, mimeType])` の構文を利用すれば十分です。文字コードなど細かい指定をしたいときは Blob に変換して、いろいろ操作したうえで `createFile(blob)` を使います。

```javascript
//　Blob => File への変換 & ルートフォルダに保存
var file = DriveApp.createFile(blob);

// それ以外のデータ型 => File & ルートフォルダに保存
var file = DriveApp.createFile(name, data[, mimeType])
```

　データの保存については、`DriveApp.addFile(file)` でルートフォルダに、`Folder.addFile(file)` で任意のフォルダに保存できます。つまり`DriveApp.getFolderById(folderId).addFile(file)` とするとフォルダ ID を指定してさくっと保存できます。
　逆に Drive 上のファイルを取得して GAS で処理したい場合は、 File => Blob の変換を行う必要があります。`getBlob()` で簡単にできます。

```javascript
// ファイルID がわかる場合
var file = DriveApp.getFileById(fileId);
var fileBlob = file.getBlob();

// ファイル名しかわからない場合は getFilesByName メソッドを利用する
// FileIterator が返ってくるので、たとえば next() で File 本体を取得する
var file = DriveApp.getFolderById(folderId).getFilesByName('***').next();
var fileBlob = file.getBlob();
```

## ローカルダウンロード
　ファイルをローカルにダウンロードさせるメソッドは GAS にはありません（インターフェースがない）。しかし方法はないではなく、URL を指定、`ContentService` を利用、`HtmlService` でブラウザの機能を利用、などの方法があります。

### URL で指定してダウンロード
　G Suite アプリは URL にパラメタ指定をすることで様々な操作、たとえば印刷やダウンロードが可能です。こうした URL パラメタは公式ドキュメントが存在しませんが、非公式まとめはぼちぼち存在します。

- [All Google Docs URL parameters/functions/commands? | Stack Exchange](https://webapps.stackexchange.com/questions/130654/all-google-docs-url-parameters-functions-commands)
- [Google sheet embed URL documentation | Stack Overflow](https://stackoverflow.com/questions/23446449/google-sheet-embed-url-documentation)

```
https://docs.google.com/spreadsheets/d/__fileId__/export?format=xlsx  [.xlsx で DL ]
https://docs.google.com/spreadsheets/d/__fileId__/export?format=csv   [ .csv で DL ]
```

　特に複雑なのがスプレッドシートで、URL でクエリを使うことができます。普通のクエリを URL エンコードしてつけるだけっぽいです。そのアプリ上で行なえる操作であれば、だいたい URL の指定でいけます。かなり複雑な指定をして PDF 化することもできるようです。

- [Query Language Reference (Version 0.7) | Charts | Google Developers](https://developers.google.com/chart/interactive/docs/querylanguage)
- [Google Apps ScriptでPDFを作成する | office の杜](https://officeforest.org/wp/2018/11/25/google-apps-script%E3%81%A7pdf%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B/)

　しかしアプリの制約を超える操作を行ないたい場合、たとえば文字コードを変換したり zip 圧縮してダウンロードしたい場合は URL での指定では実現できません。

### ContentService の機能を利用
　文字列データを扱う `ContentService` クラスの `downloadAsFile(name)` メソッドを利用します。`TextOutput` 型を返り値に指定して、これを適当なところから呼びます。

```javascript
const doGet = () => {
  const str = /* ここにデータを入れる */;
  return output = ContentService.createTextOutput()
         .setMimeType(ContentService.MimeType.TEXT)
         .setContent(str)
         .downloadAsFile("output.txt");
}
```

　スクリプトをエディタ上で実行するだけではダメで、`doGet/doPost` に入れてウェブアプリとして発行した URL にアクセスしたり、スプレッドシートから関数を実行したりすることで、テキストファイルがダウンロードできます。公式に「ブラウザの設定によりうまく動かない可能性がある」旨書いてあるので、HTML5 の `download` 属性のような仕組みを使っているのかもしれません。JSON を返す API 作成などの用途が考えられます。

参考：[【GAS】WebアプリやAPIも作れちゃう！HtmlService、ContentServiceについて、できることをまとめてみた。　～ その④ JSON API ～](https://webird-programming.tech/archives/518)

　なお、ファイル名を指定しないとダウンロード時にエラーが出ます。MIME 型の指定はしなくてもダウンロードできるし、指定された MIME 型から自動で拡張子を補うこともありません。


### HtmlService を介してダウンロード
　HTML に渡すことで、処理に使える道具が増えます。Web アプリなら素直にブラウザに `HtmlOutput` を返しましょう。Spreadsheet や Document 紐付きのスクリプトなら HTML を表示できるダイアログ（`showModalDialog()` あるいは `showSidebar()`）があるので、これを利用します。

参考：[Serve HTML as a Google Docs, Sheets, Slides, or Forms user interface](https://developers.google.com/apps-script/guides/html#serve_html_as_a_google_docs_sheets_slides_or_forms_user_interface)

　文字列データは `HtmlService` にそのまま渡せるので、幅広い選択肢があります。HTML を介することで JavaScript (ECMA) に実装された様々な機能が使えるようになるからです。以下のリンクでは、GAS で文字列を HTML 内の適当なところに渡し、それを内容とするオブジェクト URL を作成し、さらにそれを自動でクリックしてダウンロードさせる手法が紹介されています。

参考：
[Google App ScriptでDriveAppを使わずに、自動でファイルダウンロードする機能を作った | Qiita](https://qiita.com/SatohJohn/items/4ee1a47502ce3aec4a5e)
[GoogleAppsScript JavaScriptを用いてCSVをローカルに書き出す実装 | Qiita](https://qiita.com/ayies128/items/315f161bc14c36327fcd)
[URL.createObjectURL() | MDN](https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL)

　また `HtmlService` を介するということは、様々な JS ライブラリを利用できるということでもあります。以下のリンクでは文字列を取得して JSON に変換するところまで GAS で行い、以降のデータ整形は CDN で呼び出した PapaParse に任せています。文字列処理を自分で組む必要はありません。
 
参考：[Google SpreadSheet のシートを Shift JIS の CSV 形式でダウンロードする GAS | Qiita](https://qiita.com/macoshita/items/e84ffc25614d4a07d8a4)

　なお、GAS 上で手軽に CDN を実現する方法として、フェッチして `eval` 関数を使う方法が考案されており、何かに使えるかもしれません。

```javascript
eval(UrlFetchApp.fetch(cdnUrl).getContentText());
```

参考：[importing external javascript to google apps script | Stack Overflow](https://stackoverflow.com/questions/18646554/importing-external-javascript-to-google-apps-script) 。

### バイナリファイル一般
　zip や pdf、画像などの非テキストファイルは単純に変数に入れるだけでは `HtmlService` に渡せません。base64 エンコードして渡す手法が一般的です。画像ならたとえば `data:image/png;base64,` とヘッダを明示的に付け加えて、ブラウザにデコードしてもらうことができます。

```javascript
// Spreadsheet のグラフ（chart）を画像にして表示する例
// Blob にはファイル名や MIME 型などの情報がついているので、バイナリ部分だけエンコードする
var imageData = Utilities.base64Encode(chart.getAs("image/png").getBytes());
var imageUrl = "data:image/png;base64," + encodeURI(imageData);
HtmlService.createHtmlOutput().append("<img src=\"" + imageUrl + "\">");
```
参考：

- [Charts Service | Google Developers](https://developers.google.com/apps-script/reference/charts)
- [GAS で zip を返す API を作りたかった | Qiita](https://qiita.com/proudust/items/5f19501ccb12b7dd0cba)
- [ブラウザでBase64で受け取ったファイルをダウンロードする](https://www.hos.co.jp/blog/20170213/)

　いったん Drive に保存してから `getDownloadUrl()` で URL を取得して自動ダウンロードさせ、その後 `DriveApp.removeFile(file)` で Drive から削除する、という回りくどい方法も考えられます。ただ Drive 上のファイルの削除はちょっとややこしい（削除ではなくファイルの所属の解除しかできないので）し、権限まわりもどう実装するか困りどころです。GAS で扱えるファイルサイズの都合（＝50MB まで）もあります。
　メールに添付して送ってしまう方法もあります。送信オプション attachment の引数は BlobSource です。

```javascript
GmailApp.sendEmail(myAddress, myTitle, myBody, {attachments: [myImg]});
```

# まとめ
- GAS に用意されているメソッドは必要最低限だが、ちょっとした組合せで結構いろんなことができる。
- 既存の JS ライブラリを利用する手段がいくつかあるので利用すべし。