Compare commits

...
22 Commits
Author SHA1 Message Date
James R. Barlow e2847ea4c3 v8.1.0 release notes 2019-02-10 02:10:48 -08:00
James R. Barlow 19e35db2b7 Fix issue when weave handoff occurs with no OCR font present
If using --tesseract-timeout 0 and any image processing on a file with
more than 100 pages, the weave handoff will occur. Ensure this
works correctly even if no Glyphless font is present.

Closes #347
2019-02-10 02:05:59 -08:00
James R. Barlow df688742d5 Fix exception on traversing corrupt ToC entries 2019-02-10 00:50:21 -08:00
James R. Barlow 42c2925f9d Activate black precommit 2019-02-08 14:09:08 -08:00
James R. Barlow 933f0b8f9b docs: more unpaper details 2019-02-08 13:05:09 -08:00
James R. Barlow 03ab5a8ee2 If --tesseract-timeout 0, say nothing when we time out
This is our "don't actually OCR" mode. No need to mention it.
2019-02-08 13:04:48 -08:00
James R. Barlow 4f06920224 Be os.nice()-r 2019-02-07 17:24:47 -08:00
James R. Barlow a733b09623 webservice: add an optional config and larger upload limit 2019-02-07 17:24:17 -08:00
James R. Barlow 5483dacf52 Fuzz 2019-02-07 17:09:47 -08:00
James R. Barlow ae7844ad88 --clean-final implies --clean
It's never made sense to leave it out before; might as well introduce it.
2019-02-07 17:08:08 -08:00
James R. Barlow a6e7485da6 docs: --unpaper-args 2019-02-07 17:06:51 -08:00
James R. Barlow 3bcc6d6121 Merge 'feature/unpaper-args' 2019-02-07 17:06:28 -08:00
James R. Barlow 9fe067bbd9 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2019-02-07 16:31:06 -08:00
James R. Barlow f095e91cb4 unpaper-args: add test case and harden feature 2019-02-07 16:21:02 -08:00
Charles Forceyandjbarlow83 66c8d4b47a Adjust the docker pull command for webservice (#346)
Not completely sure this is correct, but I think `docker pull jbarlow83/ocrmypdf-webservice` might be the correct command for getting the web service version.  It installs as expected:

```
docker pull jbarlow83/ocrmypdf-webservice
Using default tag: latest
latest: Pulling from jbarlow83/ocrmypdf-webservice
38e2e6cd5626: Already exists 
705054bc3f5b: Already exists 
c7051e069564: Already exists 
7308e914506c: Already exists 
3977c3cd82d1: Already exists 
ec01b9573956: Already exists 
b508b5192a3c: Already exists 
ace6e737fffb: Already exists 
0a453ee84e11: Already exists 
f8cb8b66151b: Already exists 
f53c3b27b23f: Already exists 
22df51ea5473: Already exists 
e38d932f9f30: Already exists 
b9d3c1d5b53b: Already exists 
68be2088ada3: Already exists 
8b17945ab41b: Pull complete 
59c4aae491bd: Pull complete 
19dce698a07e: Pull complete 
Digest: sha256:0cc9433d490c9a65389403757bf6081a30bcd248055340a8789c23d9cdf9ac8a
Status: Downloaded newer image for jbarlow83/ocrmypdf-webservice:latest
```
2019-01-25 10:41:42 -08:00
James R. Barlow 721489a06c docs: remove reference to --skip-repair since the argument was removed 2019-01-18 05:44:11 -08:00
James R. Barlow 9a4493f211 Add --unpaper-args
Needs test code and stricter validation
2019-01-18 05:33:28 -08:00
James R. Barlow edb4d6c586 docs: Clarify ArchLinux edition is in AUR 2019-01-18 05:29:37 -08:00
James R. Barlow b8cd3acd9e v8.0.1 notes 2019-01-17 00:57:28 -08:00
James R. Barlow 03779e33da docs: Update some install procedures for v8 changes
[ci skip]
2019-01-12 00:33:36 -08:00
James R. Barlow c466483e82 docs: Explain intermediate files 2019-01-11 14:52:05 -08:00
James R. Barlow e3a58219d1 Ensure XObjects with no subtype don't cause an exception
Closes #325
2019-01-08 16:46:08 -08:00
18 changed files with 410 additions and 109 deletions
+5
View File
@@ -16,4 +16,9 @@ COPY .docker/webservice.py /application
USER docker
VOLUME ["/config"]
# This config file is optional
ENV OCRMYPDF_WEBSERVICE_SETTINGS "/config/config.py"
ENTRYPOINT ["python3", "/application/webservice.py"]
+6 -4
View File
@@ -32,11 +32,9 @@ import shlex
app = Flask(__name__)
app.secret_key = "secret"
app.config['MAX_CONTENT_LENGTH'] = 50_000_000
app.config.from_envvar("OCRMYPDF_WEBSERVICE_SETTINGS", silent=True)
uploaddir = TemporaryDirectory(prefix="ocrmypdf-upload")
downloaddir = TemporaryDirectory(prefix="ocrmypdf-download")
app.config["UPLOAD_FOLDER"] = uploaddir
ALLOWED_EXTENSIONS = set(["pdf"])
@@ -45,9 +43,13 @@ def allowed_file(filename):
def do_ocrmypdf(file):
uploaddir = TemporaryDirectory(prefix="ocrmypdf-upload")
downloaddir = TemporaryDirectory(prefix="ocrmypdf-download")
filename = secure_filename(file.filename)
up_file = os.path.join(uploaddir.name, filename)
file.save(up_file)
down_file = os.path.join(downloaddir.name, filename)
cmd_args = [arg for arg in shlex.split(request.form["params"])]
+6
View File
@@ -0,0 +1,6 @@
repos:
- repo: https://github.com/ambv/black
rev: stable
hooks:
- id: black
language_version: python3.6
+50 -1
View File
@@ -1,6 +1,34 @@
Advanced features
=================
Control of unpaper
------------------
OCRmyPDF uses ``unpaper`` to provide the implementation of the ``--clean`` and ``--clean-final`` arguments. `unpaper <https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md>`_ provides a variety of image processing filters to improve images.
By default, OCRmyPDF uses only ``unpaper`` arguments that were found to be safe to use on almost all files without having to inspect every page of the file afterwards. This is particularly true when only ``--clean`` is used, since that instructs OCRmyPDF to only clean the image before OCR and not the final image.
However, if you wish to use the more aggressive options in ``unpaper``, you may use ``--unpaper-args '...'`` to override the OCRmyPDF's defaults and forward other arguments to unpaper. This option will forward arguments to ``unpaper`` without any knowledge of what that program considers to be valid arguments. The string of arguments must be quoted as shown in the examples below. No filename arguments may be included. OCRmyPDF will assume it can append input and output filename of intermediate images to the ``--unpaper-args`` string.
In this example, we tell ``unpaper`` to expect two pages of text on a sheet (image), such as occurs when two facing pages of a book are scanned. ``unpaper`` uses this information to deskew each independently and clean up the margins of both.
.. code-block:: bash
ocrmypdf --clean --clean-final --unpaper-args '--layout double' input.pdf output.pdf
ocrmypdf --clean --clean-final --unpaper-args '--layout double --no-noisefilter' input.pdf output.pdf
.. warning::
Some ``unpaper`` features will reposition text within the image. ``--clean-final`` is recommended to avoid this issue.
.. warning::
Some ``unpaper`` features cause multiple input or output files to be consumed or produced. OCRmyPDF requires ``unpaper`` to consume one file and produce one file. An deviation from that condition will result in errors.
.. note::
``unpaper`` uses uncompressed PBM/PGM/PPM files for its intermediate files. For large images or documents, it can take a lot of temporary disk space.
Control of OCR options
----------------------
@@ -17,7 +45,6 @@ If ``--redo-ocr`` is issued, then a detailed text analysis is performed. Text is
If ``--force-ocr`` is issued, then all pages will be rasterized to images, discarding any hidden OCR text, and rasterizing any printable text. This is useful for redoing OCR, for fixing OCR text with a damaged character map (text is selectable but not searchable), and destroying redacted information. Any forms and vector graphics will be rasterized as well.
Time and image size limits
""""""""""""""""""""""""""
@@ -189,3 +216,25 @@ user interface. They may be imported from ``ocrmypdf.exceptions``.
* - 130
- ``ExitCode.ctrl_c``
- The program was interrupted by pressing Ctrl+C.
Debugging the intermediate files
--------------------------------
OCRmyPDF normally saves its intermediate results to a temporary folder and deletes this folder when it exits, whether it succeeded or failed.
If the ``-k`` argument is issued on the command line, OCRmyPDF will keep the temporary folder and print the location, whether it succeeded or failed (provided the Python interpreter did not crash). An example message is:
.. code-block::
Temporary working files saved at:
/tmp/com.github.ocrmypdf.u20wpz07
The organization of this folder is an implementation detail and subject to change between releases. However the general organization is that working files on a per page basis have the page number as a prefix (starting with page 1), an infix indicates the processing stage, and a suffix indicates the file type. Some important files include:
* ``.page.png`` - what the input page looks like
* ``.image`` - the image we will show the user if we are in a mode that changes the final appearance; may be in one of several image formats
* ``.text.pdf`` - the OCR file; this will load as a blank page but should have visible text if checked with a tool like pdftotext or pdfminder.six
* ``.ocr.png`` - the file that is sent to Tesseract for OCR; depending on arguments this may differ from the presentation image
* ``layers.rendered.pdf`` - the composite PDF, before metadata repair and optimization
* ``images/*`` - images extracted during the optimization process; here the prefix indicates a PDF object ID not a page number
+5 -7
View File
@@ -18,12 +18,12 @@ The ``--tag`` argument tells parallel to print the filename as a prefix whenever
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
OCRmyPDF automatically repairs PDFs before parsing and gathering information from them. If you are already repairing PDFs with ``qpdf`` prior to attempting OCR, or you can use ``--skip-repair`` to skip this step. It may improve performance for large files, since repairing PDFs is single-threaded.
OCRmyPDF automatically repairs PDFs before parsing and gathering information from them.
Directory trees
---------------
This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes
This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes
.. code-block:: bash
@@ -66,7 +66,7 @@ This user contributed script also provides an example of batch processing.
log_file = script_dir + '/ocr-tree.log'
logging.basicConfig(
level=logging.INFO, format='%(asctime)s %(message)s',
level=logging.INFO, format='%(asctime)s %(message)s',
filename=log_file, filemode='w')
for dir_name, subdirs, file_list in os.walk(start_dir):
@@ -144,11 +144,11 @@ This is only possible for x86-based Synology products. Some Synology products us
timestamp_OCR = time.strftime("%Y-%m-%d-%H%M_OCR_")
filename_OCR = timestamp_OCR + file_noext + '.pdf'
docker_mount = dir_name + ':/home/docker'
# create string for pdf processing
# create string for pdf processing
# diskstation needs a user:group docker:docker. find uid:gid of your diskstation docker:docker with id docker.
# use this uid:gid in -u flag
# rw rights for docker:docker at source dir are also necessary
# the script is processed as root user via chron
# the script is processed as root user via chron
cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u=1030:65538', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR]
logging.info(cmd)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
@@ -210,5 +210,3 @@ Alternatives
""""""""""""
* `Watchman <https://facebook.github.io/watchman/>`_ is a more powerful alternative to ``watchmedo``.
+58 -37
View File
@@ -6,9 +6,12 @@ Installation
|latest|
The easiest way to install OCRmyPDF to follow the steps for your operating system/platform.
The easiest way to install OCRmyPDF is to follow the steps for your operating
system/platform, although sometimes this version may be out of date.
If you want to use the latest version of OCRmyPDF, your best bet is to install the most recent version your platform provides, and then upgrade that version by installing the Python binary wheels.
If you want to use the latest version of OCRmyPDF, your best bet is to install
the most recent version your platform provides, and then upgrade that version by
installing the Python binary wheels.
.. contents:: Platform-specific steps
:depth: 2
@@ -136,22 +139,56 @@ To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 16.04 LTS
^^^^^^^^^^^^^^^^
No package is currently available for Ubuntu 16.04, but you can install the dependencies manually:
No package is available for Ubuntu 16.04. OCRmyPDF 8.0 and newer require Python
3.6. Ubuntu 16.04 ships Python 3.5, but you can install Python 3.6 on it. Or,
you can skip Python 3.6 and install OCRmyPDF 7.x or older - for that procedure,
please see the installation documentation for the version of OCRmyPDF you plan
to use.
**Install system packages for OCRmyPDF**
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
sudo apt-get install -y software-properties-common python-software-properties
sudo add-apt-repository -y \
ppa:jonathonf/python-3.6 \
ppa:alex-p/tesseract-ocr
sudo apt-get update
sudo apt-get install -y \
ghostscript \
libexempi3 \
libffi6 \
pngquant \
python3-cffi \
python3-pip \
python3.6 \
qpdf \
tesseract-ocr \
unpaper
If you wish install OCRmyPDF for the current user, and ensure that the ``PATH``
This will install a Python 3.6 binary at ``/usr/bin/python3.6`` alongside the
system's Python 3.5. Do not remove the system Python. This will also install
Tesseract 4.0 from a PPA, since the version available in Ubuntu 16.04 is too old
for OCRmyPDF.
Now install pip for Python 3.6. This will install the Python 3.6 version of
``pip`` at ``/usr/local/bin/pip``.
.. code-block:: bash
curl https://bootstrap.pypa.io/get-pip.py | sudo python3.6
**Install OCRmyPDF**
OCRmyPDF requires the locale to be set for UTF-8. **On some minimal Ubuntu
installations systems**, it may be necessary to set the locale.
.. code-block:: bash
# Optional: Only need to set these if they are not already set
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
Now install OCRmyPDF for the current user, and ensure that the ``PATH``
environment variable contains ``$HOME/.local/bin``.
.. code-block:: bash
@@ -159,38 +196,20 @@ environment variable contains ``$HOME/.local/bin``.
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
Alternately, you can install ocrmypdf system-wide. (Not recommended.)
.. code-block:: bash
sudo pip3 install ocrmypdf
At your option, you may upgrade Ubuntu 16.04 LTS to Tesseract 4.0 for improved OCR results.
.. code-block:: bash
sudo apt-get install -y software-properties-common python-software-properties
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
sudo apt-get update
sudo apt-get upgrade tesseract-ocr
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 14.04 LTS
^^^^^^^^^^^^^^^^
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because it is older and does not provide ``pip``.
Update apt-get:
.. code-block:: bash
sudo apt-get update
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other
options, because of its age. Several backports are required. For explanations of
some steps of this procedure, see the similar steps for Ubuntu 16.04.
Install system dependencies:
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
software-properties-common python-software-properties \
zlib1g-dev \
@@ -200,9 +219,13 @@ Install system dependencies:
pngquant \
qpdf
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1), Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and Tesseract 3.x on your system. Python 3.6 will be installed alongside the system Python 3.4.
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1),
Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and
Tesseract 3.x on your system. Python 3.6 will be installed alongside the system
Python 3.4.
If you prefer to not modify your system in this matter, consider using a Docker container.
If you prefer to not modify your system in this matter, consider using a Docker
container.
.. code-block:: bash
@@ -228,8 +251,6 @@ Now we need to install ``pip`` and let it install ocrmypdf:
curl https://bootstrap.pypa.io/ez_setup.py -o - | python3.6 && python3.6 -m easy_install pip
pip3.6 install ocrmypdf
The ``wget`` command will download a program and run it.
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
@@ -239,14 +260,14 @@ These installation instructions omit the optional dependency ``unpaper``, which
To add JBIG2 encoding, see :ref:`jbig2`.
ArchLinux
^^^^^^^^^
ArchLinux (AUR)
^^^^^^^^^^^^^^^
.. image:: https://repology.org/badge/version-for-repo/aur/ocrmypdf.svg
:alt: ArchLinux
:target: https://repology.org/metapackage/ocrmypdf
The author is aware of an `ArchLinux User Repository package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. You can use the following command.
There is an `ArchLinux User Repository package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. You can use the following command.
.. code-block:: bash
@@ -378,7 +399,7 @@ Assuming you have a Docker engine running, you can download one of the three ava
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- As above, with all available language packs.
* - ocrmypdf-webservice
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- ``docker pull jbarlow83/ocrmypdf-webservice``
- All language packs, and a simple HTTP wrapper allowing OCRmyPDF to be used as a web service. Note that this component is licensed under AGPLv3.
For example:
+19
View File
@@ -13,6 +13,25 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
find: [^`]\#([0-9]{1,3})[^0-9]
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
v8.1.0
------
- Added a feature, ``--unpaper-args``, which allows passing arbitrary arguments to ``unpaper`` when using ``--clean`` or ``--clean-final``. The default, very conservative unpaper settings are suppressed.
- The argument ``--clean-final`` now implies ``--clean``. It was possible to issue ``--clean-final`` on its before this, but it would have no useful effect.
- Fixed an exception on traversing corrupt table of contents entries (specifically, those with invalid destination objects)
- Fixed an issue when using ``--tesseract-timeout`` and image processing features on a file with more than 100 pages. `#347 <https://github.com/jbarlow83/OCRmyPDF/issues/347>`_
- OCRmyPDF now always calls ``os.nice(5)`` to signal to operating systems that it is a background process.
v8.0.1
------
- Fixed an exception when parsing PDFs that are missing a required field. `#325 <https://github.com/jbarlow83/OCRmyPDF/issues/325>`_
- pikepdf 1.0.5 is now required, to address some other PDF parsing issues.
v8.0.0
------
+22
View File
@@ -0,0 +1,22 @@
[tool.black]
line-length = 88
py36 = true
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| docs
| misc
| \.egg-info
)/
'''
+1 -1
View File
@@ -251,7 +251,7 @@ setup(
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
'pdfminer.six == 20181108 ; sys_platform != "darwin"',
'pikepdf >= 1.0.2, < 2',
'pikepdf >= 1.0.5, < 2',
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
+22
View File
@@ -278,6 +278,13 @@ preprocessing.add_argument(
help="Clean page as above, and incorporate the cleaned image in the final "
"PDF. Might remove desired content.",
)
preprocessing.add_argument(
'--unpaper-args',
type=str,
default=None,
help="A quoted string of arguments to pass to unpaper. Requires --clean. "
"Example: --unpaper-args '--layout double'.",
)
preprocessing.add_argument(
'--oversample',
metavar='DPI',
@@ -623,12 +630,25 @@ def _optional_program_recommended(name, version_fn, min_version, for_argument):
def check_options_preprocessing(options, log):
if options.clean_final:
options.clean = True
if options.unpaper_args and not options.clean:
raise argparse.ArgumentError(
None, "--clean is required for --unpaper-args"
)
if any((options.clean, options.clean_final)):
from .exec import unpaper
_optional_program_required(
'unpaper', unpaper.version, '6.1', '--clean, --clean-final'
)
try:
if options.unpaper_args:
options.unpaper_args = unpaper.validate_custom_args(
options.unpaper_args
)
except Exception as e:
raise argparse.ArgumentError(None, str(e))
def check_options_ocr_behavior(options, log):
@@ -1081,6 +1101,8 @@ def run_pipeline(args=None):
build_pipeline(options, work_folder, _log, context)
atexit.register(cleanup_working_files, work_folder, options)
if hasattr(os, 'nice'):
os.nice(5)
cmdline.run(options)
except ruffus_exceptions.RethrownJobError as e:
if options.verbose:
+2 -2
View File
@@ -566,7 +566,7 @@ def preprocess_clean(input_file, output_file, log, context):
pageinfo = get_pageinfo(input_file, context)
dpi = get_page_square_dpi(pageinfo, options)
unpaper.clean(input_file, output_file, dpi, log)
unpaper.clean(input_file, output_file, dpi, log, options.unpaper_args)
def select_ocr_image(infiles, output_file, log, context):
@@ -648,7 +648,7 @@ def ocr_tesseract_hocr(input_file, output_files, log, context):
def select_visible_page_image(infiles, output_file, log, context):
"Selects a whole page image that we can show the user (if necessary)"
"""Selects a whole page image that we can show the user (if necessary)"""
options = context.get_options()
if options.clean_final:
+19 -11
View File
@@ -17,6 +17,7 @@
from itertools import groupby
from pathlib import Path
import os
import pikepdf
@@ -24,8 +25,11 @@ from .exec import tesseract
from .helpers import flatten_groups, page_number
MAX_OPEN_PAGE_PDFS = int(os.environ.get('_OCRMYPDF_MAX_OPEN_PAGE_PDFS', 100))
def _update_page_resources(*, page, font, font_key, procset):
"Update this page's fonts with a reference to the Glyphless font"
"""Update this page's fonts with a reference to the Glyphless font"""
if '/Resources' not in page:
page['/Resources'] = pikepdf.Dictionary({})
@@ -34,7 +38,7 @@ def _update_page_resources(*, page, font, font_key, procset):
fonts = resources['/Font']
except KeyError:
fonts = pikepdf.Dictionary({})
if font_key not in fonts:
if font_key is not None and font_key not in fonts:
fonts[font_key] = font
resources['/Font'] = fonts
@@ -160,7 +164,7 @@ def _weave_layers_graft(
def _find_font(text, pdf_base):
"Copy a font from the filename text into pdf_base"
"""Copy a font from the filename text into pdf_base"""
font, font_key = None, None
possible_font_names = ('/f-0-0', '/F1')
@@ -177,6 +181,8 @@ def _find_font(text, pdf_base):
break
if pdf_text_font:
font = pdf_base.copy_foreign(pdf_text_font)
if font_key is None:
print('font_key is None')
return font, font_key
@@ -245,12 +251,14 @@ def _fix_toc(pdf_base, pageref_remap, log):
Inner helper function: change the objgen for any page from the old we
invalidated to its new one.
"""
if not isinstance(dest_node, pikepdf.Array):
return
pageref = dest_node[0]
if pageref['/Type'] == '/Page' and pageref.objgen in pageref_remap:
new_objgen = pageref_remap[pageref.objgen]
dest_node[0] = pdf_base.get_object(new_objgen)
try:
pageref = dest_node[0]
if pageref['/Type'] == '/Page' and pageref.objgen in pageref_remap:
new_objgen = pageref_remap[pageref.objgen]
dest_node[0] = pdf_base.get_object(new_objgen)
except (IndexError, TypeError) as e:
log.warning("This file may contain invalid table of contents entries")
log.debug(e)
def visit_remap_dest(pdf_base, node, log):
"""
@@ -390,7 +398,7 @@ def weave_layers(infiles, output_file, log, context):
content_rotation - autorotate_correction
) % 360
if len(keep_open) > 100:
if len(keep_open) > MAX_OPEN_PAGE_PDFS:
# qpdf limitations require us to keep files open when we intend
# to copy content from them before saving. However, we want to keep
# a lid on file handles and memory usage, so for big files we're
@@ -407,7 +415,7 @@ def weave_layers(infiles, output_file, log, context):
pdf_base = pikepdf.open(interim)
procset = pdf_base.pages[0].Resources.ProcSet
font = pdf_base.pages[0].Resources.Font.get(font_key)
font, font_key = None, None # Reacquire this information
_fix_toc(pdf_base, pagerefs, log)
pdf_base.save(output_file)
+5 -3
View File
@@ -200,7 +200,9 @@ def tesseract_log_output(log, stdout, input_file):
log.info(prefix + line.strip())
def page_timedout(log, input_file):
def page_timedout(log, input_file, timeout):
if timeout == 0:
return
prefix = f"{(page_number(input_file)):4d}: [tesseract] "
log.warning(prefix + " took too long to OCR - skipping")
@@ -257,7 +259,7 @@ def generate_hocr(
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
# it does not have a valid hOCR file.
page_timedout(log, input_file)
page_timedout(log, input_file, timeout)
_generate_null_hocr(output_hocr, output_sidecar, input_file)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
@@ -347,7 +349,7 @@ def generate_pdf(
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_text)
except TimeoutExpired:
page_timedout(log, input_image)
page_timedout(log, input_image, timeout)
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image)
+55 -32
View File
@@ -19,13 +19,15 @@
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
import os
import shlex
import subprocess
import sys
from functools import lru_cache
from subprocess import STDOUT, CalledProcessError, check_output
from tempfile import NamedTemporaryFile
from subprocess import PIPE, STDOUT, CalledProcessError
from tempfile import TemporaryDirectory
from . import get_version
from ..exceptions import MissingDependencyError
from ..exceptions import MissingDependencyError, SubprocessOutputError
try:
from PIL import Image
@@ -64,43 +66,64 @@ def run(input_file, output_file, dpi, log, mode_args):
im.close()
raise MissingDependencyError() from e
with NamedTemporaryFile(suffix=suffix) as input_pnm, NamedTemporaryFile(
suffix=suffix, mode="r+b"
) as output_pnm:
with TemporaryDirectory() as tmpdir:
input_pnm = os.path.join(tmpdir, f'input{suffix}')
output_pnm = os.path.join(tmpdir, f'output{suffix}')
im.save(input_pnm, format='PPM')
im.close()
os.unlink(output_pnm.name)
args_unpaper.extend([input_pnm.name, output_pnm.name])
# To prevent any shenanigans from accepting arbitrary parameters in
# --unpaper-args, we:
# 1) run with cwd set to a tmpdir with only unpaper's files
# 2) forbid the use of '/' in arguments, to prevent changing paths
# 3) append absolute paths for the input and output file
# This should ensure that a user cannot clobber some other file with
# their unpaper arguments (whether intentionally or otherwise)
args_unpaper.extend([input_pnm, output_pnm])
try:
stdout = check_output(
args_unpaper, close_fds=True, universal_newlines=True, stderr=STDOUT
proc = subprocess.run(
args_unpaper,
check=True,
close_fds=True,
universal_newlines=True,
stderr=STDOUT,
cwd=tmpdir,
stdout=PIPE,
)
except CalledProcessError as e:
log.debug(e.output)
raise e from e
else:
log.debug(stdout)
# unpaper sets dpi to 72
Image.open(output_pnm.name).save(output_file, dpi=(dpi, dpi))
log.debug(proc.stdout)
# unpaper sets dpi to 72; fix this
try:
Image.open(output_pnm).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 clean(input_file, output_file, dpi, log):
run(
input_file,
output_file,
dpi,
log,
[
'--layout',
'none',
'--mask-scan-size',
'100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-deskew', # don't deskew
],
)
def validate_custom_args(args: str):
unpaper_args = shlex.split(args)
if any('/' in arg for arg in unpaper_args):
raise ValueError('No filenames allowed in --unpaper-args')
return unpaper_args
def clean(input_file, output_file, dpi, log, unpaper_args=None):
default_args = [
'--layout',
'none',
'--mask-scan-size',
'100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-deskew', # don't deskew
]
if not unpaper_args:
unpaper_args = default_args
run(input_file, output_file, dpi, log, unpaper_args)
+2
View File
@@ -391,6 +391,8 @@ def _image_xobjects(container):
xobjs = resources['/XObject'].as_dict()
for xobj in xobjs:
candidate = xobjs[xobj]
if not '/Subtype' in candidate:
continue
if candidate['/Subtype'] == '/Image':
pdfimage = candidate
yield (pdfimage, xobj)
+1 -1
View File
@@ -147,7 +147,7 @@ def no_outpdf(tmpdir):
@pytest.helpers.register
def check_ocrmypdf(input_file, output_file, *args, env=None):
"Run ocrmypdf and confirmed that a valid file was created"
"""Run ocrmypdf and confirmed that a valid file was created"""
p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env)
# ensure py.test collects the output, use -s to view
+68 -10
View File
@@ -15,11 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import argparse
from os import fspath
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from ocrmypdf import __main__ as main
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec import unpaper
# pytest.helpers is dynamic
# pylint: disable=no-member
@@ -30,26 +35,79 @@ run_ocrmypdf = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof
@pytest.fixture(scope='session')
def have_unpaper():
try:
unpaper.version()
except Exception:
return False
else:
return True
@pytest.fixture(scope="session")
def spoof_unpaper_oldversion(tmpdir_factory):
return spoof(tmpdir_factory, unpaper='unpaper_oldversion.py')
return spoof(tmpdir_factory, unpaper="unpaper_oldversion.py")
@pytest.mark.skipif(True, reason="needs new fixture implementation")
def test_no_unpaper(resources, no_outpdf):
# <disable unpaper here>
p, out, err = run_ocrmypdf(
resources / 'c02-22.pdf', no_outpdf, '--clean', env=os.environ
)
assert p.returncode == ExitCode.missing_dependency
input_ = fspath(resources / "c02-22.pdf")
output = fspath(no_outpdf)
options = main.parser.parse_args(args=["--clean", input_, output])
with patch("ocrmypdf.exec.unpaper.version") as mock_unpaper_version:
mock_unpaper_version.side_effect = FileNotFoundError("unpaper")
with pytest.raises(SystemExit):
main.check_options(options, log=MagicMock())
def test_old_unpaper(spoof_unpaper_oldversion, resources, no_outpdf):
p, out, err = run_ocrmypdf(
resources / 'c02-22.pdf', no_outpdf, '--clean', env=spoof_unpaper_oldversion
resources / "c02-22.pdf", no_outpdf, "--clean", env=spoof_unpaper_oldversion
)
assert p.returncode == ExitCode.missing_dependency
@pytest.mark.skipif(not have_unpaper(), reason="requires unpaper")
def test_clean(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'skew.pdf', outpdf, '-c', env=spoof_tesseract_noop)
check_ocrmypdf(resources / "skew.pdf", outpdf, "-c", env=spoof_tesseract_noop)
@pytest.mark.skipif(not have_unpaper(), reason="requires unpaper")
def test_unpaper_args_valid(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(
resources / "skew.pdf",
outpdf,
"-c",
"--unpaper-args",
"--layout double", # Spaces required here
env=spoof_tesseract_noop,
)
@pytest.mark.skipif(not have_unpaper(), reason="requires unpaper")
def test_unpaper_args_invalid_filename(spoof_tesseract_noop, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / "skew.pdf",
outpdf,
"-c",
"--unpaper-args",
"/etc/passwd",
env=spoof_tesseract_noop,
)
assert "No filenames allowed" in err
assert p.returncode == ExitCode.bad_args
@pytest.mark.skipif(not have_unpaper(), reason="requires unpaper")
def test_unpaper_args_invalid(spoof_tesseract_noop, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / "skew.pdf",
outpdf,
"-c",
"--unpaper-args",
"unpaper is not going to like these arguments",
env=spoof_tesseract_noop,
)
# Can't tell difference between unpaper choking on bad arguments or some
# other unpaper failure
assert p.returncode == ExitCode.child_process_error
+64
View File
@@ -0,0 +1,64 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from unittest.mock import MagicMock
import logging
import os
import pytest
import pikepdf
from ocrmypdf._weave import _fix_toc, _update_page_resources
check_ocrmypdf = pytest.helpers.check_ocrmypdf
def test_invalid_toc(resources, outdir, caplog):
pdf = pikepdf.open(resources / 'toc.pdf')
# Corrupt a TOC entry
pdf.Root.Outlines.Last.Dest = pikepdf.Array([None, 0.0, 0.1, 0.2])
pdf.save(outdir / 'test.pdf')
pdf = pikepdf.open(outdir / 'test.pdf')
remap = {}
remap[pdf.pages[0].objgen] = pdf.pages[0].objgen # Dummy remap
# Confirm we complain about the TOC and don't throw an exception
log = logging.getLogger()
_fix_toc(pdf, remap, log)
assert 'invalid table of contents entries' in caplog.text
def test_no_glyphless_weave(resources, outdir):
pdf = pikepdf.open(resources / 'francais.pdf')
pdf_aspect = pikepdf.open(resources / 'aspect.pdf')
pdf_cmyk = pikepdf.open(resources / 'cmyk.pdf')
pdf.pages.extend(pdf_aspect.pages)
pdf.pages.extend(pdf_cmyk.pages)
pdf.save(outdir / 'test.pdf')
env = os.environ.copy()
env['_OCRMYPDF_MAX_OPEN_PAGE_PDFS'] = '2'
check_ocrmypdf(
outdir / 'test.pdf',
outdir / 'out.pdf',
'--deskew',
'--tesseract-timeout',
'0',
env=env,
)