Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be9a68c5e | ||
|
|
6c34d59836 | ||
|
|
386453d178 | ||
|
|
615a7561b5 | ||
|
|
c4c64c3ea0 | ||
|
|
21279f5784 | ||
|
|
a63a21a7fc | ||
|
|
1c4d5d79f7 | ||
|
|
644581ed3c | ||
|
|
77f7621bbc | ||
|
|
42713b77d7 | ||
|
|
690f88119d | ||
|
|
78f391536b | ||
|
|
7bdd1828a9 | ||
|
|
a8f513eeeb | ||
|
|
af18bc0684 | ||
|
|
b621df6947 | ||
|
|
313c9e7dc1 | ||
|
|
9d04795f7f | ||
|
|
9a08e71e7f | ||
|
|
790d3022f6 | ||
|
|
ec311af796 | ||
|
|
c725bf79da | ||
|
|
9559f76fae | ||
|
|
45736b7c2b | ||
|
|
5629e960b9 | ||
|
|
79fd8d01a5 | ||
|
|
79fe7a0a85 | ||
|
|
b4b32a35b5 | ||
|
|
4634b3db55 | ||
|
|
f5053158d4 | ||
|
|
dfa4ce1612 | ||
|
|
585595a98e | ||
|
|
f6396fbaac | ||
|
|
3859bae85e | ||
|
|
ee1a7baae7 | ||
|
|
a4da05b66b | ||
|
|
4d67812d51 | ||
|
|
3534742ef9 | ||
|
|
8bfd46c80d | ||
|
|
cc6e9cecc0 | ||
|
|
208657f840 | ||
|
|
f3de980447 | ||
|
|
eb8992e58b | ||
|
|
72ad618ae6 | ||
|
|
f07d0c39bb | ||
|
|
9c5c7d9be0 | ||
|
|
0b19b084e2 |
@@ -6,6 +6,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
- ci
|
- ci
|
||||||
- release/*
|
- release/*
|
||||||
|
- feature/*
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|||||||
+7
-10
@@ -7,31 +7,28 @@ repos:
|
|||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
- repo: https://github.com/asottile/seed-isort-config
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: v2.2.0
|
rev: 5.9.3
|
||||||
hooks:
|
|
||||||
- id: seed-isort-config
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-isort
|
|
||||||
rev: v5.9.3 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
args: ["--profile", "black"]
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 21.7b0
|
rev: 21.9b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python
|
language_version: python
|
||||||
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v1.17.0
|
rev: v1.19.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.24.0
|
rev: v2.29.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: ["--py36-plus"]
|
args: ["--py36-plus"]
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.910
|
rev: v0.910-1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
|||||||
+8
-8
@@ -12,7 +12,7 @@ subprocess call anyway, as this provides isolation of its activities.
|
|||||||
Example
|
Example
|
||||||
=======
|
=======
|
||||||
|
|
||||||
OCRmyPDF one high-level function to run its main engine from an
|
OCRmyPDF provides one high-level function to run its main engine from an
|
||||||
application. The parameters are symmetric to the command line arguments
|
application. The parameters are symmetric to the command line arguments
|
||||||
and largely have the same functions.
|
and largely have the same functions.
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ and largely have the same functions.
|
|||||||
if __name__ == '__main__': # To ensure correct behavior on Windows and macOS
|
if __name__ == '__main__': # To ensure correct behavior on Windows and macOS
|
||||||
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True)
|
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True)
|
||||||
|
|
||||||
With a few exceptions, all of the command line arguments are available
|
With some exceptions, all of the command line arguments are available
|
||||||
and may be passed as equivalent keywords.
|
and may be passed as equivalent keywords.
|
||||||
|
|
||||||
A few differences are that ``verbose`` and ``quiet`` are not available.
|
A few differences are that ``verbose`` and ``quiet`` are not available.
|
||||||
@@ -41,29 +41,29 @@ execution. To do this, it will:
|
|||||||
- manage the signal flags of its worker processes
|
- 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 :func:`ocrmypdf.ocr()` must be sufficiently
|
||||||
privileged to perform these actions.
|
privileged to perform these actions.
|
||||||
|
|
||||||
There currently is no option to manage how jobs are scheduled other
|
There currently is 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.
|
||||||
|
|
||||||
Creating a child process to call ``ocrmypdf.ocr()`` is suggested. That
|
Creating a child process to call :func:`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 fails for any reason.
|
OCRmyPDF fails for any reason.
|
||||||
|
|
||||||
Programs that call ``ocrmypdf.ocr()`` should also install a SIGBUS signal
|
Programs that call :func:`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
|
||||||
mapped file fails. OCRmyPDF may use memory mapping.
|
mapped file fails. OCRmyPDF may use memory mapping.
|
||||||
|
|
||||||
``ocrmypdf.ocr()`` will take a threading lock to prevent multiple runs of itself
|
:func:`ocrmypdf.ocr()` will take a threading lock to prevent multiple runs of itself
|
||||||
in the same Python interpreter process. This is not thread-safe, because of how
|
in the same Python interpreter process. This is not thread-safe, because of how
|
||||||
OCRmyPDF's plugins and Python's library import system work. If you need to parallelize
|
OCRmyPDF's plugins and Python's library import system work. If you need to parallelize
|
||||||
OCRmyPDF, use processes.
|
OCRmyPDF, use processes.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
On Windows and macOS, the script that calls ``ocrmypdf.ocr()`` must be
|
On Windows and macOS, the script that calls :func:`ocrmypdf.ocr()` must be
|
||||||
protected by an "ifmain" guard (``if __name__ == '__main__'``). If you do
|
protected by an "ifmain" guard (``if __name__ == '__main__'``). If you do
|
||||||
not take at least one of these steps, process semantics will prevent
|
not take at least one of these steps, process semantics will prevent
|
||||||
OCRmyPDF from working correctly.
|
OCRmyPDF from working correctly.
|
||||||
@@ -96,7 +96,7 @@ Exceptions
|
|||||||
|
|
||||||
OCRmyPDF may throw standard Python exceptions, ``ocrmypdf.exceptions.*``
|
OCRmyPDF may throw standard Python exceptions, ``ocrmypdf.exceptions.*``
|
||||||
exceptions, some exceptions related to multiprocessing, and
|
exceptions, some exceptions related to multiprocessing, and
|
||||||
``KeyboardInterrupt``. The parent process should provide an exception
|
:exc:`KeyboardInterrupt`. The parent process should provide an exception
|
||||||
handler. OCRmyPDF will clean up its temporary files and worker processes
|
handler. OCRmyPDF will clean up its temporary files and worker processes
|
||||||
automatically when an exception occurs.
|
automatically when an exception occurs.
|
||||||
|
|
||||||
|
|||||||
+11
-5
@@ -31,9 +31,16 @@
|
|||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ['sphinx.ext.napoleon', 'sphinx_issues']
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.autosummary',
|
||||||
|
'sphinx.ext.napoleon',
|
||||||
|
'sphinx_issues',
|
||||||
|
]
|
||||||
|
|
||||||
# Extension settings
|
# Extension settings
|
||||||
|
intersphinx_mapping = {'https://docs.python.org/': None}
|
||||||
napoleon_use_rtype = False
|
napoleon_use_rtype = False
|
||||||
issues_github_path = "jbarlow83/OCRmyPDF"
|
issues_github_path = "jbarlow83/OCRmyPDF"
|
||||||
|
|
||||||
@@ -56,7 +63,7 @@ master_doc = 'index'
|
|||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'ocrmypdf'
|
project = 'ocrmypdf'
|
||||||
copyright = (
|
copyright = (
|
||||||
'2020, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
'2021, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
||||||
)
|
)
|
||||||
author = 'James R. Barlow'
|
author = 'James R. Barlow'
|
||||||
|
|
||||||
@@ -88,11 +95,10 @@ if on_rtd:
|
|||||||
]
|
]
|
||||||
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
||||||
|
|
||||||
|
from importlib_metadata import version as package_version
|
||||||
from pkg_resources import get_distribution, DistributionNotFound
|
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = get_distribution('ocrmypdf').version
|
release = package_version('ocrmypdf')
|
||||||
version = '.'.join(release.split('.')[:2])
|
version = '.'.join(release.split('.')[:2])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
OCRmyPDF documentation
|
OCRmyPDF documentation
|
||||||
======================
|
======================
|
||||||
|
|
||||||
|
.. figure:: images/logo.svg
|
||||||
|
|
||||||
OCRmyPDF adds an optical character 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.
|
||||||
|
|
||||||
|
|||||||
+14
-9
@@ -2,7 +2,12 @@
|
|||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
OCRmyPDF is a Python 3 application and library that adds OCR layers to PDFs.
|
OCRmyPDF is an application and library that adds text "layers" to images
|
||||||
|
in PDFs, making scanned image PDFs searchable. It uses OCR to guess what text
|
||||||
|
is contained in images. It is written in Python. OCRmyPDF supports plugins
|
||||||
|
that allow customization of its processing steps, and is very tolerant of
|
||||||
|
PDFs that contain scanned images and "born digital" content that needs no
|
||||||
|
text recognition.
|
||||||
|
|
||||||
About OCR
|
About OCR
|
||||||
=========
|
=========
|
||||||
@@ -26,7 +31,7 @@ exactly. They contain `vector
|
|||||||
graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`__
|
graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`__
|
||||||
that can contain raster objects such as scanned images. Because PDFs can
|
that can contain raster objects such as scanned images. Because PDFs can
|
||||||
contain multiple pages (unlike many image formats) and can contain fonts
|
contain multiple pages (unlike many image formats) and can contain fonts
|
||||||
and text, it is a good formats for exchanging scanned documents.
|
and text, it is a good format for exchanging scanned documents.
|
||||||
|
|
||||||
|image|
|
|image|
|
||||||
|
|
||||||
@@ -35,9 +40,9 @@ have one image. Some scanners or scanning software will segment pages
|
|||||||
into monochromatic text and color regions for example, to improve the
|
into monochromatic text and color regions for example, to improve the
|
||||||
compression ratio and appearance of the page.
|
compression ratio and appearance of the page.
|
||||||
|
|
||||||
Rasterizing a PDF is the process of generating an image suitable for
|
Rasterizing a PDF is the process of generating corresponding raster images.
|
||||||
display or analyzing with an OCR engine. OCR engines like Tesseract work
|
OCR engines like Tesseract work with images, not scalable vector graphics
|
||||||
with images, not vector objects.
|
or mixed raster-vector-text graphics such as PDF.
|
||||||
|
|
||||||
About PDF/A
|
About PDF/A
|
||||||
===========
|
===========
|
||||||
@@ -76,7 +81,7 @@ OCRmyPDF analyzes each page of a PDF to determine the colorspace and
|
|||||||
resolution (DPI) needed to capture all of the information on that page
|
resolution (DPI) needed to capture all of the information on that page
|
||||||
without losing content. It uses
|
without losing content. It uses
|
||||||
`Ghostscript <http://ghostscript.com/>`__ to rasterize the page, and
|
`Ghostscript <http://ghostscript.com/>`__ to rasterize the page, and
|
||||||
then performs on OCR on the rasterized image to create an OCR "layer".
|
then performs on OCR the rasterized image to create an OCR "layer".
|
||||||
The layer is then grafted back onto the original PDF.
|
The layer is then grafted back onto the original PDF.
|
||||||
|
|
||||||
While one can use a program like Ghostscript or ImageMagick to get an
|
While one can use a program like Ghostscript or ImageMagick to get an
|
||||||
@@ -84,9 +89,9 @@ image and put the image through Tesseract, that actually creates a new
|
|||||||
PDF and many details may be lost. OCRmyPDF can produce a minimally
|
PDF and many details may be lost. OCRmyPDF can produce a minimally
|
||||||
changed PDF as output.
|
changed PDF as output.
|
||||||
|
|
||||||
OCRmyPDF also some image processing options like deskew which improve
|
OCRmyPDF also provides some image processing options, like deskew, which
|
||||||
the appearance of files and quality of OCR. When these are used, the OCR
|
improves the appearance of files and quality of OCR. When these are used,
|
||||||
layer is grafted onto the processed image instead.
|
the OCR layer is grafted onto the processed image instead.
|
||||||
|
|
||||||
By default, OCRmyPDF produces archival PDFs – PDF/A, which are a
|
By default, OCRmyPDF produces archival PDFs – PDF/A, which are a
|
||||||
stricter subset of PDF features designed for long term archives. If
|
stricter subset of PDF features designed for long term archives. If
|
||||||
|
|||||||
@@ -12,6 +12,60 @@ 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.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Python 3.6 reaches end of life on December 23, 2021. We will end support
|
||||||
|
for Python 3.6 around that time. The change will be marked with a major
|
||||||
|
release.
|
||||||
|
|
||||||
|
v12.7.2
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Fixed "invalid version number" error for Tesseract packaging with nonstandard
|
||||||
|
version "5.0.0-rc1.20211030".
|
||||||
|
- Fixed use of deprecated ``importlib.resources.read_binary``.
|
||||||
|
- Replace some uses of string paths with ``pathlib.Path``.
|
||||||
|
- Fixed a leaked file handle when using ``--output-type none``.
|
||||||
|
- Removed shims to support versions of pikepdf that are no longer supported.
|
||||||
|
|
||||||
|
v12.7.1
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Declare support for pdfminer.six v20211012.
|
||||||
|
|
||||||
|
v12.7.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Fixed test suite failure when using pikepdf 3.2.0 that was compiled with pybind11
|
||||||
|
2.8.0. :issue:`843`
|
||||||
|
- Improve advice to user about using ``--max-image-mpixels`` if OCR fails for this
|
||||||
|
reason.
|
||||||
|
- Minor documentation fixes. (Thanks to @mara004.)
|
||||||
|
- Don't require importlib-metadata and importlib-resources backports on versions of
|
||||||
|
Python where the standard library implementation is sufficient.
|
||||||
|
(Thanks to Marco Genasci.)
|
||||||
|
|
||||||
|
v12.6.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Implemented ``--output-type=none`` to skip producing PDFs for applications that
|
||||||
|
only want sidecar files (:issue:`787`).
|
||||||
|
- Fixed ambiguities in descriptions of behavior of ``--jbig2-lossy``.
|
||||||
|
- Various improvements to documentation.
|
||||||
|
|
||||||
|
v12.5.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Fixed build failure for the combination of PyPy 3.6 and pikepdf 3.0. This
|
||||||
|
combination can work in a source build but does not work with wheels.
|
||||||
|
- Accepted bot that wanted to upgrade our deprecated requirements.txt.
|
||||||
|
- Documentation updates.
|
||||||
|
- Replace pkg_resources and install dependency on setuptools with
|
||||||
|
importlib-metadata and importlib-resources.
|
||||||
|
- Fixed regression in hocrtransform causing text to be omitted when this
|
||||||
|
renderer was used.
|
||||||
|
- Fixed some typing errors.
|
||||||
|
|
||||||
v12.4.0
|
v12.4.0
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
|||||||
+15
-21
@@ -24,45 +24,39 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
|
|
||||||
# pylint: disable=logging-format-interpolation
|
# pylint: disable=logging-format-interpolation
|
||||||
# pylint: disable=logging-not-lazy
|
# pylint: disable=logging-not-lazy
|
||||||
|
|
||||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
script_dir = Path(__file__).parent
|
||||||
print(script_dir + '/batch.py: Start')
|
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
start_dir = sys.argv[1]
|
start_dir = Path(sys.argv[1])
|
||||||
else:
|
else:
|
||||||
start_dir = '.'
|
start_dir = Path('.')
|
||||||
|
|
||||||
if len(sys.argv) > 2:
|
if len(sys.argv) > 2:
|
||||||
log_file = sys.argv[2]
|
log_file = Path(sys.argv[2])
|
||||||
else:
|
else:
|
||||||
log_file = script_dir + '/ocr-tree.log'
|
log_file = script_dir.with_name('ocr-tree.log')
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s %(message)s',
|
format='%(asctime)s %(message)s',
|
||||||
filename=log_file,
|
filename=log_file,
|
||||||
filemode='w',
|
filemode='a',
|
||||||
)
|
)
|
||||||
|
|
||||||
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
||||||
|
|
||||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
for filename in start_dir.glob("**/*.py"):
|
||||||
logging.info(dir_name + '\n')
|
logging.info(f"Processing {filename}")
|
||||||
os.chdir(dir_name)
|
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||||
for filename in file_list:
|
if result == ocrmypdf.ExitCode.already_done_ocr:
|
||||||
file_ext = os.path.splitext(filename)[1]
|
logging.error("Skipped document because it already contained text")
|
||||||
if file_ext == '.pdf':
|
elif result == ocrmypdf.ExitCode.ok:
|
||||||
full_path = dir_name + '/' + filename
|
logging.info("OCR complete")
|
||||||
print(full_path)
|
logging.info(result)
|
||||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
|
||||||
if result == ocrmypdf.ExitCode.already_done_ocr:
|
|
||||||
print("Skipped document because it already contained text")
|
|
||||||
elif result == ocrmypdf.ExitCode.ok:
|
|
||||||
print("OCR complete")
|
|
||||||
logging.info(result)
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function __fish_ocrmypdf_output_type
|
|||||||
echo -e "pdfa-1\t"(_ "output a PDF/A-1b")
|
echo -e "pdfa-1\t"(_ "output a PDF/A-1b")
|
||||||
echo -e "pdfa-2\t"(_ "output a PDF/A-2b")
|
echo -e "pdfa-2\t"(_ "output a PDF/A-2b")
|
||||||
echo -e "pdfa-3\t"(_ "output a PDF/A-3b")
|
echo -e "pdfa-3\t"(_ "output a PDF/A-3b")
|
||||||
|
echo -e "none\t"(_ "do not produce an output PDF (for example, if you only care about --sidecar)")
|
||||||
end
|
end
|
||||||
complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "select PDF output options"
|
complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "select PDF output options"
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ if len(sys.argv) > 1:
|
|||||||
else:
|
else:
|
||||||
start_dir = '.'
|
start_dir = '.'
|
||||||
|
|
||||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
for dir_name, _subdirs, file_list in os.walk(start_dir):
|
||||||
logging.info(dir_name)
|
logging.info(dir_name)
|
||||||
os.chdir(dir_name)
|
os.chdir(dir_name)
|
||||||
for filename in file_list:
|
for filename in file_list:
|
||||||
|
|||||||
@@ -72,3 +72,11 @@ testpaths = ["tests"]
|
|||||||
addopts = "-n auto"
|
addopts = "-n auto"
|
||||||
markers = ["slow"]
|
markers = ["slow"]
|
||||||
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
|
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
'pluggy', 'tqdm', 'coloredlogs', 'img2pdf', 'cffi', '_cffi_backend', 'pdfminer.*', 'reportlab.*'
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ img2pdf == 0.4.0
|
|||||||
pdfminer.six == 20201018
|
pdfminer.six == 20201018
|
||||||
pikepdf == 2.10.0
|
pikepdf == 2.10.0
|
||||||
pluggy == 0.13.1
|
pluggy == 0.13.1
|
||||||
Pillow == 8.2.0
|
Pillow == 8.3.2
|
||||||
reportlab == 3.5.66
|
reportlab == 3.5.66
|
||||||
tqdm == 4.59.0
|
tqdm == 4.59.0
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ classifiers =
|
|||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
Programming Language :: Python :: 3.8
|
Programming Language :: Python :: 3.8
|
||||||
Programming Language :: Python :: 3.9
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
Topic :: Scientific/Engineering :: Image Recognition
|
Topic :: Scientific/Engineering :: Image Recognition
|
||||||
Topic :: Text Processing :: Indexing
|
Topic :: Text Processing :: Indexing
|
||||||
Topic :: Text Processing :: Linguistic
|
Topic :: Text Processing :: Linguistic
|
||||||
@@ -49,12 +50,14 @@ install_requires =
|
|||||||
cffi>=1.9.1 # must be a setup and install requirement
|
cffi>=1.9.1 # must be a setup and install requirement
|
||||||
coloredlogs>=14.0 # strictly optional
|
coloredlogs>=14.0 # strictly optional
|
||||||
img2pdf>=0.3.0,<0.5 # pure Python
|
img2pdf>=0.3.0,<0.5 # pure Python
|
||||||
pdfminer.six!=20200720,>=20191110,<=20201018
|
pdfminer.six!=20200720,>=20191110,<=20211012
|
||||||
pikepdf>=2.10.0
|
pikepdf>=2.10.0
|
||||||
pluggy>=0.13.0,<2
|
pluggy>=0.13.0,<2
|
||||||
reportlab>=3.5.66
|
reportlab>=3.5.66
|
||||||
setuptools
|
|
||||||
tqdm>=4
|
tqdm>=4
|
||||||
|
importlib-metadata>=4;python_version<'3.8' # until Python 3.8
|
||||||
|
importlib-resources>=5;python_version<'3.9' # until Python 3.9
|
||||||
|
pikepdf<3;implementation_name=="pypy" and python_version=='3.6'
|
||||||
python_requires = >=3.6
|
python_requires = >=3.6
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
package_dir =
|
package_dir =
|
||||||
@@ -62,8 +65,8 @@ package_dir =
|
|||||||
platforms = any
|
platforms = any
|
||||||
setup_requires =
|
setup_requires =
|
||||||
cffi>=1.9.1 # to build the leptonica module
|
cffi>=1.9.1 # to build the leptonica module
|
||||||
setuptools_scm
|
setuptools-scm
|
||||||
setuptools_scm_git_archive
|
setuptools-scm-git-archive
|
||||||
zip_safe = False
|
zip_safe = False
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
@@ -105,3 +108,9 @@ test = pytest
|
|||||||
[check-manifest]
|
[check-manifest]
|
||||||
ignore =
|
ignore =
|
||||||
.github
|
.github
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
ignore = D203,F401,W503,E501,E203,F841
|
||||||
|
exclude = .git,__pycache__,docs/conf.py,build,dist,.venv,.venvpp,.eggs,tmp,src/ocrmypdf/lib/
|
||||||
|
max-complexity = 10
|
||||||
|
max-line-length = 100
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
"""Interface to Tesseract executable"""
|
"""Interface to Tesseract executable"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from os import fspath
|
from os import fspath
|
||||||
@@ -20,7 +18,6 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ocrmypdf.api import StrPath
|
|
||||||
from ocrmypdf.exceptions import (
|
from ocrmypdf.exceptions import (
|
||||||
MissingDependencyError,
|
MissingDependencyError,
|
||||||
SubprocessOutputError,
|
SubprocessOutputError,
|
||||||
@@ -62,8 +59,8 @@ class TesseractVersion(StrictVersion):
|
|||||||
r'''
|
r'''
|
||||||
^(\d+) \. (\d+) (\. (\d+))? # groups: 1/major, 2/minor, 3/[skip], 4/patch
|
^(\d+) \. (\d+) (\. (\d+))? # groups: 1/major, 2/minor, 3/[skip], 4/patch
|
||||||
[-]? # optional hyphen separator
|
[-]? # optional hyphen separator
|
||||||
(?:(alpha|beta|rc|dev)?[.\-\ ]?(\d+)?)? # 5/prerelease, 6/prerelease_num
|
(?: ((?:alpha|beta|rc|dev)\d*)? [.\-\ ]? (\d+)? )? # 5/prerelease, 6/prerelease_num
|
||||||
(?:-(\d+)-g[0-9a-f]+)? # untagged git version
|
(?:(?:-\d+)?-g[0-9a-f]+)? # untagged git version
|
||||||
$
|
$
|
||||||
''',
|
''',
|
||||||
re.VERBOSE | re.ASCII,
|
re.VERBOSE | re.ASCII,
|
||||||
@@ -74,7 +71,7 @@ class TesseractVersion(StrictVersion):
|
|||||||
super().parse(vstring)
|
super().parse(vstring)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
if 'int() argument must be a string' in str(e):
|
if 'int() argument must be a string' in str(e):
|
||||||
super().parse(vstring + '0')
|
super().parse(vstring + '-0')
|
||||||
|
|
||||||
|
|
||||||
def version():
|
def version():
|
||||||
@@ -251,7 +248,7 @@ def generate_hocr(
|
|||||||
|
|
||||||
# Reminder: test suite tesseract test plugins will break after any changes
|
# Reminder: test suite tesseract test plugins will break after any changes
|
||||||
# to the number of order parameters here
|
# to the number of order parameters here
|
||||||
args_tesseract.extend([os.fspath(input_file), os.fspath(prefix), 'hocr', 'txt'])
|
args_tesseract.extend([fspath(input_file), fspath(prefix), 'hocr', 'txt'])
|
||||||
args_tesseract.extend(tessconfig)
|
args_tesseract.extend(tessconfig)
|
||||||
try:
|
try:
|
||||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||||
@@ -274,7 +271,7 @@ def generate_hocr(
|
|||||||
# The sidecar text file will get the suffix .txt; rename it to
|
# The sidecar text file will get the suffix .txt; rename it to
|
||||||
# whatever caller wants it named
|
# whatever caller wants it named
|
||||||
if prefix.with_suffix('.txt').exists():
|
if prefix.with_suffix('.txt').exists():
|
||||||
shutil.move(prefix.with_suffix('.txt'), output_text)
|
prefix.with_suffix('.txt').replace(output_text)
|
||||||
|
|
||||||
|
|
||||||
def use_skip_page(output_pdf, output_text):
|
def use_skip_page(output_pdf, output_text):
|
||||||
@@ -321,18 +318,18 @@ def generate_pdf(
|
|||||||
if user_patterns:
|
if user_patterns:
|
||||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||||
|
|
||||||
prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes
|
prefix = output_pdf.parent / Path(output_pdf.stem)
|
||||||
|
|
||||||
# Reminder: test suite tesseract test plugins might break after any changes
|
# Reminder: test suite tesseract test plugins might break after any changes
|
||||||
# to the number of order parameters here
|
# to the number of order parameters here
|
||||||
|
|
||||||
args_tesseract.extend([os.fspath(input_file), os.fspath(prefix), 'pdf', 'txt'])
|
args_tesseract.extend([fspath(input_file), fspath(prefix), 'pdf', 'txt'])
|
||||||
args_tesseract.extend(tessconfig)
|
args_tesseract.extend(tessconfig)
|
||||||
try:
|
try:
|
||||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||||
stdout = p.stdout
|
stdout = p.stdout
|
||||||
if os.path.exists(prefix + '.txt'):
|
if prefix.with_suffix('.txt').exists():
|
||||||
shutil.move(prefix + '.txt', output_text)
|
prefix.with_suffix('.txt').replace(output_text)
|
||||||
except TimeoutExpired:
|
except TimeoutExpired:
|
||||||
page_timedout(timeout)
|
page_timedout(timeout)
|
||||||
use_skip_page(output_pdf, output_text)
|
use_skip_page(output_pdf, output_text)
|
||||||
|
|||||||
@@ -96,12 +96,12 @@ def run(
|
|||||||
try:
|
try:
|
||||||
with Image.open(output_pnm) as imout:
|
with Image.open(output_pnm) as imout:
|
||||||
imout.save(output_file, dpi=(dpi, dpi))
|
imout.save(output_file, dpi=(dpi, dpi))
|
||||||
except (FileNotFoundError, OSError):
|
except OSError as e:
|
||||||
raise SubprocessOutputError(
|
raise SubprocessOutputError(
|
||||||
"unpaper: failed to produce the expected output file. "
|
"unpaper: failed to produce the expected output file. "
|
||||||
+ " Called with: "
|
+ " Called with: "
|
||||||
+ str(args_unpaper)
|
+ str(args_unpaper)
|
||||||
) from None
|
) from e
|
||||||
|
|
||||||
|
|
||||||
def validate_custom_args(args: str) -> List[str]:
|
def validate_custom_args(args: str) -> List[str]:
|
||||||
|
|||||||
+38
-50
@@ -11,8 +11,19 @@ from contextlib import suppress
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pikepdf
|
from pikepdf import (
|
||||||
from pikepdf.objects import Dictionary, Name
|
Dictionary,
|
||||||
|
Name,
|
||||||
|
Object,
|
||||||
|
Operator,
|
||||||
|
Page,
|
||||||
|
Pdf,
|
||||||
|
PdfError,
|
||||||
|
PdfMatrix,
|
||||||
|
Stream,
|
||||||
|
parse_content_stream,
|
||||||
|
unparse_content_stream,
|
||||||
|
)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
MAX_REPLACE_PAGES = 100
|
MAX_REPLACE_PAGES = 100
|
||||||
@@ -47,51 +58,28 @@ def strip_invisible_text(pdf, page):
|
|||||||
render_mode = 0
|
render_mode = 0
|
||||||
text_objects = []
|
text_objects = []
|
||||||
|
|
||||||
rich_page = pikepdf.Page(page)
|
rich_page = Page(page)
|
||||||
rich_page.contents_coalesce()
|
rich_page.contents_coalesce()
|
||||||
for operands, operator in pikepdf.parse_content_stream(page, ''):
|
for operands, operator in parse_content_stream(page, ''):
|
||||||
if not in_text_obj:
|
if not in_text_obj:
|
||||||
if operator == pikepdf.Operator('BT'):
|
if operator == Operator('BT'):
|
||||||
in_text_obj = True
|
in_text_obj = True
|
||||||
render_mode = 0
|
render_mode = 0
|
||||||
text_objects.append((operands, operator))
|
text_objects.append((operands, operator))
|
||||||
else:
|
else:
|
||||||
stream.append((operands, operator))
|
stream.append((operands, operator))
|
||||||
else:
|
else:
|
||||||
if operator == pikepdf.Operator('Tr'):
|
if operator == Operator('Tr'):
|
||||||
render_mode = operands[0]
|
render_mode = operands[0]
|
||||||
text_objects.append((operands, operator))
|
text_objects.append((operands, operator))
|
||||||
if operator == pikepdf.Operator('ET'):
|
if operator == Operator('ET'):
|
||||||
in_text_obj = False
|
in_text_obj = False
|
||||||
if render_mode != 3:
|
if render_mode != 3:
|
||||||
stream.extend(text_objects)
|
stream.extend(text_objects)
|
||||||
text_objects.clear()
|
text_objects.clear()
|
||||||
|
|
||||||
def convert(op):
|
content_stream = unparse_content_stream(stream)
|
||||||
try:
|
page.Contents = Stream(pdf, content_stream)
|
||||||
return op.unparse()
|
|
||||||
except AttributeError:
|
|
||||||
return str(op).encode('ascii')
|
|
||||||
|
|
||||||
if hasattr(pikepdf, 'unparse_content_stream'):
|
|
||||||
content_stream = pikepdf.unparse_content_stream(stream)
|
|
||||||
else:
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
for operands, operator in stream:
|
|
||||||
if operator == pikepdf.Operator('INLINE IMAGE'):
|
|
||||||
iim = operands[0]
|
|
||||||
line = iim.unparse()
|
|
||||||
else:
|
|
||||||
line = (
|
|
||||||
b' '.join(convert(op) for op in operands)
|
|
||||||
+ b' '
|
|
||||||
+ operator.unparse()
|
|
||||||
)
|
|
||||||
lines.append(line)
|
|
||||||
|
|
||||||
content_stream = b'\n'.join(lines)
|
|
||||||
page.Contents = pikepdf.Stream(pdf, content_stream)
|
|
||||||
|
|
||||||
|
|
||||||
class OcrGrafter:
|
class OcrGrafter:
|
||||||
@@ -99,14 +87,14 @@ class OcrGrafter:
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.path_base = context.origin
|
self.path_base = context.origin
|
||||||
|
|
||||||
self.pdf_base = pikepdf.open(self.path_base)
|
self.pdf_base = Pdf.open(self.path_base)
|
||||||
self.font, self.font_key = None, None
|
self.font, self.font_key = None, None
|
||||||
|
|
||||||
self.pdfinfo = context.pdfinfo
|
self.pdfinfo = context.pdfinfo
|
||||||
self.output_file = context.get_path('graft_layers.pdf')
|
self.output_file = context.get_path('graft_layers.pdf')
|
||||||
|
|
||||||
self.procset = self.pdf_base.make_indirect(
|
self.procset = self.pdf_base.make_indirect(
|
||||||
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
|
Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
|
||||||
)
|
)
|
||||||
|
|
||||||
self.emplacements = 1
|
self.emplacements = 1
|
||||||
@@ -130,7 +118,7 @@ class OcrGrafter:
|
|||||||
# We are updating the old page with a rasterized PDF of the new
|
# We are updating the old page with a rasterized PDF of the new
|
||||||
# page (without changing objgen, to preserve references)
|
# page (without changing objgen, to preserve references)
|
||||||
log.debug("Emplacement update")
|
log.debug("Emplacement update")
|
||||||
with pikepdf.open(image) as pdf_image:
|
with Pdf.open(path_image) as pdf_image:
|
||||||
self.emplacements += 1
|
self.emplacements += 1
|
||||||
foreign_image_page = pdf_image.pages[0]
|
foreign_image_page = pdf_image.pages[0]
|
||||||
self.pdf_base.pages.append(foreign_image_page)
|
self.pdf_base.pages.append(foreign_image_page)
|
||||||
@@ -203,7 +191,7 @@ class OcrGrafter:
|
|||||||
self.pdf_base.save(next_file)
|
self.pdf_base.save(next_file)
|
||||||
self.pdf_base.close()
|
self.pdf_base.close()
|
||||||
|
|
||||||
self.pdf_base = pikepdf.open(next_file)
|
self.pdf_base = Pdf.open(next_file)
|
||||||
self.procset = self.pdf_base.pages[0].Resources.ProcSet
|
self.procset = self.pdf_base.pages[0].Resources.ProcSet
|
||||||
self.font, self.font_key = None, None # Ensure we reacquire this information
|
self.font, self.font_key = None, None # Ensure we reacquire this information
|
||||||
self.interim_count += 1
|
self.interim_count += 1
|
||||||
@@ -219,7 +207,7 @@ class OcrGrafter:
|
|||||||
font, font_key = None, None
|
font, font_key = None, None
|
||||||
possible_font_names = ('/f-0-0', '/F1')
|
possible_font_names = ('/f-0-0', '/F1')
|
||||||
try:
|
try:
|
||||||
with pikepdf.open(text) as pdf_text:
|
with Pdf.open(text) as pdf_text:
|
||||||
try:
|
try:
|
||||||
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
|
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
|
||||||
except (AttributeError, IndexError, KeyError):
|
except (AttributeError, IndexError, KeyError):
|
||||||
@@ -233,7 +221,7 @@ class OcrGrafter:
|
|||||||
if pdf_text_font:
|
if pdf_text_font:
|
||||||
font = self.pdf_base.copy_foreign(pdf_text_font)
|
font = self.pdf_base.copy_foreign(pdf_text_font)
|
||||||
return font, font_key
|
return font, font_key
|
||||||
except (FileNotFoundError, pikepdf.PdfError):
|
except (FileNotFoundError, PdfError):
|
||||||
# PdfError occurs if a 0-length file is written e.g. due to OCR timeout
|
# PdfError occurs if a 0-length file is written e.g. due to OCR timeout
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
@@ -242,9 +230,9 @@ class OcrGrafter:
|
|||||||
*,
|
*,
|
||||||
page_num: int,
|
page_num: int,
|
||||||
textpdf: Path,
|
textpdf: Path,
|
||||||
font: pikepdf.Object,
|
font: Object,
|
||||||
font_key: pikepdf.Object,
|
font_key: Object,
|
||||||
procset: pikepdf.Object,
|
procset: Object,
|
||||||
text_rotation: int,
|
text_rotation: int,
|
||||||
strip_old_text: bool,
|
strip_old_text: bool,
|
||||||
):
|
):
|
||||||
@@ -255,7 +243,7 @@ class OcrGrafter:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# This is a pointer indicating a specific page in the base file
|
# This is a pointer indicating a specific page in the base file
|
||||||
with pikepdf.open(textpdf) as pdf_text:
|
with Pdf.open(textpdf) as pdf_text:
|
||||||
pdf_text_contents = pdf_text.pages[0].Contents.read_bytes()
|
pdf_text_contents = pdf_text.pages[0].Contents.read_bytes()
|
||||||
|
|
||||||
base_page = self.pdf_base.pages.p(page_num)
|
base_page = self.pdf_base.pages.p(page_num)
|
||||||
@@ -270,13 +258,13 @@ class OcrGrafter:
|
|||||||
mediabox = [float(base_page.MediaBox[v]) for v in range(4)]
|
mediabox = [float(base_page.MediaBox[v]) for v in range(4)]
|
||||||
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
|
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
|
||||||
|
|
||||||
translate = pikepdf.PdfMatrix().translated(-wt / 2, -ht / 2)
|
translate = PdfMatrix().translated(-wt / 2, -ht / 2)
|
||||||
untranslate = pikepdf.PdfMatrix().translated(wp / 2, hp / 2)
|
untranslate = PdfMatrix().translated(wp / 2, hp / 2)
|
||||||
corner = pikepdf.PdfMatrix().translated(mediabox[0], mediabox[1])
|
corner = PdfMatrix().translated(mediabox[0], mediabox[1])
|
||||||
# -rotation because the input is a clockwise angle and this formula
|
# -rotation because the input is a clockwise angle and this formula
|
||||||
# uses CCW
|
# uses CCW
|
||||||
text_rotation = -text_rotation % 360
|
text_rotation = -text_rotation % 360
|
||||||
rotate = pikepdf.PdfMatrix().rotated(text_rotation)
|
rotate = PdfMatrix().rotated(text_rotation)
|
||||||
|
|
||||||
# Because of rounding of DPI, we might get a text layer that is not
|
# Because of rounding of DPI, we might get a text layer that is not
|
||||||
# identically sized to the target page. Scale to adjust. Normally this
|
# identically sized to the target page. Scale to adjust. Normally this
|
||||||
@@ -287,7 +275,7 @@ class OcrGrafter:
|
|||||||
scale_y = hp / ht
|
scale_y = hp / ht
|
||||||
|
|
||||||
# log.debug('%r', scale_x, scale_y)
|
# log.debug('%r', scale_x, scale_y)
|
||||||
scale = pikepdf.PdfMatrix().scaled(scale_x, scale_y)
|
scale = PdfMatrix().scaled(scale_x, scale_y)
|
||||||
|
|
||||||
# Translate the text so it is centered at (0, 0), rotate it there, adjust
|
# Translate the text so it is centered at (0, 0), rotate it there, adjust
|
||||||
# for a size different between initial and text PDF, then untranslate, and
|
# for a size different between initial and text PDF, then untranslate, and
|
||||||
@@ -310,14 +298,14 @@ class OcrGrafter:
|
|||||||
pdf_draw_xobj = (
|
pdf_draw_xobj = (
|
||||||
(b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
|
(b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
|
||||||
)
|
)
|
||||||
new_text_layer = pikepdf.Stream(self.pdf_base, pdf_draw_xobj)
|
new_text_layer = Stream(self.pdf_base, pdf_draw_xobj)
|
||||||
|
|
||||||
if strip_old_text:
|
if strip_old_text:
|
||||||
strip_invisible_text(self.pdf_base, base_page)
|
strip_invisible_text(self.pdf_base, base_page)
|
||||||
|
|
||||||
if hasattr(pikepdf.Page, 'contents_add'):
|
if hasattr(Page, 'contents_add'):
|
||||||
# pikepdf >= 2.14 adds this method and deprecates the one below
|
# pikepdf >= 2.14 adds this method and deprecates the one below
|
||||||
pikepdf.Page(base_page).contents_add(new_text_layer, prepend=True)
|
Page(base_page).contents_add(new_text_layer, prepend=True)
|
||||||
else:
|
else:
|
||||||
# pikepdf < 2.14
|
# pikepdf < 2.14
|
||||||
base_page.page_contents_add(
|
base_page.page_contents_add(
|
||||||
|
|||||||
+15
-7
@@ -293,12 +293,13 @@ def exec_concurrent(context: PdfContext, executor: Executor):
|
|||||||
# Merge layers to one single pdf
|
# Merge layers to one single pdf
|
||||||
pdf = ocrgraft.finalize()
|
pdf = ocrgraft.finalize()
|
||||||
|
|
||||||
# PDF/A and metadata
|
if options.output_type != 'none':
|
||||||
log.info("Postprocessing...")
|
# PDF/A and metadata
|
||||||
pdf = post_process(pdf, context, executor)
|
log.info("Postprocessing...")
|
||||||
|
pdf = post_process(pdf, context, executor)
|
||||||
|
|
||||||
# Copy PDF file to destination
|
# Copy PDF file to destination
|
||||||
copy_final(pdf, options.output_file, context)
|
copy_final(pdf, options.output_file, context)
|
||||||
|
|
||||||
|
|
||||||
def configure_debug_logging(log_filename: Path, prefix: str = ''):
|
def configure_debug_logging(log_filename: Path, prefix: str = ''):
|
||||||
@@ -399,7 +400,7 @@ def run_pipeline(options, *, plugin_manager, api=False):
|
|||||||
return ExitCode.invalid_output_pdf
|
return ExitCode.invalid_output_pdf
|
||||||
report_output_file_size(options, start_input_file, options.output_file)
|
report_output_file_size(options, start_input_file, options.output_file)
|
||||||
|
|
||||||
except (KeyboardInterrupt if not api else NeverRaise) as e:
|
except (KeyboardInterrupt if not api else NeverRaise):
|
||||||
if options.verbose >= 1:
|
if options.verbose >= 1:
|
||||||
log.exception("KeyboardInterrupt")
|
log.exception("KeyboardInterrupt")
|
||||||
else:
|
else:
|
||||||
@@ -413,7 +414,14 @@ def run_pipeline(options, *, plugin_manager, api=False):
|
|||||||
else:
|
else:
|
||||||
log.error(type(e).__name__)
|
log.error(type(e).__name__)
|
||||||
return e.exit_code
|
return e.exit_code
|
||||||
except (Exception if not api else NeverRaise) as e: # pylint: disable=broad-except
|
except (PIL.Image.DecompressionBombError if not api else NeverRaise) as e:
|
||||||
|
log.exception(
|
||||||
|
"A decompression bomb error was encountered while executing the "
|
||||||
|
"pipeline. Use the argument --max-image-mpixels to raise the maximum "
|
||||||
|
"image pixel limit."
|
||||||
|
)
|
||||||
|
return ExitCode.other_error
|
||||||
|
except (Exception if not api else NeverRaise): # pylint: disable=broad-except
|
||||||
log.exception("An exception occurred while executing the pipeline")
|
log.exception("An exception occurred while executing the pipeline")
|
||||||
return ExitCode.other_error
|
return ExitCode.other_error
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+19
-12
@@ -26,12 +26,7 @@ from ocrmypdf.exceptions import (
|
|||||||
MissingDependencyError,
|
MissingDependencyError,
|
||||||
OutputFileAccessError,
|
OutputFileAccessError,
|
||||||
)
|
)
|
||||||
from ocrmypdf.helpers import (
|
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink, samefile
|
||||||
is_file_writable,
|
|
||||||
is_iterable_notstr,
|
|
||||||
monotonic,
|
|
||||||
safe_symlink,
|
|
||||||
)
|
|
||||||
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
|
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
|
||||||
from ocrmypdf.subprocess import check_external_program
|
from ocrmypdf.subprocess import check_external_program
|
||||||
|
|
||||||
@@ -68,7 +63,7 @@ def check_options_languages(options, ocr_engine_languages):
|
|||||||
missing_languages = options.languages - ocr_engine_languages
|
missing_languages = options.languages - ocr_engine_languages
|
||||||
if missing_languages:
|
if missing_languages:
|
||||||
msg = (
|
msg = (
|
||||||
f"OCR engine does not have language data for the following "
|
"OCR engine does not have language data for the following "
|
||||||
"requested languages: \n"
|
"requested languages: \n"
|
||||||
)
|
)
|
||||||
msg += '\n'.join(lang for lang in missing_languages)
|
msg += '\n'.join(lang for lang in missing_languages)
|
||||||
@@ -80,12 +75,18 @@ def check_options_output(options):
|
|||||||
is_latin = options.languages.issubset(HOCR_OK_LANGS)
|
is_latin = options.languages.issubset(HOCR_OK_LANGS)
|
||||||
|
|
||||||
if options.pdf_renderer.startswith('hocr') and not is_latin:
|
if options.pdf_renderer.startswith('hocr') and not is_latin:
|
||||||
msg = (
|
log.warning(
|
||||||
"The 'hocr' PDF renderer is known to cause problems with one "
|
"The 'hocr' PDF renderer is known to cause problems with one "
|
||||||
"or more of the languages in your document. Use "
|
"or more of the languages in your document. Use "
|
||||||
"--pdf-renderer auto (the default) to avoid this issue."
|
"`--pdf-renderer auto` (the default) to avoid this issue."
|
||||||
|
)
|
||||||
|
|
||||||
|
if options.output_type == 'none' and options.output_file != os.devnull:
|
||||||
|
raise BadArgsError(
|
||||||
|
"Since you specified `--pdf-renderer none`, the output file "
|
||||||
|
f"{options.output_file} cannot be produced. Set the output file to "
|
||||||
|
f"{os.devnull} to suppress this message."
|
||||||
)
|
)
|
||||||
log.warning(msg)
|
|
||||||
|
|
||||||
lossless_reconstruction = False
|
lossless_reconstruction = False
|
||||||
if not any(
|
if not any(
|
||||||
@@ -112,6 +113,10 @@ def check_options_sidecar(options):
|
|||||||
raise BadArgsError(
|
raise BadArgsError(
|
||||||
"--sidecar filename must be specified when output file is stdout."
|
"--sidecar filename must be specified when output file is stdout."
|
||||||
)
|
)
|
||||||
|
elif options.output_file == os.devnull:
|
||||||
|
raise BadArgsError(
|
||||||
|
"--sidecar filename must be specified when output file is /dev/null or NUL."
|
||||||
|
)
|
||||||
options.sidecar = options.output_file + '.txt'
|
options.sidecar = options.output_file + '.txt'
|
||||||
if options.sidecar == options.input_file or options.sidecar == options.output_file:
|
if options.sidecar == options.input_file or options.sidecar == options.output_file:
|
||||||
raise BadArgsError(
|
raise BadArgsError(
|
||||||
@@ -155,10 +160,12 @@ def _pages_from_ranges(ranges: str) -> Set[int]:
|
|||||||
try:
|
try:
|
||||||
new_pages = list(range(int(start) - 1, int(end)))
|
new_pages = list(range(int(start) - 1, int(end)))
|
||||||
if not new_pages:
|
if not new_pages:
|
||||||
raise BadArgsError(f"invalid page subrange '{start}-{end}'")
|
raise BadArgsError(
|
||||||
|
f"invalid page subrange '{start}-{end}'"
|
||||||
|
) from None
|
||||||
pages.extend(new_pages)
|
pages.extend(new_pages)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise BadArgsError("invalid page range") from None
|
raise BadArgsError(f"invalid page subrange '{g}'") from None
|
||||||
|
|
||||||
if not pages:
|
if not pages:
|
||||||
raise BadArgsError(
|
raise BadArgsError(
|
||||||
|
|||||||
@@ -5,9 +5,12 @@
|
|||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
|
||||||
import pkg_resources
|
try:
|
||||||
|
from importlib_metadata import version as _package_version
|
||||||
|
except ImportError:
|
||||||
|
from importlib.metadata import version as _package_version
|
||||||
|
|
||||||
PROGRAM_NAME = 'ocrmypdf'
|
PROGRAM_NAME = 'ocrmypdf'
|
||||||
|
|
||||||
# Official PEP 396
|
# Official PEP 396
|
||||||
__version__ = pkg_resources.get_distribution('ocrmypdf').version
|
__version__ = _package_version('ocrmypdf')
|
||||||
|
|||||||
+15
-4
@@ -15,10 +15,7 @@ from pathlib import Path
|
|||||||
from typing import AnyStr, BinaryIO, Iterable, Optional, Union
|
from typing import AnyStr, BinaryIO, Iterable, Optional, Union
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from ocrmypdf._logging import ( # pylint: disable=unused-import
|
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
|
||||||
PageNumberFilter,
|
|
||||||
TqdmConsole,
|
|
||||||
)
|
|
||||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||||
from ocrmypdf._sync import run_pipeline
|
from ocrmypdf._sync import run_pipeline
|
||||||
from ocrmypdf._validation import check_options
|
from ocrmypdf._validation import check_options
|
||||||
@@ -338,3 +335,17 @@ def ocr( # pylint: disable=unused-argument
|
|||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'PageNumberFilter',
|
||||||
|
'TqdmConsole',
|
||||||
|
'Verbosity',
|
||||||
|
'check_options',
|
||||||
|
'configure_logging',
|
||||||
|
'create_options',
|
||||||
|
'get_parser',
|
||||||
|
'get_plugin_manager',
|
||||||
|
'ocr',
|
||||||
|
'run_pipeline',
|
||||||
|
]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from multiprocessing.pool import Pool, ThreadPool
|
from multiprocessing.pool import Pool, ThreadPool
|
||||||
from typing import Callable, Iterable, Optional, Tuple, Type, Union
|
from typing import Callable, Iterable, Type, Union
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import os
|
|||||||
from ocrmypdf import hookimpl
|
from ocrmypdf import hookimpl
|
||||||
from ocrmypdf._exec import tesseract
|
from ocrmypdf._exec import tesseract
|
||||||
from ocrmypdf.cli import numeric
|
from ocrmypdf.cli import numeric
|
||||||
from ocrmypdf.exceptions import MissingDependencyError
|
|
||||||
from ocrmypdf.helpers import clamp
|
from ocrmypdf.helpers import clamp
|
||||||
from ocrmypdf.pluginspec import OcrEngine
|
from ocrmypdf.pluginspec import OcrEngine
|
||||||
from ocrmypdf.subprocess import check_external_program
|
from ocrmypdf.subprocess import check_external_program
|
||||||
|
|||||||
+8
-5
@@ -147,7 +147,7 @@ Online documentation is located at:
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--output-type',
|
'--output-type',
|
||||||
choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2', 'pdfa-3'],
|
choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2', 'pdfa-3', 'none'],
|
||||||
default='pdfa',
|
default='pdfa',
|
||||||
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
|
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
|
||||||
"long term archiving (default, recommended) but may not suitable "
|
"long term archiving (default, recommended) but may not suitable "
|
||||||
@@ -155,7 +155,8 @@ Online documentation is located at:
|
|||||||
"also has problems with full Unicode text. 'pdf' attempts to "
|
"also has problems with full Unicode text. 'pdf' attempts to "
|
||||||
"preserve file contents as much as possible. 'pdf-a1' creates a "
|
"preserve file contents as much as possible. 'pdf-a1' creates a "
|
||||||
"PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'. 'pdf-a3' creates a "
|
"PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'. 'pdf-a3' creates a "
|
||||||
"PDF/A3-b file.",
|
"PDF/A3-b file. 'none' will produce no output, which may be helpful if "
|
||||||
|
"only the --sidecar is desired.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use null string '\0' as sentinel to indicate the user supplied no argument,
|
# Use null string '\0' as sentinel to indicate the user supplied no argument,
|
||||||
@@ -340,8 +341,9 @@ Online documentation is located at:
|
|||||||
"Control how PDF is optimized after processing:"
|
"Control how PDF is optimized after processing:"
|
||||||
"0 - do not optimize; "
|
"0 - do not optimize; "
|
||||||
"1 - do safe, lossless optimizations (default); "
|
"1 - do safe, lossless optimizations (default); "
|
||||||
"2 - do some lossy optimizations; "
|
"2 - do lossy JPEG and JPEG2000 optimizations; "
|
||||||
"3 - do aggressive lossy optimizations (including lossy JBIG2)"
|
"3 - do more aggressive lossy JPEG and JPEG2000 optimizations. "
|
||||||
|
"To enable lossy JBIG2, see --jbig2-lossy."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
optimizing.add_argument(
|
optimizing.add_argument(
|
||||||
@@ -379,7 +381,8 @@ Online documentation is located at:
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help=(
|
help=(
|
||||||
"Enable JBIG2 lossy mode (better compression, not suitable for some "
|
"Enable JBIG2 lossy mode (better compression, not suitable for some "
|
||||||
"use cases - see documentation)."
|
"use cases - see documentation). Only takes effect if --optimize 1 or "
|
||||||
|
"higher is also enabled."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
optimizing.add_argument(
|
optimizing.add_argument(
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# © 2021 James R. Barlow: github.com/jbarlow83
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
|
||||||
|
"""Data files used to generate certain PDFs."""
|
||||||
@@ -221,15 +221,7 @@ def check_pdf(input_file: Path) -> bool:
|
|||||||
# If linearization is missing entirely, we do not complain. We do
|
# If linearization is missing entirely, we do not complain. We do
|
||||||
# complain if linearization is present but incorrect.
|
# complain if linearization is present but incorrect.
|
||||||
pdf.check_linearization(sio)
|
pdf.check_linearization(sio)
|
||||||
except RuntimeError:
|
except (RuntimeError, pikepdf.ForeignObjectError):
|
||||||
pass
|
|
||||||
except (
|
|
||||||
# Workaround for a problematic pikepdf version
|
|
||||||
# pragma: no cover
|
|
||||||
getattr(pikepdf, 'ForeignObjectError')
|
|
||||||
if pikepdf.__version__ == '2.1.0'
|
|
||||||
else NeverRaise
|
|
||||||
):
|
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
linearize_msgs = sio.getvalue()
|
linearize_msgs = sio.getvalue()
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ class HocrTransform:
|
|||||||
interword_spaces: bool,
|
interword_spaces: bool,
|
||||||
show_bounding_boxes: bool,
|
show_bounding_boxes: bool,
|
||||||
):
|
):
|
||||||
if line is not None:
|
if line is None:
|
||||||
return
|
return
|
||||||
pxl_line_coords = self.element_coordinates(line)
|
pxl_line_coords = self.element_coordinates(line)
|
||||||
line_box = self.pt_from_pixel(pxl_line_coords)
|
line_box = self.pt_from_pixel(pxl_line_coords)
|
||||||
|
|||||||
+32
-18
@@ -25,8 +25,16 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pikepdf
|
from pikepdf import (
|
||||||
from pikepdf import Dictionary, Name, Object, Pdf, PdfImage
|
Dictionary,
|
||||||
|
Name,
|
||||||
|
Object,
|
||||||
|
ObjectStreamMode,
|
||||||
|
Pdf,
|
||||||
|
PdfImage,
|
||||||
|
Stream,
|
||||||
|
UnsupportedImageTypeError,
|
||||||
|
)
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ocrmypdf import leptonica
|
from ocrmypdf import leptonica
|
||||||
@@ -63,7 +71,7 @@ def jpg_name(root: Path, xref: Xref) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def extract_image_filter(
|
def extract_image_filter(
|
||||||
pike: Pdf, root: Path, image: Object, xref: Xref
|
pike: Pdf, root: Path, image: Stream, xref: Xref
|
||||||
) -> Optional[Tuple[PdfImage, Tuple[Name, Object]]]:
|
) -> Optional[Tuple[PdfImage, Tuple[Name, Object]]]:
|
||||||
del pike # unused args
|
del pike # unused args
|
||||||
del root
|
del root
|
||||||
@@ -89,7 +97,7 @@ def extract_image_filter(
|
|||||||
return None # Don't mess with wide gamut images
|
return None # Don't mess with wide gamut images
|
||||||
|
|
||||||
if filtdp[0] == Name.JPXDecode:
|
if filtdp[0] == Name.JPXDecode:
|
||||||
log.debug(f"Skipping JPEG2000 iamge, xref {xref}")
|
log.debug(f"Skipping JPEG2000 image, xref {xref}")
|
||||||
return None # Don't do JPEG2000
|
return None # Don't do JPEG2000
|
||||||
|
|
||||||
if filtdp[0] == Name.CCITTFaxDecode and filtdp[1].get('/K', 0) >= 0:
|
if filtdp[0] == Name.CCITTFaxDecode and filtdp[1].get('/K', 0) >= 0:
|
||||||
@@ -104,7 +112,7 @@ def extract_image_filter(
|
|||||||
|
|
||||||
|
|
||||||
def extract_image_jbig2(
|
def extract_image_jbig2(
|
||||||
*, pike: pikepdf.Pdf, root: Path, image: Object, xref: Xref, options
|
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||||
) -> Optional[XrefExt]:
|
) -> Optional[XrefExt]:
|
||||||
del options # unused arg
|
del options # unused arg
|
||||||
|
|
||||||
@@ -123,16 +131,16 @@ def extract_image_jbig2(
|
|||||||
# 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.get(Name.ColorSpace, None)
|
||||||
if colorspace is not None or pim.image_mask:
|
if colorspace is not None or pim.image_mask:
|
||||||
try:
|
try:
|
||||||
# Set to DeviceGray temporarily; we already in 1 bpc.
|
# Set to DeviceGray temporarily; we already in 1 bpc.
|
||||||
pim.obj.ColorSpace = pikepdf.Name.DeviceGray
|
pim.obj.ColorSpace = Name.DeviceGray
|
||||||
imgname = root / f'{xref:08d}'
|
imgname = root / f'{xref:08d}'
|
||||||
with imgname.open('wb') as f:
|
with imgname.open('wb') as f:
|
||||||
ext = pim.extract_to(stream=f)
|
ext = pim.extract_to(stream=f)
|
||||||
imgname.rename(imgname.with_suffix(ext))
|
imgname.rename(imgname.with_suffix(ext))
|
||||||
except pikepdf.UnsupportedImageTypeError:
|
except UnsupportedImageTypeError:
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
# Restore image colorspace after temporarily setting it to DeviceGray
|
# Restore image colorspace after temporarily setting it to DeviceGray
|
||||||
@@ -145,7 +153,7 @@ def extract_image_jbig2(
|
|||||||
|
|
||||||
|
|
||||||
def extract_image_generic(
|
def extract_image_generic(
|
||||||
*, pike: Pdf, root: Path, image: PdfImage, xref: Xref, options
|
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||||
) -> Optional[XrefExt]:
|
) -> Optional[XrefExt]:
|
||||||
result = extract_image_filter(pike, root, image, xref)
|
result = extract_image_filter(pike, root, image, xref)
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -178,7 +186,7 @@ def extract_image_generic(
|
|||||||
with imgname.open('wb') as f:
|
with imgname.open('wb') as f:
|
||||||
ext = pim.extract_to(stream=f)
|
ext = pim.extract_to(stream=f)
|
||||||
imgname.rename(imgname.with_suffix(ext))
|
imgname.rename(imgname.with_suffix(ext))
|
||||||
except pikepdf.UnsupportedImageTypeError:
|
except UnsupportedImageTypeError:
|
||||||
return None
|
return None
|
||||||
return XrefExt(xref, ext)
|
return XrefExt(xref, ext)
|
||||||
elif (
|
elif (
|
||||||
@@ -374,7 +382,7 @@ def convert_to_jbig2(
|
|||||||
jbig2_symfile = root / (prefix + '.sym')
|
jbig2_symfile = root / (prefix + '.sym')
|
||||||
if jbig2_symfile.exists():
|
if jbig2_symfile.exists():
|
||||||
jbig2_globals_data = jbig2_symfile.read_bytes()
|
jbig2_globals_data = jbig2_symfile.read_bytes()
|
||||||
jbig2_globals = pikepdf.Stream(pike, jbig2_globals_data)
|
jbig2_globals = Stream(pike, jbig2_globals_data)
|
||||||
jbig2_globals_dict = Dictionary(JBIG2Globals=jbig2_globals)
|
jbig2_globals_dict = Dictionary(JBIG2Globals=jbig2_globals)
|
||||||
elif options.jbig2_page_group_size == 1:
|
elif options.jbig2_page_group_size == 1:
|
||||||
jbig2_globals_dict = None
|
jbig2_globals_dict = None
|
||||||
@@ -445,8 +453,8 @@ def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
|
|||||||
with output.open('wb') as f:
|
with output.open('wb') as f:
|
||||||
img2pdf.convert(fspath(filename), outputstream=f)
|
img2pdf.convert(fspath(filename), outputstream=f)
|
||||||
|
|
||||||
with pikepdf.open(output) as pdf_image:
|
with Pdf.open(output) as pdf_image:
|
||||||
foreign_image = next(pdf_image.pages[0].images.values())
|
foreign_image = next(iter(pdf_image.pages[0].images.values()))
|
||||||
local_image = pike.copy_foreign(foreign_image)
|
local_image = pike.copy_foreign(foreign_image)
|
||||||
|
|
||||||
im_obj = pike.get_object(xref, 0)
|
im_obj = pike.get_object(xref, 0)
|
||||||
@@ -525,12 +533,15 @@ def transcode_pngs(
|
|||||||
_transcode_png(pike, filename, xref)
|
_transcode_png(pike, filename, xref)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_EXECUTOR = SerialExecutor()
|
||||||
|
|
||||||
|
|
||||||
def optimize(
|
def optimize(
|
||||||
input_file: Path,
|
input_file: Path,
|
||||||
output_file: Path,
|
output_file: Path,
|
||||||
context,
|
context,
|
||||||
save_settings,
|
save_settings,
|
||||||
executor: Executor = SerialExecutor(),
|
executor: Executor = DEFAULT_EXECUTOR,
|
||||||
) -> None:
|
) -> None:
|
||||||
options = context.options
|
options = context.options
|
||||||
if options.optimize == 0:
|
if options.optimize == 0:
|
||||||
@@ -544,7 +555,7 @@ def optimize(
|
|||||||
if options.jbig2_page_group_size == 0:
|
if options.jbig2_page_group_size == 0:
|
||||||
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
|
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
|
||||||
|
|
||||||
with pikepdf.Pdf.open(input_file) as pike:
|
with Pdf.open(input_file) as pike:
|
||||||
root = output_file.parent / 'images'
|
root = output_file.parent / 'images'
|
||||||
root.mkdir(exist_ok=True)
|
root.mkdir(exist_ok=True)
|
||||||
|
|
||||||
@@ -574,9 +585,12 @@ def optimize(
|
|||||||
log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
|
log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
|
||||||
|
|
||||||
if savings < 0:
|
if savings < 0:
|
||||||
log.info("Image optimization did not improve the file - discarded")
|
log.info(
|
||||||
|
"Image optimization did not improve the file - "
|
||||||
|
"optimizations will not be used"
|
||||||
|
)
|
||||||
# We still need to save the file
|
# We still need to save the file
|
||||||
with pikepdf.open(input_file) as pike:
|
with Pdf.open(input_file) as pike:
|
||||||
pike.remove_unreferenced_resources()
|
pike.remove_unreferenced_resources()
|
||||||
pike.save(output_file, **save_settings)
|
pike.save(output_file, **save_settings)
|
||||||
else:
|
else:
|
||||||
@@ -623,7 +637,7 @@ def main(infile, outfile, level, jobs=1):
|
|||||||
dict(
|
dict(
|
||||||
compress_streams=True,
|
compress_streams=True,
|
||||||
preserve_pdfa=True,
|
preserve_pdfa=True,
|
||||||
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
object_stream_mode=ObjectStreamMode.generate,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
copy(fspath(tmpout), fspath(outfile))
|
copy(fspath(tmpout), fspath(outfile))
|
||||||
|
|||||||
+12
-6
@@ -13,13 +13,21 @@ import base64
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterator, Union
|
from typing import Dict, Iterator, Union
|
||||||
|
|
||||||
|
try:
|
||||||
|
from importlib_resources import files as package_files
|
||||||
|
except ImportError:
|
||||||
|
from importlib.resources import files as package_files
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
import pkg_resources
|
import pkg_resources # deprecated
|
||||||
|
|
||||||
|
# Deprecated
|
||||||
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
|
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
|
||||||
|
# Deprecated
|
||||||
SRGB_ICC_PROFILE = pkg_resources.resource_filename('ocrmypdf', ICC_PROFILE_RELPATH)
|
SRGB_ICC_PROFILE = pkg_resources.resource_filename('ocrmypdf', ICC_PROFILE_RELPATH)
|
||||||
|
|
||||||
|
SRGB_ICC_PROFILE_NAME = 'sRGB.icc'
|
||||||
|
|
||||||
|
|
||||||
def _postscript_objdef(
|
def _postscript_objdef(
|
||||||
alias: str,
|
alias: str,
|
||||||
@@ -97,12 +105,10 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
|
|||||||
References:
|
References:
|
||||||
Adobe PDFMARK Reference: https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
Adobe PDFMARK Reference: https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
||||||
"""
|
"""
|
||||||
if icc == 'sRGB':
|
if icc != 'sRGB':
|
||||||
icc_profile = SRGB_ICC_PROFILE
|
|
||||||
else:
|
|
||||||
raise NotImplementedError("Only supporting sRGB")
|
raise NotImplementedError("Only supporting sRGB")
|
||||||
|
|
||||||
bytes_icc_profile = Path(icc_profile).read_bytes()
|
bytes_icc_profile = (package_files('ocrmypdf.data') / SRGB_ICC_PROFILE).read_bytes()
|
||||||
ps = '\n'.join(_make_postscript(icc, bytes_icc_profile, 3))
|
ps = '\n'.join(_make_postscript(icc, bytes_icc_profile, 3))
|
||||||
|
|
||||||
# We should have encoded everything to pure ASCII by this point, and
|
# We should have encoded everything to pure ASCII by this point, and
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -30,8 +30,14 @@ from typing import (
|
|||||||
)
|
)
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
import pikepdf
|
from pikepdf import (
|
||||||
from pikepdf import Name, Object, Pdf, PdfInlineImage, PdfMatrix
|
Object,
|
||||||
|
Pdf,
|
||||||
|
PdfImage,
|
||||||
|
PdfInlineImage,
|
||||||
|
PdfMatrix,
|
||||||
|
parse_content_stream,
|
||||||
|
)
|
||||||
|
|
||||||
from ocrmypdf._concurrent import Executor, SerialExecutor
|
from ocrmypdf._concurrent import Executor, SerialExecutor
|
||||||
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
||||||
@@ -181,9 +187,7 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
|||||||
operator_whitelist = ' '.join(vector_ops | text_showing_ops | image_ops)
|
operator_whitelist = ' '.join(vector_ops | text_showing_ops | image_ops)
|
||||||
|
|
||||||
for n, graphobj in enumerate(
|
for n, graphobj in enumerate(
|
||||||
_normalize_stack(
|
_normalize_stack(parse_content_stream(contentstream, operator_whitelist))
|
||||||
pikepdf.parse_content_stream(contentstream, operator_whitelist)
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
operands, operator = graphobj
|
operands, operator = graphobj
|
||||||
if operator == 'q':
|
if operator == 'q':
|
||||||
@@ -303,18 +307,20 @@ class ImageInfo:
|
|||||||
*,
|
*,
|
||||||
name='',
|
name='',
|
||||||
pdfimage: Optional[Object] = None,
|
pdfimage: Optional[Object] = None,
|
||||||
inline: Optional[Object] = None,
|
inline: Optional[PdfInlineImage] = None,
|
||||||
shorthand=None,
|
shorthand=None,
|
||||||
):
|
):
|
||||||
self._name = str(name)
|
self._name = str(name)
|
||||||
self._shorthand = shorthand
|
self._shorthand = shorthand
|
||||||
|
|
||||||
|
pim: Union[PdfInlineImage, PdfImage]
|
||||||
|
|
||||||
if inline is not None:
|
if inline is not None:
|
||||||
self._origin = 'inline'
|
self._origin = 'inline'
|
||||||
pim = inline.iimage
|
pim = inline
|
||||||
elif pdfimage is not None:
|
elif pdfimage is not None:
|
||||||
self._origin = 'xobject'
|
self._origin = 'xobject'
|
||||||
pim = pikepdf.PdfImage(pdfimage)
|
pim = PdfImage(pdfimage)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Either pdfimage or inline must be set")
|
raise ValueError("Either pdfimage or inline must be set")
|
||||||
self._width = pim.width
|
self._width = pim.width
|
||||||
@@ -335,7 +341,7 @@ class ImageInfo:
|
|||||||
self._enc = None
|
self._enc = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._color = FRIENDLY_COLORSPACE.get(pim.colorspace)
|
self._color = FRIENDLY_COLORSPACE.get(pim.colorspace or '')
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
self._color = None
|
self._color = None
|
||||||
if self._enc == Encoding.jpeg2000:
|
if self._enc == Encoding.jpeg2000:
|
||||||
@@ -418,7 +424,7 @@ def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]:
|
|||||||
|
|
||||||
for n, inline in enumerate(contentsinfo.inline_images):
|
for n, inline in enumerate(contentsinfo.inline_images):
|
||||||
yield ImageInfo(
|
yield ImageInfo(
|
||||||
name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline
|
name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline.iimage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -443,7 +449,7 @@ def _image_xobjects(container) -> Iterator[Tuple[Object, str]]:
|
|||||||
xobjs = resources['/XObject'].as_dict()
|
xobjs = resources['/XObject'].as_dict()
|
||||||
for xobj in xobjs:
|
for xobj in xobjs:
|
||||||
candidate: Object = xobjs[xobj]
|
candidate: Object = xobjs[xobj]
|
||||||
if not '/Subtype' in candidate:
|
if '/Subtype' not in candidate:
|
||||||
continue
|
continue
|
||||||
if candidate['/Subtype'] == '/Image':
|
if candidate['/Subtype'] == '/Image':
|
||||||
pdfimage = candidate
|
pdfimage = candidate
|
||||||
@@ -613,7 +619,7 @@ def _pdf_pageinfo_sync_init(pdf: Pdf, infile: Path, pdfminer_loglevel):
|
|||||||
|
|
||||||
# If the pdf is not opened, open a copy for our worker process to use
|
# If the pdf is not opened, open a copy for our worker process to use
|
||||||
if pdf is None:
|
if pdf is None:
|
||||||
worker_pdf = pikepdf.open(infile)
|
worker_pdf = Pdf.open(infile)
|
||||||
|
|
||||||
def on_process_close():
|
def on_process_close():
|
||||||
worker_pdf.close()
|
worker_pdf.close()
|
||||||
@@ -627,7 +633,7 @@ def _pdf_pageinfo_sync(args):
|
|||||||
pdf = thread_pdf if thread_pdf is not None else worker_pdf
|
pdf = thread_pdf if thread_pdf is not None else worker_pdf
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
if not pdf: # When called with SerialExecutor
|
if not pdf: # When called with SerialExecutor
|
||||||
pdf = stack.enter_context(pikepdf.open(infile))
|
pdf = stack.enter_context(Pdf.open(infile))
|
||||||
page = PageInfo(pdf, pageno, infile, check_pages, detailed_analysis)
|
page = PageInfo(pdf, pageno, infile, check_pages, detailed_analysis)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
@@ -871,6 +877,9 @@ class PageInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_EXECUTOR = SerialExecutor()
|
||||||
|
|
||||||
|
|
||||||
class PdfInfo:
|
class PdfInfo:
|
||||||
"""Get summary information about a PDF"""
|
"""Get summary information about a PDF"""
|
||||||
|
|
||||||
@@ -882,13 +891,13 @@ class PdfInfo:
|
|||||||
progbar: bool = False,
|
progbar: bool = False,
|
||||||
max_workers: int = None,
|
max_workers: int = None,
|
||||||
check_pages=None,
|
check_pages=None,
|
||||||
executor: Executor = SerialExecutor(),
|
executor: Executor = DEFAULT_EXECUTOR,
|
||||||
):
|
):
|
||||||
self._infile = infile
|
self._infile = infile
|
||||||
if check_pages is None:
|
if check_pages is None:
|
||||||
check_pages = range(0, 1_000_000_000)
|
check_pages = range(0, 1_000_000_000)
|
||||||
|
|
||||||
with pikepdf.open(infile) as pdf:
|
with Pdf.open(infile) as pdf:
|
||||||
if pdf.is_encrypted:
|
if pdf.is_encrypted:
|
||||||
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
||||||
self._pages = _pdf_pageinfo_concurrent(
|
self._pages = _pdf_pageinfo_concurrent(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from collections.abc import Mapping
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from distutils.version import LooseVersion, Version
|
from distutils.version import LooseVersion, Version
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
|
||||||
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
||||||
from subprocess import run as subprocess_run
|
from subprocess import run as subprocess_run
|
||||||
from typing import Callable, Optional, Type, Union
|
from typing import Callable, Optional, Type, Union
|
||||||
@@ -27,7 +26,9 @@ from ocrmypdf.exceptions import MissingDependencyError
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def run(args, *, env=None, logs_errors_to_stdout=False, **kwargs):
|
def run(
|
||||||
|
args, *, env=None, logs_errors_to_stdout: bool = False, **kwargs
|
||||||
|
) -> CompletedProcess:
|
||||||
"""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
|
||||||
@@ -65,7 +66,9 @@ def run(args, *, env=None, logs_errors_to_stdout=False, **kwargs):
|
|||||||
return proc
|
return proc
|
||||||
|
|
||||||
|
|
||||||
def run_polling_stderr(args, *, callback, check=False, env=None, **kwargs):
|
def run_polling_stderr(
|
||||||
|
args, *, callback: Callable[[str], None], check: bool = False, env=None, **kwargs
|
||||||
|
) -> CompletedProcess:
|
||||||
"""Run a process like ``ocrmypdf.subprocess.run``, and poll stderr.
|
"""Run a process like ``ocrmypdf.subprocess.run``, and poll stderr.
|
||||||
|
|
||||||
Every line of produced by stderr will be forwarded to the callback function.
|
Every line of produced by stderr will be forwarded to the callback function.
|
||||||
@@ -83,6 +86,8 @@ def run_polling_stderr(args, *, callback, check=False, env=None, **kwargs):
|
|||||||
with Popen(args, env=env, **kwargs) as proc:
|
with Popen(args, env=env, **kwargs) as proc:
|
||||||
lines = []
|
lines = []
|
||||||
while proc.poll() is None:
|
while proc.poll() is None:
|
||||||
|
if proc.stderr is None:
|
||||||
|
continue
|
||||||
for msg in iter(proc.stderr.readline, ''):
|
for msg in iter(proc.stderr.readline, ''):
|
||||||
if process_log.isEnabledFor(logging.DEBUG):
|
if process_log.isEnabledFor(logging.DEBUG):
|
||||||
process_log.debug(msg.strip())
|
process_log.debug(msg.strip())
|
||||||
@@ -102,7 +107,7 @@ def _fix_process_args(args, env, kwargs):
|
|||||||
env = os.environ
|
env = os.environ
|
||||||
|
|
||||||
# Search in spoof path if necessary
|
# Search in spoof path if necessary
|
||||||
program = args[0]
|
program = str(args[0])
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
from ocrmypdf.subprocess._windows import fix_windows_args
|
from ocrmypdf.subprocess._windows import fix_windows_args
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from itertools import chain, filterfalse
|
from itertools import chain
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Iterator, Optional, Tuple, TypeVar, cast
|
from typing import Any, Callable, Iterable, Iterator, Set, Tuple, TypeVar
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import winreg
|
import winreg
|
||||||
@@ -113,7 +113,7 @@ SHIMS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def fix_windows_args(program, args, env):
|
def fix_windows_args(program: str, args, env):
|
||||||
"""Adjust our desired program and command line arguments for use on Windows"""
|
"""Adjust our desired program and command line arguments for use on Windows"""
|
||||||
|
|
||||||
if sys.version_info < (3, 8):
|
if sys.version_info < (3, 8):
|
||||||
@@ -137,14 +137,12 @@ def fix_windows_args(program, args, env):
|
|||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
def unique_everseen(iterable, key=None):
|
def unique_everseen(iterable: Iterable[T], key: Callable[[T], T]) -> Iterator[T]:
|
||||||
"List unique elements, preserving order. Remember all elements ever seen."
|
"List unique elements, preserving order."
|
||||||
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||||
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||||
seen = set()
|
seen: Set[T] = set()
|
||||||
seen_add = seen.add
|
seen_add = seen.add
|
||||||
if key is None:
|
|
||||||
key = lambda x: x
|
|
||||||
for element in iterable:
|
for element in iterable:
|
||||||
k = key(element)
|
k = key(element)
|
||||||
if k not in seen:
|
if k not in seen:
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ def outpdf(tmp_path):
|
|||||||
return tmp_path / 'out.pdf'
|
return tmp_path / 'out.pdf'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def outtxt(tmp_path):
|
||||||
|
return tmp_path / 'out.txt'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def no_outpdf(tmp_path):
|
def no_outpdf(tmp_path):
|
||||||
"""This just documents the fact that a test is not expected to produce
|
"""This just documents the fact that a test is not expected to produce
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ def test_jpeg_in_jpeg_out(resources, outpdf):
|
|||||||
'tests/plugins/tesseract_noop.py',
|
'tests/plugins/tesseract_noop.py',
|
||||||
)
|
)
|
||||||
with pikepdf.open(outpdf) as pdf:
|
with pikepdf.open(outpdf) as pdf:
|
||||||
assert next(pdf.pages[0].images.values()).Filter == pikepdf.Name.DCTDecode
|
assert next(iter(pdf.pages[0].images.values())).Filter == pikepdf.Name.DCTDecode
|
||||||
|
|||||||
+31
-6
@@ -745,14 +745,13 @@ def test_pdfa_n(pdfa_level, resources, outpdf):
|
|||||||
assert pdfa_info['conformance'] == f'PDF/A-{pdfa_level}B'
|
assert pdfa_info['conformance'] == f'PDF/A-{pdfa_level}B'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
def test_decompression_bomb_error(resources, outpdf):
|
||||||
PIL.__version__ < '5.0.0', reason="Pillow < 5.0.0 doesn't raise the exception"
|
|
||||||
)
|
|
||||||
@pytest.mark.slow
|
|
||||||
def test_decompression_bomb(resources, outpdf):
|
|
||||||
p, _out, err = run_ocrmypdf(resources / 'hugemono.pdf', outpdf)
|
p, _out, err = run_ocrmypdf(resources / 'hugemono.pdf', outpdf)
|
||||||
assert 'decompression bomb' in err
|
assert 'decompression bomb' in err and '--max-image-mpixels' in err
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_decompression_bomb_succeeds(resources, outpdf):
|
||||||
p, _out, err = run_ocrmypdf(
|
p, _out, err = run_ocrmypdf(
|
||||||
resources / 'hugemono.pdf', outpdf, '--max-image-mpixels', '2000'
|
resources / 'hugemono.pdf', outpdf, '--max-image-mpixels', '2000'
|
||||||
)
|
)
|
||||||
@@ -881,3 +880,29 @@ def test_image_dpi_threshold(resources, outpdf):
|
|||||||
'tests/plugins/tesseract_noop.py',
|
'tests/plugins/tesseract_noop.py',
|
||||||
)
|
)
|
||||||
assert outpdf.exists()
|
assert outpdf.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_outputtype_none_bad_setup(resources, outpdf):
|
||||||
|
p, _out, err = run_ocrmypdf(
|
||||||
|
resources / 'trivial.pdf',
|
||||||
|
outpdf,
|
||||||
|
'--output-type=none',
|
||||||
|
'--plugin',
|
||||||
|
'tests/plugins/tesseract_noop.py',
|
||||||
|
)
|
||||||
|
assert p.returncode == ExitCode.bad_args
|
||||||
|
assert 'Set the output file to' in err
|
||||||
|
|
||||||
|
|
||||||
|
def test_outputtype_none(resources, outtxt):
|
||||||
|
p, _out, err = run_ocrmypdf(
|
||||||
|
resources / 'trivial.pdf',
|
||||||
|
os.devnull,
|
||||||
|
'--output-type=none',
|
||||||
|
'--sidecar',
|
||||||
|
outtxt,
|
||||||
|
'--plugin',
|
||||||
|
'tests/plugins/tesseract_noop.py',
|
||||||
|
)
|
||||||
|
assert p.returncode == ExitCode.ok
|
||||||
|
assert outtxt.exists()
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ def test_srgb_in_unicode_path(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_kodak_toc(resources, outpdf):
|
def test_kodak_toc(resources, outpdf):
|
||||||
_output = check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
resources / 'kcs.pdf',
|
resources / 'kcs.pdf',
|
||||||
outpdf,
|
outpdf,
|
||||||
'--output-type',
|
'--output-type',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
@@ -237,6 +238,13 @@ def test_version_comparison():
|
|||||||
need_version='4.0.0',
|
need_version='4.0.0',
|
||||||
version_parser=TesseractVersion,
|
version_parser=TesseractVersion,
|
||||||
)
|
)
|
||||||
|
vd.check_external_program(
|
||||||
|
program="tesseract",
|
||||||
|
package="tesseract",
|
||||||
|
version_checker=lambda: '5.0.0-rc1.20211030',
|
||||||
|
need_version='4.0.0',
|
||||||
|
version_parser=TesseractVersion,
|
||||||
|
)
|
||||||
vd.check_external_program(
|
vd.check_external_program(
|
||||||
program="tesseract",
|
program="tesseract",
|
||||||
package="tesseract",
|
package="tesseract",
|
||||||
@@ -298,3 +306,8 @@ def test_sidecar_equals_output(resources, no_outpdf):
|
|||||||
op = no_outpdf
|
op = no_outpdf
|
||||||
with pytest.raises(BadArgsError, match=r'--sidecar'):
|
with pytest.raises(BadArgsError, match=r'--sidecar'):
|
||||||
run_ocrmypdf_api(resources / 'trivial.pdf', op, '--sidecar', op)
|
run_ocrmypdf_api(resources / 'trivial.pdf', op, '--sidecar', op)
|
||||||
|
|
||||||
|
|
||||||
|
def test_devnull_sidecar(resources):
|
||||||
|
with pytest.raises(BadArgsError, match=r'--sidecar.*NUL'):
|
||||||
|
run_ocrmypdf_api(resources / 'trivial.pdf', os.devnull, '--sidecar')
|
||||||
|
|||||||
Reference in New Issue
Block a user