最終更新: 2026/06/22 18:56
MAS-157: 写真(JPEG/PNG)対応の証憑OCR + 電帳法品質チェック
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-157 |
| カテゴリ | 外部データ取込 |
| Phase | P1.5 |
| 優先度 | ★★★ |
| 所要時間 | 1-2時間 |
| 対象ファイル | 500_import/502_receipt_reader.js (既存修正) |
| 前提案件 | なし(独立着手可能) |
目的
現行の 502_receipt_reader.js は PDF のみ対応しているが、スマホで撮影した領収書・レシートの写真(JPEG/PNG)も Gemini OCR で読み取れるよう拡張する。加えて、電帳法スキャナ保存の品質要件(解像度・カラー)を自動チェックし、不適合な画像には再撮影を促す警告を出す。
Gemini API は既に画像入力をサポートしており、主な変更は MIME タイプ判定の拡張と inline_data の動的設定のみ。抽出プロンプトは既存をそのまま流用可能(ADR-0007 補足決定 参照)。
現在のコード
1. ファイル取得(PDF のみ) — 502_receipt_reader.js:39
var files = folder.getFilesByType(MimeType.PDF);
MimeType.PDF 固定のため、JPEG/PNG ファイルは取得されない。
2. MIME タイプ固定 — 502_receipt_reader.js:197
{ inline_data: { mime_type: 'application/pdf', data: base64Pdf } }
mime_type が 'application/pdf' にハードコードされており、画像を渡す場合は動的に設定する必要がある。
3. メニュー表示名 — 101_sys_config.js:319
.addItem('📄 領収書PDFの読み込み (Drive)', 'importReceiptPdfs')
「PDF」という表記が名称に含まれている。画像対応後は「証憑」に変更すべき。
4. 関数名・ファイルヘッダー — 502_receipt_reader.js:1-4, 12
// 📄 05_receipt_reader.js — 領収書PDF読み込み (Google Drive + Gemini API)
// 指定フォルダ内のPDFを Gemini で解析し、receipt タブに出力
function importReceiptPdfs() {
コメントと関数名が PDF 前提。画像を含む汎用名に更新する。
修正方針
Step 1: ファイル取得の MIME タイプ拡張
getFilesByType() は単一 MIME タイプしか受け付けないため、対応する全 MIME タイプのファイルを収集する。
// --- フォルダから証憑ファイル取得(PDF + 画像) ---
var SUPPORTED_MIMES = [
MimeType.PDF, // application/pdf
MimeType.JPEG, // image/jpeg
MimeType.PNG // image/png
];
var allFiles = [];
for (var mi = 0; mi < SUPPORTED_MIMES.length; mi++) {
var iter = folder.getFilesByType(SUPPORTED_MIMES[mi]);
while (iter.hasNext()) {
allFiles.push(iter.next());
}
}
if (allFiles.length === 0) return ui.alert('📄 フォルダ内に対応ファイルがありません。\n対応形式: PDF, JPEG, PNG');
Step 2: MIME タイプの動的設定
callGeminiForReceipt_() に MIME タイプを引数として渡し、inline_data で動的に設定する。
// 呼び出し側(importReceiptPdfs 内)
var blob = file.getBlob();
var mimeType = blob.getContentType(); // 'application/pdf', 'image/jpeg', 'image/png'
var base64 = Utilities.base64Encode(blob.getBytes());
// 画像品質チェック(画像ファイルのみ)
if (mimeType !== 'application/pdf') {
var qualityResult = checkImageQuality_(blob, mimeType);
if (qualityResult.warnings.length > 0) {
Utils.logInfo(FUNC, '⚠️ ' + fileName + ': ' + qualityResult.warnings.join(', '));
if (qualityResult.reject) {
errors.push(fileName + ': 品質不適合 - ' + qualityResult.warnings.join(', '));
errorCount++;
continue;
}
}
}
var extractedList = callGeminiForReceipt_(apiKey, base64, mimeType, fileName);
callGeminiForReceipt_() のシグネチャ変更:
// 変更前
function callGeminiForReceipt_(apiKey, base64Pdf, fileName) {
// 変更後
function callGeminiForReceipt_(apiKey, base64Data, mimeType, fileName) {
ペイロード内の mime_type を動的に:
// 変更前
{ inline_data: { mime_type: 'application/pdf', data: base64Pdf } }
// 変更後
{ inline_data: { mime_type: mimeType, data: base64Data } }
Step 3: 電帳法品質チェック関数の追加
電帳法スキャナ保存要件に基づく画像品質チェックを行うヘルパー関数を追加する。
/**
* 画像の電帳法スキャナ保存品質をチェック
* @param {GoogleAppsScript.Base.Blob} blob - 画像Blob
* @param {string} mimeType - MIMEタイプ
* @returns {{ warnings: string[], reject: boolean }}
*/
function checkImageQuality_(blob, mimeType) {
var warnings = [];
var reject = false;
var bytes = blob.getBytes();
if (mimeType === MimeType.JPEG || mimeType === 'image/jpeg') {
var dims = getJpegDimensions_(bytes);
if (dims) {
// 電帳法: 解像度200dpi以上
// レシート想定サイズ: 約80mm x 200mm (3.15" x 7.87")
// 200dpi × 3.15" ≈ 630px (短辺の最低ライン)
var minDimension = Math.min(dims.width, dims.height);
if (minDimension < 630) {
warnings.push('解像度不足の可能性(短辺' + minDimension + 'px, 推奨630px以上)');
reject = true;
}
// グレースケール判定(JPEGコンポーネント数)
if (dims.components === 1) {
warnings.push('グレースケール画像(カラー必須)');
reject = true;
}
}
} else if (mimeType === MimeType.PNG || mimeType === 'image/png') {
var pngDims = getPngDimensions_(bytes);
if (pngDims) {
var minDimPng = Math.min(pngDims.width, pngDims.height);
if (minDimPng < 630) {
warnings.push('解像度不足の可能性(短辺' + minDimPng + 'px, 推奨630px以上)');
reject = true;
}
// PNGカラータイプ: 0=グレースケール, 2=RGB, 3=パレット, 4=グレー+α, 6=RGBA
if (pngDims.colorType === 0 || pngDims.colorType === 4) {
warnings.push('グレースケール画像(カラー必須)');
reject = true;
}
}
}
// ファイルサイズチェック(極端に小さい画像の警告)
if (bytes.length < 10000) {
warnings.push('ファイルサイズが極端に小さい(' + Math.round(bytes.length / 1024) + 'KB)');
// reject はしない(Gemini に渡して判断させる)
}
return { warnings: warnings, reject: reject };
}
/**
* JPEGバイナリからピクセル寸法とコンポーネント数を取得
* SOFn マーカー (0xFFC0-0xFFC3) をパースする
* @param {number[]} bytes
* @returns {{ width: number, height: number, components: number }|null}
*/
function getJpegDimensions_(bytes) {
if (bytes.length < 2 || bytes[0] !== 0xFF || bytes[1] !== 0xD8) return null;
var i = 2;
while (i < bytes.length - 1) {
if (bytes[i] !== 0xFF) { i++; continue; }
var marker = bytes[i + 1] & 0xFF;
// SOF0-SOF3 (0xC0-0xC3)
if (marker >= 0xC0 && marker <= 0xC3) {
if (i + 9 >= bytes.length) return null;
var height = ((bytes[i + 5] & 0xFF) << 8) | (bytes[i + 6] & 0xFF);
var width = ((bytes[i + 7] & 0xFF) << 8) | (bytes[i + 8] & 0xFF);
var components = bytes[i + 9] & 0xFF;
return { width: width, height: height, components: components };
}
// その他のマーカー: セグメント長を読んでスキップ
if (marker === 0xD9 || marker === 0xDA) break; // EOI or SOS
if (i + 3 >= bytes.length) break;
var segLen = ((bytes[i + 2] & 0xFF) << 8) | (bytes[i + 3] & 0xFF);
i += 2 + segLen;
}
return null;
}
/**
* PNGバイナリからピクセル寸法とカラータイプを取得
* IHDRチャンクをパースする
* @param {number[]} bytes
* @returns {{ width: number, height: number, colorType: number }|null}
*/
function getPngDimensions_(bytes) {
// PNG signature: 137 80 78 71 13 10 26 10
if (bytes.length < 24) return null;
if (bytes[0] !== 137 || bytes[1] !== 80 || bytes[2] !== 78 || bytes[3] !== 71) return null;
// IHDR starts at byte 8 (length) + 4 (type) + data
// Width: bytes 16-19, Height: bytes 20-23, ColorType: byte 25
var width = ((bytes[16] & 0xFF) << 24) | ((bytes[17] & 0xFF) << 16) | ((bytes[18] & 0xFF) << 8) | (bytes[19] & 0xFF);
var height = ((bytes[20] & 0xFF) << 24) | ((bytes[21] & 0xFF) << 16) | ((bytes[22] & 0xFF) << 8) | (bytes[23] & 0xFF);
var colorType = bytes.length > 25 ? (bytes[25] & 0xFF) : -1;
return { width: width, height: height, colorType: colorType };
}
Step 4: コメント・関数名・メニューの更新
502_receipt_reader.js ファイルヘッダー
// 変更前
// 📄 05_receipt_reader.js — 領収書PDF読み込み (Google Drive + Gemini API)
// 指定フォルダ内のPDFを Gemini で解析し、receipt タブに出力
// 変更後
// 📄 05_receipt_reader.js — 証憑読み込み (Google Drive + Gemini API)
// 指定フォルダ内のPDF・画像(JPEG/PNG)を Gemini で解析し、receipt タブに出力
importReceiptPdfs() の JSDoc 更新
// 変更前
/**
* メニュー: 📄 領収書PDFの読み込み (Drive)
* スクリプトプロパティに必要な設定:
* GEMINI_API_KEY: Gemini API のキー
* RECEIPT_FOLDER_ID: 領収書PDFを入れるGoogle DriveフォルダのID
*/
// 変更後
/**
* メニュー: 📄 証憑の読み込み (Drive)
* PDF・画像(JPEG/PNG)に対応。画像は電帳法スキャナ保存の品質チェックを実施。
* スクリプトプロパティに必要な設定:
* GEMINI_API_KEY: Gemini API のキー
* RECEIPT_FOLDER_ID: 証憑ファイルを入れるGoogle DriveフォルダのID
*/
関数名 importReceiptPdfs は変更しない。 既存メニュー登録やドキュメントからの参照があるため、後方互換性を維持する。
101_sys_config.js メニュー表示名の更新
// 変更前
.addItem('📄 領収書PDFの読み込み (Drive)', 'importReceiptPdfs')
// 変更後
.addItem('📄 証憑の読み込み (Drive: PDF/写真)', 'importReceiptPdfs')
Step 5: 結果ダイアログの更新
// 変更前
if (pdfFiles.length === 0) return ui.alert('📄 フォルダ内にPDFがありません。');
// ...
var msg = '📄 読み込み完了\n✅ 成功: ' + successCount + '件\n❌ 失敗: ' + errorCount + '件';
// ...
msg += '\n\n処理済みPDFは「processed」フォルダに移動しました。';
ui.alert('📄 領収書PDF読み込み 結果', msg, ui.ButtonSet.OK);
// 変更後
if (allFiles.length === 0) return ui.alert('📄 フォルダ内に対応ファイルがありません。\n対応形式: PDF, JPEG, PNG');
// ...
var msg = '📄 読み込み完了\n✅ 成功: ' + successCount + '件\n❌ 失敗: ' + errorCount + '件';
// ...
msg += '\n\n処理済みファイルは「processed」フォルダに移動しました。';
ui.alert('📄 証憑読み込み 結果', msg, ui.ButtonSet.OK);
影響範囲
| 変更対象 | 変更内容 | 変更量 |
|---|---|---|
500_import/502_receipt_reader.js | MIME拡張 + 品質チェック追加 + コメント更新 | ~120行追加 |
100_config/101_sys_config.js | メニュー表示名変更 | 1行変更 |
- 既存動作への影響なし: PDF の処理ロジックは変更しない。MIME タイプ判定の拡張と品質チェックの追加のみ
callGeminiForReceipt_()のシグネチャにmimeType引数が追加されるが、この関数はプライベート関数(末尾_)であり外部呼び出しはないpostProcessReceiptData_()は変更不要(画像由来のデータも同じフォーマットで receipt タブに書き込まれるため)
注意事項
- HEIC (iPhone 標準形式) は初期対応から除外する。 Google Drive にアップロードされた時点で JPEG に自動変換されるケースが多い。変換されない場合は手動で JPEG に変換してもらう運用とする。HEIC 対応は運用実績を見て判断(GAS の
MimeTypeに HEIC 定数がないため、文字列'image/heif'での追加が必要になる) getJpegDimensions_()/getPngDimensions_()はバイナリパースのため、破損ファイルではnullを返す。 null の場合は品質チェックをスキップし、Gemini に渡して処理させる(品質チェック失敗でファイルを拒否しない)- 解像度チェックの閾値 630px は「レシートの短辺 ≈ 80mm ≈ 3.15インチ × 200dpi」から算出。 A4 請求書(短辺210mm)の場合は 1654px が基準だが、レシートを想定して低めに設定。閾値は定数化しておき、運用で調整可能にする
- 1 枚に複数レシートが写っている場合: Gemini の抽出プロンプトに「複数ページの場合あり」の指示が既にあるため、1 画像から複数レコードが返る可能性がある。既存の
extractedListループで正しく処理される - 変数名
pdfFiles→allFilesに変更する。 画像も含むため、変数名を汎用化する - 関数名
importReceiptPdfsは変更しない。 既にメニュー・ドキュメントから参照されており、関数名変更は影響範囲が広い。JSDoc とコメントで画像対応済みであることを明記する
実データ検証(MCP でのデータ確認が必要な場合)
| 確認項目 | 確認方法 | 理由 |
|---|---|---|
| Google Drive フォルダ内のファイル形式 | 対象フォルダを手動確認 | JPEG/PNG 以外の形式(HEIC, WEBP 等)が存在するか確認 |
| 35_wrk_receipt のヘッダー構成 | MCP で 35 タブのヘッダー行を取得 | DDL 定義と実データの列順一致を確認 |
| 既存 PDF 取込結果の品質 | 35 タブの既存データを確認 | 画像 OCR 結果と同等の品質が期待できるか基準を把握 |
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| ADR-0007: Gemini API 領収書解析 | 補足決定: MAS-157 は Gemini Flash を継続使用(MIME タイプ拡張のみ) |
| CLAUDE.md | Env.geminiApiKey() / Env.receiptFolderId() の使用規約 |
| spec_receipt_import.md | 領収書取込フローの業務仕様 |
| dev_mas-146_cc_auto_settlement.md | MAS-157 の画像 OCR 結果はクレカ消込にも利用可能 |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | HEIC (iPhone 標準形式) → JPEG 変換の要否 | Google Drive 自動変換に依存するか、GAS 側で 'image/heif' を追加対応するか。TODO_future.md から転記 |
| 2 | 解像度チェックの閾値 | 200dpi 相当のピクセル数をレシートサイズから逆算。本仕様では短辺 630px(レシート80mm想定)としたが、A4 請求書なら 1654px。運用で調整要。TODO_future.md から転記 |
| 3 | 1 枚に複数レシートが写っている場合の扱い | 既存プロンプトの「ページごとに1つの JSON」で対応可能だが、精度は未検証。TODO_future.md から転記 |
| 4 | タイムスタンプ要件への対応方針 | Drive 更新日時で代替可能か、別途タイムスタンプサービスが必要か。TODO_future.md から転記 |
| 5 | 品質チェックの reject 運用 | 品質不適合時にファイルを完全拒否するか、警告付きで処理を続行するかの判断。本仕様では reject=true(拒否)としたが、運用開始時は警告のみ(reject=false)にして様子を見る選択肢もある |
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-157「写真(JPEG/PNG)対応の証憑OCR + 電帳法品質チェック」を実装してください。
## 実行前タスク
以下のファイルを読み込んでください:
1. `500_import/502_receipt_reader.js` — 現行の領収書PDF読み込み実装。以下を重点確認:
- `importReceiptPdfs()` (L12-175): メイン関数。ファイル取得→Gemini呼び出し→シート書き込み
- `callGeminiForReceipt_()` (L184-281): Gemini API呼び出し。mime_type がハードコード
- `postProcessReceiptData_()` (L288-361): 同一取引先の突合補正(変更不要)
2. `100_config/101_sys_config.js` — L319 のメニュー表示名を変更
3. `CLAUDE.md` — コーディング規約
4. `docs/dev/dev_mas-157_photo_ocr.md` — 本仕様書
## 修正対象ファイル
- `500_import/502_receipt_reader.js` — 既存ファイルの修正
- `100_config/101_sys_config.js` — メニュー表示名の1行変更のみ
## 実装内容
### A: `502_receipt_reader.js` の修正
1. **ファイルヘッダーコメント更新** (L1-4):
- 「領収書PDF読み込み」→「証憑読み込み」
- 「指定フォルダ内のPDFを」→「指定フォルダ内のPDF・画像(JPEG/PNG)を」
2. **JSDoc 更新** (L6-11):
- メニュー名更新、画像対応・電帳法チェックの記述追加
- `RECEIPT_FOLDER_ID` の説明を「証憑ファイルを入れる」に変更
3. **SUPPORTED_MIMES 定数の追加** (L39 の前):
```js
var SUPPORTED_MIMES = [MimeType.PDF, MimeType.JPEG, MimeType.PNG];
```
4. **ファイル取得の変更** (L39-43):
```js
// 変更前
var files = folder.getFilesByType(MimeType.PDF);
var pdfFiles = [];
while (files.hasNext()) { pdfFiles.push(files.next()); }
// 変更後
var allFiles = [];
for (var mi = 0; mi < SUPPORTED_MIMES.length; mi++) {
var iter = folder.getFilesByType(SUPPORTED_MIMES[mi]);
while (iter.hasNext()) { allFiles.push(iter.next()); }
}
```
5. **変数名 `pdfFiles` → `allFiles` に全置換** (L45, L89 等)
6. **空ファイルメッセージ更新** (L45):
```js
if (allFiles.length === 0) return ui.alert('📄 フォルダ内に対応ファイルがありません。\n対応形式: PDF, JPEG, PNG');
```
7. **Gemini 呼び出し前に品質チェックを挿入** (L94-97 の後):
```js
var blob = file.getBlob();
var mimeType = blob.getContentType();
var base64 = Utilities.base64Encode(blob.getBytes());
// 画像品質チェック(PDF以外)
if (mimeType !== 'application/pdf') {
var qualityResult = checkImageQuality_(blob, mimeType);
if (qualityResult.warnings.length > 0) {
Utils.logInfo(FUNC, '⚠️ ' + fileName + ': ' + qualityResult.warnings.join(', '));
if (qualityResult.reject) {
errors.push(fileName + ': 品質不適合 - ' + qualityResult.warnings.join(', '));
errorCount++;
continue;
}
}
}
```
8. **callGeminiForReceipt_ 呼び出しに mimeType を追加** (L99):
```js
var extractedList = callGeminiForReceipt_(apiKey, base64, mimeType, fileName);
```
9. **callGeminiForReceipt_ のシグネチャと payload 修正** (L184, L197):
- 引数: `base64Pdf` → `base64Data`, `mimeType` 追加
- payload: `mime_type: 'application/pdf'` → `mime_type: mimeType`
- 変数名: `base64Pdf` → `base64Data`
10. **品質チェック関数3つを末尾に追加**:
- `checkImageQuality_(blob, mimeType)` — 電帳法品質チェック
- `getJpegDimensions_(bytes)` — JPEG バイナリパース
- `getPngDimensions_(bytes)` — PNG バイナリパース
11. **結果ダイアログの更新** (L167-169):
- 「処理済みPDFは」→「処理済みファイルは」
- ダイアログタイトル「領収書PDF読み込み 結果」→「証憑読み込み 結果」
12. **ページ数警告のコメント修正** (L110-116): 画像の場合はページ数チェック不要なので、PDF の場合のみ実行する条件を追加
### B: `101_sys_config.js` の修正
L319 のメニュー表示名を変更:
```js
.addItem('📄 証憑の読み込み (Drive: PDF/写真)', 'importReceiptPdfs')
```
## 制約
- 関数名 `importReceiptPdfs` は変更しない(後方互換性維持)
- `callGeminiForReceipt_()` のプロンプト文は変更しない(既存の抽出指示で画像にも対応可能)
- `postProcessReceiptData_()` は変更しない
- HEIC 形式は対応しない(Google Drive の自動変換に依存)
- `getJpegDimensions_` / `getPngDimensions_` がnullを返した場合、品質チェックをスキップして処理を続行する(ファイルを拒否しない)
## 動作確認
`npm run push:dev` 後:
1. メニュー「🔍 消込・マッチング」に「📄 証憑の読み込み (Drive: PDF/写真)」が表示されること
2. Drive フォルダに JPEG 画像(レシート写真)を配置
3. 「📄 証憑の読み込み」を実行
4. **検証**: JPEG ファイルが読み取られ、35_wrk_receipt に正しくデータが追加されること
5. **検証**: 低解像度画像(短辺 < 630px)で品質警告が出ること
6. **検証**: 処理済みファイルが processed フォルダに移動すること
7. 既存の PDF ファイルでも正常に動作すること(回帰テスト)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| ファイル読み込み・構造理解 | あり | 既存コードの全体構造把握 |
| MIME タイプ拡張 | なし | 仕様書で変更箇所が特定済み |
| 品質チェック関数の実装 | あり | バイナリパースのロジック確認 |
| メニュー・コメント更新 | なし | 定型作業 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 既存コードの分析、電帳法要件の設計、バイナリパースの正確性に高い推論力が必要 |
| 実装 | Claude Sonnet 4.6 | 変更箇所は仕様書で特定済みだが、バイナリパース関数の正確な実装に中程度の判断力が必要 |
| 動作確認 | ユーザー手動 | Drive フォルダへの画像配置 → 実行 → 結果確認の手動操作が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-16 | 初版作成 |