LoginSignup
0
0

More than 1 year has passed since last update.

Google Apps ScriptとGoogleスライドを使って画像を合成をする

Last updated at Posted at 2023-03-19

Google Apps Scriptを使って、2つの画像を1つの画像に合成してみました。見積書に押印する(見積書画像に、押印画像を重ね合わせる)のに使うつもりです。

JavaScriptで画像を合成するにはcanvasが便利ですが、 ウェブブラウザー上でしか使えません。 今回はサーバー側で合成したかったので、GoogleスライドをApps Scriptで操作することで実現しました。一時的なスライドを作り、そこに2つの画像を挿入後、1つの画像として出力します。

2023/04/02 追記:
後日、Google Apps Script側でcanvasを含むHTMLを作成し、それをPDFとして保存すれば、2つの画像を合成できることが分かりました。

2023/04/08 追記:
後日、DocsServiceAppを使うことで、サイズを指定したGoogleスライドを作成できることが分かりました。pptxファイルを作って、変換しているようです。またSlides.Presentations.Pages.getThumbnailを使えば、Apps Scriptの実行を終了させることなくサムネイル画像を生成できます。

DocsServiceAppによる実現方法

Google Apps Scriptを使って、ある画像baseImageに別の画像stampImageを重ね合わせて、新しい画像combinedImageを作ります。baseImageとstampImageはGoogleドライブに保存されています。

事前準備

サービスにDrive APIGoogle Slides APIを追加します。

ライブラリーにDocsServiceAppを追加します。Script IDは
108j6x_ZX544wEhGkgddFYM6Ie09edDqXaFwnW3RVFQCLHw_mEueqUHTW です。

実装

const baseImageId = '1wUip1VniS0ZsoBn-0D-F9ZXbbDrWsV8d';
const stampImageId = '1VadRiRqNG_OgRrz3uVdgnevUoQZ8EaTh';
const combinedImageFolderId = '1HUvuvdM8Euvff-mBDmzmrd2j1TCkJHlW';

const stamp = {
  x: 360, y: 165, width: 85, height: 85
};

function generateCombinedImage() {
  // baseImageと同じサイズのスライドを作る
  const baseImage = Drive.Files.get(baseImageId);
  const { width, height } = baseImage.imageMediaMetadata;
  const presentationId = DocsServiceApp.createNewSlidesWithPageSize({
    title: 'temp slide',
    width: { unit: 'pixel', size: width },
    height: { unit: 'pixel', size: height }
  });
  const slide = SlidesApp.openById(presentationId);
  const page = slide.getSlides()[0];
  
  // 画像を挿入する
  const baseFile = DriveApp.getFileById(baseImageId);
  const baseBlob = baseFile.getBlob();
  page.insertImage(baseBlob);
  const stampBlob = DriveApp.getFileById(stampImageId).getBlob();
  page.insertImage(stampBlob, stamp.x, stamp.y, stamp.width, stamp.height);
  slide.saveAndClose();

  // サムネイル画像を生成する
  const thumbnail = Slides.Presentations.Pages.getThumbnail(presentationId, page.getObjectId(), {
    'thumbnailProperties.thumbnailSize': 'LARGE',
    'thumbnailProperties.mimeType': 'PNG'
  });
  const thumbnailUrl = thumbnail.contentUrl.replace(/=s\d+/, '=s' + width);
  const thumbnailBlob = UrlFetchApp.fetch(thumbnailUrl).getBlob().setName('combined_' + baseFile.getName());
  DriveApp.getFolderById(combinedImageFolderId).createFile(thumbnailBlob);
  DriveApp.getFileById(presentationId).setTrashed(true);
}

GPT-4を使って作ったときの所感

何とか実現はできたもののかなり苦戦しました。予め準備するものが2つあります。

  1. サイズを調整したスライド。合成結果の画像サイズをApps Scriptから制御できないので、テンプレートとスライドを準備して、それをコピーして使います。
  2. 2つの関数の間で情報を連携するためのGoogleスプレッドシート。Apps Scriptからスライドを操作した結果を反映するにはSlidesApp.Presentation.saveAndClose()を実行する必要がありますが、スライドの画像を取得するためにはそれだけでは足りず、一時的なスライドを作成したらApps Scriptを終了し、サムネイル画像を生成する関数を別途呼び出す必要がありました。時間をあけて2つの関数を実行するために使います。

