Google Apps ScriptでPDFを作成する【GAS】

Google Apps Scriptそのものには、印刷に関するメソッドがありません。しかし、ファイルのエクスポートではPDF形式がサポートされています。これは、URLを組み立てて投げることで、プログラム側からも利用出来るようになっています。

PDF化は何かと利用価値はありますが、これが実装できると、自動的にPDF化してメールに添付して送る、ドライブの所定の場所に自動的に格納するなどが可能になります。また、スプレッドシート

固有の機能ではないので、他のドキュメントも利用することが出来ますよ。

※旧方式のPDF出力では、現在のような詳細な出力指定は出来ません。新方式のPDF出力では詳細な出力が可能であり、またカスタム改ページや、指定したシートのみPDF化なども可能になっています。

使用するメソッド・クラス、準備するもの

  1. 今回使用するスプレッドシート
  2. UrlfetchAppクラスの各種メソッド
  3. DriveAppクラスのgetAsメソッド

ソースコード

メソッドを使って簡単にPDF化して取得

この方法は最も手っ取り早いPDF化の方法です。しかし、制御が出来ません。よって、細かいPDF化の為のオプションが存在しないので、そのドキュメントおよびスプレッドシートがまるごとPDFになるだけです。今回はスプレッドシートをPDF化して、ファイルを生成し、そのままログインユーザ名を取得して、操作者にそのままPDFをメールに添付して送り付けます。

//ファイル自体をざっくりPDF化する
function quickpdf(){
   //自分のIDと名前を取得する
   var sheet = SpreadsheetApp.getActiveSpreadsheet();
   var pdfname = "スプレッドシート.pdf";
   
   //PDFに変換する
   var pdf = sheet.getAs('application/pdf');
   
   //ドライブにファイルを生成する
   DriveApp.createFile(pdf).setName(pdfname);
   
   //作成したPDFファイルをメールに添付して送る
   var mail = GetUser();
   var subject = 'PDF送るよ'
   var body = 'まぁ、送るんで見てくれ'
   MailApp.sendEmail(mail, subject, body, {attachments:pdf});
   
   //メッセージ表示
   SpreadsheetApp.getUi().alert("PDFをメールで送ったよ");

}
  • 今回はフォルダの指定はしていないので、ルートディレクトリにPDFが生成されます。
  • 生成しなくてもメールに添付してそのまま送りつけてしまってもOKです。
  • getAsメソッドの引数にPDFのMIMETYPEを渡しています。これが最も重要です。
  • 特定のシートのみPDF化が出来ません。
  • 何も他にオプションが無いので、スプレッドシートではセルの枠線などもバッチリ映り込んでしまいますので、ドキュメント向けです。

URLを組み立ててPDFをエクスポートして取得

getAsメソッドだと自由が効かないので、使い勝手が悪いです。そこで、URLを組み立ててPDF化する方法があります。特定の1シートのみをPDF化することが可能です申請書類などをスプレッドシートで生成し、PDF化してメールで送る場合にはこの手法が最も良い方法でしょう。

ソースコード

//特定のシート(現在見てるシート)のみPDF化する
function makepdf() {   
  //アクティブシートのIDとGIDを取得する
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var sheetID = sheet.getSheetId();
  var key = ss.getId();
  var ui = SpreadsheetApp.getUi();
  var token = ScriptApp.getOAuthToken();

  //URLの組み立て
  var url = "https://docs.google.com/spreadsheets/d/" + key + "/export?gid=" + sheetID + "&format=pdf&portrait=false&size=A4&gridlines=false&fitw=true"
  
  //PDF生成するURLをfetchする
  var pdf = UrlFetchApp.fetch(url, {headers: {'Authorization': 'Bearer ' +  token}}).getBlob().setName("test" + ".pdf");
    
  //作成したPDFファイルをメールに添付して送る
  var mail = GetUser();
  var subject = 'PDF送りましたよっと。'
  var body = 'テストPDFの送信パート2'
  MailApp.sendEmail(mail, subject, body, {attachments:pdf});
  
  ui.alert("画像PDFが送信されました。")

}
  • 今回は、アクティブシートをA4用紙横印刷でグリッドライン無しのフィットページにてPDFを作成しています。
  • スクリプトトリガーなどで実行する場合は、実行者の権限で動かせます。また、ウェブアプリケーション上でもこのコードは実行者の権限設定ができますので、匿名の者であっても、ウェブアプリケーション化してしまえば、コードを実行することが可能です。
  • アクセス者の権限で呼び出す場合、必ずその人のアカウントで認証が必要です。またこの場合、実行時のQuotaは一人単位なので、設置者権限だとQuotaに引っかかる場合は、アクセスしてきてるユーザの権限で動かすと良い。
  • 但し上記の場合、PDFをドライブに生成する時は、そのフォルダへのアクセス権限が必要。
  • urlfetchappは連続で呼び出すと、途中でアクセス過多で503エラーや403エラー、429エラーが出るので、沢山呼び出す時は、10秒くらいUtilities.sleep入れてウェイトする必要があります。一日に呼び出せるQuotaにも注意が必要です。PDFのファイルサイズにも注意。
  • ちなみにAPI呼び出しすぎの場合の403エラーは、User Rate Limit Execeedというエラーがそれです。間隔を空けましょう。

書き込みが間に合わない時の処置

データの書き込みを行い、其の流れでPDFを生成する場合には、データの書き込みが間に合わずPDFが生成されてしまうケースが、しばしばあります。このままでは、折角のPDFもデータが欠けた状態で作成されてしまうので、以下のような無限ループを利用して、「最終書き込みセル」を確認しながら、スクリプトをウェイトさせます。
//最終rangeに書き込みがされていない場合、無限ループ
var rangechk = SpreadsheetApp.openById(filesId).getRange("comments").getValue();
while (rangechk == ""){
    rangechk = SpreadsheetApp.openById(filesId).getRange("comments").getValue();
}
最終書き込みをするであろう場所を今回は、名前付き範囲で指定しています。
その中身が空の状態の時は無限ループします。値が確認できたら、PDF生成ルーチンへ進むといった感じです。

