document-generation-pdf について
Document Generation & PDF Automationは、法律、人事、業務のワークフローでPDFの生成、フォーム入力、文書の組み立てを自動化する開発者向けスキルです。技術選定、実装パターン、本番運用時の確認事項を扱います。
主な機能
- 01PDFライブラリ選定のガイダンス
- 02プログラムによるフォーム入力
- 03HTMLからPDFを生成する方法
- 04PDFの結合と文書パケット作成
- 05本番運用とデータ取扱いのチェックリスト
活用シーン
- 01政府フォームの一括入力の自動化
- 02請求書や証明書の生成
- 03複数文書のパケット作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill document-generation-pdfインストール後、Claude Code で/document-generation-pdfを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.claude-plugin/plugin.jsonreferences/document-assembly.mdpdf-lib-guide.mdpuppeteer-templates.mdscripts/document_assembler.tsform_filler.ts詳細
文書生成と PDF 自動化
法律、人事、ビジネスのワークフロー向けに、PDF 文書をプログラムで生成、入力、組み立てるエキスパートです。
使用する場面
✅ 次の場合に使用:
- 法律フォームの自動化(前科抹消、移民、契約)
- 請求書/領収書の大規模生成
- 証明書の作成(修了、参加、受賞)
- テンプレートからの契約書の組み立て
- 政府機関のフォーム入力(IRS、裁判所への提出書類)
- 複数文書のパケット作成
❌ 次の場合には使用しない:
- PDF の簡単な閲覧(ブラウザーまたは pdf.js を使用)
- 基本的なファイル変換(オンラインツールを使用)
- OCR によるテキスト抽出(Tesseract.js または AWS Textract を使用)
- PDF の手作業による編集(Adobe Acrobat を使用)
テクノロジーの選択
pdf-lib と Puppeteer と LaTeX の比較
| 機能 | pdf-lib | Puppeteer | LaTeX |
|---|---|---|---|
| フォーム入力 | ✅ ネイティブ対応 | ❌ 複雑 | ❌ 非対応 |
| テンプレートのレンダリング | ❌ 非対応 | ✅ HTML/CSS | ✅ テンプレート |
| パフォーマンス(PDF 1000 件) | 5s | 60s | 30s |
| ファイルサイズ | 小 | 中 | 小 |
| 署名フィールド | ✅ 対応 | ❌ 非対応 | ❌ 非対応 |
| 最適な用途 | 政府機関のフォーム | 請求書、レポート | 学術論文 |
タイムライン:
- 2000年代:学術文書には LaTeX を使用
- 2010年:生成に PDFKit(Node.js)を使用
- 2017年:HTML → PDF 変換に Puppeteer を使用
- 2019年:純粋な JS によるフォーム入力に pdf-lib を使用
- 2024年:pdf-lib はフォーム処理の標準です
判断ツリー:
Need to fill existing form? → pdf-lib
Need complex layouts? → Puppeteer (HTML/CSS)
Need academic formatting? → LaTeX
Need to merge PDFs? → pdf-lib
Need digital signatures? → pdf-lib + DocuSign API
よくあるアンチパターン
アンチパターン 1:簡単なフォーム入力に Puppeteer を使用する
初心者の考え:「何にでも Puppeteer を使います。汎用性が高いので」
問題:12 倍遅く、メモリを 10 倍消費し、フォームフィールドを保持できません。
誤った方法:
// ❌ Puppeteer for simple form filling (SLOW!)
import puppeteer from 'puppeteer';
async function fillForm(data: FormData): Promise<Buffer> {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Load PDF in browser
await page.goto(`file://${pdfPath}`);
// Somehow fill form fields? (hacky)
await page.evaluate((data) => {
// Can't easily access PDF form fields from DOM
// Would need to convert PDF → HTML first
}, data);
const pdf = await page.pdf();
await browser.close();
return pdf;
}
誤りの理由:
- ブラウザーのオーバーヘッド(インスタンスあたり 200MB 以上の RAM)
- PDF のネイティブフォームフィールドにアクセスできない
- インタラクティブなフォーム機能が失われる
- pdf-lib より 12 倍遅い
正しい方法:
// ✅ pdf-lib for form filling (FAST!)
import { PDFDocument } from 'pdf-lib';
async function fillForm(templatePath: string, data: FormData): Promise<Uint8Array> {
// Load existing PDF form
const existingPdfBytes = await fs.readFile(templatePath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Get form
const form = pdfDoc.getForm();
// Fill text fields
form.getTextField('applicant_name').setText(data.name);
form.getTextField('case_number').setText(data.caseNumber);
form.getTextField('date_of_birth').setText(data.dob);
// Fill checkboxes
if (data.hasPriorConvictions) {
form.getCheckBox('prior_convictions').check();
}
// Fill dropdowns
form.getDropdown('state').select(data.state);
// Flatten form (make non-editable)
form.flatten();
// Save
return await pdfDoc.save();
}
パフォーマンスの比較(PDF 1000 件):
- Puppeteer:60 秒、4GB RAM
- pdf-lib:5 秒、200MB RAM
タイムラインの背景:
- 2017年:Puppeteer がリリースされ、PDF に誰もが使用
- 2019年:pdf-lib がリリースされ、適切なフォーム処理が可能に
- 2024年:政府機関のフォームには pdf-lib が標準
アンチパターン 2:フォームをフラット化しない
問題:ユーザーが入力済みフォームを編集できるため、データに不整合が生じます。
誤った方法:
// ❌ Don't flatten - form stays editable
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
form.getTextField('name').setText('John Doe');
const pdfBytes = await pdfDoc.save();
// User can open PDF and change "John Doe" to anything!
誤りの理由:
- ユーザーが公式文書を変更できる
- データベースのデータと一致しなくなる
- 文書の完全性が損なわれる
正しい方法:
// ✅ Flatten form after filling
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
form.getTextField('name').setText('John Doe');
// Flatten (convert fields to static text)
form.flatten();
const pdfBytes = await pdfDoc.save();
// User can't edit filled values ✅
フラット化しない場合:
- 下書き文書(ユーザーによる確認・編集が必要)
- 複数ステップのワークフロー(途中までの入力)
- ユーザーが手動で入力するテンプレート
アンチパターン 3:改ページを設定せずに HTML から PDF を生成する
初心者の考え:「Puppeteer を使えば HTML → PDF は簡単です」
問題:文の途中でページが切り替わります。
誤った方法:
// ❌ No page break control
const html = `
<div class="contract">
<h1>Mutual Agreement</h1>
<p>Long paragraph that might split across pages...</p>
<section>
<h2>Terms and Conditions</h2>
<ol>
<li>Term 1 that could get cut off...</li>
<li>Term 2...</li>
</ol>
</section>
</div>
`;
const pdf = await page.pdf({ format: 'A4' });
// Result: Ugly page breaks in middle of sections
正しい方法:
// ✅ Explicit page break control with CSS
const html = `
<style>
@media print {
.page-break { page-break-after: always; }
.avoid-break { page-break-inside: avoid; }
h1, h2, h3 {
page-break-after: avoid;
page-break-inside: avoid;
}
section {
page-break-inside: avoid;
}
}
</style>
<div class="contract">
<section class="avoid-break">
<h1>Mutual Agreement</h1>
<p>This entire section stays together...</p>
</section>
<div class="page-break"></div>
<section class="avoid-break">
<h2>Terms and Conditions</h2>
<ol>
<li>Term 1</li>
<li>Term 2</li>
</ol>
</section>
</div>
`;
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in'
}
});
CSS の印刷用プロパティ:
page-break-before: always- 要素の前で強制的に改ページpage-break-after: always- 要素の後で強制的に改ページpage-break-inside: avoid- 要素をページ内に収める
アンチパターン 4:署名フィールドを処理しない
問題:生成された PDF で署名フィールドをクリックできません。
誤った方法:
// ❌ Add signature as image (not a real signature field)
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const signatureImage = await pdfDoc.embedPng(signaturePngBytes);
const page = pdfDoc.getPage(0);
page.drawImage(signatureImage, {
x: 100,
y: 100,
width: 200,
height: 50
});
await pdfDoc.save();
// Not a real signature field - can't be signed digitally
誤りの理由:
- 法的に認められない(単なる画像にすぎない)
- DocuSign/Adobe Sign を使用できない
- 署名のメタデータ(誰が、いつ)がない
正しい方法 1:署名フィールドを作成する(DocuSign 用)
// ✅ Create signature field for electronic signing
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
// Create signature field
const signatureField = form.createTextField('applicant_signature');
signatureField.addToPage(pdfDoc.getPage(0), {
x: 100,
y: 100,
width: 200,
height: 50
});
// Mark as signature field (metadata)
signatureField.updateWidgets({
borderWidth: 1,
borderColor: { type: 'RGB', red: 0, green: 0, blue: 0 }
});
await pdfDoc.save();
// DocuSign can now detect and fill this field ✅
正しい方法 2:DocuSign API を統合する
// ✅ Send to DocuSign for e-signature
import { ApiClient, EnvelopesApi } from 'docusign-esign';
async function sendForSignature(pdfBytes: Uint8Array, signerEmail: string) {
const apiClient = new ApiClient();
apiClient.setBasePath('https://demo.docusign.net/restapi');
const envelopesApi = new EnvelopesApi(apiClient);
const envelope = {
emailSubject: 'Please sign: Expungement Petition',
documents: [{
documentBase64: Buffer.from(pdfBytes).toString('base64'),
name: 'Petition.pdf',
fileExtension: 'pdf',
documentId: '1'
}],
recipients: {
signers: [{
email: signerEmail,
name: 'John Doe',
recipientId: '1',
tabs: {
signHereTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '100'
}]
}
}]
},
status: 'sent'
};
return await envelopesApi.createEnvelope(accountId, { envelopeDefinition: envelope });
}
アンチパターン 5:入力済み PDF を暗号化せずに保存する
問題:機密性の高い法律文書が平文で保存されます。
誤った方法:
// ❌ Save PDF to disk unencrypted
const pdfBytes = await pdfDoc.save();
await fs.writeFile(`./documents/${caseId}.pdf`, pdfBytes);
// Sensitive data accessible to anyone with file system access
誤りの理由:
- 法律/医療データが漏えいする
- HIPAA/GDPR に違反する
- データ侵害の責任が発生する
正しい方法 1:保存時に暗号化する
// ✅ Encrypt PDF with user password
const pdfBytes = await pdfDoc.save({
userPassword: generateSecurePassword(),
ownerPassword: process.env.PDF_OWNER_PASSWORD,
permissions: {
printing: 'highResolution',
modifying: false,
copying: false,
annotating: false,
fillingForms: false,
contentAccessibility: true,
documentAssembly: false
}
});
await fs.writeFile(`./documents/${caseId}.pdf`, pdfBytes);
正しい方法 2:暗号化ストレージ(SSE を使用する S3)に保存する
// ✅ Upload to S3 with server-side encryption
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3Client = new S3Client({ region: 'us-east-1' });
await s3Client.send(new PutObjectCommand({
Bucket: 'expungement-documents',
Key: `cases/${caseId}/petition.pdf`,
Body: pdfBytes,
ServerSideEncryption: 'AES256',
Metadata: {
caseId: caseId,
documentType: 'petition',
generatedAt: new Date().toISOString()
},
ACL: 'private' // Not publicly accessible
}));
// Store S3 URL in database (encrypted)
await db.documents.insert({
case_id: caseId,
s3_url: encrypt(`s3://expungement-documents/cases/${caseId}/petition.pdf`),
created_at: new Date()
});
コンプライアンス要件:
- HIPAA:保存時と転送時の暗号化、アクセスログの記録
- GDPR:削除する権利、データ最小化
- 州弁護士会の規則:弁護士と依頼人間の秘匿特権の保護
本番環境向けチェックリスト
□ Form fields filled correctly (test with validation)
□ Forms flattened after filling (non-editable)
□ Page breaks controlled (no mid-sentence splits)
□ Signature fields created (for DocuSign/Adobe Sign)
□ PDFs encrypted at rest (S3 SSE or user password)
□ Access logged (who viewed/downloaded)
□ Auto-deletion scheduled (retention policy)
□ Fonts embedded (cross-platform compatibility)
□ File size optimized (<5MB per document)
□ Batch generation tested (1000+ PDFs)
使用する場合と避ける場合
| シナリオ | 適切ですか? |
|---|---|
| 50 件以上の政府機関の書類に記入する | ✅ はい - pdf-lib で自動化します |
| テンプレートから請求書を生成する | ✅ はい - HTML から Puppeteer で生成します |
| 証明書を大規模に作成する | ✅ はい - pdf-lib または LaTeX を使います |
| 複数の書類をまとめたパケットを作成する | ✅ はい - pdf-lib で結合します |
| ブラウザーで PDF を表示する | ❌ いいえ - pdf.js またはブラウザーを使います |
| PDF を手作業で編集する | ❌ いいえ - Adobe Acrobat を使います |
| OCR でテキストを抽出する | ❌ いいえ - Tesseract/Textract を使います |
大規模なフラット PDF オーバーレイ(政府機関の書類)
政府機関の裁判所用書類の多くはフラット PDF(入力可能なフィールドなし)です。正確な X,Y 座標にテキストを重ねる必要があります。書類を一から作成してはいけません。裁判所に却下されます。
3 段階の戦略
1. FILLABLE PDF → Use form.getTextField().setText() (best)
2. FLAT PDF + COORDINATES → Draw text overlays at X,Y positions (this section)
3. NO TEMPLATE EXISTS → Emergency fallback with warning banner (worst)
アンチパターン 6:書類を一から作成する
初心者の考え方:「PDF にフォームフィールドがないので、新しい PDF を作ればいい」
問題点:裁判所は公式でない書類を却下します。独自のレイアウトは公式書類と一致しません。
誤った方法:
// ❌ NEVER DO THIS - courts reject custom forms
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]);
page.drawText('IN THE CIRCUIT COURT OF...', { x: 200, y: 720 });
page.drawText(`Defendant: ${data.name}`, { x: 100, y: 600 });
// Result: Court clerk says "This isn't our form" and rejects filing
正しい方法:公式テンプレートの上に描画する
// ✅ Load official form, draw text in blank spaces
const templateBytes = await fs.readFile('public/forms/OR-set-aside-motion.pdf');
const pdfDoc = await PDFDocument.load(templateBytes);
const page = pdfDoc.getPage(3); // Page 4 is the form
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
// Draw in blank spaces at measured coordinates
page.drawText(data.county.toUpperCase(), { x: 310, y: 700, size: 11, font });
page.drawText(data.caseNumber, { x: 432, y: 655, size: 10, font });
page.drawText(data.fullName, { x: 72, y: 568, size: 11, font });
大規模な座標の測定
問題点:50 州 × 10 種類以上の書類について、X,Y 座標を手作業で測定するのは不可能です。
解決策 1:ビジュアル座標選択ツール
pdf-coordinates を使います。PDF をクリックして X,Y 座標を取得できます。
- PDF をアップロードし、空欄をクリックします
- 座標系として左下(PDF 標準)を選択します
- 座標ラベル付きの注釈入り PDF をエクスポートします
- オフラインで動作し、データはアップロードされません
解決策 2:JSON 設定(Zerodha のパターン)
座標はコードではなく、JSON 設定で定義します。
// field-mappings/oregon-set-aside.json
{
"templatePath": "/forms/or/OR-criminal-set-aside.pdf",
"pageIndex": 3,
"fields": [
{
"name": "county",
"x": 310,
"y": 700,
"fontSize": 11,
"maxWidth": 120,
"transform": "uppercase"
},
{
"name": "caseNumber",
"x": 432,
"y": 655,
"fontSize": 10,
"maxWidth": 100,
"maxChars": 12
},
{
"name": "fullName",
"x": 72,
"y": 568,
"fontSize": 11,
"maxWidth": 250,
"shrinkToFit": true
}
]
}
解決策 3:デバッググリッドのオーバーレイ
開発中に一時的なグリッドを追加します。
async function drawDebugGrid(page: PDFPage, font: PDFFont) {
const { width, height } = page.getSize();
// Draw grid every 50 points
for (let x = 0; x <= width; x += 50) {
page.drawLine({
start: { x, y: 0 },
end: { x, y: height },
thickness: 0.2,
color: rgb(0.9, 0.9, 0.9),
});
if (x % 100 === 0) {
page.drawText(String(x), { x: x + 2, y: 5, size: 6, font });
}
}
// Same for horizontal lines...
}
テキストのはみ出しへの対処
問題点:長い名前が隣のフィールドにはみ出したり、途中で切れたりします。
3 つのはみ出し対策:
// 1. Truncate with ellipsis
function truncate(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return text.slice(0, maxChars - 2) + '..';
}
// 2. Shrink font to fit (min 7pt for readability)
function shrinkToFit(text: string, maxWidthPts: number, startSize: number): number {
let fontSize = startSize;
const charWidth = (size: number) => size * 0.52; // Helvetica avg
while (fontSize > 7 && text.length * charWidth(fontSize) > maxWidthPts) {
fontSize -= 0.5;
}
return fontSize;
}
// 3. Multi-line wrap (for addresses)
page.drawText(longAddress, {
x: 100,
y: 500,
size: 9,
font,
maxWidth: 200, // pdf-lib auto-wraps at this width
lineHeight: 12,
});
フィールド入力の制約 - データ収集時にはみ出しを防ぎます。
export const FIELD_CONSTRAINTS = {
fullName: { maxLength: 35 },
county: { maxLength: 12 },
caseNumber: { maxLength: 12 },
agency: { maxLength: 28 },
charges: { maxLength: 45 },
} as const;
複数の州の書類を扱う本番環境のワークフロー
1. DOWNLOAD official forms
└─ Store in /public/forms/{state}/ with metadata.json
2. INSPECT each form
└─ Run pdf-lib inspection to check: fillable? how many fields?
3. CATEGORIZE forms
├─ Fillable → Map field names in field-mappings/{state}.ts
└─ Flat → Measure coordinates with pdf-coordinates tool
4. CREATE JSON config for flat PDFs
└─ One JSON file per form with all X,Y coordinates
5. TEST with debug grid
└─ Generate test PDF with colored text + coordinate labels
6. QA visual verification
└─ Compare filled PDF against original blank form
7. DOCUMENT constraints
└─ Export maxLength/maxWidth limits for form inputs
法的な提出要件
裁判所の電子申請システムで必要な条件:
- ✅ フラット化された書類(インタラクティブフィールドなし)
- ✅ デジタル署名のメタデータなし(DocuSign は電子申請で問題を起こします)
- ✅ 標準フォントが埋め込まれている(Helvetica、Times)
- ✅ ファイルサイズが 10MB 未満
- ✅ 公式書類のレイアウトと完全に一致している
裁判所向けのフラット化:
// If form had fields, flatten them
const form = pdfDoc.getForm();
if (form.getFields().length > 0) {
form.flatten(); // Converts to static text
}
// For overlay text, it's already static - no flattening needed
const pdfBytes = await pdfDoc.save();
座標系のリファレンス
PDF coordinate system (pdf-lib default):
- Origin: Bottom-left corner (0, 0)
- X increases: Left → Right
- Y increases: Bottom → Top
- Units: Points (72 points = 1 inch)
- Letter size: 612 x 792 points (8.5" x 11")
To convert from top-left origin (e.g., Adobe Acrobat display):
y_pdf = 792 - y_topLeft
Common positions (Letter size):
- Top margin: y = 720-750
- Header area: y = 700-750
- Body start: y = 650-700
- Left margin: x = 50-72
- Right edge: x = 540-560
- Bottom margin: y = 50-72
参考資料
/references/pdf-lib-guide.md- フォームへの記入、フィールドの種類、フラット化、暗号化/references/puppeteer-templates.md- HTML テンプレート、改ページ、印刷用スタイル設定/references/document-assembly.md- PDF の結合、パケットの作成、透かし- pdf-coordinates - ビジュアル X,Y 座標選択ツール
- Zerodha pdf_text_overlay - JSON 設定のパターン
スクリプト
scripts/form_filler.ts- JSON データから PDF フォームに記入し、バッチ処理しますscripts/document_assembler.ts- 複数の PDF を結合し、表紙や透かしを追加しますscripts/generate-test-overlay-pdf.ts- デバッググリッドでオーバーレイ座標をテストします
このスキルの対象:PDF 生成 | フォームへの記入 | 書類の自動化 | デジタル署名 | pdf-lib | Puppeteer | LaTeX | DocuSign | フラット PDF オーバーレイ | 座標マッピング