Introduction What's New Features Installation Quick Start API Reference Configuration License

PDF.js Viewer for Qt (PySide6 / PyQt6)

Production-ready, embeddable PDF viewer widgets for PySide6 and PyQt6 applications, powered by Mozilla's PDF.js with basic annotation support.

Introduction

The PDF.js Viewer packages provides extensive PDF viewing and basic annotation capabilities for Qt-based Python applications. Available in two variants—pdfjs-viewer-pyside6 (LGPL) and pdfjs-viewer-pyqt6 (GPL)—these packages offer identical APIs with different licensing to accommodate both open-source and proprietary use cases.

The packages are lightweight wrappers around the PDF.js reference implementation. With extensive configuration options, individual features of the viewer can be enabled or disabled according to your needs. In addition, the packages provide a connection between the viewer and Qt, allowing it to be controlled through function calls. They also expose signals that enable the main application to react to events occurring within the viewer.

The quick‑start guide on this page is supplemented by comprehensive example implementations, which can be found in the examples directories of the GitHub repositories for pdfjs-viewer-pyside6 and pdfjs-viewer-pyqt6.

Visit my Youtube Channel to explore additional open‑source projects and gain insights into ongoing developments. The channel offers a steady look at practical implementations, background details, and new ideas that may support your own work.

If these packages save you time and effort and you’d like to support / sponsor my projects, 🍕 buy me a pizza (or one every month…) — every bite helps fuel more code.

Key Benefits:
  • Full-featured PDF viewing with zoom, rotation, and navigation
  • Basic annotation tools (highlight, text, ink, stamps)
  • Save PDFs with annotations
  • Flexible print request handling with multiple modes
  • Automatic light/dark mode support
  • PyInstaller-ready for distribution
  • 100% API compatibility between PySide6 and PyQt6 versions

Which Package Should You Use?

Package Qt Binding License Proprietary Use
pdfjs-viewer-pyside6 PySide6 LGPL v3 ✅ Yes
pdfjs-viewer-pyqt6 PyQt6 GPL v3 ❌ No
Licensing Note: If you're building a proprietary application, use pdfjs-viewer-pyside6. The PyQt6 version (GPL) requires your application to be open-source under GPL v3.

What's New in v1.1.0

Version 1.1.0 Release - Major feature update with improved stability, unsaved changes protection, and memory leak fixes.

Unsaved Changes Protection

New unsaved_changes_action configuration with three modes:

  • "disabled" - No protection (default)
  • "prompt" - Show save/discard dialog
  • "auto_save" - Auto-save on close

Plus new has_unsaved_changes() and handle_unsaved_changes() methods.

Sequential Printing (Memory Fix)

Print processing is now sequential instead of parallel to eliminate memory leaks when printing large documents.

  • print_parallel_pages is deprecated
  • Stable memory usage for all document sizes
  • Improved reliability for QT_DIALOG mode

Global Stability Configuration

New configure_global_stability() function for application-wide Chromium settings.

  • Must be called before QApplication
  • Configures GPU, WebGL, cache settings
  • Maximum stability for embedded systems

Python 3.10+ Required

Updated Python version requirements:

  • Minimum: Python 3.10
  • Added support for Python 3.13
  • Removed: Python 3.8, 3.9

Breaking Changes

  • Python 3.8 and 3.9 no longer supported - Upgrade to Python 3.10+
  • print_parallel_pages deprecated - Setting is ignored; printing is always sequential

Features

🖼️ PDF.js Integration

View, zoom, rotate, and navigate PDFs with reference implementation's capabilities.

✏️ Rich Annotations

Highlight, draw, add text and stamps to PDFs.

💾 Save with Annotations

Export PDFs with annotations.

🖨️ Flexible Printing

Three print modes: system viewer, Qt dialog for printing, or custom handling.

🎨 Theme Support

Automatic light/dark mode following system preferences.

⚙️ Viewer Options

Control page, zoom, and sidebar when loading PDFs.

🔒 Security

Basic checks added, unused features disabled, suppress external links.

📦 PyInstaller Ready

Automatic bundling for frozen applications. Call pdfjs_viewer.freeze_support() before QApplication when using QT_DIALOG.

🎛️ Feature Control

Enable/disable specific UI features via configuration.

🔧 7 Configuration Presets