また、データ取得前にSpreadsheetApp.flush()をすれば即データが取得できる場合もありますので、活用してみてください。

URLオプション

URLに続けてオプションを付加すると、Chromeでの印刷時と同じオプションを付け加えることが出来る。重要度の高い物を赤字、次に必要な物を青字で説明してみた。

  • key : スプレッドシートのIDを指定する
  • gid : シートIDを指定する。シート毎に付いているもので、URLに入ってる。このオプション指定をしないと、全ページ印刷の対象となる。省略可。
  • format : 出力フォーマットを指定。PDFなので、format=pdfとして引数を取る。以前は、fmcmdで引数に数字を取っていた。

formatの引数に指定できるものは以下の数種類。要するに標準で装備してる「形式を指定してダウンロード」の対象物が取れるようになっているわけです。今回はスプレッドシートのみ紹介しますが、ドキュメントやスライドなどでは若干使えるもの使えないもの、違うものがあります(例えば、ドキュメントの場合、4.はodsではなくodtとなる)。

  1. pdf : 今回のテーマ。PDF形式で出力します。
  2. xlsx : Microsoft Excel形式で出力します。出力は、xlsx形式なので注意
  3. csv : UTF-8でエンコードされたCSVファイル。正直あまり使わない。Excelにインポートする時は、拡張子をtxtに変更してから取り込みましょう。
  4. ods : OpenDocument形式。但しこれは、スプレッドシートの場合の形式です。OpenOffice.orgのcalcの拡張子を指定すると覚えておけばOK。使わないと思うけれど。
  5. tsv : タブ区切りのテキストファイル形式です。
  6. zip : シートをHTML形式で出力しZIPで固めてエクスポート。その為、このオプションだけは、gid=の指定を入れてしまうとエラーになります。単純にexport?format=zipだけしか指定してはいけません。

その他のオプション指定項目は以下の通り。よく使うものだけ青色で表示。margin系は4方向すべて指定しなければなりません。現在、複数のsheetidを指定したPDF出力法がわからない・・・・カンマ区切りで渡してもだめでした。

  1. size : 出力するサイズ。A4と入れるとA4形式となる。番号での指定もできるようだ。また、10x20とするとインチ指定ですが横x縦のカスタムサイズ指定で出力可能。
  2. fzr : 各ページに行見出しを含めるかどうか。trueで含める、falseで含めない。
  3. fzc : 各ページに列見出しを含めるかどうか。trueで含める、falseで含めない。
  4. portrait : trueで縦出力、falseで横出力
  5. fitw : ページをフィットさせるかどうか。trueでフィット、falseで原寸大。
  6. gridlines : グリッドラインの有無。trueで有り、falseで無し。
  7. printtitle : ドキュメントのタイトルの有無。trueで有り、falseで無し。
  8. sheetnames : シート名の有無。trueで有り、falseで無し。
  9. pagenum : ページ番号の指定。CENTERと入れると真ん中に表示される。
  10. attachment : 不明。getFileというコマンドと連携してるとかしてないとか。
  11. locale : 言語の指定。必要ないと思う。
  12. range : シート単位ではなく、指定範囲内を印刷。名前付き範囲を指定したり、A1:D5の範囲指定ならば、A1%3AD5(%3Aでコロン)といったURLエンコードした範囲を与える
  13. widget : rangeの指定時のみ使用可能。今見ている範囲内をtrueとすると印刷範囲とする変なオプション。使いドコロ不明。
  14. top_margin : 上の余白(cmで指定)
  15. bottom_margin : 下の余白
  16. left_margin : 左の余白
  17. right_margin : 右の余白
  18. scale : 1= 標準100%, 2= 幅に合わせる, 3= 高さに合わせる,  4= ページに合わせる
  19. printnotes:コメントを印刷するかどうか。trueで印刷する。
  20. ir:不明
  21. ic:不明
  22. r1c1 : R1C1方式でのrangeの指定。&c1=0&r1=0&c2=4&r2=42といった感じに指定する。
  23. horizontal_alignment:水平方向の位置。LEFT/CENTER/RIGHTを指定
  24. vertical_alignment:垂直方向の位置。TOP/MIDDLE/BOTTOMを指定
  25. pageorder:複数ページの印刷時にどちらの方向から印刷するか。1で前から後ろ、2で後ろから前。

とりわけ、特定の範囲を指定してのPDF化の場合には、URLエンコードが必要なので、以下のように予め範囲指定をURLエンコードしておいて、URL組み立てでつないで上げれば楽です。encodeURIComponentでそれが可能です。

//範囲指定をURLエンコード
var range = encodeURIComponent("A1:E21");

//URLの組み立て
  var url = "https://docs.google.com/spreadsheets/d/" + key + "/export?gid=" + sheetID
              + "&format=pdf&portrait=false&size=A4&gridlines=false&fitw=true"
              + "&range=" + range;

こうすることで、指定のシートのA1:E21の範囲をA4横でグリッドライン非表示、自動でフィットするようにPDF化してくれます。

暫定:特定の複数のページをPDF化する

現在、gidは1個しか指定ができません。印刷オプション的には特定のシートを複数選択して、それだけを印刷可能なので何らかのオプションが隠れていると思うのですが、指定方法が不明です。

そこで暫定的な方法としては、PDF化をする際に印刷対象にしないスプレッドシートを隠してからPDF化を実行すると、非表示にしたシートは印刷対象から外れます。非表示にするコードは以下の通り。このコードだと、水根貨物線跡というシートが一旦非表示になってからPDF化されて、再度表示しています。全3枚中2枚がPDF化されて印刷されます。gid指定は行っていません。エラー発生時を想定して、try{}catch(e){}で、エラー時には再度、showするコードを入れておくと良いと思います。

