From 58642d8411e4f4a2ad1eccd7b1c70bb8c5897bb0 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Wed, 10 Jun 2026 13:38:05 -0700 Subject: [PATCH] fix: report un-optimizable images as a warning, not a traceback The optimizer is best-effort: any image it cannot process is left unchanged in the output, which remains valid. Previously, an extraction failure (e.g. an exotic colorspace pikepdf cannot transcode) was logged with log.exception, printing a full traceback at ERROR level that alarmed users even though nothing was wrong with the output (issue #846). Trap such failures with a concise warning that the image was left unchanged and the output is still valid, and demote the traceback to debug verbosity for diagnosis. --- docs/releasenotes/version17.md | 6 ++++ src/ocrmypdf/optimize.py | 11 ++++++-- tests/test_optimize.py | 50 ++++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/docs/releasenotes/version17.md b/docs/releasenotes/version17.md index e50a05ef..dbbf8c72 100644 --- a/docs/releasenotes/version17.md +++ b/docs/releasenotes/version17.md @@ -5,6 +5,12 @@ ## v17.6.0 +- When the optimizer encounters an image it cannot process (for example, an + exotic colorspace that cannot be transcoded), it now logs a concise warning + that the image was left unchanged rather than printing an alarming + traceback. The output file was already valid in these cases; only the + reporting was misleading. The full traceback is still available at debug + verbosity (`-v 1`) ({issue}`846`). - `--pdfa-image-compression=auto` (the default) now selects lossless image compression at `-O0` so Ghostscript no longer transcodes lossless images to JPEG during PDF/A generation. At `-O1` and above, `auto` continues to defer diff --git a/src/ocrmypdf/optimize.py b/src/ocrmypdf/optimize.py index 6aaf5833..6061fad2 100644 --- a/src/ocrmypdf/optimize.py +++ b/src/ocrmypdf/optimize.py @@ -354,9 +354,16 @@ def extract_images( pdf=pdf, root=root, image=image, xref=xref, options=options ) except Exception: # pylint: disable=broad-except - log.exception( - f"xref {xref}: While extracting this image, an error occurred" + # Optimization is best-effort: an image we cannot process is simply + # left unchanged in the output, which remains valid. Report this as + # a concise warning rather than an alarming traceback (issue #846); + # the full detail is still available at debug verbosity. + log.warning( + f"xref {xref}: this image could not be processed by the " + "optimizer and was left unchanged. The output file is still " + "valid." ) + log.debug(f"xref {xref}: image optimization error detail", exc_info=True) errors += 1 else: if result: diff --git a/tests/test_optimize.py b/tests/test_optimize.py index c35af697..e38f54a3 100644 --- a/tests/test_optimize.py +++ b/tests/test_optimize.py @@ -197,14 +197,14 @@ def test_optimize_off(resources, outpdf): def test_group3(resources): with pikepdf.open(resources / 'ccitt.pdf') as pdf: im = pdf.pages[0].Resources.XObject['/Im1'] - assert ( - opt.extract_image_filter(im, im.objgen[0]) is not None - ), "Group 4 should be allowed" + assert opt.extract_image_filter(im, im.objgen[0]) is not None, ( + "Group 4 should be allowed" + ) im.DecodeParms['/K'] = 0 - assert ( - opt.extract_image_filter(im, im.objgen[0]) is None - ), "Group 3 should be disallowed" + assert opt.extract_image_filter(im, im.objgen[0]) is None, ( + "Group 3 should be disallowed" + ) def test_find_formx(resources): @@ -234,9 +234,7 @@ def test_find_formx_circular_reference(resources, tmp_path, caplog): # entries that all point back to /Form1 itself, creating a fan-out # cycle of branching factor 3. form = pdf.pages[0].obj.Resources.XObject.Form1 - form.Resources.XObject = Dictionary( - {'/Fm0': form, '/Fm1': form, '/Fm2': form} - ) + form.Resources.XObject = Dictionary({'/Fm0': form, '/Fm1': form, '/Fm2': form}) pdf.save(out) caplog.set_level(logging.WARNING, logger='ocrmypdf.optimize') @@ -244,9 +242,7 @@ def test_find_formx_circular_reference(resources, tmp_path, caplog): opt._find_image_xrefs(pdf) n_warnings = sum( - 1 - for r in caplog.records - if 'Recursion depth exceeded' in r.getMessage() + 1 for r in caplog.records if 'Recursion depth exceeded' in r.getMessage() ) # Without the fix this is in the tens of thousands. assert n_warnings == 0, ( @@ -255,6 +251,36 @@ def test_find_formx_circular_reference(resources, tmp_path, caplog): ) +def test_extract_images_traps_errors_as_warning(resources, tmp_path, caplog): + """Regression for issue #846. + + The optimizer is best-effort: any image it cannot process can simply be + passed through unchanged. When extraction of an image raises (e.g. an + exotic colorspace pikepdf cannot transcode), the user should see a concise + warning that the image was left unchanged, not an alarming traceback + logged at ERROR level. + """ + import logging + from unittest.mock import Mock + + def boom(*, pdf, root, image, xref, options): + raise NotImplementedError("synthetic extraction failure") + + caplog.set_level(logging.DEBUG, logger='ocrmypdf.optimize') + with pikepdf.open(resources / 'francais.pdf') as pdf: + results = list(opt.extract_images(pdf, tmp_path, Mock(), boom)) + + # The error is trapped, not propagated, and nothing is extracted. + assert results == [] + # A friendly warning is emitted... + assert any( + r.levelno == logging.WARNING and 'left unchanged' in r.getMessage() + for r in caplog.records + ) + # ...and no traceback is logged at ERROR level or above. + assert not any(r.levelno >= logging.ERROR for r in caplog.records) + + def test_extract_image_filter_with_pdf_image(): image = Dictionary() image.Subtype = Name.Image