【有料級】画像を含むGoogleフォームの回答をGASで取得し、Googleドキュメントに反映後、Word文書にしてメールを自動送信しよう!

過去に、Googleフォームの回答内容をGASで取得し、Googleドキュメント内の本文書き換え→PDF化したものをメールで送信するという技をお伝えしたところ、以下のようなお問い合わせを頂きました

出張者が、Googleフォームで氏名・出張日等を入力するとともに、「画像」をアップロードし、それをGoogleドキュメントのフォーマットでメールで送りたいと考えています。参考記事のように、PDF化したものでもよいのですが、出張者が修正できるようにした方が融通が利くと考えられるため、ドキュメントのまま送信したいです。

お問い合わせ内容の要約

まず、GASは画像を扱うことに弱いです

こーすけ先生

でも、大丈夫
ImgAppライブラリを活用してなんとかします!!!

質問者様から「Googleドキュメント」をメール送信したいとご要望を頂きましたが、Googleドキュメントはあくまでもクラウド上のサービスであるため、ここではWordに変換しましょう!と助言させて頂きました

今回の記事は、Googleフォームの回答(画像含む)をメール送信するというもので、非常に実用的だと思います

こーすけ先生

お問い合わせ頂いた方から許可を頂いたため、皆様にも「画像」を扱う場合について共有したいと思います!

本記事の内容
  • Googleフォームの回答内容をGASで取得して、Googleドキュメントで作成したテンプレートに取得した回答に書き換える
  • ImgAppライブラリを用いて画像を読み込む
  • 自動作成したGoogleドキュメントをWord形式に変換する
  • 変換したWord文書を添付ファイルとして相手先にメールを送信する
こーすけ先生

なんと「有料級の回答」と言って頂きました!
私自身、2024年4月から起業予定なので、業務に役立つ情報が提供できたと自信がつきました!

この記事を読めば、画像を含むGoogleフォームの回答データを取得し、Googleドキュメント内に反映→Word文書化する方法を学ぶことができます!めちゃくちゃ便利なので、是非読んでいってください!

この記事の執筆者について
  • GASの人
  • ITベンダSEとして12年勤務する中で民間、金融、官公庁の現場を一通り経験済
  • 現在は公務員をやりながら起業に向けて着々と準備中
GASなら任せろ!

​GASを極めたい方や、業務の効率化を図りたい方は、ぜひこの記事を読んください!
難しいことはGASに任せて、我々人間は楽しちゃいましょう!

こーすけ先生

当ブログでは実際に仕事でGASを扱っている私が、GASの魅力について徹底的に取り上げていきます!

目次

事前準備

本記事で取り組む演習
  • Googleフォームの回答内容をGASで取得して、Googleドキュメントで作成したテンプレートに取得した回答に書き換える
  • ImgAppライブラリを用いて画像を読み込む
  • 自動作成したGoogleドキュメントをWord形式に変換する
  • 変換したWord文書を添付ファイルとして相手先にメールを送信する
こーすけ先生

本記事は初心者の方向けに丁寧に解説しているので、事前準備を飛ばして先に進みたい!という方はここをクリックして、ジャンプしてください

【事前準備①】Googleフォームを作成する

スクロールできます
項目名 タイプ必須
メールアドレスメール
氏名を選択してくださいプルダウン
欠席日を入力してください日付
連日にまたがって欠席する場合は、欠席期間の終了日を入力してください日付
届出理由を選択してくださいラジオボタン
欠席理由のエビデンスを必要に応じてアップロードしてくださいファイルのアップロード
こーすけ先生

参考に私が作成したフォームは以下となります!

こーすけ先生

1つでも回答がないとエラーになってしまうので、回答しておいてください!

【事前準備②】Googleドキュメントを作成する

こーすけ先生

それでは、次にGoogleドキュメントで欠席届テンプレートを作成しましょう!

どんな様式でも構いませんが上記フォームの値をマッピングさせるため、それを意識して作成してみて下さい!

こーすけ先生

今回、こちらの「欠席届(画像ver)」を使って演習します!
必要に応じてコピーして使ってください!