//現在のブックをPDF化(特定シートのみ)
function multiplepdf() {   
  //アクティブシートのIDとGIDを取得する
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var sheetID = sheet.getSheetId();
  var key = ss.getId();
  var ui = SpreadsheetApp.getUi();
  var token = ScriptApp.getOAuthToken();
  
  //特定のページを非表示にする
  ss.getSheetByName("水根貨物線跡").hideSheet();

  //URLの組み立て
  var url = "https://docs.google.com/spreadsheets/d/" + key + "/export?format=pdf&portrait=false&size=A4&gridlines=false&fitw=true"
  
  //PDF生成するURLをfetchする
  var pdf = UrlFetchApp.fetch(url, {headers: {'Authorization': 'Bearer ' +  token}}).getBlob().setName("test" + ".pdf");
  
  //シートを再度表示する
  ss.getSheetByName("水根貨物線跡").showSheet();
    
  //作成したPDFファイルをメールに添付して送る
  var mail = GetUser();
  var subject = 'PDF送りましたよっと。'
  var body = 'テストPDFの送信パート4'
  MailApp.sendEmail(mail, subject, body, {attachments:pdf});
  
  ui.alert("画像PDFが送信されました。")
}

新方式対応のPDFエクスポート

基本的な作り方

URLを組み立ててPDFをエクスポートする手段は、Google Apps初期の頃からあった古い手段です。現在、Google Spreadsheetなどのファイル⇒ダウンロードで使用されているのはPOST通信でパラメータを送る手段になっているようです。ダウンロード実行時にChrome Devtoolで確認すると、そのパラメータらしきものが、Form Dataに存在します。

※ちょっとリンクの表示が遅い・・・

図:現在のエクスポート用パラメータ

この内容をロシアのとある人が解析しウェブサイトで情報とサンプルコードを公開しています。StackOverFlowでも紹介されています。カスタム改ページについても実行ができるようになりました。

//直接生成ダウンロードのメインルーチン
function myExportPDF(){
  var as = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = as.getActiveSheet();
  exportPDF(as.getId(), //スプレッドシートのID
    [
      [sheet.getSheetId().toString(), //現在開いてるシートのID(以下のパラメータがない場合、現在のシート全体
       0,                             //最初の行(0で一番最初の行から、1で2行目という具合)
       160,                           //最後の行(160で160行目までを印刷対象とする)
       0,                             //最初の列(0で一番最初のA列から。1でB列から)
       8                              //最後の列(8でH列までを意味する)
      ]
    ],
    [
      0,         //コメントの表示・非表示
      null,
      1,         //グリッド線の表示・非表示(1で非表示)
      0,         //ページ番号の表示
      0,         //スプレッドシートのファイル名を表示
      0,         //シート名の表示
      0,         //現在の日付を表示
      0,         //現在の時刻を表示
      1,         //固定行の繰り返し(ヘッダー行を固定するかどうか)
      1,         //固定列の繰り返し
      1,         //ページ順ソート(2で降順、1で昇順)
      1,
      null,
      null,
      1,         //水平方向位置(3:中央、2:右揃え、1:左揃え)
      1          //垂直センターリング(1:上端、2:中央、3:下端)
    ],
    [
      "A4",      //用紙サイズ(カスタムサイズの場合には、8.5x13といった指定。8.5が幅で13は高さ。単位はインチ)
      1,         //垂直ページの方向(0:横、1:縦)
      2,         //スケール - (1:標準100%、2:幅に合わせる、3:高さに合わせる、4:ページに合わせる、5:カスタム数値)
      1,         //カスタムスケール(1で100%, 0.5で50%)
      [
        0.75,    //上のマージン(インチ)
        0.75,    //下のマージン
        0.7,     //左のマージン
        0.7      //右のマージン
      ]
    ]
  );
}

//PDFをPOST通信で生成する(直接ダウンロード)
function exportPDF(ssID,source,options,format){
  var dt=new Date();
  var d=encodeDate(dt.getFullYear(),dt.getMonth(),dt.getDate(),dt.getHours(),dt.getMinutes(),dt.getSeconds());
  var pc=[null,null,null,null,null,null,null,null,null,0,
          source,
          10000000,null,null,null,null,null,null,null,null,null,null,null,null,null,null,
          d,
          null,null,
          options,
          format,
          null,0,null,0];
 
 //PDF生成用URL
 var url = "https://docs.google.com/spreadsheets/d/"+ssID+"/pdf?esid="+(Math.round(Math.random()*10000000))+"&id="+ssID+"&a=true&pc="+JSON.stringify(pc)+"&gf=[]";
 var js = "<a href='" + url + "'>PDFダウンロード</a>";
  
  var html = HtmlService.createHtmlOutput(js)
    .setHeight(100)
    .setWidth(200)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
  SpreadsheetApp.getUi().showModalDialog(html, "Save To PDF"); 
}

//日付をエンコードして生成する
function encodeDate(yy,mm,dd,hh,ii,ss){
  var days=[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  if(((yy % 4) == 0) && (((yy % 100) != 0) || ((yy % 400) == 0)))days[1]=29;
  for(var i=0; i<mm; i++)dd+=days[i];
  yy--;
  return ((((yy * 365 + ((yy-(yy % 4)) / 4) - ((yy-(yy % 100)) / 100) + ((yy-(yy % 400)) / 400) + dd - 693594) * 24 + hh) * 60 + ii) * 60 + ss)/86400.0;
}
  • 直接ダウンロードは、実行するとユーザのアクションを待たずにダイレクトにダウンロードが始まります。
  • 直接ダウンロードの場合、UrlfetchAppを利用していないので、制限に掛からず実行が可能です。
  • メール添付やドライブ保存はPOST通信をUrlfetchAppを利用しているので、URL組立方式と同様です。
  • nullやパラメータ値が入ってるものの、まだ未解明部分のパラメータがあるみたい・・・
  • パラメータは、pcと書かれている変数の配列部分がそれに該当します。
  • 上記のコードはGASを実行するとwindow.openで新しいタブでファイルがダイレクトダウンロードされ、google.script.host.closeで閉じれる変わった仕様です。実行しただけでダウンロード出来るので、他でも利用できそうなテクニックですね。
  • Google Chrome 83.x以降は、GASからwindow.openで開いてもURLは実行されなくなってしまいました・・・ので、GAS実行でダイレクトダウンロードは出来ないです(ついでにURL表示してクリックしてもダウンロードされずエラー)。。。こちらにそのIssueが登録されています。詳細な情報はこちらにあります。GAS側に.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)をつけるべしとありますが、つけてもallow-downloadがiframeに付与されないので、GAS側の対応待になってるようです。(この問題は解消されました)
  • 2020/11/20 - Google側仕様変更で、PDF生成URLにランダムな数値をつけるesidというフラグが追加されていないと動作しないということで、本家コードに追記がされました。

