将来的にプロジェクトでPDFのテストデータを作成する必要が見込まれたため、その準備として対応方法をまとめました。
目次
目的
- AWS bedrockに読み込ませるためのテストデータを作成する
- テストデータは源泉徴収票をPDFまたは画像(JPEG)を対象とする
- 画像(JPEG)のファイルサイズを調整し、境界値テストを実施できるようにする
使用環境
- OS: Windows 10 / 11
- Python: 3.11(conda仮想環境)
- PDF描画ライブラリ: PyMuPDF(fitz)
環境構築手順
# 仮想環境作成
conda create -n pdfenv python=3.11
# 有効化
conda activate pdfenv
# pipでPyMuPDFをインストール(condaには無い)
pip install pymupdf
# 動作確認
python -c "import fitz; print(fitz.__doc__)"
出力例:
```
PyMuPDF 1.26.4: Python bindings for the MuPDF 1.26.7 library (rebased implementation).
Python 3.11 running on win32 (64-bit).
```
入力ファイル(源泉徴収票)
国税庁:F1-1 給与所得の源泉徴収票(同合計表)
https://www.nta.go.jp/taxes/tetsuzuki/shinsei/annai/hotei/pdf/r07/02.pdf
1ページ目には入力フォームが設定されたフォーマットが含まれています。
出力ファイル
- 入力フォームを削除し、値が固定されたPDF(左側半分)を出力
→※値の編集はできない - PDFを画像化したJPEGファイルを生成
処理フロー
- 入力ファイル内の各フォームのキー値の抽出
- 各キーと値をマッピングしたJSONデータを用意
- 各フォームにJSONデータの値を設定
- PDFをフラット化(フォーム削除処理)して出力
- PDFの内容を画像として出力
コード
1.入力ファイル内の各フォームのキー値の抽出
import fitz # PyMuPDF
import json
pdf_path = "r07_02.pdf"
output_json = "input_data_template.json"
doc = fitz.open(pdf_path)
# すべてのページからフィールド名を収集
field_names = set()
for page_index in range(len(doc)):
page = doc.load_page(page_index)
widgets = page.widgets()
if not widgets:
continue
for w in widgets:
if w.field_name:
field_names.add(w.field_name)
# テンプレート構造を生成
template_data = [{name: "" for name in sorted(field_names)}]
# JSONファイルとして出力
with open(output_json, "w", encoding="utf-8") as f:
json.dump(template_data, f, ensure_ascii=False, indent=2)
2.各キーと値をマッピングしたJSONデータを用意
[
{
"氏名": "テスト 太郎",
"住 所 又 は 居 所": "東京都千代田区霞が関1−1−1",
"支払(内)": "5000000",
"源泉徴収税額": "150000",
"社会保険料等の金額": "700000",
"生命保険料の控除額": "80000",
"地震保険料の控除額": "50000",
"配偶者(特別)控除の額": "380000",
"基礎控除の額": "480000",
"支払者住所(居所) 又は所在地": "東京都中央区",
"支払者氏名又は名称": "株式会社 テスト開発"
}
]
3.値の設定〜PDF出力
# -----------------------------
# 設定
# -----------------------------
INPUT_PDF = "r07_02_filled.pdf"
OUTPUT_DIR = "output_final"
TARGET_BYTES = 4.3 * 1024 * 1024 # JPEGターゲットサイズ 4.3MB
os.makedirs(OUTPUT_DIR, exist_ok=True)
dpi_for_png = 300 # PNGは固定DPI
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
BAKED_PDF = os.path.join(OUTPUT_DIR, f"filled_baked_{ts}.pdf")
LEFT_PDF = os.path.join(OUTPUT_DIR, f"filled_left_{ts}.pdf")
LEFT_JPEG = os.path.join(OUTPUT_DIR, f"filled_left_{ts}.jpg")
def main():
if not os.path.exists(INPUT_PDF):
print(f"入力PDFが見つかりません: {INPUT_PDF}")
return
doc = fitz.open(INPUT_PDF)
print(f"Opened: {INPUT_PDF} pages={len(doc)}")
# (1) フォームデバッグ
# debug_print_widgets(doc)
# (2) フォームを焼き込み
print("Calling doc.bake() ...")
doc.bake()
print("doc.bake() done.")
# (3) 焼き込み済みPDFを保存
doc.save(BAKED_PDF)
print(f"Baked PDF saved: {BAKED_PDF}")
# (4) 左半分をPDFとして切り出し
page0 = doc[0]
rect = page0.rect
clip_rect = fitz.Rect(0, 0, rect.width / 2, rect.height)
new_doc = fitz.open()
new_page = new_doc.new_page(width=clip_rect.width, height=clip_rect.height)
new_page.show_pdf_page(new_page.rect, doc, 0, clip=clip_rect)
new_doc.save(LEFT_PDF)
new_doc.close()
print(f"Left-half PDF saved: {LEFT_PDF}")
# (5) JPEG出力(ターゲットサイズに寄せる)
save_jpeg_with_target_size(page0, LEFT_JPEG, TARGET_BYTES, clip_rect)
doc.close()
print("Done.")
4.画像出力
# DPI を調整してターゲットファイルサイズに寄せるJPEG出力
def save_jpeg_with_target_size(page, output_path, target_bytes, clip_rect):
min_dpi = 72
max_dpi = 3000
best_dpi = min_dpi
best_size = 0
best_pix = None
for trial in range(15):
dpi = (min_dpi + max_dpi) / 2
matrix = fitz.Matrix(dpi / 72.0, dpi / 72.0)
pix = page.get_pixmap(matrix=matrix, clip=clip_rect, alpha=False)
# Pixmapのバイトサイズで比較
size = len(pix.tobytes("jpeg"))
print(f"[trial {trial}] dpi={dpi:.1f}, size={size/1024:.2f}KB")
if size > target_bytes:
max_dpi = dpi
else:
min_dpi = dpi
best_dpi = dpi
best_size = size
best_pix = pix
if abs(size - target_bytes)/target_bytes < 0.01:
best_pix = pix
best_dpi = dpi
best_size = size
break
if best_pix is None:
best_pix = page.get_pixmap(matrix=fitz.Matrix(300/72,300/72), clip=clip_rect, alpha=False)
best_dpi = 300
best_size = len(best_pix.tobytes("jpeg"))
# 最終的にJPEGとして保存
best_pix.save(output_path, output="jpeg")
print(f"→ 出力JPEG: {output_path} ({best_size/1024/1024:.2f}MB, dpi={best_dpi:.1f})")
return best_pix, best_dpi
問題点など
pypdf
ライブラリが使用できなかった
プロジェクト内では当初pypdf
ライブラリを使用していましたが、以下の問題により利用を見送りました。
PDFには値が反映されず、画像出力時のみ値が反映される
原因
PDFにはフィールド値(実データ)と外観ストリーム(表示内容)が存在しますが、プログラムで値を設定しても外観ストリームが更新されない場合があり、そのままフラット化すると値が表示されないことが原因である
と考えられます。
PDFを画像出力した場合、値は反映されるものの、一部で文字化けが発生
「支払いを受ける者」欄は正常に表示される一方、「支払者」欄では文字化けが発生していました。
原因(推測)
フォームに設定されているフォントがHelvetica系のフォントであったため、本来は代替フォントで表示されるはずが、日本語対応していない外観ストリームの問題でうまく描画されなかった可能性が高いと考えています。

現時点ではPyMuPDF
を採用していますが、原因や解決方法が分かれば再検討の余地があります。
残課題
- PDFのファイルサイズを任意に調整できるようにしたい
→/Metadata
にダミーデータを追加すればサイズを増やすことができると考えています。 - PNGなど別画像の形式に対応したい
→ JPEGなどの非可逆圧縮形式とは異なり、可逆圧縮形式のため別アプローチが必要になります。