Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f51ed9d01 | ||
|
|
731e6792c7 | ||
|
|
c35ec0b4aa | ||
|
|
03aaf575dc | ||
|
|
de939951d4 | ||
|
|
7725d16a26 | ||
|
|
8a74408d83 | ||
|
|
3d0dc95a06 | ||
|
|
04a57a3cc2 | ||
|
|
d0c22ce01d | ||
|
|
23c95e9660 | ||
|
|
eecab9b95d | ||
|
|
8abc2f113c | ||
|
|
949d2ff1c2 | ||
|
|
1c8b763d53 | ||
|
|
bb91393b85 | ||
|
|
cc9c0d819e | ||
|
|
a72b8caf47 | ||
|
|
fdd9b8b8ce | ||
|
|
c096b4ca8c | ||
|
|
427add3008 | ||
|
|
c45871700d | ||
|
|
6821e8eeb2 | ||
|
|
a4f07756a5 | ||
|
|
f24fb0e0c5 | ||
|
|
73b88a0a6f | ||
|
|
c42f39e2d4 | ||
|
|
5e5fe3175f | ||
|
|
cab65d1f11 | ||
|
|
245f05d5f4 | ||
|
|
dda751f9e3 | ||
|
|
3d37ae988a | ||
|
|
717acd9855 | ||
|
|
2e4431cc63 | ||
|
|
f7387b0859 | ||
|
|
a09f6b8977 | ||
|
|
d63449c214 | ||
|
|
a86805f0d9 | ||
|
|
7d2009ccef | ||
|
|
18ae5db06d | ||
|
|
9a1838f102 | ||
|
|
e20346032d | ||
|
|
693a27d76c | ||
|
|
203966d86b | ||
|
|
7eca8508fd | ||
|
|
b85270df1c | ||
|
|
aff597cef4 | ||
|
|
61b05b3dee | ||
|
|
453c4ef602 | ||
|
|
cf4b04f92d | ||
|
|
06c6999987 | ||
|
|
013c5a369f | ||
|
|
07891d994a | ||
|
|
6baf8668a6 | ||
|
|
4ba2962c56 | ||
|
|
7ad92f5db4 | ||
|
|
4dad09cc91 | ||
|
|
7b2e0c7a7a | ||
|
|
7f08f15fc9 | ||
|
|
825c0f8b2a | ||
|
|
dbe880bc41 | ||
|
|
2ec516b6ff | ||
|
|
7942a01e50 | ||
|
|
df684f9344 | ||
|
|
ae16e95e42 | ||
|
|
2ac8e8a0cc | ||
|
|
0a0ceda71f | ||
|
|
220f1ce161 | ||
|
|
c62a8a97c9 | ||
|
|
f8a1136979 | ||
|
|
9ca29c787b | ||
|
|
6af748a251 | ||
|
|
9041867f86 | ||
|
|
04099b087c | ||
|
|
6d6234714c | ||
|
|
520be23481 | ||
|
|
346c3c8dd3 | ||
|
|
bd534c3313 | ||
|
|
2625368aed | ||
|
|
8ac94879f1 | ||
|
|
dd8c0f3756 | ||
|
|
010f353a5e | ||
|
|
e0a18edb92 | ||
|
|
c6f2eea058 | ||
|
|
bf89e38c69 | ||
|
|
e1f0640d42 | ||
|
|
71b54035ba | ||
|
|
325cc0beca | ||
|
|
1a9f09c4d5 | ||
|
|
4fed4e2af3 | ||
|
|
74cc2346a5 | ||
|
|
cc7e328358 | ||
|
|
d25397e2b0 | ||
|
|
bc11454e1c | ||
|
|
2025a096c3 | ||
|
|
38fe14b108 | ||
|
|
1b7b2f3695 | ||
|
|
5d67cc76cc | ||
|
|
27a3813207 | ||
|
|
b06e0bfdcd | ||
|
|
d616f25324 | ||
|
|
b03028e31f | ||
|
|
e08c42fd3d | ||
|
|
16901f7134 | ||
|
|
dffceedd85 | ||
|
|
e5541e435c |
@@ -0,0 +1,3 @@
|
||||
Please include the command line and (if needed) a test file with your issue report.
|
||||
|
||||
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
|
||||
+6
-2
@@ -1,8 +1,9 @@
|
||||
# Development environment
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
venv-*/
|
||||
venv*/
|
||||
pyvenv.cfg
|
||||
tasks.py
|
||||
|
||||
# Package building
|
||||
*.egg-info/
|
||||
@@ -15,6 +16,9 @@ wheelhouse/
|
||||
# Automatically generated files
|
||||
ocrmypdf/lib/_*.py
|
||||
ocrmypdf/version.py
|
||||
docs/_build/
|
||||
docs/_static/
|
||||
docs/_templates/
|
||||
|
||||
# Code coverage
|
||||
.coverage
|
||||
@@ -26,5 +30,5 @@ log/
|
||||
.ipynb_checkpoints/
|
||||
tests/cache/
|
||||
tests/output/
|
||||
tests/resources/private
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2015, The OCRmyPDF Authors
|
||||
Copyright (c) 2013-2016, The OCRmyPDF Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
|
||||
+46
-2
@@ -1,4 +1,48 @@
|
||||
recursive-exclude tests/output *
|
||||
# requirements
|
||||
include requirements.txt
|
||||
include test_requirements.txt
|
||||
include share/*
|
||||
include dev_requirements.txt
|
||||
|
||||
# git
|
||||
include .git_archival.txt
|
||||
|
||||
# docker
|
||||
include Dockerfile
|
||||
include Dockerfile.polyglot
|
||||
include .dockerignore
|
||||
|
||||
# tests
|
||||
include pytest.ini
|
||||
recursive-include tests *.jpg
|
||||
recursive-include tests *.pdf
|
||||
recursive-include tests *.py
|
||||
recursive-include tests *.rst
|
||||
recursive-include tests *.txt
|
||||
recursive-exclude tests/output *
|
||||
recursive-exclude tests/output_pageinfo *
|
||||
recursive-exclude tests/resources/private *
|
||||
recursive-exclude tests/cache *
|
||||
|
||||
# documentation
|
||||
include *.rst
|
||||
include pipeline.svg
|
||||
recursive-exclude .github *
|
||||
recursive-include docs *.py
|
||||
recursive-include docs *.rst
|
||||
recursive-include docs *.svg
|
||||
recursive-exclude docs/_build *
|
||||
|
||||
|
||||
# support files
|
||||
recursive-include ocrmypdf/data *
|
||||
recursive-include share *
|
||||
include *.py
|
||||
exclude tasks.py
|
||||
|
||||
# code
|
||||
recursive-include ocrmypdf *.py
|
||||
exclude ocrmypdf/lib/_leptonica.py
|
||||
include OCRmyPDF.sh
|
||||
include docker-wrapper.sh
|
||||
|
||||
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
# Copyright (c) 2013-14: fritz-hh from Github (https://github.com/fritz-hh)
|
||||
##############################################################################
|
||||
|
||||
echo "This script is deprecated. Use 'ocrmypdf' instead."
|
||||
|
||||
python3 -m ocrmypdf.main "$@"
|
||||
|
||||
+23
-278
@@ -2,7 +2,20 @@ OCRmyPDF
|
||||
========
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
be searched or copy-pasted.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf # it's a scriptable command line program
|
||||
-l eng+fra # it supports multiple languages
|
||||
--rotate-pages # it can fix pages that are misrotated
|
||||
--deskew # it can deskew crooked PDFs!
|
||||
--title "My PDF" # it can change output metadata
|
||||
--jobs 4 # it uses multiple cores by default
|
||||
--output-type pdfa # it produces PDF/A by default
|
||||
input_scanned.pdf # takes PDF input (or images)
|
||||
output_searchable.pdf # produces validated PDF output
|
||||
|
||||
|
||||
Main features
|
||||
-------------
|
||||
@@ -19,7 +32,7 @@ Main features
|
||||
- Processes pages in parallel when more than one CPU core is
|
||||
available
|
||||
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`_ engine
|
||||
- Supports the `39 languages <https://code.google.com/p/tesseract-ocr/downloads/list>`_ recognized by Tesseract
|
||||
- Supports more than `100 languages <https://github.com/tesseract-ocr/tessdata>`_ recognized by Tesseract
|
||||
- Battle-tested on thousands of PDFs, a test suite and continuous integration
|
||||
|
||||
For details: please consult the `release notes <RELEASE_NOTES.rst>`_.
|
||||
@@ -38,286 +51,18 @@ Linux/UNIX: I found many, but none of them were really satisfying.
|
||||
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
|
||||
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
|
||||
|
||||
... so I decided to develop my own tool (using various existing scripts
|
||||
as an inspiration)
|
||||
...so I decided to develop my own tool (using various existing scripts
|
||||
as an inspiration).
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
|
||||
|
||||
These steps describe how to install OCRmyPDF on your system.
|
||||
|
||||
- `Installing on Debian and Ubuntu`_ (Debian stretch and Ubuntu 16.10 or later)
|
||||
- `Installing the Docker image`_
|
||||
- `Installing on Mac OS X`_
|
||||
- `Installing on Ubuntu 14.04 LTS`_
|
||||
- Installing and running on `Windows`_ using the Docker image
|
||||
|
||||
If you prefer to install from source or install OCRmyPDF to a Python virtual environment, see steps for `Installing HEAD revision from sources`_.
|
||||
|
||||
.. _Windows: `Installing on Windows`_
|
||||
|
||||
|
||||
Installing on Debian and Ubuntu
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
|
||||
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
``apt-get install ocrmypdf``.
|
||||
|
||||
.. _Docker:
|
||||
|
||||
Installing the Docker image
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For many users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
|
||||
|
||||
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install
|
||||
a Docker image of the latest release.
|
||||
|
||||
Follow the Docker installation instructions for your platform. If you can run this command
|
||||
successfully, your system is ready to download and execute the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
||||
the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
Then tag it to give a more convenient name, just ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
|
||||
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Alternative step: If you need all language packs
|
||||
docker pull jbarlow83/ocrmypdf-polyglot
|
||||
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
|
||||
|
||||
You can then run ocrmypdf using the command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run ocrmypdf --help
|
||||
|
||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, such as this in this template:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
|
||||
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
|
||||
|
||||
|
||||
Installing on Mac OS X
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These instructions probably work on all Mac OS X versions later than 10.7 (Lion). OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
|
||||
|
||||
If it's not already present, `install Homebrew <http://brew.sh/>`_.
|
||||
|
||||
Update Homebrew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew update
|
||||
|
||||
Install or upgrade the required Homebrew packages, if any are missing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
|
||||
Install the required Tesseract OCR engine with the language packs you plan to use:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
Update the homebrew pip and install Pillow:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
You can then install OCRmyPDF from PyPI:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
|
||||
The command line program should now be available:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
Installing on Ubuntu 14.04 LTS
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
|
||||
|
||||
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
|
||||
Update apt-get:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
|
||||
Install system dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
||||
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
|
||||
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 is not motivated to figure how to set up a Ubuntu PPA to distribute it. You can create a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
|
||||
|
||||
Installing on Windows
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Direct installation on Windows is not possible. Install the _`Docker` container as described above. Ensure that your command prompt can run the docker "hello world" container.
|
||||
|
||||
Running on Windows
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The command line syntax to run ocrmypdf from a command prompt will resemble:
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
|
||||
|
||||
Installing HEAD revision from sources
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you have ``git`` and ``python3.4`` or ``python3.5`` installed, you can install from source. When the ``pip`` installer runs,
|
||||
it will alert you if dependencies are missing.
|
||||
|
||||
To install the HEAD revision from sources in the current Python 3 environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
On certain Linux distributions such as Ubuntu, you may need to use
|
||||
run the install command as superuser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Note that this will alter your system's Python distribution. If you prefer
|
||||
to not install as superuser, you can install the package in a Python virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
|
||||
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
||||
you activate the virtual environment.
|
||||
|
||||
To run the program:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
If not yet installed, the script will notify you about dependencies that
|
||||
need to be installed. The script requires specific versions of the
|
||||
dependencies. Older version than the ones mentioned in the release notes
|
||||
are likely not to be compatible to OCRmyPDF.
|
||||
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
|
||||
|
||||
Languages
|
||||
---------
|
||||
@@ -331,13 +76,13 @@ you can often find packages that provide language packs:
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
sudo apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
||||
languages can be requested.
|
||||
|
||||
Support
|
||||
-------
|
||||
Documentation and support
|
||||
-------------------------
|
||||
|
||||
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||
|
||||
@@ -345,7 +90,7 @@ Once ocrmypdf is installed, the built-in help which explains the command syntax
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
The `Wiki <https://github.com/jbarlow83/OCRmyPDF/wiki>`_ page also contains some tips and suggests.
|
||||
Our `documentation is served on Read the Docs <https://ocrmypdf.readthedocs.io/en/latest/index.html>`_.
|
||||
|
||||
If you detect an issue, please:
|
||||
|
||||
|
||||
+77
-1
@@ -1,6 +1,82 @@
|
||||
RELEASE NOTES
|
||||
=============
|
||||
|
||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
||||
|
||||
v4.3.3:
|
||||
=======
|
||||
|
||||
- Fixed PDF/A creation with Ghostscript 9.20 properly
|
||||
- Fixed an exception on inline stencil masks with a missing optional parameter
|
||||
|
||||
|
||||
v4.3.2:
|
||||
=======
|
||||
|
||||
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
|
||||
|
||||
|
||||
v4.3.1:
|
||||
=======
|
||||
|
||||
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
|
||||
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
|
||||
- Ability to specify multiple languages for multilingual documents is now advertised in documentation
|
||||
- Reduced the file sizes of some test resources
|
||||
- Cleaned up debug output
|
||||
- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed
|
||||
|
||||
|
||||
v4.3:
|
||||
=====
|
||||
|
||||
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
|
||||
- Better documentation
|
||||
- Fixed an issue with PDFs that draw images when the raster stack depth is zero
|
||||
- ocrmypdf can now redirect its output to stdout for use in a shell pipeline
|
||||
|
||||
+ This does not improve performance since temporary files are still used for buffering
|
||||
+ Some output validation is disabled in this mode
|
||||
|
||||
v4.2.5:
|
||||
=======
|
||||
|
||||
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
|
||||
- Removed non-free file milk.pdf
|
||||
|
||||
|
||||
v4.2.4:
|
||||
=======
|
||||
|
||||
- Fixed an error (#90) caused by PDFs that use stencil masks properly
|
||||
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
|
||||
|
||||
v4.2.3:
|
||||
=======
|
||||
|
||||
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
|
||||
- Integrated a few fixes to simplify downstream packaging (Debian)
|
||||
|
||||
+ The test suite no longer assumes it is installed
|
||||
+ If running Linux, skip a test that passes Unicode on the command line
|
||||
|
||||
- Added a test case to check explicit masks and stencil masks
|
||||
- Added a test case for indirect objects and linearized PDFs
|
||||
- Deprecated the OCRmyPDF.sh shell script
|
||||
|
||||
|
||||
v4.2.2:
|
||||
=======
|
||||
|
||||
- Improvements to documentation
|
||||
|
||||
|
||||
v4.2.1:
|
||||
=======
|
||||
|
||||
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
|
||||
- Implemented stdin streaming
|
||||
|
||||
|
||||
v4.2:
|
||||
=====
|
||||
@@ -213,7 +289,7 @@ Changes
|
||||
-------
|
||||
|
||||
- Default output format is now PDF/A-2b instead of PDF/A-1b
|
||||
- Python 3.5 and OS X El Capitan are now supported platforms - no changes were
|
||||
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
|
||||
needed to implement support
|
||||
- Improved some error messages related to missing input files
|
||||
- Fixed issue #20 - uppercase .PDF extension not accepted
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
check-manifest>=0.33
|
||||
setuptools-scm>=1.11.1
|
||||
twine>=1.8.1
|
||||
coverage>=4.2
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 915 585" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="b" y2="445" gradientUnits="userSpaceOnUse" y1="179" gradientTransform="translate(0 -2.06)" x2="-29.7" x1="322">
|
||||
<stop stop-color="#333" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="a" y2="414" gradientUnits="userSpaceOnUse" y1="159" x2="490" x1="815">
|
||||
<stop stop-color="#33f" offset="0"/>
|
||||
<stop stop-color="#3f3fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#b)" d="m403 247c-12 115-135 122-368 123-4.3-1.07-7.32-7.33-6-41 76-37 151-124 167-236 93 123 201 40.9 207 154z"/>
|
||||
<g stroke-width="3.45" fill="none">
|
||||
<path stroke="#000" d="m11.8 11.8h411v411l-411 0.01v-411z"/>
|
||||
<path stroke="#448" d="m489 11.7h415v411h-415v-411z"/>
|
||||
</g>
|
||||
<path d="m876 244c-12 115-133 120-366 121-6-14-10-40-3-43 76-37.3 136-106 152-218 38 48 209 101 217 140z" fill="url(#a)"/>
|
||||
<g id="RasterLarge" transform="matrix(1.36 0 0 1.28 -161 -636)">
|
||||
<path fill="#999" d="m287 730h-30v-60h20v20h10v10h10v10h20v10h40v-10h20v-20h10v-20h-10v-10h-10v-10h-40v-10h-30v-10h-20v-10h-10v-10h-10v-60h10v-10h10v-10h10v-10h90v10h30v60h-20v-10h-10v-20h-10v-10h-20v-10h-30v10h-20v10h-10v20h10v20h30v10h30v10h30v10h10v10h10v10h10v60h-10v10h-10v10h-10v10h-100v-10z"/>
|
||||
<path fill="#555" d="m297 730h-30v-10h-10v-50h20v20h10v20h10 10v10h60v-10h10v-10h10v-40h-20v-10h-20v-10h-40v-10h-20v-10h-20v-20h-10v-50h10v-10h10v-10h20v-10h70v10h30v10h10v50h-20v-20h-10v-20h-20v-10h-50v10h-10v10h-10v30h10v10h20v10h30v10h30v10h20v10h10v10h10v50h-10v20h-10v10h-20v10h-80z"/>
|
||||
<path d="m307 730h-30v-10h-20v-50h20v30h10v10h10v10h70v-10h20v-50h-10v-10h-20v-10h-40v-10h-30v-10h-10v-10h-10v-20h-10v-30h10v-20h20v-10h20v-10h50v10h30v10h20v50h-20v-30h-10v-10h-10v-10h-70v10h-10v40h10v10h10v10h30v10h40v10h20v10h10v20h10v40h-10v20h-20v10h-30v10h-50v-10z"/>
|
||||
</g>
|
||||
<g font-size="40" font-family="sans-serif" text-anchor="middle">
|
||||
<g font-size="100">
|
||||
<text y="518" x="210">Raster</text>
|
||||
<text y="518" x="695" fill="#338">Vector</text>
|
||||
</g>
|
||||
<text y="563" x="210">.jpeg .gif .png</text>
|
||||
<text y="563" x="696" fill="#338">.svg</text>
|
||||
</g>
|
||||
<path id="VectorLarge" fill="#005" d="m661 294v-62.5l23.4 0.184c0.678 20.8 7.32 36.3 19.9 46.4 12.7 9.93 32.1 14.9 57.9 14.9 24.1 0 42.5-4.29 55.1-12.9 12.7-8.71 19.1-21.3 19.1-37.9 0-13.2-3.86-23.4-11.6-30.5-7.59-7.11-23.7-14-48.4-20.8l-40.1-10.9c-29-7.97-49.5-17.9-61.4-29.8-11.8-11.9-17.7-28.2-17.7-48.9 0-23.3 9.15-41.4 27.4-54.3 18.3-12.9 44-19.3 77.1-19.3 14.1 0 29.6 1.41 46.4 4.23 16.8 2.7 34.7 6.68 53.7 12v58.5h-23c-2.3-19.4-9.49-33.4-21.6-41.9-11.9-8.71-30.2-13.1-54.7-13.1-21.4 0-37.7 3.99-49 12-11.1 7.85-16.7 19.3-16.7 34.4 0 13.1 4.2 23.4 12.6 30.9 8.4 7.48 26.2 14.9 53.5 22.3l37.6 10.1c27.5 7.48 47.1 17 58.8 28.7 11.8 11.5 17.7 27 17.7 46.5 0 26.6-9.42 46.7-28.3 60.2s-46.9 20.2-84.2 20.2c-16.7 0-33.7-1.53-51-4.6-18-3-35-7-53-13z"/>
|
||||
<use xlink:href="#VectorLarge" transform="matrix(.17 0 0 .17 392 313)"/>
|
||||
<use xlink:href="#RasterLarge" transform="matrix(.173 0 0 .173 -8.25 314)" height="100%" width="100%" y="0" x="0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
Executable
+350
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ocrmypdf documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Sep 4 14:29:43 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
# 'sphinx.ext.mathjax',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'ocrmypdf'
|
||||
copyright = '2016, James R. Barlow'
|
||||
author = 'James R. Barlow'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
from ocrmypdf import VERSION as OCRMYPDF_VERSION
|
||||
|
||||
_version_parts = OCRMYPDF_VERSION.split('.')
|
||||
|
||||
version = '.'.join(_version_parts[0:2])
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = OCRMYPDF_VERSION
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#
|
||||
# today = ''
|
||||
#
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#
|
||||
today_fmt = '%Y-%m-%d'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
html_theme_options = {
|
||||
'display_version': False
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents.
|
||||
# "<project> v<release> documentation" by default.
|
||||
#
|
||||
# html_title = 'ocrmypdf v4.2'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#
|
||||
# html_extra_path = []
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# bottom, using the given strftime format.
|
||||
# The empty string is equivalent to '%b %d, %Y'.
|
||||
#
|
||||
# html_last_updated_fmt = None
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
|
||||
#
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# 'ja' uses this config value.
|
||||
# 'zh' user can custom change `jieba` dictionary path.
|
||||
#
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'ocrmypdfdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation',
|
||||
'James R. Barlow', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# latex_appendices = []
|
||||
|
||||
# It false, will not define \strong, \code, itleref, \crossref ... but only
|
||||
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
|
||||
# packages.
|
||||
#
|
||||
# latex_keep_old_macro_names = True
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
|
||||
author, 'ocrmypdf', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#
|
||||
# texinfo_no_detailmenu = False
|
||||
@@ -0,0 +1,153 @@
|
||||
Cookbook
|
||||
========
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
|
||||
Help!
|
||||
"""""
|
||||
|
||||
ocrmypdf has built-in help.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
|
||||
Add an OCR layer and convert to PDF/A
|
||||
"""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf input.pdf output.pdf
|
||||
|
||||
Add an OCR layer and output a standard PDF
|
||||
""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
|
||||
Modify a file in place
|
||||
""""""""""""""""""""""
|
||||
|
||||
The file will only be overwritten if OCRmyPDF is successful.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf myfile.pdf myfile.pdf
|
||||
|
||||
Correct page rotation
|
||||
"""""""""""""""""""""
|
||||
|
||||
OCR will attempt to automatic correct the rotation of each page. This can help fix a scanning job that contains a mix of landscape and portrait pages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
|
||||
|
||||
You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive.
|
||||
|
||||
|
||||
OCR languages other than English
|
||||
""""""""""""""""""""""""""""""""
|
||||
|
||||
By default OCRmyPDF assumes the document is English.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
|
||||
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
|
||||
|
||||
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
|
||||
|
||||
|
||||
OCR images, not PDFs
|
||||
--------------------
|
||||
|
||||
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the results to run ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
|
||||
|
||||
If given a single image as input, OCRmyPDF will try converting it to a PDF on its own. This feature may be removed at some point, because OCRmyPDF does not specialize in converting images to PDFs.
|
||||
|
||||
You can also use Tesseract 3.04+ directly to convert single page images or multi-page TIFFs to PDF:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tesseract my-image.jpg output-prefix pdf
|
||||
|
||||
Image processing
|
||||
----------------
|
||||
|
||||
OCRmyPDF perform some image processing on each page of a PDF, if desired. The same processing is applied to each page. It is suggested that the user review files after image processing as these commands might remove desirable content, especially from poor quality scans.
|
||||
|
||||
* ``--rotate-pages`` attempts to determine the correct orientation for each page and rotates the page if necessary.
|
||||
|
||||
* ``--remove-background`` attempts to detect and remove a noisy background from grayscale or color images. Monochrome images are ignored. This should not be used on documents that contain color photos as it may remove them.
|
||||
|
||||
* ``--deskew`` will correct pages were scanned at a skewed angle by rotating them back into place. Skew determination and correction is performed using `Postl's variance of line sums <http://www.leptonica.com/skew-measurement.html>`_ algorithm as implemented in `Leptonica <http://www.leptonica.com/index.html>`_.
|
||||
|
||||
* ``--clean`` uses `unpaper <https://www.flameeyes.eu/projects/unpaper>`_ to clean up pages before OCR, but does not alter the final output. This makes it less likely that OCR will try to find text in background noise.
|
||||
|
||||
* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important.
|
||||
|
||||
|
||||
OCR and correct document skew (crooked scan)
|
||||
""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --deskew input.pdf output.pdf
|
||||
|
||||
|
||||
Hot (watched) folders
|
||||
---------------------
|
||||
|
||||
To set up a "hot folder" that will trigger an OCR operation for every file inserted, use a program like Python `watchdog <https://pypi.python.org/pypi/watchdog>`_ (supports all major OS).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install watchdog
|
||||
|
||||
watchdog installs the command line program ``watchmedo``, which can be told to run ``ocrmypdf`` on any .pdf added to the current directory (``.``) and place the result in the previously created ``out/`` folder.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd hot-folder
|
||||
mkdir out
|
||||
watchmedo shell-command \
|
||||
--patterns="*.pdf" \
|
||||
--ignore-directories \
|
||||
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
||||
. # don't forget the final dot
|
||||
|
||||
For more complex behavior you can write a Python script around to use the watchdog API.
|
||||
|
||||
On file servers, you could configure watchmedo as a system service so it will run all the time.
|
||||
|
||||
Caveats
|
||||
"""""""
|
||||
|
||||
* ``watchmedo`` may not work properly on a networked file system, depending on the capabilities of the file system client and server.
|
||||
* This simple recipe does not filter for the type of file system event, so file copies, deletes and moves, and directory operations, will all be sent to ocrmypdf, producing errors in several cases. Disable your watched folder if you are doing anything other than copying files to it.
|
||||
* If the source and destination directory are the same, watchmedo may create an infinite loop.
|
||||
|
||||
|
||||
Batch jobs
|
||||
----------
|
||||
|
||||
Consider using the excellent `GNU Parallel <https://www.gnu.org/software/parallel/>`_ to apply OCRmyPDF to multiple files at once.
|
||||
|
||||
Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once.
|
||||
|
||||
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
parallel -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
|
||||
|
||||
If you have thousands of files to work with, contact the author.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
Common error messages
|
||||
=====================
|
||||
|
||||
Page already has text
|
||||
---------------------
|
||||
|
||||
.. code::
|
||||
|
||||
ERROR - 1: page already has text! – aborting (use --force-ocr to force OCR)
|
||||
|
||||
You ran ocrmypdf on a file that already contains printable text or a hidden OCR text layer (it can't quite tell the difference). You probably don't want to do this, because the file is already searchable.
|
||||
|
||||
As the error message suggests, your options are:
|
||||
|
||||
- ``ocrmypdf --force-ocr`` to :ref:`rasterize <raster-vector>` all vector content and run OCR on the images. This is useful if a previous OCR program failed, or if the document contains a text watermark.
|
||||
|
||||
- ``ocrmypdf --skip-text`` to skip OCR and other processing on any pages that contain text. Text pages will be copied into the output PDF without modification.
|
||||
|
||||
|
||||
Input file 'filename' is not a valid PDF
|
||||
----------------------------------------
|
||||
|
||||
OCRmyPDF passes files through qpdf, a program that fixes errors in PDFs, before it tries to work on them. In most cases this happens because the PDF is corrupt and
|
||||
truncated (incomplete file copying) and not much can be done.
|
||||
|
||||
You can try rewriting the file with Ghostscript or pdftk:
|
||||
|
||||
- ``gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf``
|
||||
|
||||
- ``pdftk input.pdf cat output output.pdf``
|
||||
|
||||
Sometimes Acrobat can repair PDFs with its `Preflight tool <https://helpx.adobe.com/acrobat/using/correcting-problem-areas-preflight-tool.html>`_.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.. ocrmypdf documentation master file, created by
|
||||
sphinx-quickstart on Sun Sep 4 14:29:43 2016.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
OCRmyPDF documentation
|
||||
======================
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
|
||||
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
introduction
|
||||
installation
|
||||
languages
|
||||
cookbook
|
||||
security
|
||||
errors
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Installing on Debian and Ubuntu
|
||||
-------------------------------
|
||||
|
||||
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
apt-get install ocrmypdf
|
||||
|
||||
.. _Docker:
|
||||
|
||||
Installing the Docker image
|
||||
---------------------------
|
||||
|
||||
For many users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
|
||||
|
||||
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install
|
||||
a Docker image of the latest release.
|
||||
|
||||
Follow the Docker installation instructions for your platform. If you can run this command
|
||||
successfully, your system is ready to download and execute the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
||||
the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
Then tag it to give a more convenient name, just ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
|
||||
.. _docker-polyglot:
|
||||
|
||||
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Alternative step: If you need all language packs
|
||||
docker pull jbarlow83/ocrmypdf-polyglot
|
||||
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
|
||||
|
||||
You can then run ocrmypdf using the command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm ocrmypdf --help
|
||||
|
||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
|
||||
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
|
||||
|
||||
|
||||
Installing on macOS (formerly Mac OS X)
|
||||
---------------------------------------
|
||||
|
||||
These instructions probably work on all macOS supported by Homebrew. OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
|
||||
|
||||
If it's not already present, `install Homebrew <http://brew.sh/>`_.
|
||||
|
||||
Update Homebrew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew update
|
||||
|
||||
Install or upgrade the required Homebrew packages, if any are missing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
|
||||
Install the required Tesseract OCR engine with the language packs you plan to use:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||
|
||||
.. _macos-all-languages:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
Update the homebrew pip and install Pillow:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
You can then install OCRmyPDF from PyPI:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
|
||||
The command line program should now be available:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
Installing on Ubuntu 14.04 LTS
|
||||
------------------------------
|
||||
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
|
||||
|
||||
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
|
||||
Update apt-get:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
|
||||
Install system dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
||||
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
|
||||
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
|
||||
|
||||
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
|
||||
|
||||
Installing on Windows
|
||||
---------------------
|
||||
|
||||
Direct installation on Windows is not possible. Install the _`Docker` container as described above. Ensure that your command prompt can run the docker "hello world" container.
|
||||
|
||||
Running on Windows
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The command line syntax to run ocrmypdf from a command prompt will resemble:
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
|
||||
|
||||
Installing HEAD revision from sources
|
||||
-------------------------------------
|
||||
|
||||
If you have ``git`` and ``python3.4`` or ``python3.5`` installed, you can install from source. When the ``pip`` installer runs,
|
||||
it will alert you if dependencies are missing.
|
||||
|
||||
To install the HEAD revision from sources in the current Python 3 environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
On certain Linux distributions such as Ubuntu, you may need to use
|
||||
run the install command as superuser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Note that this will alter your system's Python distribution. If you prefer
|
||||
to not install as superuser, you can install the package in a Python virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
|
||||
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
||||
you activate the virtual environment.
|
||||
|
||||
To run the program:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
If not yet installed, the script will notify you about dependencies that
|
||||
need to be installed. The script requires specific versions of the
|
||||
dependencies. Older version than the ones mentioned in the release notes
|
||||
are likely not to be compatible to OCRmyPDF.
|
||||
@@ -0,0 +1,76 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
|
||||
|
||||
|
||||
About OCR
|
||||
---------
|
||||
|
||||
`Optical character recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`_ is technology that converts images of typed or handwritten text, such as in a scanned document, to computer text that can be searched and copied.
|
||||
|
||||
OCRmyPDF uses `Tesseract <https://github.com/tesseract-ocr/tesseract>`_, the best available open source OCR engine, to perform OCR.
|
||||
|
||||
.. _raster-vector:
|
||||
|
||||
About PDFs
|
||||
----------
|
||||
|
||||
PDFs are page description files that attempts to preserve a layout exactly. They can contain `vector graphic files <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
|
||||
|
||||
.. image:: bitmap_vs_svg.svg
|
||||
|
||||
A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page.
|
||||
|
||||
Rasterizing a PDF is the process of generating an image suitable for display or analyzing with an OCR engine. OCR engines like Tesseract work with images, not vector objects.
|
||||
|
||||
|
||||
About PDF/A
|
||||
-----------
|
||||
|
||||
`PDF/A <https://en.wikipedia.org/wiki/PDF/A>`_ is an ISO-standardized subset of the full PDF specification that is designed for archiving (the 'A' stands for Archive). PDF/A differs from PDF primarily by omitting features that would make it difficult to read the file in the future, such as embedded Javascript, video, audio and references to external fonts. All fonts and resources needed to interpret the PDF must be contained within it. Because PDF/A disables Javascript and other types of embedded content, it is probably more secure.
|
||||
|
||||
There are various conformance levels and versions, such as "PDF/A-2b".
|
||||
|
||||
Generally speaking, the best format for scanned documents is PDF/A. Some governments and jurisdictions, US Courts in particular, `mandate the use of PDF/A <https://pdfblog.com/2012/02/13/what-is-pdfa/>`_ for scanned documents.
|
||||
|
||||
Since most people who scan documents are interested in reading them indefinitely into the future, OCRmyPDF generates PDF/A-2b by default.
|
||||
|
||||
PDF/A has a few drawbacks. Some PDF viewers include an alert that the file is a PDF/A, which may confuse some users. It also tends to produce larger files than PDF, because it embeds certain resources even if they are commonly available. PDF/A files can be digitally signed, but may not be encrypted, to ensure they can be read in the future. Fortunately, converting from PDF/A to a regular PDF is trivial, and any PDF viewer can view PDF/A.
|
||||
|
||||
|
||||
What OCRmyPDF does
|
||||
------------------
|
||||
|
||||
OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript <http://ghostscript.com/>`_ to rasterize the page, and then performs on OCR on the rasterized image. It is not enough to simply extract the images from each page and run OCR on them individually. Of course one could use Ghostscript or another PDF rasterizer and then pass the image to Tesseract. OCRmyPDF automates this process and produces a minimally changed output file that contains the same information, colorspace and resolution.
|
||||
|
||||
The Tesseract OCR engine can output 'hOCR' files, which are XML files that contain a description of the text it found on the page. OCRmyPDF will render a new PDF that contains only the hidden text layer, and merge this with the original page.
|
||||
|
||||
Alternately, OCRmyPDF can use the Tesseract OCR engine to directly output PDFs for each page, then merge them.
|
||||
|
||||
By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be disabled with the ``--output-type pdf`` argument.
|
||||
|
||||
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these limitations, as do any other programs that rely on Tesseract:
|
||||
|
||||
* The OCR is not as accurate as commercial solutions such as Abbyy.
|
||||
* It is not capable of recognizing handwriting.
|
||||
* It may find gibberish and report this as OCR output.
|
||||
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
|
||||
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
|
||||
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
|
||||
|
||||
OCRmyPDF is also limited by the PDF specification:
|
||||
|
||||
* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others.
|
||||
|
||||
Ghostscript also imposes some limitations:
|
||||
|
||||
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
|
||||
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
@@ -0,0 +1,48 @@
|
||||
.. _lang-packs:
|
||||
|
||||
Installing additional language packs
|
||||
====================================
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English.
|
||||
|
||||
Tesseract supports `most languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
|
||||
|
||||
For Linux users, you can often find packages that provide language packs:
|
||||
|
||||
Debian and Ubuntu users
|
||||
-----------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Display a list of all Tesseract language packs
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
||||
languages can be requested using either ``-l eng+fre`` (English and French) or ``-l eng -l fre``.
|
||||
|
||||
Mac OS X (macOS) users
|
||||
----------------------
|
||||
|
||||
You can install additional language packs by :ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
|
||||
|
||||
Docker users
|
||||
------------
|
||||
|
||||
Users of the Docker image may use the alternative :ref:`"polyglot" container <docker-polyglot>` which includes all languages.
|
||||
|
||||
Known limitations
|
||||
-----------------
|
||||
|
||||
As of v4.2, users of ocrmypdf working languages outside the Latin alphabet should use the following syntax:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf -l eng+gre --output-type pdf --pdf-renderer tesseract
|
||||
|
||||
The reasons for this are:
|
||||
|
||||
* The latest version of Ghostscript (9.19 as of this writing) has unfixed bugs in Unicode handling that generate invalid character maps, so Ghostscript cannot be used for PDF/A conversion
|
||||
* The default "hocr" PDF renderer does not handle Asian fonts properly
|
||||
@@ -0,0 +1,44 @@
|
||||
PDF Security Issues
|
||||
===================
|
||||
|
||||
OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware.
|
||||
|
||||
Recognizing that many users have an interest in handling PDFs and applying OCR to PDFs they did not generate themselves, this article discusses the security implications of PDFs and how users can protect themselves.
|
||||
|
||||
The disclaimer applies: this software has no warranties of any kind.
|
||||
|
||||
PDFs may contain malware
|
||||
------------------------
|
||||
|
||||
PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of packages long and references several annexes each of which are similar in length. PDFs can contain video, audio, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors.
|
||||
|
||||
In short, PDFs `may contain viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`_.
|
||||
|
||||
This `article <https://theinvisiblethings.blogspot.ca/2013/02/converting-untrusted-pdfs-into-trusted.html>`_ describes a method which allows potentially hostile PDFs to be viewed and rasterized safely in a disposable virtual machine. A trusted PDF created in this manner is converted to images and loses all information making it searchable. OCRmyPDF could be used restore searchability.
|
||||
|
||||
How OCRmyPDF processes PDFs
|
||||
---------------------------
|
||||
|
||||
OCRmyPDF must open and interpret your PDF in order to insert an OCR layer. First, it runs all PDFs through `qpdf <https://github.com/qpdf/qpdf>`_, a program that repairs PDFs with syntax errors. This is done because, in the author's experience, a significant number of PDFs in the wild especially those created by scanners are not well-formed files. qpdf makes it more likely that OCRmyPDF will succeed, but offers no security guarantees. qpdf is also used to split the PDF into single page PDFs.
|
||||
|
||||
After qpdf, OCRmyPDF examines each page using `PyPDF2 <https://github.com/mstamy2/PyPDF2>`_. This library also has no warranties or guarantees.
|
||||
|
||||
Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://ghostscript.com/>`_ in ``-dSAFER`` mode.
|
||||
|
||||
Depending on the options specified, OCRmyPDF may graft the OCR layer into the existing PDF or it may essentially reconstruct ("re-fry") a visually identical PDF that may be quite different at the binary level. That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
|
||||
|
||||
Using OCRmyPDF online
|
||||
---------------------
|
||||
|
||||
OCRmyPDF is not designed to be deployed "as a service", in a setting where a user/attacker could upload a file for OCR processing online. It is not designed to be secure in this case.
|
||||
|
||||
Abbyy Cloud OCR is a viable commercial alternative with a web services API. The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation.
|
||||
|
||||
Password protection, digital signatures and certification
|
||||
---------------------------------------------------------
|
||||
|
||||
OCRmyPDF cannot remove password protection from a PDF. ``qpdf``, one of its dependencies, has this capability. After OCR is applied, password protection is not permitted on PDF/A documents but the file can be converted to regular PDF.
|
||||
|
||||
Many programs exist which are capable of inserting an image of someone's signature. On its own, this offers no security guarantees. It is trivial to remove the signature image and apply it to other files. This practice offers no real security.
|
||||
|
||||
Important documents can be digitally signed and certified to attest to their authorship. OCRmyPDF cannot do this. Open source tools such as pdfbox (Java) have this capability as does Adobe Acrobat.
|
||||
@@ -1,6 +1,9 @@
|
||||
from enum import IntEnum
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
import pkg_resources
|
||||
|
||||
VERSION = pkg_resources.get_distribution('ocrmypdf').version
|
||||
|
||||
|
||||
class ExitCode(IntEnum):
|
||||
@@ -28,3 +31,4 @@ def page_number(input_file):
|
||||
|
||||
def is_iterable_notstr(thing):
|
||||
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
||||
|
||||
|
||||
Executable
+1557
File diff suppressed because it is too large
Load Diff
+32
-6
@@ -21,7 +21,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
'-dFirstPage=%i' % pageno,
|
||||
'-dLastPage=%i' % pageno,
|
||||
'-o', tmp.name,
|
||||
'-r{0}x{1}'.format(str(xres), str(yres)),
|
||||
'-r{0}x{1}'.format(str(round(xres)), str(round(yres))),
|
||||
input_file
|
||||
]
|
||||
|
||||
@@ -29,7 +29,10 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
if stdout:
|
||||
log.debug(stdout)
|
||||
if 'error' in stdout:
|
||||
log.error(stdout) # Ghostscript puts errors in stdout
|
||||
else:
|
||||
log.debug(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
|
||||
@@ -39,7 +42,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
log.error('Ghostscript rendering failed')
|
||||
|
||||
|
||||
def generate_pdfa(pdf_pages, output_file, threads=1):
|
||||
def generate_pdfa(pdf_pages, output_file, log, threads=1):
|
||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||
args_gs = [
|
||||
get_program("gs"),
|
||||
@@ -53,9 +56,32 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
|
||||
"-sProcessColorModel=DeviceRGB",
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-sPDFACompatibilityPolicy=2",
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
check_call(args_gs)
|
||||
copy(gs_pdf.name, output_file)
|
||||
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
if stdout:
|
||||
if 'error' in stdout:
|
||||
log.error(stdout)
|
||||
elif 'overprint mode not set' in stdout:
|
||||
# Unless someone is going to print PDF/A documents on a
|
||||
# magical sRGB printer I can't see the removal of overprinting
|
||||
# being a problem....
|
||||
log.debug(
|
||||
"Ghostscript had to remove PDF 'overprinting' from the "
|
||||
"input file to complete PDF/A conversion. "
|
||||
)
|
||||
else:
|
||||
log.debug(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
|
||||
if p.returncode == 0:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
# PDF/A - check PDF/A status elsewhere
|
||||
copy(gs_pdf.name, output_file)
|
||||
else:
|
||||
log.error('Ghostscript PDF/A failed')
|
||||
|
||||
+109
-22
@@ -1,14 +1,10 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# © 2013-15: jbarlow83 from Github (https://github.com/jbarlow83)
|
||||
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
|
||||
#
|
||||
#
|
||||
# Use Leptonica to detect find and remove page skew. Leptonica uses the method
|
||||
# of differential square sums, which its author claim is faster and more robust
|
||||
# than the Hough transform used by ImageMagick.
|
||||
# Python FFI wrapper for Leptonica library
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
@@ -44,22 +40,34 @@ class LeptonicaErrorTrap(object):
|
||||
|
||||
"""
|
||||
def __enter__(self):
|
||||
from io import UnsupportedOperation
|
||||
self.tmpfile = TemporaryFile()
|
||||
|
||||
# Save the old stderr, and redirect stderr to temporary file
|
||||
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
|
||||
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno())
|
||||
sys.stderr.flush()
|
||||
try:
|
||||
self.copy_of_stderr = os.dup(sys.stderr.fileno())
|
||||
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(),
|
||||
inheritable=False)
|
||||
except UnsupportedOperation:
|
||||
self.copy_of_stderr = None
|
||||
return
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
# Restore old stderr
|
||||
os.dup2(self.old_stderr_fileno, sys.stderr.fileno())
|
||||
sys.stderr.flush()
|
||||
if self.copy_of_stderr is not None:
|
||||
os.dup2(self.copy_of_stderr, sys.stderr.fileno())
|
||||
os.close(self.copy_of_stderr)
|
||||
|
||||
# Get data from tmpfile (in with block to ensure it is closed)
|
||||
with self.tmpfile as tmpfile:
|
||||
tmpfile.seek(0) # Cursor will be at end, so move back to beginning
|
||||
leptonica_output = tmpfile.read().decode(errors='replace')
|
||||
|
||||
assert self.tmpfile.closed
|
||||
assert not sys.stderr.closed
|
||||
|
||||
# If there are Python errors, let them bubble up
|
||||
if exc_type:
|
||||
logger.warning(leptonica_output)
|
||||
@@ -104,6 +112,12 @@ class Pix:
|
||||
|
||||
Leptonica's reference counting is not threadsafe. This class can be used
|
||||
in a threadsafe manner if a Python threading.Lock protects the data.
|
||||
|
||||
This class treats Pix objects as immutable. All methods return new
|
||||
modified objects. This allows convenient chaining:
|
||||
|
||||
>>> Pix.read('filename.jpg').scale((0.5, 0.5)).deskew().show()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, pix):
|
||||
@@ -117,6 +131,22 @@ class Pix:
|
||||
else:
|
||||
return "<leptonica.Pix image NULL>"
|
||||
|
||||
def _repr_png_(self):
|
||||
"""iPython display hook
|
||||
|
||||
returns png version of image
|
||||
"""
|
||||
|
||||
data = ffi.new('l_uint8 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixWriteMemPng(data, size, self._pix, 0)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixWriteMemPng")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
return ffi.buffer(char_data, size[0])[:]
|
||||
|
||||
def __getstate__(self):
|
||||
data = ffi.new('l_uint32 **')
|
||||
size = ffi.new('size_t *')
|
||||
@@ -142,6 +172,9 @@ class Pix:
|
||||
cdata_uint32, len(state['data']))
|
||||
Pix.__init__(self, pix)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__getstate__() == other.__getstate__()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._pix.w
|
||||
@@ -201,18 +234,29 @@ class Pix:
|
||||
"Returns a PIL.Image version of this Pix"
|
||||
from PIL import Image
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
pix_swapped = Pix(lept.pixEndianByteSwapNew(self._pix))
|
||||
# Leptonica manages data in words, so it implicitly does an endian
|
||||
# swap. Tell Pillow about this when it reads the data.
|
||||
pix = self
|
||||
if sys.byteorder == 'little':
|
||||
if self.mode == 'RGB':
|
||||
raw_mode = 'XBGR'
|
||||
elif self.mode == 'RGBA':
|
||||
raw_mode = 'ABGR'
|
||||
elif self.mode == '1':
|
||||
raw_mode = '1;I'
|
||||
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
|
||||
else:
|
||||
raw_mode = self.mode
|
||||
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
|
||||
else:
|
||||
raw_mode = self.mode # no endian swap needed
|
||||
|
||||
size = (pix_swapped._pix.wpl * 4, pix_swapped._pix.h)
|
||||
buf = ffi.buffer(pix_swapped._pix.data, size[0] * size[1])
|
||||
size = (pix._pix.w, pix._pix.h)
|
||||
bytecount = pix._pix.wpl * 4 * pix._pix.h
|
||||
buf = ffi.buffer(pix._pix.data, bytecount)
|
||||
stride = pix._pix.wpl * 4
|
||||
|
||||
im_raw = Image.frombytes(self.mode, size, buf, 'raw')
|
||||
|
||||
# Leptonica stores images in 32-bit words
|
||||
# Need to crop the any trailing amount
|
||||
box = (0, 0, self.width, self.height)
|
||||
im = im_raw.crop(box)
|
||||
im = Image.frombytes(self.mode, size, buf, 'raw', raw_mode, stride)
|
||||
|
||||
return im
|
||||
|
||||
@@ -231,10 +275,10 @@ class Pix:
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixDeskew(self._pix, reduction_factor))
|
||||
|
||||
def scale(self, scalex, scaley):
|
||||
def scale(self, scale_xy):
|
||||
"Returns the pix object rescaled according to the proportions given."
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixScale(self._pix, scalex, scaley))
|
||||
return Pix(lept.pixScale(self._pix, scale_xy[0], scale_xy[1]))
|
||||
|
||||
def rotate180(self):
|
||||
with LeptonicaErrorTrap():
|
||||
@@ -352,6 +396,33 @@ class Pix:
|
||||
black,
|
||||
white))
|
||||
|
||||
def gamma_trc(self, gamma=1.0, minval=0, maxval=255):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixGammaTRC(
|
||||
ffi.NULL,
|
||||
self._pix,
|
||||
gamma,
|
||||
minval,
|
||||
maxval
|
||||
))
|
||||
|
||||
def background_norm(
|
||||
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
|
||||
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixBackgroundNorm(
|
||||
self._pix,
|
||||
mask or ffi.NULL,
|
||||
grayscale or ffi.NULL,
|
||||
tile_size[0],
|
||||
tile_size[1],
|
||||
fg_threshold,
|
||||
min_count,
|
||||
bg_val,
|
||||
smooth_kernel[0],
|
||||
smooth_kernel[1]
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def make_pixel_sum_tab8():
|
||||
@@ -462,6 +533,22 @@ def deskew(infile, outfile, dpi):
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
|
||||
|
||||
def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
|
||||
black_threshold=70, white_threshold=190):
|
||||
try:
|
||||
pix = Pix.read(infile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open file: %s" % infile)
|
||||
|
||||
pix = pix.background_norm(tile_size=tile_size).gamma_trc(
|
||||
gamma, black_threshold, white_threshold)
|
||||
|
||||
try:
|
||||
pix.write_implied_format(outfile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Python wrapper to access Leptonica")
|
||||
|
||||
@@ -61,6 +61,12 @@ PIX * pixRead ( const char *filename );
|
||||
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
|
||||
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
|
||||
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
|
||||
l_int32
|
||||
pixWriteMemPng(l_uint8 **pdata,
|
||||
size_t *psize,
|
||||
PIX *pix,
|
||||
l_float32 gamma);
|
||||
|
||||
void pixDestroy ( PIX **ppix );
|
||||
|
||||
PIX *
|
||||
@@ -131,6 +137,50 @@ pixClipRectangle(PIX *pixs,
|
||||
BOX *box,
|
||||
BOX **pboxc);
|
||||
|
||||
PIX *
|
||||
pixBackgroundNorm(PIX *pixs,
|
||||
PIX *pixim,
|
||||
PIX *pixg,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 thresh,
|
||||
l_int32 mincount,
|
||||
l_int32 bgval,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy);
|
||||
|
||||
PIX *
|
||||
pixGammaTRC(PIX *pixd,
|
||||
PIX *pixs,
|
||||
l_float32 gamma,
|
||||
l_int32 minval,
|
||||
l_int32 maxval);
|
||||
|
||||
|
||||
l_int32
|
||||
pixNumSignificantGrayColors(PIX *pixs,
|
||||
l_int32 darkthresh,
|
||||
l_int32 lightthresh,
|
||||
l_float32 minfract,
|
||||
l_int32 factor,
|
||||
l_int32 *pncolors);
|
||||
|
||||
l_int32
|
||||
pixColorFraction(PIX *pixs,
|
||||
l_int32 darkthresh,
|
||||
l_int32 lightthresh,
|
||||
l_int32 diffthresh,
|
||||
l_int32 factor,
|
||||
l_float32 *ppixfract,
|
||||
l_float32 *pcolorfract);
|
||||
|
||||
PIX *
|
||||
pixColorMagnitude(PIX *pixs,
|
||||
l_int32 rwhite,
|
||||
l_int32 gwhite,
|
||||
l_int32 bwhite,
|
||||
l_int32 type);
|
||||
|
||||
void
|
||||
boxDestroy(BOX **pbox);
|
||||
|
||||
|
||||
Executable → Regular
+4
-1364
File diff suppressed because it is too large
Load Diff
+103
-21
@@ -9,6 +9,14 @@ import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
|
||||
try:
|
||||
from math import isclose
|
||||
except ImportError:
|
||||
def isclose(a, b, rel_tol=1e-9):
|
||||
"Python 3.4 does not have math.isclose()"
|
||||
diff = abs(b - a)
|
||||
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
FRIENDLY_COLORSPACE = {
|
||||
@@ -52,6 +60,9 @@ FRIENDLY_COMP = {
|
||||
}
|
||||
|
||||
|
||||
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def _matrix_from_shorthand(shorthand):
|
||||
"""Convert from PDF matrix shorthand to full matrix
|
||||
|
||||
@@ -73,13 +84,46 @@ def _shorthand_from_matrix(matrix):
|
||||
return tuple(map(float, (a, b, c, d, e, f)))
|
||||
|
||||
|
||||
def _is_unit_square(shorthand):
|
||||
values = map(float, shorthand)
|
||||
pairwise = zip(values, UNIT_SQUARE)
|
||||
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
|
||||
|
||||
RasterSettings = namedtuple('RasterSettings',
|
||||
['name', 'shorthand', 'stack_depth'])
|
||||
|
||||
InlineSettings = namedtuple('InlineSettings',
|
||||
['settings', 'shorthand', 'stack_depth'])
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _interpret_contents(contentstream):
|
||||
"""Interpret the PDF content stream
|
||||
|
||||
The stack represents the state of the PDF graphics stack. We are only
|
||||
interested in the current transformation matrix (CTM) so we only track
|
||||
this object; a full implementation would need to track many other items.
|
||||
|
||||
The CTM is initialized to the mapping from user space to device space.
|
||||
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
|
||||
to the transformation to device space. For example if set to
|
||||
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
|
||||
|
||||
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
|
||||
image there should be a 'cm' that sets up an image coordinate system
|
||||
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
|
||||
page.
|
||||
|
||||
PDF units suit our needs so we initialize ctm to the identity matrix.
|
||||
|
||||
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
|
||||
|
||||
"""
|
||||
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand((1, 0, 0, 1, 0, 0))
|
||||
ctm = _matrix_from_shorthand(UNIT_SQUARE)
|
||||
image_raster_settings = []
|
||||
inline_images = []
|
||||
|
||||
@@ -96,12 +140,16 @@ def _interpret_contents(contentstream):
|
||||
_matrix_from_shorthand(operands), ctm)
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
image_raster_settings.append(
|
||||
(image_name, _shorthand_from_matrix(ctm)))
|
||||
raster = RasterSettings(
|
||||
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
image_raster_settings.append(raster)
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline_images.append(
|
||||
(settings, _shorthand_from_matrix(ctm)))
|
||||
inline = InlineSettings(
|
||||
settings=settings, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
inline_images.append(inline)
|
||||
|
||||
return ContentsInfo(
|
||||
raster_settings=image_raster_settings,
|
||||
@@ -175,24 +223,30 @@ def _get_dpi(ctm_shorthand, image_size):
|
||||
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||
"Find inline images on the page"
|
||||
|
||||
for n, im in enumerate(contentsinfo.inline_images):
|
||||
settings, shorthand = im
|
||||
for n, inline in enumerate(contentsinfo.inline_images):
|
||||
image = {}
|
||||
image['name'] = str('inline-%02d' % n)
|
||||
image['width'] = settings['/W']
|
||||
image['height'] = settings['/H']
|
||||
image['bpc'] = settings['/BPC']
|
||||
image['color'] = FRIENDLY_COLORSPACE.get(settings['/CS'], '-')
|
||||
image['width'] = inline.settings['/W']
|
||||
image['height'] = inline.settings['/H']
|
||||
if '/BPC' in inline.settings:
|
||||
image['bpc'] = inline.settings['/BPC']
|
||||
else:
|
||||
image['bpc'] = 8
|
||||
if '/CS' in inline.settings:
|
||||
image['color'] = FRIENDLY_COLORSPACE.get(inline.settings['/CS'], '-')
|
||||
else:
|
||||
image['color'] = '-'
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
if '/F' in settings:
|
||||
filter_ = settings['/F']
|
||||
if '/F' in inline.settings:
|
||||
filter_ = inline.settings['/F']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
image['enc'] = 'image'
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(shorthand, (image['width'], image['height']))
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
inline.shorthand, (image['width'], image['height']))
|
||||
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
||||
yield image
|
||||
|
||||
@@ -213,7 +267,21 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
image['name'] = str(xobj)
|
||||
image['width'] = pdfimage['/Width']
|
||||
image['height'] = pdfimage['/Height']
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
if '/BitsPerComponent' in pdfimage:
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
else:
|
||||
image['bpc'] = 8
|
||||
|
||||
# Fixme: this is incorrectly treats explicit masks as stencil masks,
|
||||
# but good enough for now. Explicit masks have /ImageMask true but are
|
||||
# never called for in content stream, instead are drawn as a /Mask on
|
||||
# other images. For our purposes finding out the details of /Mask
|
||||
# will seldom matter.
|
||||
if '/ImageMask' in pdfimage:
|
||||
image['type'] = 'stencil' if pdfimage['/ImageMask'].value \
|
||||
else 'image'
|
||||
else:
|
||||
image['type'] = 'image'
|
||||
if '/Filter' in pdfimage:
|
||||
filter_ = pdfimage['/Filter']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
@@ -242,21 +310,30 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
|
||||
for raster in contentsinfo.raster_settings:
|
||||
# Loop in case the same image is display multiple times on a page
|
||||
if raster[0] != image['name']:
|
||||
if raster.name != image['name']:
|
||||
continue
|
||||
|
||||
if raster.stack_depth == 0 and _is_unit_square(raster.shorthand):
|
||||
# At least one PDF in the wild (and test suite) draws an image
|
||||
# when the graphics stack depth is 0, meaning that the image
|
||||
# gets drawn into a square of 1x1 PDF units (or 1/72",
|
||||
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
|
||||
# these from our DPI calculation for the page.
|
||||
continue
|
||||
shorthand = raster[1]
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
shorthand, (image['width'], image['height']))
|
||||
raster.shorthand, (image['width'], image['height']))
|
||||
|
||||
# When image is used multiple times take the highest DPI it is
|
||||
# rendered at
|
||||
image['dpi_w'] = max(dpi_w, image.get('dpi_w', 0))
|
||||
image['dpi_h'] = max(dpi_h, image.get('dpi_h', 0))
|
||||
|
||||
image['dpi_w'] = Decimal(image['dpi_w'])
|
||||
image['dpi_h'] = Decimal(image['dpi_h'])
|
||||
image['dpi'] = (image['dpi_w'] * image['dpi_h']) ** Decimal(0.5)
|
||||
DPI_PREC = Decimal('1.000')
|
||||
image['dpi_w'] = Decimal(image['dpi_w']).quantize(DPI_PREC)
|
||||
image['dpi_h'] = Decimal(image['dpi_h']).quantize(DPI_PREC)
|
||||
dpi = Decimal(image['dpi_w'] * image['dpi_h']).sqrt()
|
||||
image['dpi'] = dpi.quantize(DPI_PREC)
|
||||
yield image
|
||||
|
||||
|
||||
@@ -302,6 +379,11 @@ def _pdf_get_pageinfo(infile, pageno: int):
|
||||
pageinfo['width_inches'] = width_pt / Decimal(72.0)
|
||||
pageinfo['height_inches'] = height_pt / Decimal(72.0)
|
||||
|
||||
try:
|
||||
pageinfo['rotate'] = int(page['/Rotate'])
|
||||
except KeyError:
|
||||
pageinfo['rotate'] = 0
|
||||
|
||||
try:
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
except AttributeError as e:
|
||||
|
||||
+20
-19
@@ -70,13 +70,14 @@ def encode_text_string(s: str) -> str:
|
||||
|
||||
From PDF 32000-1:2008 a string object may be included in hexademical form
|
||||
if it is enclosed in angle brackets. For general Unicode the string should
|
||||
be UTF-16 (big endian) with byte order marks. A non-hexademical
|
||||
representation is doable but this is preferable since it allows the output
|
||||
Postscript file to be completely ASCII and no escaping of Postscript
|
||||
characters is necessary.
|
||||
be UTF-16 (big endian) with byte order marks. Many strings including all
|
||||
ASCII strings fall could be encoded as PdfDocEncoding literals provided
|
||||
that certain Postscript sequences are escaped. But it's far simpler to
|
||||
encode everything as UTF-16.
|
||||
'''
|
||||
|
||||
# Sometimes lazy C programmer leave their NULs at the end of strings
|
||||
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
||||
# insert into PDFs
|
||||
# tests/resources/aspect.pdf is one example (created by ImageMagick)
|
||||
s = s.replace('\x00', '')
|
||||
|
||||
@@ -135,24 +136,24 @@ def file_claims_pdfa(filename):
|
||||
aboutUri='',
|
||||
namespace='http://www.aiim.org/pdfa/ns/id/')
|
||||
except AttributeError:
|
||||
return {'pass': False, 'output': 'pdf', 'message': 'No XMP metadata'}
|
||||
return {'pass': False, 'output': 'pdf',
|
||||
'conformance': 'No XMP metadata'}
|
||||
|
||||
pdfa_dict = {attr.localName: attr.value for attr in pdfa_nodes}
|
||||
pdfa_dict['pass'] = False
|
||||
pdfa_dict['output'] = 'pdf'
|
||||
if pdfa_dict:
|
||||
part_conformance = pdfa_dict['part'] + pdfa_dict['conformance']
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
if not pdfa_dict:
|
||||
return {'pass': False, 'output': 'pdf',
|
||||
'conformance': 'No XMP metadata'}
|
||||
|
||||
message = 'File claims to be PDF/A-{}'.format(
|
||||
part_conformance)
|
||||
part_conformance = pdfa_dict['part'] + pdfa_dict['conformance']
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
|
||||
if part_conformance in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['message'] = message
|
||||
else:
|
||||
pdfa_dict['message'] = 'File is a regular PDF'
|
||||
conformance = 'PDF/A-{}'.format(
|
||||
part_conformance)
|
||||
|
||||
if part_conformance in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
|
||||
return pdfa_dict
|
||||
|
||||
|
||||
+12
-11
@@ -10,11 +10,7 @@ from . import ExitCode, get_program, page_number
|
||||
from collections import namedtuple
|
||||
|
||||
from subprocess import Popen, PIPE, CalledProcessError, \
|
||||
TimeoutExpired, check_output, STDOUT
|
||||
try:
|
||||
from subprocess import DEVNULL
|
||||
except ImportError:
|
||||
DEVNULL = open(os.devnull, 'wb')
|
||||
TimeoutExpired, check_output, STDOUT, DEVNULL
|
||||
|
||||
|
||||
OrientationConfidence = namedtuple(
|
||||
@@ -151,11 +147,16 @@ def page_timedout(log, input_file):
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, pageinfo):
|
||||
def _generate_null_hocr(output_hocr, image):
|
||||
"""Produce a .hocr file that reports no text detected on a page that is
|
||||
the same size as the input image."""
|
||||
from PIL import Image
|
||||
|
||||
im = Image.open(image)
|
||||
w, h = im.size
|
||||
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
f.write(HOCR_TEMPLATE.format(w, h))
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
@@ -185,11 +186,11 @@ def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
_generate_null_hocr(output_hocr, input_file)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if 'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
_generate_null_hocr(output_hocr, input_file)
|
||||
return
|
||||
|
||||
raise e from e
|
||||
|
||||
+263
-222
@@ -4,294 +4,335 @@
|
||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
||||
-->
|
||||
<!-- Title: Pipeline: Pages: 1 -->
|
||||
<svg width="1452pt" height="824pt"
|
||||
viewBox="0.00 0.00 1452.00 824.08" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 820.083)">
|
||||
<svg width="1444pt" height="973pt"
|
||||
viewBox="0.00 0.00 1444.00 973.18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 969.181)">
|
||||
<title>Pipeline:</title>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-820.083 1448,-820.083 1448,4 -4,4"/>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-969.181 1440,-969.181 1440,4 -4,4"/>
|
||||
<g id="clust1" class="cluster"><title>clustertasks</title>
|
||||
<polygon fill="none" stroke="black" points="8,-8 8,-808.083 1436,-808.083 1436,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="722" y="-780.083" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
|
||||
<polygon fill="none" stroke="black" points="8,-8 8,-957.181 1428,-957.181 1428,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="718" y="-929.181" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
|
||||
</g>
|
||||
<!-- t0 -->
|
||||
<g id="node1" class="node"><title>t0</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1256.54,-762.083 1033.46,-762.083 1029.46,-758.083 1029.46,-726.083 1252.54,-726.083 1256.54,-730.083 1256.54,-762.083"/>
|
||||
<polyline fill="none" stroke="black" points="1252.54,-758.083 1029.46,-758.083 "/>
|
||||
<polyline fill="none" stroke="black" points="1252.54,-758.083 1252.54,-726.083 "/>
|
||||
<polyline fill="none" stroke="black" points="1252.54,-758.083 1256.54,-762.083 "/>
|
||||
<text text-anchor="middle" x="1143" y="-738.083" font-family="Times,serif" font-size="20.00">repair_pdf</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1219.77,-911.181 1034.23,-911.181 1030.23,-907.181 1030.23,-875.181 1215.77,-875.181 1219.77,-879.181 1219.77,-911.181"/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1030.23,-907.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1215.77,-875.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1219.77,-911.181 "/>
|
||||
<text text-anchor="middle" x="1125" y="-887.181" font-family="Times,serif" font-size="20.00">triage</text>
|
||||
</g>
|
||||
<!-- t1 -->
|
||||
<g id="node2" class="node"><title>t1</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1234.11,-683.155 1030,-700.057 825.888,-683.155 826.078,-655.806 1233.92,-655.806 1234.11,-683.155"/>
|
||||
<polygon fill="none" stroke="black" points="1238.13,-686.834 1030,-704.069 821.866,-686.834 822.11,-651.808 1237.89,-651.808 1238.13,-686.834"/>
|
||||
<text text-anchor="middle" x="1030" y="-669.596" font-family="Times,serif" font-size="20.00">split_pages</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1238.54,-853.181 1015.46,-853.181 1011.46,-849.181 1011.46,-817.181 1234.54,-817.181 1238.54,-821.181 1238.54,-853.181"/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1011.46,-849.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1234.54,-817.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1238.54,-853.181 "/>
|
||||
<text text-anchor="middle" x="1125" y="-829.181" font-family="Times,serif" font-size="20.00">repair_pdf</text>
|
||||
</g>
|
||||
<!-- t0->t1 -->
|
||||
<g id="edge1" class="edge"><title>t0->t1</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1113.9,-725.961C1103.58,-719.89 1091.66,-712.873 1080.09,-706.069"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1081.75,-702.982 1071.35,-700.927 1078.2,-709.015 1081.75,-702.982"/>
|
||||
</g>
|
||||
<!-- t14 -->
|
||||
<g id="node16" class="node"><title>t14</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1428.08,-625.109 1089.92,-625.109 1085.92,-621.109 1085.92,-589.109 1424.08,-589.109 1428.08,-593.109 1428.08,-625.109"/>
|
||||
<polyline fill="none" stroke="black" points="1424.08,-621.109 1085.92,-621.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1424.08,-621.109 1424.08,-589.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1424.08,-621.109 1428.08,-625.109 "/>
|
||||
<text text-anchor="middle" x="1257" y="-601.109" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
|
||||
</g>
|
||||
<!-- t0->t14 -->
|
||||
<g id="edge22" class="edge"><title>t0->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1219.32,-726.004C1229.98,-720.592 1239.75,-713.466 1247,-704.083 1261.92,-684.78 1263.1,-656.385 1261.35,-635.486"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1264.8,-634.881 1260.22,-625.328 1257.84,-635.653 1264.8,-634.881"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1125,-875.075C1125,-871.384 1125,-867.394 1125,-863.424"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-863.182 1125,-853.182 1121.5,-863.182 1128.5,-863.182"/>
|
||||
</g>
|
||||
<!-- t2 -->
|
||||
<g id="node3" class="node"><title>t2</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1029.88,-625.109 748.117,-625.109 744.117,-621.109 744.117,-589.109 1025.88,-589.109 1029.88,-593.109 1029.88,-625.109"/>
|
||||
<polyline fill="none" stroke="black" points="1025.88,-621.109 744.117,-621.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1025.88,-621.109 1025.88,-589.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1025.88,-621.109 1029.88,-625.109 "/>
|
||||
<text text-anchor="middle" x="887" y="-601.109" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1329.11,-774.253 1125,-791.156 920.888,-774.253 921.078,-746.905 1328.92,-746.905 1329.11,-774.253"/>
|
||||
<polygon fill="none" stroke="black" points="1333.13,-777.932 1125,-795.167 916.866,-777.932 917.11,-742.906 1332.89,-742.906 1333.13,-777.932"/>
|
||||
<text text-anchor="middle" x="1125" y="-760.694" font-family="Times,serif" font-size="20.00">split_pages</text>
|
||||
</g>
|
||||
<!-- t1->t2 -->
|
||||
<g id="edge2" class="edge"><title>t1->t2</title>
|
||||
<path fill="none" stroke="#0044a0" d="M980.788,-651.715C965.266,-644.498 948.187,-636.558 932.905,-629.452"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="934.195,-626.193 923.652,-625.15 931.244,-632.54 934.195,-626.193"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1125,-817.059C1125,-813.44 1125,-809.484 1125,-805.438"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-805.334 1125,-795.334 1121.5,-805.334 1128.5,-805.334"/>
|
||||
</g>
|
||||
<!-- t15 -->
|
||||
<g id="node17" class="node"><title>t15</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="632.082,-716.208 293.918,-716.208 289.918,-712.208 289.918,-680.208 628.082,-680.208 632.082,-684.208 632.082,-716.208"/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 289.918,-712.208 "/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 628.082,-680.208 "/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 632.082,-716.208 "/>
|
||||
<text text-anchor="middle" x="461" y="-692.208" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
|
||||
</g>
|
||||
<!-- t1->t15 -->
|
||||
<g id="edge23" class="edge"><title>t1->t15</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1026.3,-817.165C989.157,-810.572 946.587,-802.788 908,-795.181 781.899,-770.319 636.479,-738.51 546.633,-718.49"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="547.289,-715.05 536.767,-716.289 545.765,-721.882 547.289,-715.05"/>
|
||||
</g>
|
||||
<!-- t18 -->
|
||||
<g id="node19" class="node"><title>t18</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1025.79,-156.452 1221,-129.134 1416.21,-156.452 1416.03,-200.654 1025.97,-200.654 1025.79,-156.452"/>
|
||||
<polygon fill="none" stroke="black" points="1021.78,-152.971 1221,-125.091 1420.22,-152.971 1420.01,-204.657 1021.99,-204.657 1021.78,-152.971"/>
|
||||
<text text-anchor="middle" x="1221" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_qpdf</text>
|
||||
</g>
|
||||
<!-- t1->t18 -->
|
||||
<g id="edge33" class="edge"><title>t1->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1238.67,-826.639C1321.4,-814.236 1419,-782.049 1419,-699.208 1419,-699.208 1419,-699.208 1419,-319.208 1419,-280.294 1424,-263.161 1398,-234.208 1389.64,-224.894 1379.82,-216.923 1369.17,-210.101"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1370.83,-207.019 1360.46,-204.872 1367.23,-213.02 1370.83,-207.019"/>
|
||||
</g>
|
||||
<!-- t3 -->
|
||||
<g id="node4" class="node"><title>t3</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1128.15,-567.109 893.852,-567.109 889.852,-563.109 889.852,-531.109 1124.15,-531.109 1128.15,-535.109 1128.15,-567.109"/>
|
||||
<polyline fill="none" stroke="black" points="1124.15,-563.109 889.852,-563.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1124.15,-563.109 1124.15,-531.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1124.15,-563.109 1128.15,-567.109 "/>
|
||||
<text text-anchor="middle" x="1009" y="-543.109" font-family="Times,serif" font-size="20.00">orient_page</text>
|
||||
</g>
|
||||
<!-- t1->t3 -->
|
||||
<g id="edge4" class="edge"><title>t1->t3</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1037.63,-651.508C1042.21,-633.96 1045.92,-609.455 1039,-589.109 1037.4,-584.404 1034.98,-579.822 1032.19,-575.552"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1034.85,-573.258 1026.1,-567.282 1029.21,-577.409 1034.85,-573.258"/>
|
||||
<polygon fill="#efa03b" stroke="black" points="1186.88,-716.208 905.117,-716.208 901.117,-712.208 901.117,-680.208 1182.88,-680.208 1186.88,-684.208 1186.88,-716.208"/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 901.117,-712.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 1182.88,-680.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 1186.88,-716.208 "/>
|
||||
<text text-anchor="middle" x="1044" y="-692.208" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
|
||||
</g>
|
||||
<!-- t2->t3 -->
|
||||
<g id="edge3" class="edge"><title>t2->t3</title>
|
||||
<path fill="none" stroke="#0044a0" d="M924.267,-589.003C936.328,-583.467 949.857,-577.257 962.502,-571.453"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="964.333,-574.463 971.961,-567.111 961.413,-568.102 964.333,-574.463"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1097.12,-742.813C1089.24,-736.345 1080.66,-729.296 1072.73,-722.788"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1074.71,-719.888 1064.76,-716.249 1070.27,-725.299 1074.71,-719.888"/>
|
||||
</g>
|
||||
<!-- t4 -->
|
||||
<g id="node5" class="node"><title>t4</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1027.3,-509.109 676.701,-509.109 672.701,-505.109 672.701,-473.109 1023.3,-473.109 1027.3,-477.109 1027.3,-509.109"/>
|
||||
<polyline fill="none" stroke="black" points="1023.3,-505.109 672.701,-505.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1023.3,-505.109 1023.3,-473.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1023.3,-505.109 1027.3,-509.109 "/>
|
||||
<text text-anchor="middle" x="850" y="-485.109" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1258.15,-658.208 1023.85,-658.208 1019.85,-654.208 1019.85,-622.208 1254.15,-622.208 1258.15,-626.208 1258.15,-658.208"/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1019.85,-654.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1254.15,-622.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1258.15,-658.208 "/>
|
||||
<text text-anchor="middle" x="1139" y="-634.208" font-family="Times,serif" font-size="20.00">orient_page</text>
|
||||
</g>
|
||||
<!-- t2->t4 -->
|
||||
<g id="edge5" class="edge"><title>t2->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1171.65,-742.795C1181.42,-735.692 1190.44,-726.864 1196,-716.208 1203.4,-702.023 1203.46,-694.36 1196,-680.208 1192.84,-674.222 1188.31,-668.98 1183.15,-664.445"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1185.3,-661.676 1175.25,-658.302 1181,-667.203 1185.3,-661.676"/>
|
||||
</g>
|
||||
<!-- t3->t4 -->
|
||||
<g id="edge5" class="edge"><title>t3->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M960.431,-531.003C943.787,-525.141 924.999,-518.524 907.704,-512.433"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="908.867,-509.132 898.272,-509.111 906.541,-515.734 908.867,-509.132"/>
|
||||
</g>
|
||||
<!-- t9 -->
|
||||
<g id="node11" class="node"><title>t9</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="314.109,-277.109 19.8906,-277.109 15.8906,-273.109 15.8906,-241.109 310.109,-241.109 314.109,-245.109 314.109,-277.109"/>
|
||||
<polyline fill="none" stroke="black" points="310.109,-273.109 15.8906,-273.109 "/>
|
||||
<polyline fill="none" stroke="black" points="310.109,-273.109 310.109,-241.109 "/>
|
||||
<polyline fill="none" stroke="black" points="310.109,-273.109 314.109,-277.109 "/>
|
||||
<text text-anchor="middle" x="165" y="-253.109" font-family="Times,serif" font-size="20.00">select_image_layer</text>
|
||||
</g>
|
||||
<!-- t3->t9 -->
|
||||
<g id="edge14" class="edge"><title>t3->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M889.675,-542.297C675.505,-530.255 248,-498.192 248,-434.109 248,-434.109 248,-434.109 248,-374.109 248,-337.597 219.83,-304.936 196.312,-284.031"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="198.437,-281.243 188.562,-277.404 193.888,-286.563 198.437,-281.243"/>
|
||||
</g>
|
||||
<!-- t15 -->
|
||||
<g id="node14" class="node"><title>t15</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1391.34,-451.109 1170.66,-451.109 1166.66,-447.109 1166.66,-415.109 1387.34,-415.109 1391.34,-419.109 1391.34,-451.109"/>
|
||||
<polyline fill="none" stroke="black" points="1387.34,-447.109 1166.66,-447.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1387.34,-447.109 1387.34,-415.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1387.34,-447.109 1391.34,-451.109 "/>
|
||||
<text text-anchor="middle" x="1279" y="-427.109" font-family="Times,serif" font-size="20.00">skip_page</text>
|
||||
</g>
|
||||
<!-- t3->t15 -->
|
||||
<g id="edge19" class="edge"><title>t3->t15</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1060.32,-531.078C1078.95,-524.591 1100.07,-516.881 1119,-509.109 1159.3,-492.568 1204.11,-471.304 1236.04,-455.645"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1237.67,-458.74 1245.1,-451.181 1234.58,-452.461 1237.67,-458.74"/>
|
||||
</g>
|
||||
<!-- t13 -->
|
||||
<g id="node15" class="node"><title>t13</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1388.24,-277.109 1007.76,-277.109 1003.76,-273.109 1003.76,-241.109 1384.24,-241.109 1388.24,-245.109 1388.24,-277.109"/>
|
||||
<polyline fill="none" stroke="black" points="1384.24,-273.109 1003.76,-273.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1384.24,-273.109 1384.24,-241.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1384.24,-273.109 1388.24,-277.109 "/>
|
||||
<text text-anchor="middle" x="1196" y="-253.109" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
|
||||
</g>
|
||||
<!-- t3->t13 -->
|
||||
<g id="edge21" class="edge"><title>t3->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1040.79,-531.094C1071.69,-511.983 1114,-477.834 1114,-434.109 1114,-434.109 1114,-434.109 1114,-374.109 1114,-337.749 1141.83,-305.063 1165.07,-284.11"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1167.46,-286.664 1172.72,-277.466 1162.87,-281.377 1167.46,-286.664"/>
|
||||
<g id="edge4" class="edge"><title>t3->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1073.02,-680.102C1081.95,-674.836 1091.92,-668.963 1101.34,-663.407"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1103.32,-666.302 1110.16,-658.209 1099.77,-660.272 1103.32,-666.302"/>
|
||||
</g>
|
||||
<!-- t5 -->
|
||||
<g id="node6" class="node"><title>t5</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="830.742,-451.109 535.258,-451.109 531.258,-447.109 531.258,-415.109 826.742,-415.109 830.742,-419.109 830.742,-451.109"/>
|
||||
<polyline fill="none" stroke="black" points="826.742,-447.109 531.258,-447.109 "/>
|
||||
<polyline fill="none" stroke="black" points="826.742,-447.109 826.742,-415.109 "/>
|
||||
<polyline fill="none" stroke="black" points="826.742,-447.109 830.742,-451.109 "/>
|
||||
<text text-anchor="middle" x="681" y="-427.109" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1271.3,-600.208 920.701,-600.208 916.701,-596.208 916.701,-564.208 1267.3,-564.208 1271.3,-568.208 1271.3,-600.208"/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 916.701,-596.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 1267.3,-564.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 1271.3,-600.208 "/>
|
||||
<text text-anchor="middle" x="1094" y="-576.208" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
|
||||
</g>
|
||||
<!-- t4->t5 -->
|
||||
<g id="edge6" class="edge"><title>t4->t5</title>
|
||||
<path fill="none" stroke="#0044a0" d="M798.376,-473.003C780.521,-467.087 760.346,-460.402 741.823,-454.264"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="742.901,-450.934 732.308,-451.111 740.699,-457.579 742.901,-450.934"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1125.25,-622.102C1121.76,-617.759 1117.94,-613.003 1114.21,-608.353"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1116.65,-605.812 1107.66,-600.209 1111.2,-610.197 1116.65,-605.812"/>
|
||||
</g>
|
||||
<!-- t8 -->
|
||||
<g id="node10" class="node"><title>t8</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="980.119,-335.109 665.881,-335.109 661.881,-331.109 661.881,-299.109 976.119,-299.109 980.119,-303.109 980.119,-335.109"/>
|
||||
<polyline fill="none" stroke="black" points="976.119,-331.109 661.881,-331.109 "/>
|
||||
<polyline fill="none" stroke="black" points="976.119,-331.109 976.119,-299.109 "/>
|
||||
<polyline fill="none" stroke="black" points="976.119,-331.109 980.119,-335.109 "/>
|
||||
<text text-anchor="middle" x="821" y="-311.109" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
|
||||
<!-- t10 -->
|
||||
<g id="node12" class="node"><title>t10</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="334,-348.208 35.7812,-320.208 334,-292.208 632.219,-320.208 334,-348.208"/>
|
||||
<text text-anchor="middle" x="334" y="-314.208" font-family="Times,serif" font-size="20.00">select_image_layer</text>
|
||||
</g>
|
||||
<!-- t4->t8 -->
|
||||
<g id="edge12" class="edge"><title>t4->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M848.03,-473.087C846.196,-457.857 843.261,-434.941 840,-415.109 836.1,-391.393 830.78,-364.592 826.761,-345.181"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="830.152,-344.296 824.68,-335.224 823.3,-345.728 830.152,-344.296"/>
|
||||
<!-- t4->t10 -->
|
||||
<g id="edge15" class="edge"><title>t4->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1019.87,-634.085C799.18,-622.991 349,-592.156 349,-525.208 349,-525.208 349,-525.208 349,-465.208 349,-428.606 344.147,-387.052 339.961,-358.022"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="343.393,-357.303 338.46,-347.926 336.469,-358.332 343.393,-357.303"/>
|
||||
</g>
|
||||
<!-- t16 -->
|
||||
<g id="node15" class="node"><title>t16</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1380.34,-270.208 1159.66,-270.208 1155.66,-266.208 1155.66,-234.208 1376.34,-234.208 1380.34,-238.208 1380.34,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1155.66,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1376.34,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1380.34,-270.208 "/>
|
||||
<text text-anchor="middle" x="1268" y="-246.208" font-family="Times,serif" font-size="20.00">skip_page</text>
|
||||
</g>
|
||||
<!-- t4->t16 -->
|
||||
<g id="edge20" class="edge"><title>t4->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1258.35,-631.473C1326.7,-619.809 1399,-591.745 1399,-525.208 1399,-525.208 1399,-525.208 1399,-397.208 1399,-350.33 1418.37,-328.748 1389,-292.208 1383.58,-285.469 1376.99,-279.879 1369.71,-275.244"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1371.3,-272.123 1360.88,-270.231 1367.85,-278.211 1371.3,-272.123"/>
|
||||
</g>
|
||||
<!-- t14 -->
|
||||
<g id="node16" class="node"><title>t14</title>
|
||||
<polygon fill="#66ccff" stroke="black" points="1380.24,-338.208 999.76,-338.208 995.76,-334.208 995.76,-302.208 1376.24,-302.208 1380.24,-306.208 1380.24,-338.208"/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 995.76,-334.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 1376.24,-302.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 1380.24,-338.208 "/>
|
||||
<text text-anchor="middle" x="1188" y="-314.208" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
|
||||
</g>
|
||||
<!-- t4->t14 -->
|
||||
<g id="edge22" class="edge"><title>t4->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1244.5,-622.179C1297.25,-607.003 1350,-578.503 1350,-525.208 1350,-525.208 1350,-525.208 1350,-465.208 1350,-422.752 1366.76,-403.169 1340,-370.208 1330.4,-358.377 1317.91,-349.368 1304.22,-342.513"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1305.62,-339.304 1295.07,-338.313 1302.7,-345.666 1305.62,-339.304"/>
|
||||
</g>
|
||||
<!-- t6 -->
|
||||
<g id="node7" class="node"><title>t6</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="801.95,-393.109 524.05,-393.109 520.05,-389.109 520.05,-357.109 797.95,-357.109 801.95,-361.109 801.95,-393.109"/>
|
||||
<polyline fill="none" stroke="black" points="797.95,-389.109 520.05,-389.109 "/>
|
||||
<polyline fill="none" stroke="black" points="797.95,-389.109 797.95,-357.109 "/>
|
||||
<polyline fill="none" stroke="black" points="797.95,-389.109 801.95,-393.109 "/>
|
||||
<text text-anchor="middle" x="661" y="-369.109" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1074.74,-542.208 779.258,-542.208 775.258,-538.208 775.258,-506.208 1070.74,-506.208 1074.74,-510.208 1074.74,-542.208"/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 775.258,-538.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 1070.74,-506.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 1074.74,-542.208 "/>
|
||||
<text text-anchor="middle" x="925" y="-518.208" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
|
||||
</g>
|
||||
<!-- t5->t6 -->
|
||||
<g id="edge7" class="edge"><title>t5->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M674.891,-415.003C673.495,-411.095 671.979,-406.852 670.48,-402.652"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="673.731,-401.351 667.072,-393.111 667.139,-403.705 673.731,-401.351"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1042.38,-564.102C1024.52,-558.185 1004.35,-551.5 985.823,-545.362"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="986.901,-542.032 976.308,-542.209 984.699,-548.677 986.901,-542.032"/>
|
||||
</g>
|
||||
<!-- t5->t8 -->
|
||||
<g id="edge11" class="edge"><title>t5->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M781.944,-415.046C792.967,-409.579 803.051,-402.434 811,-393.109 822.009,-380.195 824.454,-361.215 824.174,-345.581"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="827.659,-345.2 823.617,-335.407 820.669,-345.583 827.659,-345.2"/>
|
||||
<!-- t9 -->
|
||||
<g id="node11" class="node"><title>t9</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1013,-426.208 694.762,-398.208 1013,-370.208 1331.24,-398.208 1013,-426.208"/>
|
||||
<text text-anchor="middle" x="1013" y="-392.208" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
|
||||
</g>
|
||||
<!-- t5->t9 -->
|
||||
<g id="edge13" class="edge"><title>t5->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1093.89,-563.996C1093.29,-548.44 1091.11,-525.146 1084,-506.208 1073.94,-479.426 1056.13,-452.609 1040.86,-432.521"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1043.62,-430.369 1034.72,-424.619 1038.09,-434.663 1043.62,-430.369"/>
|
||||
</g>
|
||||
<!-- t7 -->
|
||||
<g id="node8" class="node"><title>t7</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="643.666,-335.109 354.334,-335.109 350.334,-331.109 350.334,-299.109 639.666,-299.109 643.666,-303.109 643.666,-335.109"/>
|
||||
<polyline fill="none" stroke="black" points="639.666,-331.109 350.334,-331.109 "/>
|
||||
<polyline fill="none" stroke="black" points="639.666,-331.109 639.666,-299.109 "/>
|
||||
<polyline fill="none" stroke="black" points="639.666,-331.109 643.666,-335.109 "/>
|
||||
<text text-anchor="middle" x="497" y="-311.109" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="993.95,-484.208 716.05,-484.208 712.05,-480.208 712.05,-448.208 989.95,-448.208 993.95,-452.208 993.95,-484.208"/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 712.05,-480.208 "/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 989.95,-448.208 "/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 993.95,-484.208 "/>
|
||||
<text text-anchor="middle" x="853" y="-460.208" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
|
||||
</g>
|
||||
<!-- t6->t7 -->
|
||||
<g id="edge8" class="edge"><title>t6->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M610.903,-357.003C593.736,-351.141 574.358,-344.524 556.518,-338.433"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="557.384,-335.03 546.79,-335.111 555.122,-341.654 557.384,-335.03"/>
|
||||
<path fill="none" stroke="#0044a0" d="M903.006,-506.102C896.726,-501.216 889.771,-495.807 883.095,-490.615"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="884.901,-487.586 874.859,-484.209 880.604,-493.111 884.901,-487.586"/>
|
||||
</g>
|
||||
<!-- t6->t8 -->
|
||||
<g id="edge10" class="edge"><title>t6->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M709.875,-357.003C726.624,-351.141 745.529,-344.524 762.933,-338.433"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="764.142,-341.718 772.425,-335.111 761.83,-335.111 764.142,-341.718"/>
|
||||
<!-- t6->t9 -->
|
||||
<g id="edge12" class="edge"><title>t6->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M977.454,-506.003C987.329,-500.504 996.562,-493.377 1003,-484.208 1012.67,-470.441 1015.74,-452.229 1016.16,-436.195"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1019.66,-436.018 1016.09,-426.041 1012.66,-436.063 1019.66,-436.018"/>
|
||||
</g>
|
||||
<!-- t10 -->
|
||||
<g id="node9" class="node"><title>t10</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="620.109,-277.109 335.891,-277.109 331.891,-273.109 331.891,-241.109 616.109,-241.109 620.109,-245.109 620.109,-277.109"/>
|
||||
<polyline fill="none" stroke="black" points="616.109,-273.109 331.891,-273.109 "/>
|
||||
<polyline fill="none" stroke="black" points="616.109,-273.109 616.109,-241.109 "/>
|
||||
<polyline fill="none" stroke="black" points="616.109,-273.109 620.109,-277.109 "/>
|
||||
<text text-anchor="middle" x="476" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
|
||||
<!-- t8 -->
|
||||
<g id="node9" class="node"><title>t8</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="676.666,-416.208 387.334,-416.208 383.334,-412.208 383.334,-380.208 672.666,-380.208 676.666,-384.208 676.666,-416.208"/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 383.334,-412.208 "/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 672.666,-380.208 "/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 676.666,-416.208 "/>
|
||||
<text text-anchor="middle" x="530" y="-392.208" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
|
||||
</g>
|
||||
<!-- t7->t10 -->
|
||||
<g id="edge9" class="edge"><title>t7->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M490.585,-299.003C489.12,-295.095 487.528,-290.852 485.953,-286.652"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="489.164,-285.245 482.376,-277.111 482.61,-287.703 489.164,-285.245"/>
|
||||
<!-- t7->t8 -->
|
||||
<g id="edge9" class="edge"><title>t7->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M769.399,-448.125C724.645,-438.98 669.394,-427.691 623.252,-418.262"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="623.748,-414.791 613.25,-416.219 622.347,-421.65 623.748,-414.791"/>
|
||||
</g>
|
||||
<!-- t7->t9 -->
|
||||
<g id="edge11" class="edge"><title>t7->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M894.62,-448.039C911.413,-441.112 931.162,-432.966 949.666,-425.333"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="951.009,-428.565 958.918,-421.516 948.339,-422.094 951.009,-428.565"/>
|
||||
</g>
|
||||
<!-- t11 -->
|
||||
<g id="node13" class="node"><title>t11</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="985.486,-277.109 642.514,-277.109 638.514,-273.109 638.514,-241.109 981.486,-241.109 985.486,-245.109 985.486,-277.109"/>
|
||||
<polyline fill="none" stroke="black" points="981.486,-273.109 638.514,-273.109 "/>
|
||||
<polyline fill="none" stroke="black" points="981.486,-273.109 981.486,-241.109 "/>
|
||||
<polyline fill="none" stroke="black" points="981.486,-273.109 985.486,-277.109 "/>
|
||||
<text text-anchor="middle" x="812" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
|
||||
</g>
|
||||
<!-- t7->t11 -->
|
||||
<g id="edge18" class="edge"><title>t7->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M592.793,-299.079C628.531,-292.726 669.303,-285.478 705.912,-278.969"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="706.887,-282.351 716.12,-277.155 705.661,-275.459 706.887,-282.351"/>
|
||||
</g>
|
||||
<!-- t12 -->
|
||||
<g id="node12" class="node"><title>t12</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="607.082,-219.109 348.918,-219.109 344.918,-215.109 344.918,-183.109 603.082,-183.109 607.082,-187.109 607.082,-219.109"/>
|
||||
<polyline fill="none" stroke="black" points="603.082,-215.109 344.918,-215.109 "/>
|
||||
<polyline fill="none" stroke="black" points="603.082,-215.109 603.082,-183.109 "/>
|
||||
<polyline fill="none" stroke="black" points="603.082,-215.109 607.082,-219.109 "/>
|
||||
<text text-anchor="middle" x="476" y="-195.109" font-family="Times,serif" font-size="20.00">add_text_layer</text>
|
||||
</g>
|
||||
<!-- t10->t12 -->
|
||||
<g id="edge15" class="edge"><title>t10->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M476,-241.003C476,-237.312 476,-233.322 476,-229.352"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="479.5,-229.111 476,-219.111 472.5,-229.111 479.5,-229.111"/>
|
||||
</g>
|
||||
<!-- t8->t9 -->
|
||||
<g id="edge13" class="edge"><title>t8->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M661.842,-299.864C658.871,-299.606 655.921,-299.354 653,-299.109 509.812,-287.095 471.159,-289.734 324.542,-277.219"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="324.526,-273.705 314.262,-276.332 323.924,-280.679 324.526,-273.705"/>
|
||||
<g id="node10" class="node"><title>t11</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="938.109,-338.208 653.891,-338.208 649.891,-334.208 649.891,-302.208 934.109,-302.208 938.109,-306.208 938.109,-338.208"/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 649.891,-334.208 "/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 934.109,-302.208 "/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 938.109,-338.208 "/>
|
||||
<text text-anchor="middle" x="794" y="-314.208" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
|
||||
</g>
|
||||
<!-- t8->t11 -->
|
||||
<g id="edge17" class="edge"><title>t8->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M818.251,-299.003C817.64,-295.204 816.979,-291.087 816.322,-287.002"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="819.775,-286.429 814.732,-277.111 812.864,-287.539 819.775,-286.429"/>
|
||||
<g id="edge10" class="edge"><title>t8->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M589.247,-380.152C629.442,-368.58 682.727,-353.241 724.863,-341.111"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="725.853,-344.468 734.495,-338.338 723.917,-337.741 725.853,-344.468"/>
|
||||
</g>
|
||||
<!-- t8->t13 -->
|
||||
<g id="edge20" class="edge"><title>t8->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M935.04,-299.079C978.311,-292.618 1027.78,-285.23 1071.94,-278.636"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1072.48,-282.093 1081.86,-277.155 1071.45,-275.17 1072.48,-282.093"/>
|
||||
<!-- t12 -->
|
||||
<g id="node14" class="node"><title>t12</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="1118.49,-270.208 775.514,-270.208 771.514,-266.208 771.514,-234.208 1114.49,-234.208 1118.49,-238.208 1118.49,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 771.514,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 1114.49,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 1118.49,-270.208 "/>
|
||||
<text text-anchor="middle" x="945" y="-246.208" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
|
||||
</g>
|
||||
<!-- t8->t12 -->
|
||||
<g id="edge19" class="edge"><title>t8->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M620.537,-380.161C641.947,-376.512 664.736,-372.939 686,-370.208 714.866,-366.5 927.304,-369.633 947,-348.208 963.502,-330.256 960.281,-301.363 954.611,-280.149"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="957.925,-279.012 951.707,-270.433 951.218,-281.017 957.925,-279.012"/>
|
||||
</g>
|
||||
<!-- t13 -->
|
||||
<g id="node13" class="node"><title>t13</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="753.082,-270.208 494.918,-270.208 490.918,-266.208 490.918,-234.208 749.082,-234.208 753.082,-238.208 753.082,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 490.918,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 749.082,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 753.082,-270.208 "/>
|
||||
<text text-anchor="middle" x="622" y="-246.208" font-family="Times,serif" font-size="20.00">add_text_layer</text>
|
||||
</g>
|
||||
<!-- t11->t13 -->
|
||||
<g id="edge16" class="edge"><title>t11->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M749.258,-302.039C726.862,-293.446 699.574,-282.975 676.019,-273.936"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="677.108,-270.605 666.518,-270.29 674.6,-277.14 677.108,-270.605"/>
|
||||
</g>
|
||||
<!-- t9->t10 -->
|
||||
<g id="edge14" class="edge"><title>t9->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M877.633,-382.056C761.19,-369.023 593.788,-350.286 475.578,-337.054"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="475.697,-333.546 465.37,-335.912 474.919,-340.503 475.697,-333.546"/>
|
||||
</g>
|
||||
<!-- t9->t12 -->
|
||||
<g id="edge16" class="edge"><title>t9->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M259.577,-241.079C294.86,-234.726 335.115,-227.478 371.259,-220.969"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="372.116,-224.372 381.337,-219.155 370.875,-217.482 372.116,-224.372"/>
|
||||
<g id="edge18" class="edge"><title>t9->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M998.887,-371.447C994.931,-364.003 990.702,-355.831 987,-348.208 983.822,-341.663 967.96,-305.616 956.669,-279.865"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="959.82,-278.337 952.601,-270.582 953.409,-281.146 959.82,-278.337"/>
|
||||
</g>
|
||||
<!-- t16 -->
|
||||
<g id="node17" class="node"><title>t16</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1031.47,-105.333 1196,-78.005 1360.53,-105.333 1360.37,-149.551 1031.63,-149.551 1031.47,-105.333"/>
|
||||
<polygon fill="none" stroke="black" points="1027.46,-101.94 1196,-73.9453 1364.54,-101.94 1364.36,-153.556 1027.64,-153.556 1027.46,-101.94"/>
|
||||
<text text-anchor="middle" x="1196" y="-111.555" font-family="Times,serif" font-size="20.00">merge_pages</text>
|
||||
<!-- t9->t14 -->
|
||||
<g id="edge21" class="edge"><title>t9->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1065.03,-374.611C1088.59,-364.381 1116.27,-352.359 1139.4,-342.314"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1140.9,-345.479 1148.68,-338.285 1138.11,-339.058 1140.9,-345.479"/>
|
||||
</g>
|
||||
<!-- t12->t16 -->
|
||||
<g id="edge26" class="edge"><title>t12->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M607.222,-185.246C722.142,-172.229 890.172,-153.196 1017.34,-138.791"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1018.04,-142.235 1027.58,-137.632 1017.25,-135.279 1018.04,-142.235"/>
|
||||
</g>
|
||||
<!-- t11->t16 -->
|
||||
<g id="edge27" class="edge"><title>t11->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M859.086,-240.997C916.223,-220.232 1014.29,-184.591 1089.94,-157.099"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1091.32,-160.323 1099.52,-153.618 1088.92,-153.744 1091.32,-160.323"/>
|
||||
</g>
|
||||
<!-- t15->t16 -->
|
||||
<g id="edge23" class="edge"><title>t15->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1321.98,-414.938C1359.16,-396.916 1407,-364.681 1407,-318.109 1407,-318.109 1407,-318.109 1407,-258.109 1407,-211.469 1370.69,-179.509 1328.2,-158.076"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1329.57,-154.854 1319.04,-153.66 1326.53,-161.158 1329.57,-154.854"/>
|
||||
</g>
|
||||
<!-- t13->t16 -->
|
||||
<g id="edge25" class="edge"><title>t13->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1196,-240.997C1196,-221.914 1196,-190.265 1196,-163.926"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1199.5,-163.759 1196,-153.759 1192.5,-163.759 1199.5,-163.759"/>
|
||||
</g>
|
||||
<!-- t14->t16 -->
|
||||
<g id="edge24" class="edge"><title>t14->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1326.64,-588.956C1373.58,-572.748 1427,-543.103 1427,-492.109 1427,-492.109 1427,-492.109 1427,-258.109 1427,-220.539 1416.55,-207.537 1388,-183.109 1376.82,-173.542 1364.16,-165.377 1350.82,-158.413"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1351.99,-155.087 1341.47,-153.781 1348.88,-161.358 1351.99,-155.087"/>
|
||||
<!-- t10->t13 -->
|
||||
<g id="edge17" class="edge"><title>t10->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M417.688,-300.029C455.345,-291.399 499.718,-281.231 537.473,-272.578"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="538.461,-275.943 547.427,-270.297 536.898,-269.12 538.461,-275.943"/>
|
||||
</g>
|
||||
<!-- t17 -->
|
||||
<g id="node18" class="node"><title>t17</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1310.18,-52 1085.82,-52 1081.82,-48 1081.82,-16 1306.18,-16 1310.18,-20 1310.18,-52"/>
|
||||
<polyline fill="none" stroke="black" points="1306.18,-48 1081.82,-48 "/>
|
||||
<polyline fill="none" stroke="black" points="1306.18,-48 1306.18,-16 "/>
|
||||
<polyline fill="none" stroke="black" points="1306.18,-48 1310.18,-52 "/>
|
||||
<text text-anchor="middle" x="1196" y="-28" font-family="Times,serif" font-size="20.00">copy_final</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="540.724,-156.455 770,-129.143 999.276,-156.455 999.062,-200.646 540.938,-200.646 540.724,-156.455"/>
|
||||
<polygon fill="none" stroke="black" points="536.704,-152.903 770,-125.112 1003.3,-152.903 1003.05,-204.648 536.954,-204.648 536.704,-152.903"/>
|
||||
<text text-anchor="middle" x="770" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_ghostscript</text>
|
||||
</g>
|
||||
<!-- t13->t17 -->
|
||||
<g id="edge27" class="edge"><title>t13->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M653.026,-234.114C665.992,-226.971 681.653,-218.343 697.24,-209.755"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="699.319,-212.606 706.388,-204.715 695.941,-206.475 699.319,-212.606"/>
|
||||
</g>
|
||||
<!-- t13->t18 -->
|
||||
<g id="edge31" class="edge"><title>t13->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M753.329,-235.235C756.584,-234.884 759.812,-234.541 763,-234.208 873.497,-222.668 902.059,-228.201 1012,-212.208 1023.74,-210.5 1035.81,-208.554 1047.95,-206.456"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1048.91,-209.84 1058.16,-204.659 1047.7,-202.946 1048.91,-209.84"/>
|
||||
</g>
|
||||
<!-- t12->t17 -->
|
||||
<g id="edge28" class="edge"><title>t12->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M908.314,-234.114C892.552,-226.77 873.424,-217.858 854.484,-209.033"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="855.759,-205.766 845.216,-204.715 852.803,-212.111 855.759,-205.766"/>
|
||||
</g>
|
||||
<!-- t12->t18 -->
|
||||
<g id="edge32" class="edge"><title>t12->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1002.86,-234.114C1029.07,-226.37 1061.19,-216.881 1092.64,-207.59"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1093.77,-210.905 1102.37,-204.715 1091.79,-204.192 1093.77,-210.905"/>
|
||||
</g>
|
||||
<!-- t16->t17 -->
|
||||
<g id="edge28" class="edge"><title>t16->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1196,-73.8665C1196,-69.8921 1196,-65.942 1196,-62.1676"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1199.5,-62.1213 1196,-52.1214 1192.5,-62.1214 1199.5,-62.1213"/>
|
||||
<g id="edge24" class="edge"><title>t16->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1155.57,-234.546C1111.13,-227.821 1059.63,-219.853 1013,-212.208 1001.53,-210.326 989.752,-208.365 977.872,-206.363"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="978.316,-202.888 967.872,-204.672 977.149,-209.79 978.316,-202.888"/>
|
||||
</g>
|
||||
<!-- t16->t18 -->
|
||||
<g id="edge29" class="edge"><title>t16->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1258.03,-233.915C1254.58,-227.933 1250.55,-220.928 1246.43,-213.785"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1249.24,-211.645 1241.21,-204.729 1243.17,-215.141 1249.24,-211.645"/>
|
||||
</g>
|
||||
<!-- t14->t17 -->
|
||||
<g id="edge26" class="edge"><title>t14->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1172.36,-302.154C1164.33,-293.013 1154.66,-281.362 1147,-270.208 1136.64,-255.121 1141.88,-244.862 1127,-234.208 1125.19,-232.915 1055.67,-220.232 978.45,-206.446"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="978.964,-202.982 968.505,-204.672 977.735,-209.874 978.964,-202.982"/>
|
||||
</g>
|
||||
<!-- t14->t18 -->
|
||||
<g id="edge30" class="edge"><title>t14->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1310.22,-302.184C1345.8,-294.579 1378.07,-284.119 1389,-270.208 1408.78,-245.028 1391.25,-224.765 1361.2,-209.211"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1362.48,-205.944 1351.96,-204.742 1359.44,-212.246 1362.48,-205.944"/>
|
||||
</g>
|
||||
<!-- t15->t17 -->
|
||||
<g id="edge25" class="edge"><title>t15->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M289.656,-684.797C164.646,-670.853 17,-641.95 17,-583.208 17,-583.208 17,-583.208 17,-319.208 17,-213.417 306.554,-181.327 526.553,-172.213"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="526.794,-175.706 536.646,-171.808 526.514,-168.711 526.794,-175.706"/>
|
||||
</g>
|
||||
<!-- t19 -->
|
||||
<g id="node20" class="node"><title>t19</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="844.361,-47.3406 995,-20.006 1145.64,-47.3406 1145.5,-91.5689 844.501,-91.5689 844.361,-47.3406"/>
|
||||
<polygon fill="none" stroke="black" points="840.355,-43.9965 995,-15.9349 1149.65,-43.9965 1149.48,-95.5749 840.519,-95.5749 840.355,-43.9965"/>
|
||||
<text text-anchor="middle" x="995" y="-53.565" font-family="Times,serif" font-size="20.00">copy_final</text>
|
||||
</g>
|
||||
<!-- t17->t19 -->
|
||||
<g id="edge35" class="edge"><title>t17->t19</title>
|
||||
<path fill="none" stroke="#0044a0" d="M841.81,-133.486C864.177,-122.839 889.024,-111.011 912.085,-100.034"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="913.64,-103.17 921.165,-95.7118 910.631,-96.8497 913.64,-103.17"/>
|
||||
</g>
|
||||
<!-- t18->t19 -->
|
||||
<g id="edge34" class="edge"><title>t18->t19</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1151.35,-134.66C1128.23,-123.705 1102.26,-111.399 1078.23,-100.009"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1079.66,-96.8131 1069.12,-95.6933 1076.66,-103.139 1079.66,-96.8131"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 23 KiB |
+1
-1
@@ -1,2 +1,2 @@
|
||||
[pytest]
|
||||
norecursedirs = lib
|
||||
norecursedirs = lib .pc .git
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[bdist_wheel]
|
||||
python-tag = py34
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
test=pytest
|
||||
|
||||
[check-manifest]
|
||||
ignore =
|
||||
.github
|
||||
@@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
if sys.version_info < (3, 4):
|
||||
print("Python 3.4 or newer is required")
|
||||
print("Python 3.4 or newer is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup # nopep8
|
||||
@@ -148,9 +148,10 @@ def check_external_program(
|
||||
|
||||
|
||||
command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
|
||||
forced = ('--force' in sys.argv)
|
||||
|
||||
|
||||
if command.startswith('install') or \
|
||||
if not forced and command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
@@ -174,6 +175,8 @@ if command.startswith('install') or \
|
||||
package='qpdf',
|
||||
version_check_args=['--version']
|
||||
)
|
||||
else:
|
||||
print('Skipping external program tests because of --force')
|
||||
|
||||
|
||||
if 'upload' in sys.argv[1:]:
|
||||
@@ -198,7 +201,7 @@ setup(
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Intended Audience :: Science/Research",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: Public Domain",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: POSIX :: BSD",
|
||||
@@ -227,7 +230,7 @@ setup(
|
||||
tests_require=tests_require,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'ocrmypdf = ocrmypdf.main:run_pipeline'
|
||||
'ocrmypdf = ocrmypdf.__main__:run_pipeline'
|
||||
],
|
||||
},
|
||||
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
||||
|
||||
+64
-25
@@ -9,21 +9,29 @@ Files derived from free sources
|
||||
These test resources come from free sources, under either public domain or Creative Commons licenses.
|
||||
In some cases they were converted from one image format to another without other changes.
|
||||
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| File | Source |
|
||||
+=====================+================================================================================+
|
||||
| c02-22.pdf | `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22 |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| congress.jpg | `US Congressional Records`_ (Public Domain) |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| graph.pdf | `Wikimedia: Pandas text analysis.png`_ (Public Domain) |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| lichtenstein.pdf | `Wikimedia: JPEG2000 Lichtenstein`_ (Creative Commons BY-SA 3.0) |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| LinnSequencer.jpg, | `Wikimedia: LinnSequencer`_ (Creative Commons Attribution-ShareAlike 3.0) |
|
||||
| linn.pdf, linn.txt | |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 50 30
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- Source
|
||||
- License
|
||||
* - c02-22.pdf
|
||||
- `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22
|
||||
- Public Domain
|
||||
* - congress.jpg
|
||||
- `US Congressional Records`_
|
||||
- Public Domain
|
||||
* - graph.pdf
|
||||
- `Wikimedia: Pandas text analysis.png`_
|
||||
- Public Domain
|
||||
* - lichtenstein.pdf
|
||||
- `Wikimedia: JPEG2000 Lichtenstein`_
|
||||
- Creative Commons BY-SA 3.0
|
||||
* - LinnSequencer.jpg, linn.pdf, linn.txt
|
||||
- `Wikimedia: LinnSequencer`_
|
||||
- Creative Commons BY-SA 3.0
|
||||
|
||||
|
||||
Files generated for this project
|
||||
================================
|
||||
@@ -31,29 +39,58 @@ Files generated for this project
|
||||
The following test resources were crafted specifically for this project, and can be used
|
||||
under the terms of the license in LICENSE.rst.
|
||||
|
||||
- aspect.pdf (a page with an image with 200 x 100 DPI resolution)
|
||||
- blank.pdf (a blank PDF page)
|
||||
- cmyk.pdf (a CMYK image created in Photoshop)
|
||||
- enormous.pdf (a very lage page)
|
||||
- francais.pdf (a page containing French accented characters)
|
||||
- hugemono.pdf (large monochrome JBIG2 page with pixel dimensions of 35000x35000)
|
||||
- invalid.pdf (a PDF file header followed by EOF marker)
|
||||
- missing_docinfo.pdf (PDF file with no /DocumentInfo section)
|
||||
.. list-table::
|
||||
:widths: 20 20 60
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- Contributor
|
||||
- Purpose
|
||||
* - aspect.pdf
|
||||
- @jbarlow83
|
||||
- test image with 200 x 100 DPI resolution
|
||||
* - blank.pdf
|
||||
- @jbarlow83
|
||||
- blank PDF
|
||||
* - cmyk.pdf
|
||||
- @jbarlow83
|
||||
- a CMYK image created in Photoshop
|
||||
* - enormous.pdf
|
||||
- @jbarlow83
|
||||
- very large PDF page
|
||||
* - epson.pdf
|
||||
- @lowesjam
|
||||
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
|
||||
* - francais.pdf
|
||||
- @jbarlow83
|
||||
- a page containing French accents (diacritics)
|
||||
* - hugemono.pdf
|
||||
- @jbarlow83
|
||||
- large monochrome 35000x35000 image in JBIG2 encoding
|
||||
* - invalid.pdf
|
||||
- @jbarlow83
|
||||
- a PDF file header followed by EOF marker
|
||||
* - masks.pdf
|
||||
- @supergrobi
|
||||
- file containing explicit masks and a stencil mask drawn without a proper transformation matrix; printout of a German Wikipedia article (CC BY-SA)
|
||||
* - missing_docinfo.pdf
|
||||
- @jbarlow83
|
||||
- PDF file with no /DocumentInfo section
|
||||
|
||||
Assemblies
|
||||
==========
|
||||
|
||||
These test resources are assemblies from other previously mentioned files, released under the same license terms as their input files.
|
||||
|
||||
- cardinal.pdf (four cardinal directions, rotated copies of LinnSequencer.jpg)
|
||||
- cardinal.pdf (four cardinal directions, baked-in rotated copies of LinnSequencer.jpg)
|
||||
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
||||
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
|
||||
- graph_ocred.pdf (from graph.pdf)
|
||||
- jbig2.pdf (congress.jpg, converted to JBIG2 encoding)
|
||||
- multipage.pdf (from several other files)
|
||||
- palette.pdf (congress.jpg, converted to a 256-color palette)
|
||||
- skew.pdf (from c02-22.pdf)
|
||||
- rotated_skew.pdf (a /Rotate'd and skewed document from LinnSequencer.jpg)
|
||||
- skew.pdf (from LinnSequencer.jpg, skew simulated by adjusting the transformation matrix)
|
||||
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2)
|
||||
|
||||
|
||||
@@ -66,3 +103,5 @@ These test resources are assemblies from other previously mentioned files, relea
|
||||
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
|
||||
|
||||
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
|
||||
|
||||
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
from subprocess import check_call
|
||||
|
||||
"""Replicate one type of Ghostscript feature elision warning during
|
||||
PDF/A creation."""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
|
||||
not permitted in PDF/A-2, overprint mode not set"""
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__filename__))
|
||||
sys.exit(0)
|
||||
|
||||
gs_args = ['gs'] + sys.argv[1:]
|
||||
check_call(gs_args)
|
||||
|
||||
if '-sDEVICE=pdfwrite' in sys.argv[1:]:
|
||||
print(elision_warning)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
"""Replicate Ghostscript PDF/A conversion failure by suppressing some
|
||||
arguments"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__filename__))
|
||||
sys.exit(0)
|
||||
|
||||
# Unless some argument is calling for PDFA generation, forward to
|
||||
# real ghostscript
|
||||
if not any(arg.startswith('-dPDFA') for arg in sys.argv):
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Remove the two arguments that tell ghostscript to create a PDF/A
|
||||
# Does not remove the Postscript definition file - not necessary
|
||||
# to cause PDF/A creation failure
|
||||
argv = []
|
||||
for arg in sys.argv:
|
||||
if arg.startswith('-dPDFA'):
|
||||
continue
|
||||
elif arg.startswith('-dPDFACompatibilityPolicy'):
|
||||
continue
|
||||
argv.append(arg)
|
||||
|
||||
real_ghostscript(argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -8,13 +8,7 @@ VERSION_STRING = '''tesseract 3.04.00
|
||||
SPOOFED: return error claiming image too big
|
||||
'''
|
||||
|
||||
"""Simulates a Tesseract crash
|
||||
|
||||
It isn't strictly necessary to crash the process and that has unwanted
|
||||
side effects like triggering core dumps or error reporting, logging and such.
|
||||
It's enough to dump some text to stderr and return an error code.
|
||||
|
||||
Follows the POSIX? convention of returning 128 + signal number.
|
||||
"""Simulates an error of Tesseract failing on attempts to process large images
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -6,6 +6,25 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
"""Cache output of tesseract to speed up test suite
|
||||
|
||||
The cache is keyed by a hash that includes the tesseract version, some of
|
||||
the command line, and the binary dump of the input file. The output file,
|
||||
stdout, and stderr are replicated on a cache hit.
|
||||
|
||||
Page orientation checks are also cached (-psm 0 stdout)
|
||||
|
||||
Errors and crashes are not cached.
|
||||
|
||||
Things not checked:
|
||||
-changes to tesseract installation that don't affect --version
|
||||
|
||||
Will fail on Tesseract 3.02.02 in "hocr" mode because it doesn't produce
|
||||
the incorrect file extension.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
CACHE_PATH = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', 'cache'))
|
||||
|
||||
@@ -29,11 +48,17 @@ def main():
|
||||
|
||||
m = hashlib.sha1()
|
||||
|
||||
version = subprocess.check_output(
|
||||
tess_version = subprocess.check_output(
|
||||
['tesseract', '--version'],
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
m.update(version)
|
||||
m.update(tess_version)
|
||||
|
||||
# Insert this source file into the hash function, to ensure that any
|
||||
# changes to this file invalidate previous hashes
|
||||
with open(__file__, 'rb') as f:
|
||||
m.update(f.read())
|
||||
|
||||
m.update(operation.encode())
|
||||
|
||||
try:
|
||||
@@ -74,10 +99,10 @@ def main():
|
||||
shutil.copy(cache_name, output_file)
|
||||
|
||||
# Replicate output
|
||||
with open(cache_name + '.stdout', 'r') as f:
|
||||
print(f.read(), end='')
|
||||
with open(cache_name + '.stderr', 'r') as f:
|
||||
print(f.read(), end='', file=sys.stderr)
|
||||
with open(cache_name + '.stdout', 'rb') as f:
|
||||
sys.stdout.buffer.write(f.read())
|
||||
with open(cache_name + '.stderr', 'rb') as f:
|
||||
sys.stderr.buffer.write(f.read())
|
||||
sys.exit(0)
|
||||
|
||||
# Cache miss
|
||||
@@ -86,23 +111,22 @@ def main():
|
||||
# Call tesseract
|
||||
p = subprocess.Popen(
|
||||
['tesseract'] + sys.argv[1:],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
# Do not cache errors or crashes
|
||||
print("Tesseract error", file=sys.stderr)
|
||||
print(stdout, end='')
|
||||
print(stderr, end='', file=sys.stderr)
|
||||
sys.stdout.buffer.write(stdout)
|
||||
sys.stderr.buffer.write(stderr)
|
||||
return p.returncode
|
||||
|
||||
with open(cache_name + '.stdout', 'w') as f:
|
||||
with open(cache_name + '.stdout', 'wb') as f:
|
||||
f.write(stdout)
|
||||
with open(cache_name + '.stderr', 'w') as f:
|
||||
with open(cache_name + '.stderr', 'wb') as f:
|
||||
f.write(stderr)
|
||||
print(stdout, end='')
|
||||
print(stderr, end='', file=sys.stderr)
|
||||
sys.stdout.buffer.write(stdout)
|
||||
sys.stderr.buffer.write(stderr)
|
||||
|
||||
# Insert file into cache
|
||||
if output_file != 'stdout':
|
||||
|
||||
@@ -10,13 +10,13 @@ VERSION_STRING = '''tesseract 3.04.00
|
||||
SPOOFED: CRASH ON OCR or -psm 0
|
||||
'''
|
||||
|
||||
"""Simulates a Tesseract crash
|
||||
"""Simulates a Tesseract crash when asked to run OCR
|
||||
|
||||
It isn't strictly necessary to crash the process and that has unwanted
|
||||
side effects like triggering core dumps or error reporting, logging and such.
|
||||
It's enough to dump some text to stderr and return an error code.
|
||||
|
||||
Follows the POSIX? convention of returning 128 + signal number.
|
||||
Follows the POSIX(?) convention of returning 128 + signal number.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -4,6 +4,18 @@ import img2pdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
"""Tesseract no-op spoof
|
||||
|
||||
To quickly run tests where getting OCR output is not necessary.
|
||||
|
||||
In 'hocr' mode, create a .hocr file that specifies no text found.
|
||||
|
||||
In 'pdf' mode, convert the image to PDF using another program.
|
||||
|
||||
In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
|
||||
+205
-54
@@ -2,7 +2,7 @@
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from __future__ import print_function
|
||||
from subprocess import Popen, PIPE, check_output, check_call
|
||||
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import suppress
|
||||
@@ -13,6 +13,7 @@ import PyPDF2 as pypdf
|
||||
from ocrmypdf import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
import platform
|
||||
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
@@ -22,11 +23,11 @@ if sys.version_info.major < 3:
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
|
||||
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
||||
OCRMYPDF = os.path.join(PROJECT_ROOT, 'OCRmyPDF.sh')
|
||||
TEST_RESOURCES = os.path.join(PROJECT_ROOT, 'tests', 'resources')
|
||||
TEST_OUTPUT = os.environ.get(
|
||||
'OCRMYPDF_TEST_OUTPUT',
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'main'))
|
||||
OCRMYPDF = [sys.executable, '-m', 'ocrmypdf']
|
||||
|
||||
|
||||
def running_in_docker():
|
||||
@@ -34,6 +35,10 @@ def running_in_docker():
|
||||
return os.path.exists('/.dockerinit')
|
||||
|
||||
|
||||
def is_linux():
|
||||
return platform.system() == 'Linux'
|
||||
|
||||
|
||||
def setup_module():
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(TEST_OUTPUT)
|
||||
@@ -41,15 +46,6 @@ def setup_module():
|
||||
os.makedirs(TEST_OUTPUT)
|
||||
|
||||
|
||||
def run_ocrmypdf_sh(input_file, output_file, *args, env=None):
|
||||
sh_args = ['sh', OCRMYPDF] + list(args) + [input_file, output_file]
|
||||
sh = Popen(
|
||||
sh_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True, env=env)
|
||||
out, err = sh.communicate()
|
||||
return sh, out, err
|
||||
|
||||
|
||||
def _infile(input_basename):
|
||||
return os.path.join(TEST_RESOURCES, input_basename)
|
||||
|
||||
@@ -59,29 +55,34 @@ def _outfile(output_basename):
|
||||
|
||||
|
||||
def check_ocrmypdf(input_basename, output_basename, *args, env=None):
|
||||
"Run ocrmypdf and confirmed that a valid file was created"
|
||||
input_file = _infile(input_basename)
|
||||
output_file = _outfile(output_basename)
|
||||
|
||||
sh, out, err = run_ocrmypdf_sh(input_file, output_file, *args, env=env)
|
||||
if sh.returncode != 0:
|
||||
p, out, err = run_ocrmypdf(input_basename, output_basename, *args, env=env)
|
||||
if p.returncode != 0:
|
||||
print('stdout\n======')
|
||||
print(out)
|
||||
print('stderr\n======')
|
||||
print(err)
|
||||
assert sh.returncode == 0
|
||||
assert p.returncode == 0
|
||||
assert os.path.exists(output_file), "Output file not created"
|
||||
assert os.stat(output_file).st_size > 100, "PDF too small or empty"
|
||||
assert out == "", \
|
||||
"The following was written to stdout and should not have been: \n" + \
|
||||
"<stdout>\n" + out + "\n</stdout>"
|
||||
return output_file
|
||||
|
||||
|
||||
def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
def run_ocrmypdf(input_basename, output_basename, *args, env=None):
|
||||
"Run ocrmypdf and let caller deal with results"
|
||||
input_file = _infile(input_basename)
|
||||
output_file = _outfile(output_basename)
|
||||
|
||||
if env is None:
|
||||
env = os.environ
|
||||
|
||||
p_args = ['ocrmypdf'] + list(args) + [input_file, output_file]
|
||||
p_args = OCRMYPDF + list(args) + [input_file, output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True, env=env)
|
||||
@@ -89,45 +90,60 @@ def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
return p, out, err
|
||||
|
||||
|
||||
def spoof(replace_program, with_spoof):
|
||||
def spoof(**kwargs):
|
||||
"""Modify environment variables to override subprocess executables
|
||||
|
||||
spoof(program1='replacement', ...)
|
||||
|
||||
Before running any executable, ocrmypdf checks the environment variable
|
||||
OCRMYPDF_PROGRAMNAME to override default program name/location, e.g.
|
||||
OCRMYPDF_GS redirects from the system path Ghostscript ("gs") to elsewhere.
|
||||
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
spoofer = os.path.join(SPOOF_PATH, with_spoof)
|
||||
if not os.access(spoofer, os.X_OK):
|
||||
os.chmod(spoofer, 0o755)
|
||||
env['OCRMYPDF_' + replace_program.upper()] = spoofer
|
||||
|
||||
for replace_program, with_spoof in kwargs.items():
|
||||
spoofer = os.path.join(SPOOF_PATH, with_spoof)
|
||||
if not os.access(spoofer, os.X_OK):
|
||||
os.chmod(spoofer, 0o755)
|
||||
env['OCRMYPDF_' + replace_program.upper()] = spoofer
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop():
|
||||
return spoof('tesseract', 'tesseract_noop.py')
|
||||
return spoof(tesseract='tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache():
|
||||
if running_in_docker():
|
||||
return os.environ.copy()
|
||||
return spoof('tesseract', "tesseract_cache.py")
|
||||
return spoof(tesseract="tesseract_cache.py")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_crash():
|
||||
return spoof('tesseract', 'tesseract_crash.py')
|
||||
return spoof(tesseract='tesseract_crash.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_big_image_error():
|
||||
return spoof('tesseract', 'tesseract_big_image_error.py')
|
||||
return spoof(tesseract='tesseract_big_image_error.py')
|
||||
|
||||
|
||||
def test_quick(spoof_tesseract_noop):
|
||||
check_ocrmypdf('c02-22.pdf', 'test_quick.pdf', env=spoof_tesseract_noop)
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_no_pdfa():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_pdfa_warning():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py')
|
||||
|
||||
|
||||
def test_quick(spoof_tesseract_cache):
|
||||
check_ocrmypdf('ccitt.pdf', 'test_quick.pdf', env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
def test_deskew(spoof_tesseract_noop):
|
||||
@@ -164,6 +180,38 @@ def test_clean(spoof_tesseract_noop):
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_remove_background(spoof_tesseract_noop):
|
||||
from PIL import Image
|
||||
|
||||
# Ensure the input image does not contain pure white/black
|
||||
im = Image.open(_infile('congress.jpg'))
|
||||
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
output_pdf = check_ocrmypdf(
|
||||
'congress.jpg', 'test_remove_bg.pdf', '--remove-background',
|
||||
'--image-dpi', '150',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
from ocrmypdf.ghostscript import rasterize_pdf
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
output_png = _outfile('remove_bg.png')
|
||||
|
||||
rasterize_pdf(
|
||||
output_pdf,
|
||||
output_png,
|
||||
xres=100,
|
||||
yres=100,
|
||||
raster_device='png16m',
|
||||
log=log)
|
||||
|
||||
|
||||
# The output image should contain pure white and black
|
||||
im = Image.open(output_png)
|
||||
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
|
||||
# This will run 5 * 2 * 2 = 20 test cases
|
||||
@pytest.mark.parametrize(
|
||||
"pdf",
|
||||
@@ -199,6 +247,9 @@ def test_preserve_metadata(spoof_tesseract_noop, output_type):
|
||||
assert pdfa_info['output'] == output_type
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_linux() and not running_in_docker(),
|
||||
reason="likely to fail if Linux locale is not configured correctly")
|
||||
@pytest.mark.parametrize("output_type", [
|
||||
'pdfa', 'pdf'
|
||||
])
|
||||
@@ -210,7 +261,7 @@ def test_override_metadata(spoof_tesseract_noop, output_type):
|
||||
chinese = '孔子'
|
||||
high_unicode = 'U+1030C is: 𐌌'
|
||||
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
input_file, output_file,
|
||||
'--title', german,
|
||||
'--author', chinese,
|
||||
@@ -255,8 +306,8 @@ def test_oversample(spoof_tesseract_cache, renderer):
|
||||
|
||||
|
||||
def test_repeat_ocr():
|
||||
sh, _, _ = run_ocrmypdf_sh('graph_ocred.pdf', 'wontwork.pdf')
|
||||
assert sh.returncode != 0
|
||||
p, _, _ = run_ocrmypdf('graph_ocred.pdf', 'wontwork.pdf')
|
||||
assert p.returncode != 0
|
||||
|
||||
|
||||
def test_force_ocr(spoof_tesseract_cache):
|
||||
@@ -403,6 +454,7 @@ def test_maximum_options(spoof_tesseract_cache, renderer, output_type):
|
||||
check_ocrmypdf(
|
||||
'multipage.pdf', 'test_multipage%s.pdf' % renderer,
|
||||
'-d', '-c', '-i', '-g', '-f', '-k', '--oversample', '300',
|
||||
'--remove-background',
|
||||
'--skip-big', '10', '--title', 'Too Many Weird Files',
|
||||
'--author', 'py.test', '--pdf-renderer', renderer,
|
||||
'--output-type', output_type,
|
||||
@@ -413,19 +465,19 @@ def test_tesseract_missing_tessdata():
|
||||
env = os.environ.copy()
|
||||
env['TESSDATA_PREFIX'] = '/tmp'
|
||||
|
||||
p, _, err = run_ocrmypdf_env(
|
||||
p, _, err = run_ocrmypdf(
|
||||
'graph_ocred.pdf', 'not_a_pdfa.pdf', '-v', '1', '--skip-text', env=env)
|
||||
assert p.returncode == ExitCode.missing_dependency, err
|
||||
|
||||
|
||||
def test_invalid_input_pdf():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'invalid.pdf', 'wont_be_created.pdf')
|
||||
assert p.returncode == ExitCode.input_file, err
|
||||
|
||||
|
||||
def test_blank_input_pdf():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'blank.pdf', 'still_blank.pdf')
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
@@ -434,7 +486,7 @@ def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash):
|
||||
# As a correctness test, make sure that --force-ocr on a PDF with no
|
||||
# content still triggers tesseract. If tesseract crashes, then it was
|
||||
# called.
|
||||
p, _, err = run_ocrmypdf_env(
|
||||
p, _, err = run_ocrmypdf(
|
||||
'blank.pdf', 'wont_be_created.pdf', '--force-ocr',
|
||||
env=spoof_tesseract_crash)
|
||||
assert p.returncode == ExitCode.child_process_error, err
|
||||
@@ -442,20 +494,20 @@ def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash):
|
||||
|
||||
|
||||
def test_french(spoof_tesseract_cache):
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'francais.pdf', 'francais.pdf', '-l', 'fra', env=spoof_tesseract_cache)
|
||||
assert p.returncode == ExitCode.ok, \
|
||||
"This test may fail if Tesseract language packs are missing"
|
||||
|
||||
|
||||
def test_klingon():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'francais.pdf', 'francais.pdf', '-l', 'klz')
|
||||
assert p.returncode == ExitCode.bad_args
|
||||
|
||||
|
||||
def test_missing_docinfo(spoof_tesseract_noop):
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'missing_docinfo.pdf', 'missing_docinfo.pdf', '-l', 'eng', '-c',
|
||||
env=spoof_tesseract_noop)
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
@@ -474,26 +526,26 @@ def test_uppercase_extension(spoof_tesseract_noop):
|
||||
|
||||
def test_input_file_not_found():
|
||||
input_file = "does not exist.pdf"
|
||||
sh, out, err = run_ocrmypdf_sh(
|
||||
p, out, err = run_ocrmypdf(
|
||||
_infile(input_file),
|
||||
_outfile("will not happen.pdf"))
|
||||
assert sh.returncode == ExitCode.input_file
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert (input_file in out or input_file in err)
|
||||
|
||||
|
||||
def test_input_file_not_a_pdf():
|
||||
input_file = __file__ # Try to OCR this file
|
||||
sh, out, err = run_ocrmypdf_sh(
|
||||
p, out, err = run_ocrmypdf(
|
||||
_infile(input_file),
|
||||
_outfile("will not happen.pdf"))
|
||||
assert sh.returncode == ExitCode.input_file
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert (input_file in out or input_file in err)
|
||||
|
||||
|
||||
def test_qpdf_repair_fails():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_QPDF'] = os.path.abspath('./spoof/qpdf_dummy_return2.py')
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'-v', '1',
|
||||
'c02-22.pdf', 'wont_be_created.pdf', env=env)
|
||||
print(out)
|
||||
@@ -502,7 +554,7 @@ def test_qpdf_repair_fails():
|
||||
|
||||
|
||||
def test_encrypted():
|
||||
p, out, err = run_ocrmypdf_env('skew-encrypted.pdf', 'wont_be_created.pdf')
|
||||
p, out, err = run_ocrmypdf('skew-encrypted.pdf', 'wont_be_created.pdf')
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert out.find('password')
|
||||
|
||||
@@ -524,19 +576,19 @@ def test_pagesegmode(renderer, spoof_tesseract_cache):
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash):
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'ccitt.pdf', 'wontwork.pdf', '-v', '1',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_crash)
|
||||
assert sh.returncode == ExitCode.child_process_error
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
|
||||
|
||||
def test_tesseract_crash_autorotate(spoof_tesseract_crash):
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'ccitt.pdf', 'wontwork.pdf',
|
||||
'-r', env=spoof_tesseract_crash)
|
||||
assert sh.returncode == ExitCode.child_process_error
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
print(out)
|
||||
@@ -556,26 +608,26 @@ def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
|
||||
def test_no_unpaper():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/no_unpaper_here.py')
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert sh.returncode == ExitCode.missing_dependency
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_old_unpaper():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/unpaper_oldversion.py')
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert sh.returncode == ExitCode.missing_dependency
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_algo4():
|
||||
sh, _, _ = run_ocrmypdf_env('encrypted_algo4.pdf', 'wontwork.pdf')
|
||||
assert sh.returncode == ExitCode.encrypted_pdf
|
||||
p, _, _ = run_ocrmypdf('encrypted_algo4.pdf', 'wontwork.pdf')
|
||||
assert p.returncode == ExitCode.encrypted_pdf
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr']) # tesseract cannot pass this test yet
|
||||
'hocr']) # tesseract cannot pass this test - resamples to square image
|
||||
def test_non_square_resolution(renderer, spoof_tesseract_cache):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = pdf_get_all_pageinfo(_infile('aspect.pdf'))
|
||||
@@ -597,3 +649,102 @@ def test_image_to_pdf(spoof_tesseract_noop):
|
||||
check_ocrmypdf(
|
||||
'LinnSequencer.jpg', 'image_to_pdf.pdf', '--image-dpi', '200',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_jbig2_passthrough(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf(
|
||||
'jbig2.pdf', 'jbig2_out.pdf',
|
||||
'--output-type', 'pdf',
|
||||
'--pdf-renderer', 'hocr',
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(out)
|
||||
assert out_pageinfo[0]['images'][0]['enc'] == 'jbig2'
|
||||
|
||||
|
||||
def test_stdin(spoof_tesseract_noop):
|
||||
input_file = _infile('francais.pdf')
|
||||
output_file = _outfile('test_stdin.pdf')
|
||||
|
||||
# Runs: ocrmypdf - output.pdf < testfile.pdf
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
p_args = OCRMYPDF + ['-', output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
stdin=input_stream, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
|
||||
def test_stdout(spoof_tesseract_noop):
|
||||
input_file = _infile('francais.pdf')
|
||||
output_file = _outfile('test_stdout.pdf')
|
||||
|
||||
# Runs: ocrmypdf francais.pdf - > test_stdout.pdf
|
||||
with open(output_file, 'wb') as output_stream:
|
||||
p_args = OCRMYPDF + [input_file, '-']
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=output_stream, stderr=PIPE,
|
||||
stdin=DEVNULL, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
from ocrmypdf import qpdf
|
||||
assert qpdf.check(output_file, log=None)
|
||||
|
||||
|
||||
def test_masks(spoof_tesseract_noop):
|
||||
check_ocrmypdf('masks.pdf', 'test_masks.pdf', env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop):
|
||||
check_ocrmypdf(
|
||||
'epson.pdf', 'test_epson.pdf',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_rotated_skew_timeout():
|
||||
"""This document contains an image that is rotated 90 into place with a
|
||||
/Rotate tag and intentionally skewed by altering the transformation matrix.
|
||||
|
||||
This tests for a bug where the combinatino of preprocessing and a tesseract
|
||||
timeout produced a page whose dimensions did not match the original's.
|
||||
"""
|
||||
|
||||
input_file = _infile('rotated_skew.pdf')
|
||||
in_pageinfo = pdf_get_all_pageinfo(input_file)[0]
|
||||
|
||||
assert in_pageinfo['height_pixels'] < in_pageinfo['width_pixels'], \
|
||||
"Expected the input page to be landscape"
|
||||
assert in_pageinfo['rotate'] == 90, "Expected a rotated page"
|
||||
|
||||
out = check_ocrmypdf(
|
||||
'rotated_skew.pdf', 'test_rotated_skew.pdf',
|
||||
'--pdf-renderer', 'hocr',
|
||||
'--deskew', '--tesseract-timeout', '0')
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(out)[0]
|
||||
|
||||
assert out_pageinfo['height_pixels'] > out_pageinfo['width_pixels'], \
|
||||
"Expected the output page to be portrait"
|
||||
|
||||
assert out_pageinfo['rotate'] == 0, \
|
||||
"Expected no page rotation for output"
|
||||
|
||||
assert in_pageinfo['width_pixels'] == out_pageinfo['height_pixels'] and \
|
||||
in_pageinfo['height_pixels'] == out_pageinfo['width_pixels'], \
|
||||
"Expected page rotation to be baked in"
|
||||
|
||||
|
||||
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa):
|
||||
p, out, err = run_ocrmypdf(
|
||||
'ccitt.pdf', 'test_pdfa_failure.pdf',
|
||||
env=spoof_no_tess_no_pdfa)
|
||||
assert p.returncode == 4, "Expected return code 4 when PDF/A fails"
|
||||
|
||||
|
||||
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning):
|
||||
check_ocrmypdf('ccitt.pdf', 'test_feature_elision.pdf',
|
||||
env=spoof_no_tess_pdfa_warning)
|
||||
|
||||
@@ -97,11 +97,6 @@ def test_single_page_image():
|
||||
assert pdfimage['width'] == 8
|
||||
assert pdfimage['color'] == 'gray'
|
||||
|
||||
# While unexpected, this is correct
|
||||
# PDF spec says /FlateDecode image must have /BitsPerComponent 8
|
||||
# So mono images get upgraded to 8-bit
|
||||
assert pdfimage['bpc'] == 8
|
||||
|
||||
# DPI in a 1"x1" is the image width
|
||||
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
|
||||
|
||||
Reference in New Issue
Block a user