プログラミング / 言語 / Python

PDFからQRコードを読み取る
— PDF→画像変換編

1. 全体の流れ

PDF 内の QR コードを Python で取得するには、以下の 2 ステップが基本です。PDF は直接 QR コードを読み取れないため、まずページを画像に変換してから QR デコードを行います。

PDF ファイル.pdf
ページを画像に変換pdf2image / PyMuPDF
QRコードを検出・デコードpyzbar / OpenCV
QRコードの値を取得文字列・URL など
なぜ PDF のまま読み取れないのか: PDF はベクター形式のドキュメント形式であり、QR コードはビットマップ画像として埋め込まれています。QR デコーダーはピクセル単位の画像データを必要とするため、一度ラスタライズ(画像化)する必要があります。

必要なライブラリの全体像

役割ライブラリ特徴
PDF → 画像変換 pdf2image Poppler ベース。DPI 指定が柔軟。Windows では別途 Poppler が必要
PDF → 画像変換 PyMuPDF(fitz) pip のみで完結。高速。Windows 対応良好
QR コード読取 pyzbar ZBar ベース。QR・バーコード両対応。軽量で高速
QR コード読取 OpenCV(cv2) 追加依存なし。OpenCV 4.x 以降で QR 検出器内蔵

2. QRコード読取ライブラリの選択

QR コード読取には主に pyzbarOpenCV の QRCodeDetector の 2 択です。まずそれぞれのインストール方法を確認します。

pyzbar のインストール

pyzbar は ZBar ライブラリの Python バインディングです。QR コードだけでなく Code128・EAN などバーコード全般も読み取れます。

bash
# pyzbar 本体
pip install pyzbar

# Pillow も必要(画像の受け渡しに使用)
pip install Pillow
Windows の場合: pyzbar は ZBar の DLL が別途必要です。pip install pyzbar[scripts] を実行するか、公式 README に従って libiconv.dlllibzbar-64.dll(または 32bit 版)を PATH の通った場所に配置してください。

OpenCV のインストール

OpenCV 4.x 以降には QR コード検出器が標準搭載されています。すでに PART 05 でインストール済みの場合は不要です。

bash
# OpenCV(GUI 不要のサーバー向け)
pip install opencv-python-headless

# または GUI 機能込み(デスクトップ環境向け)
pip install opencv-python
どちらを選ぶか: pyzbar は QR コード専用ライブラリのため検出率が高く、特に小さい QR コードや斜めになった QR コードに強いです。OpenCV は追加の ZBar インストールが不要な分セットアップが楽です。迷ったら pyzbar を推奨します。

3. PDF→画像変換:pdf2image の使い方

pdf2image は Poppler の pdftoppm コマンドをラップした Python ライブラリです。DPI(解像度)を自由に指定でき、高解像度な画像変換が得意です。

インストール

bash
# pdf2image のインストール
pip install pdf2image

# ── Poppler のインストール(別途必要)──────────────────────────────
# ▼ Windows
#   https://github.com/oschwartz10612/poppler-windows/releases
#   上記から最新版をダウンロードして解凍し、
#   bin フォルダのパスを環境変数 PATH に追加するか、
#   convert_from_path(..., poppler_path=r"C:\poppler\bin") で直接指定する

# ▼ macOS(Homebrew)
#   brew install poppler

# ▼ Ubuntu / Debian
#   apt-get install poppler-utils

基本的な使い方 — 全ページを変換

Python
from pdf2image import convert_from_path

# PDF の全ページを Pillow Image のリストとして取得
# dpi=200 以上を推奨(QR コードは細かいパターンのため)
pages = convert_from_path("sample.pdf", dpi=200)

print(f"総ページ数: {len(pages)}")

for i, page in enumerate(pages):
    print(f"  ページ {i+1}: サイズ={page.size}, モード={page.mode}")
    # 画像を保存したい場合(確認用)
    # page.save(f"page_{i+1:03d}.png", "PNG")
