Compare commits

...
38 Commits
Author SHA1 Message Date
James R. Barlow 8224d89bc6 v11.3.4 release notes 2020-11-18 11:57:28 -08:00
James R. Barlow a2bbbe2a26 v11.3.4 release notes 2020-11-18 11:56:29 -08:00
James R. Barlow 43f41863fa check_pdf: document how we handle linearization 2020-11-18 11:54:07 -08:00
James R. Barlow d71e50e83d Fix "readLinearizationData for file that is not linearized"
pikepdf 2.1.0 throws wrong type of exception in this case, so special-case it.

Closes #680
Closes #681
2020-11-18 11:52:17 -08:00
James R. Barlow 1f598da3c1 ghostscript: better docs and comments 2020-11-18 11:34:17 -08:00
James R. Barlow d0cdbd5e1c watcher: include uppercase .PDF too 2020-11-12 02:29:47 -08:00
James R. Barlow 5c56f61209 unpaper: type hints 2020-11-11 02:59:37 -08:00
James R. Barlow 9bec85470a Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-11-10 04:08:05 -08:00
James R. Barlow a03863a17d docs: fix link to docker image 2020-11-10 04:08:01 -08:00
James R. Barlow 22cd9b2364 docs: fix csv-table errors 2020-11-10 04:07:49 -08:00
pretentious7andGitHub 4fc7d6d93e fix typo "charcter" -> "character" (#673) 2020-11-09 16:53:02 -08:00
James R. Barlow 71f0e7f545 v11.3.3 release notes 2020-11-07 00:53:33 -08:00
James R. Barlow 895fddd85e Replace most uses of universal_newlines with text
The parameters are equivalent but the latter is better named. Since
Python 3.6 doesn't support text= we use our wrapper to add it in that
place.

This is for subprocess.run.
2020-11-07 00:48:08 -08:00
James R. Barlow 5a59e4d543 unpaper: don't use universal_newlines=True
There's no specific reason to do this. We can log binary output equally
 well.
2020-11-07 00:18:27 -08:00
James R. Barlow b51abf2249 azure: Fix indentation mistake 2020-11-04 12:19:35 -08:00
James R. Barlow 6d3f9ff15a api: rework ocr() slightly to simplify variable handling 2020-11-03 17:10:52 -08:00
James R. Barlow 5d1d1a712b docs: more details about macOS API changes
Due to fork->spawn
2020-11-03 17:09:58 -08:00
James R. Barlow 6d5f8133e0 docs: show ifmain guard in example 2020-11-03 15:28:33 -08:00
James R. Barlow 13018d3d5c ci: Extend test matrix to Python 3.9 2020-11-03 04:15:14 -08:00
James R. Barlow 14a85f9473 Fix pinned dependencies 2020-11-03 04:12:47 -08:00
James R. Barlow d22a1b3367 v11.3.2 release notes (2)
Since we never tagged it, fix other things.
2020-11-03 02:03:25 -08:00
James R. Barlow b913e5dfef ghostscript: don't repeat log in debug
Subprocess already does this for us.
2020-11-03 01:45:06 -08:00
James R. Barlow dd8a5a4c72 Fix log domain names
ocrmypdf.subprocess.subprocess.ghostscript -> ocrmypdf.subprocess.ghostscript
2020-11-03 01:44:35 -08:00
James R. Barlow 36e9a54f02 Remove extraneous page rotation
This was added in commit b5ccbfd but seems to have been ill-advised.
2020-11-03 01:34:28 -08:00
James R. Barlow 3707af3b74 Change pdf.root to pdf.Root 2020-11-03 01:30:31 -08:00
James R. Barlow ced7ad9164 unpaper: round off DPI 2020-11-03 01:14:57 -08:00
James R. Barlow 54bbbfdeb3 Fix UnboundLocalError when considering ImageMasks for optimization
Uncovered by test file in issue 667, although unrelated to that issue.
2020-11-03 01:08:14 -08:00
James R. Barlow 7f73a6ed1e Some Python 3.9 fixes 2020-11-03 00:45:47 -08:00
James R. Barlow dce206d3dc Fix pre-commit for Py3.9 2020-11-03 00:20:25 -08:00
James R. Barlow 9304c856cf Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-11-02 02:47:36 -08:00
James R. Barlow e5df98cbdf v11.3.2 release notes 2020-11-02 02:43:32 -08:00
James R. Barlow 19bf3aeb00 api: improve typing 2020-11-02 02:33:34 -08:00
James R. Barlow e86be0031c unpaper: fix process output handling
With the ocrmypdf.subprocess wrapper, logging the output here
is redundant and loses the page number context.
2020-11-02 01:07:41 -08:00
James R. Barlow 6425977998 unpaper: use pnm instead of png
Some users reported problems with PNG recently; try PNM.

Fixes #665
Fixes #667
2020-11-02 01:05:56 -08:00
James R. Barlow d57df2d980 subprocess: support programs that write their messages to stdout 2020-11-02 01:00:59 -08:00
James R. Barlow 664d0c7969 Document configure_debug_logging 2020-11-02 00:59:00 -08:00
James R. Barlow a354663ee1 Fix typo in API documentation 2020-11-02 00:58:28 -08:00
Graham MilnandGitHub b21b048ec4 Add macOS brew language support (#615)
Note `brew` command for installing additional languages on macOS.
2020-10-30 01:09:06 -07:00
28 changed files with 233 additions and 154 deletions
+1 -1
View File
@@ -19,5 +19,5 @@ repos:
rev: 19.10b0 rev: 19.10b0
hooks: hooks:
- id: black - id: black
language_version: python3.8 language_version: python
exclude: ^src/ocrmypdf/lib/_leptonica.py exclude: ^src/ocrmypdf/lib/_leptonica.py
+3
View File
@@ -92,6 +92,9 @@ apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified lan
# Arch Linux users # Arch Linux users
pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs
# brew macOS users
brew install tesseract-lang
``` ```
You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested. You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested.
+5 -2
View File
@@ -22,6 +22,8 @@ stages:
python.version: "3.7" python.version: "3.7"
Python38: Python38:
python.version: "3.8" python.version: "3.8"
Python39:
python.version: "3.9"
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
@@ -59,6 +61,8 @@ stages:
python.version: "3.7" python.version: "3.7"
Python38: Python38:
python.version: "3.8" python.version: "3.8"
Python39:
python.version: "3.9"
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
@@ -156,9 +160,8 @@ stages:
# versionSpec: "$(python.version)" # versionSpec: "$(python.version)"
- bash: | - bash: |
brew update brew update
brew unlink python@2
brew upgrade python brew upgrade python
echo "Using Python `python3 --version`" echo "Using `python3 --version`"
displayName: "Update brew and Python" displayName: "Update brew and Python"
- bash: | - bash: |
brew install \ brew install \
+11 -10
View File
@@ -20,7 +20,8 @@ and largely have the same functions.
import ocrmypdf import ocrmypdf
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True) if __name__ == '__main__': # To ensure correct behavior on Windows
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True)
With a few exceptions, all of the command line arguments are available With a few exceptions, all of the command line arguments are available
and may be passed as equivalent keywords. and may be passed as equivalent keywords.
@@ -35,8 +36,9 @@ The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line
execution. To do this, it will: execution. To do this, it will:
- create a monitoring thread - create a monitoring thread
- create worker processes (forking itself) - create worker processes (on Linux, forking itself; on Windows and macOS, by
- manage the signal flags of worker processes spawning)
- manage the signal flags of its worker processes
- execute other subprocesses (forking and executing other programs) - execute other subprocesses (forking and executing other programs)
The Python process that calls ``ocrmypdf.ocr()`` must be sufficiently The Python process that calls ``ocrmypdf.ocr()`` must be sufficiently
@@ -47,9 +49,9 @@ There is no currently no option to manage how jobs are scheduled other
than the argument ``jobs=`` which will limit the number of worker than the argument ``jobs=`` which will limit the number of worker
processes. processes.
Forking a child process to call ``ocrmypdf.ocr()`` is suggested. That Creating a child process to call ``ocrmypdf.ocr()`` is suggested. That
way your application will survive and remain interactive even if way your application will survive and remain interactive even if
OCRmyPDF does not. OCRmyPDF fails for any reason.
Programs that call ``ocrmypdf.ocr()`` should also install a SIGBUS signal Programs that call ``ocrmypdf.ocr()`` should also install a SIGBUS signal
handler (except on Windows), to raise an exception if access to a memory handler (except on Windows), to raise an exception if access to a memory
@@ -57,11 +59,10 @@ mapped file fails. OCRmyPDF may use memory mapping.
.. warning:: .. warning::
On Windows, the script that calls ``ocrmypdf.ocr()`` must be protected On Windows and macOS, the script that calls ``ocrmypdf.ocr()`` must be
by an "ifmain" guard (``if __name__ == '__main__'``) or you must use protected by an "ifmain" guard (``if __name__ == '__main__'``). If you do
``ocrmypdf.ocr(...use_threads=True)``. If you do not take at least one not take at least one of these steps, process semantics will prevent
of these steps, Windows process semantics will prevent OCRmyPDF from working OCRmyPDF from working correctly.
correctly.
Logging Logging
------- -------
+1 -1
View File
@@ -127,7 +127,7 @@ Users may need to customize the script to meet their requirements.
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)" "OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``" "OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``"
"OCR_DESKEW", "Apply deskew to crooked input PDFs" "OCR_DESKEW", "Apply deskew to crooked input PDFs"
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={"rotate_pages": true}'``. "OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true}'``."
"OCR_POLL_NEW_FILE_SECONDS", "Polling interval" "OCR_POLL_NEW_FILE_SECONDS", "Polling interval"
"OCR_LOGLEVEL", "Level of log messages to report" "OCR_LOGLEVEL", "Level of log messages to report"
+2
View File
@@ -1,3 +1,5 @@
.. _docker:
===================== =====================
OCRmyPDF Docker image OCRmyPDF Docker image
===================== =====================
+1 -1
View File
@@ -1,7 +1,7 @@
OCRmyPDF documentation OCRmyPDF documentation
====================== ======================
OCRmyPDF adds an optical charcter recognition (OCR) text layer to scanned PDF OCRmyPDF adds an optical character recognition (OCR) text layer to scanned PDF
files, allowing them to be searched. files, allowing them to be searched.
PDF is the best format for storing and exchanging scanned documents. PDF is the best format for storing and exchanging scanned documents.
+2 -2
View File
@@ -604,7 +604,7 @@ However, the OCR-to-text-layer functionality is available.
Docker Docker
------ ------
You can also :ref:`Install the Docker <docker-install>` container on Windows. Ensure that You can also :ref:`Install the Docker <docker>` container on Windows. Ensure that
your command prompt can run the docker "hello world" container. your command prompt can run the docker "hello world" container.
Installing on FreeBSD Installing on FreeBSD
@@ -630,7 +630,7 @@ Installing the Docker image
For some users, installing the Docker image will be easier than For some users, installing the Docker image will be easier than
installing all of OCRmyPDF's dependencies. installing all of OCRmyPDF's dependencies.
See `OCRmyPDF Docker Image <docker>`__ for more information. See :ref:`docker` for more information.
Installing with Python pip Installing with Python pip
========================== ==========================
+32
View File
@@ -12,6 +12,38 @@ may be unreliable. Use the API to depend on precise behavior.
The public API may be useful in scripts that launch OCRmyPDF processes or that The public API may be useful in scripts that launch OCRmyPDF processes or that
wish to use some of its features for working with PDFs. wish to use some of its features for working with PDFs.
v11.3.4
=======
- Fixed an error message 'called readLinearizationData for file that is not
linearized' that may occur when pikepdf 2.1.0 is used. (Upgrading to pikepdf
2.1.1 also fixes the issue.)
- File watcher now automatically includes ``.PDF`` in addition to ``.pdf`` to
better support case sensitive file systems.
- Some documentation and comment improvements.
v11.3.3
=======
- If unpaper outputs non-UTF-8 data, quietly fix this rather than choke on the
conversion. (Possibly addresses #671.)
v11.3.2
=======
- Explicitly require pikepdf 2.0.0 or newer when running on Python 3.9. (There are
concerns about the stability of pybind11 2.5.x with Python 3.9, which is used in
pikepdf 1.x.)
- Fixed another issue related to page rotation.
- Fixed an issue where image marked as image masks were not properly considered
as optimization candidates.
- On some systems, unpaper seems to be unable to process the PNGs we offer it
as input. We now convert the input to PNM format, which unpaper always accepts.
Fixes #665 and #667.
- DPI sent to unpaper is now rounded to a more reasonable number of decimal digits.
- Debug and error messages from unpaper were being suppressed.
- Some documentation tweaks.
v11.3.1 v11.3.1
======= =======
+1 -1
View File
@@ -45,7 +45,7 @@ OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1')) POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
USE_POLLING = bool(os.getenv('OCR_USE_POLLING', '')) USE_POLLING = bool(os.getenv('OCR_USE_POLLING', ''))
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO').upper() LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO').upper()
PATTERNS = ['*.pdf'] PATTERNS = ['*.pdf', '*.PDF']
log = logging.getLogger('ocrmypdf-watcher') log = logging.getLogger('ocrmypdf-watcher')
+7 -7
View File
@@ -1,12 +1,12 @@
# requirements.txt can be used to replicate the developer's build environment # requirements.txt can be used to replicate the developer's build environment
# setup.py lists a separate set of requirements that are looser to simplify # setup.py lists a separate set of requirements that are looser to simplify
# installation # installation
cffi == 1.14.0 cffi == 1.14.3
coloredlogs == 14.0 # technically optional coloredlogs == 14.0 # technically optional
img2pdf == 0.3.6 img2pdf == 0.4.0
pdfminer.six == 20200517 pdfminer.six == 20201018
pikepdf == 1.16.1 pikepdf == 2.0.0
pluggy == 0.13.1 pluggy == 0.13.1
Pillow == 7.1.2 Pillow == 8.0.1
reportlab == 3.5.42 reportlab == 3.5.55
tqdm == 4.46.1 tqdm == 4.51.0
+2 -1
View File
@@ -73,7 +73,8 @@ setup(
'coloredlogs >= 14.0', # strictly optional 'coloredlogs >= 14.0', # strictly optional
'img2pdf >= 0.3.0, < 0.5', # pure Python, so track HEAD closely 'img2pdf >= 0.3.0, < 0.5', # pure Python, so track HEAD closely
'pdfminer.six >= 20191110, != 20200720, <= 20201018', 'pdfminer.six >= 20191110, != 20200720, <= 20201018',
'pikepdf >= 1.14.0, < 3', "pikepdf >= 1.14.0, < 3 ; python_version < '3.9'",
"pikepdf >= 2.0.0 ; python_version >= '3.9'",
'Pillow >= 7.0.0', 'Pillow >= 7.0.0',
'pluggy >= 0.13.0, < 1.0', 'pluggy >= 0.13.0, < 1.0',
'reportlab >= 3.3.0', # oldest released version with sane image handling 'reportlab >= 3.3.0', # oldest released version with sane image handling
+18 -16
View File
@@ -25,24 +25,24 @@ from ocrmypdf.subprocess import get_version, run
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
missing_gs_error = """
---------------------------------------------------------------------
This error normally occurs when ocrmypdf find can't Ghostscript.
Please ensure Ghostscript is installed and its location is added to
the system PATH environment variable.
For details see:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
_gswin = None _gswin = None
if os.name == 'nt': if os.name == 'nt':
_gswin = which('gswin64c') _gswin = which('gswin64c')
if not _gswin: if not _gswin:
_gswin = which('gswin32c') _gswin = which('gswin32c')
if not _gswin: if not _gswin:
raise MissingDependencyError( raise MissingDependencyError(missing_gs_error)
"""
---------------------------------------------------------------------
This error normally occurs when ocrmypdf can't Ghostscript. Please
ensure Ghostscript is installed and its location is added to the
system PATH environment variable.
For details see:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
)
_gswin = Path(_gswin).stem _gswin = Path(_gswin).stem
GS = _gswin if _gswin else 'gs' GS = _gswin if _gswin else 'gs'
@@ -122,8 +122,6 @@ def rasterize_pdf(
stderr = p.stderr.decode(errors='replace') stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr): if _gs_error_reported(stderr):
log.error(stderr) log.error(stderr)
elif stderr:
log.debug(stderr)
with Image.open(BytesIO(p.stdout)) as im: with Image.open(BytesIO(p.stdout)) as im:
if rotation is not None: if rotation is not None:
@@ -148,6 +146,9 @@ def generate_pdfa(
pdf_version: str = '1.5', pdf_version: str = '1.5',
pdfa_part: str = '2', pdfa_part: str = '2',
): ):
# Ghostscript's compression is all or nothing. We can either force all images
# to JPEG, force all to Flate/PNG, or let it decide how to encode the images.
# In most case it's best to let it decide.
compression_args = [] compression_args = []
if compression == 'jpeg': if compression == 'jpeg':
compression_args = [ compression_args = [
@@ -175,8 +176,9 @@ def generate_pdfa(
strategy = 'RGB' if version() >= '9.19' else '/RGB' strategy = 'RGB' if version() >= '9.19' else '/RGB'
if version() == '9.23': if version() == '9.23':
# 9.23: new feature JPEG passthrough is broken in some cases, best to # 9.23: added JPEG passthrough as a new feature, but with a bug that
# disable it always # incorrectly formats some images. Fixed as of 9.24. So we disable this
# feature for 9.23.
# https://bugs.ghostscript.com/show_bug.cgi?id=699216 # https://bugs.ghostscript.com/show_bug.cgi?id=699216
compression_args.append('-dPassThroughJPEGImages=false') compression_args.append('-dPassThroughJPEGImages=false')
+1 -3
View File
@@ -99,9 +99,7 @@ def get_languages():
args_tess = ['tesseract', '--list-langs'] args_tess = ['tesseract', '--list-langs']
try: try:
proc = run( proc = run(args_tess, text=True, stdout=PIPE, stderr=STDOUT, check=True)
args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT, check=True
)
output = proc.stdout output = proc.stdout
except CalledProcessError as e: except CalledProcessError as e:
raise MissingDependencyError(lang_error(e.output)) from e raise MissingDependencyError(lang_error(e.output)) from e
+42 -38
View File
@@ -13,10 +13,11 @@
import logging import logging
import os import os
import shlex import shlex
from decimal import Decimal
from pathlib import Path from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError from subprocess import PIPE, STDOUT
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Tuple from typing import List, Optional, Tuple, Union
from PIL import Image from PIL import Image
@@ -24,10 +25,12 @@ from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.subprocess import get_version from ocrmypdf.subprocess import get_version
from ocrmypdf.subprocess import run as external_run from ocrmypdf.subprocess import run as external_run
DecFloat = Union[Decimal, float]
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def version(): def version() -> str:
return get_version('unpaper') return get_version('unpaper')
@@ -53,23 +56,25 @@ def _setup_unpaper_io(tmpdir: Path, input_file: Path) -> Tuple[Path, Path]:
except KeyError: except KeyError:
raise MissingDependencyError( raise MissingDependencyError(
"Failed to convert image to a supported format." "Failed to convert image to a supported format."
) from e ) from None
if im_modified or input_file.suffix != '.png': if im_modified or input_file.suffix != '.pnm':
input_png = tmpdir / 'input.png' input_pnm = tmpdir / 'input.pnm'
im.save(input_png, format='PNG', compress_level=1) im.save(input_pnm, format='PPM')
else: else:
# No changes, PNG input, just use the file we already have # No changes, PNG input, just use the file we already have
input_png = input_file input_pnm = input_file
output_pnm = tmpdir / f'output{suffix}' output_pnm = tmpdir / f'output{suffix}'
return input_png, output_pnm return input_pnm, output_pnm
def run(input_file, output_file, dpi, mode_args): def run(
args_unpaper = ['unpaper', '-v', '--dpi', str(dpi)] + mode_args input_file: Path, output_file: Path, dpi: DecFloat, mode_args: List[str]
) -> None:
args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
input_png, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file) input_pnm, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file)
# To prevent any shenanigans from accepting arbitrary parameters in # To prevent any shenanigans from accepting arbitrary parameters in
# --unpaper-args, we: # --unpaper-args, we:
@@ -78,41 +83,40 @@ def run(input_file, output_file, dpi, mode_args):
# 3) append absolute paths for the input and output file # 3) append absolute paths for the input and output file
# This should ensure that a user cannot clobber some other file with # This should ensure that a user cannot clobber some other file with
# their unpaper arguments (whether intentionally or otherwise) # their unpaper arguments (whether intentionally or otherwise)
args_unpaper.extend([os.fspath(input_png), os.fspath(output_pnm)]) args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)])
external_run(
args_unpaper,
close_fds=True,
check=True,
stderr=STDOUT, # unpaper writes logging output to stdout and stderr
stdout=PIPE, # and cannot send file output to stdout
cwd=tmpdir,
logs_errors_to_stdout=True,
)
try: try:
proc = external_run( with Image.open(output_pnm) as imout:
args_unpaper, imout.save(output_file, dpi=(dpi, dpi))
check=True, except (FileNotFoundError, OSError):
close_fds=True, raise SubprocessOutputError(
universal_newlines=True, "unpaper: failed to produce the expected output file. "
stderr=STDOUT, # unpaper writes logging output to stdout and stderr + " Called with: "
cwd=tmpdir, # and cannot send file output to stdout + str(args_unpaper)
stdout=PIPE, ) from None
)
except CalledProcessError as e:
log.debug(e.stderr)
raise e from e
else:
log.debug(proc.stderr)
try:
with Image.open(output_pnm) as imout:
imout.save(output_file, dpi=(dpi, dpi))
except (FileNotFoundError, OSError):
raise SubprocessOutputError(
"unpaper: failed to produce the expected output file. "
+ " Called with: "
+ str(args_unpaper)
) from None
def validate_custom_args(args: str): def validate_custom_args(args: str) -> List[str]:
unpaper_args = shlex.split(args) unpaper_args = shlex.split(args)
if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args): if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args):
raise ValueError('No filenames allowed in --unpaper-args') raise ValueError('No filenames allowed in --unpaper-args')
return unpaper_args return unpaper_args
def clean(input_file, output_file, dpi, unpaper_args=None): def clean(
input_file: Path,
output_file: Path,
dpi: DecFloat,
unpaper_args: Optional[List[str]] = None,
):
default_args = [ default_args = [
'--layout', '--layout',
'none', 'none',
-3
View File
@@ -536,9 +536,6 @@ def create_ocr_image(image: Path, page_context: PageContext):
# Pillow requires integer DPI # Pillow requires integer DPI
dpi = tuple(round(coord) for coord in im.info['dpi']) dpi = tuple(round(coord) for coord in im.info['dpi'])
if page_context.pageinfo.rotation != 0:
log.info(f"Rotating {page_context.pageinfo.rotation}")
im = im.rotate(page_context.pageinfo.rotation)
im.save(output_file, dpi=dpi) im.save(output_file, dpi=dpi)
return output_file return output_file
+1 -1
View File
@@ -82,7 +82,7 @@ def _setup_plugins(
pm.register(module) pm.register(module)
def get_plugin_manager(plugins: List[str], builtins=True): def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
pm = OcrmypdfPluginManager( pm = OcrmypdfPluginManager(
project_name='ocrmypdf', project_name='ocrmypdf',
setup_func=partial(_setup_plugins, plugins=plugins, builtins=builtins), setup_func=partial(_setup_plugins, plugins=plugins, builtins=builtins),
+8 -6
View File
@@ -55,6 +55,7 @@ from ocrmypdf._validation import (
) )
from ocrmypdf.exceptions import ExitCode, ExitCodeException from ocrmypdf.exceptions import ExitCode, ExitCodeException
from ocrmypdf.helpers import ( from ocrmypdf.helpers import (
NeverRaise,
available_cpu_count, available_cpu_count,
check_pdf, check_pdf,
pikepdf_enable_mmap, pikepdf_enable_mmap,
@@ -301,13 +302,14 @@ def exec_concurrent(context: PdfContext):
copy_final(pdf, options.output_file, context) copy_final(pdf, options.output_file, context)
class NeverRaise(Exception): def configure_debug_logging(log_filename, prefix: str = ''):
"""An exception that is never raised""" """
Create a debug log file at a specified location.
pass # pylint: disable=unnecessary-pass Arguments:
log_filename: Where to the put the log file.
prefix: The logging domain prefix that should be sent to the log.
def configure_debug_logging(log_filename, prefix=''): """
log_file_handler = logging.FileHandler(log_filename, delay=True) log_file_handler = logging.FileHandler(log_filename, delay=True)
log_file_handler.setLevel(logging.DEBUG) log_file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter( formatter = logging.Formatter(
+15 -14
View File
@@ -10,7 +10,7 @@ import os
import sys import sys
from enum import IntEnum from enum import IntEnum
from pathlib import Path from pathlib import Path
from typing import BinaryIO, Iterable, Union from typing import AnyStr, BinaryIO, Iterable, Optional, Union
from warnings import warn from warnings import warn
from ocrmypdf._logging import PageNumberFilter, TqdmConsole from ocrmypdf._logging import PageNumberFilter, TqdmConsole
@@ -26,7 +26,8 @@ except ModuleNotFoundError:
coloredlogs = None coloredlogs = None
PathOrIO = Union[BinaryIO, os.PathLike, str, bytes] StrPath = Union[os.PathLike, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
class Verbosity(IntEnum): class Verbosity(IntEnum):
@@ -202,7 +203,7 @@ def ocr( # pylint: disable=unused-argument
language: Iterable[str] = None, language: Iterable[str] = None,
image_dpi: int = None, image_dpi: int = None,
output_type=None, output_type=None,
sidecar: os.PathLike = None, sidecar: Optional[StrPath] = None,
jobs: int = None, jobs: int = None,
use_threads: bool = None, use_threads: bool = None,
title: str = None, title: str = None,
@@ -239,7 +240,7 @@ def ocr( # pylint: disable=unused-argument
user_words: os.PathLike = None, user_words: os.PathLike = None,
user_patterns: os.PathLike = None, user_patterns: os.PathLike = None,
fast_web_view: float = None, fast_web_view: float = None,
plugins: Iterable[Union[str, Path]] = None, plugins: Iterable[StrPath] = None,
keep_temporary_files: bool = None, keep_temporary_files: bool = None,
progress_bar: bool = None, progress_bar: bool = None,
**kwargs, **kwargs,
@@ -261,7 +262,7 @@ def ocr( # pylint: disable=unused-argument
read. read.
output_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is output_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is
interpreted as file system path to the output file. If the object interpreted as file system path to the output file. If the object
appears to be a writable stream (with methods such as ``.read()`` and appears to be a writable stream (with methods such as ``.write()`` and
``.seek()``), the output will be written to this stream. If ``.seek()``), the output will be written to this stream. If
``output_file`` is ``"-"``, the output will be written to ``sys.stdout`` ``output_file`` is ``"-"``, the output will be written to ``sys.stdout``
(provided that standard output does not seem to be a terminal device). (provided that standard output does not seem to be a terminal device).
@@ -298,18 +299,18 @@ def ocr( # pylint: disable=unused-argument
else: else:
plugins = list(plugins) plugins = list(plugins)
parser = get_parser() # No new variable names should be assigned until these two steps are run
_plugin_manager = get_plugin_manager(plugins) create_options_kwargs = {k: v for k, v in locals().items() if k != 'kwargs'}
_plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
create_options_kwargs = {
k: v for k, v in locals().items() if not k.startswith('_') and k != 'kwargs'
}
create_options_kwargs.update(kwargs) create_options_kwargs.update(kwargs)
parser = get_parser()
create_options_kwargs['parser'] = parser
plugin_manager = get_plugin_manager(plugins)
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
if 'verbose' in kwargs: if 'verbose' in kwargs:
warn("ocrmypdf.ocr(verbose=) is ignored. Use ocrmypdf.configure_logging().") warn("ocrmypdf.ocr(verbose=) is ignored. Use ocrmypdf.configure_logging().")
options = create_options(**create_options_kwargs) options = create_options(**create_options_kwargs)
check_options(options, _plugin_manager) check_options(options, plugin_manager)
return run_pipeline(options=options, plugin_manager=_plugin_manager, api=True) return run_pipeline(options=options, plugin_manager=plugin_manager, api=True)
+17 -5
View File
@@ -58,6 +58,10 @@ class Resolution(namedtuple('Resolution', ('x', 'y'))):
return f"Resolution({self.x}x{self.y} dpi)" return f"Resolution({self.x}x{self.y} dpi)"
class NeverRaise(Exception):
"""An exception that is never raised"""
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike): def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
""" """
Helper function: relinks soft symbolic link if necessary Helper function: relinks soft symbolic link if necessary
@@ -186,17 +190,25 @@ def check_pdf(input_file: Path) -> bool:
log.warning(msg) log.warning(msg)
sio = StringIO() sio = StringIO()
linearize = None linearize_msgs = ''
try: try:
# If linearization is missing entirely, we do not complain. We do
# complain if linearization is present but incorrect.
pdf.check_linearization(sio) pdf.check_linearization(sio)
except RuntimeError: except RuntimeError:
pass pass
except (
getattr(pikepdf, 'ForeignObjectError')
if pikepdf.__version__ == '2.1.0' # This version may throw wrong exception
else NeverRaise
):
pass
else: else:
linearize = sio.getvalue() linearize_msgs = sio.getvalue()
if linearize: if linearize_msgs:
log.warning(linearize) log.warning(linearize_msgs)
if not messages and not linearize: if not messages and not linearize_msgs:
return True return True
return False return False
finally: finally:
+23 -19
View File
@@ -115,25 +115,29 @@ def extract_image_jbig2(
and filtdp[0] != Name.JBIG2Decode and filtdp[0] != Name.JBIG2Decode
and jbig2enc.available() and jbig2enc.available()
): ):
try: # Save any colorspace associated with the image, so that we
# Save any colorspace associated with the image, so that we # will export a pure 1-bit PNG with no palette or ICC profile.
# will export a pure 1-bit PNG with no palette or ICC profile. # Showing the palette or ICC to jbig2enc will cause it to perform
# Showing the palette or ICC to jbig2enc will cause it to perform # colorspace transform to 1bpp, which will conflict the palette or
# colorspace transform to 1bpp, which will conflict the palette or # ICC if it exists.
# ICC if it exists. colorspace = pim.obj.get(pikepdf.Name.ColorSpace, None)
colorspace = pim.obj.ColorSpace if colorspace is not None or pim.image_mask:
# Set to DeviceGray temporarily; we already in 1 bpc. try:
pim.obj.ColorSpace = pikepdf.Name.DeviceGray # Set to DeviceGray temporarily; we already in 1 bpc.
imgname = root / f'{xref:08d}' pim.obj.ColorSpace = pikepdf.Name.DeviceGray
with imgname.open('wb') as f: imgname = root / f'{xref:08d}'
ext = pim.extract_to(stream=f) with imgname.open('wb') as f:
imgname.rename(imgname.with_suffix(ext)) ext = pim.extract_to(stream=f)
except pikepdf.UnsupportedImageTypeError: imgname.rename(imgname.with_suffix(ext))
return None except pikepdf.UnsupportedImageTypeError:
finally: return None
# Restore image colorspace after temporarily setting it to DeviceGray finally:
pim.obj.ColorSpace = colorspace # Restore image colorspace after temporarily setting it to DeviceGray
return XrefExt(xref, ext) if colorspace is not None:
pim.obj.ColorSpace = colorspace
else:
del pim.obj.ColorSpace
return XrefExt(xref, ext)
return None return None
+4 -4
View File
@@ -818,12 +818,12 @@ class PdfInfo:
check_pages=check_pages, check_pages=check_pages,
detailed_analysis=detailed_analysis, detailed_analysis=detailed_analysis,
) )
self._needs_rendering = pdf.root.get('/NeedsRendering', False) self._needs_rendering = pdf.Root.get('/NeedsRendering', False)
self._has_acroform = False self._has_acroform = False
if '/AcroForm' in pdf.root: if '/AcroForm' in pdf.Root:
if len(pdf.root.AcroForm.get('/Fields', [])) > 0: if len(pdf.Root.AcroForm.get('/Fields', [])) > 0:
self._has_acroform = True self._has_acroform = True
elif '/XFA' in pdf.root.AcroForm: elif '/XFA' in pdf.Root.AcroForm:
self._has_acroform = True self._has_acroform = True
@property @property
+27 -10
View File
@@ -25,13 +25,21 @@ from ocrmypdf.exceptions import MissingDependencyError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def run(args, *, env=None, **kwargs): def run(args, *, env=None, logs_errors_to_stdout=False, **kwargs):
"""Wrapper around :py:func:`subprocess.run` """Wrapper around :py:func:`subprocess.run`
The main purpose of this wrapper is to log subprocess output in an orderly The main purpose of this wrapper is to log subprocess output in an orderly
fashion that indentifies the responsible subprocess. An additional fashion that indentifies the responsible subprocess. An additional
task is that this function goes to greater lengths to find possible Windows task is that this function goes to greater lengths to find possible Windows
locations of our dependencies when they are not on the system PATH. locations of our dependencies when they are not on the system PATH.
Arguments should be identical to ``subprocess.run``, except for following:
Arguments:
logs_errors_to_stdout: If True, indicates that the process writes its error
messages to stdout rather than stderr, so stdout should be logged
if there is an error. If False, stderr is logged. Could be used with
stderr=STDOUT, stdout=PIPE for example.
""" """
if not env: if not env:
env = os.environ env = os.environ
@@ -43,25 +51,34 @@ def run(args, *, env=None, **kwargs):
args = _fix_windows_args(program, args, env) args = _fix_windows_args(program, args, env)
log.debug("Running: %s", args) log.debug("Running: %s", args)
process_log = log.getChild('subprocess.' + os.path.basename(program)) process_log = log.getChild(os.path.basename(program))
if sys.version_info < (3, 7) and os.name == 'nt': if sys.version_info < (3, 7):
# Can't use close_fds=True on Windows with Python 3.6 or older if os.name == 'nt':
# https://bugs.python.org/issue19575, etc. # Can't use close_fds=True on Windows with Python 3.6 or older
kwargs['close_fds'] = False # https://bugs.python.org/issue19575, etc.
kwargs['close_fds'] = False
if 'text' in kwargs:
# Convert run(...text=) to run(...universal_newlines=) for Python 3.6
kwargs['universal_newlines'] = kwargs['text']
del kwargs['text']
stderr = None stderr = None
stderr_name = 'stderr' if not logs_errors_to_stdout else 'stdout'
try: try:
proc = subprocess_run(args, env=env, **kwargs) proc = subprocess_run(args, env=env, **kwargs)
except CalledProcessError as e: except CalledProcessError as e:
stderr = getattr(e, 'stderr', None) stderr = getattr(e, stderr_name, None)
raise raise
else: else:
stderr = getattr(proc, 'stderr', None) stderr = getattr(proc, stderr_name, None)
finally: finally:
if process_log.isEnabledFor(logging.DEBUG) and stderr: if process_log.isEnabledFor(logging.DEBUG) and stderr:
with suppress(AttributeError, UnicodeDecodeError): with suppress(AttributeError, UnicodeDecodeError):
stderr = stderr.decode('utf-8', 'replace') stderr = stderr.decode('utf-8', 'replace')
process_log.debug("stderr = %s", stderr) if logs_errors_to_stdout:
process_log.debug("stdout/stderr = %s", stderr)
else:
process_log.debug("stderr = %s", stderr)
return proc return proc
@@ -107,7 +124,7 @@ def get_version(
proc = run( proc = run(
args_prog, args_prog,
close_fds=True, close_fds=True,
universal_newlines=True, text=True,
stdout=PIPE, stdout=PIPE,
stderr=STDOUT, stderr=STDOUT,
check=True, check=True,
+2 -2
View File
@@ -128,7 +128,7 @@ def run_ocrmypdf_api(input_file, output_file, *args):
@pytest.helpers.register @pytest.helpers.register
def run_ocrmypdf(input_file, output_file, *args, universal_newlines=True): def run_ocrmypdf(input_file, output_file, *args, text=True):
"Run ocrmypdf and let caller deal with results" "Run ocrmypdf and let caller deal with results"
p_args = ( p_args = (
@@ -151,7 +151,7 @@ def run_ocrmypdf(input_file, output_file, *args, universal_newlines=True):
p_args, p_args,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
universal_newlines=universal_newlines, universal_newlines=text, # When dropping support for Python 3.6 change to text=
env=env, env=env,
check=False, check=False,
) )
+2 -2
View File
@@ -598,7 +598,7 @@ def test_compression_preserved(ocrmypdf_exec, resources, image, outpdf):
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
stdin=input_stream, stdin=input_stream,
universal_newlines=True, universal_newlines=True, # When dropping support for Python 3.6 change to text=
check=False, check=False,
) )
@@ -659,7 +659,7 @@ def test_compression_changed(ocrmypdf_exec, resources, image, compression, outpd
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
stdin=input_stream, stdin=input_stream,
universal_newlines=True, universal_newlines=True, # When dropping support for Python 3.6 change to text=
check=False, check=False,
) )
assert p.returncode == ExitCode.ok, p.stderr assert p.returncode == ExitCode.ok, p.stderr
+2 -2
View File
@@ -302,8 +302,8 @@ def test_kodak_toc(resources, outpdf):
p = pikepdf.open(outpdf) p = pikepdf.open(outpdf)
if pikepdf.Name.First in p.root.Outlines: if pikepdf.Name.First in p.Root.Outlines:
assert isinstance(p.root.Outlines.First, pikepdf.Dictionary) assert isinstance(p.Root.Outlines.First, pikepdf.Dictionary)
def test_metadata_fixup_warning(resources, outdir, caplog): def test_metadata_fixup_warning(resources, outdir, caplog):
+1 -1
View File
@@ -241,7 +241,7 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir):
'--rotate-pages', '--rotate-pages',
'--rotate-pages-threshold', '--rotate-pages-threshold',
'0.001', '0.001',
universal_newlines=False, text=False,
) )
err = err.decode('utf-8', errors='replace') err = err.decode('utf-8', errors='replace')
assert p.returncode == 0, err assert p.returncode == 0, err
+2 -2
View File
@@ -137,9 +137,9 @@ def test_report_file_size(tmp_path, caplog):
caplog.clear() caplog.clear()
waste_of_space = b'Dummy' * 5000 waste_of_space = b'Dummy' * 5000
pdf.root.Dummy = waste_of_space pdf.Root.Dummy = waste_of_space
pdf.save(in_) pdf.save(in_)
pdf.root.Dummy2 = waste_of_space + waste_of_space pdf.Root.Dummy2 = waste_of_space + waste_of_space
pdf.save(out) pdf.save(out)
with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch( with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch(