GoogleSlideでGASを活用した話

この記事はand factory.inc Advent Calendar 2024 4日目の記事です。

https://qiita.com/advent-calendar/2024/andfactory

# 導入
プロジェクトに長く関わっているとGoogleSlideで作成された説明資料などを目にすることもよくあると思います
スライド枚数が多いと全てに目を通すのが大変になったり、見る必要はないが情報として残っているスキップスライドがあったりして全貌を把握するのが難しくなってきたりします
今回、各スライドの情報をSpreadsheetに落とし込んで全貌が把握できるようにしていきたいと思います

# やったこと
- スライドの各情報をスプレッドシートに出力する
- 画像については一旦Driveにあげて、上げたものをSpreadSheetの`IMAGE`関数で読み込む
- グラフは画像して出力して、上記画像と同様に扱う
- テーブルについても中身を出力する
- スキップスライドであるかどうかのフラグも出力する

# script
<details><summary>上記をChatGPTにお願いして出力＆調整されたスクリプトがこちら</summary>

```javascript
function myFunction() {
  slidesId = "読み込み対象のスライドID"
  sheetId = "出力先スプレッドシートID"
  folderId = "画像の保存用フォルダID"
  exportSlidesData(slidesId, sheetId, folderId)
}

function exportSlidesData(slidesId, sheetId, folderId) {
  // 1. スライドとシートのオブジェクトを取得
  const presentation = SlidesApp.openById(slidesId);
  const slides = presentation.getSlides();
  const presentationName = presentation.getName(); // スライドのファイル名を取得
  const spreadsheet = SpreadsheetApp.openById(sheetId);

  // 2. フォルダオブジェクトを取得
  const folder = DriveApp.getFolderById(folderId);

  // 3. シートが存在する場合は取得、存在しない場合は新規作成
  let sheet = spreadsheet.getSheetByName(presentationName);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(presentationName); // 新しいシートを作成
  } else {
    sheet.clear(); // 既存のシートがある場合、内容をクリア
  }

  // 4. シートのヘッダー行を設定
  sheet.appendRow(['スライドURL', 'スキップ設定', '要素1', '要素2', '要素3', '...']);

  // 5. 各スライドの情報を取得してシートに出力
  slides.forEach((slide, slideIndex) => {
    const slideData = [];

    // スライドのURLを取得
    const slideUrl = `https://docs.google.com/presentation/d/${slidesId}/edit#slide=id.${slide.getObjectId()}`;
    slideData.push(slideUrl);
    Logger.log(`スライド ${slideIndex + 1}: URL を追加 - ${slideUrl}`);

    // スキップ設定を取得
    const skipStatus = slide.isSkipped();
    slideData.push(skipStatus);
    Logger.log(`スライド ${slideIndex + 1}: スキップ設定 を追加 - ${skipStatus}`);

    // スライド内の各要素を収集
    slide.getPageElements().forEach((element, elementIndex) => {
      Logger.log(`type ${element.getPageElementType()}`)
      if (element.getPageElementType() === SlidesApp.PageElementType.SHAPE) {
        // テキストエリアの場合
        const text = element.asShape().getText().asString();
        slideData.push(text);
        Logger.log(`スライド ${slideIndex + 1}, 要素 ${elementIndex + 1}: テキストエリア を追加 - ${text}`);
      } else if (element.getPageElementType() === SlidesApp.PageElementType.IMAGE) {
        // 画像の場合：指定フォルダにアップロードし、URLを取得
        const imageBlob = element.asImage().getBlob();
        const imageFile = folder.createFile(imageBlob);
        const imageId = imageFile.getId();
        const imageUrl = `https://drive.google.com/uc?id=${imageId}`;
        slideData.push(`=IMAGE("${imageUrl}")`);
        Logger.log(`スライド ${slideIndex + 1}, 要素 ${elementIndex + 1}: 画像 を追加 - ${imageUrl}`);
      } else if (element.getPageElementType() === SlidesApp.PageElementType.TABLE) {
        // テーブルの場合：行を改行で区切って結合
        const table = element.asTable();
        const tableData = [];
        for (let r = 0; r < table.getNumRows(); r++) {
          const row = [];
          for (let c = 0; c < table.getRow(r).getNumCells(); c++) {
            row.push(table.getRow(r).getCell(c).getText().asString().trim());
          }
          tableData.push(row.join(' | ')); // 同じ行のデータをカラム区切りで結合
        }
        const tableText = `[テーブル]\n${tableData.join('\n')}`; // 行を改行で区切り
        slideData.push(tableText);
        Logger.log(`スライド ${slideIndex + 1}, 要素 ${elementIndex + 1}: テーブル を追加 - ${tableText}`);
      } else if (element.getPageElementType() === SlidesApp.PageElementType.SHEETS_CHART) {
        // グラフの場合：画像として保存
        const chartBlob = element.asSheetsChart().asImage(); // 画像として取得
        const chartFile = folder.createFile(chartBlob);
        const chartId = chartFile.getId();
        const chartUrl = `https://drive.google.com/uc?id=${chartId}`;
        slideData.push(`=IMAGE("${chartUrl}")`);
        Logger.log(`スライド ${slideIndex + 1}, 要素 ${elementIndex + 1}: グラフ を追加 - ${chartUrl}`);
      }
    });

    // スライドのデータを1行に書き込む
    sheet.appendRow(slideData);
    Logger.log(`スライド ${slideIndex + 1}: データをスプレッドシートに書き込み完了`);
  });

  // 完了メッセージ
  Logger.log("データの出力が完了しました。各属性がログに記録されています。");
}
```
</details>

主要箇所としては以下のあたり

```javascript
  const presentation = SlidesApp.openById(slidesId);
  const slides = presentation.getSlides();
