見出し画像

Vue3コンポーネントをPDF出力する

こんにちは。株式会社レスキューナウの新プロダクトチームに業務委託で参画している開発者の白石です。
現在、私のチームではVue3とquasarを採用し、開発を進めています。
画面に表示している内容で帳票出力する機能を実装したので方法を書いておきます。
今回はquasarのテーブルコンポーネントをPDF化してみようと思います。

必要なライブラリ

PDF化するにあたって使用するライブラリは2つ。
*Vue3コンポーネントをPDF化するライブラリは他にもいくつかありましたが、Viteとの相性が悪くてうまくいかなかったりして下記ライブラリを採用しました。

pdfmakeはJavaScriptでPDFを生成するためのライブラリ
html-to-pdfmakeはDOM要素を読み込むpdfmake向けライブラリ
html-to-pdfmakeは<table>タグやセル結合させるためのrowspan/colspanも認識してPDFデータに変換してくれる。

PDF化するVue3コンポーネント(quasar)を用意

まずPDF化するテーブルを用意します。
テーブルにはQTableではなくQMarkupTableを使用しています。
理由はQTableに含まれるソート機能やページネーション機能、自動で付与されるスタイルがhtml-to-pdfmakeには邪魔であり、QMarkupTableで<th><tr>タグに直接インラインスタイル付与することでPDF上のテーブル列幅を調整することができるためです。

    <template>
      <q-markup-table
        ref="refPrintTableDom"
        class="scroll"
        separator="cell"
        wrap-cells
        dense
        bordered
      >
        <thead>
          <tr>
            <th colspan="2" :style="{ width: '100pt' }">aaaaaaaaaaaa</th>
            <th colspan="3" :style="{ width: '390pt' }">bbbbbbbbbbbbbb</th>
          </tr>
          <tr>
            <th :style="{ width: '65pt' }">1111111111</th>
            <th :style="{ width: '35pt' }">2222222222</th>
            <th :style="{ width: '130pt' }">3333333333</th>
            <th :style="{ width: '130pt' }">4444444444</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td rowspan="2">a1</td>
            <td>a2</td>
            <td>a3</td>
            <td>a4</td>
          </tr>
          <tr>
            <td rowspan="0"></td>
            <td>a2'</td>
            <td>a3'</td>
            <td>a4'</td>
          </tr>
          <tr>
            <td>b1</td>
            <td>b2</td>
            <td>b3</td>
            <td>b4</td>
          </tr>
        </tbody>
      </q-markup-table>
    </template>
テーブル(画面)

PDF出力機能実装

必要なものをインポートする

<script setup lang="ts">
import * as pdfMake from "pdfmake/build/pdfmake";
import htmlToPdfmake from "html-to-pdfmake";

import { ref } from "vue";
import { QMarkupTable } from "quasar";
              '
              '
              '
              '
</script>

PDFドキュメントのスタイルを定義するオブジェクトを作る

const documentDefined = {
  pageSize: "A4",                // PDF用紙サイズ設定
  pageOrientation: "landscape",  // 用紙向き設定(横向き)
  pageMargins: [10, 10, 10, 30], // PDF用紙マージン設定[左、上、右、下]
  content: [content],            // 表示したいコンテンツをセット。(後でcontentを定義する)
  footer: (currentPage: number, pageCount: number) => {
    return [
      {
        fontSize: 8,
        text: "page " + currentPage.toString() + " of " + pageCount,
        alignment: "center",
        margin: [0, 20],
      },
    ];
  },                            // 用紙フッターにページ表記をセット
};

PDFドキュメントの内容を定義するオブジェクトを作る

//<template>で用意したQMarkupTableコンポーネントを参照する
const refPrintTableDom = ref<QMarkupTable>();  
const reportContentForPdf = refPrintTableDom.value.$el.outerHTML

const option = {
    defaultStyles: {
      table: { fontSize: 7 },
  },
  tableAutoSize: true,
}

// htmlToPdfmakeの第一引数にPDF化したいDOMStringを、第二引数にスタイル定義など任意
const content =  htmlToPdfmake(reportContentForPdf,option)

作ったcontentはdocumentDefinedのcontentプロパティにセットする

PDF出力実行

下記でPDFファイルが作られます。
注)テーブルコンポーネントに日本語が含まれる場合は別途日本語フォントをpdfMakeにセットしてあげる必要があります。

pdfMake.createPdf(docDefinition).download(“帳票.pdf”)


// テーブルコンポーネントに日本語が含まれる場合はこっち↓
import fontData from "@/assets/jp-font.json";
const useFontSetting =  {
   ipa: {
      normal: "ipagp.ttf",
      bold: "ipagp.ttf",
   },
},
pdfMake.createPdf(docDefinition,null,useFontSetting,fontData).download(“帳票.pdf”)

余談

rowspanなどでセル結合する際に結合先のDOM要素に対する扱いがQMarkupTableとhtml-to-pdfMakeの間で違いがありました。

QMarkupTableでは結合先の要素でも空の<td>タグが必要になりますが、html-to-pdfMakeでは空の<td>タグを入れていると表示が崩れます。
以下のようにhtml-to-pdfMakeにouterHTMLを渡す際、正規表現で<td>タグを削除しています。

  const reportContentForPdf = refPrintTableDom.value.$el.outerHTML.replaceAll(
    /<td class=\"text-left body-text\" rowspan=\"0\".*?>.*?<\/td>/g,
    ""
  );

クライアントサイドでPDFを生成するにあたって

私のプロジェクトはSPAでビルドツールにViteを採用しています。なんの設定もせずにvite buildをすると日本語フォントのデータがJavaScriptに組み込まれます。これではPDF出力機能を持つページに遷移するたびに日本語フォントファイルのデータ(8Mくらい)を含むJavaScriptファイルをダウンロードしてしまいます。
なのでvite.config.jsで以下のように記載して日本語フォントのデータがJavaScriptに含まれないよう切り出しました。

  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          "jp-font": ["./src/assets/jp-font.json"],
        },
      },
    },
  },

切り出した日本語フォントのファイルはキャッシュして何度もダウンロードしないようにします。
私のプロジェクトではNginxを使っていたのでnginx.confを以下のようにしてキャッシュ設定しています。

server {
    #ブラウザキャッシュ設定
    location ~ /assets/jp-font.*.js {
        expires 1M; #有効期限
    }
}

最後に

現在、レスキューナウでは、災害情報の提供、災害情報を活用した安否確認サービスなどのWebサービスの開発エンジニアを募集しています!
社員・フリーランスに関わらず、参画後に安心してご活躍できることを目指し、応募された方の特性・ご希望にマッチしたチームをご紹介します。
ちょっと話を聞いてみたい、ぜひ応募したい、など、当社にご興味を持っていただけましたら、お気軽にエントリーください!!