diff --git a/docs/releasenotes/version17.md b/docs/releasenotes/version17.md index 374694fc..eeb65043 100644 --- a/docs/releasenotes/version17.md +++ b/docs/releasenotes/version17.md @@ -3,6 +3,16 @@ # v17 +## v17.7.2 + +- Fixed an uncaught `UnicodeDecodeError` when processing a PDF whose + `/DocumentInfo` dictionary contains a `/Name` key encoded in Latin-1 (or + another non-UTF-8 encoding), such as `/Saks#e5r`. `repair_docinfo_nuls` now + treats such a block as malformed, logs a message, and continues instead of + crashing the pipeline ({issue}`1540`). Current pikepdf releases tolerate these + keys by surrogate-escaping them, but older versions raised while iterating the + dictionary. + ## v17.7.1 - Fixed a severe, Windows-specific performance regression in the "Scanning diff --git a/src/ocrmypdf/_metadata.py b/src/ocrmypdf/_metadata.py index f973a2a0..ae6ef894 100644 --- a/src/ocrmypdf/_metadata.py +++ b/src/ocrmypdf/_metadata.py @@ -88,8 +88,12 @@ def repair_docinfo_nuls(pdf): if isinstance(v, str) and b'\x00' in bytes(v): pdf.docinfo[k] = bytes(v).replace(b'\x00', b'') modified = True - except TypeError: - # TypeError can also be raised if dictionary items are unexpected types + except (TypeError, UnicodeDecodeError): + # TypeError: DocumentInfo is not a dictionary, or its items are + # unexpected types. + # UnicodeDecodeError: a DocumentInfo key or value contains bytes that + # are not valid PDFDocEncoding/UTF-16, e.g. a Latin-1 /Name key such as + # /Saks#e5r. Older pikepdf raised while iterating such a block (#1540). log.error("File contains a malformed DocumentInfo block - continuing anyway.") return modified diff --git a/tests/resources/README.rst b/tests/resources/README.rst index a1aa4044..59e728a2 100644 --- a/tests/resources/README.rst +++ b/tests/resources/README.rst @@ -76,6 +76,9 @@ the copyright holder(s) and license(s) applicable to these resources. * - missing_docinfo.pdf - synthetic - PDF file with no /DocumentInfo section + * - docinfo_latin1_key.pdf + - synthetic + - PDF whose /DocumentInfo dictionary has a /Name key with Latin-1 bytes (/Saks#e5r) that is not valid UTF-8 * - overlay.pdf - synthetic - PDF file generated by PDFPen pro that triggered content stream parse errors diff --git a/tests/resources/docinfo_latin1_key.pdf b/tests/resources/docinfo_latin1_key.pdf new file mode 100644 index 00000000..1315ead9 --- /dev/null +++ b/tests/resources/docinfo_latin1_key.pdf @@ -0,0 +1,32 @@ +%PDF-1.3 +%¿÷¢þ +1 0 obj +<< /Pages 3 0 R /Type /Catalog >> +endobj +2 0 obj +<< /Author (Geomatikk AS) /Beskrivelse () /Creator (OCRmyPDF 16.10.0 / EasyOCR-PDF 1.7.2) /CreatorVersion (6.36.0.918) /Dokumentidplanreg () /Enhetsnavn () /Hyperlink (1) /Opprinnelse () /Producer (pikepdf 9.5.2) /RegistrationDate (N/A) /Saksansvarlig#20enhet () /Saksbehandler () /Saksnr () /Saks#e5r () /Status () >> +endobj +3 0 obj +<< /Count 1 /Kids [ 4 0 R ] /Type /Pages >> +endobj +4 0 obj +<< /Contents 5 0 R /MediaBox [ 0 0 612 792 ] /Parent 3 0 R /Resources << >> /Type /Page >> +endobj +5 0 obj +<< /Length 0 /Filter /FlateDecode >> +stream + +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000398 00000 n +0000000457 00000 n +0000000563 00000 n +trailer << /Info 2 0 R /Root 1 0 R /Size 6 /ID [] >> +startxref +633 +%%EOF diff --git a/tests/test_metadata.py b/tests/test_metadata.py index be8826bb..153ceba0 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -6,13 +6,14 @@ from __future__ import annotations import datetime as dt import warnings from shutil import copyfile +from unittest.mock import MagicMock, patch import pikepdf import pytest from pikepdf.models.metadata import decode_pdf_date from ocrmypdf._jobcontext import PdfContext -from ocrmypdf._metadata import metadata_fixup +from ocrmypdf._metadata import metadata_fixup, repair_docinfo_nuls from ocrmypdf._pipeline import convert_to_pdfa from ocrmypdf.api import setup_plugin_infrastructure from ocrmypdf.cli import get_options_and_plugins @@ -43,6 +44,32 @@ def test_preserve_docinfo(output_type, resources, outpdf): assert pdfa_info['output'] == output_type +def test_repair_docinfo_nuls_undecodable_key(caplog): + """A DocumentInfo key with bytes that don't decode must not crash. + + Some PDFs use a /Name dictionary key in DocumentInfo whose bytes are not + valid PDFDocEncoding/UTF-8 (e.g. Latin-1 ``/Saks#e5r``). Older pikepdf + raised UnicodeDecodeError while iterating such a dictionary. The repair + must log and continue rather than propagate the exception. See #1540. + """ + pdf = MagicMock() + pdf.docinfo.items.side_effect = UnicodeDecodeError( + 'utf-8', b'Saks\xe5r', 4, 5, 'invalid continuation byte' + ) + # Make isinstance(pdf.docinfo, Dictionary) succeed so we reach the loop. + with patch('ocrmypdf._metadata.Dictionary', MagicMock): + result = repair_docinfo_nuls(pdf) + assert result is False + assert 'malformed DocumentInfo' in caplog.text + + +def test_repair_docinfo_nuls_undecodable_key_real_file(resources): + """Opening a real file with a Latin-1 DocumentInfo key must not crash.""" + with pikepdf.open(resources / 'docinfo_latin1_key.pdf') as pdf: + # Should return without raising regardless of pikepdf's decode behavior. + repair_docinfo_nuls(pdf) + + @pytest.mark.parametrize("output_type", ['pdfa', 'pdf']) def test_override_metadata(output_type, resources, outpdf, caplog): input_file = resources / 'c02-22.pdf'