```
- 各スライドは `SlidesApp.openById(slidesId)` でリストとして取得

```javascript
    slide.getPageElements().forEach((element, elementIndex) => {
```
- スライド内の要素は `slide.getPageElements()` でリストとして所得

```javascript
      if (element.getPageElementType() === SlidesApp.PageElementType.SHAPE) {
        // テキストエリアの場合
        const text = element.asShape().getText().asString();
        slideData.push(text);
      } 
```
- 要素の属性を `element.getPageElementType()` で取得して属性に応じて処理を行う

```javascript
       // 画像の場合：指定フォルダにアップロードし、URLを取得
        const imageBlob = element.asImage().getBlob();
        const imageFile = folder.createFile(imageBlob);
        const imageId = imageFile.getId();
        const imageUrl = `https://drive.google.com/uc?id=${imageId}`;
        slideData.push(`=IMAGE("${imageUrl}")`);
```
- 画像は `element.asImage().getBlog()` で取得
- `folder.createFile(imageBlob)` でdriveに保存
- 保存された画像のIDは `imageFile.getId()` で取得


```javascript
        const chartBlob = element.asSheetsChart().getBlob();
        const chartFile = folder.createFile(chartBlob);
```
- グラフも `element.asSheetsChart().getBlob()` で取得して画像として保存できる

# 実行結果
## 元スライド

1 | 2 | 3
---|---|---
![スクリーンショット 2024-11-29 14.14.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/959271b1-f914-1589-ef05-253a90bc4abe.png)|![スクリーンショット 2024-11-29 14.14.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/8fe53f1f-f34f-a555-7851-3b150148c5be.png)|![スクリーンショット 2024-11-29 14.14.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/60a86ef1-3870-4e95-7269-2c0b52a994de.png)

4 | 5 | 6(スキップ設定）
---|---|---
![スクリーンショット 2024-11-29 14.14.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/e30aff4f-3173-8251-eab9-67c204b5bd55.png)|![スクリーンショット 2024-11-29 14.14.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/9cc39276-062c-3a37-b1ce-217123af8a7c.png)|![スクリーンショット 2024-11-29 14.14.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/da3d15b7-e406-0c8b-a7d9-040b631c76c2.png)

## 出力結果
spreadsheet | drive
---|---
![スクリーンショット 2024-11-29 14.28.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/c39ed4a4-9524-6086-4aa3-a3b5cbe8dda6.png)|![スクリーンショット 2024-11-29 14.19.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324924/3117cf05-8943-4e40-51b3-011ce9889921.png)

## メモ
- データの読み込みはスライドに配置されたタイミングの古いものから行われる
  - スライド3においてテキストは上から1, 3, 2と並んでいますが、読み込みは配置の古いものから行われています
      - テキストボックスの中身を更新した場合も取得順は変わりませんでした
  - 横方向についても画像を真ん中、左、右の順で配置しましたが、出力は配置の古い順
- 本手法でスプレッドシートから画像にアクセスする際、ディレクトリは「リンクを知っている全員」で共有されている必要あるため、内容によっては配慮が必要
- 非表示スライドを削除したい場合は `slide.remove()` で可能
- 今回試してはないですが、PowerPointで作成されたスライドをGoogleSlideに読み込んで、そこからSpreadsheetに落としこむこともできそう
 
# まとめ
- これで膨大なスライドに何が入っているのかを個別にシートを開く必要がなく、またスキップスライドを弾くことができるようになりました
- Slideではスライドを1枚ずつ情報を確認する必要がありましたが、スプレッドシートに落とし込むこと俯瞰しやすく、かつその他ツールへの転記も容易になりました


# 参考情報
- slideのリファレンス
    - https://developers.google.com/apps-script/reference/slides?hl=ja
- driveのリファレンス
    - https://developers.google.com/apps-script/reference/drive?hl=ja
- spreadsheetのリファレンス
    - https://developers.google.com/apps-script/reference/spreadsheet?hl=ja