mcpField
B
boxpdf

boxpdf

ローカル
earonesty.github.io開発ツール195 週間前に更新

boxpdf について

boxpdf は pdf-lib を基盤とする TypeScript の PDF レイアウトライブラリで、宣言的なレイアウト部品を使い、表、テーマ、フォームウィジェットを含む複数ページの PDF を作成できます。アプリに PDF 出力を組み込む開発者やテンプレートを使う開発者向けで、エージェント用のリソース専用 MCP サーバーも付属します。

主な機能

  1. 01宣言的なスタック・テキストレイアウト
  2. 02複数ページと表のページ分割
  3. 03テーマと PDF フォームウィジェット
  4. 04ストリーミング PDF 生成
  5. 05CLI によるテンプレート作成

活用シーン

  1. 01領収書、履歴書、証明書の PDF 作成
  2. 02表やヘッダーを含む文書のレイアウト
  3. 03入力可能なフォーム付き PDF の作成
  4. 04テンプレートから PDF ファイルを準備

MCP 対応

CLI には、エージェントがドキュメントやテンプレートにアクセスするためのリソース専用 MCP サーバーが含まれます。公開するリソースの詳細は README に記載されていません。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"earonesty-boxpdf": {
"command": "npx",
"args": [
"-y",
"boxpdf"
]
}
}
}
起動コマンドnpx -y boxpdf

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npmboxpdf1.0.3

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

@boxpdf/writer

pdf-lib 上のボックスレイアウト DSL です。移植性の高い JavaScript で実装されており、Node 20 以降、Cloudflare Workers、Deno、ブラウザーで動作します。

ライブギャラリー:https://earonesty.github.io/boxpdf/

関連リポジトリ:boxpdf-html(HTML → BoxPDF)および streaming-pdf-reader(PDF → ドキュメントモデル、HTML、構造化データ)。

import { cleanTheme, flowToPdf, hline, hstack, standardFonts, text, vstack } from "@boxpdf/writer";

const bytes = await flowToPdf(async (pdf) => {
  const { font, bold } = await standardFonts(pdf);
  const theme = cleanTheme({ font, bold });

  return [
    vstack({ gap: 8 },
      text("Receipt #18472", theme.type.h1),
      text("May 14, 2026", theme.type.caption)
    ),
    hline(theme.hr),
    hstack({ gap: 16, justify: "between", width: 515 },
      text("Wool socks", theme.type.body),
      text("$28.00", { ...theme.type.body, font: bold, align: "right", width: 80 })
    )
  ];
});

flowToPdf がドキュメントのライフサイクルを管理し、保存済みのバイト列を返します。standardFonts は組み込みの Helvetica ファミリー(レギュラー、ボールド、イタリック、ボールドイタリック)を1回の呼び出しで埋め込みます。

ドキュメントを自分で管理しますか?明示的な方法も引き続き使用できます。
import { PDFDocument, StandardFonts } from "pdf-lib";
import { cleanTheme, renderFlow, text, vstack } from "@boxpdf/writer";

const pdf  = await PDFDocument.create();
const font = await pdf.embedFont(StandardFonts.Helvetica);
const bold = await pdf.embedFont(StandardFonts.HelveticaBold);
const theme = cleanTheme(font, bold);

await renderFlow(pdf, [
  vstack({ gap: 8 },
    text("Receipt #18472", theme.type.h1),
    text("May 14, 2026", theme.type.caption)
  )
]);

const bytes = await pdf.save();

renderFlow(pdf, nodes, options) は、自分で所有するドキュメントにページ分割して { pages } を返します。複数回のレンダリング、ページオブジェクト、カスタムの save() オプションが必要な場合に使用してください。boxpdf は、この明示的なライフサイクルで使用する PDFDocument と StandardFonts を再エクスポートします。

インストール

npm install @boxpdf/writer pdf-lib

pdf-lib はピア依存関係です。

旧パッケージ名

