Building a Custom AI PDF Reader in Python: From a Jupyter Prototype to Tested Modules
Research papers are much easier to read when the reader fits the way you work. I wanted a PDF reader that could eventually support bookmarks, notes, annotations, summaries, question answering, text-to-speech, and voice commands. Rather than trying to write a complete PDF engine from scratch, I started with a small Python prototype. The goal was simple: learn each layer of the application properly, build a working foundation, and only then move toward a desktop application. This article documents the first stage of that journey: an interactive PDF reader in Jupyter, local persistence for bookmarks and notes, a cleaner module structure, automated tests, and the lessons learned along the way. The project goal The long-term goal is a customizable desktop PDF reader for research reading. The eventual application may include: PDF navigation and zoom Text search with highlighted matches Bookmarks and reading progress Page-linked notes and annotations Local or cloud LLM summaries and question answering Text-to-speech Voice commands For the first milestone, I deliberately kept the scope smaller. I focused on PDF rendering, navigation, search, persistence, and tests. Choosing the first technology stack I chose Python because it allowed me to experiment quickly. My first stack was: Need Tool PDF rendering, text extraction, and search PyMuPDF Interactive prototype interface JupyterLab and ipywidgets Image handling Pillow Local saved data JSON Version control Git and GitHub Automated tests pytest The future desktop UI will use PySide6, but Jupyter was a useful place to learn the reader logic before dealing with desktop-window layouts, signals, menus, and packaging. Building the first reader The prototype opens a local PDF with PyMuPDF: from pathlib import Path import pymupdf PDF_PATH = Path("test.pdf") document = pymupdf.open(PDF_PATH) print(document.page_count) A PDF page is not automatically an image. PyMuPDF renders a page into a pixmap, and Pillow converts the pixel data into an image that Jupyter can display. from PIL import Image page = document[0] pixmap = page.get_pixmap(dpi=120) image = Image.frombytes( "RGB", (pixmap.width, pixmap.height), pixmap.samples ) image This gave me the basic page view. From there, I added buttons for Previous, Next, Go to page, Zoom In, and Zoom Out. A key lesson: application state One important concept I learned was application state. Instead of letting each button manage unrelated variables, I kept the reader's current information together: reader_state = { "current_page": 0, "zoom_dpi": 120, "search_results": [], "search_index": 0, "bookmarks": [], "notes": [] } The UI follows a simple pattern: A button changes reader_state. The application saves important changes if necessary. A refresh function renders the current state. For example, page navigation uses one shared function: def change_page(new_page): if not 0
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to