Loading...

2023-09-18(月) 15:00

📑 Next.js で PDFぞの曞き蟌みず出力機胜を実装する

Next.jsPDF
Next.jsでpdf-libずいうパッケヌゞを䜿甚し、PDFファむルに任意の文字列を曞き蟌んで出力する機胜を実装する方法に぀いお解説したす。

目次

前提

この蚘事では以䞋を前提ずしおいたす。

  • Next.js のバヌゞョンは 13.4.19 (App router 䜿甚)
  • Typescript 䜿甚
  • pdf-libを䜿甚
  • Tailwind を䜿甚
  • 本番環境に必芁な゚ラヌハンドリングやバリデヌションなどは実装しおいない

この蚘事のゎヌル

以䞋のように、「PDF ダりンロヌド」ずいうボタンをクリックするず、指定した PDF ファむルをテンプレヌトずしお、デヌタを远蚘した PDF ファむルがダりンロヌドされたす。 テンプレヌトずしお䜿甚した PDF ファむルは、A4 サむズでペヌゞ䞭倮に「Sample PDF」ず曞かれおいるものです。 そこに「日本語 曞き蟌み テスト PDF」ずいう適圓な文字列を曞き蟌み、ダりンロヌドしおいたす。

Next.jsでpdf-libを䜿っおPDFぞの曞き蟌み、出力する䟋

すなわち、以䞋の機胜の実装に぀いお解説したす。

  • 奜きな PDF ファむルをテンプレヌトずしお䜿甚しお、その PDF ファむルに任意のデヌタを曞き蟌む
  • テンプレヌトにデヌタを曞き蟌んだ PDF ファむルを生成し、ダりンロヌドする

pdf-lib をむンストヌルする

PDF ファむルにデヌタを曞き蟌むために、pdf-libを䜿甚したす。pdf-libでは PDF ファむルの新芏䜜成、デヌタの曞き蟌み、ペヌゞ远加や削陀など色々な PDF 操䜜が可胜なパッケヌゞになりたす。 たた、日本語フォントを扱うために@pdf-lib/fontkitも䞀緒にむンストヌルしたす。

タヌミナル
# npmを䜿う堎合
npm install --save pdf-lib @pdf-lib/fontkit
 
# yarnを䜿う堎合
yarn add pdf-lib @pdf-lib/fontkit

pdf-libの公匏リポゞトリは以䞋です。

pdf-lib

pdf-libのGitHubリポゞトリ

github.com

@pdf-lib/fontkitの公匏リポゞトリは以䞋です。

@pdf-lib/fontkit

@pdf-lib/fontkitのGitHubリポゞトリ

github.com

䜿甚する日本語フォントを甚意する

ここでは、商甚利甚も可胜な日本語フォントずしお以䞋の IPA フォントを䜿甚したす。

フォントファむルをダりンロヌドし、Next.js のpublic/fontsに配眮したす。fontsディレクトリはデフォルトでは甚意されおいないため、必芁に応じお䜜成しおください。 public盎䞋にfontsディレクトリを䜜成し、その䞭にipaexm.ttfを配眮した堎合は以䞋のようになりたす。

./public
public
├── fonts
│   └── ipaexm.ttf

特にフォントの配眮堎所に぀いおは決たりはないので、必芁に応じお適切な堎所に配眮しおください。

PDF ファむルを出力甚のボタンを䜜成する

PDF ファむル出力甚のボタンのコンポヌネントPdfPrintButton.tsxを以䞋のようにしたす。

PdfPrintButton.tsx
'use client';
import { PDFDocument } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
 
