Google Apps Scriptで差し込み印刷的な何か【GAS】

Google ドキュメントは、正直な所、他のワープロソフトに比較すると、かなり機能的には貧弱です。また、スプレッドシートと比較すると、スクリプトも難解な構造になっており、サンプルも少ないです。そんなGoogleドキュメントですが、標準搭載されていない機能で要望が高いのが、差し込み印刷です。標準で搭載されていても良いのにと思うのですが(それっぽいのが2023年5月に差込機能は装備されました。)

しかし、Googleドキュメント類は印刷に関するメソッドがないので、スクリプトから印刷はできません。よって、印刷の一歩手前までをなんとか出来ないだろうか?また、同様のテクニックを使って、これまでスプレッドシートでテンプレートを作り、書類を作ってたものを、Googleドキュメントで出来ないだろうか?と考えた結果、ようやく形になりました。今回はダイアログではなく、サイドバーで実装しています。

ちなみに既にアドオン形式では、DocumentMerge by PandaDocというものがありましたが、英語UIな上に画像類などはどうもテンプレートからコピーしてくれないみたいなので、若干不便です。但し、データ元であるスプレッドシートのシートの指定とカラムの指定が柔軟でとても良く出来ているアドオンです。

今回しようするファイル等

サンプルコードのPicker APIは古いコードとなっているので以下のエントリーの最新のコードより改造を加えて下さい。また、2024年4月、これを更に発展させてPDFを連続で差込生成することが出来るようになりました。

Pickerでファイルやフォルダを選択する画面を装備する

Google Apps Scriptで差込で直接PDFを生成する【GAS】

ソースコード

正直言って、結構な量のコードになっているので、いずれ最適化しないといけないなと思いつつ、手が付けられずにいたので、主要な部分のコードだけ。残りは上記のファイルからコピーして除いてみて下さい。リッチなデザインとユーザビリティもちょこっと考慮してるので、実際にはもっと少ないコードで書けると思います。大分無駄にメソッド呼び出してる面もありますし。

GAS側コード

//置換用コンソールをsidebarで表示する
function openSidebar() {
  var ui = DocumentApp.getUi();
  var html = HtmlService.createHtmlOutputFromFile('index').setTitle('差込印刷').setSandboxMode(HtmlService.SandboxMode.IFRAME);
  ui.showSidebar(html);
}
 
//差込データをドキュメントのカーソル位置に挿入する
function insertPara(num){
  var Properties = PropertiesService.getScriptProperties();
  var spfile = Properties.getProperty("sheet");
  var sheet = SpreadsheetApp.openById(spfile);
  var lastcolumn = sheet.getLastColumn();
  var range = sheet.getActiveSheet().getRange(1,1,1,lastcolumn).getValues();
  var temp = 1;
  
  //インサートする文字列を取得しておく
  var inserttext = range[0][num];
  
  //document情報を取得しておく
  var doc = DocumentApp.getActiveDocument();
  var body = DocumentApp.getActiveDocument().getBody();
  var ui = DocumentApp.getUi()
  
  //カーソル状態か選択状態かによって、作業をわける
  try{
    var selection = doc.getSelection();
    var selectedElements = selection.getSelectedElements();
    //var selectedElement = selectedElements[0];
  }catch(e){
    temp = 0;
  }
  
  if(temp != 0){
      ui.alert("選択状態では、挿入が出来ません");
  }
  
  try{
    //カーソル状態時の作業
    var body = doc.getCursor();
    body.insertText("§" + inserttext + "§");
  }catch(e){
    ui.alert("選択状態では、挿入が出来ません");
  }
}
 
//テンプレートファイルを指定のフォルダにコピーする
function copyme(){
  var Properties = PropertiesService.getScriptProperties();
  var targetfolder = Properties.getProperty("folder");
  docname = DocumentApp.getActiveDocument().getName() + "_" + getDate();
  var docs = DocumentApp.getActiveDocument().getId();
  var ui = DocumentApp.getUi();
  
  if(targetfolder == undefined){
    ui.alert("フォルダ格納先が指定されていませんよ。");
  }
 
  var folder = DriveApp.getFolderById(targetfolder);
  var file = DriveApp.getFileById(docs);
  var id = file.makeCopy(docname,folder).getId();
  return id;
}
 
