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.
- 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 |
What's New in v1.1.0
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_pagesis 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_pagesdeprecated - 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
- 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
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 |
from pdfjs_viewer import PDFViewerWidget, PDFViewerConfig, PrintHandler
# SYSTEM mode (default) - Open PDF in OS default application
config = PDFViewerConfig(print_handler=PrintHandler.SYSTEM)
viewer = PDFViewerWidget(config=config)
# QT_DIALOG mode - a basic print dialog
config = PDFViewerConfig(
print_handler=PrintHandler.QT_DIALOG,
print_dpi=300,
print_fit_to_page=True
)
viewer = PDFViewerWidget(config=config)
# EMIT_SIGNAL mode (custom handling)
config = PDFViewerConfig(print_handler=PrintHandler.EMIT_SIGNAL)
viewer = PDFViewerWidget(config=config)
viewer.print_data_ready.connect(my_custom_print_handler)
from pdfjs_viewer import PDFViewerWidget, PDFViewerConfig, PrintHandler
# SYSTEM mode (default) - Open PDF in OS default application
config = PDFViewerConfig(print_handler=PrintHandler.SYSTEM)
viewer = PDFViewerWidget(config=config)
# QT_DIALOG mode - a basic print dialog
config = PDFViewerConfig(
print_handler=PrintHandler.QT_DIALOG,
print_dpi=300,
print_fit_to_page=True
)
viewer = PDFViewerWidget(config=config)
# EMIT_SIGNAL mode (custom handling)
config = PDFViewerConfig(print_handler=PrintHandler.EMIT_SIGNAL)
viewer = PDFViewerWidget(config=config)
viewer.print_data_ready.connect(my_custom_print_handler)
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:
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:
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) |
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.
External link handling involves two settings that work together:
allow_external_links(PDFSecurityConfig) - The "master switch"False(default): All external link clicks are blocked silentlyTrue: External links can be opened (subject to confirmation)
confirm_before_external_link(PDFSecurityConfig) - Confirmation promptTrue(default): Shows a dialog asking user to confirmFalse: 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 |
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.
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
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.