ABCABC Tech Catalog

Googleスライドをまとめて画像出力する方法

退屈なことはGoogle Apps Script(GAS)にやらせよう

ブログ執筆や資料作成の際、↓のような形でGoogle スライドを画像にして利用することがあります。

file1

1枚であれば、ファイル ⇒ ダウンロード から 画像形式を選択して現在選択中のスライドを画像形式でダウンロードすることができますよね。

file2

しかし、スライドの全ページ特定のページだけを画像化して利用したいとき、皆さんはどのように対応されていますか?

残念ながら、Googleスライドでは複数のスライドを一気に画像化する機能は現状提供されていません。

地道に1枚ずつダウンロードしたり、一旦PDFで出力して画像に変換したり、アドオンを使ったり、場合によってはスクリーンショットで済ましてしまうこともあると思いますが、もう少し簡単に必要なスライドをまとめて画像化してダウンロードできれば便利ですよね。

機能がなければ作ってしまえということで、選択中のスライドを画像化して一気にまとめてダウンロードできる機能をGoogle Apps Script(GAS)で開発しました。

誰でも簡単に利用いただけるように工夫したので、GASなんて使ったことがない・・・という方もぜひ挑戦してみてください。

事前の準備

たったの3ステップで準備が完了します。

  1. スクリプトをコピペする
  2. Slides APIを有効化する
  3. 認証を通す

Step1: スクリプトをコピペする

画像化したいGoogle スライドを開いたら、拡張機能 ⇒ Apps Script をクリック

file3

Apps Scriptを編集する画面が立ち上がるので、画像のように、最初から書かれている文字列(スクリプト)を消し、以下のスクリプトをコピーして貼り付けます。(GAS初心者の方はスクリプトの中身がわからなくても一切問題ありません!無心でコピペしてください)

file4

/**
 * GoogleスライドのUIにカスタムメニューを追加する関数
 */
function onOpen() {
  const UI = SlidesApp.getUi(); // UIインスタンスを取得
  // カスタムメニューを作成し、メニューアイテムを追加
  UI.createMenu('スライド画像出力')
    .addItem('画像で出力(選択中のスライド)', 'selectFormatAndExport')
    .addToUi(); // UIにメニューを追加
}

/**
 * 画像形式を選択するモーダルを表示する関数
 */
function selectFormatAndExport() {
  const htmlOutput = HtmlService.createHtmlOutput(
    `<!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <style>
          body { font-family: 'Google Sans', 'Roboto', sans-serif; margin: 0; padding: 10px; }
          .container { text-align: center; padding: 20px; }
          p { margin: 15px 0; }
          button { margin: 10px; padding: 10px 20px; font-size: 16px; border: none; border-radius: 4px; cursor: pointer; }
          button:hover { box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
          .hidden { display: none; }
        </style>
      </head>
      <body>
        <div class="container">
          <p id="message">出力する画像形式を選択してください。</p>
          <button id="pngButton" onclick="closeDialogAndExport('PNG')">PNG</button>
          <button id="jpegButton" onclick="closeDialogAndExport('JPEG')">JPEG</button>
          <p id="statusMessage" class="hidden">処理中...</p>
        </div>
        <script>
          function closeDialogAndExport(format) {
            document.getElementById('pngButton').disabled = true;
            document.getElementById('jpegButton').disabled = true;
            document.getElementById('message').classList.add('hidden');
            document.getElementById('statusMessage').classList.remove('hidden');
            google.script.run.withSuccessHandler(function() {
              google.script.host.close();
            }).withFailureHandler(function(error) {
              console.error('Error: ' + error);
              alert('エクスポートに失敗しました。');
              document.getElementById('pngButton').disabled = false;
              document.getElementById('jpegButton').disabled = false;
              document.getElementById('message').classList.remove('hidden');
              document.getElementById('statusMessage').classList.add('hidden');
            }).exportSelectedSlides(format);
          }
        </script>
      </body>
    </html>`
  )
  .setWidth(500)
  .setHeight(200);
  SlidesApp.getUi().showModalDialog(htmlOutput, '画像形式を選択');
}