Pre-configured settings for common use cases.

🌐 Cross-Platform

Works on Windows, macOS, and Linux.

📡 Signal-Based API

Qt signals for all major events and state changes.

Installation

PySide6 Version (LGPL)

pip install pdfjs-viewer-pyside6

PyQt6 Version (GPL)

pip install pdfjs-viewer-pyqt6
🔧 Requirements (all installed automatically)
  • Python ≥ 3.10
  • PySide6 ≥ 6.10.0 or PyQt6 ≥ 6.10.0
  • PySide6-WebEngine ≥ 6.10.0 or PyQt6-WebEngine ≥ 6.9.0
  • pypdfium2 ≥ 4.0.0
  • Pillow ≥ 9.0.0
  • pikepdf ≥ 8.0.0
⚠️ Compatibility Issues / Bugs in QWebEngineView
This module uses QWebEngineView. Please note that bugs in this component can lead to critical errors that may cause the main application to crash. Before reporting an issue to us, please check whether downgrading or upgrading the component by one version resolves the problem, and consult our GitHub repository for notes on known versions with issues related to the integration of PDF.js - Also keep in mind that an issue in PySide6 does not necessarily occur in PyQt6 as well. The same applies to identical package versions on different operating systems. 🤷‍♂️

Quick Start

Basic Viewer (PySide6)

from PySide6.QtWidgets import QApplication, QMainWindow
from pdfjs_viewer import PDFViewerWidget

app = QApplication([])
window = QMainWindow()
window.resize(1024, 768)

# Create viewer
viewer = PDFViewerWidget()
viewer.load_pdf("document.pdf")
# or show blank viewer 
# viewer.show_blank_page()
# Note: PDF.js loads a demo file if you don't load a PDF or show a blank page!

# Connect signals
viewer.pdf_loaded.connect(lambda meta: print(f"Loaded: {meta['filename']}"))
viewer.pdf_saved.connect(lambda data, path: print(f"Saved to {path}"))

window.setCentralWidget(viewer)
window.show()
app.exec()

Basic Viewer (PyQt6)

from PyQt6.QtWidgets import QApplication, QMainWindow
from pdfjs_viewer import PDFViewerWidget

app = QApplication([])
window = QMainWindow()
window.resize(1024, 768)

# Create viewer
viewer = PDFViewerWidget()
viewer.load_pdf("document.pdf")
# or show blank viewer 
# viewer.show_blank_page()
# Note: PDF.js loads a demo file if you don't load a PDF or show a blank page!

# Connect signals
viewer.pdf_loaded.connect(lambda meta: print(f"Loaded: {meta['filename']}"))
viewer.pdf_saved.connect(lambda data, path: print(f"Saved to {path}"))

window.setCentralWidget(viewer)
window.show()
app.exec()

Viewer Options

Control how PDFs are displayed when loaded:

# Open at specific page with custom zoom
viewer.load_pdf("document.pdf", page=5, zoom="page-width")

# Open with bookmarks sidebar visible
viewer.load_pdf("document.pdf", pagemode="bookmarks")

# Combine multiple options
viewer.load_pdf(
    "document.pdf",
    page=10,
    zoom=150,          # 150% zoom
    pagemode="thumbs"  # Show thumbnails
)
# Open at specific page with custom zoom
viewer.load_pdf("document.pdf", page=5, zoom="page-width")

# Open with bookmarks sidebar visible
viewer.load_pdf("document.pdf", pagemode="bookmarks")

# Combine multiple options
viewer.load_pdf(
    "document.pdf",
    page=10,
    zoom=150,          # 150% zoom
    pagemode="thumbs"  # Show thumbnails
)

Supported options:

  • page: Page number to open (1-indexed)
  • zoom: Named ("page-width", "page-height", "page-fit", "auto") or numeric (10-1000)
  • pagemode: Sidebar state - "none", "thumbs", "bookmarks", "attachments"
  • nameddest: Named destination to navigate to

API Reference

PDFViewerWidget

Main widget class for viewing PDFs.

Methods

