目的
puppeteerをdockerで動かして、PDFを生成させたくなった。
環境
- Apple M3(Apple Silicon)
- Node.js 20
デフォルトのchromeを使いたかったが…
$ pnpm add puppeteer
上記のコマンドではデフォルトで、chrome for testingとchrome-headless-shellのバイナリーが$HOME/.cache/puppeteerへインストールされる。
しかし、このまま使うと以下のエラーがでてしまった。
Error: Failed to launch the browser process!
rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2
これはchromeがarm64のlinux用のバイナリを提供していないことが原因な模様。
arm64 linuxに対応しているchromiumを使うことにする。
dockerfile
次のようなdockerfileで動作した。
FROM node:20-slim
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y \
chromium \
fonts-ipafont-gothic \
fonts-wqy-zenhei \
fonts-thai-tlwg \
fonts-kacst \
fonts-freefont-ttf \
libxss1 \
--no-install-recommends && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install pnpm -g
# Puppeteer は同梱ブラウザをダウンロードしない(システム Chromium を使用)
ENV PUPPETEER_SKIP_DOWNLOAD="true"
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
CMD ["pnpm", "start"]
chromiumのインストールの他に、以下を参考にして主要なフォントに対応する。
PUPPETEER_SKIP_DOWNLOAD="true"を設定することで、不要なchrome for testingのインストールをスキップさせる。
PUPPETEER_EXECUTABLE_PATHにchromiumのパスを設定しておく。
ソースコード
launchOptions.executablePathにchromiumのパスを指定し、sandboxなしで動作させる。
#!/usr/bin/env node
import puppeteer from 'puppeteer';
async function generatePdf(): Promise<void> {
const outputPath = process.argv[2] ?? 'output/output.pdf';
const html = `
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Sample PDF</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; margin: 24px; }
h1 { font-size: 24px; margin-bottom: 8px; }
p { line-height: 1.6; }
.box { margin-top: 16px; padding: 12px 16px; border: 1px solid #ddd; border-radius: 8px; }
</style>
</head>
<body>
<h1>Hello, PDF</h1>
<p>これは Puppeteer で HTML 文字列から生成した PDF です。</p>
<p>こんにちは(日本語)</p>
<p>Hello(英語)</p>
<p>你好(中国語・簡体字)</p>
<p>您好(中国語・繁体字)</p>
<p>Hola(スペイン語)</p>
<p>Bonjour(フランス語)</p>
<p>Guten Tag(ドイツ語)</p>
<p>Здравствуйте(ロシア語)</p>
<p>Olá(ポルトガル語)</p>
<p>Ciao(イタリア語)</p>
<p>안녕하세요(韓国語)</p>
<p>مرحباً(アラビア語)</p>
<p>नमस्ते(ヒンディー語)</p>
<p>สวัสดี(タイ語)</p>
<p>Xin chào(ベトナム語)</p>
<p>Halo(インドネシア語)</p>
<p>Merhaba(トルコ語)</p>
<p>Hallo(オランダ語)</p>
<p>Γειά σας(ギリシャ語)</p>
<p>Hej(スウェーデン語)</p>
<div class="box">
出力先: <code>${outputPath}</code>
</div>
</body>
</html>`;
const launchOptions: Parameters<typeof puppeteer.launch>[0] = {};
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
launchOptions.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
launchOptions.args = ['--no-sandbox', '--disable-setuid-sandbox'];
}
const browser = await puppeteer.launch(launchOptions);
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.emulateMediaType('screen');
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' }
});
// eslint-disable-next-line no-console
console.log(`PDF を出力しました: ${outputPath}`);
} finally {
await browser.close();
}
}
generatePdf().catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exitCode = 1;
});