【Python】そうだ、スキャンPDFをOCRして検索可能PDFに変換しよう

こんにちは、ローカルマシンで頑張りたい協会のハルミです。
以前、ローカルOCRライブラリを紹介して結構な反応がありました。今回はこちらのライブラリを活用してみる編ということで記事を書いてます。
スキャンPDFって何かと不便
スキャンPDFが何なのか一応説明しておくと、プリンターなどでスキャンしてPDFデータに変換された、テキストデータが全く無いPDFファイルのことです。
これの何が厄介かというと、
-
テキストの検索・コピーができない
-
AI活用しにくい
みたいなことがあります。
こんなこと説明されなくても社会人の皆さんは常々思っていることではあると思いますが...
Googleドライブとかにアップロードしたりすれば簡単にテキスト化処理出来ますが、例のごとく社内制約によって使えずなので何かいい方法はないか...🤔
そうだ、Pythonで無料でテキスト埋込しよう
前置きはこの辺にしておいて、完全ローカルで既存のPDF上に透明テキストを埋め込み、検索可能PDF(Searchable PDF)に変換していくプロセスを作っていきましょう。
記事の最後の方には、こちらで解説したものを簡単に実行できるTUIツールにしたものも紹介してますので、利用だけしてみたい方はセクション飛ばしてどうぞ!
必要パッケージのインストール
パッケージマネージャーにはuvを使用していきます。
uv init
uv add numpy opencv-python pypdf pypdfium2 reportlab onnxocr
pip install numpy opencv-python pypdf pypdfium2 reportlab onnxocr
| ライブラリ | 目的 |
|---|---|
| numpy | OCRの前処理 |
| OpenCV | OCRの前処理 |
| pypdf | 元のPDFへオーバーレイPDFを合成 |
| pypdfium2 | PDFページを画像としてレンダリング |
| reportlab | テキストレイヤーを描画したPDFを作成 |
| OnnxOCR | CPU推論で高速OCR処理 |
全体フロー
処理の概念図は以下の通りです。
各ステップを分解して、簡単なサンプルコードで説明していきます。
1. PDFページを画像(PIL Image)として取り出す
OCRを行うには PDF を画像に変換する必要があります。
ここで使うのが pypdfium2 です。
import pypdfium2 as pdfium
def render_pdf_to_image(pdf_path, dpi=300):
pdf = pdfium.PdfDocument(pdf_path)
page = pdf[0] # 例:最初の1ページのみ
scale = dpi / 72
pil_img = page.render(scale=scale).to_pil()
return pil_img
ポイント
-
PDFは72dpiベース → OCRのために300dpi前後へスケールアップ
-
to_pil()でPillow画像になるので、OpenCVやnumpyで扱える
2. OCRの実行
別記事にて紹介したOnnxOCRを使ってPDF画像をOCRしていきます。
import numpy as np
import cv2
from onnxocr.onnx_paddleocr import ONNXPaddleOcr
ocr = ONNXPaddleOcr(use_gpu=False, lang="japan")
def run_ocr(pil_img):
rgb = np.array(pil_img)
bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
results = ocr.ocr(bgr)
return results
OnnxOCRの実行結果は以下のようなデータが格納されています
[
[
[[x1,y1], [x2,y2], [x3,y3], [x4,y4]], ["文字列", 信頼度]
],
...
]
3. OCR結果を“矩形+テキスト”に正規化する
こちらの処理は無くても問題ありませんが、OCR結果は4点の四角形(クアッド)ですが、PDFに文字を押し込むためには
**単純な矩形(x1,y1,x2,y2)**が扱いやすいです。後の処理のためにデータを扱いやすくしておきます。
def normalize_ocr_results(ocr_results):
items = []
for line in ocr_results[0]:
quad = line[0]
text = line[1][0]
xs = [p[0] for p in quad]
ys = [p[1] for p in quad]
items.append({
"text": text,
"bbox": (min(xs), min(ys), max(xs), max(ys)),
})
return items
4. ReportLabで「透明テキストレイヤーPDF」を作る
ここがいちばん重要な部分です。
OCRで得た文字位置へ、透明文字(Invisible Text) を配置し、元PDFに合成すると、“検索可能PDF”になります。
import io
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
pdfmetrics.registerFont(UnicodeCIDFont("HeiseiKakuGo-W5")) # 日本語フォント
def create_overlay_pdf(page_w, page_h, ocr_items):
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=(page_w, page_h))
c.setFillAlpha(0.0) # 完全透明
for item in ocr_items:
x1, y1, x2, y2 = item["bbox"]
text = item["text"]
fontsize = max(6, (y2 - y1) * 0.9)
c.setFont("HeiseiKakuGo-W5", fontsize)
# PDF座標は左下原点なので上下を反転
baseline_y = page_h - y2
c.drawString(x1, baseline_y, text)
c.save()
return buf.getvalue()
ポイント
-
setFillAlpha(0.0)で透明化する -
OCR座標は画像座標(上が0) → PDFは下が0 → 上下反転が必要
-
y座標は
page_h - y2
5. PyPDFで元PDFとオーバーレイPDFを合体する
最後の仕上げです。作成した透明文字のPDFオーバーレイを元のPDFに合体させればいい感じにテキスト埋込PDFの生成完了です。
from pypdf import PdfReader, PdfWriter
def merge_overlay(original_pdf, overlay_bytes, output_pdf):
reader = PdfReader(original_pdf)
overlay_reader = PdfReader(io.BytesIO(overlay_bytes))
writer = PdfWriter()
page = reader.pages[0]
overlay_page = overlay_reader.pages[0]
page.merge_page(overlay_page)
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
プログラムを実行してみる
記事に掲載できるいい感じのスキャンPDFの素材が見つからなかったので、それっぽい書類データを画像にしたものをPDFに変換してスキャンPDFとして処理にかけてみました

