PDFにOCR結果を埋め込んで検索したらハイライトがあたるようにする

# 概要
通常は画像で作られたPDFはPDF内検索ができない。
[hOCR](https://en.wikipedia.org/wiki/HOCR)などの規格もあるが、以下のステップを踏むことでPDFにOCRの結果を埋め込んで検索したらハイライトがあたるようにすることができる。

- OCRをかけて文字の読み取り結果とPDF内での位置をとる
- PDFの画像の上に文字書き込み用のレイヤーを作り文字を書き込む
- 文字書き込み用のレイヤーとオリジナルPDFページを結合する
- PDF内検索をした場合にハイライトがあたる

こちらの記事では以下のPDFのページにOCRを実行して検索をかけられるようにしてみる。

- 対象PDF
    - ![sample 20.21.34.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110482/deb0ae8d-1201-cc59-594e-4fa3c7d55a84.jpeg)
- 成果物
    - ![スクリーンショット 2023-08-10 8.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110482/a06bf09c-398f-7233-4afd-45104a418d06.png)

## OCRをかけて文字の読み取り結果とPDF内での位置をとる

Azure AI Vision OCR の場合は題材のPDFにOCRにかけると以下のようなjsonが取得できる。読み取った文字と文字の位置などが含まれている。

- Azure AI Vision OCRの場合、boundingBoxの位置は画像の左上からのポジションであることに注意
- Azure AI Vision OCRの場合、boundingBoxの数値の単位がinchの場合もあるのことに注意
- [Azure AI Vision OCRのjsonサンプル](https://learn.microsoft.com/ja-jp/azure/ai-services/computer-vision/how-to/call-read-api#sample-json-output)

``` json
{
    "boundingBox": [
        1.571,
        9.8961,
        3.4002,
        9.7777,
        3.4218,
        10.0469,
        1.5925,
        10.1653
    ],
    "appearance": {
        "style": {
            "name": "handwriting",
            "confidence": 1.0
        }
    },
    "text": "こんにちは世界",
    "words": [
        {
            "boundingBox": [
                1.5818,
                9.9177,
                1.7324,
                9.8961,
                1.7539,
                10.1546,
                1.5925,
                10.1653
            ],
            "text": "こ",
            "confidence": 0.996
        },
        {
            "boundingBox": [
                1.7432,
                9.8961,
                1.9153,
                9.8854,
                1.9261,
                10.1438,
                1.7539,
                10.1546
            ],
            "text": "ん",
            "confidence": 0.998
        },
        {
            "boundingBox": [
                1.9476,
                9.8854,
                2.1305,
                9.8746,
                2.1413,
                10.133,
                1.9584,
                10.1438
            ],
            "text": "に",
            "confidence": 0.996
        },
        {
            "boundingBox": [
                2.2274,
                9.8638,
                2.3995,
                9.8531,
                2.4103,
                10.1223,
                2.2381,
                10.133
            ],
            "text": "ち",
            "confidence": 0.998
        },
        {
            "boundingBox": [
                2.4318,
                9.8531,
                2.6147,
                9.8423,
                2.6255,
                10.1115,
                2.4426,
                10.1223
            ],
            "text": "は",
            "confidence": 0.997
        },
        {
            "boundingBox": [
                2.8515,
                9.8208,
                3.0236,
                9.81,
                3.0344,
                10.0792,
                2.8622,
                10.09
            ],
            "text": "世",
            "confidence": 0.998
        },
        {
            "boundingBox": [
                3.0882,
                9.81,
                3.3034,
                9.7884,
                3.3142,
                10.0684,
                3.099,
                10.0792
            ],
            "text": "界",
            "confidence": 0.996
        }
    ]
}
```

## PDFの画像の上に文字書き込み用のレイヤーを作り文字を書き込む

このステップは以下の処理が必要になる。

- 元のPDFのページと同じ大きさのページ(文字書き込み用のレイヤー)を作る
- 作成したページにOCR結果から取得したboundingBoxの位置に取得した文字を描画する

#### 元のPDFのページと同じ大きさのページ(文字書き込み用のレイヤー)を作る

Pythonで実装する場合には[reportlab](https://www.reportlab.com/)が便利だ。canvasというモジュールを使って、PDFページを作成する。そこに、OCRで読み取った文字を書き込んでいく。

``` python
can = canvas.Canvas("文字書き込み用のレイヤーPDFの書き出し先パス", pagesize=pagesize)
can.drawString(x_position, y_position, "書き込むテキスト")
```

#### 作成したページにOCR結果から取得したboundingBoxの位置に取得した文字を描画する

作成したページにOCR結果から取得したboundingBoxの位置に取得した文字を描画する。
reportlabを用いた場合はdrawStringを使って書き込んだ後に保存する。

``` python
can.setFillColorRGB(0, 0, 0, 0)  #透明な文字
can.drawString(x_position, y_position, "書き込むテキスト")
can.showPage()
can.save()
```

この際にy_positionに注意する。Azure AI Vision OCRの場合、boundingBoxの位置は画像の左上を起点としたポジションであるのに対し、多くのPDFライブラリはPDFのポジションが左下が起点になる。reportlabも左下が起点になるので、y_positionを変換する必要がある。


## 文字書き込み用のレイヤーとオリジナルPDFページを結合する

文字書き込み用のレイヤーとオリジナルPDFページを結合する。
Pythonで実装する場合にはpypdfのPdfReader,PdfWriterを用いると簡潔に実装できる。

``` python
reader = PdfReader("オリジナルPDFのパス")
pdf = PdfWriter()  # OCR結果を書き込む最終成果物のPDF
new_pdf = PdfReader("文字書き込み用のレイヤーPDFの書き出し先パス")
reader.pages[0].merge_page(new_pdf.pages[0])
pdf.add_page(reader.pages[0])
```

## PDF内検索をした場合にハイライトがあたる
文字書き込み用のレイヤーを結合したPDFは検索した場合にハイライトがあたるようになる。

![スクリーンショット 2023-08-10 8.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110482/a06bf09c-398f-7233-4afd-45104a418d06.png)

# 実用する上で課題になってくること

以上でPDF内検索ができるようになるが、運用してみると以下のような課題が出た。どこまでの課題を拾うべきかを検討する必要がある。

- 透明な文字をピッタリとポジショニングするのは困難であるのでハイライトの位置がずれる
- 回転しているPDFの補正が必要
    - 参考: https://qiita.com/nassy20/items/baa8d5cf01159292db24
- 縦書き、横書きのレイアウトが存在する
- 複数の文字角度が混ざるドキュメントが存在する
- 回転した文字はそのままの角度で透明な文字を書き込まないと、文字がPDF領域からはみ出て検索できなくなる
- 画像の文字が離れていると別の単語として扱われる
    - 以下のレイアウトだと"軽"と"油"に分かれがち
    - ![スクリーンショット 2023-08-10 8.57.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110482/5c0911c1-f074-e3a4-7c1e-651309b97839.png)

以上です。