複数ページを指定して出力

旧方式の場合、指定したシートだけを選択してPDF化が、シートの非表示というテクニックを使ってでないと出力が出来ませんでした。新方式では、配列に複数のシートIDを指定することができるので、非常に便利です。

//複数シートを指定して出力する新方式
function myExportPDF2(){
  //出力するシートを指定
  var as = SpreadsheetApp.getActiveSpreadsheet();
  var sheet1 = as.getSheetByName("パッションフルーツ");
  var sheet2 = as.getSheetByName("桜島");

  //PDF出力
  exportPDF(as.getId(), //スプレッドシートのID
    [[sheet1.getSheetId().toString()],[sheet2.getSheetId().toString()]],  //複数シートを指定
    [0,null,1,0,0,0,0,0,1,1,1,1,null,null,1,1],
    ["A4",1,2,1,[0.75,0.75,0.7,0.7]]
  );
}
  • サンプルシートの1枚目と3枚目を指定して、1つのPDFとして出力させています。
  • 配列で複数シートを指定すれば飛び飛びでの指定も可能となっています。

カスタム改ページを指定してPDF化

基本的なコード

StackOverFlowにて、カスタム改ページ時のパラメータが解析され、今回の新方式のPDFでそれらを指定して出力する事ができるようになりました。今回のコードで言えば、exportPDFのパラメータのpc変数の中にあるformat指定の後の部分がそれになります。ただし、指定方法がとてもむずかしいので、固定的に改ページ指定するようなケースで利用することになると思います。

//カスタム改ページで出力する
function myExportPDF3(){
  //出力するシートを指定
  var as = SpreadsheetApp.getActiveSpreadsheet();
  var sheet1 = as.getSheetByName("改ページ");

  //PDF出力
  exportPDF2(as.getId(), //スプレッドシートのID
    [[sheet1.getSheetId().toString()]],  
    [1,null,0,0,0,0,0,0,1,1,2,1,null,null,2,1],
    ["A4",0,6,1,[0.75,0.75,0.7,0.7]]  //A4横で指定しています。
  );
}

//カスタム改ページの場合の指定方法
function exportPDF2(ssID,source,options,format){
  var dt=new Date();
  
  //sourceから値だけを取り出す
  var sval = source[0][0];

  //出力指定
  var d=encodeDate(dt.getFullYear(),dt.getMonth(),dt.getDate(),dt.getHours(),dt.getMinutes(),dt.getSeconds());
  var pc=[null,null,null,null,null,null,null,null,null,0,
          source,
          10000000,null,null,null,null,null,null,null,null,null,null,null,null,null,null,
          d,
          null,null,
          options,
          format,
          null,0,
          [[sval,[[17,17]],[[8,8]]]]]; //ここがカスタム改ページの指定場所
 
  //PDF生成用URL
  var url = "https://docs.google.com/spreadsheets/d/"+ssID+"/pdf?esid="+(Math.round(Math.random()*10000000))+"&id="+ssID+"&a=true&pc="+JSON.stringify(pc)+"&gf=[]";
  var js = "<a href='" + url + "'>PDFダウンロード</a>";

  //ダイアログを表示
  var html = HtmlService.createHtmlOutput(js)
    .setHeight(100)
    .setWidth(200)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
  SpreadsheetApp.getUi().showModalDialog(html, "Save To PDF"); 
}
  • 今回は改ページがわかりやすいように、横向き印刷としています。そのため、myExportPDF3にて、A4横で指定を掛けています。
  • exportPDF2にて出力をするのですが、変更箇所はpc変数の一番最後。通常ここはnull,0として終わっているのですが、ここが変更箇所になります。
  • sourceはシートのIDです。それに加えて配列でcm指定だと思われる2箇所の改ページの場所を縦横で入れてると思われる数値を指定。
  • これらは、Google Chromeのデベロッパーツール、ネットワーク内のpdf出力と思われるURL内にあるform dataにて確認が出来ます。
  • 実際にこれでPDF出力を行うと本来は1ページ目に2つ画像が入るところが、1ページ目は1個のみ、残りは2ページ目以降に収まるように印刷をしてくれます。

図:Developer Toolのネットワークにて解析

図:これをGASで実行するのが今回の目的

改ページ指定が複雑でわかりにくい

前述のコードのうち、改ページの指定の部分が非常にわかりにくいです。ここをより詳細に解説すると、以下のような形になります。([[sval,[[17,17]],[[8,8]]]]])と記述のある場所です。

[
  [
    sheetid,
    [[17,17],[30,30]],
    [[8,8],[8,8]]
  ]
]
  • 2次元配列でこの部分は構成されています。
  • sheetid, 1つ目の配列、2つ目の配列で1つの塊となります。
  • 各配列内の数字は列番号や行番号になります。
  • 1つ目の配列は[17,17]と[30,30]というのが入っていますが、それぞれ同じ数字を指定することになっています。
  • そして、1つ目の配列は行番号を指定します。よって、17行目と30行目で改ページを入れるという意味になります。
  • 2つ目の配列は[8,8]と[8,8]というのが入っていますが、こちらも同じ数字を指定することになっています。
  • そして、2つ目の配列は列番号を指定します。よって、8列目で改ページを入れるという意味になります。
  • 1つ目の配列の1つ目の項目である17と、2つ目の1つ目の8が対になりますので、これで17行目+8列目の範囲で改ページを入れますという意味になります。
  • [8,8]が2つある理由はこれは、1ページ目も2ページ目もどちらも8列目で改ページを入れるという意味になります。
  • それぞれ必要な数だけ、1つ目、2つ目の配列に改ページを入れる行番号・列番号を入れることで複雑な改ページを実現することが可能です。