出力例
総ページ数: 3 ページ 1: サイズ=(1654, 2339), モード=RGB ページ 2: サイズ=(1654, 2339), モード=RGB ページ 3: サイズ=(1654, 2339), モード=RGB

特定ページのみ変換

Python
from pdf2image import convert_from_path

# first_page / last_page で変換するページ範囲を指定(1始まり)
pages = convert_from_path(
    "sample.pdf",
    dpi=200,
    first_page=1,   # 変換開始ページ
    last_page=1     # 変換終了ページ(1ページ目のみ)
)
first_page_img = pages[0]
print(f"ページ1のサイズ: {first_page_img.size}")

Windows で Poppler パスを直接指定する場合

Python
from pdf2image import convert_from_path

# Windows で Poppler の bin フォルダを直接指定する場合
pages = convert_from_path(
    "sample.pdf",
    dpi=200,
    poppler_path=r"C:\poppler\poppler-24.08.0\Library\bin"  # ← 実際のパスに変更
)
DPI の目安: QR コードは細かいパターンで構成されており、解像度が低いと読み取れない場合があります。dpi=150 は最低ライン、dpi=200〜300 が実用的な推奨値です。PDF ページが大きく QR コードが小さい場合は dpi=300 以上を使いましょう。

4. PDF→画像変換:PyMuPDF(fitz)の使い方

PyMuPDF は fitz という名前でインポートします。pip のみで完結するため、Poppler 不要で Windows でも手軽にセットアップできます。処理速度も pdf2image より速い傾向があります。

インストール

bash
# PyMuPDF のインストール(外部依存なし・pip のみで完結)
pip install PyMuPDF

基本的な使い方 — 全ページを変換

Python
import fitz          # PyMuPDF
from PIL import Image
import io

def pdf_to_images_pymupdf(pdf_path: str, dpi: int = 200) -> list:
    """
    PDF の全ページを Pillow Image のリストに変換する(PyMuPDF 版)
    Args:
        pdf_path: PDF ファイルパス
        dpi: 出力解像度(QR コード用に 200 以上推奨)
    Returns:
        list[PIL.Image.Image]
    """
    doc = fitz.open(pdf_path)
    images = []

    # zoom = dpi / 72  (PDF の標準解像度は 72 DPI)
    zoom = dpi / 72
    mat = fitz.Matrix(zoom, zoom)  # 拡大行列

    for page_num in range(len(doc)):
        page = doc[page_num]
        # ページをピクセルマップとしてレンダリング
        pix = page.get_pixmap(matrix=mat)
        # PNG バイト列に変換 → Pillow Image に変換
        img_bytes = pix.tobytes("png")
        pil_img = Image.open(io.BytesIO(img_bytes))
        images.append(pil_img)
        print(f"ページ {page_num + 1}: サイズ={pil_img.size}")

    doc.close()
    return images


# 使用例
pages = pdf_to_images_pymupdf("sample.pdf", dpi=200)
print(f"総ページ数: {len(pages)}")

特定ページのみ変換

Python
import fitz
from PIL import Image
import io

def get_page_image(pdf_path: str, page_number: int = 0, dpi: int = 200) -> Image.Image:
    """
    指定ページだけを画像に変換する
    Args:
        pdf_path:    PDF ファイルパス
        page_number: ページ番号(0 始まり)
        dpi:         出力解像度
    Returns:
        PIL.Image.Image
    """
    doc = fitz.open(pdf_path)
    page = doc[page_number]          # 0 始まりでアクセス
    zoom = dpi / 72
    mat = fitz.Matrix(zoom, zoom)
    pix = page.get_pixmap(matrix=mat)
    img = Image.open(io.BytesIO(pix.tobytes("png")))
    doc.close()
    return img


# 1 ページ目(index=0)だけ取得
img = get_page_image("sample.pdf", page_number=0, dpi=200)
print(f"1ページ目のサイズ: {img.size}")

pdf2image と PyMuPDF の比較