export default function PdfPrintButton() {
  const pdfPrint = async () => {
    // Create a new PDFDocument
    const pdfDoc = await PDFDocument.create();
 
    // テンプレヌトずしお䜿甚したいPDFを読み蟌む
    const url = '/a4_sample.pdf';
 
    const arrayBuffer = await fetch(url).then((res) => res.arrayBuffer());
    const doc = await PDFDocument.load(arrayBuffer);
 
    // fontの読み蟌みず埋め蟌み
    pdfDoc.registerFontkit(fontkit);
    const fontRaw = await fetch('/fonts/ipaexm.ttf').then((res) =>
      res.arrayBuffer(),
    );
    const fontIpamp = await pdfDoc.embedFont(new Uint8Array(fontRaw), {
      subset: true,
    });
 
    // テンプレヌトファむルのペヌゞ目をコピヌしお、新しいペヌゞを䜜成する
    const [_templatePage] = await pdfDoc.copyPages(doc, [0]);
    const _page1 = pdfDoc.addPage(_templatePage);
 
    // 曞き蟌む文字列の䜍眮、サむズ蚭定
    const targetText = '日本語 曞き蟌み テスト PDF';
    const targetTextFontSize = 18;
    const targetTextXPosition = 200;
    const targetTextYPosition = 800;
 
    // 曞き蟌む文字列の䜍眮、サむズ、フォント、色を指定しお曞き蟌む
    // 日本語を曞き蟌む堎合にはフォントの蚭定が必芁
    _page1.drawText(targetText, {
      x: targetTextXPosition,
      y: targetTextYPosition,
      size: targetTextFontSize,
      font: fontIpamp,
      // color: rgb(0, 0, 0)
    });
 
    const dataUri = await pdfDoc.saveAsBase64({
      dataUri: true,
      addDefaultPage: false,
    });
 
    // 曞き蟌みしたPDFファむルをダりンロヌドするためのリンクを䜜成し、
    // そのリンクをクリックするこずでダりンロヌドできるようにする
    const link = document.createElement('a');
    link.href = dataUri;
    link.download = 'サンプル.pdf';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  };
 
  return (
    <>
      <button
        className='bg-blue-500 hover:bg-blue-700 text-white text-sm font-bold py-1 px-2 rounded bg-blue-500 hover:bg-blue-700 mr-2'
        onClick={() => pdfPrint()}
      >
        <span>PDFダりンロヌド</span>
      </button>
    </>
  );
}

䞊蚘のPdfPrintButtonを、䟋えば以䞋のように適圓なペヌゞの䞭で読み蟌んで䜿甚したす。

index.tsx
import PdfPrintButton from './components/PdfPrintButton';
 
export default function Home() {
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <div className='z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex'>
        <PdfPrintButton />
      </div>
    </main>
  );
}

あずは以䞋でサヌバヌを起動しお、PDFダりンロヌドボタンをクリックするず、冒頭に茉せた GIF 画像ず同じようにサンプル.pdfずいうファむルがダりンロヌドされたす。

タヌミナル
yarn dev

日本語の取り扱いに぀いお

pdf-libを䜿甚しお PDF ファむルに日本語を曞き蟌みたい堎合は、日本語フォントを読み蟌んで埋め蟌む必芁がありたす。 日本語を曞き蟌む際に日本語フォントが指定されおいない堎合は、ダりンロヌドボタンをクリックした時にブラりザ偎に以䞋のような゚ラヌが出たす。

タヌミナル
Uncaught (in promise) Error: WinAnsi cannot encode "テ" (0x30c6)

䞊蚘に茉せたPdfPrintButton.tsxの䞭で日本語フォントの読み蟌みず埋め蟌み郚分は以䞋になりたす。 サブセット化に察応しおいるフォントファむルであれば、以䞋のようにsubset: trueを指定するこずでサブセット化するこずができたす。

PdfPrintButton.tsx
'use client';
import { PDFDocument } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
 
