mirror of
https://github.com/jsvine/pdfplumber.git
synced 2026-08-29 16:40:24 +08:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
@@ -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)
|
||||
Reference in New Issue
Block a user