元の boxpdf パッケージも引き続きサポートされており、同じビルドから同じバージョンで公開されています。既存のインポートと boxpdf CLI は、変更なしで引き続き動作します。

npm install boxpdf pdf-lib

新しいプロジェクトでは @boxpdf/writer を使用してください。どちらのパッケージ名も同じ API を公開し、boxpdf コマンドを提供します。

機能

  • 宣言的なレイアウトプリミティブ:vstack、hstack、text、image、hline、vline、spacer、flex、keepTogether、link、svgPath、table。
  • レイアウト対応の AcroForm フィールド:テキスト、チェックボックス、ラジオボタン、ドロップダウン、オプションリスト、プッシュボタンのウィジェット。
  • パディング、マージン、背景、背景画像、ボーダー、borderRadius、オーバーフロークリッピング、flex-grow、flex-shrink、justify、align。
  • インライン要素の混在、インライン置換ノード、強制改行、ぶら下げインデント、オプションの段落回り込みに対応したリッチな段落。
  • maxLines による切り詰めに対応したワードラップ、オプションの breakWords、折り返しなしの制御。
  • テーマ:cleanTheme、stripeTheme、editorialTheme、brutalistTheme。
  • ページごとのヘッダーとフッター、スタックの分割、テーブル行の分割に対応した複数ページのフロー。
  • メモリ使用量を抑えたストリーミング生成。
  • PDF リンク注釈、テキスト装飾、ドキュメントメタデータ。
  • コアは圧縮後約 7 KB です。カスタムフォントを使う場合、loadFont または embedInter を呼び出したときに限り @pdf-lib/fontkit が読み込まれます。

テンプレート

templates/ のファイルには、レシート、搭乗券、履歴書、注文確認書、証明書のテンプレートがあります。それぞれ単一ファイルです。

CLI でアプリにひな形を作成できます。

npx boxpdf init receipt --out src/pdf/receipt.ts
npx boxpdf list

CLI には、エージェント向けのリソース専用 MCP サーバーも付属しています。

claude mcp add boxpdf -- npx -y boxpdf mcp

テーマ

import { cleanTheme, editorialTheme, standardFonts } from "@boxpdf/writer";

const theme = cleanTheme(await standardFonts(pdf));            // Helvetica
const serif = editorialTheme(await standardFonts(pdf, "times")); // serif + italic slot

各テーマファクトリーは、standardFonts(pdf) および embedInter(pdf) が返すものと同じ { font, bold, italic? } オブジェクト、または従来の位置引数によるフォント指定を受け付けます。

cleanTheme({ font, bold })            // or cleanTheme(font, bold)
stripeTheme({ font, bold })
editorialTheme({ font, bold, italic }) // or editorialTheme(font, bold, italic)
brutalistTheme({ font, bold })         // courier regular + bold

standardFonts(pdf, family) は "helvetica"(デフォルト)、"times"、または "courier" を受け取り、{ font, bold, italic, boldItalic } を返します。すべてのテーマが同じ構造を公開します:colors、spacing、radii、type、card、hr。

API

コンテナー
  • vstack(style, ...children)。垂直レイアウトです。
  • hstack(style, ...children)。水平レイアウトです。
  • keepTogether({ gap?, margin? }, ...children)。分割せずにページネーションします。

コンテナーの style:

