DEV Community

Kavindu Kavishka Premarathna
Kavindu Kavishka Premarathna

Posted on

Progress Update: I Turned My Python PDF Prototype into a Desktop Research Reader

My previous milestone was an interactive PDF-reader prototype running in Jupyter. It could render PDF pages, navigate, search text, save bookmarks and notes, and restore reading progress.

This update is about the next major step: moving that prototype into a real desktop application with PySide6. The project is still not an AI reader yet, but it now has the core features I need before adding an LLM: reading, navigation, search, notes, bookmarks, and saved annotations.

What changed

The reader is now a desktop application launched with:

python -m src.main
Enter fullscreen mode Exit fullscreen mode

The desktop version currently supports:

  • Open any local PDF with a native file picker
  • Render pages in a scrollable window
  • Previous, Next, and Go-to-page navigation
  • Zoom controls and a Fit page view
  • Mouse-wheel scrolling within pages
  • Mouse-wheel page changes at the top or bottom of a page
  • Persistent last-read-page restoration
  • Dockable Bookmarks panel
  • Dockable Notes panel
  • Text search with highlighted results
  • Persistent highlight, underline, and strike-through annotations
  • Annotations panel with open and delete actions
  • Keyboard shortcuts for common reading actions

The important design choice is that bookmarks, notes, reading progress, and annotations remain in local reader data rather than modifying the original PDF. That keeps the original paper safe and lets the application later provide an explicit export option.

From notebook UI to desktop UI

The Jupyter prototype used ipywidgets. It was useful for learning the reader logic, but it was not the experience of a normal PDF application.

I moved the UI into PySide6 and kept the underlying reader services separate:

custom-ai-pdf-reader/
├── notebooks/
│   └── Untitled.ipynb
├── src/
│   ├── main.py
│   ├── pdf_service.py
│   ├── reader_state.py
│   └── storage_service.py
├── tests/
│   └── test_storage.py
├── pytest.ini
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

This separation has been useful:

Module Responsibility
main.py Desktop interface, controls, panels, events
pdf_service.py Open PDFs, render pages, extract and search text
reader_state.py Current page, zoom level, search state, bookmarks, notes, annotations
storage_service.py Save and load reader data from JSON

The UI can change from Jupyter to PySide6 without rewriting the PDF and persistence logic.

Building the first desktop reader

The desktop reader uses a QMainWindow as the application shell. The PDF page is rendered with PyMuPDF, converted into a Qt image, and shown through a QLabel inside a QScrollArea.

image = render_page(
    document=self.document,
    page_number=current_page,
    dpi=dpi
)

qimage = self.pil_to_qimage(image)
self.original_pixmap = QPixmap.fromImage(qimage)
self.update_page_display()
Enter fullscreen mode Exit fullscreen mode

A scroll area is important because zoomed pages can become larger than the available window. It allows normal vertical and horizontal scrolling instead of forcing a page to remain at one fixed size.

Fit page and mouse-wheel navigation

A reader should not start by showing an oversized or cropped page. I added a Fit page mode that scales the rendered page to the available viewport while preserving its aspect ratio.

fitted_pixmap = self.original_pixmap.scaled(
    viewport_size,
    Qt.AspectRatioMode.KeepAspectRatio,
    Qt.TransformationMode.SmoothTransformation
)
Enter fullscreen mode Exit fullscreen mode

The fit mode automatically updates when the window is resized. Manual zoom disables fit mode, and the Fit page button restores it.

I also added a more natural mouse-wheel behavior:

  • Scroll normally while reading a large page.
  • Scroll down at the bottom of the page to move to the next PDF page.
  • Scroll up at the top of the page to move to the previous PDF page.

This makes the reader feel closer to a standard document viewer while keeping page-based persistence and navigation.

Bookmarks and notes in dockable panels

Bookmarks and notes were already stored by the prototype. The desktop reader now exposes them through dockable side panels.

The Bookmarks panel supports:

  • Add a custom label to the current page
  • Double-click a bookmark to open its page
  • Delete a selected bookmark
  • Restore bookmarks for the opened document

The Notes panel supports:

  • Write a multi-line note for the current page
  • Save a timestamp with the note
  • Double-click a note to return to its page
  • Delete a selected note
  • Restore notes when the same PDF is opened again

I used QDockWidget so the panels can be docked at either side of the reader or closed temporarily. The View menu includes toggle actions, so a closed panel can always be reopened.

Desktop search

The reader can search the entire document and move through matching pages. PyMuPDF returns page rectangles for each search match, which are useful because they are measured in PDF coordinates.

rectangles = page.search_for(query)
Enter fullscreen mode Exit fullscreen mode

When the app renders a page at a specific DPI, PDF rectangles must be converted to pixels:

scale = dpi / 72
pixel_x = pdf_x * scale
Enter fullscreen mode Exit fullscreen mode

