😎

satoriを活用してヘッドレスブラウザ不要でPDFを生成した

2023/12/18に公開

初めまして、株式会社DELTAでインターンをしている飯田です。

この記事はNext.js Advent Calendar 2023の19日目の記事になります。

今回は、実際にDELTAの業務で行ったことを記事にしようと思います。

はじめに

現在、私たちはプロジェクトの一つとして、Vercelを利用したNext.jsベースの開発に取り組んでいます。このプロジェクトでは次のような要望がありました。

「ユーザがボタンをクリックすることで請求書をPDF形式でダウンロードできるようにしたい」

この機能を実装するにあたり、まずはクライアントサイドで実装するか、サーバーサイドで実装するかを検討しました。

サーバーサイドの場合、通常PDFの生成にはPuppeteerPlaywrightのようなヘッドレスブラウザが用いられることが多いです。

しかし、今回はこれらの一般的な方法を採用せず、satoriというツールを使用しました。

本記事ではなぜsatoriを選んだのか、そしてsatoriを使ったPDF生成の方法を紹介します。

クライアントサイド vs サーバーサイド

まず、PDFを生成するにあたり、クライアント側かサーバー側のどちらで実装するのかを考えました。
結論から言うと、サーバーサイドで生成しました。

クライアントサイド

クライアントサイドでPDFを生成する方法の一つとしてhtml2canvasjspdfのようなライブラリを用いる方法が考えられます。
これは、Webページの内容を<canvas>要素としてキャプチャし、そのキャプチャされたコンテンツをPDFとして出力する方法になります。

クライアントサイドで生成する利点は以下のようなことが考えられます。

  • サーバーの介入がないため、ブラウザ上でほぼリアルタイムにPDFを生成・ダウンロードが可能
  • サーバーサイドでのPDF生成やデータの送受信が不要なため、サーバーのトラフィックや負荷が低減される

欠点としては以下が考えられます。

  • JSで動的に生成されるコンテンツが含まれる場合、キャプチャのタイミングによっては期待通りの描画が得られないことがある
  • あまり画質がよろしくない?←ローカル環境で実装した結果、サーバーサイドに比べて画質が悪かった。(もっといいやり方があったかもしれない)

サーバーサイド

サーバーサイドで生成する利点は以下のようなことが考えられます。

  • 描画の正確さが高い
  • クライアント(ユーザ)からの改ざんされる可能性が低い
  • ブラウザのメモリを消費しないのでクライアントのスペックに無関係に利用できる

欠点としては以下のようなことが考えられます。

  • 大量のリクエストがあると、サーバーの負荷が高まる

今回のプロジェクトでは、アクセスできる人が限られていたので、大量のリクエストが来ることはないと考えました。
また、生成したPDFをGoogle Driveに格納することを想定していたので、サーバーサイドで処理してしまえば、容易に外部ストレージにアップロードができ、かつクライアントサイドよりもセキュアだと判断し、サーバーサイドで生成することを選択しました。

ヘッドレスブラウザの使用を避けた理由

今回のプロジェクトはVercelをホスティングサービスとして使用していましたが、ここで一つ大きな課題に直面しました。それは、Vercelの厳しいデプロイ制限です。

Vercelの制限とヘッドレスブラウザの問題点

Vercelでは、デプロイ可能なパッケージのサイズに50MBという制限が設定されています。
この制限のため、デプロイする際にはパッケージサイズに注意を払う必要がありました。
特に、PuppeteerやPlaywrightのようなヘッドレスブラウザを使用するとなると、容量が大きいために50MBの制限を容易に超えてしまうのです。

ヘッドレスブラウザとAWS Lambdaの可能性と限界

次に、ヘッドレスブラウザをAWS Lambdaで動作させる可能性について検討しました。

具体的には、ブラウザバイナリ(chromium) を含まないpuppeteer-coreplaywright-coreと、AWS Lambda上で動作するchrome-aws-lambda を組み合わせる方法です。

しかし、このアプローチにも問題がありました。

puppeteer-corechrome-aws-lambdaの組み合わせは約55MB、playwright-corechrome-aws-lambda の組み合わせは約52MBと、これらはVercelの50MBのデプロイ制限を超えてしまいます。
したがって、Vercelを使用している私たちのプロジェクトでは、この方法を採用することが難しい状況でした。
ただし、古いバージョンのpuppeteer-coreplaywright-coreを使用すれば、Vercelの制限内でのデプロイが可能との報告もありました。

satoriを選択した理由

私たちのプロジェクトでは、Vercelの50MBというデプロイ制限をクリアする必要がありました。
その解決策として、satoriを採用することにしました。理由としては、satoriは、ヘッドレスブラウザを用いる方法に比べてはるかに軽量であり、Vercelの制限内に収めることが可能だったためです。

satoriの概要

satoriはHTMLとCSSをSVG形式に変換するJavaScriptライブラリです。
このライブラリでは、フォントを設定できたり、cssの調整が可能であり、多くのウェブページのレイアウトやスタイリングも再現することができます。ただし、一部のcssプロパティはsatoriで対応できないものも存在します。
satoriで対応しているcssはこちらで確認できます。

satoriを使用したPDF生成プロセス

satoriはHTMLテキストを受け取り、SVGの生成までを担います。その後、最終的なPDF生成には、pdfkitsvg-to-pdfといった他のライブラリを使用しました。これらのライブラリを組み合わせることで、satoriで生成されたSVGデータをPDFに変換し、必要なPDF文書を作成することができます。

satoriを用いたsvgの生成

satoriはJSX記法をサポートしているため、Reactアプリであれば簡単に導入することができます。

svg.tsx
import satori from 'satori'
const svg = await satori(
    <>
        <InvoiceTemplate {...invoiceData} />
    </>
    ,
    {
        width: 1024,
        height: 1200,
        fonts: [
        {
            name: "NotoSansJP-Medium",
            data: fontData,
            weight: 400,
            style: "normal",
        },
        ],
    }
);

このコードを実行すると以下のようなsvgデータに変換できます。

svg
<svg ...><path d="..." fill="black"></path></svg>

まとめ

今回、技術選定をするところから実装するところまでを任せていただきました。
クライアントサイド、サーバーサイドどちらで実装するのかだったり、様々なツールのメリットデメリットを調べた上で、DELTA代表の丹さんと相談しながら進めていきました。
当初は、どの技術を使えばPDFを効率的に生成できるのかを中心に考えていました。しかし、丹さんからたくさんアドバイスをいただく中で、「ただ機能を実装できるかどうかだけはでなく、Vercel環境でデプロイできるかどうかまでを検討しなければいけない」という助言をいただきました。
単に技術選定を行うだけでなく、その選んだ技術が実際のプロジェクト環境、特にVercelのような特定のホスティング環境でうまく機能するかどうかのような運用上の要件を総合的に考慮することの重要性を学ぶことができました。

無事要件を満たした機能を実装できてよかったです!

We're hiring!

https://note.com/delta_sevenrich/n/n15f551a4d7a5

最後までお読みいただきありがとうございます。
現在DELTA では一緒に働いてくださる仲間を大募集中です!

インターン生も募集しています!
https://herp.careers/v1/sevenrich7/ui6yj1xM6TtM

ご興味をお持ちいただけましたら、お気軽にフォームからご連絡ください🎆

https://docs.google.com/forms/u/1/d/e/1FAIpQLSfQuWNU1il5lq2rVdICM0tSK_jTsjqwc52LYEwUxBq7_ImtrQ/viewform

Discussion