プログラムによってテキストが選択可能になった!
OCR実行時に返ってくるbboxをもとに座標や文字の大きさを決めているので、実際の文字とはやや位置や文字の大きさはズレてはいますが、いい感じに透明文字を埋め込めています。
ちなみにテキストをコピーして貼り付けた結果は以下のような感じ。やや文字認識にミスがありますが、使用用途によっては十分な認識精度かと思います。
令和7年12月1日
関係者各位
口一力ルOCR実行委員会
委員長ハルミ
スキ一PDF変換大会の開催中止につて(お知らせ
拝啓時下ますます二清祥のこととお喜び申し上げます。平素は当会の運営につき
まして格別のご高配を賜り、厚く御礼申し上げます。
さて、開催を予定しておりました標記のスキPDF変換大会は、委員長の体調不
良のため、慎重な協議の結果、中止が決定いたしましたので、(ここに)お知せ
いたします。
実行委員一同、開催に向けて準備をしてまいりましたが、苦渋の決断をせさるを
得ない状況なりました。開を心待ちにしてくださっていた皆ま、関係者の皆
さまには大変ご迷惑かけすることとな「大変ご迷惑かけいたしますこ
と、深くおび申し上げす。なにとご理解(ご了承いただきますようご
理解(ご了承)のほよろしくお願い申し上げます。
敬具
記
1.イベト[催し物・行事名
2.開催日令和7年12月2日(日)
3.お問↓合わせ先:000-000-0000
以上
ここについてはOCRの性能によるところが大きいので、この記事ではあまり触れないでおきます。
TUIツールとして公開しました
以上簡単な処理ですが、この処理をTUIで簡単に実行するためのTUIアプリケーションも作ってPyPIに公開してみたので、気になる人はよかったら使ってみてください。
TUIの構築にはtextual を使っていて、ターミナルでPDFのOCR→検索可能PDF生成を行えます。詳しくはREADME をご覧ください。

TUIツールの画面(ターミナル上で動いています)
pip install pdfembed
pdfembed
おわりに
簡単な解説記事でしたが、実際かなり実用的だったので紹介してみました。
OCRライブラリ、サービスの多くは文字認識と同時にバウンディングボックスも取得できるので上手く活用できました。
画像認識モデルをローカルで実用的に動かせるようになればもっと実用的なものが作れそうなので、今後のAIの進歩に期待ですね🔥
それではまた👋
参考リンク
- TUIツール作成ライブラリ
Discussion