{{}}(可変文字部)について

がすぴょん

この{{date}}みたいに波括弧で囲んであるのは何なの?

こーすけ先生

波括弧の部分は「可変文字部」といって、{{}}内を入力内容に書き換える魔法のような存在なんだ!詳しくは、以前の記事で説明しているよ!
是非読んでみてね!

【事前準備③】コピーしたドキュメントの出力場所の作成

こーすけ先生

一時的にコピーしたドキュメントを収納するフォルダ(temp)を作りましょう!

// ドキュメント出力先
const DOCX_OUTDIR = DriveApp.getFolderById('〇〇〇〇〇〇(自分のフォルダID)');
こーすけ先生

あとでフォルダIDを記載する必要があるので、忘れずにメモしておきましょう!

このフォルダは、一時的にコピーしたGoogleドキュメントを保存する場所です
実行後にコピーしたファイルは削除されるため、何も残りません

【お題】Googleフォームの回答をGoogleドキュメント内に反映し、Word文書にしてメールを自動送信する

がすぴょん

作成したGoogleフォームの「︙」をクリックして「〈〉スクリプトエディタ」から、コンテナバインド型でエディタを開こう!

まずはコード全文を確認

こーすけ先生

まずはコード全文を確認しよう!

// 欠席届テンプレートファイル
const DOC_TEMPLATE = DriveApp.getFileById('〇〇〇〇〇〇(自分のドキュメントID)');

// ドキュメント出力先
const DOCX_OUTDIR = DriveApp.getFolderById('〇〇〇〇〇〇(自分のフォルダID)');

// 最後の削除用リスト
let delIdLst = new Array();

function gaslog_formSubmit(e) {
  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = FormApp.getActiveForm().getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }

  // 回答をもとに欠席届を作成する 
  let wFileRtn = createGDoc(itemResponses);
  // DOCX変換してファイルIDを取得する
  let wPdfId = createDocx(wFileRtn[0], wFileRtn[1]);
  delIdLst.push(wFileRtn[0]);
  delIdLst.push(wPdfId);
  
  // 今回はDOCXファイルを添付してメールを送信する
  sendEmailEx(
    e !== undefined ? e.response.getRespondentEmail() : '(eが未定義の場合に送信されるメールアドレス)'
    , wFileRtn[2]
    , `受講生より欠席届が送信されました。`
    , {attachments: DriveApp.getFileById(wPdfId).getBlob()}
  );

  // 各種保存したファイルを削除する
  delIdLst.forEach(function(delId){
    DriveApp.getFileById(delId).setTrashed(true);
  });
}