Method Description
load_pdf(source, page, zoom, pagemode, nameddest) Load PDF from file path with optional viewer options
load_pdf_bytes(pdf_data, filename, page, zoom, pagemode, nameddest) Load PDF from bytes with optional viewer options
show_blank_page() Show empty viewer (respects current theme)
save_pdf(output_path=None) Save PDF with annotations (returns bytes)
print_pdf() Trigger print dialog (behavior depends on print_handler config)
has_annotations() Check if PDF has been annotated (returns bool)
goto_page(page) Navigate to specific page
get_page_count() Get total page count (returns int)
get_current_page() Get current page number (returns int)
has_unsaved_changes() Check if PDF has unsaved annotation changes (returns bool)
handle_unsaved_changes() Handle unsaved changes according to config (returns bool)
set_pdfjs_path(path) Set custom PDF.js path and reload viewer
get_pdfjs_version() Get bundled PDF.js version string

Signals

All signals developers can listen to:

Signal Parameters Description
pdf_loaded metadata: dict Emitted when PDF successfully loads. Metadata includes filename, page count, and PDF information.
pdf_saved data: bytes, path: str Emitted when PDF is saved. Provides PDF data with annotations and save path.
print_requested data: bytes Emitted when print is triggered using SYSTEM or QT_DIALOG handlers.
print_data_ready data: bytes, filename: str Emitted when using EMIT_SIGNAL print handler for custom print handling.
annotation_modified none Emitted when annotations are added, changed, or removed.
page_changed current: int, total: int Emitted when current page changes. Provides page number and total count.
error_occurred message: str Emitted when errors occur during PDF operations.
external_link_blocked url: str Emitted when an external link is blocked by security settings.

Configuration

Print Handling

The viewer supports three print handling modes:

Mode Signal Use Case
SYSTEM print_requested(bytes) OS-based viewing / printing (default)
QT_DIALOG print_requested(bytes) Embedded Qt print dialog
EMIT_SIGNAL print_data_ready(bytes, str) Custom print handling
📦 PyInstaller Note: When using QT_DIALOG in a PyInstaller-frozen application, call pdfjs_viewer.freeze_support() at the very beginning of your entry point, before creating QApplication:
import pdfjs_viewer

def main():
    pdfjs_viewer.freeze_support()   # required for QT_DIALOG in frozen builds
    app = QApplication(sys.argv)
    ...
This is not needed for SYSTEM or EMIT_SIGNAL handlers, and is a safe no-op in non-frozen environments.

Unsaved Changes Handling

Protect users from accidentally losing annotation work with the unsaved_changes_action configuration:

Mode Behavior Use Case
"disabled" No protection (default) Read-only viewers, trusted environments
"prompt" Shows Save/Discard dialog on close Production applications, form filling
"auto_save" Automatically saves on close Kiosk mode, auto-backup scenarios
from pdfjs_viewer import PDFViewerWidget, ConfigPresets

# Enable unsaved changes protection
config = ConfigPresets.annotation()
config.features.unsaved_changes_action = "prompt"
viewer = PDFViewerWidget(config=config)

# Check programmatically before closing
if viewer.has_unsaved_changes():
    viewer.handle_unsaved_changes()  # Shows save/discard dialog

# Or connect to the annotation_modified signal
viewer.annotation_modified.connect(
    lambda: print("Annotations modified")
)
from pdfjs_viewer import PDFViewerWidget, ConfigPresets

# Enable unsaved changes protection
config = ConfigPresets.annotation()
config.features.unsaved_changes_action = "prompt"
viewer = PDFViewerWidget(config=config)

# Check programmatically before closing
if viewer.has_unsaved_changes():
    viewer.handle_unsaved_changes()  # Shows save/discard dialog

# Or connect to the annotation_modified signal
viewer.annotation_modified.connect(
    lambda: print("Annotations modified")
)

Global Stability Configuration

For maximum stability in embedded systems or crash-prone environments, apply Chromium flags before creating QApplication:

Important: configure_global_stability() must be called before creating QApplication. These settings affect all QWebEngine instances in your application.
import sys
from pdfjs_viewer.stability import configure_global_stability

# Apply stability flags (call BEFORE QApplication!)
configure_global_stability(
    disable_gpu=True,
    disable_webgl=True,
    disable_gpu_compositing=True,
    disable_unnecessary_features=True,
)

# Now create your application
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)

from pdfjs_viewer import PDFViewerWidget
viewer = PDFViewerWidget()
import sys
from pdfjs_viewer.stability import configure_global_stability