//現在のテンプレ内容をスプレッドシートの指定レコード数分、ページコピーし、差込データを置き換える。
function copyDoc(id,data,datalength) {
  //ページ複製用データを取得
  var sourceDoc = DocumentApp.openById(id).getBody();
  var totalElements = sourceDoc.getNumChildren();
  var copydocs = sourceDoc.copy();
  
  //ページ複製と置換用の変数取得
  var Properties = PropertiesService.getScriptProperties();
  var spfile = Properties.getProperty("sheet");
  var sheet = SpreadsheetApp.openById(spfile);
  var lastcolumn = sheet.getLastColumn();
  var range = sheet.getActiveSheet().getRange(1,1,1,lastcolumn).getValues();
  var ui = DocumentApp.getUi();
  var targetdocs = sourceDoc;
 
  //とりあえず改行コードを入れておく
  targetdocs.appendPageBreak();
  
  for(var z = 1;z<datalength;z++){
    //ページ複製ルーチン
    if(z != 1){
      for( var j = 0; j < totalElements; ++j ) {
        var element = copydocs.getChild(j).copy();
        var type = element.getType();
  
        if( type == "PARAGRAPH" ){
          targetdocs.appendParagraph(element);
        }
        else if( type == "TABLE"){
          targetdocs.appendTable(element);
          }
        else if( type == "LIST_ITEM"){
          targetdocs.appendListItem(element);
        }
      }
      
      targetdocs.appendPageBreak();
    }  
 
    //ワード置換ルーチン
    for(var p = 0;p<lastcolumn;p++){
      var choicecol = String("§" + range[0][p] + "§");
      var tempdata = data[z][p];
      targetdocs.replaceText(choicecol, tempdata);
    }
  }
}

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 type="text/javascript" src="https://www.google.com/jsapi"></script>
<script>
    google.load("visualization", "1", {packages:["corechart"]});
    google.setOnLoadCallback(test);
    
    function test(){
      google.script.run.withSuccessHandler(onSuccess).waitid();
      google.script.run.withSuccessHandler(onSuccess2).waitid2();
      google.script.run.withSuccessHandler(onSuccess3).columnget();
    }
    
    $(function(){
        $('#btnOK').click(function(){
            google.script.run.fileman();
        });
    });

    $(function(){
        $('#btnOK2').click(function(){
            google.script.run.folderman();
        });
    });
    
    $(function(){
        $('#btnOK3').click(function(){
            google.script.run.insertdoc();
        });
    });
    
    function onSuccess(data){
        document.getElementById("test").innerHTML = "データファイル:" + data;
    }
    function onSuccess2(data){
        document.getElementById("test2").innerHTML = "格納先:" + data;
    }
    function onSuccess3(data){
        var tempstr = JSON.parse(data);
        document.getElementById("htmler").innerHTML = tempstr;
    }

    function clickfunc(obj) {
      var num = Number(obj.title);
      google.script.run.insertPara(num);
      return false;
    }
</script>
<style>
    div.wasabi{
      padding:3px 5px;
      border-color:#0B0099;
      border-width:0 0 1px 7px;
      border-style:solid;
      background:#F8F8F8; 
    }
    div.kousin {
      overflow-y: auto;
      width:95%; height:200px;
      padding:3px;
      border:1px solid #000;
      color:#000000;
      background-color:#ffffff;
      line-height:1.0em;
      margin-left: auto;
      margin-right: auto;
    }
    div.boxContainer {
      overflow: hidden;
      width:100%;
    }
    .box {
      float: left;
      width: 100%;
      margin-right:5px;
    }
    #linkArea{
      position: relative;
      width: 100%;
      height: 100%;
      border-color: #999999;
      border-style: none;
    }
    #linkArea a{
      display: block;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
</style>
<div class="wasabi">使用する差込データ</div>
<p><button id="btnOK" class="action">データ選択</button></p>
<div id="test"></div><p>
<div class="wasabi">ファイル格納場所</div><p>
<p><button id="btnOK2" class="action">フォルダ選択</button></p>
<div id="test2"></div><p>
<div class="wasabi">差込文字列の挿入</div><p>
<div class="kousin"><p><div id="htmler"></div></div><p>
<div class="wasabi">差込の実行</div><p>
<p><button id="btnOK3" class="create">RUN</button></p>

実行と結果

使い方