図:改ページを想定してる範囲

図:うまく1つのシートを3分割に改ページ出来た

新方式での作成をちょっと手軽にしてみた

とは言え、前述のコードは旧式のそれよりも複雑で非常に扱いにくい。そこで、もう少し扱いやすいのように整理して、ある程度のパラメータ類はルールに基づいて生成するようにラッピングしてみました。

以下のうち、パラメータを構築する関数と個別パラメータの値をセットする関数はそのままコピペして利用し、リクエストする本体の関数側でJSON形式でまとめておけば、新方式でPDFが生成できるように組んであります。

リクエストする本体の関数

後述の2つの関数はコピペで利用しますが、この本体のPDF化を指示する関数で重要なのは、リクエストを構築する「json」の作成部分が肝になります。残りは決まり事なので、そのまま利用することでドライブにファイルが生成されるようになります。

注意点としては

  • PDF化対象にするシートは、sheetlistパラメータに対して複数指定することが可能です。それぞれのシートIDが必要になります。
  • リクエストを構築するJSONの部分では、sheetlistは基本はsheetidだけの2次元配列でOK。fistline等のオプションは必要に応じて追加する。
  • また、同様にカスタム改ページをセットしたい場合には、pagebrake(カスタム改ページ)の項目をそれぞれに追加する必要があります。
  • カスタム改ページを利用する場合はautoheightオプションは6で指定する必要があります。
  • pagesizeオプションは必須です。デフォルトはA4で指定されています。
//PostでPDF化を実行
function createPostPDF(){
  //出力先フォルダを指定する
  let targetfolder = "ここに出力するフォルダのIDを指定する";
  let folder = DriveApp.getFolderById(targetfolder);

  //スプレッドシートを取得する
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let ssid = ss.getId();
  let filename = ss.getName();

  //PDF化対象にするシート
  let sheet1 = ss.getSheetByName("パッションフルーツ").getSheetId().toString();
  let sheet2 = ss.getSheetByName("水根貨物線跡").getSheetId().toString();

  //リクエストを構築する
  let json = {
    "sheetlist" : [
      {
        "sheetid" : sheet1,
        "firstline" : 0,
        "lastline" : 160,
        "firstcolumn" : 0,
        "lastcolumn" : 8,
        "pagebrake" : [
          {
            "start":5,
            "end": 8
          },
          {
            "start": 11,
            "end": 8
          }
        ]
      },
      {
        "sheetid" : sheet2,
      }
    ],
    "pagesize": "A4",
    "autoheight":6
  }

  //PDFオプションを構築
  let pdfopt = createpdfoption(json);

  //リクエストオプション
  let options = {
    'method': 'post',
    'payload': pdfopt,
    'headers': {Authorization: "Bearer " + ScriptApp.getOAuthToken()},
    'muteHttpExceptions': true
  }

  //esidを生成
  let esid = (Math.round(Math.random()*10000000));

  //リクエストURLを構築
  let url = "https://docs.google.com/spreadsheets/d/"+ssid+"/pdf?esid=" + esid +"&id="+ssid;

  //HTTPリクエスト
  let blob = UrlFetchApp.fetch(url, options).getBlob();
  
  //ドライブにファイルを生成
  folder.createFile(blob).setName(filename +".pdf");
}

上記のうち、必要最低限の指定で良いというケースであるならば、以下のようなコードになります。sheetlistに対してsheetidのみを列挙し、その他のオプションはpagesizeのみを指定する形になります。これで指定シートのみがA4で印刷され、各設定値はすべてデフォルト値が利用されます。

//リクエストを構築する
  let json = {
    "sheetlist" : [
      {
        "sheetid" : sheet1
      },
      {
        "sheetid" : sheet2,
      }
    ],
    "pagesize": "A4"
  }
パラメータを構築する関数

リクエストする本体の関数側から送られてきたリクエスト用のJSONの値を元に、PDF化する場合のパラメータを整理して生成する関数になります。後述の個別パラメータの値をセットする関数を利用して1つずつ構築しています。

また、カスタム改ページについても対応しています。仕様としてのポイントは以下の通りになります。

  • JSONのうちsheetlistのパラメータとpagesizeの2つを元に構築します。
  • シートは特定シートのみのPDF化と複数ページに対応しています。
  • 各シートの印刷開始・印刷終了の行列番号の指定が無い場合は該当のシートは全て印刷となります。
  • カスタム改ページ(pagebrakeの部分)の指定が無い場合にはnullとして指定されます。
  • pagebrake内のstartは改ページを入れる行番号です(何行目という指定)
  • pagebrake内のendは改ページを入れる列番号です(何列目という指定)
  • 上記の例で言えば、5行目8列目で1つ改ページ、11行目8列目で1つ改ページという2つの改ページを指定しています。
  • shownote以降の個別オプション等はsheetlistの個別の配列内に指定されたパラメータがある場合にはそれを利用し、無い場合にはデフォルト値が利用されます(sheetlist内で指定してはいけません)
  • カスタム改ページを利用する場合には、autoheightの値は6を指定する必要があります。
  • 整形されたパラメータデータがpayloadとしてリクエストする本体の関数へ返却されます。
