はじめに

こんにちは!

Uniforce株式会社のフロントエンド開発課の課長の根岸です。

今回はフロントエンド(TypeScript)でのPDF生成について紹介します。

画像などを変換するのではなく、アプリケーションの持っている情報を特定の座標に描画したPDFファイルです。

また、要件として罫線のある表を描画する想定です。

描画するコードそのものでは長くなってしまうため、ライブラリ選定理由・罫線のある表を描画するために使った関数・実装時のコード整理などについてのみとします。

利用したライブラリはPDF-LIBです。

環境

UniforceではNuxt.jsの3系を利用しているため、Vueも3系、TypeScriptとなります。

"nuxt": "3.11.2"
"pdf-lib": "1.17.1"

PDF生成

ライブラリ選定

PDF生成をできるライブラリはいくつかありますが、下記の理由からPDF-LIBを選択しました。

  • フォントの読み込み方法が簡単
    • PDFライブラリについて調べていると日本語フォントの導入がひとつの詰まりやすいポイントであることが分かりました
    • 読み込み方を見比べていると、PDF-LIBassetsフォルダなどに配置したフォントファイルをコンポーネントで読み込めば良さそうだったため理解しやすかったです
  • 文字揃えなどの機能がなくライブラリ利用者側が計算をする方針
    • 今回は罫線のある表を描画するという前提がありましたが、ライブラリがそれらの要件を満たせるかをそれぞれ調査するよりも、計算は自前でやってしまう前提の方が今後の要件追加などを考えても良いと考えました

描画

要件である表の描画を目指すために必要な機能を紹介します。

  • 文字列

https://pdf-lib.js.org/docs/api/classes/pdfpage#drawtext

drawText関数で文字列を描画できます。

  • 罫線(四角形)

https://pdf-lib.js.org/docs/api/classes/pdfpage#drawrectangle

drawRectangle関数で四角形を描画できます。

  • 文字列の幅計算

https://pdf-lib.js.org/docs/api/classes/pdffont#widthoftextatsize

widthOfTextAtSize関数で文字列の描画に必要な横幅を取得することができます。

  • 改行・文字揃え

今回は出力するPDFファイルの横幅には指定があったため、各四角形の横幅は固定値でした。

各四角形に入る文字列が四角形に入りきらない場合もあるため、改行をする必要があります。

widthOfTextAtSize関数で文字列の横幅を指定して必要な箇所で改行を挟みます。

同様に文字揃えについても文字列の横幅を計算することで、文字列の描画開始位置(X)を計算することができます。

  • フォント指定

今回はNuxt.jsなので、assetsフォルダにフォントファイル(.otf)を配置しました。

PDFを生成するコート上で、下記のようにフォントファイルを読み込み、PDFFont型を取得します。

import FontRegular from './assets/fonts/NotoSerifJP-Regular.otf'

const fontBytes = await fetch(FontRegular).then((res) => res.arrayBuffer())
const font = await pdfDoc.embedFont(fontBytes)

このPDFFontdrawText関数などに渡してあげることで、日本語フォントでの文字列描画ができます。

コード整理

ある程度自由にPDFを作れるようにしつつ、罫線を用いた表を描画出来るようにしました。

ただの文字列・改ページで分断されないようにブロック化した文字列・テーブルの行(計算で囲まれた表)の3種類の型を定義しました。

利用者はこれらの型のユニオン型であるPDFElement型の配列をPDF生成クラスに渡し、PDF生成クラスでは配列の要素ごとに描画処理をしていきます。

描画時はX, Y軸の位置を指定する必要があるため、配列のループ処理の中で1行目はY軸0、2行目は1行目の高さをY軸に加算する…といった具合に描画を進めていきます。

参考までに、下記は定義した型のイメージです。

// テキスト用のPDF要素
type PDFTextElement = {
type: 'text'
text: string
size: number
align: 'center' | 'left' | 'right'
}

// テキストブロック用のPDF要素
// ブロックで定義することで改ページ時にブロックが分断されないようにする
type PDFTextBlockElement = {
type: 'textBlock'
elements: PDFTextElement[]
}

// テーブル行用のPDF要素
// 枠で囲まれて描画される
type PDFTableRowElement = {
type: 'tableRow'
rows: {
text: string
size: number
width: number
}[]
}

type PDFElement = PDFTextElement | PDFTextBlockElement | PDFTableRowElement

保存

https://pdf-lib.js.org/docs/api/classes/pdfdocument#save

PDFの保存にはsave関数を使います。

save関数の戻り値はPromise<UInt8Array>型のため、あとは自由に保存処理を実行するだけです。

おわりに

PDFを生成するライブラリはいくつかありますが、今回はPDF-LIBを用いて表のあるPDFを生成しました。

計算をライブラリ利用者に任せる代わりに、関数ごとの動作やオプションも分かりやすく、なおかつ必要な関数は揃えてくれるため詰まることなく実現することができました。

ここまで見ていただき、ありがとうございました!