# Apply stability flags (call BEFORE QApplication!)
configure_global_stability(
    disable_gpu=True,
    disable_webgl=True,
    disable_gpu_compositing=True,
    disable_unnecessary_features=True,
)

# Now create your application
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)

from pdfjs_viewer import PDFViewerWidget
viewer = PDFViewerWidget()

This sets environment variables that Chromium reads at startup, providing stability improvements at the application level. Safe per-widget defaults (isolated profile, no cache, no WebGL) are always applied automatically.

Configuration Presets

7 pre-configured presets for common use cases:

Preset (click for details) Features Best For
readonly No printing, saving, or annotations Kiosk displays, untrusted PDFs
simple Print, save, basic annotations General PDF viewing
annotation All annotation tools enabled (default) PDF review, collaboration
form Text input, signatures Form filling, contracts
kiosk Print only, maximum stability 24/7 public terminals
safer Minimal features, maximum reliability Embedded systems, older Qt
unrestricted Everything enabled Development, testing
from pdfjs_viewer import PDFViewerWidget, ConfigPresets, PrintHandler

# Simple preset usage
viewer = PDFViewerWidget(preset="readonly")

# Customize a preset
viewer = PDFViewerWidget(
    preset="readonly",
    customize={
        "features": {
            "save_enabled": True,
            "freetext_enabled": True,
            "ink_enabled": True,
        },
        "security": {
            "allow_external_links": True,
            "block_remote_content": False,
        },
    }
)

# Hybrid approach (recommended)
config = ConfigPresets.annotation()
config.print_handler = PrintHandler.EMIT_SIGNAL
config.features.stamp_enabled = False
viewer = PDFViewerWidget(config=config)
from pdfjs_viewer import PDFViewerWidget, ConfigPresets, PrintHandler

# Simple preset usage
viewer = PDFViewerWidget(preset="readonly")

# Customize a preset
viewer = PDFViewerWidget(
    preset="readonly",
    customize={
        "features": {
            "save_enabled": True,
            "freetext_enabled": True,
            "ink_enabled": True,
        },
        "security": {
            "allow_external_links": True,
            "block_remote_content": False,
        },
    }
)

# Hybrid approach (recommended)
config = ConfigPresets.annotation()
config.print_handler = PrintHandler.EMIT_SIGNAL
config.features.stamp_enabled = False
viewer = PDFViewerWidget(config=config)

All Configurable Options

Complete reference of all configuration options available:

Understanding Default Values
The default values shown below are the class defaults used when you instantiate these classes directly. When using PDFViewerWidget() without explicit configuration, the annotation preset is applied instead—see the Configuration Presets section for the actual default values.

Recommendation: For most use cases, use the hybrid approach documented under Configuration Presets. Start with a preset and modify only the settings you need. Working with these configuration classes directly is mainly useful for development or special cases.
Click to expand all configurable options

PDFFeatures (features)

Option Type Default Description
print_enabled bool True Enable print button in toolbar
save_enabled bool True Enable save button in toolbar
load_enabled bool True Enable load PDF button in toolbar
presentation_mode bool False Enable fullscreen presentation mode button (not working in Qt)
highlight_enabled bool True Enable highlight annotation tool
freetext_enabled bool True Enable text annotation tool
ink_enabled bool True Enable drawing/ink annotation tool
stamp_enabled bool True Enable stamp annotation tool
stamp_alttext_enabled bool True Enable alt-text dialog for stamps
bookmark_enabled bool False Show URL of loaded document button (not working in Qt)
scroll_mode_buttons bool True Show scroll mode buttons (vertical/horizontal/wrapped)
spread_mode_buttons bool True Show spread mode buttons (single/two-page)
unsaved_changes_action str "disabled" Action on close with unsaved changes: "disabled", "prompt", or "auto_save"

PDFSecurityConfig (security)

Option Type Default Description
allow_external_links bool False Allow clicking external links in PDFs
confirm_before_external_link bool True Show confirmation dialog before opening external links
block_remote_content bool True Block loading remote images and resources
allowed_protocols list[str] ["http", "https"] Allowed URL protocols for links
custom_csp str None Custom Content Security Policy (replaces default CSP entirely)
Custom Content Security Policy (CSP)
The custom_csp option allows you to provide a complete custom Content Security Policy that replaces the default CSP. This is useful when:
  • You need to allow specific external resources (fonts, images, scripts)
  • You're embedding content that requires special permissions
  • You need to integrate with specific trusted domains

