pdf-libを使用したPDF書き出し機能の実装

はじめに

こんにちは、プレックスの 種井 です。

プレックスジョブでは、会員ユーザー様向けに、プロフィールなどの入力情報から履歴書や職務経歴書を自動で作成できる機能を提供しています。

PDFの生成方法に関しては、サーバー側で行う方法もありましたが

  • 各クライアントのリソースに頼ることで、サーバーの負荷を抑えることができる

という観点から、クライアント側でPDF生成を行うことにしました。

今回は、履歴書・職務経歴書の自動作成機能の実装時に使用したpdf-libというライブラリの使い方、実装時に考慮した点に関して紹介していきたいと思います。

pdf-libとは?

ピュアjavascriptで書かれた、PDF生成用のライブラリであり、PDFの新規作成や、編集、フォームへの入力を行うことができます。

PDF生成用のライブラリは他にも選択肢がありますが、後述する日本語フォントへの対応のしやすさからpdf-libの使用を決めました。

環境

バージョン
Nodejs +12.22.0
React.js 17.0.2
Next.js 12.0.3
pdf-lib 1.17.1
@pdf-lib/fontkit 1.1.1

導入方法

npm

$ npm install --save pdf-lib

yarn

$ yarn add pdf-lib

基本的な使用方法の紹介

PDFの新規作成

const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage()
page.drawText('sample')
const pdfBytes = await pdfDoc.save()
// pdfBytesを使用してファイルの書き込みを行う
...

既存PDFの編集

const pdfURL = '/pdfs/template.pdf'
const pdfBytes = await fetch(pdfURL).then((res) => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(pdfBytes)
const pages = pdfDoc.getPages()
pages[0].drawText('sample')
const pdfBytes = await pdfDoc.save()
// pdfBytesを使用してファイルの書き込みを行う
...

既存PDFの編集(フォーム入力)

const pdfURL = '/pdfs/template.pdf'
const pdfBytes = await fetch(pdfURL).then((res) => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(pdfBytes)
const form = pdfDoc.getForm()
const textField = this.form.getTextField('name')
textField.setText('sample name')
const pdfBytes = await pdfDoc.save()
// pdfBytesを使用してファイルの書き込みを行う
...

日本語フォントへの対応

デフォルトでは、日本語フォントが使用できないため、@pdf-lib/fontkit 使用して、カスタムフォントを設定する必要があります。

※ 以下、Next.jsへの導入を前提として説明します。

fontkitのインストールを行います。

npm

$ npm install --save @pdf-lib/fontkit

yarn

$ yarn add @pdf-lib/fontkit

例として、adobe-fonts/source-han-sansからカスタムフォントをダウンロードしてPDFドキュメントに埋め込んでみます。

public/fonts/HanSansJP ディレクトリを作成し、ダウンロードしたカスタムフォント(SourceHanSansJP-Regular.otfファイル)を配置します。

コンポーネントのマウント時にfetchメソッドでpublic/fonts/HanSansJP 以下にあるカスタムフォントを読み込んでおきます。

const [font, setFont] = useState<ArrayBuffer>(new ArrayBuffer(0))

useEffect(() => { 
    const setupFont = async () => { 
        const fontBytes = await fetch('/fonts/HanSansJP/SourceHanSansJP-Regular.otf').then((res) => res.arrayBuffer() ) 
        setFont(fontBytes) } 
    }, [])

fontkit(@pdf-lib/fontkit)と読み込んでおいたカスタムフォントの設定を行います。

import fontkit from '@pdf-lib/fontkit'
import { PDFDocument } from 'pdf-lib'
...
const onGenerate = async () => {
    const pdfDoc = await PDFDocument.create()
    pdfDoc.registerFontkit(fontkit) 
    const customFont = await pdfDoc.embedFont(font)
    const page = pdfDoc.addPage()
    page.drawText('プレックスジョブ', {
        font: customFont, // カスタムフォントの設定
    })
    const pdfBytes = await pdfDoc.save()
...
}

これで、日本語フォントへの対応が可能になりました。

バンドルサイズの削減

pdf-libやfontkitはバンドルサイズが大きいので注意が必要です。

  • pdf-lib: 758.6k (gzipped: 338k)
  • fontkit: 427.9k (gzipped: 176.7k)

Next.jsを使用しているので、ビルド時にCode Splittingされるとはいえ、上記を使用したページにおいては、初回レンダリングに不要なバンドルはDynamic importを使用して、バンドルサイズを削減しておきたいです。

以下、使用時にライブラリをインポートする例です。

...
const onGenerate = async () => {
    // Dynamic import
    const { PDFDocument } = await import('pdf-lib')
    const fontkit = await import('@pdf-lib/fontkit').then((value) => value.default)
    
    const pdfDoc = await PDFDocument.create()
    pdfDoc.registerFontkit(fontkit) 
    const customFont = await pdfDoc.embedFont(font)
    const page = pdfDoc.addPage()
    page.drawText('プレックスジョブ', {
        font: customFont, // カスタムフォントの設定
    })
    const pdfBytes = await pdfDoc.save()
...
}

PDF生成時にライブラリを読み込むようにすることで、バンドルサイズを減らすことができました。 (実際のプロジェクトに適用した際には両ライブリを使用するページで350kb程度の削減になりました)

終わりに

今回はPDFの生成をクライアントで行うためのpdf-libライブラリの使用方法、カスタムフォント対応、バンドルサイズの削減など、実装時に考慮した点について紹介してみました。

  • 動的なレイアウトを行う場合の考慮点
  • PDFのフォーム機能を使用したテンプレートの作成方法
  • 画像の埋め込み

などに関しても今後のブログ記事で紹介したいと考えています。

PDFの作成機能は業務系のアプリケーションを中心に必要になることが多いですが、いざ実装するとなると参考資料が多くないので、少しでも参考になればと思います。

最後になりますが、プレックスではソフトウェアエンジニアフロントエンドエンジニアを募集しています。

少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。

dev.plex.co.jp

参考