/**
 * 選択されたスライドをエクスポートする関数
 * @param {string} format - エクスポートする画像形式
 */
function exportSelectedSlides(format) {
  const UI = SlidesApp.getUi(); // UIインスタンスを取得
  try {
    const PRESENTATION = SlidesApp.getActivePresentation(); // アクティブなプレゼンテーションを取得
    const PRESENTATION_NAME = PRESENTATION.getName(); // プレゼンテーションの名前を取得
    const SELECTION = PRESENTATION.getSelection();
    if (SELECTION.getSelectionType() === SlidesApp.SelectionType.PAGE) {
      const PAGES = SELECTION.getPageRange().getPages();
      if (PAGES.length === 0) {
        UI.alert('エラー: スライドが選択されていません。');
        return;
      }
      const BLOBS = []; // Blobを格納するための配列を初期化
      // 各スライドのBlobを作成し、配列に格納
      PAGES.forEach((page, index) => {
        const SLIDE = page.asSlide();
        BLOBS.push(createSlideBlob(SLIDE, PRESENTATION_NAME, index, format));
      });
      // スライドが1枚の場合はそのまま出力、2枚以上の場合はZIPで出力
      if (BLOBS.length === 1) {
        saveToDrive(BLOBS[0], PRESENTATION_NAME, PRESENTATION); // 画像をドライブに保存
      } else {
        const ZIP = Utilities.zip(BLOBS, `${PRESENTATION_NAME}.zip`); // 画像をZIPに圧縮
        saveToDrive(ZIP, PRESENTATION_NAME, PRESENTATION); // ZIPをドライブに保存
      }
    } else {
      UI.alert('エラー: スライドが選択されていません。');
    }
  } catch (e) {
    UI.alert('エラーが発生しました: ' + e.toString());
  }
}

/**
 * 指定されたフォーマットのBlobを作成する関数
 * @param {Slide} slide - スライド
 * @param {string} presentationName - プレゼンテーションの名前
 * @param {number} index - スライドのインデックス
 * @param {string} format - 画像形式
 * @return {Blob} 作成されたBlob
 */
function createSlideBlob(slide, presentationName, index, format) {
  try {
    // スライドのサムネイルURLを取得
    const THUMBNAIL_URL = getThumbnailUrl(SlidesApp.getActivePresentation().getId(), slide.getObjectId());
    let blob = UrlFetchApp.fetch(THUMBNAIL_URL).getBlob(); // サムネイルをBlobとして取得
    // 選択されたフォーマットがjpegの場合は画像形式を変換(デフォルトはPNG)
    if (format === 'JPEG') {
      blob = blob.getAs('image/jpeg');
    }
    // Blobの名前を設定し、拡張子を適切な形式に変更
    blob.setName(`${presentationName}_${String(index + 1).padStart(2, '0')}.${format.toLowerCase()}`);
    return blob; // Blobを返す
  } catch (e) {
    throw new Error('サムネイルの取得に失敗しました: ' + e.toString());
  }
}

/**
 * ファイルをGoogleドライブに保存する関数
 * @param {Blob} fileBlob - 画像かzip
 * @param {string} presentationName - プレゼンテーションの名前
 * @param {Presentation} presentation - アクティブなプレゼンテーション
 */