フィールド型説明
width / heightnumber固定寸法です。それ以外の場合は内容に合わせたサイズになります。
padding / marginnumber | { top, right, bottom, left }一括指定または各辺ごとの指定です。
backgroundRGB単色で塗りつぶします。
backgroundImage{ image, width, height, offsetX?, offsetY?, repeat? }子要素の背後に画像を描画し、ボックス内にクリップします。
border{ color, width }ボックスの周囲に1pt以上の線を描画します。
borderSides{ top?, right?, bottom?, left? }{ color, width } を使って各辺に線を描画します。
borderRadiusnumber角の半径です。
overflow"visible" | "hidden"スタック内の子要素と絶対配置された子孫要素を、ボックスの矩形内にクリップします。
position"relative" | "absolute"ボックスのCSS風の配置方法です。
top / right / bottom / leftnumber絶対配置のオフセットをポイント単位で指定します。
zIndexnumber配置されたボックスの描画順です。値が大きいほど後に描画されます。
rotatenumberボックスの中心を軸に、時計回りに度単位で描画を回転します。レイアウトは変わりません。
transformBoxTransform[]translate、scale、rotate、skew、matrix による順序付きの描画変換です。
transformOrigin{ x, y }{ length, percent } の各成分で基点を指定します。既定値はボックスの中心です。
grownumber親の主軸に沿ったフレックス伸長比です。
shrinknumberフレックス縮小比です。
breakInside"auto" | "avoid"renderFlow での分割に関するヒントです。avoid はボックスを分割不可にします。
gapnumber子要素間の間隔です。
justify"start" | "center" | "end" | "between" | "around" | "evenly"主軸方向の配置方法です。
align"start" | "center" | "end" | "stretch" | "baseline"交差軸方向の配置方法です。baseline は hstack の行での使用を想定しています。
リーフ
  • text(content, { size, font, color?, align?, width?, lineHeight?, maxLines?, underline?, strikethrough?, margin? })。width を設定すると単語単位で折り返します。maxLines を設定すると、末尾を省略記号で切り詰めます。既定の lineHeight では、ディセンダーを含むフォントの全高が使われます。
  • paragraph({ width?, align?, lineHeight?, margin?, paddingLeft?, textIndent?, wrap?, floats? }, ...runs)。混在するインラインテキストランと、段落全体で一緒に折り返される分割不可のインラインノードを扱います。run(text, style)、linkRun(text, style, href)、inlineNode(node, { verticalAlign?, href? }) を使用します。ラン内の改行は強制改行になり、wrap: false を設定するとソフトラップが無効になります。
  • image(pdfImage, { width, height, margin? })。埋め込み済みの PDFImage を受け取ります。
  • imageFit(pdfImage, { width, height, fit?, margin? })。固定矩形の中央に画像を描画し、全体が収まるように(既定)またはクリップしながら全体を覆うように拡大縮小します。
  • spacer(size, { grow? }) / flex(weight = 1)。固定または伸長する間隔です。
  • hline({ color, thickness?, width?, margin? })。
  • vline({ color, thickness?, height?, margin? })。
  • link({ href }, child)。子要素をラップし、描画されたバウンディングボックスに対してPDFリンク注釈を登録します。
  • table({ columns, rows, ... })。ヘッダー行・フッター行、区切り線、スタイル付きセル、renderFlow での行単位のページ分割に対応した、固定・自動・比率指定の列です。セルには通常のノード、または { content, colSpan?, padding?, background?, border?, borderSides?, borderRadius?, align?, valign? } を指定できます。
AcroForm フィールド

フォームウィジェットはアトミックなレイアウトノードであるため、手動でページ座標を指定しなくても、スタック、テーブル、ページネーション、ストリーミングドキュメント内で使用できます。

import {
  checkbox,
  dropdown,
  flowToPdf,
  standardFonts,
  text,
  textField,
  vstack
} from "@boxpdf/writer";

const bytes = await flowToPdf(async (pdf) => {
  const { font } = await standardFonts(pdf);
  return [
    vstack({ gap: 10 },
      text("Registration", { size: 18, font }),
      textField({
        name: "person.name",
        width: 260,
        height: 26,
        font,
        fontSize: 11,
        required: true
      }),
      dropdown({
        name: "person.state",
        width: 140,
        height: 26,
        font,
        options: ["CA", "NY", "WA"]
      }),
      checkbox({
        name: "terms.accepted",
        width: 16,
        height: 16,
        required: true
      })
    )
  ];
});
  • textField({ name, width, height, ... })。初期value、multiline、password、maxLength、combed、配置、共通のフィールドフラグと外観オプションに対応しています。パスワードテキストフィールドは、exported: trueを明示的に設定しない限り、デフォルトでエクスポートできません。
  • checkbox({ name, width, height, checked? })。
  • radioOption({ name, option, width, height, selected? })。同じ名前のノードは1つのラジオグループを形成します。
  • dropdown({ name, options, width, height, selected?, editable?, sorted? })。ビューワー間で一貫した動作になるよう、単一選択に固定されています。
  • optionList({ name, options, width, height, selected?, multiselect?, sorted? })。
  • button({ name, label, width, height, ... })。移植性のあるプッシュボタンウィジェットと外観を作成します。BoxPDF は PDF JavaScript や送信アクションを追加しません。標準の SubmitForm アクションはリーダーに依存し、ブラウザのビューワーでは、オリジンポリシーによりローカル PDF からの送信がブロックされる場合があります。