//PDF出力オプションを組み立てる
function createpdfoption(json) {
  //リクエスト用の日付を生成する
  let dt=new Date();
  let d=encodeDate(dt.getFullYear(),dt.getMonth(),dt.getDate(),dt.getHours(),dt.getMinutes(),dt.getSeconds());
 
  //シートリストから印刷対象データを生成
  let pagebrake = [];
  let sheetman = json["sheetlist"];
 
  //シート個別のデータ
  let sheetset = [];
  let startarr = [];
  let endarr = [];
  for(let i = 0;i<sheetman.length;i++){
    //レコードを一個取り出す
    let rec = sheetman[i];
 
    //一時配列を用意する
    let temparr = [];
 
    //sheetidをpushする
    temparr.push(rec.sheetid);
 
    //4つのオプション値がある場合
    if(rec.firstline !== undefined){
      temparr.push(rec.firstline)
    }
 
    if(rec.lastline !== undefined){
      temparr.push(rec.lastline)
    }
 
    if(rec.firstcolumn !== undefined){
      temparr.push(rec.firstcolumn)
    }
 
    if(rec.lastcolumn !== undefined){
      temparr.push(rec.lastcolumn)
    }
 
    //temparrをsheetsetに追加する
    sheetset.push(temparr);
 
    //pagebrakeがある場合の処理
    if(rec.pagebrake !== undefined){
      //pagebrakeの配列を取得する
      let brakeman = rec.pagebrake;

      //カスタム改ページ用データを構築する
      let pgarr = [];
 
      //シートIDをpushする
      pgarr.push(rec.sheetid);
 
      //改ページ用の2つの配列
      let startpoint,endpoint;

      //改ページデータを構築する
      for(let j = 0;j<brakeman.length;j++){
        //レコードを一個取り出す
        let brec = brakeman[j];

        //startとendを取り込む
        startpoint = [brec.start,brec.start];
        endpoint = [brec.end,brec.end];

        //2つの配列に追加する
        startarr.push(startpoint);
        endarr.push(endpoint);
      }

      //stendをpgarrにpushする
      pgarr.push(startarr);
      pgarr.push(endarr);

      //pagebrakeにpushする
      pagebrake.push(pgarr)
    }
  }

  //pagebrakeが空っぽの場合
  if(pagebrake.length == 0){
    pagebrake = null;
  }
 
  //値をはめこむ
  let array = [
    sheetset,
    [
      setJsonOption(json,"shownote",0),       //ノートの表示・非表示
      null,
      setJsonOption(json,"showgrid",0),       //グリッド線の表示・非表示(1で表示)
      setJsonOption(json,"showpagenum",0),    //ページ番号の表示(1で表示)
      setJsonOption(json,"showfilename",0),   //スプレッドシートのファイル名を表示(1で表示)
      setJsonOption(json,"showsheetname",0),  //シート名の表示(1で表示)
      setJsonOption(json,"shownowdate",0),    //現在の日付を表示(1で表示)
      setJsonOption(json,"shownowtime",0),    //現在の時刻を表示(1で表示)
      setJsonOption(json,"fixheader",0),      //固定行の繰り返し(ヘッダー行を固定するかどうか。1で固定化)
      setJsonOption(json,"fixheadrotation",0),//固定列の繰り返し(1で繰り返す)
      setJsonOption(json,"pagesort",2),       //ページ順ソート(1で降順、2で昇順)
      1,
      null,
      null,
      setJsonOption(json,"horizoncenter",0),  //水平センターリング(1:左 2:右 3:センタリング)
      setJsonOption(json,"virticalcenter",0)  //垂直センターリング(1:上端 2:センタリング 3:下端)
    ],
    [
      setJsonOption(json,"pagesize","A4"),    //用紙サイズ
      setJsonOption(json,"direction", 0),     //垂直ページの方向(0で縦向き、1で横向き)
      setJsonOption(json,"autoheight", 2),    //高さを揃える(カスタム改ページを利用する場合は6を指定する)
      setJsonOption(json,"customscale", 1),   //1が100%, 指定時は0.7などの小数点を使う
      [
        setJsonOption(json,"margintop", 0.75),    //上のマージン(インチ)
        setJsonOption(json,"marginbottom", 0.75), //下のマージン
        setJsonOption(json,"marginleft", 0.7),    //左のマージン
        setJsonOption(json,"marginright", 0.7)    //右のマージン
      ]
    ],
    pagebrake
  ]
 
  //payloadを構築
  let pc=[null,null,null,null,null,null,null,null,null,0,
          array[0],
          10000000,null,null,null,null,null,null,null,null,null,null,null,null,null,null,
          d,
          null,null,
          array[1],
          array[2],
          null,
          0,
          array[3],
          0
        ];
  
  let payload = "a=true&pc="+JSON.stringify(pc)+"&gf=[]"
 
  //payloadを返却する
  return payload;
}
個別パラメータの値をセットする関数

与えられたパラメータ名と値を元にリクエストするパラメータ構築する関数に返すだけの関数。パラメータ名の指定が無い場合は、デフォルト値を返すようにしています。リクエストする本体の関数でパラメータ指定が無い場合にはデフォルト値が使われる仕組みです。

//JSONの値から対象の値が入ってるかチェック
function setJsonOption(json,keyname,defaultvalue){
  //対象のキーの値を取得し判定する
  let target = json[keyname];
  
  //undefinedだった場合オプション指定が無いのでdefaultvalueを利用する
  if(target == undefined){
    target = defaultvalue
  }

  //値を返す
  return target
}

パスワード付きPDFを作りたい

Google Apps Scriptだけでは暗号化PDFは作る手段がありません。しかし、Node.jsのモジュールが使えれば作成する事ができます。自分もようやく最近作れるようになったのですが。。。

Node.jsが使える環境がGoogle Cloud Platformには複数あります。其のうちの一つである「Google Cloud Functions」にて、Node.js + hummus-recipeにて実現が可能です。無料枠がかなり広いので、超低コストでGoogle Apps Scriptの機能を拡張し、パスワード付きPDFを作る事が可能になります。

詳細については、以下のエントリーを参考に挑戦してみてください。サンプルも用意してあります。ちなみに、このモジュール、パスワード付きPDFを作るだけでなく、結合や加工(テキスト挿入やイメージ追加)、挿入や追加、分割やオーバーレイなど様々な事ができます。実装次第では一気にPDF作成の可能性が広がります。

Google Apps ScriptでCloud Functionsの関数を実行する【GAS】

