mcpField
D
document-generation-pdf

document-generation-pdf

github.com/curiositechドキュメント257273 回インストール2 日前に更新
リポジトリ

document-generation-pdf について

Document Generation & PDF Automationは、法律、人事、業務のワークフローでPDFの生成、フォーム入力、文書の組み立てを自動化する開発者向けスキルです。技術選定、実装パターン、本番運用時の確認事項を扱います。

主な機能

  1. 01PDFライブラリ選定のガイダンス
  2. 02プログラムによるフォーム入力
  3. 03HTMLからPDFを生成する方法
  4. 04PDFの結合と文書パケット作成
  5. 05本番運用とデータ取扱いのチェックリスト

活用シーン

  1. 01政府フォームの一括入力の自動化
  2. 02請求書や証明書の生成
  3. 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.json
references/
document-assembly.md
pdf-lib-guide.md
puppeteer-templates.md
scripts/
document_assembler.ts
form_filler.ts

詳細

文書生成と PDF 自動化

法律、人事、ビジネスのワークフロー向けに、PDF 文書をプログラムで生成、入力、組み立てるエキスパートです。

使用する場面

✅ 次の場合に使用:

  • 法律フォームの自動化(前科抹消、移民、契約)
  • 請求書/領収書の大規模生成
  • 証明書の作成(修了、参加、受賞)
  • テンプレートからの契約書の組み立て
  • 政府機関のフォーム入力(IRS、裁判所への提出書類)
  • 複数文書のパケット作成

❌ 次の場合には使用しない:

  • PDF の簡単な閲覧(ブラウザーまたは pdf.js を使用)
  • 基本的なファイル変換(オンラインツールを使用)
  • OCR によるテキスト抽出(Tesseract.js または AWS Textract を使用)
  • PDF の手作業による編集(Adobe Acrobat を使用)

テクノロジーの選択

pdf-lib と Puppeteer と LaTeX の比較
機能pdf-libPuppeteerLaTeX
フォーム入力✅ ネイティブ対応❌ 複雑❌ 非対応
テンプレートのレンダリング❌ 非対応✅ HTML/CSS✅ テンプレート
パフォーマンス(PDF 1000 件)5s60s30s
ファイルサイズ小中小
署名フィールド✅ 対応❌ 非対応❌ 非対応
最適な用途政府機関のフォーム請求書、レポート学術論文

タイムライン:

  • 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 (&lt;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 オーバーレイ | 座標マッピング

コメント