function createGDoc(itemResponses) {
  // テンプレートファイルをコピーする
  const wCopyFile = DOC_TEMPLATE.makeCopy()
        , wCopyFileId = wCopyFile.getId()
        , wCopyDoc = DocumentApp.openById(wCopyFileId); // コピーしたファイルをGoogleドキュメントとして開く
  let wCopyDocBody = wCopyDoc.getBody()                 // Googleドキュメント内の本文を取得する
      , today = dayjs.dayjs()
      , wName
      , wAbsentDate;

  delIdLst.push(wCopyFileId);
  wCopyDocBody = wCopyDocBody.replaceText(`{{date}}`, today.format('YYYY年MM月DD日'));
  itemResponses.forEach(function(itemResponse){
    switch (itemResponse.getItem().getTitle()) {
      case '氏名を選択してください。':
        wName = itemResponse.getResponse();
        wCopyDocBody = wCopyDocBody.replaceText(`{{name}}`, wName);
        break;

      case '欠席日を入力してください。':
        dateF = dayjs.dayjs(itemResponse.getResponse()).format('YYYY年MM月DD日');
        break;

      case '連日にまたがって欠席する場合は、欠席期間の終了日を入力してください。': // 欠席の場合のみ対応
        if ( itemResponse.getResponse()!='' ) {
          dateT = dayjs.dayjs(itemResponse.getResponse()).format('YYYY年MM月DD日');
        } else {
          dateT = dateF;
        }
        wAbsentDate = dateF;
        wCopyDocBody = wCopyDocBody.replaceText(`{{applydatefrom}}`, wAbsentDate);
        wCopyDocBody = wCopyDocBody.replaceText(`{{applydateto}}`, dateT);
        break;

      case '届出理由を選択してください。':
        wCopyDocBody = wCopyDocBody.replaceText(`{{reason}}`, itemResponse.getResponse());
        break;

      case '欠席理由のエビデンスを必要に応じてアップロードしてください。':
        // 画像挿入位置を把握
        let insertImageIdx;
        wCopyDocBody.getParagraphs().some(function(para, pIdx){
          if (para.getText().includes(`{{evidence}}`)) {
            insertImageIdx = pIdx+1;
            wCopyDocBody.replaceText(`{{evidence}}`, '');
            return;
          }
        });
        itemResponse.getResponse().forEach(function(item) {
          delIdLst.push(item);
          // 撮影日時を印字
          let shootingTxt = getShootingDate(item);
          if (shootingTxt) {
            wCopyDocBody.insertParagraph(insertImageIdx, shootingTxt);
            insertImageIdx++;
          }
          // 画像を縮小:150px程度としたうえで配置
          let convImage = ImgApp.doResize(item, 150);
          wCopyDocBody.insertImage(insertImageIdx, convImage.blob).setWidth(150);
          insertImageIdx++;
        });
        break;

      default:
        break;
    }
  });
  wCopyDoc.saveAndClose();
  // ファイル名を変更する
  let fileName = `欠席届_${wName}_${today.format('YYYYMMDD')}`;
  wCopyFile.setName(fileName);
  // コピーしたファイルIDとファイル名、メール用の件名を返却する(あとでこのIDをもとにDOCXに変換するため)
  return [wCopyFileId, fileName, `【自動送信メール】欠席届の送信_${wName}_${wAbsentDate}`];
}

function getShootingDate(id) {
  // Drive APIを用いて、Exifデータを取得する
  let metaData = DriveV2.Files.get(id).imageMediaMetadata;
  if (metaData) {
    return metaData.date;
  } else {
    return null;
  }
}

function createDocx(docId, fileName) {
  // Word変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/document/d/${docId}/export?format=docx`;

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  };
  // Word(*.docx)を作成する
  let wBlob = UrlFetchApp.fetch(wUrl, wOtions).getBlob().setName(fileName + '.docx');

  // Wordを指定したフォルダに保存する
  return DOCX_OUTDIR.createFile(wBlob).getId();
}

function sendEmailEx(_recipient, _subject, _body, _option) {
  // 引数の内容でメールを下書き保存する
  const mailDraft = GmailApp.createDraft(_recipient, _subject, _body, _option);
  // 下書き保存したメールIDから下書きを取得し、メール送信を依頼する
  GmailApp.getDraft(mailDraft.getId()).send();
}
こーすけ先生

1つずつ解説していくよ!

こーすけ先生

こちらの記事から派生した内容なので、被ってる部分の説明は省略してます!上記記事も併せてお読みください!

【STEP1】ライブラリを追加する

こーすけ先生

ライブラリは、「Days.js」と「ImgApp」の2つを追加する必要があるよ!

Days.jsライブラリを追加する

STEP
メニューバーから「+(ライブラリを追加)」をクリック
STEP
出てきたダイアログのスクリプトIDに以下のIDを入力し、「検索」をクリック

1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB

STEP
「追加」をクリックすると、GAS上で「Day.jsライブラリ」を使用できるようになる

ImgAppライブラリを活用して、画像データを圧縮する

こーすけ先生

このライブラリはGASが苦手とする画像編集を補完するライブラリです

主に、画像のサイズ取得やリサイズ、トリミング等を行うことができます

追加用スクリプトID
1T03nYHRho6XMWYcaumClcWr6ble65mAT8OLJqRFJ5lukPVogAN2NDl-y

こーすけ先生

「Days.jsライブラリ」と同じ要領で、ImgAppをライブラリに追加してみてね!

【STEP2】GASエディタの「サービスを追加」からDrive API を追加する