外部のPDFファイルを自分のドライブにコピーしたい

外部で公開してるPDFファイルを、自分のDriveに定期的にコピーしたい要望はちょいちょい伺います。これは非常に簡単で、スクリプトトリガーなどと組み合わせると、自動で定期的に自分のドライブにつっこんでくれるので、取りこぼすことなく全自動でPDFを入手することが可能です。

//メニュー
function onOpen() {
    var ui = SpreadsheetApp.getUi();
    ui.createMenu('🐟 PDFゲッター')
    .addItem('PDFを取得する', 'myFunction')
    .addToUi();
}

function myFunction() {
  //対象となるPDF
  var url = "ここに外部のPDFファイルへの直リンクを入れる";
  
  //日付を生成する
  var dateman = new Date();
  var namedate = dateman.getFullYear(dateman) + "_" +  dateman.getMonth(dateman) + "_" + dateman.getDate(dateman) + ".pdf";
  
  //PDFを取得する
  var pdf = UrlFetchApp.fetch(url).getBlob().setName(namedate);
  
  //Driveにファイル生成
  var pdfid = DriveApp.getFolderById("ここに生成先フォルダのIDを入れる").createFile(pdf).getId();
  
  //作成したPDFファイルをメールに添付して送る
  var mail = GetUser();
  var subject = 'PDF送りましたよっと。'
  var body = 'テストPDFの送信パート2'
  MailApp.sendEmail(mail, subject, body, {attachments:pdf});

}

//現在のユーザのアドレスを取得
function GetUser() {
  var objUser = Session.getActiveUser();
  return objUser.getEmail();
}
  • ついでに、自分自身へメールに添付して送るコードもついています。
  • 上記コードはgetBlobで取得したものをattachmentで添付してしまっていますが、bodyの中に生成したPDFへの共有リンクなどを貼り付けて送るほうが、ドライブ容量無駄遣いせずに済みます。共有リンクは、https://drive.google.com/file/d/ファイルのID/viewとなります。
  • Google Driveのファイルの直リンクで入れるとダウンロードになります。https://drive.google.com/uc?export=download&id=ファイルのIDとなりますので、上記コードの場合、pdfidをファイルのIDに入れて、bodyに1文追加すればOK。

複数のPDFを結合する

Cloud Functionsを使わないと出来ないと思っていたPDFの結合ですが、Google Apps ScriptのV8対応によって、スクリプトだけで結合が出来るようになったようです。こちらのサイトに掲示されてるコードを利用する事で、個別PDFファイルの配列を渡すだけで、個別PDFのあるフォルダ内に結合したPDFファイルが生成されるようになります。ベースになってるコードはこちらのコード

実際にこのフォルダに3個のPDFファイルをアップロードしておき、スクリプト内でフォルダIDを指定し、投げてみた所、バッチリ結合したファイルが生成されました。同じスクリプトにはSplitPdfという関数(PDFの分割用かな?)もあるのですが、こちらはうまく動作せず。

このコードはV8を有効化しないと利用できません。ひょっとしたらV8対応によってこれまでGASでは実現できなかったものが、GASだけで色々と実現出来るようになってるかもしれませんね。

2024年4月現在、この手法は一部文字化けや真っ白のPDFが交じるなど色々問題が出ていたので、PDFLibを利用する手法を推奨しています。以下のエントリーに別途まとめました。

Google Apps ScriptでPDFを結合する方法【GAS】

//結合するPDFが入ってるフォルダ
var mergeman = "ここにPDFファイルの入ってるフォルダのIDを入れる";

//PDFを結合する
function pdf_merge() {
  //UIを取得
  let ui = SpreadsheetApp.getUi();

  //フォルダを取得する
  let folder = DriveApp.getFolderById(mergeman);
  
  //フォルダ内のPDFをリスト化
  let pdflist = folder.getFiles();

  //リスト用の配列
  let mergearr = [];
  
  //拡張子の指定(正規表現)
  let pattern = /.*\.pdf$/;
  
  //結合後ファイル名の指定
  let filename = "結合後のPDF.pdf"
  
  //mergearrにPDFデータをpush
  while (pdflist.hasNext()){
    //ファイルを取得
    var file = pdflist.next();
    
    //拡張子をチェック
    if (pattern.test(file.getName())){
      mergearr.push(file);
    }
  }
  
  //PDF結合実行
  let mergedFile = mergePdfs(folder, filename, mergearr);
  
  //終了メッセージ
  ui.alert("PDFの結合が完了しました。")

}

セル内に画像表示時の挙動

スプレッドシートでは現在、セル内に画像表示は2種類のパターンが利用できます。1つはimage関数を使った手法(但し、誰でもアクセス出来る必要がある)。もう一つがセル内に埋め込みする方法。

この時、このシートを印刷すると以下の挙動になります。

  1. 埋め込みの場合はきちんとPDF化で表示可能です
  2. image関数の場合、Google Drive上の直リンクの場合、画像が公開されていてもPDF化で表示が出来ません
  3. また、画像非公開の場合はimage関数の場合そもそもシート上でも表示出来ません
  4. 但し、外部のサーバで公開されている画像の場合は、image関数の場合であってもPDF化で表示可能です。

よって、このような画像埋め込みのケースのシートをPDF化して画像が見える状態にしたい場合は、image関数を使わずに画像を外部サーバで公開するか?埋め込みを利用するようにしましょう。これは新旧両方式共に同じです。

埋め込みに関しては最近ようやくメソッドも実装されたので動的に埋め込んで印刷も可能になりました。

Google Apps Scriptでスプレッドシートに画像を貼り付ける【GAS】

図:image関数の場合の挙動が怪しい

フォームPDFの読み書き

入力のできるフォーム付きPDFファイル、この操作はGoogle Apps Script単体では実現出来ません。しかし、以下のエントリーにあるようにPDF-LIBとfontkitを利用することで、GASからフォームの値を取得したり、フォームに値をセットできるようになりました。

