From 10f6fb40c258c86fd04d86bade20f69fb07faabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20Th=C3=A9nault?= Date: Sat, 11 Dec 2021 18:25:19 +0100 Subject: [PATCH] Attempt to handle decompression error on some broken PDF files (#637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Attempt to handle decompression error on some broken PDF files from times to times we go through files where no text is detected, while readers like evince reads the pdf nicely. After digging it occured this is because the PDF includes some badly compressed data. This may be fixed by uncompressing byte per byte and ignoring the error on the last check bytes (arbitrarily found to be the 3 last). This has been largely inspired by https://github.com/mstamy2/PyPDF2/issues/422 and the test file has been taken from there, so credits to @zegrep. * Attempt to handle decompression error on some broken PDF files from times to times we go through files where no text is detected, while readers like evince reads the pdf nicely. After digging it occured this is because the PDF includes some badly compressed data. This may be fixed by uncompressing byte per byte and ignoring the error on the last check bytes (arbitrarily found to be the 3 last). This has been largely inspired by mstamy2/PyPDF2#422 and the test file has been taken from there, so credits to @zegrep. * Use a warnings instead of raising exception where zlib error is detected before the CRC checksum. * Add line to CHANGELOG.md * Only try decompressing if not in strict mode * Change error into warning because warning.warn needs a subclass of Warning Co-authored-by: Sylvain Thénault Co-authored-by: Pieter Marsman --- CHANGELOG.md | 3 +++ pdfminer/pdfdocument.py | 2 +- pdfminer/pdftypes.py | 34 +++++++++++++++++++++++++++- samples/zen_of_python_corrupted.pdf | Bin 0 -> 12472 bytes tests/test_highlevel_extracttext.py | 7 ++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 samples/zen_of_python_corrupted.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index a580bca..02dc419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Support for identity cmap's ([#626](https://github.com/pdfminer/pdfminer.six/pull/626)) +### Fixed +- Hande decompression error due to CRC checksum error ([#637](https://github.com/pdfminer/pdfminer.six/pull/637)) + ## [20211012] ### Added diff --git a/pdfminer/pdfdocument.py b/pdfminer/pdfdocument.py index 8858970..cac09f2 100644 --- a/pdfminer/pdfdocument.py +++ b/pdfminer/pdfdocument.py @@ -40,7 +40,7 @@ class PDFEncryptionError(PDFException): pass -class PDFPasswordIncorrect(PDFEncryptionError): +class PDFEncryptionWarning(UserWarning): pass diff --git a/pdfminer/pdftypes.py b/pdfminer/pdftypes.py index 6190ea9..5e0ef60 100644 --- a/pdfminer/pdftypes.py +++ b/pdfminer/pdftypes.py @@ -1,8 +1,11 @@ import zlib +import warnings import logging +import io import sys from typing import (TYPE_CHECKING, Any, Dict, Iterable, Optional, Union, List, Tuple, cast) + from .lzw import lzwdecode from .ascii85 import ascii85decode from .ascii85 import asciihexdecode @@ -216,6 +219,29 @@ def stream_value(x: object) -> "PDFStream": return x +def decompress_corrupted(data): + """Called on some data that can't be properly decoded because of CRC checksum + error. Attempt to decode it skipping the CRC. + """ + d = zlib.decompressobj() + f = io.BytesIO(data) + result_str = b'' + buffer = f.read(1) + i = 0 + try: + while buffer: + result_str += d.decompress(buffer) + buffer = f.read(1) + i += 1 + except zlib.error: + # Let the error propagates if we're not yet in the CRC checksum + if i < len(data) - 3: + # Import here to prevent circualr import + from .pdfdocument import PDFEncryptionWarning + warnings.warn("Data-loss while decompressing corrupted data", PDFEncryptionWarning) + return result_str + + class PDFStream(PDFObject): def __init__( @@ -303,12 +329,18 @@ class PDFStream(PDFObject): # will get errors if the document is encrypted. try: data = zlib.decompress(data) + except zlib.error as e: if settings.STRICT: error_msg = 'Invalid zlib bytes: {!r}, {!r}'\ .format(e, data) raise PDFException(error_msg) - data = b'' + + try: + data = decompress_corrupted(data) + except zlib.error: + data = b'' + elif f in LITERALS_LZW_DECODE: data = lzwdecode(data) elif f in LITERALS_ASCII85_DECODE: diff --git a/samples/zen_of_python_corrupted.pdf b/samples/zen_of_python_corrupted.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cbe015657d42cf3ed2fc332e5ba35b223178c650 GIT binary patch literal 12472 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