📝

GASで取得したデータの画像化・トリミング→ツイートの備忘録

2022/08/27に公開約6,200字

参考サイトのコピペになってしまうところはなるべく省き、調べたこと、追加した部分、詰まったところなどを残しておきます。

本題

GASでスクレイピングした情報(文字列)を配列に格納し、それを画像化・トリミング→ツイートする機能を実装しました。GASもJavaScriptもほぼ経験がありませんでしたが、参考サイトがたくさんあったおかげで形にすることができました。
エラー処理省略しています。

〜流れ〜
(画像化してツイートしたい内容を配列で持っている状態)
→スライドに書き込む
→スライドを画像としてドライブに保存
→保存した画像をトリミング・上書き
→画像を添付してツイート

画像化してドライブに保存

どうやって画像化するか

画像化の方法について調べたところ、手作業を挟まず画像化するには

  • スライドを利用して画像保存
  • PDFに変換する

といった方法が見つかりました。スプレッドシートからスクリーンショットのように画像保存するのはできないようです。

PDF化した場合、ツイートに添付するためにJPEG等の形式に変換する必要がありますが、【随時更新】GASでできないことまとめを読むと、PDFファイルはGASでは扱いづらそうです。
実際に画像変換している記事〔Googleスプレッドシートを画像にしてSlackに連携(定期投稿)する方法〕でもConvertAPIといった外部のAPIを挟んでおり、PDFにするメリットがなければスライドを使う方が手軽だと感じました。
というわけで、今回はスライド経由で画像化しました。

スライドの中身の書き換え

スライドのページサイズを変更するメソッドが見つからなかったので、スライドのファイルはあらかじめ用意しておき、中身をスクリプト内で書き換えます。

中身の書き換え方法については、直接テキストボックスに書き込む他にも、スプレッドシート等とリンクさせ更新することもできるようです。〔【GAS】スプレッドシートを画像保存できない壁を乗り越える(Googleスライド利用)
私の場合は書き込みたい文字列の配列が既にあり、表グラフも利用しません。スプレッドシートに書き込むと余計な工程が1つ増えてしまうので、直接テキストボックスに入れました。

今回私の用意したスライドには複数のテキストボックスがありました。目的のテキストボックスに書き込むにはオブジェクトIDが必要です。オブジェクトIDは毎回変化するものではないので、こちらの記事〔GASでGoogleスライドのテキストボックスの文字列を取得する方法〕を参考に取得しておきます。

const presentation = SlidesApp.openByUrl('SlideURL');  //もしくはopenById
const ObjId        = 'オブジェクトID';
const shape        = presentation.getPageElementById(ObjId).asShape();

//テキストを消したい場合
shape.getText().clear();

//書き込み
shape.getText().setText('書き込む文字列');

setTextに配列は渡せないので、join等で結合し一気に書き込みます。フォントサイズの変更や文字装飾もできます。
これで内容の書き換えは完了です。

ちなみにテキストボックスの自動調整機能(行数が多いとき文字がすべて収まるように縮小してくれる)を使いたかったのですが、機能が用意されていないのか方法が見つかりませんでした。なので今回は行数=配列の要素数でテキストサイズを変更するようにしました。

画像変換

【GAS】Googleスライドの資料を画像で一括ダウンロードする方法を参考にしましたが、いくつか変更点があるのでコードを載せます。

変更点

  • 指定のスライド1枚のみ保存
  • 変換先のフォーマットをJPEG
  • ファイル名を指定
  • 特定のフォルダに保存
  • 同名ファイルが既に存在するときは上書き(デフォルトだと上書きされない)
  • 画像のIDを返す(トリミングに使用するため)
const presentation_id = presentation.getId();
const folder          = DriveApp.getFolderById('フォルダID');

//引数について
//slide   :presentation.getSlides()[0]  ←保存したいスライド
//fileName:'image' ←保存したいファイル名
function convertSlideToImg(presentationId, folder, slide, fileName) {
  const options = {
    method: 'get',
    headers: {'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()},
    muteHttpExceptions: true
  };
  const pageId = slide.getObjectId();
  const imgName = fileName + '.jpeg';
  const url = 'https://docs.google.com/presentation/d/' + presentationId + 
	      '/export/jpeg?id=' + presentationId + '&pageid=' + pageId;
  let imgId = '';
  
  const res = UrlFetchApp.fetch(url, options);
  if (res.getResponseCode() === 200) {
    let file = folder.getFilesByName(imgName);
    if (file.hasNext()) {
      imgId = file.next().getId();
      Drive.Files.update({}, imgId, res.getBlob());
    } else {
      imgId = folder.createFile(res.getBlob()).setName(imgName).getId();
    }   
  }
  return imgId;
}

指定のスライド1枚のみ出力するよう変更したのは、スライドのレイアウトを数パターン用意して使い分けたかったからです。

getFilesByName(imgName)で指定のフォルダ内の同名ファイルを探し、あった場合は上書き、無かったら新規作成しています。上書きがDrive.Files.update、新規作成がfolder.createFileです。画像の上書きにはDrive APIを有効にしておく必要があります。〔GASでGoogleドライブ上にある画像を更新する

フォルダは共有設定を「リンクを知っている人全員」にしておきます(ドライブ上の画像をツイートするとき画像のURLを使用するため。設定しないと添付できません)。フォルダごと設定すれば中身にも適応されます。
共有設定はfolder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW)でもできますが、一度設定すればその後も保持されるのでコードを追加するまでもないと思います。