コードを書くのにChatGPT-4を使いました。しかし思ったよりも多くの時間を使ってしまいました。Apps ScriptはAPIに統一感がなく、使いこなすための細かなノウハウが色々と必要です。こうした制約事項が多く存在する実行環境では、事前に十分な情報をChatGPTに提供することが重要です。ChatGPTへの指示について、試行錯誤した結果「最初にこう伝えるべきだった」とまとめた指示を以下に掲載します。制約事項としてかなり細かな点まで伝えています。ChatGPTは「分からない」といわずに、あたかも実行できそうなコードを提示してきます。実行してみないと正しいかどうかが分からないので、検証にとても時間がかかってしまいました。

生成されたコード

Code.gs
// 事前に分かっている情報
var baseImageId = '...';
var stampImageId = '...';
var templatePresentationId = '...';
var combinedImageFolderId = '...';
var spreadsheetId = '...';
const x = 481;
const y = 215;
const width = 100;
const height = 100;

function createTemporarySlide() {
  // テンプレート用のGoogleスライドを複製
  var templatePresentationFile = DriveApp.getFileById(templatePresentationId);
  var temporarySlideFile = templatePresentationFile.makeCopy('Temporary Slide');
  var temporarySlideId = temporarySlideFile.getId();

  // 一時的なスライドにアクセス
  var temporarySlide = SlidesApp.openById(temporarySlideId);
  var slideWidth = temporarySlide.getPageWidth();
  var slideHeight = temporarySlide.getPageHeight();

  // 画像を追加
  var baseImage = Drive.Files.get(baseImageId);
  var stampImage = Drive.Files.get(stampImageId);
  var baseImageBlob = DriveApp.getFileById(baseImageId).getBlob();
  var stampImageBlob = DriveApp.getFileById(stampImageId).getBlob();

  // baseImageのサイズを取得
  var baseImageWidth = baseImage.imageMediaMetadata.width;
  var baseImageHeight = baseImage.imageMediaMetadata.height;

  // baseImageを挿入
  var ratio = Math.min(slideWidth / baseImageWidth, slideHeight / baseImageHeight);
  var insertedBaseImage =  temporarySlide.getSlides()[0].insertImage(baseImageBlob);
  insertedBaseImage.setWidth(baseImageWidth * ratio).setHeight(baseImageHeight * ratio);

  // stampImageをリサイズおよび位置を調整して挿入
  var resizedWidth = width * ratio;
  var resizedHeight = height * ratio;
  var newPositionX = x * ratio;
  var newPositionY = y * ratio;

  var insertedStampImage = temporarySlide.getSlides()[0].insertImage(stampImageBlob);
  insertedStampImage.setTop(newPositionY).setLeft(newPositionX).setWidth(resizedWidth).setHeight(resizedHeight);

  // スライドへの変更を保存して閉じる
  temporarySlide.saveAndClose();

  // 一時的なスライドの情報をスプレッドシートに追加
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  var sheet = spreadsheet.getSheetByName('Temporary Slides');
  sheet.appendRow([temporarySlideId, 'Not generated']);
}

function generateCombinedImage() {
  // スプレッドシートから未生成の一時的なスライドを取得
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  var sheet = spreadsheet.getSheetByName('Temporary Slides');
  var data = sheet.getDataRange().getValues();
  for (var i = 0; i < data.length; i++) {
    if (data[i][1] === 'Not generated') {
      var temporarySlideId = data[i][0];
      
      // サムネイル画像を生成
      var temporarySlideFile = DriveApp.getFileById(temporarySlideId);
      var thumbnailBlob = temporarySlideFile.getThumbnail().getAs('image/png');
      var combinedImage = DriveApp.createFile(thumbnailBlob);
      
      // 生成された画像を指定のフォルダに移動
      var combinedImageFolder = DriveApp.getFolderById(combinedImageFolderId);
      combinedImage.moveTo(combinedImageFolder);
      
      // スプレッドシートのステータスを更新
      sheet.getRange(i + 1, 2).setValue('Generated');
      
      // 一時的なスライドを削除
      temporarySlideFile.setTrashed(true);
    }
  }
}

// トリガーを設定してgenerateCombinedImage()を実行する(例:1分おき)
function createTimeDrivenTrigger() {
  ScriptApp.newTrigger('generateCombinedImage')
  .timeBased()
  .everyMinutes(1)
  .create();
}

ChatGPT-4への指示