項目pdf2imagePyMuPDF(fitz)
外部依存 要 Poppler pip のみ
Windows 対応 Poppler の配置が必要 そのまま動く
処理速度 普通 速い
DPI 指定 dpi= で直接指定 zoom = dpi/72 で計算
ページ指定 first_page / last_page インデックスで直接指定
推奨: Windows 環境や手軽さを重視するなら PyMuPDF、Linux/macOS サーバーで Poppler が使えるなら pdf2image も選択肢です。本記事では以降 PyMuPDF を中心に解説します。

5. pyzbar でQRコードを読み取る

pyzbar は Pillow Image を直接受け取れるため、PDF→画像変換と非常に相性が良いです。

基本的な使い方

Python
from pyzbar.pyzbar import decode
from PIL import Image

# 画像ファイルから QR コードを読み取る
img = Image.open("sample_with_qr.png")
decoded_objects = decode(img)

print(f"検出した QR コード数: {len(decoded_objects)}")

for obj in decoded_objects:
    # obj.type  : シンボルの種類("QRCODE", "EAN13" など)
    # obj.data  : デコードされたバイト列
    # obj.rect  : 位置情報(left, top, width, height)
    # obj.polygon: 4 頂点の座標リスト

    value = obj.data.decode("utf-8")   # bytes → 文字列に変換
    print(f"種類   : {obj.type}")
    print(f"値     : {value}")
    print(f"位置   : left={obj.rect.left}, top={obj.rect.top}, "
          f"width={obj.rect.width}, height={obj.rect.height}")
    print(f"頂点   : {obj.polygon}")
    print("---")
出力例
検出した QR コード数: 2 種類 : QRCODE 値 : https://example.com/item/12345 位置 : left=120, top=80, width=200, height=200 頂点 : [Point(x=120, y=80), Point(x=320, y=80), Point(x=320, y=280), Point(x=120, y=280)] --- 種類 : QRCODE 値 : 管理番号:ABC-9876 位置 : left=500, top=80, width=200, height=200 頂点 : [Point(x=500, y=80), Point(x=700, y=80), Point(x=700, y=280), Point(x=500, y=280)] ---

QR コードの値だけをリストで取得

Python
from pyzbar.pyzbar import decode
from PIL import Image

def extract_qr_values(pil_image: Image.Image) -> list[str]:
    """画像から QR コードの値をリストで返す(QR コードのみフィルタ)"""
    objects = decode(pil_image)
    return [
        obj.data.decode("utf-8")
        for obj in objects
        if obj.type == "QRCODE"
    ]


img = Image.open("sample_with_qr.png")
values = extract_qr_values(img)
print(values)
# ['https://example.com/item/12345', '管理番号:ABC-9876']
文字コードについて: obj.data はバイト列(bytes)です。通常は UTF-8 でデコードできますが、旧システムで作成された QR コードは Shift-JIS の場合があります。その場合は obj.data.decode("shift_jis") を試してください。

6. OpenCV でQRコードを読み取る

OpenCV 4.x には QRCodeDetector クラスが内蔵されています。pyzbar のような追加依存なしで使えるため、OpenCV をすでに使っている環境に適しています。

基本的な使い方

Python
import cv2
import numpy as np
from PIL import Image

def decode_qr_opencv(pil_image: Image.Image) -> list[dict]:
    """
    OpenCV の QRCodeDetector で QR コードを検出・デコードする
    Args:
        pil_image: Pillow Image オブジェクト
    Returns:
        list of dict: {'value': str, 'polygon': list}
    """
    # Pillow Image → NumPy 配列(BGR 形式)に変換
    img_np = np.array(pil_image.convert("RGB"))
    img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)

    # QRCodeDetector を初期化
    detector = cv2.QRCodeDetector()

    # detectAndDecodeMulti: 画像内の全 QR コードを一括検出・デコード
    # retval: 検出成功フラグ(bool)
    # decoded_info: デコードされた文字列のタプル
    # points: 各 QR コードの 4 頂点座標
    # straight_qrcode: 正規化された QR コード画像
    retval, decoded_info, points, _ = detector.detectAndDecodeMulti(img_cv)

    results = []
    if retval and decoded_info:
        for value, pts in zip(decoded_info, points):
            if value:  # 空文字(デコード失敗)はスキップ
                polygon = pts.astype(int).tolist()
                results.append({
                    'value': value,
                    'polygon': polygon
                })

    return results


