2023-12-20

SQLite と PDF ファイル ~ PySide6

SQLite は、パブリックドメインの軽量なリレーショナルデータベース管理システム (RDBMS) です。他の多くのデータベース管理システムとは対照的に、サーバとしてではなくアプリケーションに組み込んで利用するデータベースです。 一般的な RDBMS と違い、API は単純にライブラリを呼び出すだけであり、データの保存に単一のファイルを使用することが特徴です。

Wikipedia より引用、編集
今回のテーマ

選択した PDF ファイルを読み込んで SQLite のデータベースに格納します。そして、データベースから読み込んだ内容を(一旦保存して) QPdfView クラスのウィジェットで開く、という GUI サンプルを紹介します。

  • 本ブログの過去記事 [1] を PySide2 から PySide6 に変更して、PySide6 / Qt6 の機能をなるべく利用できるような内容に見直しました。

下記の OS 環境で動作確認をしています。

Fedora Workstation 39 x86_64
Python 3.11.6
PySide6 6.6.1

少し長いサンプルになってしまいましたが、以下にサンプル qt_sqlite_pdf.py を示しました。

qt_sqlite_pdf.py
import base64
import os
import sys
import tempfile
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction
from PySide6.QtPdf import QPdfDocument
from PySide6.QtPdfWidgets import QPdfView
from PySide6.QtSql import QSqlDatabase, QSqlQuery
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QFileDialog,
QMainWindow,
QSizePolicy,
QToolBar,
QWidget,
)
def create_table():
query = QSqlQuery()
sql = """
CREATE TABLE IF NOT EXISTS file (
name_file TEXT UNIQUE,
content NONE
);
"""
if not query.exec(sql):
print(query.lastError())
def get_content_from_filename(filename: str) -> bytes:
content = None
query = QSqlQuery()
sql = """
SELECT content FROM file
WHERE name_file = "%s";
""" % filename
query.exec(sql)
if query.next():
content_str = query.value(0)
content = base64.b64decode(content_str.encode())
return content
def get_list_file(list_file: list):
query = QSqlQuery()
sql = 'SELECT name_file FROM file;'
flag = query.exec(sql)
while query.next():
list_file.append(query.value(0))
if not flag:
print(query.lastError())
def insert_filename_content(filename: str, content_str: str):
sql = 'INSERT INTO file VALUES(?, ?);'
query = QSqlQuery()
query.prepare(sql)
query.bindValue(0, filename)
query.bindValue(1, content_str)
"""
note: This does not work!
query.bindValue(
1, content,
type=QSql.ParamTypeFlag.In | QSql.ParamTypeFlag.Binary
)
"""
if not query.exec():
print(query.lastError())
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.dbname = 'test.sqlite'
self.con = QSqlDatabase.addDatabase('QSQLITE')
self.con.setDatabaseName(self.dbname)
self.init_table()
self.combo = None
self.init_ui()
self.update_filelist()
self.setWindowTitle('SQLite & PDF Test')
self.resize(600, 800)
def init_table(self):
if self.con.open():
create_table()
self.con.close()
def init_ui(self):
menubar = self.menuBar()
menu_file = menubar.addMenu('&File')
file_open = QAction('Open', self)
file_open.setShortcut('Ctrl+O')
file_open.setStatusTip('open PDF file')
file_open.triggered.connect(self.show_dialog)
menu_file.addAction(file_open)
toolbar = QToolBar()
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
self.combo = QComboBox()
self.combo.setSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Preferred
)
self.combo.currentTextChanged.connect(self.on_current_text_changed)
toolbar.addWidget(self.combo)
view = QPdfView(self)
view.setPageMode(QPdfView.PageMode.MultiPage)
view.setZoomMode(QPdfView.ZoomMode.FitToWidth)
self.setCentralWidget(view)
def show_dialog(self):
dialog = QFileDialog()
dialog.setWindowTitle('PDF file selection')
dialog.setNameFilters(['PDF files (*.pdf)'])
if dialog.exec():
filename = dialog.selectedFiles()[0]
basename = os.path.basename(filename)
f = open(filename, 'rb')
with f:
content = base64.b64encode(f.read())
content_str: str = content.decode()
if self.con.open():
insert_filename_content(basename, content_str)
self.con.close()
self.update_filelist()
self.combo.setCurrentText(basename)
def update_filelist(self):
list_name_file = list()
if self.con.open():
get_list_file(list_name_file)
self.con.close()
self.combo.clear()
for name_file in list_name_file:
self.combo.addItem(name_file)
def on_current_text_changed(self):
filename = self.combo.currentText()
if len(filename) == 0:
return
if self.con.open():
content = get_content_from_filename(filename)
self.con.close()
if content is not None:
filepath = os.path.join(tempfile.gettempdir(), filename)
with open(filepath, 'wb') as f:
f.write(content)
document = QPdfDocument(self)
document.load(filepath)
view: QWidget | QPdfView = self.centralWidget()
view.setDocument(document)
def main():
app = QApplication()
ex = Example()
ex.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

サンプルの説明

初期化

サンプルを実行すると、データベースに接続するインスタンスを生成します。このとき指定した名前のデータベースファイル test.sqlite が存在してければ作成されます。

class Example(QMainWindow):
    def __init__(self):
        super().__init__()
        self.dbname = 'test.sqlite'
        self.con = QSqlDatabase.addDatabase('QSQLITE')
        self.con.setDatabaseName(self.dbname)
        self.init_table()
           :
           :

その後、使用するデータベースのテーブルの初期化をします。

