スプレッドシートのデータをCSSを使って好きなデザインで印刷(PDF化)する

## はじめに

スプレッドシートを印刷するのは簡単なんですが、スタイリングはあまり自由じゃありませんよね。
かといえど、複数のデータはやはりスプレッドシートで管理する方が楽。。。。
ということで、今回はGoogleAppsScriptを使って、好きなスタイルのページを印刷できるようにしていこうと思います。

今時印刷？！と言われるかもしれませんが、PDF化としても利用できるので気になった方はぜひご覧くださいませ

## 記事にするほどのことなのか？

今回はGoogleAppsScriptを使って、WebAppとして表示した画面を印刷できるようにしていこうと思います。
これを聞くだけで、
「`Ctrl+P`or`Command+P`で印刷できるじゃん？」
って方はいるでしょう。。。

ところがどっこい、GoogleAppsScriptのWebAppってiframeを使って公開してるんですよね。

![スクリーンショット 2023-07-04 22.20.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/c44daf1f-4be3-5a1f-521c-f38440892b27.png)

この状態で印刷するとこうなってしまうのです。

![スクリーンショット 2023-07-04 22.25.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/09e787fd-09ad-3331-90ad-18b462d5fa2e.png)


ということで、この制限から逃れつつ、好きなCSSを使いつつ印刷していこうと思います。

## トライ

ってことでやっていきましょー

### 印刷できるようにする

iframeだからできねーぞって書きました。
その通りです。なので、Client側のJavaScriptの機能である`URL.createObjectURL`を使って回避します。

説明はMDN様にお任せいたしますが、
メモリを使って一時的なURLを生成することができます。
devToolを使った時に、動画のURLが`blob:https://...`のような形式をしていることを見ている方がいるといいなーという感じ。

https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL_static

ってことで、順序よくいきましょう。

前提として、WebAppのデプロイ方法は省きます。
わからない方は下記を参照。

https://programming-zero.net/gas-webapp-deploy/#outline_1__2_2

1. `URL.createObjectURL`を使って、新しいタブで開けるようにする
    ```javascript:code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }

    ```
    ```html:top.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <style>
          .text-center {
            text-align: center;
          }
        </style>
      </head>
      <body>
        <div class="text-center">
          <button id="openPage">印刷ページを表示する</button>
        </div>
        <script>
          document.querySelector("#openPage").addEventListener("click", () => {
            const blob = new Blob(['hello world'],{type:"text/plain"});
            const url = URL.createObjectURL(blob);
            window.open(url);
            URL.revokeObjectURL(url);
          });
        </script>
      </body>
    </html>
    ```
    `code.gs`については、説明いらないですね。
    GETメソッドでアクセスが来たら、HTMLのページを返すおまじないのようなものです
    `top.html`についても、簡単に。
    ボタンをクリックするイベントをJavaScriptで記載しています。
    最初はHTMLとか関係なしに、テキストで`hello world`が表示されるようにURLを作成して、新しいタブで開かれます。
    その後、`URL.revokeObjectURL`でメモリを破棄しています。
    そのため、新しく開かれたタブは正常に開かれますが、再度読み込むと`ファイルにアクセスできませんでした`と表示されます。

    | top.html | クリック後 | blobページを再度読み込み |
    | -------- | ------- | -------------------- |
    | ![スクリーンショット 2023-07-04 22.47.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/ead91437-eb09-0e52-23e3-972c8c5a8782.png) | ![スクリーンショット 2023-07-04 22.48.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/60e62737-4fc5-2ad0-b612-db4f54d6be5f.png) | ![スクリーンショット 2023-07-04 22.49.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/e00400be-a4eb-3fa7-3789-d2b5d94e7ecf.png) |