すべてのフィールドは、margin、alignSelf、readOnly、required、exported、hidden、backgroundColor、borderColor、borderWidthに対応しています。テキストを含むフィールドでは、さらにfont、fontSize、textColorにも対応しています。フィールド名はドキュメント全体で一意です。同じ名前を再利用すると、同じ論理フィールドに別のウィジェットが追加されます。異なる種類のフィールドに同じ名前を再利用すると、エラーが発生します。パスワードテキストフィールドはデフォルトでexported: falseになるため、意図的に送信またはエクスポートを許可するには、exported: trueを設定してください。フォームは通常の出力、ストリーミング出力、暗号化出力で使用できます。準拠するリーダーで変更を許可するかどうかは、暗号化のfillForms権限で制御してください。 共有論理フィールドでは、テキスト、ドロップダウン、オプションリストの初期化状態(value、options、初期selected)は、最初のノードによって固定されます。ラジオグループのフラグ(offToggleable、mutuallyExclusive)も最初のノードでのみ設定されます。radioOptionのselected: trueはそのオプションを選択済みにしますが、selected: falseにしても既存の選択は解除されません。

呼び出し元が所有するPDFDocumentを扱う場合は、getFormValues(pdf)、setFormValues(pdf, values)、flattenForm(pdf)を使用してください。WinAnsi 以外のテキストを更新するときは、setFormValuesまたはflattenFormに埋め込みフォントを{ font }として渡すと、pdf-lib がそのフォントを使って外観を再生成します。

AcroForm ウィジェットはページ描画操作ではなく、PDF 注釈です。そのため、変換された BoxPDF の祖先ノード内には配置できません。BoxPDF は位置がずれたウィジェットを出力する代わりに、エラーを発生させます。XFA、署名フィールド、PDF JavaScript、暗号署名は、コアのフォームレイヤーの対象外です。

レンダリング
  • flowToPdf(build, options?)。バイト列を得る最短の方法です。PDFDocumentを作成し、build(pdf)コールバックに渡します(そこでフォントや画像を埋め込み、最上位ノードを返します)。renderFlowでページ分割し、保存済みのUint8Arrayを返します。renderFlowと同じoptionsを指定します。
  • renderFlow(pdf, nodes[], options)。最上位の子ノードの並びをページ分割します。最上位のvstackノードは子ノードの間で分割できます。table()は行の間で分割され、続きのページではヘッダーが繰り返されます。分割を避けたいブロックにはkeepTogether()またはbreakInside: "avoid"を使用してください。オプション:size、margin、header?、footer?、reserveBottom?、title?、author?、subject?、keywords?、creator?、producer?、debug?、warnings?、profile?。ヘッダーとフッターには{ pageNumber, totalPages }が渡されます。デフォルトはLETTER(612×792)です。A4の場合は{ size: PageSizes.A4 }を渡してください。最上位の子ノードの測定幅がページのコンテンツ領域を超えると、boxpdfはconsole.warnを出力します。warnings: falseで抑制できます。
  • savePdf(pdf, options?)。呼び出し元が所有するドキュメントを保存します。必要に応じてパスワード暗号化も指定できます。pdf.save()を直接呼び出した場合は、常にpdf-libの暗号化されない動作が維持されます。
  • streamFlow(pdf, writable, asyncIterable, options)。ページごとに段階的にレンダリングします。ページ数にかかわらず、メモリ使用量は一定に保たれます。各ページが閉じるたびに、PDFバイト列をWritableStream<Uint8Array>に書き込みます。仕様については、以下のストリーミングのセクションを参照してください。
  • renderToPdf(node, options)。1ページ用の便利な関数です。
  • pageInner(size, margin) / pageContent(size, margin)。ページの内側のコンテンツ幅または矩形を計算します。
  • render(node, page, x, yTop, parentWidth)。既存のPDFPage上の指定位置にサブツリーを描画します。
  • measure(node, parentWidth)。レンダリングとは独立して、固有サイズを計算します。

