タイトルとOGPはChatGPTが考えたものを穏便にしたものです。

ここのところ、ブログも書かずTwitterもあまりせず、ひたすらHieronymusを開発しています。というのは前回と同じです。

元々Hieronymusは「社内で使う会計システム」だったので、「帳票」というものは存在せず、アウトプットはExcelシートを出力するようにしていました。 どうしても「紙」が必要なら、Excelで印刷すれば良いわけです。 会計資料がExcelで出て来るのは、それはそれで便利ですしね。

前はそれで良かったのですが、現在の版には見積書や請求書を出す機能を作りました。 見積書や請求書については元々は「それ用のページ」を作って、ローカルで印刷orPDF化すればいいと思っていて、そう実装しました。

背景

使えるということであれば「それ用のページ」で良いとは言え、会計システムの方には「電子化証憑の管理」という機能がありますので、請求書を発行したら自動的にそっちに登録するようにした方が便利です。 請求書発行をして作ったPDFをあらためて登録するというフローよりは、請求書を発行する時にPDFを生成してそのまま証憑管理に登録してしまって、同じPDFをダウンロードして印刷する方が、面倒が少ないわけです。

そのためにサーバサイドでPDFを作る必要がありました。

Node.jsでPDFを出力する時の第一選択はPuppeteerです。 比較的古くからあり、多くの人が使って来た実績があります。 ネットにも情報が大量にあります。 (PuppeteerはPDFを作るだけではありませんが)

ということで、ほぼ思考停止状態でPuppeteerを使うことにしました。

Puppeteerが使いものにならない

試した構成

  • 帳票をSvelteで書く(これはEJSで書いたものを書き換えるだけ)
  • SSRでHTML化(ちょっと面倒だけど想定範囲)
  • Puppeteerで Chromium を起動して PDF 出力

ある意味この手のアプリでは王道です。 スタイルシートの調整とかは、EJSで書いていた時に済んでいるので、ほぼ右から左にHTML生成までは行けます。

問題は最後のところです。

Puppeteerが使いものにならない

Puppeteerは定番ということでネットに情報が大量にありますし、ChatGPTも「簡単っしょ」みたいなノリで言って来るので油断をしていたのですが、要約すると以下のようなことが起きました。

  • puppeteer-core + chromium の組み合わせが ARM環境でまともに動かない
  • headless: true を指定しても ウィンドウが開く(Firefox)
  • --headless を追加したら launch() が永遠に返ってこない
  • バイナリの場所を指定しても エラー詳細が出ず、黙って落ちる
  • どのオプションをどう足しても、launch() から返ってこない or WS接続できない

出て来るエラーを読む限りでは、多分これはARM環境であることが大きいのではないかと思われます。

Puppeteerはブラウザを要求しますが、このブラウザの挙動を制御するために原則的にブラウザは自前のものを使います。 そのため、Puppeteerはブラウザを同梱(と言うか自分でダウンロードします)しています。 この同梱のブラウザはARM用もあるらしいのですが、その選択の処理がおかしくて謎挙動をしてしまいます。

そこで既にあるブラウザを使おうとするのですが、これはこれで想定の甘いコードのようで、いくら指定しても起動できません。