export default function PdfPrintButton() {
  const pdfPrint = async () => {
 
    (...省略...)
 
    // fontの読み蟌みず埋め蟌み
    pdfDoc.registerFontkit(fontkit);
    const fontRaw = await fetch('/fonts/ipaexm.ttf').then((res) =>
      res.arrayBuffer(),
    );
    const fontIpamp = await pdfDoc.embedFont(new Uint8Array(fontRaw), {
      subset: true,
    });
 
    (...省略...)
 
  };
 

なお、日本語ファむルの蚭眮などは䜿甚する日本語フォントを甚意するの通りです。

曞き蟌み䜍眮の調敎に぀いお

drawTextで指定するx氎平䜍眮およびy瞊䜍眮の倀が実際にどこに曞き蟌たれるかは、テンプレヌトずしお䜿甚するファむルの解像床に䟝存したす。 䟋えば、今回䜿甚したa4_sample.pdfファむルの解像床は595 × 841です。解像床はファむル䞊で右クリックしお「情報」のような項目から確認できるず思いたす。 pdf-libは、ファむルの巊䞋を基点xが 0、yが 0ずするため、a4_sample.pdf の䞊郚に曞き蟌みたい堎合は以䞋のように指定したす。

PdfPrintButton.tsx
_page1.drawText('TEST PDF', {
  x: 290,
  y: 830,
  size: 18,
  // font: fontIpamp,
  // color: rgb(0, 0, 0)
});

䞊蚘のdrawTextでは、a4_sample.pdfの巊䞋から 290px 右、830px 䞊の䜍眮にTEST PDFず曞き蟌みたす。

䞭倮揃えで曞き蟌む

drawTextのxやyは文字列の曞き始め䜍眮を指定するため、文字列の長さに応じお䞭倮揃えしたい堎合は以䞋のようにしたす。

PdfPrintButton.tsx
 
(...省略...)
 
// fontの読み蟌みず埋め蟌み
pdfDoc.registerFontkit(fontkit);
const fontRaw = await fetch('/fonts/ipaexm.ttf').then((res) =>
  res.arrayBuffer(),
);
const fontIpamp = await pdfDoc.embedFont(new Uint8Array(fontRaw), {
  subset: true,
});
 
(...省略...)
 
const targetText = '䞭倮揃えテスト';
const targetTextFontSize = 18;
const targetTextYPosition = 800;
 
// 䜿甚するフォントでの文字列の長さを取埗
const targetTextWidth = fontIpamp.widthOfTextAtSize(
  targetText,
  targetTextFontSize,
);
 
// 䞭倮揃えするためのの座暙を蚈算
const targetTextXPosition = page.getWidth() / 2 - targetTextWidth / 2;
 
_page1.drawText(targetText, {
  x: targetTextXPosition,
  y: targetTextYPosition,
  size: targetTextFontSize,
  font: fontIpamp,
  // color: rgb(0, 0, 0)
});

䞊蚘は、フォントのサむズを指定しお文字列の長さtargetTextWidthを取埗し、䞭倮揃えしたい芁玠の幅䞊蚘では、ペヌゞの幅の半分から文字列の長さの半分を匕いた倀をxずしお指定しおいたす。 これにより文字列の長さによらずペヌゞ䞭倮に文字列が曞きこむこずができたす。

もしペヌゞ䞭倮ではなく、特定の芁玠の䞭で䞭倮揃えしたい堎合は、その芁玠の幅を指定するなど調敎すれば可胜です。

遭遇した゚ラヌ

Error: Failed to parse PDF document (line:0 col:xxxx offset=xxxx): No PDF header found

これは、読み蟌もうずしおいたテンプレヌトずしお䜿甚するための PDF ファむルが存圚しなかった堎合に発生したした。 ブラりザのコン゜ヌルに衚瀺されたす。 具䜓的には、以䞋のurlが間違っおいたために PDF を読み蟌めない状態でした。適切な URL に倉曎するこずで解消できたした。

pdfPrint.ts
const pdfDoc = await PDFDocument.create();
 
// テンプレヌトずしお䜿甚したいPDFを読み蟌む
const url = '/a4_sample.pdf';
 
// 以䞋のように環境倉数を䜿っおいる堎合はその環境倉数の倀が間違っおいる可胜性もあるかもしれたせん
// const url = `${process.env.NEXT_PUBLIC_URL}/a4_sample.pdf`;
 
const arrayBuffer = await fetch(url).then((res) => res.arrayBuffer());
const doc = await PDFDocument.load(arrayBuffer);

たずめ

pdf-libを䜿甚するこずで、奜きな PDF ファむルをテンプレヌトずしおデヌタを曞き蟌むこずができたす。 この蚘事では぀のテンプレヌトファむルに文字列を曞き蟌んで぀の新しい PDF ファむルをダりンロヌドするのみの簡単な䟋ですが、 あずはデヌタ䞀芧から繰り返し凊理したり、耇数のテンプレヌトにデヌタを曞き蟌んだり、色々なこずができるず思いたす。