1. GASで用意したHTMLが表示されるようにする
    先ほどの例では、普通のテキストで表示させていましたが、CSSで印刷するためにもHTMLで表示できるようにしましょう。
    先ほどのコードの箇所を下記のように変えても良いのですが、自分が編集したいときに見ずらいったらありゃしないですよね。(CSSも使いづらいし)
    ```diff_javascript
    - const blob = new Blob(['hello world'],{type:"text/plain"});
    + const blob = new Blob(['<h1>hello world</h1>'],{type:"text/html"});
    ```

    ということで、テンプレートはGASのHTML上で管理できるようにしましょう。
    新しくプリント用のテンプレートを用意します
    (日本語を使う場合は念のためにmetaタグでutf-8を指定しておきましょう)
    ```html:print.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="UTF-8">
      </head>
      <body>
        <h1>プリントアウト用</h1>
      </body>
    </html>
    ```

    そして、`top.html`でクリックした際にテンプレートを取得して、その内容を表示できるようにしましょう。
    ```javascript:code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    function getTemplate(){
      return HtmlService.createHtmlOutputFromFile("print").getContent();
    }
    ```
    ```html:top.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="UTF-8">
        <style>
          .text-center {
            text-align: center;
          }
        </style>
      </head>
      <body>
        <div class="text-center">
          <button id="openPage">印刷ページを表示する</button>
        </div>
        <script>
          document.querySelector("#openPage").addEventListener("click", () => {
            google.script.run.withSuccessHandler((templateText) => {
              const blob = new Blob([templateText],{type:"text/html"});
              const url = URL.createObjectURL(blob);
              window.open(url);
              URL.revokeObjectURL(url);
            }).getTemplate();
          });
        </script>
      </body>
    </html>
    ```
    `code.gs`には新しく`getTemplate`関数を追加して、`print.html`の内容を返す内容にしましょう。ただし、返り値はStringが好ましいので、`getContent`で返します。
    `top.html`には新しく`google.script.run`を使って、GASの`getTemplate`関数を呼び出します。
    `withSuccessHandler`を使って、返り値を引数に、blobを作成します。
    流れとしては同じですが、今回MIME Typeは`text/html`を指定することで、HTMLページを開くことができます

    完成したら読み込んでみましょう！
    クリックすると数秒のラグが発生します（GASの関数を呼び出しているため）が、
    クリック後は下記のような画面が表示されると思います。

    ![スクリーンショット 2023-07-04 23.07.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/b223a169-8585-1c0b-e502-02f9ad9ab974.png)

    後ほど説明しますが、印刷用のCSSを当てるとこんな感じで、1枚目、2枚目と表示することができます
    ![スクリーンショット 2023-07-04 23.09.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/0e14fb99-eafc-4c3b-0a80-b58fb3785044.png)

    ということで、印刷できるようになりましたね！

### スプレッドシートからデータを表示してみよう

次のステップです。CSSを使っても良いのですが、先にスプレッドシートからデータを取り出せるようにしましょう。

どのような印刷物にするか考えていなかった、、、
適当にチケット形式でいきましょう！（笑）
ささっとこんな感じのデータを作ってみました。

![スクリーンショット 2023-07-04 23.18.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/f5d422ff-ea41-e237-ce1b-a3f3d4f88ddf.png)

1. データを取得するGASコードを書く
    ```javascript:code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    function getTemplate(){
      return HtmlService.createHtmlOutputFromFile("print").getContent();
    }
    
    function getSheetData(){
      const sheet = SpreadsheetApp.openById("1YtiB4LDCl8jAaXJdbHhG6OKsCWYpUNxSXQ38RZX6zEA").getSheetByName("ticket-data");
      const data = sheet.getRange(`A2:C${sheet.getLastRow()}`).getValues()
        .map(line=>{
          const [ticketId, name, price] = line;
          return {ticketId, name, price}
        });
      return data;
    }
    ```
    新しく`getSheetData`関数を作成します。
    使いやすいように、getValuesで取得したデータをJSON形式で返せるように少しいじります。