# 使用例
img = Image.open("sample_with_qr.png")
qr_results = decode_qr_opencv(img)

print(f"検出した QR コード数: {len(qr_results)}")
for r in qr_results:
    print(f"  値     : {r['value']}")
    print(f"  頂点   : {r['polygon']}")

OpenCV の QR 読取:1つだけ検出する場合

Python
import cv2
import numpy as np
from PIL import Image

def decode_qr_single_opencv(pil_image: Image.Image) -> str | None:
    """
    画像内の最初の QR コードをデコードして値を返す
    見つからない場合は None を返す
    """
    img_np = np.array(pil_image.convert("RGB"))
    img_cv = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)

    detector = cv2.QRCodeDetector()
    # detectAndDecode は最初の1つのみ検出
    value, points, _ = detector.detectAndDecode(img_cv)

    if value:
        return value
    return None


img = Image.open("sample_with_qr.png")
result = decode_qr_single_opencv(img)
print(f"QR コードの値: {result}")
OpenCV の制限: OpenCV の QR 読取は pyzbar に比べて検出率がやや低く、特に小さい QR コードや QR コードが傾いている場合に読み取り失敗することがあります。PDF から変換した画像で読み取れない場合は pyzbar を試してください。

7. PDF→画像変換 + QR読取の統合スクリプト

PyMuPDF で PDF をページ画像に変換し、pyzbar で QR コードを読み取る統合スクリプトです。

Python — qr_from_pdf.py
"""
qr_from_pdf.py
PDF のページを画像に変換し、各ページの QR コードを読み取るスクリプト
"""

import io
import fitz                          # PyMuPDF
from PIL import Image
from pyzbar.pyzbar import decode


def pdf_page_to_image(pdf_path: str, page_number: int, dpi: int = 200) -> Image.Image:
    """PDF の指定ページを Pillow Image に変換する"""
    doc = fitz.open(pdf_path)
    page = doc[page_number]
    zoom = dpi / 72
    mat = fitz.Matrix(zoom, zoom)
    pix = page.get_pixmap(matrix=mat)
    img = Image.open(io.BytesIO(pix.tobytes("png")))
    doc.close()
    return img


def read_qr_from_image(pil_image: Image.Image) -> list[dict]:
    """Pillow Image から QR コードを読み取り、値と位置情報を返す"""
    objects = decode(pil_image)
    results = []
    for obj in objects:
        if obj.type == "QRCODE":
            results.append({
                'value':   obj.data.decode("utf-8", errors="replace"),
                'rect':    obj.rect,     # Rect(left, top, width, height)
                'polygon': obj.polygon   # [Point(x,y), ...]
            })
    return results


def scan_pdf_for_qr(pdf_path: str, dpi: int = 200) -> dict:
    """
    PDF の全ページを走査して QR コードを抽出する

    Returns:
        {
            'total_pages': int,
            'pages': {
                page_num: [{'value': str, 'rect': ..., 'polygon': ...}, ...]
            },
            'all_values': [str, ...]   # 全ページの QR 値フラット
        }
    """
    doc = fitz.open(pdf_path)
    total_pages = len(doc)
    doc.close()

    result = {'total_pages': total_pages, 'pages': {}, 'all_values': []}

    for page_num in range(total_pages):
        img = pdf_page_to_image(pdf_path, page_num, dpi=dpi)
        qr_list = read_qr_from_image(img)

        if qr_list:
            result['pages'][page_num + 1] = qr_list
            for qr in qr_list:
                result['all_values'].append(qr['value'])
            print(f"ページ {page_num + 1}: {len(qr_list)} 件検出")
        else:
            print(f"ページ {page_num + 1}: QR コードなし")

    return result