不正防止の観点から、確実に当日の写真が添付されたかを確認したいので、写真の撮影日時(最終更新日)をドキュメントへ出力したい。

お問い合わせ内容の要約
こーすけ先生

写真の撮影日時を取得するためには、いわゆる画像のExif情報を取得する必要があります

Exif情報とは、写真の撮影日時やカメラの機種名等、様々な情報を含んだデータの集まりのこと

GASの通常の方式でファイルを取得した場合、DriveApp.getFileById(item)はExif情報を取得することができません

こーすけ先生

そのため、「Drive API 」をサービスを追加したうえで、Drive API経由でファイルにアクセスしましょう!

STEP
サービスを追加をクリックし、「Drive API ドキュメント」を選択する
こーすけ先生

画像のExif情報を取得するために「v2」にする必要がありますが、デフォルトでは、「v3」になっています

ここで「v2」を選べば、STEP2に進まず、に進んで構いません
バージョンを変更したくない方は、STEP2に進んでください

STEP
設定(歯車マーク)をクリックし、appsscript.jsonを編集できるよう設定する
こーすけ先生

「appsscript.jsonマニュフェストファイルをエディタで表示する」にチェック

STEP
appsscript.jsonからDriveAPIのバージョンをV2に下げる
      {
        "userSymbol": "DriveV2",
        "version": "v2",
        "serviceId": "drive"
      },
こーすけ先生

上記のように「version」を「v2」に変更しましょう!

これで、画像に撮影日時の情報が存在する場合、情報を読み取ることができるようになりました

【STEP3】Googleフォームのフォーム項目をGASで取得する

がすぴょん

作成したGoogleフォームの「︙」をクリックして「〈〉スクリプトエディタ」から、コンテナバインド型でエディタを開こう!

function gaslog_formSubmit(e) {
  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = FormApp.getActiveForm().getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }
  console.log(itemResponses);
}
18:50:29	お知らせ	実行開始
18:50:31	情報	[ { toString: [Function],
    getItem: [Function],
    setFeedback: [Function],
    setScore: [Function],
    getScore: [Function],
    getFeedback: [Function],
    getResponse: [Function] },
…
  { toString: [Function],
    getItem: [Function],
    setFeedback: [Function],
    setScore: [Function],
    getScore: [Function],
    getFeedback: [Function],
    getResponse: [Function] } ]
18:50:30	お知らせ	実行完了
こーすけ先生

オブジェクトがしっかり取得できていることが分かりましたね!

【STEP3】の補足説明

こーすけ先生

補足説明が不要な方は【STEP4】までジャンプしてください!

  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } 

もし 、イベントオブジェクトeundefined でない場合、イベントオブジェクトから回答情報を取得します

e.response はイベントオブジェクト内のフォームの回答に関するプロパティであり、getItemResponses() メソッドは回答に含まれる個々の質問と回答のペアを含むオブジェクトの配列を返します

else {
    const wFormRes = FormApp.getActiveForm().getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }

eundefined である場合、このブロックが実行されます

こーすけ先生

つまり、直接フォームの回答を取得する必要があります

const wFormRes = FormApp.getActiveForm().getResponses();

フォームの全ての回答を取得し、それを wFormRes という変数に格納しています

itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();

wFormRes 配列の最後の要素(最新の回答)を取得し、その回答に含まれる質問と回答のペアを含むオブジェクトの配列を itemResponses に代入しています

こーすけ先生

この条件分岐を使用することで、送信イベントが発生した場合と、直接回答を取得する場合の2つのケースに対応できます!

イベントオブジェクトが存在する場合は、それから回答情報を取得し、存在しない場合はフォームの全回答から最新の回答を取得しています

【STEP4】Googleフォームの回答内容を取得し、Googleドキュメントを書き換える

こーすけ先生

この部分は、フォームの回答を取得し、Googleドキュメント本文を回答内容に書き換えしています!

// 欠席届テンプレートファイル
const DOC_TEMPLATE = DriveApp.getFileById('〇〇〇〇〇〇(自分のドキュメントID)');
// ドキュメント出力先
const DOCX_OUTDIR = DriveApp.getFolderById('〇〇〇〇〇〇(自分のフォルダID)');