Example:

config = PDFViewerConfig(
    security=PDFSecurityConfig(
        custom_csp="default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https://trusted.com"
    )
)

Warning: Setting a custom CSP completely overrides the default security policy. Ensure your CSP is properly configured to avoid security vulnerabilities.

Understanding External Link Behavior
External link handling involves two settings that work together:
  • allow_external_links (PDFSecurityConfig) - The "master switch"
    • False (default): All external link clicks are blocked silently
    • True: External links can be opened (subject to confirmation)
  • confirm_before_external_link (PDFSecurityConfig) - Confirmation prompt
    • True (default): Shows a dialog asking user to confirm
    • False: Opens external links directly without confirmation
allow_external_links confirm_before_external_link Result
False (any) Links blocked silently
True True Confirmation dialog shown
True False Links open immediately

Custom handling: Connect to the external_link_blocked(str) signal to react when external links are blocked.

PDFViewerConfig (main)

Option Type Default Description
auto_open_folder_on_save bool True Open folder after saving PDF
disable_context_menu bool True Disable Qt's native context menu
print_handler PrintHandler SYSTEM Print mode: SYSTEM, QT_DIALOG, or EMIT_SIGNAL
print_dpi int 300 DPI for Qt print dialog rendering
print_fit_to_page bool True Scale to fit page vs actual size
print_parallel_pages int 1 Deprecated v1.1.0 - Ignored. Printing is now sequential to prevent memory leaks.
default_zoom str "auto" Default zoom: "auto", "page-fit", "page-width", or percentage
sidebar_visible bool False Show sidebar by default
spread_mode str "none" Page spread mode: "none", "odd", "even"

License & Dependencies

Package Licensing

Package License Qt Binding Qt Binding License Proprietary Use
pdfjs-viewer-pyside6 LGPL v3 PySide6 LGPL v3 ✅ Yes
pdfjs-viewer-pyqt6 GPL v3 PyQt6 GPL v3 ❌ No
PySide6 (LGPL) Notice:
This module uses PySide6 (Qt for Python), licensed under the LGPL v3.
You may replace the PySide6 and Qt shared libraries in the application directory.
PyQt6 (GPL) Notice:
This module uses PyQt6, licensed under the GPL v3.
Applications using this module must be licensed under GPL v3 or compatible license and provide source code to users.

Runtime Dependencies

Component Version License Used For Repository
PDF.js (bundled) 5.4.530 Apache 2.0 PDF rendering and viewer UI github.com/mozilla/pdf.js
PySide6 ≥ 6.10.0 LGPL v3 Qt bindings (PySide6 package) code.qt.io/pyside
PyQt6 ≥ 6.10.0 GPL v3 Qt bindings (PyQt6 package) github.com/PyQt

Print Dependencies

These packages are installed automatically and used for the PrintHandler.QT_DIALOG print mode:

Component Version License Used For Repository
pypdfium2 ≥ 4.0.0 BSD-3-Clause / Apache 2.0 PDF page rendering github.com/pypdfium2
Pillow ≥ 9.0.0 MIT-CMU (PIL) Image processing for printing github.com/python-pillow
pikepdf ≥ 8.0.0 MPL 2.0 PDF page extraction for print ranges github.com/pikepdf

These dependencies are included in the default installation — no extra install step is needed.

License Comparison: PySide6 vs PyQt6

PySide6 (LGPL):

  • ✅ Can be used in proprietary applications
  • ✅ No need to open-source your application
  • ⚠️ Must keep Qt libraries as external shared libraries (not statically linked)
  • ⚠️ Users must be able to replace PySide6/Qt libraries

PyQt6 (GPL):

  • ⚠️ Must license your application under GPL v3
  • ⚠️ Must provide source code to users
  • ⚠️ Must include GPL v3 license and copyright notices
  • ✅ No library replacement requirements
Legal Disclaimer:
This license information is provided to the best of our knowledge but is NOT legal advice. The information may be incomplete or incorrect. If you are unsure which license to use for your project, please consult with a professional lawyer or licensing expert who can provide guidance specific to your situation.

Additional Resources