Add pdfplumber.repair(...) and .open(repair=True)

This commit adds convenience methods to repair PDFs on the fly and/or to
write repaired PDFs to disk.

Currently, this does so via Ghostscript using the method we've asked
many users to try by following the instructions at
https://superuser.com/questions/278562/how-can-i-fix-repair-a-corrupted-pdf-file

Now, hopefully, this saves folks a few steps.
This commit is contained in:
Jeremy Singer-Vine
2023-07-16 17:07:42 -04:00
parent df0e027fc7
commit db6ae97bfb
5 changed files with 131 additions and 3 deletions
+2
View File
@@ -3,6 +3,7 @@ __all__ = [
"utils",
"pdfminer",
"open",
"repair",
"set_debug",
]
@@ -12,5 +13,6 @@ import pdfminer.pdftypes
from . import utils
from ._version import __version__
from .pdf import PDF
from .repair import repair
open = PDF.open
+9 -3
View File
@@ -15,6 +15,7 @@ from pdfminer.psparser import PSException
from ._typing import T_num, T_obj_list
from .container import Container
from .page import Page
from .repair import _repair
from .utils import resolve_and_decode
logger = logging.getLogger(__name__)
@@ -64,12 +65,17 @@ class PDF(Container):
path_or_fp: Union[str, pathlib.Path, BufferedReader, BytesIO],
pages: Optional[Union[List[int], Tuple[int]]] = None,
laparams: Optional[Dict[str, Any]] = None,
password: str = "",
password: Optional[str] = None,
strict_metadata: bool = False,
repair: bool = False,
) -> "PDF":
if isinstance(path_or_fp, (str, pathlib.Path)):
stream: Union[BufferedReader, BytesIO] = open(path_or_fp, "rb")
stream: Union[str, pathlib.Path, BufferedReader, BytesIO]
if repair:
stream = _repair(path_or_fp, password=password)
stream_is_external = False
elif isinstance(path_or_fp, (str, pathlib.Path)):
stream = open(path_or_fp, "rb")
stream_is_external = False
else:
stream = path_or_fp
+62
View File
@@ -0,0 +1,62 @@
import pathlib
import shutil
import subprocess
from io import BufferedReader, BytesIO
from typing import Optional, Union
def _repair(
path_or_fp: Union[str, pathlib.Path, BufferedReader, BytesIO],
password: Optional[str] = None,
) -> BytesIO:
executable = shutil.which("gs") or shutil.which("gswin32c")
if executable is None: # pragma: nocover
raise Exception(
"Cannot find Ghostscript, which is required for repairs.\n"
"Visit https://www.ghostscript.com/ for installation instructions."
)
repair_args = [
executable,
"-o",
"-",
"-sDEVICE=pdfwrite",
"-dPDFSETTINGS=/prepress",
]
if password:
repair_args += [f"-sPDFPassword={password}"]
if isinstance(path_or_fp, (str, pathlib.Path)):
stdin = None
repair_args += [str(pathlib.Path(path_or_fp).absolute())]
else:
stdin = path_or_fp
repair_args += ["-"]
stdout, stderr = subprocess.Popen(
repair_args,
stdin=subprocess.PIPE if stdin else None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
).communicate(stdin.read() if stdin else None)
if len(stderr):
raise Exception(f"{stderr.decode('utf-8')}")
return BytesIO(stdout)
def repair(
path_or_fp: Union[str, pathlib.Path, BufferedReader, BytesIO],
outfile: Optional[Union[str, pathlib.Path]] = None,
password: Optional[str] = None,
) -> Optional[BytesIO]:
repaired = _repair(path_or_fp, password)
if outfile:
with open(outfile, "wb") as f:
f.write(repaired.read())
return None
else:
return repaired
Binary file not shown.
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python
import os
import tempfile
import unittest
import pytest
import pdfplumber
HERE = os.path.abspath(os.path.dirname(__file__))
class Test(unittest.TestCase):
def test_from_issue_932(self):
path = os.path.join(HERE, "pdfs/malformed-from-issue-932.pdf")
with pdfplumber.open(path) as pdf:
page = pdf.pages[0]
char = page.chars[0]
assert char["bottom"] > page.height
with pdfplumber.open(path, repair=True) as pdf:
page = pdf.pages[0]
char = page.chars[0]
assert char["bottom"] < page.height
with pdfplumber.repair(path) as repaired:
with pdfplumber.open(repaired) as pdf:
page = pdf.pages[0]
char = page.chars[0]
assert char["bottom"] < page.height
def test_other_repair_inputs(self):
path = os.path.join(HERE, "pdfs/malformed-from-issue-932.pdf")
with pdfplumber.open(open(path, "rb"), repair=True) as pdf:
page = pdf.pages[0]
char = page.chars[0]
assert char["bottom"] < page.height
def test_bad_repair_path(self):
path = os.path.join(HERE, "pdfs/abc.xyz")
with pytest.raises(Exception):
with pdfplumber.open(path, repair=True):
pass
def test_repair_to_file(self):
path = os.path.join(HERE, "pdfs/malformed-from-issue-932.pdf")
with tempfile.NamedTemporaryFile("wb") as out:
pdfplumber.repair(path, outfile=out.name)
with pdfplumber.open(out.name) as pdf:
page = pdf.pages[0]
char = page.chars[0]
assert char["bottom"] < page.height
def test_repair_password(self):
path = os.path.join(HERE, "pdfs/password-example.pdf")
with pdfplumber.open(path, repair=True, password="test") as pdf:
assert len(pdf.pages[0].chars)