📄

【Node.js +TypeScript】PDFファイルの各ページをPNG画像に変換する

2023/05/27に公開

先々にやりたいことに向けての簡易調査だったので、
軽い気持ちで手を動かしていましたが思いの他ハマりどころが多かったのでメモとして。

PDFファイルをNode.jsでCanvasに書き出すのにpdfjs-distを使いました。

調べたらおなじハマり方をしている人が多かったですが、

import * as pdfjs from 'pdfjs-dist';
const pdfPath = '/path/to/pdf.png';
const pdfData = new Uint8Array(fs.readFileSync(pdfPath));
const pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;

みたいな書き方をすると下記のようなエラーが出ます。

The browser/environment lacks native support for critical functionality used by the PDF.js library (e.g. `Path2D` and/or `ReadableStream`); please use a `legacy`-build instead.

丁寧にエラーにlegacy buildを代わりに使いましょうと出るので、

import * as pdfjs from 'pdfjs-dist/legacy/build/pdf';

としました。

ロードしたPDFファイルをPNG画像化するためにCanvasに書き出す必要があるのですが、
やりたいことはブラウザ上での実行ではなくNode.js上での実行なので、
node-canvasを使います。
PDFをCanvasに書き出すためにはキャンバスコンテキストとPDFのViewport情報が必要ですが、
Viewport情報を取り出す際に

const pdfData = new Uint8Array(fs.readFileSync(pdfPath));
const pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdfDoc.numPages; i++) {
	const page = await pdfDoc.getPage(i);
	const viewport = page.getViewport();
}  

みたいにgetViewport()をそのまま叩くと正しく取り出せないので、

const viewport = page.getViewport({ scale: 1.0 });

のようにスケールを指定します。

あとはpdfjs-distの機能でレンダリングするだけですが、
render()で待ち受けているCanvasRenderingContext2Dの型定義と比べると、
node-canvasが持つCanvasRenderingContext2Dの型定義では足りない(ちょっと表現がちがいますが・・・)ものがあるので、今回は思考停止してanyキャストします。

await page.render({ canvasContext: ctx as any, viewport }).promise;

あとはCanvasの情報を取り出してPNGファイルでもいいしbase64でもいいので保存してやりたいことは完了。

コード全文は下記。


import fs from 'fs';
import path from 'path';
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf';
import { createCanvas } from 'canvas';

const pdfPath = '/path/to/pdf.pdf';
const outDir = '/path/to/out';

// PDFファイルの読み込み
const pdfData = new Uint8Array(fs.readFileSync(pdfPath));
const pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;

for (let i = 1; i <= pdfDoc.numPages; i++) {
	const page = await pdfDoc.getPage(i);
	const viewport = page.getViewport({ scale: 1.0 });
	const canvas = createCanvas(viewport.width, viewport.height);
	const ctx = canvas.getContext('2d');

	// TODO ここでanyをつかわない方法をさがしたい
	await page.render({ canvasContext: ctx as any, viewport }).promise;

	// 書き出し
	const image = canvas.toBuffer();
	const writeableData = new Uint8Array(image);
	await fs.promises.writeFile(`${path.join(outDir, `out_${i}`)}.png`, writeableData);
}  

開発メンバーの環境がMacやWindowsやWSLだったりとばらばらな場合、
node-canvasだと環境構築でつまづく人もでてくる可能性があるので、
@napi-rs/canvasみたいなものを使ってもいいのかもしれないがまだ何も検証もできていないので今後の宿題。

「ひとまず動いているだけ」という状態なので本格的に利用する場合はいろいろと気にするところは多い。

こんなことを頑張らなくてもchild_processつかって外部ツールをコマンドでたたいた方がいいのかもしれない。

Discussion