PDF.js-based PDF viewer widget for PyQt6 with annotation support
Project description
PDF.js Viewer Widget for PyQt6
A production-ready, embeddable PDF viewer widget for PyQt6 applications powered by Mozilla's PDF.js.
Visit PDF.js Viewer for Qt homepage for more information.
Support the project: Buy me a pizza! 👍
Features
- 🖼️ PDF.js Integration - View, zoom, rotate, and navigate PDFs
- ✏️ Annotations - Highlight, draw, add text, stamps
- 💾 Save with Annotations - Export PDFs with baked-in annotations
- 🖨️ Print Support - Multiple print handlers (system, Qt dialog, custom)
- 🎨 Theme Support - Automatic light/dark mode following system preferences
- 📄 Blank Page Support - Show empty viewer with
show_blank_page() - ⚙️ Viewer Options - Control page, zoom, and sidebar when loading PDFs
- 🔒 Security - Suppress external links
- 🎛️ Feature Control - Enable/disable specific UI features
- 💾 Unsaved Changes Protection - Prompt, auto-save, or ignore unsaved annotations
- 🌍 Localization - 21+ languages for print and save dialogs
- 📦 PyInstaller Ready - Automatic bundling for frozen applications
- 🔧 Customizable - Use custom PDF.js versions
- 🌐 Cross-Platform - Works on Windows, macOS, and Linux
Installation
pip install pdfjs-viewer-pyqt6
Quick Start
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 page
# viewer.show_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
)
Supported Options:
page: Page number to open (1-indexed)zoom: Zoom level - named ("page-width","page-height","page-fit","auto") or numeric (10-1000)pagemode: Sidebar state -"none","thumbs","bookmarks", or"attachments"nameddest: Named destination to navigate to
Theme Support
The viewer automatically follows system theme preferences for light and dark mode.
Theme Features
- Automatic Detection: Follows system/application theme via CSS
prefers-color-scheme - Built-in Dark Mode: PDF.js includes native dark mode styling that activates automatically
- No Configuration Needed: Works out of the box
Blank Page Display
Show an empty viewer without a PDF loaded:
viewer.show_blank_page()
The blank page automatically follows the system theme.
Configuration
Disable Features
from pdfjs_viewer import PDFViewerWidget, PDFViewerConfig, PDFFeatures
features = PDFFeatures(
print_enabled=True,
save_enabled=True,
load_enabled=False, # Disable file loading
presentation_mode=False, # Disable presentation mode button
stamp_enabled=False, # Disable stamp annotations
)
config = PDFViewerConfig(features=features)
viewer = PDFViewerWidget(config=config)
Security Settings
from pdfjs_viewer import PDFSecurityConfig
security = PDFSecurityConfig(
allow_external_links=False, # Block external URLs
block_remote_content=True, # Block remote resources
)
config = PDFViewerConfig(security=security)
viewer = PDFViewerWidget(config=config)
Custom PDF.js
You can initialize the widget with a customized version of PDF.js
viewer = PDFViewerWidget(
pdfjs_path="/path/to/custom/pdfjs-5.5.0-dist"
)
This "should" work fine for minor releases or cosmetic changes and will most likely break for major releases.
API Reference
PDFViewerWidget
Main widget class for viewing PDFs.
Methods
load_pdf(source: str | Path | bytes)- Load PDF from file path or bytesshow_blank_page()- Show empty viewer (respects current theme)save_pdf(output_path: str = None) -> bytes- Save PDF with annotationsprint_pdf()- Trigger print dialogget_pdf_data() -> bytes- Get current PDF data with annotationshas_annotations() -> bool- Check if PDF has been annotatedhas_unsaved_changes() -> bool- Check if document has unsaved annotationshandle_unsaved_changes() -> bool- Handle unsaved changes per configgoto_page(page: int)- Navigate to specific pageget_page_count() -> int- Get total page countget_current_page() -> int- Get current page numberset_features_enabled(features: PDFFeatures)- Update feature flags
Signals
All signals developers can listen to:
pdf_loaded(metadata: dict)- Emitted when PDF successfully loads. Metadata includes filename, page count, and other PDF information.pdf_saved(data: bytes, path: str)- Emitted when PDF is saved. Provides PDF data with baked-in annotations and the save path.print_requested(data: bytes)- Emitted when print is triggered using SYSTEM or QT_DIALOG handlers. Contains PDF data ready for printing.print_data_ready(data: bytes, filename: str)- Emitted when using EMIT_SIGNAL print handler. Provides PDF data and original filename for custom print handling.annotation_modified()- Emitted when annotations are added, changed, or removed.page_changed(current: int, total: int)- Emitted when current page changes. Provides current page number and total page 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 Classes
Note: 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 Configuration Presets 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.
PDFFeatures
Controls which UI features are enabled.
PDFFeatures(
# Core actions
print_enabled: bool = True,
save_enabled: bool = True,
load_enabled: bool = True,
presentation_mode: bool = False,
# Annotation tools
highlight_enabled: bool = True,
freetext_enabled: bool = True,
ink_enabled: bool = True,
stamp_enabled: bool = True,
stamp_alttext_enabled: bool = True, # Enable alt-text dialog for stamps
# Navigation
bookmark_enabled: bool = False,
scroll_mode_buttons: bool = True,
spread_mode_buttons: bool = True,
# Unsaved changes behavior
unsaved_changes_action: str = "disabled", # "disabled", "prompt", "auto_save"
)
PDFSecurityConfig
Security and privacy settings.
PDFSecurityConfig(
allow_external_links: bool = False,
confirm_before_external_link: bool = True, # Show confirmation dialog before opening
block_remote_content: bool = True,
allowed_protocols: List[str] = ["http", "https"],
custom_csp: str = None, # Optional custom Content Security Policy
)
PDFViewerConfig
Main configuration container.
PDFViewerConfig(
features: PDFFeatures = PDFFeatures(),
security: PDFSecurityConfig = PDFSecurityConfig(),
# Behavior
auto_open_folder_on_save: bool = True,
disable_context_menu: bool = True,
# Print handling
print_handler: PrintHandler = PrintHandler.SYSTEM,
print_dpi: int = 300,
print_fit_to_page: bool = True,
# PDF.js settings
default_zoom: str = "auto",
sidebar_visible: bool = False,
spread_mode: str = "none", # "none", "odd", "even"
)
Print Handling
The viewer supports multiple print handling modes to fit different application needs. Configure via the print_handler setting in PDFViewerConfig.
Print Handler Modes
SYSTEM (Default)
Opens PDF with system default viewer for printing. Simple and reliable.
from pdfjs_viewer import PDFViewerWidget, PDFViewerConfig, PrintHandler
config = PDFViewerConfig(print_handler=PrintHandler.SYSTEM)
viewer = PDFViewerWidget(config=config)
# Listen to print_requested signal
viewer.print_requested.connect(lambda data: print(f"Opening {len(data)} bytes in system viewer"))
Best for:
- Simple applications
- Delegating to OS print dialog
- Maximum compatibility
QT_DIALOG
Uses a basic Qt print dialog with pypdfium2 for PDF rendering. Requires pypdfium2 package.
config = PDFViewerConfig(
print_handler=PrintHandler.QT_DIALOG,
print_dpi=300, # DPI for rendering (default: 300)
print_fit_to_page=True # Scale to fit page (default: True)
)
viewer = PDFViewerWidget(config=config)
# Listen to print_requested signal
viewer.print_requested.connect(lambda data: print("Qt print dialog opening"))
Best for:
- Embedded printing without external apps
- Custom print settings UI
- Direct printer access
Requirements:
pip install pypdfium2
EMIT_SIGNAL
Emits signal with PDF data for completely custom handling. No built-in printing.
config = PDFViewerConfig(print_handler=PrintHandler.EMIT_SIGNAL)
viewer = PDFViewerWidget(config=config)
# Listen to print_data_ready signal (NOT print_requested)
def handle_print(pdf_data: bytes, filename: str):
print(f"Custom print handling for {filename}")
# Send to print server, save to queue, etc.
viewer.print_data_ready.connect(handle_print)
Best for:
- Server-side printing
- Print queue systems
- Custom print workflows
- Cloud printing services
Print Handler Comparison
| Mode | Signal | Requires pypdfium2 | Use Case |
|---|---|---|---|
| SYSTEM | print_requested(bytes) |
No | Simple, OS-based printing |
| QT_DIALOG | print_requested(bytes) |
Yes | Embedded Qt print dialog |
| EMIT_SIGNAL | print_data_ready(bytes, str) |
No | Custom print handling |
Print Configuration Options
Additional settings available for QT_DIALOG mode:
config = PDFViewerConfig(
print_handler=PrintHandler.QT_DIALOG,
print_dpi=300, # Rendering DPI (default: 300)
print_fit_to_page=True # Scale to fit vs actual size (default: True)
)
Configuration Presets
The package provides 7 pre-configured presets for common use cases. You can use them as-is or customize them for your specific needs.
Available Presets
from pdfjs_viewer import PDFViewerWidget, ConfigPresets
# List all available presets
print(ConfigPresets.list())
# ['readonly', 'simple', 'annotation', 'form', 'kiosk', 'safer', 'unrestricted']
1. readonly - View-Only Mode
Maximum security, no editing capabilities.
viewer = PDFViewerWidget(preset="readonly")
Features: No printing, no saving, no annotations, no external links Best for: Kiosk displays, untrusted PDFs, embedded viewing
2. simple - Basic Viewer
Standard PDF viewing with print/save.
viewer = PDFViewerWidget(preset="simple")
Features: Print, save, basic annotations (highlight, text) Best for: General PDF viewing, most common use case
3. annotation - Full Editing (Default)
All annotation and editing tools enabled. This is the default preset when no configuration is specified.
viewer = PDFViewerWidget(preset="annotation")
# or simply
viewer = PDFViewerWidget() # annotation is the default
Features: All annotation tools, file loading, external links Best for: PDF review, collaborative annotation, document workflows
4. form - Form Filling
Optimized for PDF form completion.
viewer = PDFViewerWidget(preset="form")
Features: Text input, signatures, no external links Best for: Government forms, insurance applications, contract signing
5. kiosk - Public Terminal
For 24/7 public displays.
viewer = PDFViewerWidget(preset="kiosk")
Features: Print only, no saving/editing, maximum stability Best for: Libraries, museums, public information terminals
6. safer - Maximum Stability
For crash-prone or embedded systems.
viewer = PDFViewerWidget(preset="safer")
Features: Minimal features, all stability options enabled, basic viewing Best for: Embedded Linux, older Qt versions, mission-critical apps
7. unrestricted - Full PDF.js
No restrictions, all features enabled.
viewer = PDFViewerWidget(preset="unrestricted")
Features: Everything enabled, developer-friendly Best for: Development, testing, fully trusted PDFs
Customizing Presets
There are three ways to customize presets to fine-tune behavior for your application:
Method 1: Simple Customization (Quick Override)
Use the customize parameter for simple property changes:
# Start with readonly, but enable saving
viewer = PDFViewerWidget(
preset="readonly",
customize={
"features": {"save_enabled": True}
}
)
# Start with simple, but use Qt print dialog
viewer = PDFViewerWidget(
preset="simple",
customize={
"print_handler": PrintHandler.QT_DIALOG,
"features": {"ink_enabled": True}
}
)
Method 2: Hybrid Approach (Recommended)
Get preset config, modify it, then pass to widget:
from pdfjs_viewer import PDFViewerWidget, ConfigPresets, PrintHandler
# Start with annotation preset
config = ConfigPresets.annotation()
# Fine-tune specific settings
config.print_handler = PrintHandler.EMIT_SIGNAL
config.features.stamp_enabled = False
config.security.allow_external_links = False
# Create viewer with customized config
viewer = PDFViewerWidget(config=config)
This approach gives you:
- IDE autocomplete for settings
- Type checking
- Clear, readable code
- Full control over configuration
Method 3: Custom Preset Builder
Use ConfigPresets.custom() for complex customizations:
from pdfjs_viewer import ConfigPresets, PrintHandler
config = ConfigPresets.custom(
base="simple",
features={
"ink_enabled": True,
"stamp_enabled": True,
},
security={
"allow_external_links": False,
},
print_handler=PrintHandler.QT_DIALOG,
print_dpi=600,
)
viewer = PDFViewerWidget(config=config)
Fine-Tuning Examples
Example 1: Readonly + Save Only
Allow users to view and save PDFs, but not edit:
config = ConfigPresets.readonly()
config.features.save_enabled = True
viewer = PDFViewerWidget(config=config)
Example 2: Annotation with Custom Print
Full annotation tools but with custom print handling:
config = ConfigPresets.annotation()
config.print_handler = PrintHandler.EMIT_SIGNAL
viewer = PDFViewerWidget(config=config)
viewer.print_data_ready.connect(my_custom_print_handler)
Example 3: Custom Feature Mix
Cherry-pick features from different presets:
from pdfjs_viewer import PDFViewerConfig, PDFFeatures, PDFSecurityConfig
config = PDFViewerConfig(
features=PDFFeatures(
print_enabled=True,
save_enabled=True,
highlight_enabled=True,
ink_enabled=True,
stamp_enabled=False,
),
security=PDFSecurityConfig(
allow_external_links=True,
block_remote_content=True,
),
print_handler=PrintHandler.QT_DIALOG,
)
viewer = PDFViewerWidget(config=config)
Preset Configuration Reference
Each preset configures two main areas:
- Features (
PDFFeatures) - Which UI elements are enabled - Security (
PDFSecurityConfig) - Link and content policies
See Configuration Classes section above for full details on available settings.
Unsaved Changes Handling
The viewer can be configured to handle unsaved annotations when:
- Closing the viewer
- Loading a new PDF
- Navigating away from the current document
Configuration
Set the unsaved_changes_action in PDFFeatures:
from pdfjs_viewer import PDFViewerWidget, ConfigPresets
# Method 1: Using preset with customize
viewer = PDFViewerWidget(
preset="annotation",
customize={"features": {"unsaved_changes_action": "prompt"}}
)
# Method 2: Modify config directly
config = ConfigPresets.annotation()
config.features.unsaved_changes_action = "prompt"
viewer = PDFViewerWidget(config=config)
Available Modes
| Mode | Description | Behavior |
|---|---|---|
"disabled" |
No warning (default) | Changes may be lost without prompting |
"prompt" |
Show dialog | User chooses: Save As / Save / Discard |
"auto_save" |
Auto-save | Automatically saves to original file |
Dialog Options (prompt mode)
When unsaved_changes_action="prompt", a dialog appears with three options:
- Save As... - Opens file picker to choose save location
- Save - Overwrites the original PDF file with annotations
- Discard - Discards changes and continues without saving
Preset Defaults
| Preset | Default Action |
|---|---|
readonly |
disabled (no editing possible) |
simple |
prompt |
annotation |
prompt |
form |
prompt |
kiosk |
disabled (no user interaction) |
safer |
prompt |
unrestricted |
disabled (backwards compatible) |
Programmatic Checking
You can check and handle unsaved changes programmatically:
# Check if there are unsaved changes
if viewer.has_unsaved_changes():
print("Document has unsaved annotations")
# Handle unsaved changes according to config
# Returns True if safe to proceed, False if user cancelled Save As
if viewer.handle_unsaved_changes():
# Safe to close or navigate
pass
Translations
The dialog is translated in 21 languages, matching the print dialog translations.
Global Stability Settings
For maximum stability, configure WebEngine settings before creating QApplication:
from pdfjs_viewer.stability import configure_global_stability
# Call BEFORE QApplication creation
configure_global_stability(
disable_gpu=True,
disable_webgl=True,
disable_gpu_compositing=True,
disable_unnecessary_features=True,
)
# Then create your application
app = QApplication(sys.argv)
Utility Functions
validate_pdf_file
Check if a file is actually a PDF before loading:
from pdfjs_viewer import validate_pdf_file
if validate_pdf_file("/path/to/file.pdf"):
viewer.load_pdf("/path/to/file.pdf")
else:
print("Not a valid PDF file")
Examples
See examples/ directory for complete examples:
- basic_viewer.py - Simple PDF viewer
- feature_control.py - Static feature configuration
- feature_selection.py - Interactive feature demo
- print_handlers.py - All print handler modes
- unsaved_changes_demo.py - Unsaved changes handling
- external_link_handler.py - Link security handling
- viewer_options.py - Page, zoom, sidebar options
PyInstaller Support
PDF.js files are automatically bundled when freezing your application. A hook automatically includes all required PDF.js files and templates.
Directory structure (PyInstaller >= 5.0):
dist/your_app/
├── your_app.exe # Executable
└── _internal/ # All resources
└── pdfjs_viewer/ # Automatically included
Requirements
- Python >= 3.10
- PyQt6 >= 6.10.0
- PyQt6-WebEngine >= 6.9.0 (excluding 6.10.0, 6.10.1, 6.10.2 due to bugs)
- pypdfium2
- Pillow
- pikepdf
PDF.js Version
This package bundles PDF.js version 5.4.530 (Apache License 2.0).
License
This package is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later).
See LICENSE for the full license text.
PyQt6 GPL Notice
This module uses PyQt6, licensed under the GPL v3.
Important for application developers:
- Applications using this module must be licensed under GPL v3 or a compatible license
- You must provide source code to users of your application
- This is a copyleft license - derivative works must also be GPL
- See LICENSE_NOTICE.md for full compliance details
For proprietary applications: Use pdfjs-viewer-pyside6 instead, which is licensed under LGPL v3.
Bundled Dependencies
- PDF.js - Apache License 2.0 (see
src/pdfjs_viewer/pdfjs/LICENSE)
Contributing
Contributions are welcome! Please open an issue or pull request on GitHub.
Support
- Issues: GitHub Issues
- Documentation: Full API documentation available in source code
Changelog
v1.1.0 (2026-01-25)
New Features
- Unsaved Changes Protection: New
unsaved_changes_actionsetting with three modes:"disabled"- No prompts (default, backwards compatible)"prompt"- Dialog with Save As / Save / Discard options"auto_save"- Automatic save before navigation
- Global Stability Configuration: New
stability.configure_global_stability()for crash prevention - PDF Validation: New
validate_pdf_file()utility function - Stamp Alt-Text Control: New
stamp_alttext_enabledto disable alt-text dialog - View Mode Controls: New
scroll_mode_buttonsandspread_mode_buttonsfeatures - Bookmark Toggle: New
bookmark_enabledfeature flag - Separate Process Printing: Print dialog now runs in isolated process for stability
Improvements
- Sequential PDF page rendering for better memory management
- Print dialog translations in 21+ languages
- Unsaved changes dialog translations in 21+ languages
- Improved home directory detection for Snap packages
- Better error handling with specific exception types
- Resource cleanup with context managers
Breaking Changes
PDFStabilityConfigremoved — safe defaults are now always applied internally. Useconfigure_global_stability()for Chromium flags.confirm_before_external_linkmoved fromPDFViewerConfigtoPDFSecurityConfigprint_parallel_pagesis deprecated and ignored (printing is now sequential)signature_enabledandcomment_enabledremoved (not exposed in PDF.js UI)presentation_modenow defaults toFalseallow_javascriptandsandbox_enabledremoved fromPDFSecurityConfig(controlled globally)- Python minimum version increased to 3.10
Bug Fixes
- Fixed memory leaks in PDF rendering
- Fixed bare except clauses throughout codebase
- Fixed resource leaks with pikepdf
- Fixed subprocess error handling for system PDF viewer
v1.0.1 (2026-01-15)
- Minor bug fixes
- Improved documentation
v1.0.0 (2026-01-12)
- Initial release
- PDF.js 5.4.530 integration
- Annotation support (highlight, text, ink, stamp)
- Save/print with annotations
- Light/dark mode synchronization
show_blank_page()function for empty viewer- Configurable feature control
- Security settings and sandbox
- PyInstaller support (>= 5.0 with
_internaldirectory)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pdfjs_viewer_pyqt6-1.1.2.tar.gz.
File metadata
- Download URL: pdfjs_viewer_pyqt6-1.1.2.tar.gz
- Upload date:
- Size: 5.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80537944ea33d97121ba9bed7f451d866f8f294c33536080bd27b624566cbf98
|
|
| MD5 |
f257a87ea8d2fe29a65b12ad041976b8
|
|
| BLAKE2b-256 |
65ff19e13dbc360c9dec3e38d05d1039f991627e5be04a8cfb9ab7dbe136d8a9
|
File details
Details for the file pdfjs_viewer_pyqt6-1.1.2-py3-none-any.whl.
File metadata
- Download URL: pdfjs_viewer_pyqt6-1.1.2-py3-none-any.whl
- Upload date:
- Size: 5.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a38a6c97e8318a5125313c9ebad3d77ccd22c009a8a812208fbd4297a719f8a
|
|
| MD5 |
c122280d54f40617893aabf6da6f99f1
|
|
| BLAKE2b-256 |
0bc461a17c79e8158e3cc40755111e7b27ec926b0e2efb9245888a91af414e4e
|