function saveToDrive(fileBlob, presentationName, presentation) {
  try {
    // プレゼンテーションが保存されているフォルダを取得
    const presentationId = presentation.getId();
    const file = DriveApp.getFileById(presentationId); // 権限の確認

    const parents = file.getParents();
    
    let PRESENTATION_FOLDER;
    if (!parents.hasNext()) {
      // プレゼンテーションがルートディレクトリに保存されている場合、ルートディレクトリを使用
      PRESENTATION_FOLDER = DriveApp.getRootFolder();
    } else {
      PRESENTATION_FOLDER = parents.next();
    }
    
    // 同名のフォルダが存在するか検索
    const FOLDERS = PRESENTATION_FOLDER.getFoldersByName(presentationName);
    let folder;
    if (FOLDERS.hasNext()) {
      // 既存のフォルダがあればそれを使用
      folder = FOLDERS.next();
    } else {
      // なければ新しいフォルダを作成
      folder = PRESENTATION_FOLDER.createFolder(presentationName);
    }
    
    // ファイルをフォルダに保存
    const IMAGE_FILE = folder.createFile(fileBlob);
    
    // フォルダとファイルへのリンクを生成
    const FOLDER_URL = `https://drive.google.com/drive/folders/${folder.getId()}`;
    const FILE_URL = `https://drive.google.com/uc?export=download&id=${IMAGE_FILE.getId()}`;

    // ダイアログを作成し、フォルダとファイルへのアクセスボタンを提供
    const HTML_OUTPUT = HtmlService.createHtmlOutput(
      `<!DOCTYPE html>
      <html>
        <head>
          <base target="_top">
          <style>
            body { font-family: 'Google Sans', 'Roboto', sans-serif; margin: 0; padding: 10px; }
            .container { text-align: center; padding: 20px; }
            p { margin: 15px 0; }
            button { margin: 10px; padding: 10px 20px; font-size: 16px; border: none; border-radius: 4px; cursor: pointer; }
            button:hover { box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
          </style>
        </head>
        <body>
          <div class="container">
            <p>ファイルをフォルダに保存しました。</p>
            <button onclick="window.open('${FOLDER_URL}','_blank')">フォルダを表示</button>
            <button onclick="window.open('${FILE_URL}','_blank')">ファイルをダウンロード</button>
          </div>
        </body>
      </html>`
    )
    .setWidth(500)
    .setHeight(200);
    // 作成したダイアログをモーダルとして表示
    SlidesApp.getUi().showModalDialog(HTML_OUTPUT, 'ファイルとフォルダへのアクセス');
  } catch (e) {
    throw new Error('ドライブへの保存に失敗しました: ' + e.toString());
  }
}

/**
 * 指定されたプレゼンテーションIDとページIDからサムネイルURLを取得する関数
 *
 * @param {string} presentationId - プレゼンテーションのID
 * @param {string} pageId - ページのID
 * @return {string} サムネイルのURL
 */
function getThumbnailUrl(presentationId, pageId) {
  // Google Slides APIのURLを構築
  const URL = `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageId}/thumbnail`;
  // APIリクエストのオプションを設定
  const OPTIONS = {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    },
    muteHttpExceptions: true,
    params: {
      'thumbnailProperties.thumbnailSize': 'LARGE' // サムネイルのサイズをLARGEに指定
    }
  };

  // APIを呼び出してサムネイルを取得
  const RESPONSE = UrlFetchApp.fetch(URL, OPTIONS);
  // レスポンスをJSONとして解析
  const JSON_RESPONSE = JSON.parse(RESPONSE.getContentText());

  // エラーがあればそれを投げる
  if (RESPONSE.getResponseCode() !== 200) {
    throw new Error('サムネイルの取得に失敗しました: ' + JSON_RESPONSE.error.message);
  }

  // サムネイルのURLを返す
  return JSON_RESPONSE.contentUrl;
}

Step2: Slides APIを有効化する

サービスという項目の右側にある+ボタン「サービスを追加」をクリック

file5

サービスを追加というモーダルが立ち上がるので、Google Slides APIを選択し、バージョンはv1、IDはSlidesのまま、追加ボタンをクリック

file6

Slidesというサービスが追加されたことを確認して、プロジェクトを保存をクリック(最上部のプロジェクト名の変更は任意)

file7

Step3: 認証を通す

もとのGoogle スライドの画面に戻り、画面を再読み込み(更新)すると、メニューバーのヘルプメニューの右側に、スライド画像出力画像で出力(選択中のスライド) という新しいメニューが追加されているのが確認できると思います。(表示されない場合、前のステップできちんとプロジェクトが保存されているかをチェック)

file8

追加されたメニューをクリックすると、認証が必要です というモーダルウィンドウが表示されるので、OKをクリック。