このスクリプトは印刷はできませんが、印刷一歩手前の書類を作る為のものです(Cloud Printがあれば、印刷まで実行できます)。よって、ユーザがスクリプトで書類を生成したら、やることは印刷だけです。差込印刷を行う為には、以下の手順を踏みます。

  1. サンプルドキュメントがテンプレートとなります。メニューより「▶差込印刷」より【差込メニュー】を実行し、サイドバーを表示します。
  2. 使用する差込用のデータ(スプレッドシート)を指定します。Google Pickerが起動するので、使用するスプレッドシートを指定します。
  3. 次に生成するドキュメントを格納するドライブの場所を指定します。同じくGoogle Pickerが起動するので、フォルダを指定します。
  4. 差込文字列の挿入は2.で指定したスプレッドシート(まだシート名の指定は出来ません)の1枚目のカラムを自動で読み取って表示してくれます。よって、カラムを2行にするだとか、オカシナ整形をすると読めなくなります。
  5. 差し込みたい位置にカーソルを移動し、4.で表示されたカラムリストをクリックすると、その場所に特別な文字列が挿入されます。これは削除したり変形させてはいけません。
  6. 最後に、RUNボタンを押すと、テンプレートを複製しスプレッドシートデータのレコード数分ページを作り、文字列を置換します。
  7. 最後に生成したドキュメントへのリンクが表示されるダイアログが出てくるので、リンクをクリックして印刷するだけ。
  8. 使用する差込データのスプレッドシートは、予め書式を「書式なしテキスト」にしておくと良いです。特に日付などのデータはそのままだと、表示してる形式ではない形式でデータが取得されてしまいます。書式なしテキストなら、見た目のままのデータで差し込んでくれます。
  9. Google Picker APIを利用していますので、クラウドコンソールでデベロッパーキーを生成し、Picker APIを有効にする必要性があります。

実行結果

Google Apps Scriptで差込印刷を実現する

ポイント

印刷メソッドがない分、テンプレートを下敷きに、1つのドキュメント内にページという形で、印刷物の元を作るのが味噌です。テンプレートの中身を画像から文字列まで含めて、ドキュメントを複製し、その後、replacetextにて文字をスプレッドシートのカラムに対応するものをもって置換をするわけです。実際には、テンプレートデータの塊を複製しては、置換し、複製しては置換する作業を繰り返しています。

ページはスプレッドシートのレコード分だけ用意しますし、挿入するカラム文字についても、自動的に挿入するスクリプトが仕込まれているので、ユーザはマウスだけで操作が完結することができます。また、サイドバーを用いてるので、ダイアログと違い、常に作業しながら行えるので便利ですね。こういうユーティリティ的なシーンではサイドバーはオススメです。

メソッド類ですが、正直まだ全体像を把握していません。もっと良いやり方があるのかもしれません。実用には十分だとは思います。今回は§記号を代用して、完全一致で置き換えるようにしています。

その後、replaceTextの正規表現は、通常のJavascriptで用いられてる正規表現とは異なり、RE構文と呼ばれるもので書かないといけないみたいです。[テスト]⇒置き換え文字としたい場合には、sourceDoc.replaceText(“(\\[)(テスト)(\\])”,”置き換え文字”);としなければならないようです。

パラグラフのコピー元ですが、テンプレートのBodyをコピーしたものから複製しています。でないと、新たにコピーが追加されて置換を行ったものを複製してしまう為です。オリジナルをキープしておくわけです。また、新たにDocumentを作らずファイルの複製を行わせてる理由は、ずっと放置されてるバグなのですが、そのままオリジナルファイルからのパラグラフコピーだと画像が出ません。ファイルの複製なら問題ないので、複製しBodyをコピーし、それをレコード数分差し込みつつ置換しています。Document.Element.Typeが画像も何もかもがParagraphで返って来るので判別ができないのもそれが理由です。tableなどは逆にちゃんと帰ってくるので、今回のような個別のappendが必要というちぐはぐ具合がなんとも言えません。

関連リンク

Google Apps Scriptで差し込み印刷的な何か【GAS】” に対して2件のコメントがあります。

  1. 波多野 より:

    質問です。

    GoogleSpreadSheet、GoogleDocumentも超初心者です。
    会社でGoogleWorkSpaceを使用しています。

    実行しようとすると【使用する差込データ】は【データ選択】、
    【ファイル格納場所】は【フォルダ選択】が表示されています。

    どちらも、別ウィンドウで再度選択するボタンが表示されますが、
    別ウィンドウ内でクリックしても、ファイルやフォルダを選択する画面が
    表示されません。

    あまりに初心者かと思いますが何卒ご指導賜りますよう
    よろしくお願い申し上げます。

    1. officeの杜 より:

      波多野様

      この記事の中にもありますが、別途Picker APIのAPI Keyをコード内に記述しないと選択画面が出てきません。

      また、現在、Picker APIの記述方法が異なってるので、以下のエントリーより最新の表示方法に装備を変更してみてください。

      https://officeforest.org/wp/2018/05/20/picker%e3%81%a7%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab%e3%82%84%e3%83%95%e3%82%a9%e3%83%ab%e3%83%80%e3%82%92%e9%81%b8%e6%8a%9e%e3%81%99%e3%82%8b%e7%94%bb%e9%9d%a2%e3%82%92%e8%a3%85%e5%82%99%e3%81%99/

コメントを残す

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

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