Chromiumだからダメなのかと思い、Firefoxにも対応しているという言葉を信じてFirefoxを入れて使おうとするのですが、これもダメ。

  • dumpio切った
  • DISPLAY消した
  • --disable-gpu--no-zygote 追加
  • Firefoxバイナリも試した(PUPPETEER_PRODUCT=firefox
  • executablePath 指定
  • それでも launch() から返ってこない

Puppeteerの開発元の意図としては「公式ビルドのChromium以外は知らん」という姿勢のようです。 ちょっとでも環境がズレるとハマります。

FireFoxをバックエンドに指定した時、launchにheadlessだけじゃなくて、argsにも--headlessとか指定するのですが、launchしてブラウザの画面が出た時は、本当に脱力でした。

これもちょっとだけ調べてみると、どうもバンドル用のFirefoxのheadlessあたりに対応の甘さがあるせいのようです。

うわ〜出た、「**RenderCompositorSWGL failed**」…これは Firefox の **headlessモード + ARM環境 + ソフトウェアレンダリング周りのバグ(というか仕様)」**です。  
さらに `/dev/video-*` のエラーもついてて、**完全にグラフィックデバイスがない or アクセス不可な環境**で、Firefoxがフリーズしかけてます。

こちらではどうしようもありません。 いや、OSSなんだから自分でやれよは正論ですけど、ここで頑張る気力も時間も義理もありません。 と言うか、リリースの都合とか考えれば、ここに力を入れてはいけません

ということで、ここでハマりかけた時にChatGPTが唐突に、

## ✅ 解決策:もうこれしかない
>**Playwright に切り替える**

とか言って来たので、Playwrightというのを使ってみることにしました。 リポジトリを見ればわかりますが、これはMS製です。

Playwrightに変えたら一瞬だった

そんなわけで、Puppeteerを捨ててPlaywrightを使うことにしてみました。

  • npm install playwright
  • npx playwright install chromium
  • あとは page.setContent()page.pdf()即、出力成功

↑はChatGPTが言ってることですが、まぁ要するにこれだけです。 悩みどころがまるでありません。 これ以外に書くことがありません。 ブラウザのバイナリがどうこうという悩みは皆無です。 Puppeteerに求めてたのはこれなんだけどなぁ。

Puppeteerとの互換性という点では、ほぼ右から左です。 参考までに、実際に使っているコードを以下に挙げます。

import { chromium } from 'playwright';

const FORM_PATH='../dist-ssr'

/**
 * 帳票名に対応するSvelteコンポーネントを動的にインポートしてPDFを生成
 * @param {string} reportName - 帳票名(例:'Invoice')
 * @param {object} props - 帳票コンポーネントに渡す props(描画データ)
 * @returns {Promise<Buffer>} - PDFのバッファ
 */
export const print = async (reportName, props) => {
  const reportPath = `${FORM_PATH}/${reportName}.js`;

  const { default: ReportComponent } = await import(reportPath);
  const origin = `http://localhost:${global.env.port}/`
  const { html, head } = ReportComponent.render(props);
  let realHead = head.replaceAll(/href="\//g,`href="${origin}`);
  const fullHTML = `
<!DOCTYPE html>
<html>
  <head>
    ${realHead}
  </head>
  <body>${html}</body>
</html>`;
  const browser = await chromium.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  const page = await browser.newPage();

  await page.setContent(fullHTML, {
    waitUntil: 'networkidle',
    timeout: 10000  // 10秒で打ち切り
  });

  const pdfBuffer = await page.pdf({
    format: 'A4',
    printBackground: true
  });

  await browser.close();
  return pdfBuffer;
}

ぼーっと見るとAPIの名前まで同じに見えるかも知れません。 それくらい似ています。 そして、ハマらない(これ大事)。

🧠 結論:Playwright一択。Puppeteerは思い出の中で死んでてくれ

↑はChatGPTが生成してますw

Puppeteerは、一見枯れているように見えて、利用者も多く、情報も多いという最初の期待に反して、環境差異・ヘッドレス挙動の不安定さ・クロスブラウザ未完成ぶりで酷い目にあいました。

実のところ、数年前からARM環境を主な開発環境にして、それまでは「実績がある」「枯れている」と思っていたものが、「86上のLinux」という環境に依存したことだということがわかって来ました。 Electronも同じようにハマりましたし、ESP-IDFも随分とハマりました。

Playwrightはその点、最初から:

  • マルチブラウザ、マルチプラットフォーム対応
  • 安定したAPI
  • 安心して動くヘッドレスモード
  • Dockerとの親和性も◎

というあたりもちゃんとしているようです。

まとめ

2025年現在の開発環境のことを思えば、ヘッドレスブラウザを扱うなら、Playwrightから始めよう。Puppeteerはもう過去の遺物ということなんではないかと思います。

もう少し実のある普遍的で主語が大きい言い方をすれば、86で実績があったからと言ってARMで問題ないとは限らないとも言えます。

以前にも言ってますが、今はあちこちのクラウドがARMを提供していますし、割安な価格設定になっています。 そのことを思えば、「86だけではなくARMでも動く」というのはmustと言っても良いんじゃないかと考えます。 そういった意味で「2025年現在の」という表現にしています。

まぁ今回はChatGPTのアシストがあったので、ハマったのは1日で済んだのですが、自分だけだったら一体どれだけかかったかわかりません。

最近のエントリー

ブラウザでの画面表示と印刷の描画差異に関する実践的考察

RSSリーダを作りました

Puppeteerは過去の遺物だったのでPlaywrightにした話

💥 Prisma vs Sequelize:実戦投入してわかった本当の話

第14回 人工知能は「第五の火」である

DeepSeek-R1-Distill-Qwen-14B-Japaneseの量子化ビット数による答えの精度の違い

使われていないMaix Amigo を活用して、バーコードリーダーにする

TinySwallow-1.5B-Instruct

cyberagent/DeepSeek-R1-Distill-Qwen-14B-Japanese

パスワードマネージャの一手法(公知化情報)

中華安お掃除ロボット(650円)を買ってみました

phi-4とDeepSeek-R1の翻訳性能比較

phi-4

DeepSeek-R1-Distill-Qwen-14B

新オフィスの様子

基本のパン

社会制度のこと

気候のこと

新春のお散歩

あけましておめでとうございます