{ debug: true }を渡すと、コンテンツボックスが赤、マージンボックスがオレンジの枠線で表示されます。

ヘルパー
  • standardFonts(pdf, family?)。pdf-lib組み込みのフォントファミリー(デフォルトは"helvetica"。"times"、"courier"も指定可能)を埋め込み、テーマにそのまま使用できる{ font, bold, italic, boldItalic }を返します。これらはコンパクトなPDF標準フォント参照を使用します。
  • loadFont(pdf, source, options?)。URL、バイト列、base64、またはデータURLからTTFを埋め込みます。
  • loadImage(pdf, source)。PNGまたはJPEGを埋め込みます(形式は自動検出されます)。
  • aspectRatio(ratio, { width }) / aspectRatio(ratio, { height })。固定比率のボックスや画像で、不足している寸法を算出します。
  • formatCurrency(n, { currency, locale })。Intl.NumberFormatのラッパーです。
  • defineStyles({ ... })。再利用可能なスタイルのセットを型付きで定義します。
  • hex("#1f8a4d") / rgb255(31, 138, 77)。色を生成します。

フォントの読み込み

3つの方法があります。

CLIでバイト列をバンドルする方法。 本番環境におすすめです。

npx boxpdf font add ./Acme-Regular.ttf=regular ./Acme-Bold.ttf=bold \
  --out src/fonts/acme.ts

export constのbase64文字列を含むsrc/fonts/acme.tsを生成します。その後、次のようにします。

import { loadFont } from "@boxpdf/writer";
import { regular, bold } from "./fonts/acme.js";

const font = await loadFont(pdf, regular);
const acmeBold = await loadFont(pdf, bold);

バイト列はバンドルに含まれるため、すぐにローカルで読み込めます。

組み込みのInterウェイト。

import { loadFont } from "@boxpdf/writer";
import { inter, interBold } from "@boxpdf/writer/inter";

const font = await loadFont(pdf, inter);
const bold = await loadFont(pdf, interBold);

boxpdf/interは、同じInterサブセットを生のbase64文字列(inter、interBold、interItalic)と、embedInter(pdf, { italic?, tabularFigures? })として再エクスポートします。

boxpdf/interをインポートすると、約325 KBのフォントバイト列と@pdf-lib/fontkitが読み込まれます。コアのみのインポートでは、より小さなコアバンドルが維持されます。

import { embedInter } from "@boxpdf/writer/inter";

const { font, bold } = await embedInter(pdf);
const theme = cleanTheme(font, bold);

金額列に表形式の数字を使うには、{ tabularFigures: true }を渡してください。

const { font, bold, tabularFont, tabularBold } = await embedInter(pdf, {
  tabularFigures: true
});

text(formatCurrency(amount), { size: 12, font: tabularBold, align: "right" });

URLから取得する方法。

const brand = await loadFont(pdf, "https://example.com/Acme-Regular.ttf");

埋め込み時に完全なTTFを取得し、サブセット化します。キャッシュが温まった状態のCloudflare Workersでは高速です(約5~15 ms)。キャッシュが冷えた状態、またはNodeでは、毎回取得にかかる時間がすべて発生します。