This allows search highlights to remain aligned with the rendered text at different zoom levels.

Persistent annotations

The newest feature is a first annotation workflow.

Because the current reader renders pages as images, it does not yet provide arbitrary click-and-drag text selection. Instead, the user searches for a word or phrase, navigates to a search result, and saves that result as one of three annotation types:

  • Highlight
  • Underline
  • Strike-through

Each annotation stores page number, PDF-coordinate rectangles, annotation type, searched text, and timestamp.

{
  "page_number": 4,
  "rectangles": [
    [80.0, 120.0, 240.0, 138.0]
  ],
  "type": "highlight",
  "text": "recognition over recall",
  "created_at": "2026-08-14 21:00"
}
Enter fullscreen mode Exit fullscreen mode

Storing rectangles in PDF coordinates is important. Screen pixels change when DPI changes, but PDF coordinates remain stable. Every time a page is rendered, the reader scales the saved rectangles using the current DPI.

scale = dpi / 72

left = x0 * scale
top = y0 * scale
right = x1 * scale
bottom = y1 * scale
Enter fullscreen mode Exit fullscreen mode

Highlights use a semi-transparent yellow fill, underlines use a blue line near the bottom of the match, and strike-through annotations use a red line through the middle of the match.

The Annotations panel lists saved annotations, opens their associated page on double-click, and lets the user delete a selected annotation.

Why annotations are stored outside the PDF

For this stage, annotations are saved in data/reader_data.json with bookmarks, notes, and reading progress.

This is intentional:

  • A paper may be read-only or signed.
  • The original PDF should remain unchanged.
  • Annotations can be deleted or edited later.
  • The project can eventually offer an explicit Export Annotated PDF action that writes to a separate copy.

The reader data now has this structure:

{
  "documents": {
    "/absolute/path/to/paper.pdf": {
      "file_name": "paper.pdf",
      "bookmarks": [],
      "notes": [],
      "annotations": [],
      "last_page": 0
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Keyboard shortcuts

To improve reading productivity, I added shortcuts for common actions:

Shortcut Action
Ctrl + O Open PDF
Ctrl + Q Exit reader
Left Arrow Previous page
Right Arrow Next page
Ctrl + F Focus search
Ctrl + B Add bookmark
Ctrl + Plus Zoom in
Ctrl + Minus Zoom out

The goal is not only to add features, but also to reduce unnecessary mouse movement while reading research papers.

Testing and version control

The JSON storage layer has automated pytest coverage. The suite checks empty storage, saving and loading data, default document data, bookmark/note/progress persistence, and valid JSON output.

5 passed in 0.02s
Enter fullscreen mode Exit fullscreen mode

I also learned to be selective when staging files. The repository ignores virtual environments, temporary Python files, local PDFs, and personal reader data:

__pycache__/
*.py[cod]
.pytest_cache/
.venv/
.ipynb_checkpoints/
data/
*.pdf
Enter fullscreen mode Exit fullscreen mode

Each tested milestone is committed separately, making it easier to return to a working version if a future feature introduces a problem.

What I learned in this milestone

This desktop stage taught me several practical lessons:

  • A notebook prototype and a desktop application can share core services while using completely different interfaces.
  • A QScrollArea is essential for readable zoomed document pages.
  • Fit-to-window and manual zoom are different modes and need clear behavior.
  • Dockable panels are a useful pattern for reader tools such as bookmarks, notes, and annotations.
  • A panel closed with its X button needs a View-menu toggle action so the user can restore it.
  • PDF coordinates and image pixels must be converted carefully to keep search and annotations aligned.
  • Local JSON storage is still suitable for a small personal reader, but SQLite will become useful as annotations and document collections grow.
  • A simple, reliable annotation workflow is better than attempting complex text selection too early.

Next steps

The basic reader is much closer to being ready for AI features. Before integrating an LLM, I still want to improve a few core areas:

  • Add an explicit clear-search button.
  • Add edit support for bookmarks and notes.
  • Add a recent-documents list.
  • Improve annotation text selection beyond search results.
  • Add export to a separate annotated PDF copy.
  • Add tests for PDF rendering and storage with annotations.

After these basics are stable, the next major phase will be LLM-assisted reading:

  • Paper and chapter summaries
  • Questions and answers over document text
  • Page-number citations for answers
  • Cached local summaries
  • Privacy controls for local versus cloud models

Closing thoughts

The project started with a single rendered page in a notebook. It now has a desktop reader with persistent research tools and a growing codebase that is modular, tested, and version controlled.

The biggest lesson has been to build the reading workflow first. An LLM can be useful, but it becomes far more useful when the application already knows which document is open, which page the user is reading, what they bookmarked, what they noted, and what they annotated.

The next phase is AI—but the reader now has a foundation worth building AI on.

Top comments (0)