フォーム付PDFを配布して回収データの集計の自動化や、役場への申請用PDF(就労証明書等)の作成の自動化などがこれで可能になります。

Google Apps ScriptでPDFフォームを操作する【GAS】

ボタンをクリックでダウンロード

これまでの説明は、スプレッドシートをPDFへ変換してUrlfetchAppで取得後にメールに添付して送るというものでした。しかし、メールではなくボタンを押したら直接ダウンロードしたいという要望があると思います。その場合、直接GASのコマンドを叩いてダウンロードは出来ませんが、例えばSidebarやダイアログにボタンを表示して、クリックするとPDFでダウンロードされるみたいな事は可能です。

GAS側コード

//PDF化してダウンロードするダイアログを表示する
function downloadlink(){
  //アクティブシートのIDとGIDを取得する
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getActiveSheet();
  var sheetID = sheet.getSheetId();
  var filename = ss.getName();
  var key = ss.getId();
  
  //作成するPDFのファイル名
  var name = filename + ".pdf";
 
  //PDF生成するURLをfetchする
  var pdf = "https://docs.google.com/spreadsheets/d/" +key+ "/export?gid="+sheetID+"&format=pdf&portrait=false&size=A4&gridlines=false&fitw=true";
  
  //配列にデータを入れる
  var array = [pdf,name]
  
  //blobデータをHTML側に送る
  return JSON.stringify(array);

}

※現在見てるスプレッドシートをPDF化するリンクとファイル名を取得して、HTML側へ返しています。

HTML側コード(サイドバー)

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<link rel="stylesheet" type="text/css" href="https://officeforest.org/wp/library/cssman/insert.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script>
 
    //リンクとファイル名をGAS側から受け取る
    google.script.run.withSuccessHandler(onSuccess).downloadlink();
 
    function onSuccess(data){
      var array = JSON.parse(data);
      var downlink = array[0];
      var filename = array[1];
      
      
      $( '#btnOK' ).click( function () {
          var link = document.createElement( 'a' );
          link.href = downlink;
          link.download = filename;
          link.click();
      });
    }
 
</script>
<style>
・・・・省略・・・・
</style>
<div class="wasabi"><img src='https://officeforest.org/wp/library/icons/pdficon.gif' border='0'>  PDFダウンロード</div>
<p><button id="btnOK" class="action">ダウンロード</button></p>

※google.script.run.withSuccessHandlerにてGAS側へリンクとファイル名を要求。返って来た値を元にjQueryでボタンにそのリンクとファイル名を設定するというものです。ボタンをクリックするとリンクが叩かれてファイルがダウンロードされます。

関連リンク

Google Apps ScriptでPDFを作成する【GAS】” に対して6件のコメントがあります。

  1. sunsun7clap より:

    分かりやすい解説ありがとうございます。
    大変恐縮ですが、googleフォームに紐ついた、スプレッドシートで集計等したシートをこちらのページにあるようにpdf化するようにしました。
    pdf化まではできたのですが、画像表示がうまく出来ず、4個の画像のうち反映できるのが1個だけだったりと、うまく読み取ってくれません。
    この集計等したシートにはimage関数を使っており、googleドライブから画像をセル内に表示するようにしています。

    シートの読み込み自体はしっかりできていることを確認できてますが、image関数のところだけうまく行きません。
    sleepしたり、flushしたりもいろいろ試していますが上手くいかないのですが、何かご存知であれば教えて頂けますと大変嬉しいです。

    1. officeの杜 より:

      sunsun7clap様

      image関数のケースと画像埋め込みのケースで検証をしてみました。
      記事にも新しいエントリーとして、「セル内に画像表示時の挙動」として検証結果を追記しました。

      結論から言えば、image関数を使ったケースでPDF化で表示出来たのは、外部のサーバに公開してる画像のケースのみで、Google Drive上の画像はシート状で表示されていても、PDF化では表示されません。

      また、画像埋め込みのケースはきちんと表示出来るので、可能な限り埋め込みを利用すると良いでしょう(こちらはGoogle Drive上の画像でもOK)

      1. sunsun7clap より:

        Officeの杜様

        早急な回答ありがとうございます!
        やはりGoogle drive上の画像をImage関数による表示では、pdf化とは相性が悪かったのですね。。。
        仰るように外部サーバーから読み込むことで動きました!
        埋め込みと削除を繰り返す形にはなりますが、これで日々使っていこうと思います!
        ありがとうございました!

        1. officeの杜 より:

          普通に手動で印刷時は出るのですが、一度GETのリクエストで外に出ると、動かないようですね。

          ちょっと手間が増えますが、埋め込みのメソッド活用で回避出来るので、ぜひチャレンジしてみて下さい

  2. Chi-papa より:

    新方式対応のPDFエクスポートで、複数ページを指定して出力する場合に、シート数が変化する場合、出力したいシートIDをどのように指定するとよいでしょうか。シートIDを文字列に変換して、指定してもうまくいきません。
    よろしくお願いします。

    1. officeの杜 より:

      Chi-papaさん

      新方式のPDF生成の場合のシートIDはソースコードにもありますが、getSheetIdで取得した値の分だけ配列を用意して、構築が必要です。
      サンプルの中のmyExportPDF2関数にあるように、[[sheet1.getSheetId().toString()],[sheet2.getSheetId().toString()]],として2枚のシートだけをといった形で構築する必要があります。

      シート一覧と各シートID、シート名の取得のコードを用意して、あげると良いでしょう

      //シート一覧とシートIDを調べる
      function getsheetlist(){
        //スプシを取得
        let ss = SpreadsheetApp.getActiveSpreadsheet();
      
        //シートの一覧を取得する
        const sheets = ss.getSheets();
      
        //シート一覧のシートIDを取得する
        for (const sheet of sheets) {
          //シートIDを取得する
          let sheetid = sheet.getSheetId();
      
          //シートIDを出力する
          console.log(sheetid)
      
          //シート名も出してみる
          console.log(sheet.getName())
        }
      }
      

sunsun7clap へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)