loadFontは、ソースにかかわらず同じ{ subset?: boolean; features?: { tnum: true } }オプションを受け付けます。表形式の数字を有効にするには、features: { tnum: true }を使用してください。

パスワード暗号化

BoxPDF は、Standard Security Handler revision 6 と AES-256 を使用して、PDF 2.0 のパスワード暗号化出力を書き出せます。暗号化にはランタイムの Web Crypto 実装を使用し、ブラウザー用バンドルに暗号化依存関係を追加しません。暗号化が要求された場合にのみ、実装が別のチャンクとして読み込まれます。

const bytes = await flowToPdf(
  async (pdf) => {
    const { font } = await standardFonts(pdf);
    return [text("Confidential", { font, size: 18 })];
  },
  {
    encryption: {
      password: "document-open-password",
      ownerPassword: "administrative-password",
      permissions: {
        printing: "lowResolution",
        copying: false,
        modify: false
      }
    }
  }
);

呼び出し元が所有するドキュメントでは、savePdf を使って保存します。

import { PDFDocument, renderFlow, savePdf } from "@boxpdf/writer";

const pdf = await PDFDocument.create();
await renderFlow(pdf, nodes);
const bytes = await savePdf(pdf, {
  encryption: { password: "open me" }
});

password は必須で、空の値にはできません。ownerPassword は任意です。省略すると、BoxPDF はランダムな内部所有者認証情報を生成して破棄します。パスワードには SASLprep が適用され、Unicode 文字を使用できます。準備後の UTF-8 バイト数は最大 127 バイトです。使用可能な権限は printing、modify、copying、annotate、fillForms、assemble です。

PDF の権限はビューアー向けの設定であり、DRM ではありません。PDF とは別の経路でパスワードを送信してください。暗号化は PDF/A と併用できません。また、BoxPDF は入力 PDF の復号や既存の署名の保持には対応していません。同じドキュメントを再度保存すると、新しい鍵、ソルト、ファイル識別子、IV が生成されます。

ストリーミング出力

長時間にわたるドキュメント生成には、renderFlow の代わりに streamFlow を使います。各ページが閉じられるたびに、PDF のバイト列が WritableStream<Uint8Array> に出力されます。ページ数にかかわらず、ピーク時のヒープ使用量は O(shared resources + one page in flight) に抑えられます。

import { PDFDocument, StandardFonts } from "pdf-lib";
import { streamFlow, text, cleanTheme } from "@boxpdf/writer";

const pdf = await PDFDocument.create();
const font = await pdf.embedFont(StandardFonts.Helvetica);
const bold = await pdf.embedFont(StandardFonts.HelveticaBold);

const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
streamFlow(pdf, writable, generate(font, bold)).catch(console.error);

return new Response(readable, {
  headers: { "content-type": "application/pdf" }
});

async function* generate(font, bold) {
  for await (const order of fetchOrders()) {
    yield buildOrderRow(font, bold, order);
  }
}

Node では、stream.Writable を適合させます。

import { createWriteStream } from "node:fs";
import { streamFlow, nodeAdapter } from "@boxpdf/writer";

const out = nodeAdapter(createWriteStream("./report.pdf"));
await streamFlow(pdf, out, nodes, {
  encryption: { password: "open me" }
});

1 つの論理的なスタックやテーブルを一度に構築するには大きすぎる場合は、flowContinuation を使ってサイズを抑えた断片を出力します。同じ ID を持つ隣接した断片は、スタックの間隔、装飾、テーブルヘッダー、行の区切り線を含め、1 つのノードであるかのようにページ分割されます。

for (let offset = 0; offset < rows.length; offset += 100) {
  const final = offset + 100 >= rows.length;
  yield flowContinuation(
    table({ columns, header, rows: rows.slice(offset, offset + 100) }),
    "orders",
    final
  );
}

継続断片は連続している必要があり、最後の断片では final: true を設定する必要があります。streamFlow は、中断されたシーケンスや未完了のシーケンスから不完全なレイアウトを黙って生成するのではなく、エラーを返します。