def init_table(self):
    if self.con.open():
        create_table()
        self.con.close()

テーブルの初期化処理は下記のように単純なものです。

def create_table():
    query = QSqlQuery()
    sql = """
        CREATE TABLE IF NOT EXISTS file (
            name_file TEXT UNIQUE,
            content NONE
        );
    """
    if not query.exec(sql):
        print(query.lastError())

その後、プログラムは GUI を生成します。

PDF ファイルを読み込んでみる

実行したサンプルのメニューから File » Open をクリックします。

qt_sqlite_pdf.py の実行例 (1)

データベースに格納する PDF ファイルを選択します。

qt_sqlite_pdf.py の実行例 (2)

ファイルをバイナリモードで読み込んで、パスを除いたファイル名と読み込んだ内容を、データベースに格納します。

def show_dialog(self):
    dialog = QFileDialog()
    dialog.setWindowTitle('PDF file selection')
    dialog.setNameFilters(['PDF files (*.pdf)'])
    if dialog.exec():
        filename = dialog.selectedFiles()[0]
        basename = os.path.basename(filename)
        f = open(filename, 'rb')
        with f:
            content = base64.b64encode(f.read())
            content_str: str = content.decode()
 
        if self.con.open():
            insert_filename_content(basename, content_str)
            self.con.close()
            self.update_filelist()
            self.combo.setCurrentText(basename)

PySide2 を利用した過去記事 [1] では、sqlite3 モジュールを利用して、なにも考えずにバイナリをデータベースに格納できたのですが、PySide6 では QSqlQuery のインスタンスで同じようにしたところ、バイナリを書き込むことができませんでした。

PySide6 ではクエリのタイプにバイナリを指定できるのですが、これでは解決できなかったので、やむなく Python の標準ライブラリで利用できる Base64 でエンコードした byte 型のデータ列を文字列にデコードして格納しています。

def insert_filename_content(filename: str, content_str: str):
    sql = 'INSERT INTO file VALUES(?, ?);'
    query = QSqlQuery()
    query.prepare(sql)
    query.bindValue(0, filename)
    query.bindValue(1, content_str)
    """
    note: This does not work!
    query.bindValue(
        1, content,
        type=QSql.ParamTypeFlag.In | QSql.ParamTypeFlag.Binary
    )
    """
    if not query.exec():
        print(query.lastError())

データベースに書き込んだ内容を読み込み直して QPdfView のインスタンスに表示されます。

qt_sqlite_pdf.py の実行例 (3)

QPdfView のインスタンス view は、QMainWindow を継承した Example クラスに setCentralWidget メソッドで配置されています。

def init_ui(self):
       :
       :
    self.combo.currentTextChanged.connect(self.on_current_text_changed)
    toolbar.addWidget(self.combo)
 
    view = QPdfView(self)
    view.setPageMode(QPdfView.PageMode.MultiPage)
    view.setZoomMode(QPdfView.ZoomMode.FitToWidth)
    self.setCentralWidget(view)

QComboBox のインスタンスに表示されている文字が変更されるとシグナルが発生して、インスタンス・メソッド on_current_text_changed メソッドが実行されます。

def on_current_text_changed(self):
    filename = self.combo.currentText()
    if len(filename) == 0:
        return
 
    if self.con.open():
        content = get_content_from_filename(filename)
        self.con.close()
 
        if content is not None:
            filepath = os.path.join(tempfile.gettempdir(), filename)
            with open(filepath, 'wb') as f:
                f.write(content)
            document = QPdfDocument(self)
            document.load(filepath)
            view: QWidget | QPdfView = self.centralWidget()
            view.setDocument(document)

このメソッドで、データベースから QComboBox のインスタンスに表示された文字列 = ファイル名に対応するファイルの中身をデータベースから読み込みます。

クエリの処理は下記のようなっています。ここで、取り出した文字列を byte 型にエンコードして、Base64 のデコードをして元のバイナリに戻しています(こうやって説明すると、なんだか面倒くさいことをしていると思います、今後の改善課題です)。

def get_content_from_filename(filename: str) -> bytes:
    content = None
    query = QSqlQuery()
    sql = """
        SELECT content FROM file
        WHERE name_file = "%s";
    """ % filename
    query.exec(sql)
    if query.next():
        content_str = query.value(0)
        content = base64.b64decode(content_str.encode())
    return content

データベースから取り出して元に戻したバイナリにファイル名をつけてテンポラリの領域に保存して、それを QPdfDocument のインスタンスで読み込んで、QPdfView のインスタンス view に渡して表示しています。

複数の PDF ファイルの読み込み

複数の PDF ファイルをデータベースに読み込むと、QComboBox のインスタンスにファイル名が列挙されます。どれかを選択すれば、その PDF ファイルが表示されます。

qt_sqlite_pdf.py の実行例 (4)

既知の問題

PySide6 / Qt6 の PDF を扱うモジュールをまだ使い慣れていないためか、標準出力に下記のような警告が出ます。特定の PDF ファイルで警告が出るのですが、それが PDF ファイルの問題なのか特定できていません。対応方法が判ったら追記します。

qt.pdf.links: skipping link with invalid page number
qt.pdf.links: skipping link with invalid page number
qt.pdf.links: skipping link with invalid page number
    :
    :
    :

参考サイト

  1. bitWalk's: 【備忘録】SQLite のデータベースでバイナリデータを扱う [2021-02-18]
  2. QPdfDocument - Qt for Python
  3. QPdfView - Qt for Python

 

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



0 件のコメント: