Read and write PDFs with Python, powered by qpdf
—
Image extraction, manipulation, and graphics operations including support for various formats and color spaces. These capabilities enable comprehensive image handling within PDF documents.
High-level PDF image handling with extraction and conversion capabilities.
class PdfImage:
"""
PDF image object handler for image extraction and manipulation.
Provides access to image properties, extraction capabilities,
and conversion to external formats like PIL Image objects.
"""
def extract_to(self, *, fileprefix: str = 'image', dirname: str = '.') -> str:
"""
Extract the image to a file with automatic format detection.
The image is saved with an appropriate file extension based on
its format and compression. Supports PNG, JPEG, TIFF, and other formats.
Parameters:
- fileprefix (str): Base filename for the extracted image
- dirname (str): Directory to save the image in
Returns:
str: Full path to the extracted image file
Raises:
UnsupportedImageTypeError: If image format is not supported
InvalidPdfImageError: If image data is corrupted or invalid
"""
def as_pil_image(self) -> Any: # PIL.Image.Image
"""
Convert the PDF image to a PIL Image object.
Returns:
PIL.Image.Image: PIL Image object that can be manipulated or saved
Raises:
DependencyError: If PIL (Pillow) is not installed
UnsupportedImageTypeError: If image cannot be converted
InvalidPdfImageError: If image data is invalid
"""
@property
def width(self) -> int:
"""
Image width in pixels.
Returns:
int: Width of the image in pixels
"""
@property
def height(self) -> int:
"""
Image height in pixels.
Returns:
int: Height of the image in pixels
"""
@property
def bpc(self) -> int:
"""
Bits per component (color depth).
Returns:
int: Number of bits per color component (typically 1, 8, or 16)
"""
@property
def colorspace(self) -> Name:
"""
Color space of the image.
Returns:
Name: Color space (e.g., Name.DeviceRGB, Name.DeviceCMYK, Name.DeviceGray)
"""
@property
def filters(self) -> list[Name]:
"""
List of filters applied to the image data.
Returns:
list[Name]: Compression and encoding filters (e.g., [Name.DCTDecode] for JPEG)
"""
@property
def filter_decodeparms(self) -> list[Object]:
"""
Decode parameters for image filters.
Returns:
list[Object]: Parameters for filter decoding
"""
@property
def image_mask(self) -> bool:
"""
Whether this image is used as a mask.
Returns:
bool: True if image is a mask (1-bit monochrome used for transparency)
"""
@property
def mask(self) -> Object:
"""
Mask associated with this image.
Returns:
Object: Mask image or soft mask for transparency effects
"""
@property
def palette(self) -> Object:
"""
Color palette for indexed color images.
Returns:
Object: Palette data for indexed color space images
"""
@property
def size(self) -> tuple[int, int]:
"""
Image dimensions as a tuple.
Returns:
tuple[int, int]: (width, height) in pixels
"""
@property
def obj(self) -> Stream:
"""
The underlying PDF stream object containing image data.
Returns:
Stream: PDF stream with image data and metadata
"""Handler for inline images embedded directly in content streams.
class PdfInlineImage:
"""
Inline image embedded directly in a PDF content stream.
Inline images are embedded directly in the page content stream
rather than being stored as separate objects with indirect references.
"""
def as_pil_image(self) -> Any: # PIL.Image.Image
"""
Convert the inline image to a PIL Image object.
Returns:
PIL.Image.Image: PIL Image object for manipulation or display
Raises:
DependencyError: If PIL (Pillow) is not installed
UnsupportedImageTypeError: If image format is not supported
InvalidPdfImageError: If image data is corrupted
"""
@property
def width(self) -> int:
"""
Inline image width in pixels.
Returns:
int: Width of the inline image
"""
@property
def height(self) -> int:
"""
Inline image height in pixels.
Returns:
int: Height of the inline image
"""
@property
def bpc(self) -> int:
"""
Bits per component for the inline image.
Returns:
int: Color depth per component
"""
@property
def colorspace(self) -> Object:
"""
Color space of the inline image.
Returns:
Object: Color space specification
"""
@property
def filters(self) -> list[Name]:
"""
Filters applied to the inline image data.
Returns:
list[Name]: Compression and encoding filters
"""
@property
def size(self) -> tuple[int, int]:
"""
Inline image dimensions.
Returns:
tuple[int, int]: (width, height) in pixels
"""Specialized exceptions for image-related operations.
class UnsupportedImageTypeError(Exception):
"""
Raised when attempting to process an unsupported image type.
This occurs when the PDF contains image formats or compression
methods that pikepdf cannot handle or convert.
"""
class InvalidPdfImageError(Exception):
"""
Raised when image data in the PDF is corrupted or invalid.
This can occur with damaged PDF files or images with
inconsistent metadata and data.
"""
class HifiPrintImageNotTranscodableError(Exception):
"""
Raised when high-fidelity print images cannot be transcoded.
Some specialized print images use formats that cannot be
easily converted to standard image formats.
"""
class ImageDecompressionError(Exception):
"""
Raised when image decompression fails.
This occurs when compressed image data cannot be properly
decompressed due to corruption or unsupported compression parameters.
"""Geometric transformation matrix for image placement and scaling.
class Matrix:
"""
PDF transformation matrix for geometric operations.
Represents a 2D transformation matrix with 6 elements:
[a b c d e f] representing the transformation:
x' = a*x + c*y + e
y' = b*x + d*y + f
"""
def __init__(self, a: float = 1, b: float = 0, c: float = 0,
d: float = 1, e: float = 0, f: float = 0) -> None:
"""
Create a transformation matrix.
Parameters:
- a, b, c, d, e, f (float): Matrix elements
"""
@staticmethod
def identity() -> Matrix:
"""
Create an identity matrix (no transformation).
Returns:
Matrix: Identity matrix [1 0 0 1 0 0]
"""
def translated(self, dx: float, dy: float) -> Matrix:
"""
Create a matrix with translation applied.
Parameters:
- dx (float): Translation in X direction
- dy (float): Translation in Y direction
Returns:
Matrix: New matrix with translation applied
"""
def scaled(self, sx: float, sy: float = None) -> Matrix:
"""
Create a matrix with scaling applied.
Parameters:
- sx (float): Scale factor in X direction
- sy (float, optional): Scale factor in Y direction (defaults to sx)
Returns:
Matrix: New matrix with scaling applied
"""
def rotated(self, angle_degrees: float) -> Matrix:
"""
Create a matrix with rotation applied.
Parameters:
- angle_degrees (float): Rotation angle in degrees
Returns:
Matrix: New matrix with rotation applied
"""
def inverse(self) -> Matrix:
"""
Calculate the inverse of this matrix.
Returns:
Matrix: Inverse transformation matrix
Raises:
ValueError: If matrix is not invertible (determinant is zero)
"""
def transform(self, point: tuple[float, float]) -> tuple[float, float]:
"""
Transform a point using this matrix.
Parameters:
- point (tuple[float, float]): Point coordinates (x, y)
Returns:
tuple[float, float]: Transformed point coordinates
"""
@property
def a(self) -> float:
"""X-scaling component."""
@property
def b(self) -> float:
"""Y-skewing component."""
@property
def c(self) -> float:
"""X-skewing component."""
@property
def d(self) -> float:
"""Y-scaling component."""
@property
def e(self) -> float:
"""X-translation component."""
@property
def f(self) -> float:
"""Y-translation component."""import pikepdf
# Open PDF with images
pdf = pikepdf.open('document_with_images.pdf')
image_count = 0
# Iterate through all pages
for page_num, page in enumerate(pdf.pages):
# Get images on this page
page_images = page.images
for name, image in page_images.items():
try:
# Extract image to file
filename = image.extract_to(
fileprefix=f'page{page_num+1}_image{image_count}',
dirname='extracted_images'
)
print(f"Extracted image: {filename}")
print(f" Size: {image.width} x {image.height}")
print(f" Color depth: {image.bpc} bits per component")
print(f" Color space: {image.colorspace}")
print(f" Filters: {image.filters}")
image_count += 1
except pikepdf.UnsupportedImageTypeError as e:
print(f"Could not extract image {name}: {e}")
except pikepdf.InvalidPdfImageError as e:
print(f"Invalid image data for {name}: {e}")
print(f"Total images extracted: {image_count}")
pdf.close()import pikepdf
from PIL import Image, ImageEnhance
pdf = pikepdf.open('document_with_images.pdf')
for page_num, page in enumerate(pdf.pages):
page_images = page.images
for name, pdf_image in page_images.items():
try:
# Convert to PIL Image
pil_image = pdf_image.as_pil_image()
# Apply image processing
if pil_image.mode == 'RGB':
# Enhance brightness
enhancer = ImageEnhance.Brightness(pil_image)
enhanced = enhancer.enhance(1.2)
# Save processed image
output_path = f'processed_page{page_num+1}_{name}.png'
enhanced.save(output_path)
print(f"Processed and saved: {output_path}")
except pikepdf.DependencyError:
print("PIL (Pillow) not installed - cannot convert to PIL format")
except Exception as e:
print(f"Error processing image {name}: {e}")
pdf.close()import pikepdf
pdf = pikepdf.open('document_with_images.pdf')
# Collect image statistics
image_stats = {
'total_images': 0,
'by_colorspace': {},
'by_filter': {},
'by_dimensions': [],
'total_size_bytes': 0
}
for page in pdf.pages:
page_images = page.images
for name, image in page_images.items():
image_stats['total_images'] += 1
# Color space statistics
colorspace = str(image.colorspace)
image_stats['by_colorspace'][colorspace] = \
image_stats['by_colorspace'].get(colorspace, 0) + 1
# Filter statistics
for filter_name in image.filters:
filter_str = str(filter_name)
image_stats['by_filter'][filter_str] = \
image_stats['by_filter'].get(filter_str, 0) + 1
# Dimension statistics
dimensions = (image.width, image.height)
image_stats['by_dimensions'].append(dimensions)
# Size estimation (from stream length)
if hasattr(image.obj, 'Length'):
image_stats['total_size_bytes'] += int(image.obj.Length)
# Detailed image info
print(f"Image {name}:")
print(f" Dimensions: {image.width} x {image.height}")
print(f" Bits per component: {image.bpc}")
print(f" Color space: {image.colorspace}")
print(f" Filters: {image.filters}")
print(f" Is mask: {image.image_mask}")
if image.mask:
print(f" Has mask/transparency")
if image.palette:
print(f" Has color palette")
# Print summary statistics
print("\n=== Image Statistics ===")
print(f"Total images: {image_stats['total_images']}")
print(f"Total estimated size: {image_stats['total_size_bytes'] / 1024:.1f} KB")
print("\nColor spaces:")
for cs, count in image_stats['by_colorspace'].items():
print(f" {cs}: {count}")
print("\nCompression filters:")
for filter_name, count in image_stats['by_filter'].items():
print(f" {filter_name}: {count}")
# Find most common dimensions
if image_stats['by_dimensions']:
from collections import Counter
dimension_counts = Counter(image_stats['by_dimensions'])
print(f"\nMost common dimensions:")
for dims, count in dimension_counts.most_common(3):
print(f" {dims[0]}x{dims[1]}: {count} images")
pdf.close()import pikepdf
pdf = pikepdf.open('document.pdf')
for page_num, page in enumerate(pdf.pages):
# Parse content stream to find inline images
instructions = page.parse_contents()
inline_image_count = 0
for instruction in instructions:
if isinstance(instruction, pikepdf.ContentStreamInlineImage):
inline_image = instruction.iimage
try:
# Convert inline image to PIL
pil_image = inline_image.as_pil_image()
# Save inline image
filename = f'page{page_num+1}_inline{inline_image_count}.png'
pil_image.save(filename)
print(f"Saved inline image: {filename}")
print(f" Size: {inline_image.width} x {inline_image.height}")
print(f" Color space: {inline_image.colorspace}")
inline_image_count += 1
except Exception as e:
print(f"Could not process inline image: {e}")
if inline_image_count > 0:
print(f"Page {page_num+1}: Found {inline_image_count} inline images")
pdf.close()import pikepdf
from PIL import Image
pdf = pikepdf.open('document.pdf')
page = pdf.pages[0]
# Create a new image to insert
new_image = Image.new('RGB', (200, 100), color='red')
new_image_path = 'replacement.png'
new_image.save(new_image_path)
# Create PDF image from file
with open(new_image_path, 'rb') as f:
image_data = f.read()
# Create image stream
image_stream = pikepdf.Stream(pdf, image_data)
image_stream.dictionary.update({
'/Type': pikepdf.Name.XObject,
'/Subtype': pikepdf.Name.Image,
'/Width': 200,
'/Height': 100,
'/ColorSpace': pikepdf.Name.DeviceRGB,
'/BitsPerComponent': 8,
'/Filter': pikepdf.Name.DCTDecode # JPEG compression
})
# Add image to page resources
if '/Resources' not in page:
page['/Resources'] = pikepdf.Dictionary()
if '/XObject' not in page['/Resources']:
page['/Resources']['/XObject'] = pikepdf.Dictionary()
# Add image with name
image_name = '/NewImage'
page['/Resources']['/XObject'][image_name] = image_stream
# Create content stream to display the image
content = f"""
q
200 0 0 100 50 700 cm
{image_name} Do
Q
"""
# Add to page content
if '/Contents' in page:
existing_content = page['/Contents']
if isinstance(existing_content, pikepdf.Array):
new_stream = pikepdf.Stream(pdf, content.encode())
existing_content.append(new_stream)
else:
# Convert single stream to array
page['/Contents'] = pikepdf.Array([existing_content])
new_stream = pikepdf.Stream(pdf, content.encode())
page['/Contents'].append(new_stream)
else:
page['/Contents'] = pikepdf.Stream(pdf, content.encode())
pdf.save('document_with_new_image.pdf')
pdf.close()import pikepdf
from PIL import Image, ImageFilter, ImageOps
def process_pdf_images(input_pdf, output_pdf):
"""Process all images in a PDF with various filters."""
pdf = pikepdf.open(input_pdf)
for page_num, page in enumerate(pdf.pages):
page_images = page.images
for name, pdf_image in page_images.items():
try:
# Convert to PIL for processing
pil_image = pdf_image.as_pil_image()
# Apply various image enhancements
if pil_image.mode == 'RGB':
# Apply unsharp mask for clarity
enhanced = pil_image.filter(ImageFilter.UnsharpMask(
radius=1, percent=150, threshold=3
))
# Auto-contrast adjustment
enhanced = ImageOps.autocontrast(enhanced, cutoff=1)
elif pil_image.mode == 'L': # Grayscale
# Enhance contrast for grayscale images
enhanced = ImageOps.autocontrast(pil_image, cutoff=2)
else:
# Skip images we can't enhance
continue
# Convert back to PDF format
# Note: This is simplified - real implementation would need
# to properly encode the image and update the PDF stream
temp_path = f'temp_enhanced_{name}.png'
enhanced.save(temp_path, optimize=True)
print(f"Enhanced image {name} on page {page_num+1}")
# Clean up temp file
import os
os.unlink(temp_path)
except Exception as e:
print(f"Could not enhance image {name}: {e}")
pdf.save(output_pdf)
pdf.close()
# Usage
process_pdf_images('input.pdf', 'enhanced_output.pdf')import pikepdf
from PIL import Image
def convert_pdf_images_to_format(pdf_path, output_format='PNG'):
"""Convert all PDF images to a specific format."""
pdf = pikepdf.open(pdf_path)
converted_images = []
for page_num, page in enumerate(pdf.pages):
page_images = page.images
for name, pdf_image in page_images.items():
try:
# Convert to PIL
pil_image = pdf_image.as_pil_image()
# Determine output filename
base_name = f'page{page_num+1}_{name}'
output_path = f'{base_name}.{output_format.lower()}'
# Convert and save
if output_format.upper() == 'JPEG' and pil_image.mode == 'RGBA':
# Convert RGBA to RGB for JPEG
rgb_image = Image.new('RGB', pil_image.size, (255, 255, 255))
rgb_image.paste(pil_image, mask=pil_image.split()[-1])
rgb_image.save(output_path, format=output_format, quality=95)
else:
pil_image.save(output_path, format=output_format)
converted_images.append(output_path)
print(f"Converted {name} to {output_path}")
except Exception as e:
print(f"Could not convert image {name}: {e}")
pdf.close()
return converted_images
# Convert all images to PNG
png_files = convert_pdf_images_to_format('document.pdf', 'PNG')
print(f"Converted {len(png_files)} images to PNG format")Install with Tessl CLI
npx tessl i tessl/pypi-pikepdf