契約
  1. すべての embedFont/embedJpg/embedPng の呼び出しは、streamFlow の前に完了している必要があります。ストリーム中に埋め込みを行うとエラーになります。
  2. イテラブルはノードごとに消費されます。ジェネレーターを渡してください。
  3. streamFlow は書き込み先の排他的な所有権を取得し、成功時には閉じ、失敗時には中止します。
  4. ストリーミング時のヘッダーとフッターには ctx.pageNumber が渡されます。「Page X of Y」を表示するヘッダーやフッターには renderFlow を使ってください。ストリーミング中に ctx.totalPages にアクセスするとエラーになります。
  5. 出力サイズは、renderFlow のデフォルトの save() より 0~5% 大きくなります。
メモリベンチマーク

レンダリング中のヒープ使用量のピークです。各測定はそれぞれ独立したサブプロセスで実行されます。1 ページあたりテキスト 50 行です。形状を比較するため、@react-pdf/renderer も含めています。

ページ数streamFlow のピークrenderFlow のピーク@react-pdf のピーク出力
5012.8 MB31.7 MB160.8 MB70 KB
25015.4 MB91.1 MB643.1 MB347 KB
50018.7 MB120.8 MB1,219.9 MB693 KB
100025.4 MB219.6 MB2,292.6 MB1.4 MB

streamFlow のピークヒープ使用量はほぼ一定です(ワークロードが 100 倍に増加しても 12 → 25 MB)。renderFlow はページ数にほぼ比例して増加します。このワークロードでは、@react-pdf/renderer は 1 ページあたり約 2.3 MB を追加し、1000 ページでは 2.3 GB に達します。設計とグラフについては docs/design/streaming.md を参照してください。

継続処理の経路には、独自のヒープ上限付きサブプロセスチェックがあります。上記の以前の比較とは異なり、すべてのフラグメントを遅延生成し、出力後に GC を強制して、V8 の割り当て時の高水位点と保持されているライブセットを区別します。

継続処理フラグメント数出力ページ数サンプリングされたピークヒープGC 後の保持ヒープ出力
10081~58 MB~16 MB129 KB
1000810~125 MB~26 MB1.3 MB

どちらの実行も --max-old-space-size=128 で完了します。ワークロードが 10 倍になると、保持ヒープは約 10 MB 増加します。主な要因は最終的なページツリーと xref インデックスです。レンダリング済みのページコンテンツと継続処理の入力は、段階的に解放されます。pnpm memory:check:continuation で再現できます。

Cloudflare Workers

コアと boxpdf/inter サブパスは、どちらも nodejs_compat なしで Workers 上で動作します。

import { Hono } from "hono";
import { cleanTheme, flowToPdf, standardFonts, text } from "@boxpdf/writer";

const app = new Hono();

app.get("/receipt.pdf", async (c) => {
  const bytes = await flowToPdf(async (pdf) => {
    const t = cleanTheme(await standardFonts(pdf));
    return [
      text("Thanks!", t.type.h1),
      text("This PDF was generated at the edge.", t.type.body)
    ];
  });
  return new Response(bytes, { headers: { "content-type": "application/pdf" } });
});

export default app;

使用例

実行可能なスクリプトは examples/ にあります。

  • receipt.ts。合計金額付きの 1 ページの領収書です。
  • itinerary.ts。2 段構成の旅行日程表です。
  • invoice.ts。ランニングヘッダーとフッター、および keepTogether を使用した複数ページの請求書です。
  • debug.ts。{ debug: true } を使ったレイアウトです。
  • themes-showcase.ts。同じ領収書を 4 種類のテーマすべてでレンダリングします。
  • inter-showcase.ts。Inter を使用したクリーンテーマです。
  • flex-shrink.ts。URL のはみ出しに対する 3 種類の挙動を並べて示します。
  • hanging-indent.ts。リストマーカーに paddingLeft と負の textIndent を使った段落です。
  • overflow-clipping.ts。絶対配置のオーバーレイと背景画像を使った、クリッピングされるカードです。