# ── メイン ────────────────────────────────────────────────────────────
if __name__ == "__main__":
    import sys

    pdf_path = sys.argv[1] if len(sys.argv) > 1 else "sample.pdf"
    result = scan_pdf_for_qr(pdf_path, dpi=200)

    print("\n" + "=" * 50)
    print(f"総ページ数: {result['total_pages']}")
    print(f"QR コード検出総数: {len(result['all_values'])}")
    print("\n【検出した QR コードの値】")
    for page_num, qr_list in result['pages'].items():
        for qr in qr_list:
            print(f"  p.{page_num:>3}  {qr['value']}")
実行例
python qr_from_pdf.py sample.pdf ページ 1: 2 件検出 ページ 2: QR コードなし ページ 3: 1 件検出 ================================================== 総ページ数: 3 QR コード検出総数: 3 【検出した QR コードの値】 p. 1 https://example.com/item/12345 p. 1 管理番号:ABC-9876 p. 3 https://example.com/invoice/789

8. 複数ページ PDF を一括処理する

ページ数が多い PDF を効率よく処理するためのテクニックを紹介します。

全ページ一括変換してから QR スキャン

Python
import io
import fitz
from PIL import Image
from pyzbar.pyzbar import decode


def scan_all_pages(pdf_path: str, dpi: int = 200) -> list[dict]:
    """
    PDF の全ページを処理し、QR コードが見つかったページの情報を返す

    Returns:
        list of dict: [{'page': int, 'value': str, 'rect': Rect}, ...]
    """
    doc = fitz.open(pdf_path)
    zoom = dpi / 72
    mat = fitz.Matrix(zoom, zoom)

    all_results = []

    for page_num in range(len(doc)):
        page = doc[page_num]
        pix = page.get_pixmap(matrix=mat)
        pil_img = Image.open(io.BytesIO(pix.tobytes("png")))

        for obj in decode(pil_img):
            if obj.type != "QRCODE":
                continue
            try:
                value = obj.data.decode("utf-8")
            except UnicodeDecodeError:
                value = obj.data.decode("shift_jis", errors="replace")

            all_results.append({
                'page':  page_num + 1,   # 1 始まり
                'value': value,
                'rect':  obj.rect
            })

    doc.close()
    return all_results


# 使用例
results = scan_all_pages("sample.pdf", dpi=200)
print(f"合計 {len(results)} 件の QR コードを検出")
for r in results:
    print(f"  p.{r['page']:>3}  {r['value']}")

メモリ節約:ページごとに逐次処理

ページ数が多い(数十ページ以上)PDF の場合、全ページを一度にメモリに乗せると重くなります。ページごとに変換→読取→破棄するのが効率的です。上記 scan_all_pages 関数はすでにこの方式を採用しています(全画像をリストに溜めずページごとに処理)。

DPI を下げてスピードアップ: QR コードが大きければ dpi=150 でも十分読み取れます。まず dpi=150 で試して読み取れない場合に dpi=200 や dpi=300 に上げるアプローチが効率的です。

9. ライブラリ比較まとめ

組み合わせセットアップ難度検出精度処理速度Windows
PyMuPDF + pyzbar(推奨) 速い DLL 配置が必要
PyMuPDF + OpenCV 最低 速い 問題なし
pdf2image + pyzbar 普通 Poppler + DLL が必要
pdf2image + OpenCV 普通 Poppler が必要
まとめ:
Windows 環境で最も手軽なのは PyMuPDF + OpenCV(pip のみで完結)。
検出精度を最大化したいなら PyMuPDF + pyzbar(DLL のセットアップが追加で必要)。
次の PART 11 では、特定エリア(右上・左上など)に絞って QR コードを読み取る方法を解説します。