Read and write PDFs with Python, powered by qpdf
—
PDF encryption, decryption, password handling, and permission management for document security. These capabilities enable comprehensive control over PDF access and usage restrictions.
Comprehensive PDF encryption configuration for protecting documents with passwords and permissions.
class Encryption:
"""
PDF encryption settings for document security.
Configures password protection and usage permissions for PDF documents.
Supports standard security handlers with various encryption strengths.
"""
def __init__(self, *, owner: str = '', user: str = '', R: int = 6,
allow: Permissions = None, aes: bool = True,
metadata: bool = True) -> None:
"""
Create encryption settings for a PDF.
Parameters:
- owner (str): Owner password (full permissions when provided)
- user (str): User password (restricted permissions when provided)
- R (int): Security handler revision (2, 3, 4, or 6)
Higher numbers provide stronger encryption
- allow (Permissions): Permitted operations for users
- aes (bool): Use AES encryption (recommended, requires R >= 4)
- metadata (bool): Encrypt document metadata
Notes:
- R=2: 40-bit RC4 encryption (weak, legacy only)
- R=3: 128-bit RC4 encryption
- R=4: 128-bit RC4 or AES encryption
- R=6: 256-bit AES encryption (strongest, recommended)
"""
@property
def user_password(self) -> str:
"""
User password for restricted access.
Returns:
str: User password string
"""
@property
def owner_password(self) -> str:
"""
Owner password for full access.
Returns:
str: Owner password string
"""
@property
def strength(self) -> int:
"""
Encryption strength indicator.
Returns:
int: Security handler revision number (2, 3, 4, or 6)
"""
@property
def permissions(self) -> Permissions:
"""
Permitted operations under user password.
Returns:
Permissions: Object specifying allowed operations
"""Fine-grained control over document usage permissions and restrictions.
class Permissions:
"""
PDF permission flags controlling document usage restrictions.
Defines what operations users can perform when accessing
a PDF with the user password (as opposed to owner password).
"""
def __init__(self, *, accessibility: bool = True, assemble: bool = True,
extract: bool = True, modify_annotation: bool = True,
modify_assembly: bool = True, modify_form: bool = True,
modify_other: bool = True, print_lowres: bool = True,
print_highres: bool = True) -> None:
"""
Create a permissions object with specified restrictions.
Parameters:
- accessibility (bool): Allow text extraction for accessibility
- assemble (bool): Allow document assembly (insert, delete, rotate pages)
- extract (bool): Allow text and graphics extraction
- modify_annotation (bool): Allow annotation modification
- modify_assembly (bool): Allow page assembly operations
- modify_form (bool): Allow form field filling and signing
- modify_other (bool): Allow other document modifications
- print_lowres (bool): Allow low resolution printing
- print_highres (bool): Allow high resolution printing
"""
@property
def accessibility(self) -> bool:
"""
Permission to extract text for accessibility purposes.
Even with extraction disabled, this allows screen readers
and other accessibility tools to access document content.
Returns:
bool: True if accessibility extraction is permitted
"""
@accessibility.setter
def accessibility(self, value: bool) -> None:
"""Set accessibility extraction permission."""
@property
def assemble(self) -> bool:
"""
Permission to assemble the document.
Controls operations like inserting, deleting, and rotating pages.
Returns:
bool: True if document assembly is permitted
"""
@assemble.setter
def assemble(self, value: bool) -> None:
"""Set document assembly permission."""
@property
def extract(self) -> bool:
"""
Permission to extract text and graphics.
Controls copying text and images from the document.
Returns:
bool: True if content extraction is permitted
"""
@extract.setter
def extract(self, value: bool) -> None:
"""Set content extraction permission."""
@property
def modify_annotation(self) -> bool:
"""
Permission to modify annotations.
Controls adding, editing, and deleting annotations and form fields.
Returns:
bool: True if annotation modification is permitted
"""
@modify_annotation.setter
def modify_annotation(self, value: bool) -> None:
"""Set annotation modification permission."""
@property
def modify_assembly(self) -> bool:
"""
Permission to modify document assembly.
Controls page-level operations and document structure changes.
Returns:
bool: True if assembly modification is permitted
"""
@modify_assembly.setter
def modify_assembly(self, value: bool) -> None:
"""Set assembly modification permission."""
@property
def modify_form(self) -> bool:
"""
Permission to fill and sign form fields.
Controls form field interaction and digital signatures.
Returns:
bool: True if form modification is permitted
"""
@modify_form.setter
def modify_form(self, value: bool) -> None:
"""Set form modification permission."""
@property
def modify_other(self) -> bool:
"""
Permission for other document modifications.
General permission for content editing and modification operations.
Returns:
bool: True if other modifications are permitted
"""
@modify_other.setter
def modify_other(self, value: bool) -> None:
"""Set other modifications permission."""
@property
def print_lowres(self) -> bool:
"""
Permission to print at low resolution.
Typically allows printing at 150 DPI or lower resolution.
Returns:
bool: True if low resolution printing is permitted
"""
@print_lowres.setter
def print_lowres(self, value: bool) -> None:
"""Set low resolution printing permission."""
@property
def print_highres(self) -> bool:
"""
Permission to print at high resolution.
Allows printing at full resolution without restrictions.
Returns:
bool: True if high resolution printing is permitted
"""
@print_highres.setter
def print_highres(self, value: bool) -> None:
"""Set high resolution printing permission."""Information about existing PDF encryption for analysis and modification.
class EncryptionInfo:
"""
Information about PDF encryption status and parameters.
Provides details about existing encryption settings
for analysis and conditional processing.
"""
@property
def owner_password_matched(self) -> bool:
"""
Whether the owner password was correctly provided.
Returns:
bool: True if owner password grants full access
"""
@property
def user_password_matched(self) -> bool:
"""
Whether the user password was correctly provided.
Returns:
bool: True if user password was used for access
"""
@property
def bits(self) -> int:
"""
Encryption strength in bits.
Returns:
int: Encryption key length (40, 128, or 256 bits)
"""
@property
def method(self) -> str:
"""
Encryption method used.
Returns:
str: Encryption algorithm ('RC4', 'AES', or 'Unknown')
"""
@property
def permissions(self) -> Permissions:
"""
Current permission settings.
Returns:
Permissions: Active permissions for user access
"""Specialized exceptions for password and encryption handling.
class PasswordError(PdfError):
"""
Raised when PDF password operations fail.
This occurs when:
- No password is provided for an encrypted PDF
- An incorrect password is provided
- Password format is invalid
"""
class DataDecodingError(PdfError):
"""
Raised when encrypted data cannot be decoded.
This can occur with:
- Corrupted encryption data
- Unsupported encryption methods
- Invalid encryption parameters
"""import pikepdf
# Create a new PDF
pdf = pikepdf.new()
pdf.add_blank_page(page_size=(612, 792))
# Add some content
content = """
BT
/F1 12 Tf
100 700 Td
(This is a protected document) Tj
ET
"""
content_stream = pikepdf.Stream(pdf, content.encode())
pdf.pages[0]['/Contents'] = content_stream
# Configure encryption with strong settings
encryption = pikepdf.Encryption(
owner='secret_owner_password',
user='user_password',
R=6, # Use 256-bit AES encryption
aes=True,
metadata=True # Encrypt metadata too
)
# Configure restrictive permissions
permissions = pikepdf.Permissions(
accessibility=True, # Always allow accessibility
extract=False, # Prevent text copying
print_lowres=True, # Allow low-res printing only
print_highres=False, # Disable high-res printing
modify_annotation=False, # Prevent annotation changes
modify_other=False # Prevent content modification
)
encryption.permissions = permissions
# Save with encryption
pdf.save('protected_document.pdf', encryption=encryption)
pdf.close()
print("Created encrypted PDF with strong protection")import pikepdf
# Try to open encrypted PDF
try:
# First attempt - no password (will fail if encrypted)
pdf = pikepdf.open('protected_document.pdf')
print("PDF is not encrypted or no password required")
except pikepdf.PasswordError:
print("PDF requires a password")
# Try with user password
try:
pdf = pikepdf.open('protected_document.pdf', password='user_password')
print("Opened with user password - restricted access")
# Check if we have owner access
if pdf.encryption_info.owner_password_matched:
print("Owner password access - full permissions")
else:
print("User password access - limited permissions")
except pikepdf.PasswordError:
print("User password incorrect, trying owner password")
try:
pdf = pikepdf.open('protected_document.pdf', password='secret_owner_password')
print("Opened with owner password - full access")
except pikepdf.PasswordError:
print("Owner password also incorrect - cannot open PDF")
pdf = None
if pdf:
# Analyze encryption settings
enc_info = pdf.encryption_info
print(f"Encryption method: {enc_info.method}")
print(f"Encryption strength: {enc_info.bits} bits")
print(f"Owner access: {enc_info.owner_password_matched}")
print(f"User access: {enc_info.user_password_matched}")
# Check permissions
perms = enc_info.permissions
print(f"Can extract text: {perms.extract}")
print(f"Can print high-res: {perms.print_highres}")
print(f"Can modify: {perms.modify_other}")
pdf.close()import pikepdf
def create_protection_levels():
"""Demonstrate different levels of PDF protection."""
# Level 1: Basic password protection, full permissions
pdf1 = pikepdf.new()
pdf1.add_blank_page()
basic_encryption = pikepdf.Encryption(
user='simple123',
owner='admin123',
R=4, # 128-bit encryption
aes=True
)
# Default permissions allow everything
pdf1.save('basic_protected.pdf', encryption=basic_encryption)
pdf1.close()
print("Created: basic_protected.pdf (password required, full permissions)")
# Level 2: Moderate protection with some restrictions
pdf2 = pikepdf.new()
pdf2.add_blank_page()
moderate_permissions = pikepdf.Permissions(
extract=False, # No copying
print_highres=False, # Low-res printing only
modify_other=False # No content modification
)
moderate_encryption = pikepdf.Encryption(
user='user456',
owner='owner456',
R=6, # 256-bit AES
allow=moderate_permissions
)
pdf2.save('moderate_protected.pdf', encryption=moderate_encryption)
pdf2.close()
print("Created: moderate_protected.pdf (restricted copying/printing)")
# Level 3: High security with minimal permissions
pdf3 = pikepdf.new()
pdf3.add_blank_page()
strict_permissions = pikepdf.Permissions(
accessibility=True, # Keep accessibility
extract=False, # No copying
print_lowres=False, # No printing at all
print_highres=False,
modify_annotation=False, # No annotations
modify_form=False, # No form filling
modify_other=False, # No modifications
assemble=False # No page operations
)
strict_encryption = pikepdf.Encryption(
user='readonly789',
owner='superadmin789',
R=6, # Strongest encryption
allow=strict_permissions,
metadata=True # Encrypt metadata too
)
pdf3.save('strict_protected.pdf', encryption=strict_encryption)
pdf3.close()
print("Created: strict_protected.pdf (view-only, no operations allowed)")
create_protection_levels()import pikepdf
def remove_encryption(input_file, password, output_file):
"""Remove encryption from a PDF if password is known."""
try:
# Open encrypted PDF
pdf = pikepdf.open(input_file, password=password)
# Check if we have owner access (required to remove encryption)
if not pdf.encryption_info.owner_password_matched:
print("Warning: Only user access - encryption removal may not work")
# Save without encryption (no encryption parameter)
pdf.save(output_file)
pdf.close()
print(f"Successfully removed encryption: {input_file} -> {output_file}")
# Verify the result
verify_pdf = pikepdf.open(output_file)
print(f"Verification: New PDF has {len(verify_pdf.pages)} pages")
print(f"Verification: PDF is encrypted: {verify_pdf.is_encrypted}")
verify_pdf.close()
except pikepdf.PasswordError:
print(f"Error: Incorrect password for {input_file}")
except Exception as e:
print(f"Error removing encryption: {e}")
# Remove encryption from various protection levels
remove_encryption('basic_protected.pdf', 'admin123', 'basic_unprotected.pdf')
remove_encryption('moderate_protected.pdf', 'owner456', 'moderate_unprotected.pdf')
remove_encryption('strict_protected.pdf', 'superadmin789', 'strict_unprotected.pdf')import pikepdf
def upgrade_encryption(input_file, old_password, new_encryption, output_file):
"""Upgrade encryption settings for an existing PDF."""
try:
# Open with existing password
pdf = pikepdf.open(input_file, password=old_password)
print(f"Current encryption: {pdf.encryption_info.method}, "
f"{pdf.encryption_info.bits} bits")
# Save with new encryption settings
pdf.save(output_file, encryption=new_encryption)
pdf.close()
print(f"Upgraded encryption: {input_file} -> {output_file}")
# Verify new encryption
verify_pdf = pikepdf.open(output_file, password=new_encryption.owner_password)
new_info = verify_pdf.encryption_info
print(f"New encryption: {new_info.method}, {new_info.bits} bits")
verify_pdf.close()
except Exception as e:
print(f"Error upgrading encryption: {e}")
# Upgrade to strongest encryption
new_encryption = pikepdf.Encryption(
owner='new_super_secure_password_2024',
user='new_user_password_2024',
R=6, # 256-bit AES
aes=True,
metadata=True,
allow=pikepdf.Permissions(
extract=False,
print_highres=True, # Allow high-quality printing
modify_other=False
)
)
upgrade_encryption('basic_protected.pdf', 'admin123', new_encryption, 'upgraded_protected.pdf')import pikepdf
import os
from pathlib import Path
def encrypt_directory(directory_path, encryption_settings, password):
"""Encrypt all PDFs in a directory."""
directory = Path(directory_path)
encrypted_dir = directory / 'encrypted'
encrypted_dir.mkdir(exist_ok=True)
pdf_files = list(directory.glob('*.pdf'))
results = {'success': [], 'failed': []}
for pdf_file in pdf_files:
try:
# Skip already encrypted files (basic check)
try:
test_pdf = pikepdf.open(pdf_file)
if test_pdf.is_encrypted:
print(f"Skipping already encrypted: {pdf_file.name}")
test_pdf.close()
continue
test_pdf.close()
except pikepdf.PasswordError:
print(f"Skipping password-protected: {pdf_file.name}")
continue
# Encrypt the PDF
pdf = pikepdf.open(pdf_file)
output_path = encrypted_dir / pdf_file.name
pdf.save(output_path, encryption=encryption_settings)
pdf.close()
results['success'].append(pdf_file.name)
print(f"Encrypted: {pdf_file.name}")
except Exception as e:
results['failed'].append((pdf_file.name, str(e)))
print(f"Failed to encrypt {pdf_file.name}: {e}")
print(f"\nEncryption complete:")
print(f" Successful: {len(results['success'])} files")
print(f" Failed: {len(results['failed'])} files")
return results
# Bulk encryption with standard settings
bulk_encryption = pikepdf.Encryption(
owner='bulk_owner_2024',
user='bulk_user_2024',
R=6,
allow=pikepdf.Permissions(
print_highres=True,
extract=False,
modify_other=False
)
)
# Encrypt all PDFs in current directory
# results = encrypt_directory('.', bulk_encryption, 'bulk_user_2024')import pikepdf
from pathlib import Path
def analyze_pdf_security(pdf_path, password=None):
"""Analyze the security settings of a PDF file."""
try:
# Try to open without password first
try:
pdf = pikepdf.open(pdf_path)
encrypted = False
except pikepdf.PasswordError:
if password:
pdf = pikepdf.open(pdf_path, password=password)
encrypted = True
else:
return {"error": "PDF is encrypted but no password provided"}
analysis = {
"file": str(pdf_path),
"encrypted": encrypted,
"pages": len(pdf.pages),
"pdf_version": pdf.pdf_version
}
if encrypted:
enc_info = pdf.encryption_info
analysis.update({
"encryption_method": enc_info.method,
"encryption_bits": enc_info.bits,
"owner_access": enc_info.owner_password_matched,
"user_access": enc_info.user_password_matched,
"permissions": {
"extract_text": enc_info.permissions.extract,
"print_lowres": enc_info.permissions.print_lowres,
"print_highres": enc_info.permissions.print_highres,
"modify_annotations": enc_info.permissions.modify_annotation,
"modify_forms": enc_info.permissions.modify_form,
"modify_other": enc_info.permissions.modify_other,
"assemble_document": enc_info.permissions.assemble,
"accessibility": enc_info.permissions.accessibility
}
})
pdf.close()
return analysis
except Exception as e:
return {"error": str(e)}
def security_report(directory_path):
"""Generate a security report for all PDFs in a directory."""
directory = Path(directory_path)
pdf_files = list(directory.glob('*.pdf'))
print(f"PDF Security Report for: {directory}")
print("=" * 60)
encrypted_count = 0
unencrypted_count = 0
error_count = 0
for pdf_file in pdf_files:
analysis = analyze_pdf_security(pdf_file)
if "error" in analysis:
print(f"\n❌ {pdf_file.name}: {analysis['error']}")
error_count += 1
elif analysis["encrypted"]:
print(f"\n🔒 {pdf_file.name}: ENCRYPTED")
print(f" Method: {analysis['encryption_method']}")
print(f" Strength: {analysis['encryption_bits']} bits")
print(f" Access Level: {'Owner' if analysis['owner_access'] else 'User'}")
# Show key restrictions
perms = analysis["permissions"]
restrictions = []
if not perms["extract_text"]: restrictions.append("No copying")
if not perms["print_highres"]: restrictions.append("No high-res printing")
if not perms["modify_other"]: restrictions.append("No modification")
if restrictions:
print(f" Restrictions: {', '.join(restrictions)}")
else:
print(" Restrictions: None")
encrypted_count += 1
else:
print(f"\n🔓 {pdf_file.name}: UNPROTECTED")
unencrypted_count += 1
print(f"\n" + "=" * 60)
print(f"Summary: {len(pdf_files)} PDFs analyzed")
print(f" Encrypted: {encrypted_count}")
print(f" Unprotected: {unencrypted_count}")
print(f" Errors: {error_count}")
# Generate security report for current directory
# security_report('.')Install with Tessl CLI
npx tessl i tessl/pypi-pikepdf