file9

アカウントを選択する画面でライドを作成したアカウントを選択すると、以下のようなアカウントへのアクセスをリクエストする画面が立ち上がるので、許可をクリックすれば準備は完了です。

file10

※認証の際にアカウントによっては、以下のような警告画面が表示されることがあります。この場合、詳細をクリックして内容を確認後、プロジェクトに移動をクリックすることで認証許可画面に戻ることができます。

file11

file12

複数のスライドを画像化してまとめてダウンロード

上記の実装が完了したプレゼンテーションで、まずは画像としてダウンロードしたいスライドを選択します。

選択するスライドは1ページでも、全ページでも、複数ページでも、1ページ飛ばしでも、大丈夫です。

1ページのみ選択の場合は画像形式で、複数スライドが選択されていた際にはZIP形式でファイルが出力されます。

※このときにスライドではなく、スライド中のオブジェクト(文字や画像など)を選択しないようにしてください。スライド以外が選択されているときは処理の途中でエラーが表示されます。

file13

スライドを選択した状態で、先程メニューバーに追加された、画像で出力(選択中のスライド)をクリックすると、画像形式を選択するモーダルウィンドウが立ち上がります。PNGかJPEGから選択してください。

file14

処理中…という表示に切り替わり、しばらくして画像の書き出しが完了すると、ファイルとフォルダへのアクセスという表示に切り替わります。

file15

フォルダを表示をクリックすると書き出したファイルが保存されたGoogleDriveのフォルダが別タブで表示されます。

file16

ファイルをダウンロードをクリックすると、PCに直接ファイルを保存することができます。

file17

file18

利用上の注意点

  • GogleドライブやGoogle スライドの各種権限が必要です。
  • ChromeなどのブラウザにログインしているデフォルトアカウントとGoogleスライドにログインしているアカウントが異なる場合、エラーが発生して動作しません。
  • 画像の書き出し数にはAPIの上限があります(1分に60枚)。Google Apps Scriptの最大実効実行時間にも制限があります(6分)。100枚程度であれば2分もかからずに書き出せました。
  • プレゼンテーションのファイルごとにGASの仕込みを行う必要があります。
  • GASを実装したプレゼンテーションを共有した場合、共有相手にも追加されたメニューが表示されます。共有相手が機能を利用するには自身で新たに認証を通す必要があります。

スクリプトの作成には生成AIをフル活用

今回の実装にあたり、GASのスクリプトの作成はほとんど生成AIにお任せしました。

改善の余地はあると思いますが、プロンプトを工夫して、実現したい機能をファンクション単位で生成させ、それをつなぎ合わせる形で全体を構成しました。コードレビュー、デバッグ、コメントの追加も生成AIに支援してもらうことで、設計や機能改善など、より創造的な作業に労力と時間を確保することができました。

社内版ChatGPT ABChat(えびちゃっと)によるコードレビュー

社内版ChatGPT ABChat(えびちゃっと)によるコードレビュー

まとめ

今回はGoogleスライドの複数ページをまとめて画像として出力する機能をGoogle Apps Script(GAS)を用いて実装する方法をご紹介しました。プレゼンテーションの各スライドを画像として保存したいときや、プレゼンテーションを一部を切り出して共有したいときなど、様々なシーンで活用可能な機能です。生成AIの力を借りてスクリプトの作成を効率化した結果、UIやUXにこだわりながらも短時間で実装を完了することができました。

今後も、GASや生成AIを活用した業務効率化の事例をご紹介してまいりますので、ぜひご期待ください。

AUTHOR

石田 直之

朝日放送グループホールディングス株式会社 デジタル・アーキテック局 データ戦略チーム

ABCグループ各社のAI利活用とデータ分析・利活用を中心に、各種デジタル施策やプロジェクトの推進に取り組んでいます。

WORK@ABC

技術力を培うための
環境と文化

ABCに昔から根付く「自分たちで開発する」文化を支える環境や取り組みをご紹介します
ABCについてもっと知る