1. データをHTMLに反映させる

    `getTemplate`の関数の中で取得できるようにしましょう。
    ```javascript:code.gs
    function doGet(){
      return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
    }
    
    function getTemplate(){
      const data = getSheetData();
      const html =  HtmlService.createTemplateFromFile("print");
      html.data = data
      return html.evaluate().getContent();
    }
    
    function getSheetData(){
      const sheet = SpreadsheetApp.openById("1YtiB4LDCl8jAaXJdbHhG6OKsCWYpUNxSXQ38RZX6zEA").getSheetByName("ticket-data");
      const data = sheet.getRange(`A2:C${sheet.getLastRow()}`).getValues()
        .map(line=>{
          const [ticketId, name, price] = line;
          return {ticketId, name, price}
        });
      return data;
    }
    ```

    最初に`getSheetData`を呼び出して、スプレッドシートのデータを取得します。
    今回は`HtmlService.createTemplateFromFile`を使って、テンプレートで呼び出します。
    HTMLのdataに、シートのデータを挿入し、
    `evaluate`でデータをHTMLに変換、`getContent`でテキストにします。


    今度は`print.html`で`data`を表示させるように変更しましょう
    とりあえず名前だけ表示させます。
    ```html:print.html
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="UTF-8">
      </head>
      <body>
        <h1>プリントアウト用</h1>
        <? for( const {ticketId, name, price} of data){ ?>
        <p><?= name ?></p>
        <? } ?>
      </body>
    </html>
    ```

    この状態で実行してみましょう。
    ボタンを押した後、新しいタブが開くのが少し遅くなります。
    エラーが出なければ新しいタブが自動で開かれるでしょう。（今回エラーが出た時の対処はしていないので悪しからず）

    この後CSSを適用していくといい感じのページを作成することができます！
    ![スクリーンショット 2023-07-04 23.50.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/5862bd66-86d2-b53a-100e-59554d2c57e8.png)

### CSSを凝ってみる

ってことで、やることはほぼ終わりました。
CSSを凝ろう！的な内容を書いていますが、そんな技術ありませんのでデザインはネットから拾ってきます

普段はTailwindCSSやCSS in JSを書いてる人間なので、CSSを当てはめるにあたってのClassの命名規則がやばいのは目を瞑っていただけたらと。。。
デザイン力も皆無のためこんな見た目ですが(￣▽￣;)
![スクリーンショット 2023-07-05 0.46.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/1cbf0887-a8df-fa8e-2315-6046ec2645dc.png)

```html:print.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <style>
      @media print {
        body {
          -webkit-print-color-adjust: exact;
          color-adjust: exact;
        }

        .page {
          height: 100vh;
          page-break-after: always;
          display: flex;
          justify-content: center;
          align-items: center;
        }
      }

      @media screen {
        .page {
          padding: 30px 0px;
        }
      }

      .ticket {
        width: 400px;
        height: 100px;
        margin: auto;
        border: 2px solid black;
        border-radius: 10px;
        display: flex;
        align-content: center;
      }

      .ticket__left {
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 10px 0px 0px 10px;
        padding: 10px 20px;
        background-color: rgba(93, 184, 93, 0.5);
        border-right: 2px black dashed;
      }

      .ticket__right {
        display: flex;
        flex-grow: 1;
        flex-direction: column;
        justify-content: center;
        padding: 10px;
      }

      .ticket__event {
        font-size: large;
        text-decoration: solid underline;
      }

      .ticket__name {
        display: flex;
        align-items: center;
      }

      .ticket__ticketId {
        display: flex;
        align-items: end;
        justify-content: end;
        font-size: small;
        color: gray;
      }
    </style>
  </head>
  <body>
    <? for( const {ticketId, name, price} of data){ ?>
    <div class="page">
      <div class="ticket">
        <div class="ticket__left">
          <span class="ticket__price"><?= price ?></span>
        </div>
        <div class="ticket__right">
          <div class="ticket__top">
            <span class="ticket__event">Event Name</span>
          </div>
          <div class="ticket__bottom">
            <span class="ticket__name"><?= name ?></span>
            <span class="ticket__ticketId"><?= ticketId ?></span>
          </div>
        </div>
      </div>
    </div>
    <? } ?>
  </body>
</html>
```

