アイトラッカーを使ってPDFからテキストを取得する

テキストベースのPDFから、注視点のテキストを取得する方法です。

クリックした単語をターミナルに出力するPDFビューワをPyMuPDFとPyQt5で自作して、アイトラッカーでマウスを操作、クリックして取得する感じです。

今回はTobii Pro Sparkを使用していますが、見るマウスに対応したアイトラッカーデバイスなら使えるはずです。

# 環境
Python 3.10.6
Tobii Pro Eye Tracker Manager 2.7.2
見るマウス
https://millmouse.wordpress.com/about/%E8%A6%8B%E3%82%8B%E3%83%9E%E3%82%A6%E3%82%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/

# プログラム
```
pip install --upgrade pymupdf
pip install PyQt5
```

--upgradeはいらなそうだけど公式のドキュメントに書いてある通りに貼っておきます

```
import sys
import fitz  # PyMuPDF
from PyQt5.QtWidgets import QApplication, QLabel, QScrollArea
from PyQt5.QtGui import QPixmap, QImage, QPainter, QColor, QPen
from PyQt5.QtCore import Qt, QRectF

PDF_PATH = "sample.pdf"  


# QScrollAreaを継承
class PDFViewer(QScrollArea):
    def __init__(self, pdf_path):
        super().__init__()
        self.doc = fitz.open(
            pdf_path
        )  
        self.page_num = 0  
        self.zoom = 3  # ズーム倍率

        self.label = QLabel() 
        self.setWidget(
            self.label
        )  
        self.setWidgetResizable(True) 

        self.setWindowTitle("PDF Viewer with Word Bounding Boxes")  # タイトル
        self.setMinimumSize(1200, 900)  # ウィンドウの最小サイズ

        self.update_page()  
        
    # メソッド：ページ画像の生成と表示、単語バウンディングボックスの描画
    def render_page(self):
        page = self.doc[self.page_num]
        mat = fitz.Matrix(self.zoom, self.zoom)
        pix = page.get_pixmap(matrix=mat)

        if pix.alpha:
            fmt = QImage.Format_RGBA8888
        else:
            fmt = QImage.Format_RGB888
        img = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt)
        if img.isNull():
            print("画像の生成に失敗しました。")
            return None
        return QPixmap.fromImage(img)

    def draw_word_boxes(self, pixmap, highlight_rect=None):
        painter = QPainter(pixmap)
        pen = QPen(QColor(255, 0, 0), 1)
        painter.setPen(pen)
        for rect, _ in self.word_boxes:
            painter.drawRect(rect)

        if highlight_rect:
            pen = QPen(QColor(0, 0, 255), 2)
            painter.setPen(pen)
            painter.drawRect(highlight_rect)

        painter.end()
        return pixmap

    def update_page(self):
        page = self.doc[self.page_num]
        words = page.get_text("words")
        self.word_boxes = []
        for w in words:
            x0, y0, x1, y1, word, *_ = w
            rect = QRectF(
                x0 * self.zoom,
                y0 * self.zoom,
                (x1 - x0) * self.zoom,
                (y1 - y0) * self.zoom,
            )
            self.word_boxes.append((rect, word))

        pixmap = self.render_page()
        if pixmap:
            pixmap = self.draw_word_boxes(pixmap)
            self.label.setPixmap(pixmap)
            self.label.resize(pixmap.size())

    # メソッド：マウスホイールイベントの処理（ズームイン・アウト）
    def wheelEvent(self, event):
        # Ctrlキー＋マウスホイールでズームイン・アウト
        if event.modifiers() & Qt.ControlModifier:
            delta = event.angleDelta().y()
            if delta > 0:
                self.zoom *= 1.1  # ズームイン
            else:
                self.zoom /= 1.1  # ズームアウト
            self.update_page()  # 再描画
        else:
            super().wheelEvent(event)  # 通常のスクロール

    # メソッド：マウスクリックイベントの処理
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            pos = self.label.mapFromParent(event.pos())
            for rect, word in self.word_boxes:
                if rect.contains(pos):
                    print(f"Clicked text: {word}")
                    self.highlight_word(rect)

    # メソッド：指定した単語の矩形を青枠でハイライトして再描画
    def highlight_word(self, rect):
        pixmap = self.render_page()
        if pixmap:
            pixmap = self.draw_word_boxes(pixmap, rect)
            self.label.setPixmap(pixmap)
            self.label.resize(pixmap.size())

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Right:  # 右矢印キーで次ページ
            if self.page_num < len(self.doc) - 1:
                self.page_num += 1
                self.update_page()
        elif event.key() == Qt.Key_Left:  # 左矢印キーで前ページ
            if self.page_num > 0:
                self.page_num -= 1
                self.update_page()
        else:
            super().keyPressEvent(event)


if __name__ == "__main__":
    app = QApplication(sys.argv)  
    viewer = PDFViewer(
        PDF_PATH
    )  
    viewer.show()  
    sys.exit(app.exec_()) 

```

クリックした単語をターミナルに出力します。画像ベースのpdfではテキストを取得できない点に注意が必要です。
左右矢印でページの切り替えを実行します。

```
self.zoom = 3
```
ここの値を変更するとデフォルトの倍率を変えることが可能です。

もっとキレイにプログラミングできるように精進します。