npm package: @gstypecode/pdf-field-extractor
Description: PDF field extraction using configurable patterns and coordinates
Latest version: 1.0.0

# PDF Region Extractor

![npm version](https://img.shields.io/npm/v/pdf-region-extractor.svg)
![Node.js Version](https://img.shields.io/node/v/pdf-region-extractor.svg)
![License](https://img.shields.io/npm/l/pdf-region-extractor.svg)

PDFから特定領域（region/field）を抽出するNode.jsパッケージ。設定ファイルベースで座標とパターンを指定し、効率的な情報抽出を実現します。

## 🚀 特徴

- **設定ファイルベース**: JSONファイルで座標・パターン・ページを指定
- **正規表現対応**: 複雑なパターンマッチングをサポート
- **座標指定抽出**: PDF内の特定領域からテキストを抽出
- **Web UI**: 設定エディタは別リポジトリ [pdf-config-editor](../pdf-config-editor) で提供
- **高性能**: 大量のPDFファイルを効率的に処理
- **TypeScript対応**: 型安全な開発体験

## 📦 インストール

```bash
npm install pdf-region-extractor
```

## 🔧 クイックスタート

### 1. 基本的な使用方法

```javascript
const PdfFieldExtractor = require('pdf-region-extractor');
const fs = require('fs').promises;

const config = {
  fields: {
    invoiceNumber: {
      description: "請求書番号",
      pattern: "請求書番号[\\s:：]*([A-Z0-9-]+)",
      pages: "all",
      region: { xMin: 100, xMax: 300, yMin: 50, yMax: 100 },
      method: "regex"
    }
  }
};

// Buffer から抽出（ライブラリ利用が主）
const pdfBuffer = await fs.readFile('./invoice.pdf');
const result = await PdfFieldExtractor.extract(pdfBuffer, config);

// または、ファイルパスから直接抽出（CLI利用など）
// const result = await PdfFieldExtractor.extractFromFile('./invoice.pdf', config);

console.log(result);
// {
//   success: true,
//   data: { invoiceNumber: "INV-2024-001" },
//   metadata: { processingTime: 120, extractionType: "field_extraction", ... }
// }
```

### 2. 設定ファイルを使用

```javascript
const PdfFieldExtractor = require('pdf-region-extractor');
const fs = require('fs').promises;

// 設定ファイルを読み込み
const configData = await fs.readFile('./config/invoice_format.json', 'utf8');
const config = JSON.parse(configData);

// PDFから情報を抽出（Buffer系）
const pdfBuffer = await fs.readFile('./invoice.pdf');
const result = await PdfFieldExtractor.extract(pdfBuffer, config);

// または、ファイルパスから直接抽出（File系）
// const result = await PdfFieldExtractor.extractFromFile('./invoice.pdf', config);
```

### 3. Web UIを使用

設定エディタは別リポジトリ [pdf-config-editor](../pdf-config-editor) に移動しました。
詳細はそちらのREADMEを参照してください。

## 📋 設定ファイル形式

```json
{
  "fields": {
    "invoiceNumber": {
      "description": "請求書番号",
      "pattern": "請求書番号[\\s:：]*([A-Z0-9-]+)",
      "pages": "all",
      "region": {
        "xMin": 100,
        "xMax": 300,
        "yMin": 50,
        "yMax": 100
      },
      "method": "regex"
    },
    "totalAmount": {
      "description": "合計金額",
      "pattern": "合計[\\s:：]*([\\d,]+)円",
      "pages": [1, 2],
      "region": {
        "xMin": 400,
        "xMax": 600,
        "yMin": 700,
        "yMax": 750
      },
      "method": "regex"
    }
  }
}
```

## 🏗️ API設計

### Buffer系とFile系の使い分け

このライブラリは2種類のAPIを提供します：

**Buffer系メソッド**（主な用途：npmパッケージとして利用）
- `extract(pdfBuffer, config, options)`
- `extractByPages(pdfBuffer, config, options)`

**File系メソッド**（主な用途：CLI利用）
- `extractFromFile(pdfPath, config, options)`
- `extractByPagesFromFile(pdfPath, config, options)`

**使用例**：
```javascript
const PdfFieldExtractor = require('pdf-region-extractor');
const fs = require('fs').promises;

// ライブラリとして利用（Buffer系）
const pdfBuffer = await fs.readFile('./invoice.pdf');
const result = await PdfFieldExtractor.extract(pdfBuffer, config);

// CLIツールとして利用（File系）
const result = await PdfFieldExtractor.extractFromFile('./invoice.pdf', config);
```

**推奨**：ほとんどのケースではnpmパッケージとして利用するため、**Buffer系メソッドを使用**してください。

## 🎯 API仕様

### `extract(pdfBuffer, config, options)`

PDFバッファから指定されたフィールドを抽出します（ライブラリ利用推奨）。

**パラメータ:**
- `pdfBuffer` (Buffer): PDFファイルのバッファ
- `config` (object): 設定オブジェクト
- `options` (object, optional): 抽出オプション
  - `debug` (boolean): デバッグモード有効化
  - `pageNumber` (number): 特定ページのみ抽出
  - `timeout` (number): タイムアウト時間（ミリ秒）

**戻り値:**
- `Promise<object>`: 抽出結果オブジェクト
  - `success` (boolean): 処理成功可否
  - `data` (object): 抽出されたデータ（形式は設定により異なる）
  - `metadata` (object): 処理情報（処理時間、ページ数、マッチ数など）
  - `error` (string): エラーメッセージ（失敗時のみ）

詳細な出力形式については [📤 出力スキーマ](#-出力スキーマoutput-schema) を参照してください。

**例:**
```javascript
const pdfBuffer = await fs.readFile('./invoice.pdf');
const result = await PdfFieldExtractor.extract(pdfBuffer, config, {
  debug: true,
  timeout: 30000
});

// 結果例
// {
//   success: true,
//   data: { invoiceNumber: "INV-2024-001", totalAmount: "50,000" },
//   metadata: { processingTime: 150, pageCount: "auto", ... }
// }
```

### `extractFromFile(pdfPath, config, options)`

PDFファイルパスから指定されたフィールドを抽出します（CLI利用など）。

**パラメータ:**
- `pdfPath` (string): PDFファイルパス
- `config` (object): 設定オブジェクト
- `options` (object, optional): 抽出オプション（`extract`と同じ）

**戻り値:**
- `Promise<object>`: `extract`と同じ形式の抽出結果オブジェクト

**例:**
```javascript
const result = await PdfFieldExtractor.extractFromFile('./invoice.pdf', config, {
  debug: true
});
```

### `loadConfig(configPath)`

設定ファイルを読み込みます。

**パラメータ:**
- `configPath` (string): 設定ファイルパス

**戻り値:**
- `object`: 設定オブジェクト

### `validateConfig(config)`

設定オブジェクトの妥当性を検証します。

**パラメータ:**
- `config` (object): 設定オブジェクト

**戻り値:**
- `boolean`: 妥当性チェック結果

### extract と extractByPages の使い分け

| メソッド | 用途 | 返り値 | ページ処理 |
|---------|------|--------|----------|
| `extract` | 全ページからマッチしたデータを取得 | マッチしたデータのみ | 一括処理 |
| `extractByPages` | 各ページの成功・失敗を個別に取得 | 全ページ分の結果配列<br>`pageResults.length === totalPages` | ページごと処理 |

**extract を使うべき場合**：
- マッチしたデータだけが必要
- ページ情報は不要
- シンプルな抽出

**extractByPages を使うべき場合**：
- 各ページの一致・不一致を確認したい
- マッチしなかったページを特定したい
- ページごとの処理結果が必要

**重要な違い**：
```javascript
// extract: マッチしたデータのみ
const result = await PdfFieldExtractor.extract(pdfBuffer, config);
// result.data.fieldName: マッチした値の配列（例：253個）

// extractByPages: 全ページ分の結果
const result = await PdfFieldExtractor.extractByPages(pdfBuffer, config);
// result.pageResults.length: 260（全ページ数）
// result.metadata.successfulPages: 253（マッチしたページ数）
// result.metadata.failedPages: 7（マッチしなかったページ数）
```

### `extractByPages(pdfBuffer, config, options)`

PDFファイルの各ページごとに情報を抽出します。**全ページの処理結果（成功・失敗含む）を取得できます**。

**重要**: `extract` メソッドと異なり、このメソッドは **`pageResults.length === totalPages`** となり、マッチしなかったページも含めて全ページ分の結果が返されます。

**パラメータ:**
- `pdfBuffer` (Buffer): PDFファイルのバッファ
- `config` (object): 抽出設定オブジェクト
- `options` (object, optional): 抽出オプション
  - `debug` (boolean): デバッグモード有効化

**戻り値:**
- `Promise<object>`: ページ別抽出結果オブジェクト
  - `success` (boolean): 処理成功可否
  - `extractionType` (string): "page-by-page"
  - `totalPages` (number): 総ページ数
  - `pageResults` (array): **各ページの抽出結果配列（`pageResults.length === totalPages`）**
    - `page` (number): ページ番号
    - `result` (object): extract メソッドと同じ形式の結果
  - `consolidatedData` (object): 全ページのデータを統合したオブジェクト
  - `metadata` (object): 処理情報
    - `successfulPages` (number): 抽出成功ページ数
    - `failedPages` (number): 抽出失敗ページ数

**例:**
```javascript
const fs = require('fs').promises;
const pdfBuffer = await fs.readFile('./invoice.pdf');
const result = await PdfFieldExtractor.extractByPages(pdfBuffer, config, {
  debug: true
});

console.log(`総ページ数: ${result.totalPages}`);
console.log(`pageResults配列の長さ: ${result.pageResults.length}`);  // totalPagesと同じ
console.log(`成功: ${result.metadata.successfulPages}ページ`);
console.log(`失敗: ${result.metadata.failedPages}ページ`);

// 各ページの結果を確認
result.pageResults.forEach(({ page, result }) => {
  if (result.success && result.data) {
    // 抽出されたすべてのフィールドを表示
    const extractedFields = Object.keys(result.data).filter(key => result.data[key] !== null);
    console.log(`ページ${page}: ${extractedFields.join(', ')}`);
  } else {
    console.log(`ページ${page}: マッチなし`);
  }
});
```

**使用例：全ページの一致・不一致を確認**
```javascript
const result = await PdfFieldExtractor.extractByPages(pdfBuffer, config);

// マッチしなかったページを特定
const unmatchedPages = result.pageResults
  .filter(({ result }) => !result.success || Object.values(result.data || {}).every(v => v === null))
  .map(({ page }) => page);

console.log('マッチしなかったページ:', unmatchedPages);
console.log(`全${result.totalPages}ページ中、${unmatchedPages.length}ページがマッチしませんでした`);
```

## 📤 出力スキーマ（Output Schema）

### 基本構造

すべての抽出結果は以下の統一された形式で返されます：

```javascript
{
  success: true,           // 処理成功可否
  data: { ... },           // 抽出データ（形式はモードにより異なる）
  metadata: {
    processingTime: 150,   // 処理時間（ミリ秒）
    pageCount: "auto",     // 処理ページ数
    matchCount: 3,         // マッチ数
    formatName: "設定名",   // 設定ファイル名
    extractionType: "field_extraction"  // 抽出タイプ
  }
}
```

### 1. フィールド抽出モード（Field Extraction）

**設定タイプ**: `field_extraction`

#### 出力ルール

- **マッチなし**: `null`
- **1要素のみマッチ**: 文字列
- **複数要素マッチ**: 配列

#### 例1: 単一マッチ

```javascript
// 設定
{
  "fields": {
    "invoiceNumber": {
      "pattern": "INV-\\d+",
      "region": { "xMin": 100, "xMax": 300, "yMin": 50, "yMax": 100 }
    }
  }
}

// 出力
{
  "success": true,
  "data": {
    "invoiceNumber": "INV-2024-001"  // 文字列
  },
  "metadata": { ... }
}
```

#### 例2: 複数テキストボックスマッチ

PDFの指定座標領域内に複数のテキストボックスが存在し、それぞれが正規表現にマッチした場合：

```javascript
// 設定
{
  "fields": {
    "studentId": {
      "pattern": "\\d+.*",
      "region": { "xMin": 40, "xMax": 200, "yMin": 100, "yMax": 150 }
    }
  }
}

// PDF内のテキストボックス構造:
// - テキストボックス1: "31030 赤瀬 龍臣"
// - テキストボックス2: "]"

// 出力
{
  "success": true,
  "data": {
    "studentId": ["31030 赤瀬 龍臣", "]"]  // 配列
  },
  "metadata": { ... }
}
```

**重要**: 各テキストボックスは個別に正規表現チェックされ、マッチした要素が配列として返されます。

#### 例3: マッチなし

```javascript
{
  "success": true,
  "data": {
    "invoiceNumber": null  // マッチなし
  },
  "metadata": { ... }
}
```

### 2. 手動テーブル抽出モード（Manual Table）

**設定タイプ**: `manual_table`

#### 出力構造

```javascript
{
  "success": true,
  "data": {
    "テーブル名": [
      {
        "column1": "値1",
        "column2": "値2",
        "column3": "値3"
      },
      {
        "column1": "値4",
        "column2": "値5",
        "column3": "値6"
      }
    ]
  },
  "metadata": {
    "processingTime": 200,
    "extractionType": "manual_table",
    ...
  }
}
```

#### 例: 請求書明細テーブル

```javascript
// 設定
{
  "tableDefinition": {
    "tableName": "invoiceItems",
    "columns": [
      { "name": "itemName", "xRange": { "min": 40, "max": 200 }, "pattern": ".+" },
      { "name": "quantity", "xRange": { "min": 210, "max": 260 }, "pattern": "\\d+" },
      { "name": "amount", "xRange": { "min": 270, "max": 340 }, "pattern": "[\\d,]+" }
    ]
  }
}

// 出力
{
  "success": true,
  "data": {
    "invoiceItems": [
      {
        "itemName": "商品A",
        "quantity": "5",
        "amount": "10,000"
      },
      {
        "itemName": "商品B",
        "quantity": "3",
        "amount": "6,000"
      }
    ]
  },
  "metadata": { ... }
}
```

### 3. 自動テーブル検出モード（Auto Table Detection）

**設定タイプ**: `auto_table`

#### 出力構造（table_matrix形式）

```javascript
{
  "success": true,
  "data": {
    "テーブル名": {
      "matrix": [
        [
          {
            "row": 0,
            "column": 0,
            "combinedText": "セル内容",
            "textBoxes": [...],  // includeCoordinates: true の場合
            "avgX": 50.5,
            "avgY": 200.3
          },
          // ...
        ],
        // ...
      ],
      "metadata": {
        "detectedRows": 10,
        "detectedColumns": 5,
        "totalCells": 50,
        "clusteringInfo": {
          "verticalClusters": 10,
          "horizontalClusters": 5,
          "algorithm": "dbscan"
        }
      }
    }
  },
  "metadata": { ... }
}
```

#### 出力構造（flat_list形式）

```javascript
{
  "success": true,
  "data": {
    "テーブル名": {
      "cells": [
        { "row": 0, "column": 0, "combinedText": "A1" },
        { "row": 0, "column": 1, "combinedText": "B1" },
        { "row": 1, "column": 0, "combinedText": "A2" },
        // ...
      ],
      "metadata": { ... }
    }
  },
  "metadata": { ... }
}
```

#### 例: 自動テーブル検出

```javascript
// 設定
{
  "autoTableDetection": {
    "tableName": "detectedTable",
    "extractionRegion": { "xMin": 50, "xMax": 550, "yMin": 200, "yMax": 600 },
    "clustering": {
      "algorithm": "dbscan",
      "verticalClustering": { "epsilon": 5.0, "minPoints": 2 },
      "horizontalClustering": { "epsilon": 10.0, "minPoints": 1 }
    },
    "output": {
      "format": "table_matrix",        // "table_matrix" または "flat_list"
      "includeCoordinates": true,      // 座標情報を含める
      "includeMetadata": true          // メタデータを含める
    }
  }
}

// 出力
{
  "success": true,
  "data": {
    "detectedTable": {
      "matrix": [
        [
          { "row": 0, "column": 0, "combinedText": "ヘッダー1", "avgX": 60, "avgY": 210 },
          { "row": 0, "column": 1, "combinedText": "ヘッダー2", "avgX": 150, "avgY": 210 }
        ],
        [
          { "row": 1, "column": 0, "combinedText": "データ1", "avgX": 60, "avgY": 230 },
          { "row": 1, "column": 1, "combinedText": "データ2", "avgX": 150, "avgY": 230 }
        ]
      ],
      "metadata": {
        "detectedRows": 2,
        "detectedColumns": 2,
        "totalCells": 4,
        "clusteringInfo": {
          "verticalClusters": 2,
          "horizontalClusters": 2,
          "algorithm": "dbscan"
        }
      }
    }
  },
  "metadata": {
    "processingTime": 180,
    "extractionType": "auto_table",
    ...
  }
}
```

### エラー時の出力

処理が失敗した場合は以下の形式で返されます：

```javascript
{
  "success": false,
  "error": "エラーメッセージ",
  "metadata": {
    "processingTime": 50
  }
}
```

### 出力設定オプション

#### フィールドモード

- 複数マッチ時は自動的に配列化
- キャプチャグループ使用時は追加フィールド生成可能

#### テーブルモード（手動）

- カラム定義に基づくオブジェクト配列
- 正規表現マッチングによる値抽出

#### テーブルモード（自動）

| オプション | 説明 | デフォルト |
|----------|------|---------|
| `format` | 出力形式（`table_matrix` / `flat_list`） | `table_matrix` |
| `includeCoordinates` | 座標情報を含める | `true` |
| `includeMetadata` | メタデータを含める | `true` |

## 🔧 Web UI（設定エディタ）

設定エディタは別リポジトリ [pdf-config-editor](../pdf-config-editor) に移動しました。

## 🚨 重要な注意事項

### 座標系について

PDFの座標系は **左下原点** です。Web UIでは自動的に変換されますが、設定ファイルを手動作成する場合は注意してください。

```javascript
// PDF座標系（左下原点）
{ xMin: 100, xMax: 300, yMin: 700, yMax: 750 }

// 画面座標系（左上原点）との変換
const pdfHeight = 842; // A4サイズの場合
const webY = pdfHeight - pdfY;
```

### 正規表現パターン

- **キャプチャグループ**: `()` で囲んだ部分が抽出されます
- **エスケープ**: JSONファイルでは `\\` でエスケープが必要
- **マルチライン**: 複数行にまたがるパターンは `[\s\S]*?` を使用

```javascript
// 良い例
"pattern": "請求書番号[\\s:：]*([A-Z0-9-]+)"

// 悪い例（エスケープ不足）
"pattern": "請求書番号[\s:：]*([A-Z0-9-]+)"
```

### ページ指定

```javascript
// 全ページ
"pages": "all"

// 特定ページ
"pages": [1, 2, 3]

// 単一ページ
"pages": 1
```

## 🐛 トラブルシューティング

### よくある問題

#### 1. PDFが読み込めない

**症状**: PDFファイル選択後にエラーが発生

**原因と解決策**:
- **破損したPDF**: 別のPDFファイルで試す
- **暗号化PDF**: パスワード保護を解除
- **大きなファイル**: ファイルサイズを確認（推奨: 10MB以下）

#### 2. 座標指定で正しく抽出できない

**症状**: 設定した座標からテキストが抽出されない

**原因と解決策**:
- **座標系の間違い**: PDFは左下原点、Web UIは左上原点
- **拡大率の影響**: 100%表示で座標を確認
- **フォントの問題**: 画像化されたテキストは抽出不可

#### 3. 正規表現がマッチしない

**症状**: パターンが正しいのにマッチしない

**原因と解決策**:
- **エスケープ不足**: JSONファイルでは `\\` が必要
- **文字エンコーディング**: 全角・半角の違いを確認
- **改行・空白**: `[\s\S]*?` で対応

#### 4. Web UIがフリーズする

**症状**: 複数操作後にブラウザが応答しない

**原因と解決策**:
- **無限ループ**: 新しいタブで開き直す
- **メモリ不足**: ページをリロード
- **大きなPDF**: PDFファイルサイズを確認

### デバッグモード

```javascript
const pdfBuffer = await fs.readFile('./invoice.pdf');
const result = await PdfFieldExtractor.extract(pdfBuffer, config, {
  debug: true,
  logLevel: 'verbose'
});
```

### ログ出力

```javascript
// 詳細ログを有効化
process.env.DEBUG = 'pdf-region-extractor:*';
```

## 🔬 技術仕様

### 依存関係

- **Node.js**: 14.0.0以上
- **pdf-parse**: PDF解析エンジン
- **PDF.js**: フロントエンド用PDFレンダリング
- **Vue.js 3**: Web UI フレームワーク
- **Express**: Webサーバー

### アーキテクチャ

```
pdf-region-extractor/
├── src/
│   ├── index.js              # メインAPI
│   ├── utils/
│   │   ├── pdf-parser.js     # PDF解析
│   │   ├── validator.js      # 設定検証
│   │   └── pdf-viewer-manager.js  # UI統合管理
│   └── extractor/
│       └── universal-detector.js  # 抽出エンジン
├── config/                   # 設定ファイル
└── tests/                    # テストスイート
```

### パフォーマンス

| 指標 | 値 |
|------|-----|
| 平均処理時間 | < 100ms/page |
| メモリ使用量 | < 50MB/PDF |
| 対応ファイルサイズ | 最大 50MB |
| 同時処理数 | 最大 10ファイル |

### 対応PDF形式

- **PDF バージョン**: 1.4 - 2.0
- **テキストPDF**: ✅ 対応
- **画像PDF**: ❌ 非対応（OCR必要）
- **暗号化PDF**: ❌ 非対応
- **フォーム付きPDF**: ⚠️ 部分対応

## 🧪 開発者向け情報

### 開発環境セットアップ

```bash
# リポジトリクローン
git clone https://github.com/your-org/pdf-region-extractor.git
cd pdf-region-extractor

# 依存関係インストール
npm install

# 開発サーバー起動
npm run dev

# テスト実行
npm test

# カバレッジレポート
npm run test:coverage
```

### TDD開発ルール

このプロジェクトは **t-wada スタイルのTDD** を採用しています。

#### RED-GREEN-REFACTOR サイクル

1. **RED**: テストを書く（失敗することを確認）
2. **GREEN**: テストを通す最小限の実装
3. **REFACTOR**: テストを保持しながら改善

#### 品質基準

- **テストカバレッジ**: 最小80%、目標90%+
- **関数分離**: 1関数1責務
- **JSDoc**: 日本語コメント必須

### コントリビューション

1. **Issue作成**: バグ報告・機能要求
2. **Fork**: リポジトリをフォーク
3. **ブランチ作成**: `feature/xxx` または `fix/xxx`
4. **テスト追加**: 新機能にはテストを追加
5. **Pull Request**: 詳細な説明を記載

### コミット規約

```
feat: 新機能追加
fix: バグ修正
test: テスト追加・修正
refactor: リファクタリング
docs: ドキュメント更新
```

## 📊 テストスイート

### 単体テスト

```bash
# 全テスト実行
npm test

# 特定テスト実行
npm test -- --grep "PDF解析"

# ウォッチモード
npm run test:watch
```

### 統合テスト

```bash
# E2Eテスト実行
npm run test:e2e

# UI テスト実行
npm run test:ui
```

### カバレッジ

```bash
# カバレッジレポート生成
npm run test:coverage

# HTMLレポート表示
open coverage/index.html
```

## 🔄 バージョン管理

### リリース履歴

- **v1.0.0**: 初期リリース
- **v1.1.0**: Web UI追加
- **v1.2.0**: 統合PDF管理システム

### 更新方法

```bash
# マイナーバージョンアップ
npm version minor

# パッチバージョンアップ
npm version patch

# リリース
npm publish
```

## 🆘 サポート

### 問題報告

- **GitHub Issues**: https://github.com/your-org/pdf-region-extractor/issues
- **メールサポート**: support@example.com
- **ドキュメント**: https://pdf-region-extractor.readthedocs.io/

### FAQ

**Q: 画像化されたPDFは対応していますか？**
A: いいえ。テキストレイヤーがあるPDFのみ対応です。OCR処理が必要な場合は別途ツールをご利用ください。

**Q: 暗号化されたPDFは処理できますか？**
A: 現在は非対応です。パスワード保護を解除してからご利用ください。

**Q: 大量のPDFファイルを効率的に処理したい**
A: バッチ処理機能を準備中です。v1.3.0でリリース予定です。

## 📄 ライセンス

MIT License - 詳細は [LICENSE](LICENSE) をご確認ください。

## 🙏 謝辞

- **PDF.js**: Mozilla Foundation
- **pdf-parse**: modesty氏
- **Vue.js**: Evan You氏
- **テスト手法**: t-wada氏のTDD思想

---

**PDF Region Extractor** は効率的な情報抽出を実現するための包括的なソリューションです。