function createGDoc(itemResponses) {
  // テンプレートファイルをコピーする
  const wCopyFile = DOC_TEMPLATE.makeCopy()
        , wCopyFileId = wCopyFile.getId()
        , wCopyDoc = DocumentApp.openById(wCopyFileId); // コピーしたファイルをGoogleドキュメントとして開く
  let wCopyDocBody = wCopyDoc.getBody()                 // Googleドキュメント内の本文を取得する
      , today = dayjs.dayjs()
      , wName
      , wAbsentDate;

  delIdLst.push(wCopyFileId);
  wCopyDocBody = wCopyDocBody.replaceText(`{{date}}`, today.format('YYYY年MM月DD日'));
  itemResponses.forEach(function(itemResponse){
    switch (itemResponse.getItem().getTitle()) {
      case '氏名を選択してください。':
        wName = itemResponse.getResponse();
        wCopyDocBody = wCopyDocBody.replaceText(`{{name}}`, wName);
        break;

      case '欠席日を入力してください。':
        dateF = dayjs.dayjs(itemResponse.getResponse()).format('YYYY年MM月DD日');
        break;

      case '連日にまたがって欠席する場合は、欠席期間の終了日を入力してください。': // 欠席の場合のみ対応
        if ( itemResponse.getResponse()!='' ) {
          dateT = dayjs.dayjs(itemResponse.getResponse()).format('YYYY年MM月DD日');
        } else {
          dateT = dateF;
        }
        wAbsentDate = dateF;
        wCopyDocBody = wCopyDocBody.replaceText(`{{applydatefrom}}`, wAbsentDate);
        wCopyDocBody = wCopyDocBody.replaceText(`{{applydateto}}`, dateT);
        break;

      case '届出理由を選択してください。':
        wCopyDocBody = wCopyDocBody.replaceText(`{{reason}}`, itemResponse.getResponse());
        break;

      case '欠席理由のエビデンスを必要に応じてアップロードしてください。':
        // 画像挿入位置を把握
        let insertImageIdx;
        wCopyDocBody.getParagraphs().some(function(para, pIdx){
          if (para.getText().includes(`{{evidence}}`)) {
            insertImageIdx = pIdx+1;
            wCopyDocBody.replaceText(`{{evidence}}`, '');
            return;
          }
        });
        itemResponse.getResponse().forEach(function(item) {
          delIdLst.push(item);
          // 撮影日時を印字
          let shootingTxt = getShootingDate(item);
          if (shootingTxt) {
            wCopyDocBody.insertParagraph(insertImageIdx, shootingTxt);
            insertImageIdx++;
          }
          // 画像を縮小:150px程度としたうえで配置
          let convImage = ImgApp.doResize(item, 150);
          wCopyDocBody.insertImage(insertImageIdx, convImage.blob).setWidth(150);
          insertImageIdx++;
        });
        break;

      default:
        break;
    }
  });
  wCopyDoc.saveAndClose();
  // ファイル名を変更する
  let fileName = `欠席届_${wName}_${today.format('YYYYMMDD')}`;
  wCopyFile.setName(fileName);
  // コピーしたファイルIDとファイル名、メール用の件名を返却する(あとでこのIDをもとにDOCXに変換するため)
  return [wCopyFileId, fileName, `【自動送信メール】欠席届の送信_${wName}_${wAbsentDate}`];
}

ここの部分は、Googleドキュメントであらかじめ作成した「欠席届」をコピーし、回答の情報をテンプレート内の特定の場所に置換します

こーすけ先生

これにより、「受講者氏名」「理由」「欠席期間」「欠席理由」などが欠席届に反映されます

{{date}}{{name}}等の、「可変文字部」部分をGoogleフォームの回答を反映させることができます

こーすけ先生

波括弧の部分は「可変文字部」といって、{{}}内を入力内容に書き換える魔法のような存在なんだ!詳しくは、以前の記事で説明しているよ!
是非読んでみてね!

こーすけ先生

