Featured image of post YomiTokuを利用してPDFを検索可能にする

YomiTokuを利用してPDFを検索可能にする

始めに

YomiTokuPDFSearchableを作成しました。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import json
import sys
import cv2
import os
from yomitoku import OCR
from yomitoku.data.functions import load_pdf
from reportlab.pdfgen import canvas
from PIL import Image
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

# 日本語対応フォント(IPAexMincho)の登録(ipaexm.ttfが必要)
pdfmetrics.registerFont(TTFont('IPAexMincho', 'ipaexm.ttf'))

def json_to_hocr(json_data, page_num):
    hocr = [
        '<!DOCTYPE html>', '<html>', '<head>', '<meta charset="UTF-8">',
        f'<title>hOCR output - Page {page_num}</title>', '</head>', '<body>',
        f'<div class="ocr_page" id="page_{page_num}">'
    ]
    for i, word in enumerate(json_data['words']):
        content = word['content']
        points = word['points']
        bbox = f"bbox {points[0][0]} {points[0][1]} {points[2][0]} {points[2][1]}"
        hocr.append(
            f'<span class="ocrx_word" id="word_{i+1}" title="{bbox}; x_wconf {int(word["rec_score"] * 100)}">{content}</span>'
        )
    hocr.append('</div>')
    hocr.append('</body>')
    hocr.append('</html>')
    return '\n'.join(hocr)

def draw_invisible_text(c, text, x, y, font_size):
    """
    PDF上に検索可能なテキストを描画します。
    ここでは、テキストを完全に透明(または背景色と同じ色)に設定することで、見た目には表示されないがPDF内に文字情報として残ります。
    """
    c.saveState()
    c.setFont("IPAexMincho", font_size)
    try:
        # 透明度を0に設定(ReportLabのバージョンによっては未対応の場合があります)
        c.setFillAlpha(0)
    except AttributeError:
        # 透明度がサポートされない場合、背景が白前提で白色に設定
        c.setFillColorRGB(1, 1, 1)
    c.drawString(x, y, text)
    c.restoreState()

def pdf_to_searchable(pdf_path, output_pdf):
    ocr = OCR(visualize=False, device="cpu")
    imgs = load_pdf(pdf_path)
    
    # OCR結果や画像を一時ファイルとして保存
    temp_img_files = []
    json_files = []
    hocr_files = []
    for i, img in enumerate(imgs):
        results, _ = ocr(img)
        json_path = f"output_{i}.json"
        results.to_json(json_path)
        json_files.append(json_path)
        
        # 画像を一時保存
        img_path = f"output_page_{i}.jpg"
        cv2.imwrite(img_path, img)
        temp_img_files.append(img_path)
        
        # 任意でhOCRも出力(ログ用)
        with open(json_path, "r", encoding="utf-8") as f:
            json_data = json.load(f)
        hocr_output = json_to_hocr(json_data, i + 1)
        hocr_path = f"output_{i}.hocr"
        with open(hocr_path, "w", encoding="utf-8") as f:
            f.write(hocr_output)
        hocr_files.append(hocr_path)
    
    # ReportLabで新たなPDFを生成
    c = canvas.Canvas(output_pdf)
    
    for i, img_path in enumerate(temp_img_files):
        pil_img = Image.open(img_path)
        width, height = pil_img.size
        c.setPageSize((width, height))
        
        # ページ背景に画像を描画
        c.drawImage(img_path, 0, 0, width=width, height=height)
        
        # 対応するOCR結果のJSONを読み込み、非表示テキストレイヤーを追加
        json_path = json_files[i]
        with open(json_path, "r", encoding="utf-8") as f:
            json_data = json.load(f)
        
        for word in json_data['words']:
            content = word['content']
            points = word['points']
            # OCRの座標は画像の左上原点。ReportLabは左下原点なので変換が必要です。
            x1, y1 = points[0]
            x2, y2 = points[2]
            pdf_x = x1
            pdf_y = height - y2  # 下部へ変換
            font_size = y2 - y1 if (y2 - y1) > 0 else 10
            draw_invisible_text(c, content, pdf_x, pdf_y, font_size)
        
        c.showPage()
    
    c.save()
    
    # 一時ファイルの削除
    for file in temp_img_files + json_files + hocr_files:
        os.remove(file)
    
    print(f"Searchable PDF created: {output_pdf}")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python script.py input.pdf output.pdf")
        sys.exit(1)

    input_pdf = sys.argv[1]
    output_pdf = sys.argv[2]

    try:
        pdf_to_searchable(input_pdf, output_pdf)
    except Exception as e:
        print(f"Error: {e}")

機能

PDFのOCR処理

yomitokuライブラリを使用してPDFを画像に変換し、OCR処理を行います。OCRの結果(テキスト、座標、信頼度)をJSON形式で保存します。特に今回、YomiTokuを利用することで縦書き文字にも対応しています。

検索可能なPDFの生成

reportlabライブラリを使用して、元のPDF画像の背景に透明なテキストレイヤーを追加します。これにより、PDFビューアでテキスト検索が可能になります。

hOCRの生成(オプション)

OCRの結果をhOCR形式(HTMLに埋め込まれたOCR情報)で保存し、ログやデバッグに使用できます。

一時ファイルの管理

OCR処理中に生成される一時ファイル(画像、JSON)を処理後に削除します。

日本語フォントのサポート

IPAexMinchoフォントを使用して、日本語テキストを正しく表示します。

使い方

1
python script.py input.pdf output.pdf

主に自炊したPDFの処理に活用するものと考えています。

その他

GPUを活用して処理すると高速ですが、CPUでも現実的な速度で処理できるかと思います。

Licensed under CC BY-NC-SA 4.0