fix: add thread safety lock to pypdfium plugin

pypdfium2/PDFium is not thread-safe - concurrent calls from different
threads can crash or corrupt the process. Added a module-level lock to
serialize all pdfium operations.

PIL image processing and file I/O are done outside the lock since they
are thread-safe, minimizing lock contention.

For maximum parallelism, users can use process-based parallelism
(use_threads=False) where each process has its own pdfium instance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
James R. Barlow
2025-12-21 12:29:17 -08:00
co-authored by Claude Opus 4.5
parent b9f488d65c
commit ae783b4ae6
2 changed files with 30 additions and 22 deletions
+30 -20
View File
@@ -5,6 +5,7 @@
from __future__ import annotations
import logging
import threading
from pathlib import Path
try:
@@ -18,6 +19,12 @@ from ocrmypdf.helpers import Resolution
log = logging.getLogger(__name__)
# pypdfium2/PDFium is not thread-safe. All calls to the library must be serialized.
# See: https://pypdfium2.readthedocs.io/en/stable/python_api.html#incompatibility-with-threading
# When using process-based parallelism (use_threads=False), each process has its own
# pdfium instance, so locking is not needed across processes.
_pdfium_lock = threading.Lock()
@hookimpl
def check_options(options):
@@ -144,33 +151,36 @@ def rasterize_pdf_page(
if pdfium is None:
return None # Fall back to Ghostscript
# Open the PDF document
pdf = _open_pdf_document(input_file)
try:
# Get the specific page (pypdfium2 uses 0-based indexing)
page = pdf[pageno - 1]
# Acquire lock to ensure thread-safe access to pypdfium2
with _pdfium_lock:
# Open the PDF document
pdf = _open_pdf_document(input_file)
try:
# Render the page to a bitmap
bitmap = _render_page_to_bitmap(page, raster_device, raster_dpi, rotation)
# Get the specific page (pypdfium2 uses 0-based indexing)
page = pdf[pageno - 1]
try:
# Convert to PIL Image
pil_image = bitmap.to_pil()
# Process image for output format and DPI
pil_image, format_name = _process_image_for_output(
pil_image, raster_device, raster_dpi, page_dpi, stop_on_soft_error
# Render the page to a bitmap
bitmap = _render_page_to_bitmap(
page, raster_device, raster_dpi, rotation
)
_save_image(pil_image, output_file, format_name)
try:
# Convert to PIL Image
pil_image = bitmap.to_pil()
finally:
bitmap.close()
finally:
bitmap.close()
page.close()
finally:
page.close()
finally:
pdf.close()
pdf.close()
# Process and save image outside the lock (PIL operations are thread-safe)
pil_image, format_name = _process_image_for_output(
pil_image, raster_device, raster_dpi, page_dpi, stop_on_soft_error
)
_save_image(pil_image, output_file, format_name)
return output_file
-2
View File
@@ -360,8 +360,6 @@ def test_simulated_scan(outdir):
'--rotate-pages',
'--plugin',
'tests/plugins/tesseract_debug_rotate.py',
'--rasterizer',
'ghostscript', # Use Ghostscript to avoid pypdfium2 thread safety issues
)
with pikepdf.open(outdir / 'out.pdf') as pdf: