[GAS] スプレッドシートに限定公開のDrive画像を埋め込む

「[Applibot Advent Calendar 2024](https://qiita.com/advent-calendar/2024/applibot)」 15日目の記事になります。
前日は [@holy-knight](https://qiita.com/holy-knight) の [UnityでUSDを使ってみたい](https://qiita.com/holy-knight/items/b2cebd2f02ba97b502d7) という記事でした。

この記事は、明日から業務で使えるスプシ×GASの小ネタとなっております。

## はじめに

みなさんご存知の通り、Google スプレッドシートはとても便利な表計算ソフトです。
コラボレーション機能が豊富な事もあり、業務においても表計算の枠を超えて情報集約ツール（？）としても活用される事があるかと思います。そんな使い方をしていると、往々にして **画像を表示したくなる** こともあります。

そんな場合、通常は以下のように [IMAGE 関数](https://support.google.com/docs/answer/3093333?hl=ja) を利用することになります。

```
=IMAGE("https://example.com/images/sample.png")
```

ただし、[IMAGE 関数のドキュメント](https://support.google.com/docs/answer/3093333?hl=ja) には以下のような記述があります。

> 重要: 使用できる URL は、drive.google.com でホストされていないもののみです。SVG ファイル形式はサポートされていません。

これは困ったことになりました。

なぜなら業務でスプレットシートを扱う場合、たいてい Google Workspace で組織のアカウントとして運用が行われます。その場合 **組織内限定で扱っている Google Drive 内の画像を、組織内限定のスプレッドシートで表示したい** という要望が発生するためです。

一応 Drive でホストされている画像でも、全体公開すれば IMAGE 関数で表示することはできますが、それはセキュリティ上好ましくありません。

また別の方法として `[挿入] > [画像]` から手動でセルに画像を埋め込むことができます。
こうすればスプシ自体に画像が含まれるので、実質的に制限なく画像を表示することができます。

![スクリーンショット 2024-12-16 9.16.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191059/b35a56ea-4b27-697e-a2be-ae115c6ecf00.png)

ただし、埋め込む方式は大量の画像を扱いたい場合に厄介な点があります。
関数ではないので一括処理が行えず非常に面倒であることや、スプシ自体のサイズが画像のぶん肥大化していくことなどです。

![スクリーンショット 2024-12-16 9.24.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191059/520bd5f7-c4c6-90b6-e52e-43b3d495ff9d.png)
↑1MBの画像を埋め込んだ時の図（画像がスプレッドシートのサイズに含まれている）

## GAS で Drive 画像をスプシに埋め込む

そこで GAS を利用して問題を解決します。
GAS であれば、セルから URL を読み取って画像をダウンロードし、スプシに画像を埋め込むまでを自動で一括処理することができます。
組織内限定で扱っている Google Drive 内の画像は、組織のアカウントであれば GAS でアクセスが可能です。これをセルに埋め込んでしまえば IMAGE 関数を使わないので制約を回避できるというわけです。（当然、スプシが見えれば埋め込まれた画像も見えるので、それぞれの公開範囲には十分注意する必要があります。）

またスプシ自体のサイズを無駄に肥大化させないよう、生画像の代わりに Google Drive のサムネを使うことで、いい塩梅に圧縮された画像を用意することもできます。

### GAS で DriveAPI を使う

GAS では様々な Google サービスの API を使う事ができ、もちろん Drive API も利用することができます。

![スクリーンショット 2024-12-15 23.39.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191059/dea03d72-aad6-65fa-ff30-8efc038472e2.png)

GAS の編集画面左パネルの `サービス` から追加する事で利用することができます。
サービスの ID として `Drive` を設定した場合、次のようにたった２行で Drive の特定ファイルのサムネ画像のバイナリを取得できます。※fileId については後述

```javascript
const file = Drive.Files.get(fileId, {'fields': 'thumbnailLink'});
const blob = UrlFetchApp.fetch(file.thumbnailLink).getBlob();
```

取得した blob は Base64 エンコードすることでセルに埋め込むことが可能です。
関数にすると、こんな感じ。

```javascript
function buildCellImage(blob) {
  var c_type = blob.getContentType();
  var base64 = Utilities.base64Encode(blob.getBytes());
  var data = "data:" + c_type + ";base64, " + base64;
  return SpreadsheetApp.newCellImage().setSourceUrl(data).build();
}
```

### FileID について

Google Drive において FileID は、ドライブ上の各ファイルを一意に識別するための文字列です。
Google Drive の UI 上でアイテムを選択しコピーして取得できる URL にも含まれています。

![スクリーンショット 2024-12-16 9.47.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191059/056c6fb6-5cc3-7615-e74e-3c546370ca96.png)

```shell
// id の部分が FileID
https://drive.google.com/open?id=1GxhYqIwd_NN6kacSnPaIWVvryKZiLVep&usp=drive_copy
```
こんな感じで URL からクエリパラメータを取得する補助関数を作っておくと便利です。

```javascript
function getQueryParams(url) {
  var params = {};
  if (!url) return params;
  var tokens = url.split('?');
  if (tokens.length < 2) return params;
  var keyValuePairs = tokens[1].split('&');
  for (var i = 0; i < keyValuePairs.length; i++) {
    var keyValue = keyValuePairs[i].split('=');
    params[decodeURIComponent(keyValue[0])] = decodeURIComponent(keyValue[1]);
  }
  return params;
}
```

## 完成

というわけで、以下が完成形のソースコード全体です。
スプシ全体から Google Drive のリンクを探して、UI から実行できるようにしてあります。

```javascript
function onOpen() {
  let ui = SpreadsheetApp.getUi();
  ui.createMenu('GAS実行')
  .addItem('ドライブURLを画像に差し替え', 'replaceDriveImage')
  .addToUi();
}

function replaceDriveImage() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const range = sheet.getDataRange();
  const values = range.getRichTextValues();
  for (let i = 0; i < values.length; i++) {
    for (let j = 0; j < values[i].length; j++) {
      const url = values[i][j].getLinkUrl();
      // URL がドライブ画像なら、ダウンロードしてセルに設定
      if (url && url.indexOf('https://drive.google.com/open') !== -1) {
        const fileId = getQueryParams(url)['id'];
        const file = Drive.Files.get(fileId, {'fields': 'thumbnailLink'});
        const blob = UrlFetchApp.fetch(file.thumbnailLink).getBlob();
        const image = buildCellImage(blob);
        sheet.getRange(i + 1, j + 1).setValue(image);
      }
    }
  }
}

function getQueryParams(url) {
  var params = {};
  if (!url) return params;
  var tokens = url.split('?');
  if (tokens.length < 2) return params;
  var keyValuePairs = tokens[1].split('&');
  for (var i = 0; i < keyValuePairs.length; i++) {
    var keyValue = keyValuePairs[i].split('=');
    params[decodeURIComponent(keyValue[0])] = decodeURIComponent(keyValue[1]);
  }
  return params;
}

function buildCellImage(blob) {
  var c_type = blob.getContentType();
  var base64 = Utilities.base64Encode(blob.getBytes());
  var data = "data:" + c_type + ";base64, " + base64;
  return SpreadsheetApp.newCellImage().setSourceUrl(data).build();
}
```

これを `拡張機能 > Apps Script` に保存し、ヘッダメニューから GAS を実行します。
すると…

![movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191059/7a37d8a7-16ef-bf81-401f-82739b9d18dc.gif)

このようにリンクが次々 Drive のサムネ画像に置き換わっていきます！

以上で **組織内限定で扱っている Google Drive 内の画像を、組織内限定のスプレッドシートで表示したい** という要望に応えることができました🥳

## おわりに

スプレッドシートでやりたいことは、大抵 GAS を利用することで実現できます。
GAS を活用するとスプレッドシート運用は大幅に捗ります。皆様もぜひご活用ください。

以上、明日から業務で使える GAS の小ネタでした。

明日は [@yakisabananoka](https://qiita.com/yakisabananoka) さんです。