From 1c89cacfef918ea2487ad014de87b6d578361a7c Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 19 Apr 2026 13:44:08 -0700 Subject: [PATCH] Respect host-set PIL.Image.MAX_IMAGE_PIXELS in Python API The API previously clobbered PIL.Image.MAX_IMAGE_PIXELS unconditionally on every call, so host applications (e.g. Paperless-NGX) that configured the PIL limit before invoking ocrmypdf.ocr() saw their setting silently overwritten with the 250 MP default. Make max_image_mpixels default to None and only apply the override when the caller explicitly sets it. The CLI default of 250 MP is unchanged. Fixes #1665 --- docs/releasenotes/version17.md | 9 +++++++ src/ocrmypdf/_options.py | 4 +-- src/ocrmypdf/_pipelines/_common.py | 11 ++++++--- tests/test_validation.py | 39 ++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/docs/releasenotes/version17.md b/docs/releasenotes/version17.md index dd99fb29..836c313e 100644 --- a/docs/releasenotes/version17.md +++ b/docs/releasenotes/version17.md @@ -3,6 +3,15 @@ # v17 +## v17.4.2 + +- Fixed Python API unconditionally overriding ``PIL.Image.MAX_IMAGE_PIXELS`` + when the caller did not explicitly set ``max_image_mpixels``. Host + applications (e.g. Paperless-NGX) that configure the PIL limit before + invoking ``ocrmypdf.ocr()`` now have their setting respected. The CLI + default of 250 megapixels is unchanged. {issue}`1665` +- Updated uv.lock to avoid pinning a vulnerable version of Pillow. {issue}`1666` + ## v17.4.1 - Fixed RTL text extraction order in the fpdf2 renderer. Arabic lam-alef diff --git a/src/ocrmypdf/_options.py b/src/ocrmypdf/_options.py index 275483df..1a6896fb 100644 --- a/src/ocrmypdf/_options.py +++ b/src/ocrmypdf/_options.py @@ -192,7 +192,7 @@ class OcrOptions(BaseModel): no_overwrite: bool = False # Advanced options - max_image_mpixels: float = 250.0 + max_image_mpixels: float | None = None pdf_renderer: str = 'auto' ocr_engine: str = 'auto' rasterizer: str = 'auto' @@ -301,7 +301,7 @@ class OcrOptions(BaseModel): @classmethod def validate_max_image_mpixels(cls, v): """Validate max image megapixels.""" - if v < 0: + if v is not None and v < 0: raise ValueError("max_image_mpixels must be non-negative") return v diff --git a/src/ocrmypdf/_pipelines/_common.py b/src/ocrmypdf/_pipelines/_common.py index 492c39cc..802c6f42 100644 --- a/src/ocrmypdf/_pipelines/_common.py +++ b/src/ocrmypdf/_pipelines/_common.py @@ -329,10 +329,13 @@ def setup_pipeline( # Note: OcrOptions is immutable, so we can't modify options.jobs directly # The jobs field should already be set correctly during OcrOptions creation - # Apply PIL max image pixels side effect - PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000) - if PIL.Image.MAX_IMAGE_PIXELS == 0: - PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore + # Apply PIL max image pixels side effect only when explicitly requested. + # When None, leave PIL.Image.MAX_IMAGE_PIXELS as the host application + # configured it. The CLI passes its own default (250.0) via argparse. + if options.max_image_mpixels is not None: + PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000) + if PIL.Image.MAX_IMAGE_PIXELS == 0: + PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore pikepdf_enable_mmap() executor = setup_executor(plugin_manager) diff --git a/tests/test_validation.py b/tests/test_validation.py index 4bd44f4b..1fee81e6 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -102,6 +102,45 @@ def test_pillow_options(): with pytest.raises(ValueError, match="max_image_mpixels must be non-negative"): make_ocr_opts(max_image_mpixels=-1) + # Default is None, meaning "do not override host-set PIL.Image.MAX_IMAGE_PIXELS" + opts = make_ocr_opts() + assert opts.max_image_mpixels is None + + +def test_pillow_max_image_pixels_not_overridden_when_unset(): + """Issue #1665: respect host-set PIL.Image.MAX_IMAGE_PIXELS. + + API callers (e.g. Paperless-NGX) that set PIL.Image.MAX_IMAGE_PIXELS + before invoking ocrmypdf should not have their setting clobbered when + max_image_mpixels is not explicitly passed. + """ + import PIL.Image + + from ocrmypdf._pipelines._common import setup_pipeline + + parser = get_parser() + pm = setup_plugin_infrastructure(plugins=[]) + pm.add_options(parser=parser) + + saved = PIL.Image.MAX_IMAGE_PIXELS + try: + PIL.Image.MAX_IMAGE_PIXELS = None # host disables the limit + opts = make_ocr_opts() + assert opts.max_image_mpixels is None + setup_pipeline(opts, pm) + assert PIL.Image.MAX_IMAGE_PIXELS is None + + PIL.Image.MAX_IMAGE_PIXELS = 1_000_000_000 # host sets a high limit + setup_pipeline(opts, pm) + assert PIL.Image.MAX_IMAGE_PIXELS == 1_000_000_000 + + # When explicitly passed, it still takes effect. + opts = make_ocr_opts(max_image_mpixels=100) + setup_pipeline(opts, pm) + assert PIL.Image.MAX_IMAGE_PIXELS == 100_000_000 + finally: + PIL.Image.MAX_IMAGE_PIXELS = saved + def test_output_tty(): with patch('sys.stdout.isatty', return_value=True), pytest.raises(BadArgsError):