あなたは分かりやすいコードを書くことに定評のあるシニアプログラマーです。私がやりたいことと制限事項を伝えるので、それに従ってGoogle Apps Scriptのコードを書いてください。

やりたいこと

  • Google Apps Scriptを使って、ある画像baseImageに別の画像stampImageを重ね合わせて、新しい画像combinedImageを作る。
  • baseImageとstampImageはGoogleドライブに保存してある。combinedImageもドライブに保存する。
  • combinedImageのサイズを決めるために、テンプレート用のGoogleスライドtemplatePresentationが予め準備されている(ファイル > ページ設定、で希望するサイズに変更)。
  • 事前に分かっていることは次の通り:
    • baseImage、stampImage、templatePresentationのそれぞれのファイルID。
    • stampImageを重ね合わせる位置である、x, y, width, height。
    • combinedImageを保存する先のフォルダーID。
    • 処理状況を記録するスプレッドシートのID。

実現するにあたっての制限事項

  • テンプレート用のGoogleスライドを複製して、画像を合成するための一時的なスライドとして利用する。複製にはDriveApp.File.makeCopy()を使い、生成したファイルのファイルIDから、SlidesApp.openById()を使ってスライドにアクセスする。
  • スライドから画像を生成するのに DriveApp.File.getThumbnail().getAs('image/png')を使う。
  • getThumbnail()メソッドで画像を生成するには、一旦、Apps Scriptの実行を終了させて、もう一度アクセスする必要があった。そこで、一時的なスライドの作成と、一時的なスライドからサムネイル画像を作成するfunctionを分ける。一時的なスライドを作成して、baseImageとstampImageを追加したあと、そのスライドのファイルIDと、サムネイル画像未生成であることをGoogleスプレッドシートにappendRow()で追記する。トリガーを使ってサムネイル画像を生成する際、そのスプレッドシートでサムネイル画像が未生成のファイルIDを対象とする。
  • 挿入するbaseImageのサイズは、一時的なスライドのサイズと同じにする。スライドのサイズとbaseImageのサイズの比率を計算して、stampImageの位置とサイズに反映する。スライドのサイズはSlidesApp.Presentation.getPageWidth()SlidesApp.Presentation.getPageHeight()で取得する。なお、getPageWidht()とgetPageHeight()にはgetMagnitude()もgetValue()も不要。
  • スライドのサイズとbaseImageのサイズの比率を計算し、その比率をbaseImageに反映して挿入する。具体的には、Math.min(slideWidth / baseImageWidth, slideHeight / baseImageHeight)を使用してスライドの幅と高さの比率の小さい方を選択し、insertedBaseImage.setWidth(baseImageWidth * ratio).setHeight(baseImageHeight * ratio)を使ってbaseImageのサイズを調整する。
  • baseImage, stampImageのサイズは、Advanced SerivceのDrive APIを使う。Drive.Files.get(fileId).imageMediaMetadataのwidthとheightで取得できる。
  • Apps Scriptの操作をスライドに反映するために、SlidesApp.Presentation.saveAndClose()メソッドを呼び出す必要がある。

特にハマったポイント

最初は、テンプレートスライドを準備せず、まっさらなスライドを作ってbaseImageのサイズに変更しようと試していました。なのに、全く動きません...。

Apps ScriptからGoogle Workspaceアプリを操作するには、2つのAPIがあります。Google ServicesとAdvanced Servicesです。スライドなら、前者がSlidesAppで、後者がSlides APIです。

Slides APIの方が、より高度な処理を実行できます。スライドを生成するcreateの仕様書には、Request bodyにpageSizeを指定できると書いてあります。

{
  "presentationId": string,
  "pageSize": {
    object (Size)
  },

しかし、Issue Trackerによると、説明にpageSizeは反映しないと書いてあるからバグじゃない、とのこと。

If a presentationId is provided, it is used as the ID of the new presentation. Otherwise, a new ID is generated. Other fields in the request, including any provided content, are ignored.

presentationId を指定すると、新しいプレゼンテーションの ID として使用されます。それ以外の場合は、新しい ID が生成されます。リクエストの他のフィールド(指定されたコンテンツを含む)は無視されます。

さすがに「なんじゃそりゃー」と思ってしまいました。私は、API仕様書で最初に引数を確認します。create()の引数にpageSizeと書いてあったので、サイズを指定してスライドを作れるんだ、と思いこんでしまったのです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0