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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+38
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user