Building a PDF Reader in 500 Lines of Python with PyQt and SQLite

This tutorial walks through building PyReadon, a functional PDF reader in ~500 lines of Python using PyQt for the GUI and SQLite for persistent storage, covering book metadata management, multi-book reading sessions, mouse-click page navigation, and database-driven state persistence.

Python Crawling & Data Mining
Python Crawling & Data Mining
Python Crawling & Data Mining
Building a PDF Reader in 500 Lines of Python with PyQt and SQLite

After two weeks of iteration, the PyReadon PDF reader implements core features: adding and deleting books, reading books, saving reading progress, and viewing book metadata. This version optimizes the book data structure, supports simultaneous reading of multiple books, adds left-click page turning, and uses SQLite to persist the library and per-book information such as file path and current page.

Book Class Design

The Book class encapsulates book metadata, page count, and reading status. The __eq__ special method compares two Book instances by their file name ( fname) to determine if they represent the same book.

class Book:
    def __init__(self, fname):
        # File name
        self.fname = fname
        # Whether the book has been read
        self.flag = None
        self._info = None
        self._page = 0
        self.get_meta_data(self.fname)

    def __eq__(self, other):
        if hasattr(other, 'fname'):
            return self.fname == other.fname
        return False

Simultaneous Reading of Multiple Books

An internal read_list stores Book objects currently being read. Each book retains its own page attribute so the program remembers the last page for every book. On startup, read_list is initialized with a placeholder None followed by all books whose flag indicates they were being read when the app last closed.

self.read_list = [None]
self.read_list.extend(book for book in self.booklist if book.flag)

Left-Click Page Turning

The MyArea widget overrides mousePressEvent. The mouse's x-coordinate ( event.pos().x()) is compared against the widget width: clicks in the left third trigger self.left() (previous page), clicks in the right third trigger self.right() (next page).

# Mouse left-click page turning
def mousePressEvent(self, event):
    pos = event.pos().x()
    width = self.size().width()
    if event.button() == Qt.LeftButton:
        if pos > width * 2 / 3:
            self.right()
        elif pos < width / 3:
            self.left()

SQLite Persistence

SQLite is chosen for its serverless, zero-configuration, and fast characteristics. On launch, read_db() reads book records from PDF.db:

If PDF.db does not exist, create it and a book_info table with columns path, page, flag.

Query all rows, construct a namedtuple for each, instantiate a Book object, restore its page and flag, and yield the book.

book_db = 'PDF.db'
book_info = namedtuple('info', 'path page flag')

def read_db():
    os.chdir(os.path.dirname(os.path.realpath(__file__)))
    if not os.path.exists(book_db):
        conn = sqlite3.connect(book_db)
        conn.execute("CREATE TABLE book_info(path, page, flag)")
        conn.close()
    conn = sqlite3.connect(book_db)
    for row in conn.execute('SELECT * FROM book_info'):
        info = book_info(*row)
        book = Book(info.path)
        book.page = info.page
        book.flag = info.flag
        yield book
    conn.close()

Saving the library state uses save2db(), which first clears the table via remove_db() then bulk-inserts current book data:

def save2db(booklist):
    conn = sqlite3.connect(book_db)
    conn.executemany("INSERT INTO book_info Values (?,?,?)",
        ((book.fname, book.page, book.flag) for book in booklist))
    conn.commit()
    conn.close()

def remove_db():
    conn = sqlite3.connect(book_db)
    conn.execute('DELETE FROM book_info')
    conn.commit()
    conn.close()

Viewing Book Metadata

A right-click context menu triggers QMessageBox.about() to display detailed metadata extracted from the PDF: path, format, title, author, creator, and producer.

elif action == item3:
    index = row_num * 8 + col_num
    book = self.booklist[index]
    info = book.info
    fmt = f'Path: {info.path}

' \
        f'Format: {info.format}

' \
        f'Title: {info.title}

' \
        f'Author: {info.author}

' \
        f'Creator: {info.creator}

' \
        f'Producer: {info.producer}'
    QMessageBox.about(self, 'Document Info', fmt)

About Dialog via Qt Designer

An Info dialog designed in Qt Designer is bound to the main window's info toolbar action:

info = Info()
reader.infobar.triggered.connect(info.show)

The complete source code is available at https://github.com/cassieeric/Python-Application.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonSQLiteDesktop ApplicationGUI DevelopmentPyQtPDF Reader
Python Crawling & Data Mining
Written by

Python Crawling & Data Mining

Life's short, I code in Python. This channel shares Python web crawling, data mining, analysis, processing, visualization, automated testing, DevOps, big data, AI, cloud computing, machine learning tools, resources, news, technical articles, tutorial videos and learning materials. Join us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.