imageIdは余白のトリミングと画像生成が成功したかチェックするのに使用します。

画像のトリミング

スクレイピングで得られるデータは毎回長さが異なるため、量が少ない場合は余白を切り取るようにしました。横幅はそのままで縦だけ短くします。
実装には【GAS】画像をリサイズする(ImgAppライブラリ)を参考にしました。この機能はImgAppライブラリを入れる必要があります。
こちらの変更点は、

  • スライドに書き込んだデータの行数で切り取り量を変える。
    ○行なら半分、▲行以内なら1/3切り取る、という感じ
  • ファイルを上書きする

です。
スライドのページサイズは最初に指定しているため、生成される画像はすべて同じサイズになるはずですが、スライドファイルを複数使用する場合を考えてgetSizeでwidth, heightを取得してトリミングするようにしました。

//引数について
//imgId  :画像のID(convertSlideToImgの戻り値)
//lineNum:書き込んだ行数
function trimMargin(imgId, lineNum){
  const file    = DriveApp.getFileById(imgId);
  const imgBlob = file.getBlob();
  const imgSize = ImgApp.getSize(imgBlob);
  const width   = imgSize['width'];
  let height    = imgSize['height'];
  
  if(lineNum <){
    //切り取り量を決定
  }
  
  const object = {
    blob: imgBlob,
    unit: 'pixel',
    crop: { t:0, b:縦の切り取り量, l:0, r:0 },
    outputWidth: width   //横幅そのまま
  };
  Drive.Files.update({}, file.getId(), ImgApp.editImage(object));
}

ドライブに保存した画像をツイート

Google Apps ScriptでTwitterに画像投稿 複数画像も! (備忘録)を参考にしました。参考というかほぼそのままなのでコードは省略します。
変更点としては、

  • 引数のimg_urlsに画像ID配列を渡すようにしたため、関数内でURLに整形する部分を追加

があります。

URLに整形というのは、このようにIDにhttps〜を足していくことです。

for (let i=0; i<img_urls.length; i++){
    imgUrls[i] = 'https://drive.google.com/uc?id=' + imgUrls[i];
  }

このhttps〜を加えることで、URLで画像を開くことができます。
画像を添付しなくてもツイートできますし、引数にre_idがありますが単純にツイートしたいならこちらも空にすればOKです。
他にも画像5枚以上のとき分割する処理を削除したりしましたが、これは残しておいても問題なかった機能なので省略。

これでツイートまでの一連の機能が完成です。

引用リツイートについて

https://twitter.com/【ユーザー名】/status/【ツイートID】 を文章に含めてツイートすると引用リツイートができます。
URLは自動的に23字に短縮されるため、URLを含んだツイートをすると23字分の文字数を取られてしまいます。しかし、引用リツイートの場合は文字数を消費しない方法があります。

このリファレンス〔Post, retrieve, and engage with Tweets〕のパラメーターにあるattachment_urlを使用します。
「拡張ツイートのステータスボディにURLがカウントされないようにするには、ツイートの添付ファイルとしてURLを提供します。このURLは、ツイートのパーマリンク、またはダイレクトメッセージのディープリンクである必要があります。Twitter以外の任意のURLは、ステータステキストに残す必要があります。attachment_urlパラメータに渡されたURLがTweet permalinkまたはDirect Message deep linkに一致しない場合、Tweetの作成に失敗し、例外が発生します。」(DeepL翻訳)
つまり、‘status’や‘in_reply_to_status_id’などに加えてattachment_urlを渡せばOKです。

発生した問題

ツイートされた画像が真っ白問題
スライドに書き込む前にshape.getText().clear()を使用していたのですが、その真っ白な状態のまま画像化されていました。スライドファイルを開いてみると文章自体はちゃんと書き込まれていたので、テキストボックスのクリア→文章書き込みの間に画像化の処理が終わってしまっているのではないかと思いスリープを挟んでも変化なし。

【解決方法】
一度スライドを閉じることで文章が保存・反映されました。
スライドの保存と終了はpresentation.saveAndClose()です。このコードの実行後にスライドを操作するには再度開く必要があるので一番最後に置き、念のためUtilities.sleep(1000)で1秒スリープを入れました。
このメソッドを知らなかったので解決までいろいろ調べ回ったのですが、「スライドの中身の書き換え」で紹介した「【GAS】スプレッドシートを画像保存できない壁を乗り越える(Googleスライド利用)」で使用されていました。

Discussion

ログインするとコメントできます