switch文を利用することによって、「〇〇」という質問に対しての回答をGoogleドキュメント本文中の{{可変文字部}}部分に置き換える作業をしているよ!

switch文について詳しく知りたい方は、過去記事で解説しているので、確認してみてください!

【STEP4】の補足説明

こーすけ先生

補足説明が不要な方は【STEP5】までジャンプしてください!

この部分のコードは、Google ドキュメント内にある特定のテキスト({{evidence}})が含まれる段落を見つけて、その次の段落の位置を insertImageIdx に保存しています

case '欠席理由のエビデンスを必要に応じてアップロードしてください。':
        // 画像挿入位置を把握
        let insertImageIdx;
        wCopyDocBody.getParagraphs().some(function(para, pIdx){
          if (para.getText().includes(`{{evidence}}`)) {
            insertImageIdx = pIdx+1;
            wCopyDocBody.replaceText(`{{evidence}}`, '');
            return;
          }
  1. .getParagraphs() メソッドでGoogle ドキュメント内の全ての段落を取得
  2. .some() メソッドにて、配列内の各要素について指定された条件を満たすかどうかを確認
    (条件を満たす最初の要素が見つかった時点でループを終了)
  3. ループの中で、各段落のテキストに {{evidence}} が含まれているかどうかをチェック
  4. Googleドキュメント本文中に{{evidence}} が含まれていれば、その段落のインデックス pIdx に1を加えた値(次の段落の位置)を insertImageIdx に保存し、{{evidence}} を空文字列に置換える

つまり、この部分のコードは、{{evidence}} というテキストが含まれる段落の次の段落の位置を insertImageIdx という変数に保存しています

こーすけ先生

この処理によって、画像情報に撮影日時が含まれていた場合、撮影日時を{{evidence}} の次の段落に挿入する準備が整えられます

 itemResponse.getResponse().forEach(function(item) {
          delIdLst.push(item);
          // 撮影日時を印字
          let shootingTxt = getShootingDate(item);
          if (shootingTxt) {
            wCopyDocBody.insertParagraph(insertImageIdx, shootingTxt);
            insertImageIdx++;
          }
こーすけ先生

もし、画像情報の中に撮影日時があれば、画像の後ろに撮影日時がテキストで挿入されます

【STEP5】Drive APIを用いて、Exifデータを取得する

こーすけ先生

「【STEP2】Drive API を追加する」を実行していないとエラーになるので気を付けてくださいね!

function getShootingDate(id) {
  // Drive APIを用いて、Exifデータを取得する
  let metaData = DriveV2.Files.get(id).imageMediaMetadata;
  if (metaData) {
    return metaData.date;
  } else {
    return null;
  }
}

上記リファレンスの中段にimageMediaMetadataに関する記述があり、写真の撮影日時(EXIF DateTime)は、以下のように取得すればよいことが分かります

imageMediaMetadata.time	

V3はGoogleのリファレンス上は行えるのですが、imageMediaMetadataを取得しようとすると、undefinedとなるため、現状はV2を使っておいた方がいいです

こーすけ先生

「Drive API」を利用して、画像のExif情報を取得する際の留意事項について解説してるのでよかったら読んでみてください!

【STEP6】本文を書き換えたGoogleドキュメントをWord形式に変換する

こーすけ先生

私の過去記事の中では createPdfメソッドを作成し、GoogleドキュメントをPDFに変換しておりました

出張者が修正できるようにした方が融通が利くと考えられるため、ドキュメントのまま送信したい

お問い合わせ内容の要約
こーすけ先生

Googleドキュメントはあくまでもクラウド上のサービスであるため、ここではWordに変換する方法を伝授します!

公開リンクを使うと、Googleアカウントを持っていない相手にもGoogleドキュメントを共有できますが、セキュリティ上の問題もあるので、Word文書をメール送信する方が無難です

function createDocx(docId, fileName) {
  // Word変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/document/d/${docId}/export?format=docx`;

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  };
  // Word(*.docx)を作成する
  let wBlob = UrlFetchApp.fetch(wUrl, wOtions).getBlob().setName(fileName + '.docx');

  // Wordを指定したフォルダに保存する
  return DOCX_OUTDIR.createFile(wBlob).getId();
}

特段ポイントとなる箇所はなく、exportするフォーマットをdocxとすればOKです

 let wUrl = `https://docs.google.com/document/d/${docId}/export?format=docx`;
がすぴょん

format=docxとするのがポイントだね!

こーすけ先生

これだけで、Word形式に変換できるから便利だよね!

【STEP7】トリガーを設定する

こーすけ先生

左側にある時計みたいなマーク「トリガー」をクリックし、以下のように設定しましょう!

こーすけ先生

これで、フォーム回答時に、回答者宛に欠席届がメールで届くようになりました!ここまで、お疲れさまでした!

完成!

こーすけ先生

これで画像情報を読み込んだ上で、Wordファイルを回答者にメールを自動送信できるようになりました!

// 欠席届テンプレートファイル
const DOC_TEMPLATE = DriveApp.getFileById('〇〇〇〇〇〇(自分のドキュメントID)');

// ドキュメント出力先
const DOCX_OUTDIR = DriveApp.getFolderById('〇〇〇〇〇〇(自分のフォルダID)');

// 最後の削除用リスト
let delIdLst = new Array();

function gaslog_formSubmit(e) {
  let itemResponses;
  // フォームの回答をイベントオブジェクトまたはフォーム自身から取得する。
  if (e !== undefined) {
    itemResponses = e.response.getItemResponses();
  } else {
    const wFormRes = FormApp.getActiveForm().getResponses();
    itemResponses =  wFormRes[wFormRes.length-1].getItemResponses();
  }

  // 回答をもとに欠席届を作成する 
  let wFileRtn = createGDoc(itemResponses);
  // DOCX変換してファイルIDを取得する
  let wPdfId = createDocx(wFileRtn[0], wFileRtn[1]);
  delIdLst.push(wFileRtn[0]);
  delIdLst.push(wPdfId);
  
  // 今回はDOCXファイルを添付してメールを送信する
  sendEmailEx(
    e !== undefined ? e.response.getRespondentEmail() : '(eが未定義の場合に送信されるメールアドレス)'
    , wFileRtn[2]
    , `受講生より欠席届が送信されました。`
    , {attachments: DriveApp.getFileById(wPdfId).getBlob()}
  );

  // 各種保存したファイルを削除する
  delIdLst.forEach(function(delId){
    DriveApp.getFileById(delId).setTrashed(true);
  });
}

function createGDoc(itemResponses) {
  // テンプレートファイルをコピーする
  const wCopyFile = DOC_TEMPLATE.makeCopy()
        , wCopyFileId = wCopyFile.getId()
        , wCopyDoc = DocumentApp.openById(wCopyFileId); // コピーしたファイルをGoogleドキュメントとして開く
  let wCopyDocBody = wCopyDoc.getBody()                 // Googleドキュメント内の本文を取得する
      , today = dayjs.dayjs()
      , wName
      , wAbsentDate;

  delIdLst.push(wCopyFileId);
  wCopyDocBody = wCopyDocBody.replaceText(`{{date}}`, today.format('YYYY年MM月DD日'));
  itemResponses.forEach(function(itemResponse){
    switch (itemResponse.getItem().getTitle()) {
      case '氏名を選択してください。':
        wName = itemResponse.getResponse();
        wCopyDocBody = wCopyDocBody.replaceText(`{{name}}`, wName);
        break;

      case '欠席日を入力してください。':
        dateF = dayjs.dayjs(itemResponse.getResponse()).format('YYYY年MM月DD日');
        break;

      case '連日にまたがって欠席する場合は、欠席期間の終了日を入力してください。': // 欠席の場合のみ対応
        if ( itemResponse.getResponse()!='' ) {
          dateT = dayjs.dayjs(itemResponse.getResponse()).format('YYYY年MM月DD日');
        } else {
          dateT = dateF;
        }
        wAbsentDate = dateF;
        wCopyDocBody = wCopyDocBody.replaceText(`{{applydatefrom}}`, wAbsentDate);
        wCopyDocBody = wCopyDocBody.replaceText(`{{applydateto}}`, dateT);
        break;

      case '届出理由を選択してください。':
        wCopyDocBody = wCopyDocBody.replaceText(`{{reason}}`, itemResponse.getResponse());
        break;

      case '欠席理由のエビデンスを必要に応じてアップロードしてください。':
        // 画像挿入位置を把握
        let insertImageIdx;
        wCopyDocBody.getParagraphs().some(function(para, pIdx){
          if (para.getText().includes(`{{evidence}}`)) {
            insertImageIdx = pIdx+1;
            wCopyDocBody.replaceText(`{{evidence}}`, '');
            return;
          }
        });
        itemResponse.getResponse().forEach(function(item) {
          delIdLst.push(item);
          // 撮影日時を印字
          let shootingTxt = getShootingDate(item);
          if (shootingTxt) {
            wCopyDocBody.insertParagraph(insertImageIdx, shootingTxt);
            insertImageIdx++;
          }
          // 画像を縮小:150px程度としたうえで配置
          let convImage = ImgApp.doResize(item, 150);
          wCopyDocBody.insertImage(insertImageIdx, convImage.blob).setWidth(150);
          insertImageIdx++;
        });
        break;

      default:
        break;
    }
  });
  wCopyDoc.saveAndClose();
  // ファイル名を変更する
  let fileName = `欠席届_${wName}_${today.format('YYYYMMDD')}`;
  wCopyFile.setName(fileName);
  // コピーしたファイルIDとファイル名、メール用の件名を返却する(あとでこのIDをもとにDOCXに変換するため)
  return [wCopyFileId, fileName, `【自動送信メール】欠席届の送信_${wName}_${wAbsentDate}`];
}

function getShootingDate(id) {
  // Drive APIを用いて、Exifデータを取得する
  let metaData = DriveV2.Files.get(id).imageMediaMetadata;
  if (metaData) {
    return metaData.date;
  } else {
    return null;
  }
}

function createDocx(docId, fileName) {
  // Word変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/document/d/${docId}/export?format=docx`;

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  };
  // Word(*.docx)を作成する
  let wBlob = UrlFetchApp.fetch(wUrl, wOtions).getBlob().setName(fileName + '.docx');

  // Wordを指定したフォルダに保存する
  return DOCX_OUTDIR.createFile(wBlob).getId();
}

function sendEmailEx(_recipient, _subject, _body, _option) {
  // 引数の内容でメールを下書き保存する
  const mailDraft = GmailApp.createDraft(_recipient, _subject, _body, _option);
  // 下書き保存したメールIDから下書きを取得し、メール送信を依頼する
  GmailApp.getDraft(mailDraft.getId()).send();
}

この関数を「▷実行」すると、Googleフォームで回答したメールアドレス宛に、Word文書が添付されたメールが送られてきます

こーすけ先生

ちゃんとWordファイルとして添付されてますね!

まとめ

今回は「画像を含むGoogleフォームの回答をGASで取得し、Googleドキュメントに反映後、Word文書にしてメールを自動送信する」というテーマで解説を行いました

GASは非常に便利ですが、今回の記事(GASで画像のExif情報を扱う)のように、使い方にはクセがあることがあります

こーすけ先生

GASで「Drive API」を利用する場合は仕様に留意してください

ぜひ、皆さんも実業務での活用に取り組んでみてください
引き続き、GASを楽しんでいきましょう!!

こーすけ先生

X(旧:Twieer)にて、ブログの更新やQiita記事の更新、GAS情報をお届けしますので、是非フォローしてください!

100日後に起業する公務員

こーすけ先生

退職までの漫画をゆるくXにて更新中!
是非フォローしてね!

この記事が気に入ったら
いいね または フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

GASの人。ITベンダSEとして12年勤務し、民間、金融、官公庁の現場を一通り経験済。html、css、JavaScript、Java、PHPも分かります。最近は専らGASで小規模アプリケーションを頻繁に作成しています。GASのことなら何でもお任せあれ!現在は公務員として働きながら、起業に向けて着々と準備中です!

コメント

コメントする

CAPTCHA


目次