OCRmyPDF チュートリアル:スキャン文書を検索可能な PDF/A ファイルに変換し、サイドカーテキスト抽出とバッチ処理を行う方法
このチュートリアルは、OCRmyPDF を用いてスキャン文書を検索可能な PDF/A 形式に変換し、サイドカーテキストの抽出やバッチ処理を自動化する実用的なワークフローとコード例を提供している。
キーポイント
包括的な環境構築と依存関係管理
Tesseract OCR、Ghostscript、unpaper などのシステム依存関係の自動インストールスクリプトや、JBIG2 圧縮ライブラリのビルド手順を含む完全なセットアップ手順を提示している。
高度な PDF 変換とアーカイブ対応
OCRmyPDF の API を活用して、単なる画像の OCR だけでなく、検索可能なテキスト層の付与や国際標準である PDF/A 形式への変換、およびファイルサイズ最適化を実現する方法を解説している。
多様な処理モードと品質向上機能
メモリ上での OCR 実行、DPI ヒントの活用、ノイズ除去(unpaper)、既に OCR が施されたファイルの再処理など、実務で遭遇する様々な課題に対応する高度な設定オプションを紹介している。
自動化とバッチ処理の実装
単一ファイルだけでなく、複数の PDF を自動的に処理するバッチ処理ワークフローを構築し、アーカイブや検索、抽出タスクのためのドキュメントデジタル化パイプラインとしての活用方法を示している。
OCRmyPDF環境の自動構築
Tesseract, Ghostscript, unpaperなどのシステムツールと、ocrmypdfやPillowなどのPythonパッケージをスクリプト内で自動的にインストール・設定します。
JBIG2エンコーダのオプションビルド
jbig2encを任意でコンパイルしてインストールすることで、スキャン文書の出力ファイルサイズを大幅に削減する高度なPDF最適化機能を有効にできます。
実行前依存関係チェック
既存のツールやライブラリが利用可能か確認し、すでに環境が整っていればインストール処理をスキップして効率的に実行を開始します。
影響分析・編集コメントを表示
影響分析
この記事は、OCRmyPDF の高度な機能を体系的に理解し、実務環境で即座に適用可能な自動化スクリプトを提供することで、企業のドキュメント管理プロセスの効率化に貢献します。特に、複雑な依存関係の解決や品質調整のノウハウをコード付きで公開している点は、開発者にとって非常に価値が高く、OCR 技術の実装ハードルを大幅に低下させるものです。
編集コメント
実務で頻出するスキャン文書の検索・保存課題に対し、具体的なコードと設定値まで示した非常に有用な技術記事です。
このチュートリアルでは、高度で自己完結型の OCRmyPDF ワークフローを構築します。まず必要なシステムおよび Python の依存関係をインストールし、外部ファイルに依存せずに OCR テストを行えるよう、スキャン用の合成画像のみを含む PDF を作成します。その後、OCRmyPDF の公式公開 API を使用して、スキャンされた文書を検索可能な PDF へ変換し、PDF/A 出力を生成し、サイドカーテキストの抽出を行い、結果を検証し、ファイルサイズを比較し、Tesseract の設定を調整し、ノイズの多いスキャンをクリーンアップし、すでに OCR が適用されているファイルを処理し、DPI ヒント付き画像を取り扱い、メモリ上で OCR を実行し、複数の PDF をバッチ処理します。このワークフローを通じて、OCRmyPDF がアーカイブ、検索、抽出、自動化処理タスクのための実用的な文書デジタル化パイプラインとしてどのように機能するかを理解できます。
OCRmyPDF システム依存関係のインストール
コードをコピーしました。別のブラウザを使用してください
import io
import os
import re
import sys
import time
import shutil
import logging
import textwrap
import subprocess
from pathlib import Path
INSTALL_JBIG2 = True
def sh(cmd: str, check: bool = True) -> int:
"""シェルコマンドを実行し、その内容を出力するとともに、結果の末尾を表示する。"""
print(f" $ {cmd}")
r = subprocess.run(cmd, shell=True, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if r.stdout and r.stdout.strip():
for ln in r.stdout.strip().splitlines()[-12:]:
print(" " + ln)
if check and r.returncode != 0:
raise RuntimeError(f"コマンドが失敗しました (リターンコード {r.returncode}): {cmd}")
return r.returncode
def install_dependencies() -> None:
"""Colab/Ubuntu 向けに OCRmyPDF のシステム依存関係および Python 依存関係をインストールする。"""
apt_pkgs = (
"tesseract-ocr tesseract-ocr-eng tesseract-ocr-osd "
"tesseract-ocr-deu tesseract-ocr-fra "
"ghostscript unpaper pngquant poppler-utils qpdf"
)
sh("apt-get update -qq", check=False)
sh(f"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq {apt_pkgs}")
sh(f'"{sys.executable}" -m pip install -q --upgrade ocrmypdf img2pdf "pillow<12"')
if INSTALL_JBIG2 and shutil.which("jbig2") is None:
try:
build_pkgs = ("autoconf automake libtool pkg-config "
"libleptonica-dev zlib1g-dev build-essential git")
sh(f"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq {build_pkgs}")
sh("rm -rf /tmp/jbig2enc && "
"git clone -q https://github.com/agl/jbig2enc.git /tmp/jbig2enc")
sh("cd /tmp/jbig2enc && ./autogen.sh >/dev/null 2>&1 && "
"./configure >/dev/null 2>&1 && make -j2 >/dev/null 2>&1 && "
"make install >/dev/null 2>&1 && ldconfig")
print(" jbig2enc:",
"インストール済み" if shutil.which("jbig2") else "ビルド済みだが、バイナリが PATH にない")
except Exception as e:
print(" jbig2enc のビルドをスキップしました (オプション):", e)
def ensure_installed() -> None:
have_tools = bool(shutil.which("tesseract") and shutil.which("gs"))
try:
import ocrmypdf
import img2pdf
from PIL import Image
have_py = True
except Exception:
have_py = False
if have_tools and have_py:
print("依存関係は既に存在します — インストールをスキップします。")
else:
print("依存関係をインストールしています (初回実行には数分かかる場合があります)...")
install_dependencies()
ensure_installed()
Google Colab で OCRmyPDF の完全な環境を構築するために、必要な標準ライブラリをインポートし、インストールワークフローを定義します。Tesseract、Ghostscript、unpaper、pngquant、poppler、qpdf などのシステムツールと、OCRmyPDF、img2pdf、Pillow などの Python パッケージをインストールします。また、オプションとして jbig2enc をビルドすることで、スキャン文書に対してより小さな出力サイズを実現する高度な PDF 最適化が可能になります。
OCRmyPDF の読み込みと合成スキャンの構築
コードをコピーしてコピーしました別のブラウザを使用してください
def _purge(*prefixes):
for name in [m for m in list(sys.modules)
if any(m == p or m.startswith(p + ".") for p in prefixes)]:
del sys.modules[name]
def _load_ocrmypdf():
_purge("PIL", "ocrmypdf")
import ocrmypdf
return ocrmypdf
try:
ocrmypdf = _load_ocrmypdf()
except ImportError as e:
if "_Ink" in str(e) or "PIL" in str(e):
print("Repairing an incompatible Pillow (reinstalling pillow<12)...")
sh(f'"{sys.executable}" -m pip install -q --force-reinstall "pillow<12"')
try:
ocrmypdf = _load_ocrmypdf()
print("Pillow repaired — continuing without a restart.")
except Exception:
raise RuntimeError(
"Pillow is still incompatible in this session. Use the Colab menu: "
"Runtime > Restart session, then run this cell again."
)
else:
raise
from ocrmypdf.exceptions import (
ExitCode,
PriorOcrFoundError,
EncryptedPdfError,
MissingDependencyError,
TaggedPDFError,
DigitalSignatureError,
DpiError,
InputFileError,
UnsupportedImageFormatError,
)
from ocrmypdf.helpers import check_pdf
from ocrmypdf.pdfa import file_claims_pdfa
import img2pdf
from PIL import Image, ImageDraw, ImageFont, ImageFilter
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
logging.getLogger("ocrmypdf").setLevel(logging.WARNING)
logging.getLogger("pdfminer").setLevel(logging.ERROR)
logging.getLogger("PIL").setLevel(logging.WARNING)
SAMPLE_TEXT_PAGES = [
"Optical Character Recognition, commonly abbreviated as OCR, is the "
"process of converting images of typed or printed text into machine "
"encoded text. This page was generated as a synthetic scan so that the "
"OCRmyPDF pipeline has something realistic to recognize and search.",
"On 14 March 2026 the archive contained 1,482 pages across 37 folders. "
"Roughly 92 percent of those pages were scanned at 200 to 300 dots per "
"inch. The remaining 8 percent were skewed and required deskewing before "
"any reliable recognition was possible.",
"After OCRmyPDF finishes, the output is a searchable PDF/A file. You can "
"select text, copy it, and run full text search across thousands of "
"documents. The original image resolution is preserved while a hidden "
"text layer is placed accurately underneath the page image.",
]
def _find_font():
for cand in (
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
):
if os.path.exists(cand):
return cand
return None
_FONT_PATH = _find_font()
FONT = ImageFont.truetype(_FONT_PATH, 40) if _FONT_PATH else ImageFont.load_default()
def _add_speckle(img, n=6000, dark=60):
"""Sprinkle light dark specks to imitate scanner noise (motivates --clean)."""
import random
px = img.load()
w, h = img.size
for _ in range(n):
px[random.randint(0, w - 1), random.randint(0, h - 1)] = random.randint(0, dark)
return img
def render_page(text, skew=False):
"""Render one A4 page (1654x2339 px ≈ 200 DPI) of dark text on white."""
W, H = 1654, 2339
img = Image.new("L", (W, H), 255)
draw = ImageDraw.Draw(img)
draw.multiline_text((150, 180), textwrap.fill(text, width=58),
fill=25, font=FONT, spacing=18)
if skew:
img = img.rotate(6, resample=Image.BICUBIC, expand=False, fillcolor=255)
img = img.filter(ImageFilter.GaussianBlur(0.6))
img = _add_speckle(img)
return img
def build_scanned_pdf(pdf_path: Path, pages_text, skew_index=1):
"""Render pages to PNGs and wrap them losslessly into an image-only PDF."""
pngs = []
for i, text in enumerate(pages_text):
img = render_page(text, skew=(i == skew_index))
p = pdf_path.parent / f"_pg_{pdf_path.stem}_{i}.png"
img.save(p, format="PNG", dpi=(200, 200))
pngs.append(str(p))
with open(pdf_path, "wb") as f:
f.write(img2pdf.convert(pngs))
for p in pngs:
os.remove(p)
return pdf_path
def do_ocr(input_file, output_file, **kw):
"""Wrapper around ocrmypdf.ocr() that disables the progress bar and times it."""
kw.setdefault("progress_bar", False)
t0 = time.perf_counter()
rc = ocrmypdf.ocr(input_file, output_file, **kw)
return rc, time.perf_counter() - t0
def tokens(s: str):
return re.findall(r"[a-z0-9]+", s.lower())
def kb(path) -> str:
return f"{Path(path).stat().st_size / 1024:,.1f} KB"
def banner(title: str):
line = "─" * 74
print(f"\n{line}\n {title}\n{line}")
Colab ランタイム内で OCRmyPDF を安全に読み込み、Pillow の互換性に関する問題が発生した場合は自動的に修復します。本チュートリアル全体で使用する OCRmyPDF 例外クラス、PDF 検証ヘルパー関数、img2pdf および Pillow ユーティリティをインポートします。また、サンプルドキュメントテキストの定義や、合成されたスキャンページのレンダリング、スキャナのようなノイズの付与、画像のみを含む PDF の作成、OCR 実行時間の計測、テキストのトークン化、ファイルサイズのフォーマット表示、セクションバナーの印刷を行うためのヘルパー関数も定義しています。
基本および高度な PDF/A OCR の実行
コードをコピーしました
別のブラウザを使用してください
banner("0 · 環境")
print("Python :", sys.version.split()[0])
print("ocrmypdf:", ocrmypdf.__version__)
sh("tesseract --version", check=False)
sh("gs --version", check=False)
sh("tesseract --list-langs", check=False)
print("unpaper :", shutil.which("unpaper"))
print("pngquant:", shutil.which("pngquant"))
print("jbig2 :", shutil.which("jbig2"), "(オプションエンコーダ)")
WORK = Path("/content/ocrmypdf_demo")
try:
WORK.mkdir(parents=True, exist_ok=True)
except Exception:
WORK = Path.cwd() / "ocrmypdf_demo"
WORK.mkdir(parents=True, exist_ok=True)
print("作業ディレクトリ :", WORK)
banner("1 · 合成画像のみの『スキャン』PDF を構築")
input_pdf = WORK / "scanned_input.pdf"
build_scanned_pdf(input_pdf, SAMPLE_TEXT_PAGES, skew_index=1)
print(f"{input_pdf.name}を作成しました ({kb(input_pdf)}, 3 ページ; 2 ページ目は傾きあり + ノイズ混入)")
print("この PDF にはまだテキストレイヤーがありません — 選択・検索しても何も返りません。")
banner("2 · 基本 OCR(傾き補正 + 自動回転)")
out_basic = WORK / "out_basic.pdf"
rc, dt = do_ocr(
input_pdf, out_basic,
language=["eng"],
deskew=True,
rotate_pages=True,
)
print(f"終了コード: {rc.name} ({int(rc)}) 所要時間 {dt:.1f}s -> {out_basic.name} ({kb(out_basic)})")
banner("3 · 高度な OCR(PDF/A-2, --optimize 3, サイドカーファイル, メタデータ)")
out_adv = WORK / "out_advanced.pdf"
sidecar = WORK / "ocr_text.txt"
rc, dt = do_ocr(
input_pdf, out_adv,
language=["eng"],
deskew=True,
rotate_pages=True,
optimize=3,
jpg_quality=80,
png_quality=80,
output_type="pdfa-2",
sidecar=sidecar,
title="OCRmyPDF Colab Tutorial",
author="Tutorial",
subject="Demonstration of OCRmyPDF",
keywords="ocr, pdf, tesseract, ocrmypdf",
)
print(f"終了コード: {rc.name} ({int(rc)}) 所要時間 {dt:.1f}s -> {out_adv.name} ({kb(out_adv)})")
sh(f'pdfinfo "{out_adv}" | grep -E "Title|Author|Subject|Keywords|Pages"', check=False)
メインのチュートリアルでは、まず Python、OCRmyPDF、Tesseract、Ghostscript、インストールされた言語、およびオプションの最適化ツールを含む OCR 環境の詳細を出力することから始めます。作業用ディレクトリを作成し、検索可能なテキストレイヤーを持たない合成スキャン PDF を生成します。その後、基本的な OCR ワークフローと、PDF/A 出力、画像最適化、サイドカーテキスト生成、ドキュメントメタデータを含む高度な OCR ワークフローの両を実行します。
検索可能性と OCR 単語再現率の検証
コードをコピー
コピー済み
別のブラウザを使用してください
banner("4 · 検索可能性の証明と OCR 単語再現率の測定")
ocr_text = sidecar.read_text(errors="ignore")
print("サイドカーテキスト(先頭 300 文字):\n" + ocr_text[:300].strip())
embedded = WORK / "embedded_text.txt"
sh(f'pdftotext "{out_adv}" "{embedded}"', check=False)
print(f"\npdftotext は出力 PDF から {len(embedded.read_text(errors='ignore').split())} 単語を抽出しました(入力には 0 でした)。")
src = tokens(" ".join(SAMPLE_TEXT_PAGES))
found = set(tokens(ocr_text))
recall = sum(1 for w in src if w in found) / max(1, len(src))
print(f"OCR 単語再現率(ソース対照): {recall * 100:.1f}% ({len(src)} ソース単語)")
banner("5 · 出力の検証とサイズ比較")
print("check_pdf (有効な PDF 構造):", check_pdf(out_adv))
print("file_claims_pdfa (PDF/A マーカー):", file_claims_pdfa(out_adv))
print(f"入力 : {kb(input_pdf)}")
print(f"基本 : {kb(out_basic)}")
print(f"高度 : {kb(out_adv)} (PDF/A-2 + 画像最適化)")
banner("6 · モードと例外:テキストスキップ / OCR 再実行 / 強制 OCR")
try:
do_ocr(out_adv, WORK / "should_fail.pdf", language=["eng"])
print("予期せぬ結果: 例外が発生しませんでした。")
except PriorOcrFoundError as e:
print(f"PriorOcrFoundError を捕捉しました(終了コード {e.exit_code}): PDF に既にテキストが含まれています。上書きするモードを選択してください:")
rc, _ = do_ocr(out_adv, WORK / "out_skiptext.pdf", language=["eng"], skip_text=True)
print(f" --skip-text -> {rc.name}")
rc, _ = do_ocr(out_adv, WORK / "out_redo.pdf", language=["eng"], redo_ocr=True)
print(f" --redo-ocr -> {rc.name}")
rc, _ = do_ocr(out_adv, WORK / "out_force.pdf", language=["eng"], force_ocr=True)
print(f" --force-ocr -> {rc.name}")
OCR がスキャンされた PDF を検索可能にしたことを証明するために、サイドカーテキストを読み取り、pdftotext を使用して出力 PDF から埋め込まれたテキストを抽出します。復元された OCR テキストを既知のソーステキストと比較し、単純な単語再現率スコアを計算します。その後、PDF 構造を検証し、PDF/A マーカーを確認し、ファイルサイズを比較し、OCRmyPDF が既に OCR テキストを含むファイルを skip-text(テキストスキップ)、redo-OCR(再 OCR)、force-OCR(強制 OCR)モードでどのように処理するかを実演します。
チューニング、クリーニング、およびメモリ内 OCR
コードをコピーしました。別のブラウザを使用してください
banner("7 · Tesseract エンジンの調整 (--oem / --psm)")
rc, dt = do_ocr(
input_pdf, WORK / "out_tuned.pdf",
language=["eng"],
tesseract_oem=1,
tesseract_pagesegmode=3,
output_type="pdf",
)
print(f"Tuned run -> {rc.name} in {dt:.1f}s")
banner("8 · unpaper による画像クリーニング (--clean / --clean-final)")
try:
rc, dt = do_ocr(
input_pdf, WORK / "out_cleaned.pdf",
language=["eng"], deskew=True,
clean=True, clean_final=True, output_type="pdf",
)
print(f"Cleaned run -> {rc.name} in {dt:.1f}s")
except Exception as e:
print("Cleaning step skipped (unpaper issue):", type(e).__name__, e)
banner("9 · 90°回転したページに対する自動向き補正 (OSD) (--rotate-pages)")
try:
rot_png = WORK / "_rot.png"
render_page(SAMPLE_TEXT_PAGES[0]).rotate(90, expand=True, fillcolor=255) \
.save(rot_png, format="PNG", dpi=(200, 200))
rot_pdf = WORK / "rotated_input.pdf"
with open(rot_pdf, "wb") as f:
f.write(img2pdf.convert([str(rot_png)]))
os.remove(rot_png)
rot_side = WORK / "rotated_text.txt"
rc, dt = do_ocr(
rot_pdf, WORK / "out_rotated_fixed.pdf",
language=["eng"], rotate_pages=True, sidecar=rot_side, output_type="pdf",
)
n = len(rot_side.read_text(errors="ignore").split())
print(f"OSD corrected the page; recovered {n} words -> {rc.name} in {dt:.1f}s")
except Exception as e:
print("Auto-orientation demo skipped:", type(e).__name__, e)
banner("10 · 単一画像の OCR (image_dpi ヒント)")
single_png = WORK / "single_scan.png"
render_page(SAMPLE_TEXT_PAGES[2]).save(single_png, format="PNG")
rc, dt = do_ocr(
single_png, WORK / "out_from_image.pdf",
language=["eng"],
image_dpi=200,
output_type="pdf",
)
print(f"Image -> searchable PDF: {rc.name} in {dt:.1f}s")
banner("11 · BytesIO ストリームによるメモリ内 OCR")
in_io = io.BytesIO(input_pdf.read_bytes())
out_io = io.BytesIO()
ocrmypdf.ocr(in_io, out_io, language=["eng"], output_type="pdf", progress_bar=False)
out_bytes = out_io.getvalue()
(WORK / "out_in_memory.pdf").write_bytes(out_bytes)
print(f"OCR'd entirely in RAM -> {len(out_bytes):,} bytes written to out_in_memory.pdf")
OCRmyPDF を介して OCR エンジンのモードとページ分割モードを直接設定し、Tesseract エンジンの調整を実験します。その後、unpaper ベースの画像クリーニングを使用してノイズの多いスキャンページを改善し、必要に応じてクリーンアップされた画像を最終出力に埋め込みます。また、自動ページ方向補正のテストや、明示的な DPI ヒントを用いて単一の画像を検索可能な PDF へ変換する処理、および BytesIO ストリームを使用してメモリ上のみで OCR を実行する処理も試します。
バッチ OCR と Typed OcrOptions API
Copy CodeCopiedUse a different Browser
banner("12 · PDF ファイルのフォルダをバッチ処理")
batch_in = WORK / "batch_in"
batch_out = WORK / "batch_out"
batch_in.mkdir(exist_ok=True)
batch_out.mkdir(exist_ok=True)
build_scanned_pdf(batch_in / "invoice_001.pdf",
[SAMPLE_TEXT_PAGES[0], SAMPLE_TEXT_PAGES[1]], skew_index=1)
build_scanned_pdf(batch_in / "memo_002.pdf",
[SAMPLE_TEXT_PAGES[2]], skew_index=-1)
print(f"{'file':<20}{'result':<14}{'time':<8}size")
for src_pdf in sorted(batch_in.glob("*.pdf")):
dst = batch_out / src_pdf.name
try:
rc, dt = do_ocr(src_pdf, dst, language=["eng"],
deskew=True, output_type="pdfa")
print(f"{src_pdf.name:<20}{rc.name:<14}{dt:<8.1f}{kb(dst)}")
except Exception as e:
print(f"{src_pdf.name:<20}{type(e).__name__:<14}{'-':<8}-")
banner("13 · 新しいスタイルの OcrOptions API (v17+)")
try:
from ocrmypdf._options import OcrOptions
opts = OcrOptions(
input_file=str(input_pdf),
output_file=str(WORK / "out_options.pdf"),
languages=["eng"],
deskew=True,
rotate_pages=True,
output_type="pdfa",
progress_bar=False,
)
rc = ocrmypdf.ocr(opts)
print(f"OcrOptions run -> {rc.name} ({int(rc)})")
except Exception as e:
print("OcrOptions API not available in this version:", type(e).__name__, e)
banner("14 · 結果")
produced = sorted(p for p in WORK.glob("*.pdf"))
for p in produced:
print(f" {p.name:<26}{kb(p)}")
for p in sorted(batch_out.glob("*.pdf")):
print(f" batch_out/{p.name:<16}{kb(p)}")
print(f"\nAll files are in: {WORK}")
try:
from google.colab import files
for p in [out_adv, out_basic, sidecar, embedded]:
if Path(p).exists():
files.download(str(p))
except Exception as e:
print("(Colab download unavailable — open the files from the panel instead.)", e)
print("\nDone.
image")
単一ファイルからフォルダレベルのバッチ処理へとワークフローを拡張するために、複数の合成入力 PDF を作成し、それぞれを OCR して出力ディレクトリに保存します。その後、検証済みの OCR 設定を構造化されたオプションオブジェクトとして渡せる新しい型付き OcrOptions API を試してみます。さらに、生成されたすべての PDF 出力(バッチ結果を含む)を一覧表示し、作業ディレクトリのパスを示し、重要なファイルをダウンロードします。
結論として、私たちは基本的なスキャン済み PDF の変換を超えた、完全な OCRmyPDF パイプラインを構築しました。現実的なスキャン入力を作成し、傾き補正と回転補正を伴う OCR を適用し、最適化された PDF/A ファイルを生成し、埋め込まれたテキストを検証し、OCR の再現率を測定し、PDF 構造を検証し、テキストスキップ、再 OCR、強制 OCR など複数の処理モードを実験しました。また、画像のクリーニング、Tesseract エンジンのチューニング、メモリ内処理、フォルダレベルのバッチ OCR といった実用的な生産機能も探求しました。
完全なコードはこちらで確認できます。Twitter で私たちをフォローすることも歓迎しますし、150k+ML の SubReddit に参加することや、ニュースレターに登録することを忘れないでください。待ってください!Telegram を使っていますか?今なら Telegram でも私たちに参加できます。
GitHub リポジトリの宣伝、Hugging Face ページ、製品リリース、ウェビナーなどのプロモーションのためにパートナーシップを希望される場合は、ご連絡ください。
本記事「OCRmyPDF Tutorial: Convert Scanned Documents into Searchable PDF/A Files with Sidecar Text Extraction and Batch Processing」は、MarkTechPost で最初に公開されました。
原文を表示
In this tutorial, we build an advanced, self-contained OCRmyPDF workflow. We start by installing the required system and Python dependencies, then create a synthetic image-only PDF for scanning so we can test OCR without relying on external files. From there, we use OCRmyPDF’s real public API to convert scanned documents into searchable PDFs, generate PDF/A outputs, extract sidecar text, validate the results, compare file sizes, tune Tesseract settings, clean noisy scans, handle already-OCRed files, process images with DPI hints, run OCR in memory, and batch-process multiple PDFs. Through this workflow, we understand how OCRmyPDF can serve as a practical document digitization pipeline for archival, search, extraction, and automated processing tasks.
Installing OCRmyPDF System Dependencies
Copy CodeCopiedUse a different Browser
import io
import os
import re
import sys
import time
import shutil
import logging
import textwrap
import subprocess
from pathlib import Path
INSTALL_JBIG2 = True
def sh(cmd: str, check: bool = True) -> int:
"""Run a shell command, echo it, and show the tail of its output."""
print(f" $ {cmd}")
r = subprocess.run(cmd, shell=True, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if r.stdout and r.stdout.strip():
for ln in r.stdout.strip().splitlines()[-12:]:
print(" " + ln)
if check and r.returncode != 0:
raise RuntimeError(f"Command failed ({r.returncode}): {cmd}")
return r.returncode
def install_dependencies() -> None:
"""Install OCRmyPDF's system + Python dependencies for Colab/Ubuntu."""
apt_pkgs = (
"tesseract-ocr tesseract-ocr-eng tesseract-ocr-osd "
"tesseract-ocr-deu tesseract-ocr-fra "
"ghostscript unpaper pngquant poppler-utils qpdf"
)
sh("apt-get update -qq", check=False)
sh(f"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq {apt_pkgs}")
sh(f'"{sys.executable}" -m pip install -q --upgrade ocrmypdf img2pdf "pillow<12"')
if INSTALL_JBIG2 and shutil.which("jbig2") is None:
try:
build_pkgs = ("autoconf automake libtool pkg-config "
"libleptonica-dev zlib1g-dev build-essential git")
sh(f"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq {build_pkgs}")
sh("rm -rf /tmp/jbig2enc && "
"git clone -q https://github.com/agl/jbig2enc.git /tmp/jbig2enc")
sh("cd /tmp/jbig2enc && ./autogen.sh >/dev/null 2>&1 && "
"./configure >/dev/null 2>&1 && make -j2 >/dev/null 2>&1 && "
"make install >/dev/null 2>&1 && ldconfig")
print(" jbig2enc:",
"installed" if shutil.which("jbig2") else "built, but binary not on PATH")
except Exception as e:
print(" jbig2enc build skipped (optional):", e)
def ensure_installed() -> None:
have_tools = bool(shutil.which("tesseract") and shutil.which("gs"))
try:
import ocrmypdf
import img2pdf
from PIL import Image
have_py = True
except Exception:
have_py = False
if have_tools and have_py:
print("Dependencies already present — skipping installation.")
else:
print("Installing dependencies (first run can take a few minutes)...")
install_dependencies()
ensure_installed()
We set up the complete OCRmyPDF environment for Google Colab by importing the required standard libraries and defining the installation workflow. We install system tools such as Tesseract, Ghostscript, unpaper, pngquant, poppler, and qpdf, along with Python packages like OCRmyPDF, img2pdf, and Pillow. We also optionally build jbig2enc so that advanced PDF optimization can produce smaller outputs for scanned documents.
Loading OCRmyPDF and Building Synthetic Scans
Copy CodeCopiedUse a different Browser
def _purge(*prefixes):
for name in [m for m in list(sys.modules)
if any(m == p or m.startswith(p + ".") for p in prefixes)]:
del sys.modules[name]
def _load_ocrmypdf():
_purge("PIL", "ocrmypdf")
import ocrmypdf
return ocrmypdf
try:
ocrmypdf = _load_ocrmypdf()
except ImportError as e:
if "_Ink" in str(e) or "PIL" in str(e):
print("Repairing an incompatible Pillow (reinstalling pillow<12)...")
sh(f'"{sys.executable}" -m pip install -q --force-reinstall "pillow<12"')
try:
ocrmypdf = _load_ocrmypdf()
print("Pillow repaired — continuing without a restart.")
except Exception:
raise RuntimeError(
"Pillow is still incompatible in this session. Use the Colab menu: "
"Runtime > Restart session, then run this cell again."
)
else:
raise
from ocrmypdf.exceptions import (
ExitCode,
PriorOcrFoundError,
EncryptedPdfError,
MissingDependencyError,
TaggedPDFError,
DigitalSignatureError,
DpiError,
InputFileError,
UnsupportedImageFormatError,
)
from ocrmypdf.helpers import check_pdf
from ocrmypdf.pdfa import file_claims_pdfa
import img2pdf
from PIL import Image, ImageDraw, ImageFont, ImageFilter
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
logging.getLogger("ocrmypdf").setLevel(logging.WARNING)
logging.getLogger("pdfminer").setLevel(logging.ERROR)
logging.getLogger("PIL").setLevel(logging.WARNING)
SAMPLE_TEXT_PAGES = [
"Optical Character Recognition, commonly abbreviated as OCR, is the "
"process of converting images of typed or printed text into machine "
"encoded text. This page was generated as a synthetic scan so that the "
"OCRmyPDF pipeline has something realistic to recognize and search.",
"On 14 March 2026 the archive contained 1,482 pages across 37 folders. "
"Roughly 92 percent of those pages were scanned at 200 to 300 dots per "
"inch. The remaining 8 percent were skewed and required deskewing before "
"any reliable recognition was possible.",
"After OCRmyPDF finishes, the output is a searchable PDF/A file. You can "
"select text, copy it, and run full text search across thousands of "
"documents. The original image resolution is preserved while a hidden "
"text layer is placed accurately underneath the page image.",
]
def _find_font():
for cand in (
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
):
if os.path.exists(cand):
return cand
return None
_FONT_PATH = _find_font()
FONT = ImageFont.truetype(_FONT_PATH, 40) if _FONT_PATH else ImageFont.load_default()
def _add_speckle(img, n=6000, dark=60):
"""Sprinkle light dark specks to imitate scanner noise (motivates --clean)."""
import random
px = img.load()
w, h = img.size
for _ in range(n):
px[random.randint(0, w - 1), random.randint(0, h - 1)] = random.randint(0, dark)
return img
def render_page(text, skew=False):
"""Render one A4 page (1654x2339 px ≈ 200 DPI) of dark text on white."""
W, H = 1654, 2339
img = Image.new("L", (W, H), 255)
draw = ImageDraw.Draw(img)
draw.multiline_text((150, 180), textwrap.fill(text, width=58),
fill=25, font=FONT, spacing=18)
if skew:
img = img.rotate(6, resample=Image.BICUBIC, expand=False, fillcolor=255)
img = img.filter(ImageFilter.GaussianBlur(0.6))
img = _add_speckle(img)
return img
def build_scanned_pdf(pdf_path: Path, pages_text, skew_index=1):
"""Render pages to PNGs and wrap them losslessly into an image-only PDF."""
pngs = []
for i, text in enumerate(pages_text):
img = render_page(text, skew=(i == skew_index))
p = pdf_path.parent / f"_pg_{pdf_path.stem}_{i}.png"
img.save(p, format="PNG", dpi=(200, 200))
pngs.append(str(p))
with open(pdf_path, "wb") as f:
f.write(img2pdf.convert(pngs))
for p in pngs:
os.remove(p)
return pdf_path
def do_ocr(input_file, output_file, **kw):
"""Wrapper around ocrmypdf.ocr() that disables the progress bar and times it."""
kw.setdefault("progress_bar", False)
t0 = time.perf_counter()
rc = ocrmypdf.ocr(input_file, output_file, **kw)
return rc, time.perf_counter() - t0
def tokens(s: str):
return re.findall(r"[a-z0-9]+", s.lower())
def kb(path) -> str:
return f"{Path(path).stat().st_size / 1024:,.1f} KB"
def banner(title: str):
line = "─" * 74
print(f"\n{line}\n {title}\n{line}")
We safely load OCRmyPDF and repair Pillow compatibility issues if they appear in the Colab runtime. We import OCRmyPDF exceptions, PDF validation helpers, img2pdf, and Pillow utilities used throughout the tutorial. We also define the sample document text and helper functions for rendering synthetic scanned pages, adding scanner-like noise, building image-only PDFs, timing OCR runs, tokenizing text, formatting file sizes, and printing section banners.
Running Basic and Advanced PDF/A OCR
Copy CodeCopiedUse a different Browser
banner("0 · Environment")
print("Python :", sys.version.split()[0])
print("ocrmypdf:", ocrmypdf.__version__)
sh("tesseract --version", check=False)
sh("gs --version", check=False)
sh("tesseract --list-langs", check=False)
print("unpaper :", shutil.which("unpaper"))
print("pngquant:", shutil.which("pngquant"))
print("jbig2 :", shutil.which("jbig2"), "(optional encoder)")
WORK = Path("/content/ocrmypdf_demo")
try:
WORK.mkdir(parents=True, exist_ok=True)
except Exception:
WORK = Path.cwd() / "ocrmypdf_demo"
WORK.mkdir(parents=True, exist_ok=True)
print("Workdir :", WORK)
banner("1 · Build a synthetic image-only 'scanned' PDF")
input_pdf = WORK / "scanned_input.pdf"
build_scanned_pdf(input_pdf, SAMPLE_TEXT_PAGES, skew_index=1)
print(f"Created {input_pdf.name} ({kb(input_pdf)}, 3 pages; page 2 is skewed + speckled)")
print("This PDF has NO text layer yet — selecting/searching it returns nothing.")
banner("2 · Basic OCR (deskew + auto-rotate)")
out_basic = WORK / "out_basic.pdf"
rc, dt = do_ocr(
input_pdf, out_basic,
language=["eng"],
deskew=True,
rotate_pages=True,
)
print(f"Exit code: {rc.name} ({int(rc)}) in {dt:.1f}s -> {out_basic.name} ({kb(out_basic)})")
banner("3 · Advanced OCR (PDF/A-2, --optimize 3, sidecar, metadata)")
out_adv = WORK / "out_advanced.pdf"
sidecar = WORK / "ocr_text.txt"
rc, dt = do_ocr(
input_pdf, out_adv,
language=["eng"],
deskew=True,
rotate_pages=True,
optimize=3,
jpg_quality=80,
png_quality=80,
output_type="pdfa-2",
sidecar=sidecar,
title="OCRmyPDF Colab Tutorial",
author="Tutorial",
subject="Demonstration of OCRmyPDF",
keywords="ocr, pdf, tesseract, ocrmypdf",
)
print(f"Exit code: {rc.name} ({int(rc)}) in {dt:.1f}s -> {out_adv.name} ({kb(out_adv)})")
sh(f'pdfinfo "{out_adv}" | grep -E "Title|Author|Subject|Keywords|Pages"', check=False)
We begin the main tutorial by printing the OCR environment details, including Python, OCRmyPDF, Tesseract, Ghostscript, installed languages, and optional optimization tools. We create a working directory and generate a synthetic scanned PDF that has no searchable text layer. We then run both a basic OCR workflow and an advanced OCR workflow with PDF/A output, image optimization, sidecar text generation, and document metadata.
Validating Searchability and OCR Word-Recall
Copy CodeCopiedUse a different Browser
banner("4 · Prove searchability + measure OCR word-recall")
ocr_text = sidecar.read_text(errors="ignore")
print("Sidecar text (first 300 chars):\n" + ocr_text[:300].strip())
embedded = WORK / "embedded_text.txt"
sh(f'pdftotext "{out_adv}" "{embedded}"', check=False)
print(f"\npdftotext extracted {len(embedded.read_text(errors='ignore').split())} "
f"words from the OUTPUT PDF (the input had 0).")
src = tokens(" ".join(SAMPLE_TEXT_PAGES))
found = set(tokens(ocr_text))
recall = sum(1 for w in src if w in found) / max(1, len(src))
print(f"OCR word-recall vs. source: {recall * 100:.1f}% ({len(src)} source words)")
banner("5 · Validate output + size comparison")
print("check_pdf (valid PDF structure):", check_pdf(out_adv))
print("file_claims_pdfa (PDF/A marker):", file_claims_pdfa(out_adv))
print(f"input : {kb(input_pdf)}")
print(f"basic : {kb(out_basic)}")
print(f"advanced : {kb(out_adv)} (PDF/A-2 + image optimisation)")
banner("6 · Modes & exceptions: skip-text / redo-ocr / force-ocr")
try:
do_ocr(out_adv, WORK / "should_fail.pdf", language=["eng"])
print("Unexpected: no exception was raised.")
except PriorOcrFoundError as e:
print(f"Caught PriorOcrFoundError (exit code {e.exit_code}): the PDF already "
f"has text. Choose a mode to override:")
rc, _ = do_ocr(out_adv, WORK / "out_skiptext.pdf", language=["eng"], skip_text=True)
print(f" --skip-text -> {rc.name}")
rc, _ = do_ocr(out_adv, WORK / "out_redo.pdf", language=["eng"], redo_ocr=True)
print(f" --redo-ocr -> {rc.name}")
rc, _ = do_ocr(out_adv, WORK / "out_force.pdf", language=["eng"], force_ocr=True)
print(f" --force-ocr -> {rc.name}")
We prove that OCR has made the scanned PDF searchable by reading the sidecar text and extracting embedded text from the output PDF using pdftotext. We compare the recovered OCR text against the known source text to calculate a simple word-recall score. We then validate the PDF structure, check the PDF/A marker, compare file sizes, and demonstrate how OCRmyPDF handles files that already contain OCR text using skip-text, redo-OCR, and force-OCR modes.
Tuning, Cleaning, and In-Memory OCR
Copy CodeCopiedUse a different Browser
banner("7 · Tesseract engine tuning (--oem / --psm)")
rc, dt = do_ocr(
input_pdf, WORK / "out_tuned.pdf",
language=["eng"],
tesseract_oem=1,
tesseract_pagesegmode=3,
output_type="pdf",
)
print(f"Tuned run -> {rc.name} in {dt:.1f}s")
banner("8 · Image cleaning with unpaper (--clean / --clean-final)")
try:
rc, dt = do_ocr(
input_pdf, WORK / "out_cleaned.pdf",
language=["eng"], deskew=True,
clean=True, clean_final=True, output_type="pdf",
)
print(f"Cleaned run -> {rc.name} in {dt:.1f}s")
except Exception as e:
print("Cleaning step skipped (unpaper issue):", type(e).__name__, e)
banner("9 · Auto-orientation (OSD) on a 90°-rotated page (--rotate-pages)")
try:
rot_png = WORK / "_rot.png"
render_page(SAMPLE_TEXT_PAGES[0]).rotate(90, expand=True, fillcolor=255) \
.save(rot_png, format="PNG", dpi=(200, 200))
rot_pdf = WORK / "rotated_input.pdf"
with open(rot_pdf, "wb") as f:
f.write(img2pdf.convert([str(rot_png)]))
os.remove(rot_png)
rot_side = WORK / "rotated_text.txt"
rc, dt = do_ocr(
rot_pdf, WORK / "out_rotated_fixed.pdf",
language=["eng"], rotate_pages=True, sidecar=rot_side, output_type="pdf",
)
n = len(rot_side.read_text(errors="ignore").split())
print(f"OSD corrected the page; recovered {n} words -> {rc.name} in {dt:.1f}s")
except Exception as e:
print("Auto-orientation demo skipped:", type(e).__name__, e)
banner("10 · OCR a single image (image_dpi hint)")
single_png = WORK / "single_scan.png"
render_page(SAMPLE_TEXT_PAGES[2]).save(single_png, format="PNG")
rc, dt = do_ocr(
single_png, WORK / "out_from_image.pdf",
language=["eng"],
image_dpi=200,
output_type="pdf",
)
print(f"Image -> searchable PDF: {rc.name} in {dt:.1f}s")
banner("11 · In-memory OCR with BytesIO streams")
in_io = io.BytesIO(input_pdf.read_bytes())
out_io = io.BytesIO()
ocrmypdf.ocr(in_io, out_io, language=["eng"], output_type="pdf", progress_bar=False)
out_bytes = out_io.getvalue()
(WORK / "out_in_memory.pdf").write_bytes(out_bytes)
print(f"OCR'd entirely in RAM -> {len(out_bytes):,} bytes written to out_in_memory.pdf")
We experiment with Tesseract engine tuning by setting OCR engine mode and page segmentation mode directly through OCRmyPDF. We then use unpaper-based image cleaning to improve noisy scanned pages and optionally embed the cleaned image into the final output. We also test automatic page orientation correction, convert a single image into a searchable PDF using an explicit DPI hint, and run OCR entirely in memory using BytesIO streams.
Batch OCR and the Typed OcrOptions API
Copy CodeCopiedUse a different Browser
banner("12 · Batch-process a folder of PDFs")
batch_in = WORK / "batch_in"
batch_out = WORK / "batch_out"
batch_in.mkdir(exist_ok=True)
batch_out.mkdir(exist_ok=True)
build_scanned_pdf(batch_in / "invoice_001.pdf",
[SAMPLE_TEXT_PAGES[0], SAMPLE_TEXT_PAGES[1]], skew_index=1)
build_scanned_pdf(batch_in / "memo_002.pdf",
[SAMPLE_TEXT_PAGES[2]], skew_index=-1)
print(f"{'file':<20}{'result':<14}{'time':<8}size")
for src_pdf in sorted(batch_in.glob("*.pdf")):
dst = batch_out / src_pdf.name
try:
rc, dt = do_ocr(src_pdf, dst, language=["eng"],
deskew=True, output_type="pdfa")
print(f"{src_pdf.name:<20}{rc.name:<14}{dt:<8.1f}{kb(dst)}")
except Exception as e:
print(f"{src_pdf.name:<20}{type(e).__name__:<14}{'-':<8}-")
banner("13 · New-style typed OcrOptions API (v17+)")
try:
from ocrmypdf._options import OcrOptions
opts = OcrOptions(
input_file=str(input_pdf),
output_file=str(WORK / "out_options.pdf"),
languages=["eng"],
deskew=True,
rotate_pages=True,
output_type="pdfa",
progress_bar=False,
)
rc = ocrmypdf.ocr(opts)
print(f"OcrOptions run -> {rc.name} ({int(rc)})")
except Exception as e:
print("OcrOptions API not available in this version:", type(e).__name__, e)
banner("14 · Results")
produced = sorted(p for p in WORK.glob("*.pdf"))
for p in produced:
print(f" {p.name:<26}{kb(p)}")
for p in sorted(batch_out.glob("*.pdf")):
print(f" batch_out/{p.name:<16}{kb(p)}")
print(f"\nAll files are in: {WORK}")
try:
from google.colab import files
for p in [out_adv, out_basic, sidecar, embedded]:
if Path(p).exists():
files.download(str(p))
except Exception as e:
print("(Colab download unavailable — open the files from the panel instead.)", e)
print("\nDone.
image")
We scale the workflow from a single file to folder-level batch processing by creating multiple synthetic input PDFs and OCRing each one into an output directory. We then try the newer typed OcrOptions API, which allows us to pass validated OCR settings as a structured options object. Also, we list all generated PDF outputs, including batch results, provide the working directory path, and download key files.
Conclusion
In conclusion, we have a complete OCRmyPDF pipeline that goes far beyond basic scanned-PDF conversion. We created realistic scanned inputs, applied OCR with deskewing and rotation correction, generated optimized PDF/A files, verified embedded text, measured OCR recall, validated PDF structure, and experimented with multiple processing modes, including skip-text, redo-OCR, and force-OCR. We also explored practical production features, including image cleaning, Tesseract engine tuning, in-memory processing, and folder-level batch OCR.
Check out the Full Codes here. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.
Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us
The post OCRmyPDF Tutorial: Convert Scanned Documents into Searchable PDF/A Files with Sidecar Text Extraction and Batch Processing appeared first on MarkTechPost.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み