Flex-shrink

hstack または vstack の任意の子に shrink: number を指定して有効にします。子の固有の主軸方向サイズの合計が親の利用可能なスペースを超えると、shrink > 0 のアイテムは shrink × baseSize に比例した割合でスペースを譲ります。shrink = 0(デフォルト)のアイテムは固定されます。

hstack(
  { width: 360, gap: 16 },
  text("Customer:", { size: 11, font: bold }),
  text("Mr. Algernon Hephaestus Constantine Pemberton-Smythe III", {
    size: 11, font, shrink: 1
  })
)

挙動:

  • テキストの子の最小幅は、空白で区切られた単語のうち最も幅の広いものと同じです。折り返しは空白の境界で行われます。
  • 単一トークンの文字列(URL、ハッシュ、スラッグ)は固有の幅を維持し、割り当てられた領域から視覚的にはみ出します。2 つのオプトイン設定で最小幅を下げられます。
    • maxLines: N。エンジンがはみ出した部分を省略記号に置き換えます。テキストは割り当てられた領域まで縮小され、… で省略されます。
    • breakWords: true。CSS の overflow-wrap: break-word を適用します。文字の境界で強制的に改行します。
  • 縮小したテキストがより多くの行に折り返されると、それに応じてコンテナーの固有の高さが大きくなります。
  • いずれかのアイテムが単語の最小幅に達すると、そのアイテムに割り当てられていた残りの縮小ウェイトは兄弟に再分配されます。
  • 親の height が子の合計より小さい固定値の場合、vstack でも動作します。
  • link は子の縮小ウェイトを引き継ぐため、リンク付きテキストも通常のテキストと同様に縮小して折り返されます。

examples/flex-shrink.ts を参照してください。

絶対配置

ボックスは、CSS に似た簡易的な配置モデルを使用できます。

vstack(
  { width: 240, height: 120, position: "relative", padding: 16 },
  text("Receipt", { size: 18, font: bold }),
  hstack(
    { position: "absolute", top: 12, right: 12, width: 70 },
    text("PAID", { size: 14, font: bold, align: "center", width: 70 })
  )
)

動作:

  • 位置指定されたボックスは、すべて絶対配置の子孫ボックスの包含ブロックになります。
  • position: "absolute" を指定すると、vstack または hstack は通常のスタックフローから外れます。
  • 絶対配置のボックスは通常の子要素の後に描画されるため、スタンプ、バッジ、オーバーレイ、透かしに使用できます。
  • top、right、bottom、left は、最も近い位置指定された祖先からのポイント単位のオフセットです。該当する祖先がない場合は、現在の render() ルートが基準になります。
  • left と right の両方が設定され、width が省略されている場合、ボックスは残りの幅いっぱいに広がります。top と bottom の組み合わせも、高さについて同様に動作します。
  • 絶対配置された兄弟要素は、zIndex の小さい順から大きい順に描画されます。同じ zIndex のボックスは、ドキュメントの順序を維持します。
  • 親のサイズ計測、間隔、フレックスの伸縮、ページ分割では、絶対配置されたボックスは無視されます。配置を安定させる必要がある場合は、包含ボックスに固定の width と height を指定してください。

制限事項

  • 位置指定では、相対位置の包含ボックス、通常フローから外れた絶対配置ボックス、ポイント単位のオフセット、zIndex、対向する辺の組み合わせによる引き伸ばしをサポートしています。
  • フォントのシェーピングは、pdf-lib と fontkit のサポートに従います。複雑なインド系文字、アラビア語、タイ語のスクリプトには、HarfBuzz ベースのスタックが必要です。現在利用可能な HarfBuzz スタックは、Cloudflare Workers 以外のランタイムを対象としています。
  • streamFlow は段階的な生成をサポートしています。PDF のリニアライズ(バイトストリームを並べ替え、1 バイト目を 1 ページ目にする処理)は、別途行う後処理として残っています。

ライセンス

MIT © Erik Aronesty

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。