印刷するとこんな感じ
![スクリーンショット 2023-07-05 0.47.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565925/5cb4a2ca-c707-f671-02a1-c110c3749b9b.png)

ちなみになぜか2枚目が空白なので、5枚のチケットに対して6枚印刷できるようになっていました。。。
なんで笑

今回重要じゃないんで放置！

### 最終形のコード

<details>
    <summary>code.gs</summary>

```javascript:code.gs
function doGet(){
  return HtmlService.createHtmlOutputFromFile("top").setTitle("GASでプリント");
}

function getTemplate(){
  const data = getSheetData();
  const html =  HtmlService.createTemplateFromFile("print");
  html.data = data
  return html.evaluate().getContent();
}

function getSheetData(){
  const sheet = SpreadsheetApp.openById("1YtiB4LDCl8jAaXJdbHhG6OKsCWYpUNxSXQ38RZX6zEA").getSheetByName("ticket-data");
  const data = sheet.getRange(`A2:C${sheet.getLastRow()}`).getValues()
    .map(line=>{
      const [ticketId, name, price] = line;
      return {ticketId, name, price}
    });
  return data;
}
```
</details>

<details>
<summary>top.html</summary>

```html:top.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <style>
      .text-center {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class="text-center">
      <button id="openPage">印刷ページを表示する</button>
    </div>
    <script>
      document.querySelector("#openPage").addEventListener("click", () => {
        google.script.run.withSuccessHandler((templateText) => {
          const blob = new Blob([templateText],{type:"text/html"});
          const url = URL.createObjectURL(blob);
          window.open(url);
          URL.revokeObjectURL(url);
        }).getTemplate();
      });
    </script>
  </body>
</html>

```
</details>
<details>
    <summary>print.html</summary>

```html:print.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <style>
      @media print {
        body {
          -webkit-print-color-adjust: exact;
          color-adjust: exact;
        }

        .page {
          height: 100vh;
          page-break-after: always;
          display: flex;
          justify-content: center;
          align-items: center;
        }
      }

      @media screen {
        .page {
          padding: 30px 0px;
        }
      }

      .ticket {
        width: 400px;
        height: 100px;
        margin: auto;
        border: 2px solid black;
        border-radius: 10px;
        display: flex;
        align-content: center;
      }

      .ticket__left {
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 10px 0px 0px 10px;
        padding: 10px 20px;
        background-color: rgba(93, 184, 93, 0.5);
        border-right: 2px black dashed;
      }

      .ticket__right {
        display: flex;
        flex-grow: 1;
        flex-direction: column;
        justify-content: center;
        padding: 10px;
      }

      .ticket__event {
        font-size: large;
        text-decoration: solid underline;
      }

      .ticket__name {
        display: flex;
        align-items: center;
      }

      .ticket__ticketId {
        display: flex;
        align-items: end;
        justify-content: end;
        font-size: small;
        color: gray;
      }
    </style>
  </head>
  <body>
    <? for( const {ticketId, name, price} of data){ ?>
    <div class="page">
      <div class="ticket">
        <div class="ticket__left">
          <span class="ticket__price"><?= price ?></span>
        </div>
        <div class="ticket__right">
          <div class="ticket__top">
            <span class="ticket__event">Event Name</span>
          </div>
          <div class="ticket__bottom">
            <span class="ticket__name"><?= name ?></span>
            <span class="ticket__ticketId"><?= ticketId ?></span>
          </div>
        </div>
      </div>
    </div>
    <? } ?>
  </body>
</html>
```
</details>

## さいごに

ということでいかがでしたでしょうか。
スプレッドシートでも印刷はできるのですが、好きなスタイルが当てられないということで、少しずる賢い方法で実装してみました。

あまり使い所はないかもしれませんが、よきGASライフを~(*>∀<)ﾉ))