Compare commits
169
Commits
v3.2.post2
...
v4.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c96823a648 | ||
|
|
3807b7d655 | ||
|
|
a45505cf1d | ||
|
|
b4a734fc0d | ||
|
|
bbd02926e1 | ||
|
|
5022ded276 | ||
|
|
c7612152ef | ||
|
|
af91642cd1 | ||
|
|
9c66334c38 | ||
|
|
8d79b94b84 | ||
|
|
d7f60b96c1 | ||
|
|
b964999427 | ||
|
|
3473345ea6 | ||
|
|
349ec5c81f | ||
|
|
ff78d7c56c | ||
|
|
ff092c8629 | ||
|
|
fe14cb57c0 | ||
|
|
507fbc01d5 | ||
|
|
325479e5be | ||
|
|
e926ecb8b2 | ||
|
|
d0cb6c0e92 | ||
|
|
5b7c8cf5d3 | ||
|
|
40baab32ac | ||
|
|
e877d37ac8 | ||
|
|
5a9f77e438 | ||
|
|
8ddd67d1e2 | ||
|
|
1605408c23 | ||
|
|
2d3b1ebf6e | ||
|
|
c74eaab7f5 | ||
|
|
c21d231388 | ||
|
|
a73afc4e76 | ||
|
|
76c364150d | ||
|
|
94a3e447cc | ||
|
|
12868b461a | ||
|
|
322085933b | ||
|
|
3fed94bb79 | ||
|
|
8c877482bd | ||
|
|
b17d589e84 | ||
|
|
368252a243 | ||
|
|
ccefda1bee | ||
|
|
3d0e8c9629 | ||
|
|
313bbbb94c | ||
|
|
0360f078de | ||
|
|
c8901666c4 | ||
|
|
7430006596 | ||
|
|
f3e06b2dbd | ||
|
|
e97df307ff | ||
|
|
1443354aa2 | ||
|
|
250e68c1cd | ||
|
|
6a380ee99c | ||
|
|
3c90bd96a9 | ||
|
|
06a7ceb25a | ||
|
|
733a8e7d58 | ||
|
|
570bbe9a05 | ||
|
|
5cc3adb39a | ||
|
|
3957a0606c | ||
|
|
11a561dbce | ||
|
|
dad2198394 | ||
|
|
e40fdc502d | ||
|
|
d446fe5922 | ||
|
|
4ca90c106d | ||
|
|
7c5e58a497 | ||
|
|
323b9a5f8e | ||
|
|
cab381a339 | ||
|
|
fe4d4c39cd | ||
|
|
ad188d7ae1 | ||
|
|
8246cc0538 | ||
|
|
6f3ac46b1c | ||
|
|
ac71c3be63 | ||
|
|
ecc0ac9b19 | ||
|
|
ea4e6bf67d | ||
|
|
46c204f533 | ||
|
|
71fbda8bf6 | ||
|
|
9b79b4a7c8 | ||
|
|
c04cc853d7 | ||
|
|
dd41e70ccc | ||
|
|
4206e74f42 | ||
|
|
68c3ce56a9 | ||
|
|
ab0e5fa425 | ||
|
|
f3b0434a87 | ||
|
|
aa394440db | ||
|
|
3b98a1a04b | ||
|
|
fcb89b0c58 | ||
|
|
ac65d6a03a | ||
|
|
2103f60906 | ||
|
|
e3c3d848c1 | ||
|
|
d4ef3411e0 | ||
|
|
71d616e413 | ||
|
|
fe651d1bf5 | ||
|
|
582ba8cfad | ||
|
|
d23291650a | ||
|
|
812fd745b6 | ||
|
|
a87aa71d85 | ||
|
|
60b2eb1455 | ||
|
|
ab3c1988c1 | ||
|
|
ee5223eea8 | ||
|
|
edd2185268 | ||
|
|
35b1ca2be2 | ||
|
|
71e493a810 | ||
|
|
6178e22e7f | ||
|
|
ef0aab060a | ||
|
|
d70ce61cfd | ||
|
|
8cd84afac8 | ||
|
|
ec2c6c312b | ||
|
|
3946bba318 | ||
|
|
2ed0b78a7b | ||
|
|
ed346d032c | ||
|
|
acd645f192 | ||
|
|
88433e4c34 | ||
|
|
1224af1780 | ||
|
|
ab13342931 | ||
|
|
d7913da484 | ||
|
|
c50e3f1329 | ||
|
|
a62f86dbd7 | ||
|
|
33b88b18db | ||
|
|
7c691c21ab | ||
|
|
4ec51729d8 | ||
|
|
07b41e479a | ||
|
|
6510bcad19 | ||
|
|
265d2ce39b | ||
|
|
1928a64cae | ||
|
|
11e575a5a3 | ||
|
|
7fbc0d6460 | ||
|
|
1ba8b1aa4b | ||
|
|
3569c76c0f | ||
|
|
16c7ac2582 | ||
|
|
4ceb59215f | ||
|
|
2e6879ee51 | ||
|
|
66fc2e9d7d | ||
|
|
2c7a6e574f | ||
|
|
78c3bf5dba | ||
|
|
98c115e3bb | ||
|
|
2752bda80b | ||
|
|
7c0940609a | ||
|
|
d30a879e2d | ||
|
|
b907234d5c | ||
|
|
b0114c9174 | ||
|
|
d2ba8c501f | ||
|
|
6a7ed7d359 | ||
|
|
6289afa1a6 | ||
|
|
9bb6fa04cb | ||
|
|
afb6f6f5c9 | ||
|
|
8a69671dbd | ||
|
|
178aee4687 | ||
|
|
8484caddfb | ||
|
|
08313316de | ||
|
|
1d0eca5c63 | ||
|
|
fe89232a30 | ||
|
|
4b51b521e2 | ||
|
|
e9ec458304 | ||
|
|
54b0ddd787 | ||
|
|
93bec22f9c | ||
|
|
0dc96442d8 | ||
|
|
58f4582517 | ||
|
|
2d15c09cca | ||
|
|
04cb8865b0 | ||
|
|
6fe32bbaf7 | ||
|
|
4abb20390d | ||
|
|
daa3916430 | ||
|
|
e9b87cefcc | ||
|
|
60593b5ad3 | ||
|
|
f708b11ea4 | ||
|
|
cb3ba8e973 | ||
|
|
ec3d92ad8e | ||
|
|
66a095d7de | ||
|
|
411981efbc | ||
|
|
350ad5210e | ||
|
|
f3b588764e | ||
|
|
b49f5a7d77 |
@@ -0,0 +1 @@
|
||||
ref-names: $Format:%D$
|
||||
+3
-1
@@ -5,4 +5,6 @@
|
||||
# (binary is a macro for -text -diff)
|
||||
*.jar binary
|
||||
*.pdf binary
|
||||
*.PDF binary
|
||||
*.PDF binary
|
||||
|
||||
.git_archival.txt export-subst
|
||||
|
||||
+2
-3
@@ -1,9 +1,7 @@
|
||||
# Development environment
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
venv-3.4/
|
||||
venv-3.5/
|
||||
venv/
|
||||
venv-*/
|
||||
pyvenv.cfg
|
||||
|
||||
# Package building
|
||||
@@ -12,6 +10,7 @@ pyvenv.cfg
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
wheelhouse/
|
||||
|
||||
# Automatically generated files
|
||||
ocrmypdf/lib/_*.py
|
||||
|
||||
+11
-9
@@ -1,15 +1,19 @@
|
||||
language: python
|
||||
|
||||
cache: pip
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- $HOME/.ccache
|
||||
- tarballs
|
||||
- packages
|
||||
- tests/cache
|
||||
|
||||
python:
|
||||
- 3.4
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
before_install:
|
||||
# Ubuntu packages
|
||||
- sudo add-apt-repository ppa:evl.ms/precise -y # for Ghostscript 9.15
|
||||
@@ -17,17 +21,15 @@ before_install:
|
||||
- sudo add-apt-repository ppa:b-eltzner/qpdfview-exp -y # for QPDF 5
|
||||
- sudo add-apt-repository ppa:itachi-san/ffmpeg -y # for libav 11.2 (for unpaper)
|
||||
- sudo apt-get update -qq # must go after all add-apt-repo
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils gcc libavformat-dev libavcodec-dev libavutil-dev automake make pkg-config xsltproc
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
|
||||
|
||||
# pip
|
||||
- pip install --upgrade pip
|
||||
|
||||
# Download, make and install unpaper (using ccache)
|
||||
- mkdir -p tarballs
|
||||
- "[ -f tarballs/unpaper-6.1.tar.xz ] || wget -q https://www.flameeyes.eu/files/unpaper-6.1.tar.xz -O tarballs/unpaper-6.1.tar.xz"
|
||||
- tar -xvf tarballs/unpaper-6.1.tar.xz
|
||||
- export PATH="/usr/lib/ccache:$PATH"
|
||||
- pushd unpaper-6.1 && ./configure --prefix=/usr && make -j && sudo make install && popd
|
||||
- mkdir -p packages
|
||||
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
|
||||
- sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
@@ -36,7 +38,7 @@ install:
|
||||
script:
|
||||
- python setup.py clean
|
||||
- python setup.py install
|
||||
- py.test
|
||||
- python setup.py test
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
+34
-10
@@ -1,6 +1,6 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
# VERSION 3.0.2
|
||||
# VERSION 3.2
|
||||
FROM debian:stretch
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
@@ -23,14 +23,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
unpaper \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
poppler-utils
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
|
||||
|
||||
RUN apt-get install -qy --no-install-recommends \
|
||||
libffi-dev \
|
||||
libpython3-dev \
|
||||
gcc
|
||||
|
||||
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
|
||||
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
|
||||
|
||||
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghostscript/sid
|
||||
|
||||
|
||||
# Enforce UTF-8
|
||||
# Borrowed from https://index.docker.io/u/crosbymichael/python/
|
||||
RUN dpkg-reconfigure locales && \
|
||||
@@ -38,9 +48,6 @@ RUN dpkg-reconfigure locales && \
|
||||
/usr/sbin/update-locale LANG=C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
# Remove the junk
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
|
||||
|
||||
# Set up a Python virtualenv and take all of the system packages, so we can
|
||||
# rely on the platform packages rather than importing GCC and compiling them
|
||||
@@ -49,18 +56,35 @@ RUN pyvenv /appenv \
|
||||
|
||||
COPY . /application/
|
||||
|
||||
# Replace stock Tesseract 3.04.00 font with improved sharp2.ttf that resolves
|
||||
# issues in many PDF viewers.
|
||||
# Discussion is in https://github.com/tesseract-ocr/tesseract/issues/182
|
||||
COPY ./share/sharp2.ttf /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
RUN chmod 644 /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
|
||||
# Set this here to force a docker version, allowing non-tagged versions to
|
||||
# be built
|
||||
# ENV SETUPTOOLS_SCM_PRETEND_VERSION=v3.3.0
|
||||
|
||||
# Install application and dependencies
|
||||
# In this arrangement Pillow and reportlab will be provided by the system
|
||||
# Even though ocrmypdf is locally present, pull from PyPI because
|
||||
# Dockerhub and setuptools_scm clash
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install --no-cache-dir /application \
|
||||
&& pip install ocrmypdf \
|
||||
&& pip install --no-cache-dir -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk
|
||||
RUN apt-get remove -qy gcc
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
|
||||
|
||||
USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
ENV OCRMYPDF_TEST_OUTPUT=/tmp/test-output
|
||||
ENV OCRMYPDF_IN_DOCKER=1
|
||||
ENV OCRMYPDF_SHARP_TTF=1
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# OCRmyPDF polyglot
|
||||
#
|
||||
# VERSION 3.0.2
|
||||
# VERSION 3.2
|
||||
FROM jbarlow83/ocrmypdf:latest
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
recursive-exclude tests/output *
|
||||
include requirements.txt
|
||||
include test_requirements.txt
|
||||
include test_requirements.txt
|
||||
include share/*
|
||||
+52
-32
@@ -48,22 +48,28 @@ Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
|
||||
|
||||
You can install it to a Python virtual environment or system-wide.
|
||||
|
||||
Installing the Docker container
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Debian and Ubuntu
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
For many users, installing the Docker container will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
``apt-get install ocrmypdf``.
|
||||
|
||||
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 container of the latest release.
|
||||
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::
|
||||
|
||||
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 container host, and then follow these optional steps to enable multiple CPUs::
|
||||
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::
|
||||
|
||||
# Optional
|
||||
# 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"
|
||||
@@ -77,8 +83,14 @@ the image::
|
||||
Then tag it to give a more convenient name, just ocrmypdf::
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
|
||||
You can then run using the command::
|
||||
|
||||
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>`__::
|
||||
|
||||
# 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::
|
||||
|
||||
docker run ocrmypdf --help
|
||||
|
||||
@@ -90,7 +102,8 @@ In this worked example, the current working directory contains an input file cal
|
||||
|
||||
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`` container name and all arguments to ``ocrmypdf`` should be listed after.
|
||||
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
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -109,21 +122,15 @@ Install or upgrade the required Homebrew packages, if any are missing::
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2
|
||||
brew install leptonica
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
brew install tesseract
|
||||
|
||||
It is also recommended that install Pillow and confirm it can read and write JPEG and PNG files::
|
||||
Update the homebrew pip and install Pillow::
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
Sometimes, the Python imaging library (Pillow) can end up being compiled and installed without support for JPEG and PNG files. (Arguably, this is an unfixed bug in Pillow's installer.) To confirm that Pillow is compiled correctly and can access JPEG and PNG files, try this command::
|
||||
|
||||
python3 -c "from PIL import Image; im = Image.new('1', (1, 1)); im.save('test.png'); im.save('test.jpg')"
|
||||
|
||||
If you have trouble getting Pillow to access JPEG and PNG files, `review the installation instructions <https://pillow.readthedocs.org/installation.html>`__.
|
||||
|
||||
You can then install OCRmyPDF from PyPI::
|
||||
|
||||
pip3 install ocrmypdf
|
||||
@@ -147,6 +154,7 @@ Install system dependencies::
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
@@ -179,7 +187,13 @@ Ubuntu 14.04 only installs ``unpaper`` version 0.4.2, which is not supported by
|
||||
Installing on Windows
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Direct installation on Windows is not possible. Install the Docker container as described above.
|
||||
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.
|
||||
|
||||
The command line syntax to run ocrmypdf from a command prompt will resemble::
|
||||
|
||||
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
|
||||
@@ -188,24 +202,18 @@ 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.
|
||||
|
||||
First, clone the HEAD revision::
|
||||
To install the HEAD revision from sources in the current Python 3 environment::
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
cd OCRmyPDF
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
To install the HEAD revision from sources::
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__, allowing customization of OCRmyPDF, use the ``-e`` flag::
|
||||
|
||||
pip3 install .
|
||||
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
|
||||
allowing customization of OCRmyPDF, use the ``-e`` flag::
|
||||
|
||||
pip3 install -e .
|
||||
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::
|
||||
|
||||
sudo pip3 install [-e] .
|
||||
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::
|
||||
@@ -228,6 +236,18 @@ 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.
|
||||
|
||||
Languages
|
||||
---------
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
|
||||
you can often find packages that provide language packs::
|
||||
|
||||
# Debian/Ubuntu users
|
||||
sudo apt-get install tesseract-ocr-chi-sim
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
@@ -245,11 +265,11 @@ In case you detect an issue, please:
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
- `c't 1-2014, page 59 <http://www.heise.de/ct/inhalt/2014/1/58/>`__:
|
||||
- `c't 1-2014, page 59 <http://heise.de/-2279695>`__:
|
||||
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
|
||||
magazine c't
|
||||
- `heise Open Source, 09/2014: Texterkennung mit
|
||||
OCRmyPDF <http://www.heise.de/-2356670>`__
|
||||
OCRmyPDF <http://heise.de/-2356670>`__
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
+139
-1
@@ -6,8 +6,146 @@ Please always read this file before installing the package
|
||||
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
|
||||
|
||||
|
||||
v4.1.3:
|
||||
=======
|
||||
|
||||
- More helpful error message for PDFs with version 4 security handler
|
||||
- Update usage instructions for Windows/Docker users
|
||||
- Fix order of operations for matrix multiplication (no effect on most users)
|
||||
- Add a few leptonica wrapper functions (no effect on most users)
|
||||
|
||||
|
||||
v4.1.2:
|
||||
=======
|
||||
|
||||
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
|
||||
- More helpful error message for an error related to certain types of malformed PDFs
|
||||
|
||||
|
||||
v4.1:
|
||||
=====
|
||||
|
||||
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
|
||||
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
|
||||
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
|
||||
|
||||
|
||||
v4.0.7:
|
||||
=======
|
||||
|
||||
- Minor correction to Ghostscript output settings
|
||||
|
||||
|
||||
v4.0.6:
|
||||
=======
|
||||
|
||||
- Update install instructions
|
||||
- Provide a sRGB profile instead of using Ghostscript's
|
||||
|
||||
|
||||
v4.0.5:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Remove some verbose debug messages from v4.0.4
|
||||
- Fixed temporary that wasn't being deleted
|
||||
- DPI is now calculated correctly for cropped images, along with other image transformations
|
||||
- Inline images are now checked during DPI calculation instead of rejecting the image
|
||||
|
||||
v4.0.4:
|
||||
=======
|
||||
|
||||
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
|
||||
|
||||
|
||||
v4.0.3:
|
||||
=======
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Page orientations detected are now reported in a summary comment
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Show stack trace if unexpect errors occur
|
||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||
abort the file
|
||||
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
||||
|
||||
|
||||
v4.0.2:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
- Improved handling of Tesseract errors and crashes
|
||||
- Fixed use of chmod on Docker that broke most test cases
|
||||
|
||||
|
||||
v4.0.1:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
|
||||
v4.0:
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
|
||||
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
|
||||
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
|
||||
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
|
||||
at image deskewing than unpaper.
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
|
||||
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
|
||||
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
|
||||
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Logging output is now much easier to read
|
||||
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
|
||||
- libffi is now required
|
||||
- Some changes were made to the Docker and Travis build environments to support libffi
|
||||
- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01,
|
||||
the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00.
|
||||
You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder
|
||||
to correct the problem.
|
||||
|
||||
|
||||
v3.2.1:
|
||||
=======
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
|
||||
- Tweaked the Dockerfiles
|
||||
|
||||
|
||||
v3.2:
|
||||
=========
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
@@ -10,9 +10,15 @@ class ExitCode(IntEnum):
|
||||
invalid_output_pdfa = 4
|
||||
file_access_error = 5
|
||||
already_done_ocr = 6
|
||||
child_process_error = 7
|
||||
encrypted_pdf = 8
|
||||
other_error = 15
|
||||
|
||||
|
||||
def get_program(name):
|
||||
envvar = 'OCRMYPDF_' + name.upper()
|
||||
return os.environ.get(envvar, name)
|
||||
|
||||
|
||||
def page_number(input_file):
|
||||
return int(os.path.basename(input_file)[0:6])
|
||||
|
||||
Binary file not shown.
@@ -5,9 +5,11 @@ from tempfile import NamedTemporaryFile
|
||||
from subprocess import Popen, PIPE, check_call
|
||||
from shutil import copy
|
||||
from . import get_program
|
||||
from .pdfa import SRGB_ICC_PROFILE
|
||||
|
||||
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log):
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
pageno=1):
|
||||
with NamedTemporaryFile(delete=True) as tmp:
|
||||
args_gs = [
|
||||
get_program('gs'),
|
||||
@@ -15,6 +17,8 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log):
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=%s' % raster_device,
|
||||
'-dFirstPage=%i' % pageno,
|
||||
'-dLastPage=%i' % pageno,
|
||||
'-o', tmp.name,
|
||||
'-r{0}x{1}'.format(str(xres), str(yres)),
|
||||
input_file
|
||||
@@ -43,12 +47,12 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
|
||||
"-dNOPAUSE",
|
||||
'-dNumRenderingThreads=' + str(threads),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=/RGB",
|
||||
"-sProcessColorModel=DeviceRGB",
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-sPDFACompatibilityPolicy=2",
|
||||
"-sOutputICCProfile=srgb.icc",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
|
||||
+347
-168
@@ -10,11 +10,16 @@
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
import argparse
|
||||
import ctypes as C
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from tempfile import TemporaryFile
|
||||
from ctypes.util import find_library
|
||||
from .lib._leptonica import ffi
|
||||
from functools import lru_cache
|
||||
from enum import Enum
|
||||
|
||||
lept = ffi.dlopen(find_library('lept'))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,67 +30,6 @@ def stderr(*objs):
|
||||
print("leptonica.py:", *objs, file=sys.stderr)
|
||||
|
||||
|
||||
from ctypes.util import find_library
|
||||
lept_lib = find_library('lept')
|
||||
if not lept_lib:
|
||||
stderr("Could not find the Leptonica library")
|
||||
sys.exit(3)
|
||||
try:
|
||||
lept = C.cdll.LoadLibrary(lept_lib)
|
||||
except Exception:
|
||||
stderr("Could not load the Leptonica library from %s", lept_lib)
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
class _PIXCOLORMAP(C.Structure):
|
||||
"""struct PixColormap from Leptonica src/pix.h
|
||||
"""
|
||||
|
||||
_fields_ = [
|
||||
("array", C.c_void_p),
|
||||
("depth", C.c_int32),
|
||||
("nalloc", C.c_int32),
|
||||
("n", C.c_int32)
|
||||
]
|
||||
|
||||
|
||||
class _PIX(C.Structure):
|
||||
"""struct Pix from Leptonica src/pix.h
|
||||
"""
|
||||
|
||||
_fields_ = [
|
||||
("w", C.c_uint32),
|
||||
("h", C.c_uint32),
|
||||
("d", C.c_uint32),
|
||||
("wpl", C.c_uint32),
|
||||
("refcount", C.c_uint32),
|
||||
("xres", C.c_int32),
|
||||
("yres", C.c_int32),
|
||||
("informat", C.c_int32),
|
||||
("text", C.POINTER(C.c_char)),
|
||||
("colormap", C.POINTER(_PIXCOLORMAP)),
|
||||
("data", C.POINTER(C.c_uint32))
|
||||
]
|
||||
|
||||
|
||||
PIX = C.POINTER(_PIX)
|
||||
|
||||
lept.pixRead.argtypes = [C.c_char_p]
|
||||
lept.pixRead.restype = PIX
|
||||
lept.pixScale.argtypes = [PIX, C.c_float, C.c_float]
|
||||
lept.pixScale.restype = PIX
|
||||
lept.pixDeskew.argtypes = [PIX, C.c_int32]
|
||||
lept.pixDeskew.restype = PIX
|
||||
lept.pixFindSkew.argtypes = [PIX, C.POINTER(C.c_float), C.POINTER(C.c_float)]
|
||||
lept.pixFindSkew.restype = C.c_int32
|
||||
lept.pixWriteImpliedFormat.argtypes = [C.c_char_p, PIX, C.c_int32, C.c_int32]
|
||||
lept.pixWriteImpliedFormat.restype = C.c_int32
|
||||
lept.pixDestroy.argtypes = [C.POINTER(PIX)]
|
||||
lept.pixDestroy.restype = None
|
||||
lept.getLeptonicaVersion.argtypes = []
|
||||
lept.getLeptonicaVersion.restype = C.c_char_p
|
||||
|
||||
|
||||
class LeptonicaErrorTrap(object):
|
||||
"""Context manager to trap errors reported by Leptonica.
|
||||
|
||||
@@ -140,103 +84,368 @@ class LeptonicaIOError(LeptonicaError):
|
||||
pass
|
||||
|
||||
|
||||
def pixRead(filename):
|
||||
"""Load an image file into a PIX object.
|
||||
class RemoveColormap(Enum):
|
||||
to_binary = 0
|
||||
to_grayscale = 1
|
||||
to_full_color = 2
|
||||
based_on_src = 3
|
||||
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If loading
|
||||
fails then the object will wrap a C null pointer.
|
||||
|
||||
class Pix:
|
||||
"""Wrapper around leptonica's PIX object.
|
||||
|
||||
Leptonica uses referencing counting on PIX objects. Also, many Leptonica
|
||||
functions return the original object with an increased reference count
|
||||
if the operation had no effect (for example, image skew was found to be 0).
|
||||
This has complications for memory management in Python. Whenever Leptonica
|
||||
returns a PIX object (new or old), we wrap it in this class, which
|
||||
registers it with the FFI garbage collector. pixDestroy() decrements the
|
||||
reference count and only destroys when the last reference is removed.
|
||||
|
||||
Leptonica's reference counting is not threadsafe. This class can be used
|
||||
in a threadsafe manner if a Python threading.Lock protects the data.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return lept.pixRead(filename.encode(sys.getfilesystemencoding()))
|
||||
|
||||
def __init__(self, pix):
|
||||
self._pix = ffi.gc(pix, Pix._pix_destroy)
|
||||
|
||||
def pixScale(pix, scalex, scaley):
|
||||
"""Returns the pix object rescaled according to the proportions given."""
|
||||
with LeptonicaErrorTrap():
|
||||
return lept.pixScale(pix, scalex, scaley)
|
||||
|
||||
|
||||
def pixDeskew(pix, reduction_factor=0):
|
||||
"""Returns the deskewed pix object.
|
||||
|
||||
A clone of the original is returned when the algorithm cannot find a skew
|
||||
angle with sufficient confidence.
|
||||
|
||||
reduction_factor -- amount to downsample (0 for default) when searching
|
||||
for skew angle
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return lept.pixDeskew(pix, reduction_factor)
|
||||
|
||||
|
||||
def pixFindSkew(pix):
|
||||
"""Returns a tuple (deskew angle in degrees, confidence value).
|
||||
|
||||
Returns (None, None) if no angle is available.
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
angle = C.c_float(0.0)
|
||||
confidence = C.c_float(0.0)
|
||||
result = lept.pixFindSkew(pix, C.byref(angle), C.byref(confidence))
|
||||
if result == 0:
|
||||
return (angle.value, confidence.value)
|
||||
def __repr__(self):
|
||||
if self._pix:
|
||||
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
|
||||
return s.format(self._pix.w, self._pix.h, self._pix.d,
|
||||
int(ffi.cast("intptr_t", self._pix)))
|
||||
else:
|
||||
return (None, None)
|
||||
return "<leptonica.Pix image NULL>"
|
||||
|
||||
def __getstate__(self):
|
||||
data = ffi.new('l_uint32 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixSerializeToMemory(self._pix, data, size)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixSerializeToMemory")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
|
||||
# Copy from C bytes to python bytes()
|
||||
data_bytes = ffi.buffer(char_data, size[0])[:]
|
||||
|
||||
# Can now free C bytes
|
||||
lept.lept_free(char_data)
|
||||
return dict(data=data_bytes)
|
||||
|
||||
def __setstate__(self, state):
|
||||
cdata_bytes = ffi.new('char[]', state['data'])
|
||||
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
|
||||
|
||||
pix = lept.pixDeserializeFromMemory(
|
||||
cdata_uint32, len(state['data']))
|
||||
Pix.__init__(self, pix)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._pix.w
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._pix.h
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._pix.d
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return (self._pix.w, self._pix.h)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return {'dpi': (self._pix.xres, self._pix.yres)}
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"Return mode like PIL.Image"
|
||||
if self.depth == 1:
|
||||
return '1'
|
||||
elif self.depth >= 16:
|
||||
return 'RGB'
|
||||
elif not self._pix.colormap:
|
||||
return 'L'
|
||||
else:
|
||||
return 'P'
|
||||
|
||||
@classmethod
|
||||
def read(cls, filename):
|
||||
"""Load an image file into a PIX object.
|
||||
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
|
||||
loading fails then the object will wrap a C null pointer.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return cls(lept.pixRead(
|
||||
filename.encode(sys.getfilesystemencoding())))
|
||||
|
||||
def write_implied_format(
|
||||
self, filename, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
self._pix, jpeg_quality, jpeg_progressive)
|
||||
|
||||
def topil(self):
|
||||
"Returns a PIL.Image version of this Pix"
|
||||
from PIL import Image
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
pix_swapped = Pix(lept.pixEndianByteSwapNew(self._pix))
|
||||
|
||||
size = (pix_swapped._pix.wpl * 4, pix_swapped._pix.h)
|
||||
buf = ffi.buffer(pix_swapped._pix.data, size[0] * size[1])
|
||||
|
||||
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)
|
||||
|
||||
return im
|
||||
|
||||
def show(self):
|
||||
return self.topil().show()
|
||||
|
||||
def deskew(self, reduction_factor=0):
|
||||
"""Returns the deskewed pix object.
|
||||
|
||||
A clone of the original is returned when the algorithm cannot find a
|
||||
skew angle with sufficient confidence.
|
||||
|
||||
reduction_factor -- amount to downsample (0 for default) when searching
|
||||
for skew angle
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixDeskew(self._pix, reduction_factor))
|
||||
|
||||
def scale(self, scalex, scaley):
|
||||
"Returns the pix object rescaled according to the proportions given."
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixScale(self._pix, scalex, scaley))
|
||||
|
||||
def rotate180(self):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
|
||||
|
||||
def rotate_orth(self, quads):
|
||||
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRotateOrth(self._pix, quads))
|
||||
|
||||
def find_skew(self):
|
||||
"""Returns a tuple (deskew angle in degrees, confidence value).
|
||||
|
||||
Returns (None, None) if no angle is available.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
angle = ffi.new('float *', 0.0)
|
||||
confidence = ffi.new('float *', 0.0)
|
||||
result = lept.pixFindSkew(self._pix, angle, confidence)
|
||||
if result == 0:
|
||||
return (angle[0], confidence[0])
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def convert_rgb_to_luminance(self):
|
||||
with LeptonicaErrorTrap():
|
||||
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
|
||||
if gray_pix:
|
||||
return Pix(gray_pix)
|
||||
return None
|
||||
|
||||
def remove_colormap(self, removal_type):
|
||||
"""Remove a palette
|
||||
|
||||
removal_type - RemovalColormap()
|
||||
"""
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
|
||||
|
||||
def otsu_adaptive_threshold(
|
||||
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
p_pix = ffi.new('PIX **')
|
||||
|
||||
result = lept.pixOtsuAdaptiveThreshold(
|
||||
self._pix,
|
||||
sx, sy,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL,
|
||||
p_pix)
|
||||
if result == 0:
|
||||
return Pix(p_pix[0])
|
||||
else:
|
||||
return None
|
||||
|
||||
def otsu_threshold_on_background_norm(
|
||||
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
|
||||
bgval=255, kernel_size=(2, 2), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
if mask is None:
|
||||
mask = ffi.NULL
|
||||
if isinstance(mask, Pix):
|
||||
mask = mask._pix
|
||||
|
||||
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
|
||||
self._pix,
|
||||
mask,
|
||||
sx, sy,
|
||||
thresh, mincount, bgval,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL
|
||||
)
|
||||
if thresh_pix == ffi.NULL:
|
||||
return None
|
||||
return Pix(thresh_pix)
|
||||
|
||||
def crop_to_foreground(
|
||||
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
|
||||
showmorph=0, display=0, pdfdir=ffi.NULL):
|
||||
with LeptonicaErrorTrap():
|
||||
cropbox = Box(lept.pixFindPageForeground(
|
||||
self._pix,
|
||||
threshold,
|
||||
mindist,
|
||||
erasedist,
|
||||
pagenum,
|
||||
showmorph,
|
||||
display,
|
||||
pdfdir))
|
||||
|
||||
print(repr(cropbox))
|
||||
|
||||
cropped_pix = lept.pixClipRectangle(
|
||||
self._pix,
|
||||
cropbox._box,
|
||||
ffi.NULL)
|
||||
|
||||
return Pix(cropped_pix)
|
||||
|
||||
def clean_background_to_white(
|
||||
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixCleanBackgroundToWhite(
|
||||
self._pix,
|
||||
mask or ffi.NULL,
|
||||
grayscale or ffi.NULL,
|
||||
gamma,
|
||||
black,
|
||||
white))
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def make_pixel_sum_tab8():
|
||||
return lept.makePixelSumTab8()
|
||||
|
||||
@staticmethod
|
||||
def correlation_binary(pix1, pix2):
|
||||
if get_leptonica_version() < 'leptonica-1.72':
|
||||
# Older versions of Leptonica (pre-1.72) have a buggy
|
||||
# implementation of pixCorrelationBinary that overflows on larger
|
||||
# images.
|
||||
pix1_count = ffi.new('l_int32 *')
|
||||
pix2_count = ffi.new('l_int32 *')
|
||||
pixn_count = ffi.new('l_int32 *')
|
||||
tab8 = Pix.make_pixel_sum_tab8()
|
||||
|
||||
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
|
||||
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
|
||||
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
|
||||
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
|
||||
|
||||
# Python converts these int32s to larger units as needed
|
||||
# to avoid overflow. Overflow happens easily here.
|
||||
correlation = (
|
||||
(pixn_count[0] * pixn_count[0]) /
|
||||
(pix1_count[0] * pix2_count[0])
|
||||
)
|
||||
return correlation
|
||||
else:
|
||||
correlation = ffi.new('float *', 0.0)
|
||||
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
|
||||
correlation)
|
||||
if result != 0:
|
||||
raise LeptonicaError("Correlation failed")
|
||||
return correlation[0]
|
||||
|
||||
@staticmethod
|
||||
def _pix_destroy(pix):
|
||||
p_pix = ffi.new('PIX **', pix)
|
||||
lept.pixDestroy(p_pix)
|
||||
# print('pix destroy ' + repr(pix))
|
||||
|
||||
|
||||
def pixWriteImpliedFormat(filename, pix, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
class Box:
|
||||
"""Wrapper around Leptonica's BOX objects.
|
||||
|
||||
See class Pix for notes about reference counting.
|
||||
"""
|
||||
fileroot, extension = os.path.splitext(filename)
|
||||
fix_pnm = False
|
||||
if extension.lower() in ('.pbm', '.pgm', '.ppm'):
|
||||
# Leptonica does not process handle these extensions correctly, but
|
||||
# does handle .pnm correctly. Add another .pnm suffix.
|
||||
filename += '.pnm'
|
||||
fix_pnm = True
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
pix, jpeg_quality, jpeg_progressive)
|
||||
def __init__(self, box):
|
||||
self._box = ffi.gc(box, Box._box_destroy)
|
||||
|
||||
if fix_pnm:
|
||||
from shutil import move
|
||||
move(filename, filename[:-4]) # Remove .pnm suffix
|
||||
def __repr__(self):
|
||||
if self._box:
|
||||
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
|
||||
self.x, self.y, self.w, self.h)
|
||||
return '<leptonica.Box NULL>'
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self._box.x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self._box.y
|
||||
|
||||
@property
|
||||
def w(self):
|
||||
return self._box.w
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
return self._box.h
|
||||
|
||||
@staticmethod
|
||||
def _box_destroy(box):
|
||||
p_box = ffi.new('BOX **', box)
|
||||
lept.boxDestroy(p_box)
|
||||
|
||||
|
||||
def pixDestroy(pix):
|
||||
"""Destroy the pix object.
|
||||
|
||||
Function signature is pixDestroy(struct Pix **), hence C.byref() to pass
|
||||
the address of the pointer.
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixDestroy(C.byref(pix))
|
||||
|
||||
|
||||
def getLeptonicaVersion():
|
||||
@lru_cache(maxsize=1)
|
||||
def get_leptonica_version():
|
||||
"""Get Leptonica version string.
|
||||
|
||||
Caveat: Leptonica expects the caller to free this memory. We don't,
|
||||
since that would involve binding to libc to access libc.free(),
|
||||
a pointless effort to reclaim 100 bytes of memory.
|
||||
|
||||
"""
|
||||
return lept.getLeptonicaVersion().decode()
|
||||
return ffi.string(lept.getLeptonicaVersion()).decode()
|
||||
|
||||
|
||||
def deskew(infile, outfile, dpi):
|
||||
try:
|
||||
pix_source = pixRead(infile)
|
||||
pix_source = Pix.read(infile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open file: %s" % infile)
|
||||
|
||||
@@ -244,14 +453,12 @@ def deskew(infile, outfile, dpi):
|
||||
reduction_factor = 1 # Don't downsample too much if DPI is already low
|
||||
else:
|
||||
reduction_factor = 0 # Use default
|
||||
pix_deskewed = pixDeskew(pix_source, reduction_factor)
|
||||
pix_deskewed = pix_source.deskew(reduction_factor)
|
||||
|
||||
try:
|
||||
pixWriteImpliedFormat(outfile, pix_deskewed)
|
||||
pix_deskewed.write_implied_format(outfile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
pixDestroy(pix_source)
|
||||
pixDestroy(pix_deskewed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -270,39 +477,12 @@ if __name__ == '__main__':
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if getLeptonicaVersion() != u'leptonica-1.69':
|
||||
if get_leptonica_version() != u'leptonica-1.69':
|
||||
print("Unexpected leptonica version: %s" % getLeptonicaVersion())
|
||||
|
||||
args.func(args)
|
||||
|
||||
|
||||
def _test_output(mode, extension, im_format):
|
||||
from PIL import Image
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile(prefix='test-lept-pnm', suffix=extension, delete=True) as tmpfile:
|
||||
im = Image.new(mode=mode, size=(100, 100))
|
||||
im.save(tmpfile)
|
||||
|
||||
pix = pixRead(tmpfile.name)
|
||||
pixWriteImpliedFormat(tmpfile.name, pix)
|
||||
pixDestroy(pix)
|
||||
|
||||
im_roundtrip = Image.open(tmpfile.name)
|
||||
assert im_roundtrip.mode == im.mode, "leptonica mode differs"
|
||||
assert im_roundtrip.format == im_format, \
|
||||
"{0}: leptonica produced a {1}".format(
|
||||
extension,
|
||||
im_roundtrip.format)
|
||||
|
||||
|
||||
def test_pnm_output():
|
||||
params = [['1', '.pbm', 'PPM'], ['L', '.pgm', 'PPM'],
|
||||
['RGB', '.ppm', 'PPM']]
|
||||
for param in params:
|
||||
_test_output(*param)
|
||||
|
||||
|
||||
def test_skew_angle():
|
||||
from PIL import Image, ImageDraw
|
||||
from tempfile import NamedTemporaryFile
|
||||
@@ -325,7 +505,6 @@ def test_skew_angle():
|
||||
rotated_im.save(tmpfile)
|
||||
pix = pixRead(tmpfile.name)
|
||||
angle, confidence = pixFindSkew(pix)
|
||||
pixDestroy(pix)
|
||||
print('{0} {1} {2}'.format(rotate_angle, angle, confidence), file=sys.stderr)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
ffi.set_source("ocrmypdf.lib._leptonica", None)
|
||||
ffi.cdef("""
|
||||
typedef signed char l_int8;
|
||||
typedef unsigned char l_uint8;
|
||||
typedef short l_int16;
|
||||
typedef unsigned short l_uint16;
|
||||
typedef int l_int32;
|
||||
typedef unsigned int l_uint32;
|
||||
typedef float l_float32;
|
||||
typedef double l_float64;
|
||||
typedef long long l_int64;
|
||||
typedef unsigned long long l_uint64;
|
||||
|
||||
struct Pix
|
||||
{
|
||||
l_uint32 w; /* width in pixels */
|
||||
l_uint32 h; /* height in pixels */
|
||||
l_uint32 d; /* depth in bits (bpp) */
|
||||
l_uint32 spp; /* number of samples per pixel */
|
||||
l_uint32 wpl; /* 32-bit words/line */
|
||||
l_uint32 refcount; /* reference count (1 if no clones) */
|
||||
l_int32 xres; /* image res (ppi) in x direction */
|
||||
/* (use 0 if unknown) */
|
||||
l_int32 yres; /* image res (ppi) in y direction */
|
||||
/* (use 0 if unknown) */
|
||||
l_int32 informat; /* input file format, IFF_* */
|
||||
l_int32 special; /* special instructions for I/O, etc */
|
||||
char *text; /* text string associated with pix */
|
||||
struct PixColormap *colormap; /* colormap (may be null) */
|
||||
l_uint32 *data; /* the image data */
|
||||
};
|
||||
typedef struct Pix PIX;
|
||||
|
||||
struct PixColormap
|
||||
{
|
||||
void *array; /* colormap table (array of RGBA_QUAD) */
|
||||
l_int32 depth; /* of pix (1, 2, 4 or 8 bpp) */
|
||||
l_int32 nalloc; /* number of color entries allocated */
|
||||
l_int32 n; /* number of color entries used */
|
||||
};
|
||||
typedef struct PixColormap PIXCMAP;
|
||||
|
||||
struct Box
|
||||
{
|
||||
l_int32 x;
|
||||
l_int32 y;
|
||||
l_int32 w;
|
||||
l_int32 h;
|
||||
l_uint32 refcount; /* reference count (1 if no clones) */
|
||||
|
||||
};
|
||||
typedef struct Box BOX;
|
||||
|
||||
""")
|
||||
|
||||
ffi.cdef("""
|
||||
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 );
|
||||
void pixDestroy ( PIX **ppix );
|
||||
|
||||
PIX *
|
||||
pixEndianByteSwapNew(PIX *pixs);
|
||||
|
||||
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
|
||||
char * getLeptonicaVersion ( );
|
||||
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
|
||||
PIX *pixRotate180(PIX *pixd, PIX *pixs);
|
||||
PIX *
|
||||
pixRotateOrth(PIX *pixs,
|
||||
l_int32 quads);
|
||||
|
||||
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
|
||||
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
|
||||
l_int32 * makePixelSumTab8 ( void );
|
||||
|
||||
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
|
||||
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
|
||||
|
||||
PIX * pixConvertRGBToLuminance(PIX *pixs);
|
||||
|
||||
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
|
||||
|
||||
l_int32
|
||||
pixOtsuAdaptiveThreshold(PIX *pixs,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
PIX **ppixth,
|
||||
PIX **ppixd);
|
||||
|
||||
PIX *
|
||||
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
|
||||
PIX *pixim,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 thresh,
|
||||
l_int32 mincount,
|
||||
l_int32 bgval,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
l_int32 *pthresh);
|
||||
|
||||
PIX *
|
||||
pixCleanBackgroundToWhite(PIX *pixs,
|
||||
PIX *pixim,
|
||||
PIX *pixg,
|
||||
l_float32 gamma,
|
||||
l_int32 blackval,
|
||||
l_int32 whiteval);
|
||||
|
||||
BOX *
|
||||
pixFindPageForeground(PIX *pixs,
|
||||
l_int32 threshold,
|
||||
l_int32 mindist,
|
||||
l_int32 erasedist,
|
||||
l_int32 pagenum,
|
||||
l_int32 showmorph,
|
||||
l_int32 display,
|
||||
const char *pdfdir);
|
||||
|
||||
PIX *
|
||||
pixClipRectangle(PIX *pixs,
|
||||
BOX *box,
|
||||
BOX **pboxc);
|
||||
|
||||
void
|
||||
boxDestroy(BOX **pbox);
|
||||
|
||||
void lept_free(void *ptr);
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ffi.compile()
|
||||
+361
-68
@@ -12,6 +12,7 @@ import multiprocessing
|
||||
import atexit
|
||||
import textwrap
|
||||
import img2pdf
|
||||
import logging
|
||||
|
||||
import PyPDF2 as pypdf
|
||||
from PIL import Image
|
||||
@@ -22,6 +23,7 @@ from ruffus import transform, suffix, merge, active_if, regex, jobs_limit, \
|
||||
formatter, follows, split, collate, check_if_uptodate
|
||||
import ruffus.ruffus_exceptions as ruffus_exceptions
|
||||
import ruffus.cmdline as cmdline
|
||||
import ruffus.proxy_logger as proxy_logger
|
||||
|
||||
from .hocrtransform import HocrTransform
|
||||
from .pageinfo import pdf_get_all_pageinfo
|
||||
@@ -29,7 +31,7 @@ from .pdfa import generate_pdfa_def
|
||||
from . import ghostscript
|
||||
from . import tesseract
|
||||
from . import qpdf
|
||||
from . import ExitCode
|
||||
from . import ExitCode, page_number
|
||||
|
||||
import pkg_resources
|
||||
|
||||
@@ -58,7 +60,6 @@ if tesseract.version() < MINIMUM_TESS_VERSION:
|
||||
MINIMUM_TESS_VERSION, tesseract.version()))
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
|
||||
try:
|
||||
import PIL.features
|
||||
check_codec = PIL.features.check_codec
|
||||
@@ -105,7 +106,7 @@ parser = cmdline.get_argparse(
|
||||
ignored_args=[
|
||||
'touch_files_only', 'recreate_database', 'checksum_file_name',
|
||||
'key_legend_in_graph', 'draw_graph_horizontally', 'flowchart_format',
|
||||
'forced_tasks', 'target_tasks', 'use_threads', 'jobs'])
|
||||
'forced_tasks', 'target_tasks', 'use_threads', 'jobs', 'log_file'])
|
||||
|
||||
parser.add_argument(
|
||||
'input_file',
|
||||
@@ -122,7 +123,7 @@ parser.add_argument(
|
||||
|
||||
metadata = parser.add_argument_group(
|
||||
"Metadata options",
|
||||
"Set output PDF/A metadata (default: use input document's title)")
|
||||
"Set output PDF/A metadata (default: use input document's metadata)")
|
||||
metadata.add_argument(
|
||||
'--title', type=str,
|
||||
help="set document title (place multiple words in quotes)")
|
||||
@@ -131,15 +132,17 @@ metadata.add_argument(
|
||||
help="set document author")
|
||||
metadata.add_argument(
|
||||
'--subject', type=str,
|
||||
help="set document")
|
||||
help="set document subject description")
|
||||
metadata.add_argument(
|
||||
'--keywords', type=str,
|
||||
help="set document keywords")
|
||||
|
||||
|
||||
preprocessing = parser.add_argument_group(
|
||||
"Preprocessing options",
|
||||
"Improve OCR quality and final image")
|
||||
preprocessing.add_argument(
|
||||
'-r', '--rotate-pages', action='store_true',
|
||||
help="automatically rotate pages based on detected text orientation")
|
||||
preprocessing.add_argument(
|
||||
'-d', '--deskew', action='store_true',
|
||||
help="deskew each page before performing OCR")
|
||||
@@ -154,20 +157,20 @@ preprocessing.add_argument(
|
||||
help="oversample images to at least the specified DPI, to improve OCR "
|
||||
"results slightly")
|
||||
|
||||
parser.add_argument(
|
||||
ocrsettings = parser.add_argument_group(
|
||||
"OCR options",
|
||||
"Control how OCR is applied")
|
||||
ocrsettings.add_argument(
|
||||
'-f', '--force-ocr', action='store_true',
|
||||
help="rasterize any fonts or vector images on each page and apply OCR")
|
||||
parser.add_argument(
|
||||
ocrsettings.add_argument(
|
||||
'-s', '--skip-text', action='store_true',
|
||||
help="skip OCR on any pages that already contain text, but include the"
|
||||
" page in final output")
|
||||
parser.add_argument(
|
||||
ocrsettings.add_argument(
|
||||
'--skip-big', type=float, metavar='MPixels',
|
||||
help="skip OCR on pages larger than the specified amount of megapixels, "
|
||||
"but include skipped pages in final output")
|
||||
# parser.add_argument(
|
||||
# '--exact-image', action='store_true',
|
||||
# help="Use original page from PDF without re-rendering")
|
||||
|
||||
advanced = parser.add_argument_group(
|
||||
"Advanced",
|
||||
@@ -185,6 +188,10 @@ advanced.add_argument(
|
||||
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
||||
help='give up on OCR after the timeout, but copy the preprocessed page '
|
||||
'into the final output')
|
||||
advanced.add_argument(
|
||||
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
|
||||
help="only rotate pages when confidence is above this value (arbitrary "
|
||||
"units reported by tesseract)")
|
||||
|
||||
debugging = parser.add_argument_group(
|
||||
"Debugging",
|
||||
@@ -224,13 +231,25 @@ if not set(options.language).issubset(tesseract.languages()):
|
||||
if options.pdf_renderer == 'auto':
|
||||
options.pdf_renderer = 'hocr'
|
||||
|
||||
if any((options.deskew, options.clean, options.clean_final)):
|
||||
if options.pdf_renderer == 'tesseract' and tesseract.version() < '3.04.01' \
|
||||
and os.environ.get('OCRMYPDF_SHARP_TTF', '') != '1':
|
||||
complain(
|
||||
"WARNING: Your version of tesseract has problems with PDF output. "
|
||||
"Some PDF viewers will fail to find searchable text.\n"
|
||||
"--pdf-renderer=tesseract is not recommended.")
|
||||
|
||||
if any((options.clean, options.clean_final)):
|
||||
try:
|
||||
from . import unpaper
|
||||
except ImportError:
|
||||
if unpaper.version() < '6.1':
|
||||
complain(
|
||||
"The installed 'unpaper' is not supported. "
|
||||
"Install version 6.1 or newer.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
except FileNotFoundError:
|
||||
complain(
|
||||
"Install the 'unpaper' program to use --deskew or --clean.")
|
||||
sys.exit(ExitCode.bad_args)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
else:
|
||||
unpaper = None
|
||||
|
||||
@@ -255,12 +274,29 @@ if options.pdf_renderer == 'hocr':
|
||||
if not options.deskew and not options.clean_final and not options.force_ocr:
|
||||
lossless_reconstruction = True
|
||||
|
||||
|
||||
# ----------
|
||||
# Logging
|
||||
|
||||
|
||||
_logger, _logger_mutex = cmdline.setup_logging(__name__, options.log_file,
|
||||
options.verbose)
|
||||
def logging_factory(logger_name, listargs):
|
||||
log_file_name, verbose = listargs
|
||||
|
||||
root_logger = logging.getLogger(logger_name)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
formatter_ = logging.Formatter("%(levelname)7s - %(message)s")
|
||||
handler.setFormatter(formatter_)
|
||||
if verbose:
|
||||
handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
root_logger.addHandler(handler)
|
||||
return root_logger
|
||||
|
||||
_logger, _logger_mutex = proxy_logger.make_shared_logger_and_proxy(
|
||||
logging_factory, __name__, [None, options.verbose])
|
||||
|
||||
|
||||
class WrappedLogger:
|
||||
@@ -351,7 +387,7 @@ def cleanup_working_files(*args):
|
||||
@transform(
|
||||
input=options.input_file,
|
||||
filter=formatter('(?i)\.pdf'),
|
||||
output=work_folder + '{basename[0]}.repaired.pdf',
|
||||
output=os.path.join(work_folder, '{basename[0]}.repaired.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def repair_pdf(
|
||||
input_file,
|
||||
@@ -363,7 +399,7 @@ def repair_pdf(
|
||||
qpdf.repair(input_file, output_file, log)
|
||||
with pdfinfo_lock:
|
||||
pdfinfo.extend(pdf_get_all_pageinfo(output_file))
|
||||
log.info(pdfinfo)
|
||||
log.debug(pdfinfo)
|
||||
|
||||
|
||||
def get_pageinfo(input_file, pdfinfo, pdfinfo_lock):
|
||||
@@ -381,11 +417,11 @@ def is_ocr_required(pageinfo, log):
|
||||
# or both. It seems quite unlikely that one would find meaningful text
|
||||
# from rasterizing vector content. So skip the page.
|
||||
log.info(
|
||||
"Page {0} has no images - skipping OCR".format(page)
|
||||
"{0:4d}: page has no images - skipping OCR".format(page)
|
||||
)
|
||||
ocr_required = False
|
||||
elif pageinfo['has_text']:
|
||||
s = "Page {0} already has text! – {1}"
|
||||
s = "{0:4d}: page already has text! – {1}"
|
||||
|
||||
if not options.force_ocr and not options.skip_text:
|
||||
log.error(s.format(page,
|
||||
@@ -404,9 +440,10 @@ def is_ocr_required(pageinfo, log):
|
||||
pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels']
|
||||
if pixel_count > (options.skip_big * 1000000):
|
||||
ocr_required = False
|
||||
log.info(
|
||||
"Page {0} is very large; skipping due to -b".format(page))
|
||||
|
||||
log.warning(
|
||||
"{0:4d}: page too big, skipping OCR "
|
||||
"({1:.1f} MPixels > {2:.1f} MPixels --skip-big)".format(
|
||||
page, pixel_count / 1000000, options.skip_big))
|
||||
return ocr_required
|
||||
|
||||
|
||||
@@ -425,7 +462,13 @@ def split_pages(
|
||||
with suppress(FileNotFoundError):
|
||||
os.unlink(oo)
|
||||
|
||||
npages = qpdf.get_npages(input_file)
|
||||
# If no files were repaired the input will be empty
|
||||
if not input_file:
|
||||
log.error("{0}: file not found or invalid argument".format(
|
||||
options.input_file))
|
||||
sys.exit(ExitCode.input_file)
|
||||
|
||||
npages = qpdf.get_npages(input_file, log)
|
||||
qpdf.split_pages(input_file, work_folder, npages)
|
||||
|
||||
from glob import glob
|
||||
@@ -441,9 +484,105 @@ def split_pages(
|
||||
os.path.basename(filename)[0:6] + alt_suffix))
|
||||
|
||||
|
||||
@active_if(options.rotate_pages)
|
||||
@transform(
|
||||
input=split_pages,
|
||||
filter=suffix('.ocr.page.pdf'),
|
||||
filter=suffix('.page.pdf'),
|
||||
output='.preview.jpg',
|
||||
output_dir=work_folder,
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def rasterize_preview(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
ghostscript.rasterize_pdf(
|
||||
input_file=input_file,
|
||||
output_file=output_file,
|
||||
xres=200,
|
||||
yres=200,
|
||||
raster_device='jpeggray',
|
||||
log=log)
|
||||
|
||||
|
||||
@collate(
|
||||
input=[split_pages, rasterize_preview],
|
||||
filter=regex(r".*/(\d{6})(\.ocr|\.skip)(?:\.page\.pdf|\.preview\.jpg)"),
|
||||
output=os.path.join(work_folder, r'\1\2.oriented.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def orient_page(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
page_pdf = next(ii for ii in infiles if ii.endswith('.page.pdf'))
|
||||
|
||||
if not options.rotate_pages:
|
||||
re_symlink(page_pdf, output_file)
|
||||
return
|
||||
preview = next(ii for ii in infiles if ii.endswith('.preview.jpg'))
|
||||
|
||||
orient_conf = tesseract.get_orientation(
|
||||
preview,
|
||||
language=options.language,
|
||||
timeout=options.tesseract_timeout,
|
||||
log=log)
|
||||
|
||||
direction = {
|
||||
0: '⇧',
|
||||
90: '⇨',
|
||||
180: '⇩',
|
||||
270: '⇦'
|
||||
}
|
||||
|
||||
apply_correction = False
|
||||
description = ''
|
||||
if orient_conf.confidence >= options.rotate_pages_threshold:
|
||||
if orient_conf.angle != 0:
|
||||
apply_correction = True
|
||||
description = ' - will rotate'
|
||||
else:
|
||||
description = ' - rotation appears correct'
|
||||
else:
|
||||
if orient_conf.angle != 0:
|
||||
description = ' - confidence too low to rotate'
|
||||
else:
|
||||
description = ' - no change'
|
||||
|
||||
log.info(
|
||||
'{0:4d}: page is facing {1}, confidence {2:.2f}{3}'.format(
|
||||
page_number(preview),
|
||||
direction.get(orient_conf.angle, '?'),
|
||||
orient_conf.confidence,
|
||||
description)
|
||||
)
|
||||
|
||||
if not apply_correction:
|
||||
re_symlink(page_pdf, output_file)
|
||||
else:
|
||||
writer = pypdf.PdfFileWriter()
|
||||
reader = pypdf.PdfFileReader(page_pdf)
|
||||
page = reader.pages[0]
|
||||
|
||||
# angle is a clockwise angle, so rotating ccw will correct the error
|
||||
rotated_page = page.rotateCounterClockwise(orient_conf.angle)
|
||||
writer.addPage(rotated_page)
|
||||
with open(output_file, 'wb') as out:
|
||||
writer.write(out)
|
||||
|
||||
with pdfinfo_lock:
|
||||
pageno = int(os.path.basename(page_pdf)[0:6]) - 1
|
||||
pageinfo = pdfinfo[pageno].copy()
|
||||
pageinfo['rotated'] = orient_conf.angle
|
||||
pdfinfo[pageno] = pageinfo
|
||||
|
||||
|
||||
@transform(
|
||||
input=orient_page,
|
||||
filter=suffix('.ocr.oriented.pdf'),
|
||||
output='.page.png',
|
||||
output_dir=work_folder,
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
@@ -466,7 +605,7 @@ def rasterize_with_ghostscript(
|
||||
for image in pageinfo['images']):
|
||||
device = 'pnggray'
|
||||
|
||||
log.debug("Rendering {0} with {1}".format(
|
||||
log.debug("Rasterize {0} with {1}".format(
|
||||
os.path.basename(input_file), device))
|
||||
xres = max(pageinfo['xres'], options.oversample or 0)
|
||||
yres = max(pageinfo['yres'], options.oversample or 0)
|
||||
@@ -493,7 +632,8 @@ def preprocess_deskew(
|
||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||
dpi = int(pageinfo['xres'])
|
||||
|
||||
unpaper.deskew(input_file, output_file, dpi, log)
|
||||
from . import leptonica
|
||||
leptonica.deskew(input_file, output_file, dpi)
|
||||
|
||||
|
||||
@transform(
|
||||
@@ -544,7 +684,6 @@ def ocr_tesseract_hocr(
|
||||
)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@collate(
|
||||
input=[rasterize_with_ghostscript, preprocess_deskew, preprocess_clean],
|
||||
filter=regex(r".*/(\d{6})(?:\.page|\.pp-deskew|\.pp-clean)\.png"),
|
||||
@@ -567,15 +706,20 @@ def select_image_for_pdf(
|
||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||
if all(image['enc'] == 'jpeg' for image in pageinfo['images']):
|
||||
# If all images were JPEGs originally, produce a JPEG as output
|
||||
Image.open(image).save(output_file, format='JPEG')
|
||||
im = Image.open(image)
|
||||
dpi = im.info.get(
|
||||
'dpi',
|
||||
(int(pageinfo['xres']), int(pageinfo['yres']))
|
||||
)
|
||||
im.save(output_file, format='JPEG', dpi=dpi)
|
||||
else:
|
||||
re_symlink(image, output_file)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@collate(
|
||||
input=[select_image_for_pdf, split_pages],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.page\.pdf)"),
|
||||
input=[select_image_for_pdf, orient_page],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.oriented\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.image-layer.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def select_image_layer(
|
||||
@@ -585,16 +729,27 @@ def select_image_layer(
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
page_pdf = next(ii for ii in infiles if ii.endswith('.page.pdf'))
|
||||
page_pdf = next(ii for ii in infiles if ii.endswith('.ocr.oriented.pdf'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image'))
|
||||
|
||||
if lossless_reconstruction:
|
||||
log.debug("{:4d}: page eligible for lossless reconstruction".format(
|
||||
page_number(page_pdf)))
|
||||
re_symlink(page_pdf, output_file)
|
||||
else:
|
||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
|
||||
with open(output_file, 'wb') as pdf:
|
||||
img2pdf.convert([image], dpi=dpi, outputstream=pdf)
|
||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'],
|
||||
options.oversample))
|
||||
imgsize = ((img2pdf.ImgSize.dpi, dpi), (img2pdf.ImgSize.dpi, dpi))
|
||||
|
||||
layout_fun = img2pdf.get_layout_fun(None, imgsize, None, None, None)
|
||||
|
||||
with open(image, 'rb') as imfile, \
|
||||
open(output_file, 'wb') as pdf:
|
||||
rawdata = imfile.read()
|
||||
pdf.write(img2pdf.convert(
|
||||
rawdata, producer="img2pdf", with_pdfrw=False,
|
||||
layout_fun=layout_fun))
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@@ -642,6 +797,10 @@ def render_hocr_debug_page(
|
||||
showBoundingboxes=True, invisibleText=False)
|
||||
|
||||
|
||||
class PdfMergeFailedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@collate(
|
||||
input=[render_hocr_page, select_image_layer],
|
||||
@@ -657,15 +816,59 @@ def add_text_layer(
|
||||
text = next(ii for ii in infiles if ii.endswith('.hocr.pdf'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image-layer.pdf'))
|
||||
|
||||
pdf_output = pypdf.PdfFileWriter()
|
||||
|
||||
pdf_text = pypdf.PdfFileReader(open(text, "rb"))
|
||||
pdf_image = pypdf.PdfFileReader(open(image, "rb"))
|
||||
|
||||
page = pdf_text.getPage(0)
|
||||
page.mergePage(pdf_image.getPage(0))
|
||||
page_text = pdf_text.getPage(0)
|
||||
|
||||
pdf_output.addPage(page)
|
||||
# The text page always will be oriented up by this stage
|
||||
# but if lossless_reconstruction, pdf_image may have a rotation applied
|
||||
# We have to eliminate the /Rotate tag (because it applies to the whole
|
||||
# page) and rotate the image layer to match the text page
|
||||
# Also, pdf_image may not have its mediabox nailed to (0, 0), so may need
|
||||
# translation
|
||||
page_image = pdf_image.getPage(0)
|
||||
rotation = page_image.get('/Rotate', 0)
|
||||
|
||||
# /Rotate is a clockwise rotation: 90 means page facing "east"
|
||||
# The negative of this value is the angle that eliminates that rotation
|
||||
rotation = -rotation % 360
|
||||
|
||||
x1 = page_image.mediaBox.getLowerLeft_x()
|
||||
x2 = page_image.mediaBox.getUpperRight_x()
|
||||
y1 = page_image.mediaBox.getLowerLeft_y()
|
||||
y2 = page_image.mediaBox.getUpperRight_y()
|
||||
|
||||
# Rotation occurs about the page's (0, 0). Most pages will have the media
|
||||
# box at (0, 0) will all content in the first quadrant but some cropped
|
||||
# files may have an offset mediabox. We translate the page so that its
|
||||
# bottom left corner after rotation is pinned to (0, 0) with the image
|
||||
# in the first quadrant.
|
||||
if rotation == 0:
|
||||
tx, ty = -x1, -y1
|
||||
elif rotation == 90:
|
||||
tx, ty = y2, -x1
|
||||
elif rotation == 180:
|
||||
tx, ty = x2, y2
|
||||
elif rotation == 270:
|
||||
tx, ty = -y1, x2
|
||||
else:
|
||||
pass
|
||||
|
||||
if rotation != 0:
|
||||
log.info("{0:4d}: rotating image layer {1} degrees".format(
|
||||
page_number(image), rotation, tx, ty))
|
||||
|
||||
try:
|
||||
page_text.mergeRotatedScaledTranslatedPage(
|
||||
page_image, rotation, 1.0, tx, ty, expand=False)
|
||||
except (AttributeError, ValueError) as e:
|
||||
if 'writeToStream' in str(e) or 'invalid literal' in str(e):
|
||||
raise PdfMergeFailedError() from e
|
||||
|
||||
|
||||
pdf_output = pypdf.PdfFileWriter()
|
||||
pdf_output.addPage(page_text)
|
||||
|
||||
with open(output_file, "wb") as out:
|
||||
pdf_output.write(out)
|
||||
@@ -673,8 +876,8 @@ def add_text_layer(
|
||||
|
||||
@active_if(options.pdf_renderer == 'tesseract')
|
||||
@collate(
|
||||
input=[preprocess_clean, split_pages],
|
||||
filter=regex(r".*/(\d{6})(?:\.pp-clean\.png|\.page\.pdf)"),
|
||||
input=[select_image_for_pdf, orient_page],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.oriented\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def tesseract_ocr_and_render_pdf(
|
||||
@@ -684,7 +887,7 @@ def tesseract_ocr_and_render_pdf(
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
input_image = next((ii for ii in input_files if ii.endswith('.png')), '')
|
||||
input_image = next((ii for ii in input_files if ii.endswith('.image')), '')
|
||||
input_pdf = next((ii for ii in input_files if ii.endswith('.pdf')))
|
||||
if not input_image:
|
||||
# Skipping this page
|
||||
@@ -748,8 +951,8 @@ def generate_postscript_stub(
|
||||
|
||||
|
||||
@transform(
|
||||
input=split_pages,
|
||||
filter=suffix('.skip.page.pdf'),
|
||||
input=orient_page,
|
||||
filter=suffix('.skip.oriented.pdf'),
|
||||
output='.done.pdf',
|
||||
output_dir=work_folder,
|
||||
extras=[_log])
|
||||
@@ -757,6 +960,10 @@ def skip_page(
|
||||
input_file,
|
||||
output_file,
|
||||
log):
|
||||
# The purpose of this step is its filter to forward only the skipped
|
||||
# files (.skip.oriented.pdf) while disregarding the processed ones
|
||||
# (.ocr.oriented.pdf). Alternative would be for merge_pages to filter
|
||||
# pages itself if it gets multiple copies of a page.
|
||||
re_symlink(input_file, output_file, log)
|
||||
|
||||
|
||||
@@ -786,7 +993,7 @@ def merge_pages(
|
||||
return key
|
||||
|
||||
pdf_pages = sorted(input_files, key=input_file_order)
|
||||
log.info(pdf_pages)
|
||||
log.debug("Final pages: " + "\n".join(pdf_pages))
|
||||
ghostscript.generate_pdfa(pdf_pages, output_file, options.jobs or 1)
|
||||
|
||||
|
||||
@@ -835,42 +1042,128 @@ def cleanup_ruffus_error_message(msg):
|
||||
return msg
|
||||
|
||||
|
||||
def do_ruffus_exception(ruffus_five_tuple):
|
||||
"""Replace the elaborate ruffus stack trace with a user friendly
|
||||
description of the error message that occurred."""
|
||||
|
||||
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
|
||||
if exc_name == 'builtins.SystemExit':
|
||||
match = re.search(r"\.(.+?)\)", exc_value)
|
||||
exit_code_name = match.groups()[0]
|
||||
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
||||
return exit_code
|
||||
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
|
||||
_log.error(cleanup_ruffus_error_message(exc_value))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'builtins.TypeError':
|
||||
# Even though repair_pdf will fail, ruffus will still try
|
||||
# to call split_pages with no input files, likely due to a bug
|
||||
if task_name == 'split_pages':
|
||||
_log.error("Input file '{0}' is not a valid PDF".format(
|
||||
options.input_file))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'subprocess.CalledProcessError':
|
||||
# It's up to the subprocess handler to report something useful
|
||||
msg = "Error occurred while running this command:"
|
||||
_log.error(msg + '\n' + exc_value)
|
||||
return ExitCode.child_process_error
|
||||
elif exc_name == 'ocrmypdf.main.PdfMergeFailedError':
|
||||
_log.error(textwrap.dedent("""\
|
||||
Failed to merge PDF image layer with OCR layer
|
||||
|
||||
Usually this happens because the input PDF file is mal-formed and
|
||||
ocrmypdf cannot automatically correct the problem on its own.
|
||||
|
||||
Try using
|
||||
ocrmypdf --pdf-renderer tesseract [..other args..]
|
||||
"""))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'PyPDF2.utils.PdfReadError' and \
|
||||
'not been decrypted' in exc_value:
|
||||
_log.error(textwrap.dedent("""\
|
||||
Input PDF uses either an encryption algorithm or a PDF security
|
||||
handler that is not supported by ocrmypdf.
|
||||
|
||||
For information about this PDF's security use
|
||||
qpdf --show-encryption [...input PDF...]
|
||||
|
||||
(Only algorithms "R = 1" and "R = 2" are supported.)
|
||||
|
||||
"""))
|
||||
return ExitCode.encrypted_pdf
|
||||
elif not options.verbose:
|
||||
_log.error(exc_stack)
|
||||
return ExitCode.other_error
|
||||
|
||||
|
||||
def traverse_ruffus_exception(e):
|
||||
"""Walk through a RethrownJobError and find the first exception.
|
||||
|
||||
The exit code will be based on this, even if multiple exceptions occurred
|
||||
at the same time."""
|
||||
|
||||
if isinstance(e[0], str) and len(e) == 5:
|
||||
return do_ruffus_exception(e)
|
||||
elif hasattr(e, '__iter__'):
|
||||
for exc in e:
|
||||
return traverse_ruffus_exception(exc)
|
||||
|
||||
|
||||
def run_pipeline():
|
||||
if not options.jobs:
|
||||
options.jobs = available_cpu_count()
|
||||
try:
|
||||
options.history_file = os.path.join(work_folder, 'ruffus_history.sqlite')
|
||||
options.history_file = os.path.join(
|
||||
work_folder, 'ruffus_history.sqlite')
|
||||
cmdline.run(options)
|
||||
except ruffus_exceptions.RethrownJobError as e:
|
||||
if options.verbose:
|
||||
print(e)
|
||||
_log.debug(str(e)) # stringify exception so logger doesn't have to
|
||||
|
||||
# Yuck. Hunt through the ruffus exception to find out what the
|
||||
# return code is supposed to be.
|
||||
for exc in e.args:
|
||||
task_name, job_name, exc_name, exc_value, exc_stack = exc
|
||||
if exc_name == 'builtins.SystemExit':
|
||||
match = re.search(r"\.(.+?)\)", exc_value)
|
||||
exit_code_name = match.groups()[0]
|
||||
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
||||
return exit_code
|
||||
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
|
||||
print(cleanup_ruffus_error_message(exc_value))
|
||||
return ExitCode.input_file
|
||||
elif exc_name == 'builtins.TypeError':
|
||||
# Even though repair_pdf will fail, ruffus will still try
|
||||
# to call split_pages with no input files, likely due to a bug
|
||||
if task_name == 'split_pages':
|
||||
print("Input file '{0}' is not a valid PDF".format(
|
||||
options.input_file))
|
||||
return ExitCode.input_file
|
||||
# Ruffus flattens exception to 5 element tuples. Because of a bug
|
||||
# in <= 2.6.3 it may present either the single:
|
||||
# (task, job, exc, value, stack)
|
||||
# or something like:
|
||||
# [[(task, job, exc, value, stack)]]
|
||||
#
|
||||
# Generally cross-process exception marshalling doesn't work well
|
||||
# and ruffus doesn't support because BaseException has its own
|
||||
# implementation of __reduce__ that attempts to reconstruct the
|
||||
# exception based on e.__init__(e.args).
|
||||
#
|
||||
# Attempting to log the exception directly marshalls it to the logger
|
||||
# which is probably in another process, so it's better to log only
|
||||
# data from the exception at this point.
|
||||
|
||||
exitcode = traverse_ruffus_exception(e.args)
|
||||
if exitcode is None:
|
||||
_log.error("Unexpected ruffus exception: " + str(e))
|
||||
_log.error(repr(e))
|
||||
return ExitCode.other_error
|
||||
else:
|
||||
return exitcode
|
||||
except Exception as e:
|
||||
_log.error(e)
|
||||
return ExitCode.other_error
|
||||
|
||||
if not validate_pdfa(options.output_file, _log):
|
||||
_log.warning('Output file: The generated PDF/A file is INVALID')
|
||||
return ExitCode.invalid_output_pdfa
|
||||
|
||||
with _pdfinfo_lock:
|
||||
_log.debug(_pdfinfo)
|
||||
direction = {0: 'n', 90: 'e',
|
||||
180: 's', 270: 'w'}
|
||||
orientations = []
|
||||
for n, page in enumerate(_pdfinfo):
|
||||
angle = _pdfinfo[n].get('rotated', 0)
|
||||
if angle != 0:
|
||||
orientations.append('{0}{1}'.format(
|
||||
n + 1,
|
||||
direction.get(angle, '')))
|
||||
if orientations:
|
||||
_log.info('Page orientations detected: ' + ' '.join(orientations))
|
||||
|
||||
return ExitCode.ok
|
||||
|
||||
|
||||
|
||||
+207
-38
@@ -3,10 +3,13 @@
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal, getcontext
|
||||
from math import hypot
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
FRIENDLY_COLORSPACE = {
|
||||
'/DeviceGray': 'gray',
|
||||
@@ -19,7 +22,11 @@ FRIENDLY_COLORSPACE = {
|
||||
'/Indexed': 'index',
|
||||
'/Separation': 'sep',
|
||||
'/DeviceN': 'devn',
|
||||
'/Pattern': '-'
|
||||
'/Pattern': '-',
|
||||
'/G': 'gray', # Abbreviations permitted in inline images
|
||||
'/RGB': 'rgb',
|
||||
'/CMYK': 'cmyk',
|
||||
'/I': 'index',
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING = {
|
||||
@@ -27,6 +34,13 @@ FRIENDLY_ENCODING = {
|
||||
'/DCTDecode': 'jpeg',
|
||||
'/JPXDecode': 'jpx',
|
||||
'/JBIG2Decode': 'jbig2',
|
||||
'/CCF': 'ccitt', # Abbreviations permitted in inline images
|
||||
'/DCT': 'jpeg',
|
||||
'/AHx': 'asciihex',
|
||||
'/A85': 'ascii85',
|
||||
'/LZW': 'lzw',
|
||||
'/Fl': 'flate',
|
||||
'/RL': 'runlength'
|
||||
}
|
||||
|
||||
FRIENDLY_COMP = {
|
||||
@@ -38,47 +52,165 @@ FRIENDLY_COMP = {
|
||||
}
|
||||
|
||||
|
||||
def _page_has_inline_images(page):
|
||||
# PDF always uses \r\n for separator regardless of platform
|
||||
# Really basic heuristic that might trigger the odd false positive
|
||||
# This is only finds the first image and is not quite spec compliant
|
||||
try:
|
||||
contents = page.getContents()
|
||||
data = contents.getData()
|
||||
except AttributeError:
|
||||
# If we can't access the contents or data (empty page?) then there
|
||||
# are no inline images
|
||||
return False
|
||||
def _matrix_from_shorthand(shorthand):
|
||||
"""Convert from PDF matrix shorthand to full matrix
|
||||
|
||||
begin_image, image_data, end_image = False, False, False
|
||||
for data in re.split(b'\s+', data):
|
||||
if data == b'BI':
|
||||
begin_image = True
|
||||
elif data == b'ID':
|
||||
image_data = True
|
||||
elif data == b'EI':
|
||||
end_image = True
|
||||
if all((begin_image, image_data, end_image)):
|
||||
return True
|
||||
return False
|
||||
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
|
||||
since the last column is always (0, 0, 1).
|
||||
"""
|
||||
|
||||
a, b, c, d, e, f = map(float, shorthand)
|
||||
return ((a, b, 0),
|
||||
(c, d, 0),
|
||||
(e, f, 1))
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo):
|
||||
def _shorthand_from_matrix(matrix):
|
||||
"""Convert from transformation matrix to PDF shorthand."""
|
||||
a, b = matrix[0][0], matrix[0][1]
|
||||
c, d = matrix[1][0], matrix[1][1]
|
||||
e, f = matrix[2][0], matrix[2][1]
|
||||
return tuple(map(float, (a, b, c, d, e, f)))
|
||||
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _interpret_contents(contentstream):
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand((1, 0, 0, 1, 0, 0))
|
||||
image_raster_settings = []
|
||||
inline_images = []
|
||||
|
||||
for op in operations:
|
||||
operands, command = op
|
||||
if command == b'q':
|
||||
stack.append(ctm)
|
||||
if len(stack) > 32:
|
||||
raise RuntimeError("PDF graphics stack overflow")
|
||||
elif command == b'Q':
|
||||
ctm = stack.pop()
|
||||
elif command == b'cm':
|
||||
ctm = matrix_mult(
|
||||
_matrix_from_shorthand(operands), ctm)
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
image_raster_settings.append(
|
||||
(image_name, _shorthand_from_matrix(ctm)))
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline_images.append(
|
||||
(settings, _shorthand_from_matrix(ctm)))
|
||||
|
||||
return ContentsInfo(
|
||||
raster_settings=image_raster_settings,
|
||||
inline_images=inline_images)
|
||||
|
||||
|
||||
def _get_dpi(ctm_shorthand, image_size):
|
||||
"""Given the transformation matrix and image size, find the image DPI.
|
||||
|
||||
PDFs do not include image resolution information within image data.
|
||||
Instead, the PDF page content stream describes the location where the
|
||||
image will be rasterized, and the effective resolution is the ratio of the
|
||||
pixel size to raster target size.
|
||||
|
||||
Normally a scanned PDF has the paper size set appropriately but this is
|
||||
not guaranteed. The most common case is a cropped image will change the
|
||||
page size (/CropBox) without altering the page content stream. That means
|
||||
it is not sufficient to assume that the image fills the page, even though
|
||||
that is the most common case.
|
||||
|
||||
A PDF image may be scaled (always), cropped, translated, rotated in place
|
||||
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
|
||||
be expressed, that is, it is not necessary to consider distortions where
|
||||
the effective DPI varies with position.
|
||||
|
||||
To determine the image scale, transform an offset axis vector v0 (0, 0),
|
||||
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
|
||||
which gives the dimensions of the image in PDF units. From there we can
|
||||
compare to actual image dimensions. PDF uses
|
||||
row vector * matrix_tranposed unlike the traditional
|
||||
matrix * column vector.
|
||||
|
||||
The offset, width and height vectors can be combined in a matrix and
|
||||
multiplied by the transform matrix. Then we want to calculated
|
||||
magnitude(width_vector - offset_vector)
|
||||
and
|
||||
magnitude(height_vector - offset_vector)
|
||||
|
||||
When the above is worked out algebraically, the effect of translation
|
||||
cancels out, and the vector magnitudes become functions of the nonzero
|
||||
transformation matrix indices. The results of the derivation are used
|
||||
in this code.
|
||||
|
||||
pdfimages -list does calculate the DPI in some way that is not completely
|
||||
naive, but it does not get the DPI of rotated images right, so cannot be
|
||||
used anymore to validate this. Photoshop works, or using Acrobat to
|
||||
rotate the image back to normal.
|
||||
|
||||
It does not matter if the image is partially cropped, or even out of the
|
||||
/MediaBox.
|
||||
|
||||
"""
|
||||
|
||||
a, b, c, d, _, _ = ctm_shorthand
|
||||
|
||||
# Calculate the width and height of the image in PDF units
|
||||
image_drawn_width = hypot(a, b)
|
||||
image_drawn_height = hypot(c, d)
|
||||
|
||||
# The scale of the image is pixels per PDF unit (1/72")
|
||||
scale_w = image_size[0] / image_drawn_width
|
||||
scale_h = image_size[1] / image_drawn_height
|
||||
|
||||
# DPI = scale * 72
|
||||
dpi_w = scale_w * 72.0
|
||||
dpi_h = scale_h * 72.0
|
||||
|
||||
return (dpi_w, dpi_h)
|
||||
|
||||
|
||||
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
|
||||
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['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
if '/F' in settings:
|
||||
filter_ = 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']))
|
||||
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
"Find images stored in XObject resources"
|
||||
|
||||
try:
|
||||
page['/Resources']['/XObject']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
# Look for XObject (out of line images)
|
||||
for xobj in page['/Resources']['/XObject']:
|
||||
# PyPDF2 returns the keys as an iterator
|
||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
||||
if pdfimage['/Subtype'] != '/Image':
|
||||
continue
|
||||
if '/ImageMask' in pdfimage:
|
||||
if pdfimage['/ImageMask']:
|
||||
continue
|
||||
image = {}
|
||||
image['name'] = str(xobj)
|
||||
image['width'] = pdfimage['/Width']
|
||||
image['height'] = pdfimage['/Height']
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
@@ -98,12 +230,33 @@ def _find_page_images(page, pageinfo):
|
||||
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
|
||||
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
image['dpi_w'] = image['width'] / pageinfo['width_inches']
|
||||
image['dpi_h'] = image['height'] / pageinfo['height_inches']
|
||||
image['dpi_w'] = image['dpi_h'] = 0
|
||||
|
||||
for raster in contentsinfo.raster_settings:
|
||||
# Loop in case the same image is display multiple times on a page
|
||||
if raster[0] != image['name']:
|
||||
continue
|
||||
shorthand = raster[1]
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
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)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo, contentsinfo):
|
||||
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
|
||||
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
@@ -136,17 +289,19 @@ def _pdf_get_pageinfo(infile, pageno: int):
|
||||
|
||||
pageinfo['has_text'] = _page_has_text(pdf, page)
|
||||
|
||||
width_pt = page['/MediaBox'][2] - page['/MediaBox'][0]
|
||||
height_pt = page['/MediaBox'][3] - page['/MediaBox'][1]
|
||||
width_pt = page.mediaBox.getWidth()
|
||||
height_pt = page.mediaBox.getHeight()
|
||||
pageinfo['width_inches'] = width_pt / Decimal(72.0)
|
||||
pageinfo['height_inches'] = height_pt / Decimal(72.0)
|
||||
|
||||
pageinfo['images'] = [im for im in _find_page_images(page, pageinfo)]
|
||||
try:
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
except AttributeError as e:
|
||||
return pageinfo
|
||||
|
||||
# Look for inline images
|
||||
if _page_has_inline_images(page):
|
||||
raise NotImplementedError(
|
||||
"Warning: input PDF contains inline images - not supported")
|
||||
contentsinfo = _interpret_contents(contentstream)
|
||||
pageinfo['images'] = [im for im in _find_page_images(
|
||||
page, pageinfo, contentsinfo)]
|
||||
|
||||
if pageinfo['images']:
|
||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||
@@ -164,3 +319,17 @@ def pdf_get_all_pageinfo(infile):
|
||||
pdf = pypdf.PdfFileReader(infile)
|
||||
getcontext().prec = 6
|
||||
return [_pdf_get_pageinfo(infile, n) for n in range(pdf.numPages)]
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('infile')
|
||||
args = parser.parse_args()
|
||||
info = pdf_get_all_pageinfo(args.infile)
|
||||
from pprint import pprint
|
||||
pprint(info)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+7
-33
@@ -5,10 +5,13 @@
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
from string import Template
|
||||
from subprocess import Popen, PIPE
|
||||
import os
|
||||
import codecs
|
||||
from . import get_program
|
||||
import pkg_resources
|
||||
|
||||
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
|
||||
|
||||
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
|
||||
'ocrmypdf', ICC_PROFILE_RELPATH)
|
||||
|
||||
|
||||
# This is a template written in PostScript which is needed to create PDF/A
|
||||
@@ -93,38 +96,9 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
||||
return result
|
||||
|
||||
|
||||
def _get_postscript_icc_path():
|
||||
"Parse Ghostscript's help message to find where iccprofiles are stored"
|
||||
|
||||
p_gs = Popen([get_program('gs'), '--help'], close_fds=True,
|
||||
universal_newlines=True,
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
out, _ = p_gs.communicate()
|
||||
lines = out.splitlines()
|
||||
|
||||
def search_paths(lines):
|
||||
seeking = True
|
||||
for line in lines:
|
||||
if seeking:
|
||||
if line.startswith('Search path'):
|
||||
seeking = False
|
||||
continue
|
||||
else:
|
||||
if line.strip().startswith('/'):
|
||||
yield from (
|
||||
path.strip() for path in line.split(':')
|
||||
if path.strip() != '')
|
||||
for root in search_paths(lines):
|
||||
path = os.path.realpath(os.path.join(root, '../iccprofiles'))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
raise FileNotFoundError("Could not find Ghostscript's iccprofiles")
|
||||
|
||||
|
||||
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
|
||||
if icc == 'sRGB':
|
||||
icc_profile = os.path.join(_get_postscript_icc_path(), 'srgb.icc')
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
else:
|
||||
raise NotImplementedError("Only supporting sRGB")
|
||||
|
||||
|
||||
+22
-19
@@ -19,15 +19,15 @@ def check(input_file, log):
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2:
|
||||
print("{0}: not a valid PDF, and could not repair it.".format(
|
||||
log.error("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
print("Details:")
|
||||
print(e.output)
|
||||
log.error("Details:")
|
||||
log.error(e.output)
|
||||
elif e.returncode == 3:
|
||||
log.info("qpdf --check returned warnings:")
|
||||
log.info(e.output)
|
||||
else:
|
||||
print(e.output)
|
||||
log.warning(e.output)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -40,32 +40,35 @@ def repair(input_file, output_file, log):
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 3 and e.output.find("operation succeeded"):
|
||||
log.debug('qpdf found and fixed errors:')
|
||||
log.debug('qpdf found and fixed errors: ' + e.output)
|
||||
log.debug(e.output)
|
||||
print(e.output)
|
||||
return
|
||||
|
||||
if e.returncode == 2 and e.output.find("invalid password"):
|
||||
print("{0}: this PDF is password-protected - password must "
|
||||
"be removed for OCR".format(input_file))
|
||||
log.error("{0}: this PDF is password-protected - password must "
|
||||
"be removed for OCR".format(input_file))
|
||||
sys.exit(ExitCode.input_file)
|
||||
elif e.returncode == 2:
|
||||
print("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
print("Details:")
|
||||
print(e.output)
|
||||
log.error("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
log.error("Details: " + e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
else:
|
||||
print("{0}: unknown error".format(
|
||||
input_file))
|
||||
print(e.output)
|
||||
log.error("{0}: unknown error".format(
|
||||
input_file))
|
||||
log.error(e.output)
|
||||
sys.exit(ExitCode.unknown)
|
||||
|
||||
|
||||
def get_npages(input_file):
|
||||
pages = check_output(
|
||||
[get_program('qpdf'), '--show-npages', input_file],
|
||||
universal_newlines=True, close_fds=True)
|
||||
def get_npages(input_file, log):
|
||||
try:
|
||||
pages = check_output(
|
||||
[get_program('qpdf'), '--show-npages', input_file],
|
||||
universal_newlines=True, close_fds=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2 and e.output.find('No such file'):
|
||||
log.error(e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
return int(pages)
|
||||
|
||||
|
||||
|
||||
+108
-29
@@ -6,7 +6,8 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from . import ExitCode, get_program
|
||||
from . import ExitCode, get_program, page_number
|
||||
from collections import namedtuple
|
||||
|
||||
from subprocess import Popen, PIPE, CalledProcessError, \
|
||||
TimeoutExpired, check_output, STDOUT
|
||||
@@ -16,6 +17,10 @@ except ImportError:
|
||||
DEVNULL = open(os.devnull, 'wb')
|
||||
|
||||
|
||||
OrientationConfidence = namedtuple(
|
||||
'OrientationConfidence',
|
||||
('angle', 'confidence'))
|
||||
|
||||
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
@@ -76,6 +81,83 @@ def languages():
|
||||
return set(lang.strip() for lang in langs.splitlines()[1:])
|
||||
|
||||
|
||||
def get_orientation(input_file, language: list, timeout: float, log):
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language),
|
||||
'-psm', '0',
|
||||
input_file,
|
||||
'stdout'
|
||||
]
|
||||
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
return OrientationConfidence(angle=0, confidence=0.0)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if ('Too few characters. Skipping this page' in e.output or
|
||||
'Image too large' in e.output):
|
||||
return OrientationConfidence(0, 0)
|
||||
raise e from e
|
||||
else:
|
||||
osd = {}
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
parts = line.split(':', maxsplit=2)
|
||||
if len(parts) == 2:
|
||||
osd[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
angle = int(osd.get('Orientation in degrees', 0))
|
||||
if 'Orientation' in osd:
|
||||
# Tesseract < 3.04.01
|
||||
# reports "Orientation in degrees" as a counterclockwise angle
|
||||
# We keep it clockwise
|
||||
assert 'Rotate' not in osd
|
||||
angle = -angle % 360
|
||||
else:
|
||||
# Tesseract == 3.04.01, hopefully also Tesseract > 3.04.01
|
||||
# reports "Orientation in degrees" as a clockwise angle
|
||||
assert 'Rotate' in osd
|
||||
|
||||
oc = OrientationConfidence(
|
||||
angle=angle,
|
||||
confidence=float(osd.get('Orientation confidence', 0)))
|
||||
return oc
|
||||
|
||||
|
||||
def tesseract_log_output(log, stdout, input_file):
|
||||
lines = stdout.splitlines()
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
for line in lines:
|
||||
if line.startswith("Tesseract Open Source"):
|
||||
continue
|
||||
elif line.startswith("Warning in pixReadMem"):
|
||||
continue
|
||||
elif 'diacritics' in line:
|
||||
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
||||
elif line.startswith('OSD: Weak margin'):
|
||||
log.warning(prefix + "unsure about page orientation")
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
else:
|
||||
log.info(prefix + line.strip())
|
||||
|
||||
|
||||
def page_timedout(log, input_file):
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, pageinfo):
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
timeout: float, pageinfo_getter, pagesegmode: int, log):
|
||||
|
||||
@@ -94,30 +176,25 @@ def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
badxml,
|
||||
'hocr'
|
||||
] + tessconfig)
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
try:
|
||||
stdout, stderr = p.communicate(timeout=timeout)
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
stdout, stderr = p.communicate()
|
||||
# Generate a HOCR file with no recognized text if tesseract times out
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
pageinfo = pageinfo_getter()
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
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())
|
||||
return
|
||||
|
||||
raise e from e
|
||||
else:
|
||||
if stdout:
|
||||
log.info(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise CalledProcessError(p.returncode, args_tesseract)
|
||||
|
||||
tesseract_log_output(log, stdout, input_file)
|
||||
if os.path.exists(badxml + '.html'):
|
||||
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
|
||||
shutil.move(badxml + '.html', badxml)
|
||||
@@ -165,17 +242,19 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
] + tessconfig)
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
|
||||
try:
|
||||
stdout, stderr = p.communicate(timeout=timeout)
|
||||
if stdout:
|
||||
log.info(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
log.info("Tesseract - page timed out")
|
||||
page_timedout(log, input_image)
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_image)
|
||||
if 'Image too large' in e.output:
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
return
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_image)
|
||||
|
||||
+15
-14
@@ -3,7 +3,7 @@
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import CalledProcessError, STDOUT, check_output, check_call
|
||||
from tempfile import NamedTemporaryFile
|
||||
import sys
|
||||
import os
|
||||
@@ -17,10 +17,9 @@ def version():
|
||||
get_program('unpaper'),
|
||||
'--version'
|
||||
]
|
||||
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
version, _ = p_unpaper.communicate(timeout=5)
|
||||
|
||||
version = check_output(
|
||||
args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT, timeout=5)
|
||||
return version.strip()
|
||||
|
||||
|
||||
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
|
||||
os.unlink(output_pnm.name)
|
||||
|
||||
args_unpaper.extend([input_pnm.name, output_pnm.name])
|
||||
p_unpaper = Popen(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stdout=PIPE, stderr=PIPE
|
||||
)
|
||||
out, err = p_unpaper.communicate()
|
||||
log.debug(out)
|
||||
log.debug(err)
|
||||
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stderr=STDOUT,
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
log.debug(e.output)
|
||||
raise e from e
|
||||
else:
|
||||
log.debug(stdout)
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
|
||||
|
||||
def deskew(input_file, output_file, dpi, log):
|
||||
|
||||
+236
-205
@@ -4,263 +4,294 @@
|
||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
||||
-->
|
||||
<!-- Title: Pipeline: Pages: 1 -->
|
||||
<svg width="1132pt" height="708pt"
|
||||
viewBox="0.00 0.00 1132.00 708.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 704.083)">
|
||||
<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)">
|
||||
<title>Pipeline:</title>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-704.083 1128,-704.083 1128,4 -4,4"/>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-820.083 1448,-820.083 1448,4 -4,4"/>
|
||||
<g id="clust1" class="cluster"><title>clustertasks</title>
|
||||
<polygon fill="none" stroke="black" points="8,-8 8,-692.083 1116,-692.083 1116,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="562" y="-664.083" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
|
||||
<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>
|
||||
</g>
|
||||
<!-- t0 -->
|
||||
<g id="node1" class="node"><title>t0</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="936.535,-646.083 713.465,-646.083 709.465,-642.083 709.465,-610.083 932.535,-610.083 936.535,-614.083 936.535,-646.083"/>
|
||||
<polyline fill="none" stroke="black" points="932.535,-642.083 709.465,-642.083 "/>
|
||||
<polyline fill="none" stroke="black" points="932.535,-642.083 932.535,-610.083 "/>
|
||||
<polyline fill="none" stroke="black" points="932.535,-642.083 936.535,-646.083 "/>
|
||||
<text text-anchor="middle" x="823" y="-622.083" font-family="Times,serif" font-size="20.00">repair_pdf</text>
|
||||
<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>
|
||||
</g>
|
||||
<!-- t1 -->
|
||||
<g id="node2" class="node"><title>t1</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="914.112,-567.155 710,-584.057 505.888,-567.155 506.078,-539.806 913.922,-539.806 914.112,-567.155"/>
|
||||
<polygon fill="none" stroke="black" points="918.134,-570.834 710,-588.069 501.866,-570.834 502.11,-535.808 917.89,-535.808 918.134,-570.834"/>
|
||||
<text text-anchor="middle" x="710" y="-553.596" font-family="Times,serif" font-size="20.00">split_pages</text>
|
||||
<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>
|
||||
</g>
|
||||
<!-- t0->t1 -->
|
||||
<g id="edge1" class="edge"><title>t0->t1</title>
|
||||
<path fill="none" stroke="#0044a0" d="M793.9,-609.961C783.582,-603.89 771.656,-596.873 760.092,-590.069"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="761.747,-586.982 751.353,-584.927 758.197,-593.015 761.747,-586.982"/>
|
||||
<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>
|
||||
<!-- t12 -->
|
||||
<g id="node14" class="node"><title>t12</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1108.08,-509.109 769.918,-509.109 765.918,-505.109 765.918,-473.109 1104.08,-473.109 1108.08,-477.109 1108.08,-509.109"/>
|
||||
<polyline fill="none" stroke="black" points="1104.08,-505.109 765.918,-505.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1104.08,-505.109 1104.08,-473.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1104.08,-505.109 1108.08,-509.109 "/>
|
||||
<text text-anchor="middle" x="937" y="-485.109" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
|
||||
<!-- 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->t12 -->
|
||||
<g id="edge19" class="edge"><title>t0->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M899.32,-610.004C909.979,-604.592 919.748,-597.466 927,-588.083 941.916,-568.78 943.099,-540.385 941.345,-519.486"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="944.8,-518.881 940.218,-509.328 937.843,-519.653 944.8,-518.881"/>
|
||||
<!-- 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"/>
|
||||
</g>
|
||||
<!-- t2 -->
|
||||
<g id="node3" class="node"><title>t2</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="592.299,-509.109 241.701,-509.109 237.701,-505.109 237.701,-473.109 588.299,-473.109 592.299,-477.109 592.299,-509.109"/>
|
||||
<polyline fill="none" stroke="black" points="588.299,-505.109 237.701,-505.109 "/>
|
||||
<polyline fill="none" stroke="black" points="588.299,-505.109 588.299,-473.109 "/>
|
||||
<polyline fill="none" stroke="black" points="588.299,-505.109 592.299,-509.109 "/>
|
||||
<text text-anchor="middle" x="415" y="-485.109" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
|
||||
<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>
|
||||
</g>
|
||||
<!-- t1->t2 -->
|
||||
<g id="edge2" class="edge"><title>t1->t2</title>
|
||||
<path fill="none" stroke="#0044a0" d="M608.89,-535.808C573.672,-527.87 534.524,-519.048 500.679,-511.42"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="501.345,-507.982 490.82,-509.198 499.806,-514.811 501.345,-507.982"/>
|
||||
<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"/>
|
||||
</g>
|
||||
<!-- t7 -->
|
||||
<g id="node9" class="node"><title>t7</title>
|
||||
<!-- 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"/>
|
||||
</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"/>
|
||||
</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>
|
||||
</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>
|
||||
<!-- t1->t7 -->
|
||||
<g id="edge11" class="edge"><title>t1->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M501.985,-548.028C416.651,-540.897 317.351,-528.985 229,-509.109 131.492,-487.174 17,-534.054 17,-434.109 17,-434.109 17,-434.109 17,-374.109 17,-340.481 4.97908,-324.525 27,-299.109 32.8004,-292.415 39.6539,-286.828 47.1559,-282.17"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="49.1993,-285.038 56.2361,-277.118 45.7959,-278.921 49.1993,-285.038"/>
|
||||
<!-- 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="node12" class="node"><title>t13</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1029.34,-451.109 808.662,-451.109 804.662,-447.109 804.662,-415.109 1025.34,-415.109 1029.34,-419.109 1029.34,-451.109"/>
|
||||
<polyline fill="none" stroke="black" points="1025.34,-447.109 804.662,-447.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1025.34,-447.109 1025.34,-415.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1025.34,-447.109 1029.34,-451.109 "/>
|
||||
<text text-anchor="middle" x="917" y="-427.109" font-family="Times,serif" font-size="20.00">skip_page</text>
|
||||
<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>
|
||||
<!-- t1->t13 -->
|
||||
<g id="edge16" class="edge"><title>t1->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M716.849,-535.484C723.825,-515.965 736.562,-488.763 757,-473.109 768.342,-464.422 781.352,-457.641 794.947,-452.352"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="796.223,-455.612 804.44,-448.923 793.845,-449.029 796.223,-455.612"/>
|
||||
</g>
|
||||
<!-- t11 -->
|
||||
<g id="node13" class="node"><title>t11</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1068.24,-335.109 687.76,-335.109 683.76,-331.109 683.76,-299.109 1064.24,-299.109 1068.24,-303.109 1068.24,-335.109"/>
|
||||
<polyline fill="none" stroke="black" points="1064.24,-331.109 683.76,-331.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1064.24,-331.109 1064.24,-299.109 "/>
|
||||
<polyline fill="none" stroke="black" points="1064.24,-331.109 1068.24,-335.109 "/>
|
||||
<text text-anchor="middle" x="876" y="-311.109" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
|
||||
</g>
|
||||
<!-- t1->t11 -->
|
||||
<g id="edge18" class="edge"><title>t1->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M710.717,-535.657C712.098,-517.744 715.879,-492.693 726,-473.109 754.651,-417.673 809.56,-368.731 844.403,-341.318"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="846.607,-344.038 852.372,-335.148 842.322,-338.503 846.607,-344.038"/>
|
||||
</g>
|
||||
<!-- t3 -->
|
||||
<g id="node4" class="node"><title>t3</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="564.742,-451.109 269.258,-451.109 265.258,-447.109 265.258,-415.109 560.742,-415.109 564.742,-419.109 564.742,-451.109"/>
|
||||
<polyline fill="none" stroke="black" points="560.742,-447.109 265.258,-447.109 "/>
|
||||
<polyline fill="none" stroke="black" points="560.742,-447.109 560.742,-415.109 "/>
|
||||
<polyline fill="none" stroke="black" points="560.742,-447.109 564.742,-451.109 "/>
|
||||
<text text-anchor="middle" x="415" y="-427.109" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
|
||||
</g>
|
||||
<!-- t2->t3 -->
|
||||
<g id="edge3" class="edge"><title>t2->t3</title>
|
||||
<path fill="none" stroke="#0044a0" d="M415,-473.003C415,-469.312 415,-465.322 415,-461.352"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="418.5,-461.111 415,-451.111 411.5,-461.111 418.5,-461.111"/>
|
||||
</g>
|
||||
<!-- t6 -->
|
||||
<g id="node8" class="node"><title>t6</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="354.119,-335.109 39.8808,-335.109 35.8808,-331.109 35.8808,-299.109 350.119,-299.109 354.119,-303.109 354.119,-335.109"/>
|
||||
<polyline fill="none" stroke="black" points="350.119,-331.109 35.8808,-331.109 "/>
|
||||
<polyline fill="none" stroke="black" points="350.119,-331.109 350.119,-299.109 "/>
|
||||
<polyline fill="none" stroke="black" points="350.119,-331.109 354.119,-335.109 "/>
|
||||
<text text-anchor="middle" x="195" y="-311.109" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
|
||||
</g>
|
||||
<!-- t2->t6 -->
|
||||
<g id="edge9" class="edge"><title>t2->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M294.315,-473.06C280.461,-467.585 267.283,-460.434 256,-451.109 223.245,-424.039 207.251,-375.559 200.099,-345.207"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="203.486,-344.31 197.925,-335.292 196.648,-345.809 203.486,-344.31"/>
|
||||
</g>
|
||||
<!-- t4 -->
|
||||
<g id="node5" class="node"><title>t4</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="587.95,-393.109 310.05,-393.109 306.05,-389.109 306.05,-357.109 583.95,-357.109 587.95,-361.109 587.95,-393.109"/>
|
||||
<polyline fill="none" stroke="black" points="583.95,-389.109 306.05,-389.109 "/>
|
||||
<polyline fill="none" stroke="black" points="583.95,-389.109 583.95,-357.109 "/>
|
||||
<polyline fill="none" stroke="black" points="583.95,-389.109 587.95,-393.109 "/>
|
||||
<text text-anchor="middle" x="447" y="-369.109" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
|
||||
</g>
|
||||
<!-- t3->t4 -->
|
||||
<g id="edge4" class="edge"><title>t3->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M424.775,-415.003C427.132,-410.878 429.703,-406.379 432.232,-401.952"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="435.362,-403.53 437.285,-393.111 429.285,-400.057 435.362,-403.53"/>
|
||||
</g>
|
||||
<!-- t3->t6 -->
|
||||
<g id="edge8" class="edge"><title>t3->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M351.041,-415.02C333.086,-409.151 313.871,-401.82 297,-393.109 269.828,-379.08 242.065,-358.149 222.377,-341.96"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="224.378,-339.071 214.46,-335.347 219.891,-344.444 224.378,-339.071"/>
|
||||
<!-- 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>
|
||||
<!-- t5 -->
|
||||
<g id="node6" class="node"><title>t5</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="665.666,-335.109 376.334,-335.109 372.334,-331.109 372.334,-299.109 661.666,-299.109 665.666,-303.109 665.666,-335.109"/>
|
||||
<polyline fill="none" stroke="black" points="661.666,-331.109 372.334,-331.109 "/>
|
||||
<polyline fill="none" stroke="black" points="661.666,-331.109 661.666,-299.109 "/>
|
||||
<polyline fill="none" stroke="black" points="661.666,-331.109 665.666,-335.109 "/>
|
||||
<text text-anchor="middle" x="519" y="-311.109" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
|
||||
<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>
|
||||
</g>
|
||||
<!-- t4->t5 -->
|
||||
<g id="edge5" class="edge"><title>t4->t5</title>
|
||||
<path fill="none" stroke="#0044a0" d="M468.994,-357.003C475.274,-352.118 482.229,-346.709 488.905,-341.516"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="491.396,-344.013 497.141,-335.111 487.099,-338.487 491.396,-344.013"/>
|
||||
</g>
|
||||
<!-- t4->t6 -->
|
||||
<g id="edge7" class="edge"><title>t4->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M370.365,-357.079C342.386,-350.862 310.55,-343.787 281.752,-337.388"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="282.225,-333.907 271.704,-335.155 280.707,-340.741 282.225,-333.907"/>
|
||||
</g>
|
||||
<!-- t4->t11 -->
|
||||
<g id="edge17" class="edge"><title>t4->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M577.461,-357.079C627.38,-350.563 684.512,-343.106 735.342,-336.47"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="735.957,-339.92 745.42,-335.155 735.051,-332.979 735.957,-339.92"/>
|
||||
<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"/>
|
||||
</g>
|
||||
<!-- t8 -->
|
||||
<g id="node7" class="node"><title>t8</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="986.109,-277.109 701.891,-277.109 697.891,-273.109 697.891,-241.109 982.109,-241.109 986.109,-245.109 986.109,-277.109"/>
|
||||
<polyline fill="none" stroke="black" points="982.109,-273.109 697.891,-273.109 "/>
|
||||
<polyline fill="none" stroke="black" points="982.109,-273.109 982.109,-241.109 "/>
|
||||
<polyline fill="none" stroke="black" points="982.109,-273.109 986.109,-277.109 "/>
|
||||
<text text-anchor="middle" x="842" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
|
||||
<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>
|
||||
</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"/>
|
||||
</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>
|
||||
</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"/>
|
||||
</g>
|
||||
<!-- t5->t8 -->
|
||||
<g id="edge6" class="edge"><title>t5->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M617.226,-299.079C654.028,-292.699 696.036,-285.416 733.699,-278.886"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="734.429,-282.312 743.685,-277.155 733.234,-275.415 734.429,-282.312"/>
|
||||
<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"/>
|
||||
</g>
|
||||
<!-- t9 -->
|
||||
<g id="node11" class="node"><title>t9</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="679.486,-277.109 336.514,-277.109 332.514,-273.109 332.514,-241.109 675.486,-241.109 679.486,-245.109 679.486,-277.109"/>
|
||||
<polyline fill="none" stroke="black" points="675.486,-273.109 332.514,-273.109 "/>
|
||||
<polyline fill="none" stroke="black" points="675.486,-273.109 675.486,-241.109 "/>
|
||||
<polyline fill="none" stroke="black" points="675.486,-273.109 679.486,-277.109 "/>
|
||||
<text text-anchor="middle" x="506" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
|
||||
</g>
|
||||
<!-- t5->t9 -->
|
||||
<g id="edge15" class="edge"><title>t5->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M515.029,-299.003C514.147,-295.204 513.191,-291.087 512.243,-287.002"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="515.617,-286.06 509.947,-277.111 508.799,-287.643 515.617,-286.06"/>
|
||||
</g>
|
||||
<!-- t10 -->
|
||||
<g id="node10" class="node"><title>t10</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="973.082,-219.109 714.918,-219.109 710.918,-215.109 710.918,-183.109 969.082,-183.109 973.082,-187.109 973.082,-219.109"/>
|
||||
<polyline fill="none" stroke="black" points="969.082,-215.109 710.918,-215.109 "/>
|
||||
<polyline fill="none" stroke="black" points="969.082,-215.109 969.082,-183.109 "/>
|
||||
<polyline fill="none" stroke="black" points="969.082,-215.109 973.082,-219.109 "/>
|
||||
<text text-anchor="middle" x="842" y="-195.109" font-family="Times,serif" font-size="20.00">add_text_layer</text>
|
||||
</g>
|
||||
<!-- t8->t10 -->
|
||||
<g id="edge12" class="edge"><title>t8->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M842,-241.003C842,-237.312 842,-233.322 842,-229.352"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="845.5,-229.111 842,-219.111 838.5,-229.111 845.5,-229.111"/>
|
||||
<!-- 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>
|
||||
</g>
|
||||
<!-- t6->t7 -->
|
||||
<g id="edge10" class="edge"><title>t6->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M185.836,-299.003C183.626,-294.878 181.216,-290.379 178.845,-285.952"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="181.915,-284.273 174.108,-277.111 175.745,-287.578 181.915,-284.273"/>
|
||||
<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"/>
|
||||
</g>
|
||||
<!-- t6->t9 -->
|
||||
<g id="edge14" class="edge"><title>t6->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M289.577,-299.079C324.86,-292.726 365.115,-285.478 401.259,-278.969"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="402.116,-282.372 411.337,-277.155 400.875,-275.482 402.116,-282.372"/>
|
||||
<!-- 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"/>
|
||||
</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>
|
||||
</g>
|
||||
<!-- t7->t10 -->
|
||||
<g id="edge13" class="edge"><title>t7->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M314.366,-242.002C317.606,-241.697 320.821,-241.399 324,-241.109 451.228,-229.521 596.262,-218.816 700.44,-211.565"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="700.976,-215.036 710.71,-210.852 700.491,-208.053 700.976,-215.036"/>
|
||||
<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"/>
|
||||
</g>
|
||||
<!-- t14 -->
|
||||
<g id="node15" class="node"><title>t14</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="774.472,-105.333 939,-78.005 1103.53,-105.333 1103.37,-149.551 774.625,-149.551 774.472,-105.333"/>
|
||||
<polygon fill="none" stroke="black" points="770.46,-101.94 939,-73.9453 1107.54,-101.94 1107.36,-153.556 770.639,-153.556 770.46,-101.94"/>
|
||||
<text text-anchor="middle" x="939" y="-111.555" font-family="Times,serif" font-size="20.00">merge_pages</text>
|
||||
<!-- 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>
|
||||
<!-- t10->t14 -->
|
||||
<g id="edge23" class="edge"><title>t10->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M862.571,-182.814C870.479,-176.165 879.888,-168.254 889.356,-160.293"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="891.89,-162.736 897.292,-153.622 887.385,-157.378 891.89,-162.736"/>
|
||||
<!-- 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>
|
||||
<!-- t9->t14 -->
|
||||
<g id="edge24" class="edge"><title>t9->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M546.87,-241.103C586.226,-225.054 647.626,-200.869 702,-183.109 730.59,-173.771 761.415,-164.679 791.074,-156.405"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="792.38,-159.675 801.082,-153.632 790.511,-152.93 792.38,-159.675"/>
|
||||
<!-- 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>
|
||||
<!-- t13->t14 -->
|
||||
<g id="edge20" class="edge"><title>t13->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M986.636,-414.956C1033.58,-398.748 1087,-369.103 1087,-318.109 1087,-318.109 1087,-318.109 1087,-258.109 1087,-215.948 1054.81,-182.761 1020.46,-159.346"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1021.98,-156.158 1011.7,-153.604 1018.15,-162.013 1021.98,-156.158"/>
|
||||
<!-- 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>
|
||||
<!-- t11->t14 -->
|
||||
<g id="edge22" class="edge"><title>t11->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M969.056,-299.033C979.177,-293.579 988.239,-286.44 995,-277.109 1020.03,-242.568 998.243,-195.803 974.929,-162.021"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="977.661,-159.825 968.996,-153.728 971.968,-163.897 977.661,-159.825"/>
|
||||
<!-- 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>
|
||||
<!-- t12->t14 -->
|
||||
<g id="edge21" class="edge"><title>t12->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1006.64,-472.956C1053.58,-456.748 1107,-427.103 1107,-376.109 1107,-376.109 1107,-376.109 1107,-258.109 1107,-220.742 1095.53,-209.426 1069,-183.109 1059.8,-173.988 1049.06,-165.927 1037.76,-158.869"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1039.41,-155.777 1029.03,-153.665 1035.83,-161.791 1039.41,-155.777"/>
|
||||
<!-- 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>
|
||||
<!-- t15 -->
|
||||
<g id="node16" class="node"><title>t15</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1053.18,-52 828.822,-52 824.822,-48 824.822,-16 1049.18,-16 1053.18,-20 1053.18,-52"/>
|
||||
<polyline fill="none" stroke="black" points="1049.18,-48 824.822,-48 "/>
|
||||
<polyline fill="none" stroke="black" points="1049.18,-48 1049.18,-16 "/>
|
||||
<polyline fill="none" stroke="black" points="1049.18,-48 1053.18,-52 "/>
|
||||
<text text-anchor="middle" x="939" y="-28" font-family="Times,serif" font-size="20.00">copy_final</text>
|
||||
<!-- 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"/>
|
||||
</g>
|
||||
<!-- t14->t15 -->
|
||||
<g id="edge25" class="edge"><title>t14->t15</title>
|
||||
<path fill="none" stroke="#0044a0" d="M939,-73.8665C939,-69.8921 939,-65.942 939,-62.1676"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="942.5,-62.1213 939,-52.1214 935.5,-62.1214 942.5,-62.1213"/>
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
</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"/>
|
||||
</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>
|
||||
</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>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
norecursedirs = lib
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
ruffus>=2.6.3
|
||||
Pillow>=2.4.0
|
||||
reportlab>=3.1.44
|
||||
PyPDF2>=1.25.1
|
||||
git+https://github.com/jbarlow83/img2pdf.git@e9bcce0afc3720752ca53a991db93f911a1df709#egg=img2pdf-0.1.5.dev
|
||||
ruffus==2.6.3
|
||||
Pillow==3.1.1
|
||||
reportlab==3.2.0
|
||||
PyPDF2==1.25.1
|
||||
img2pdf==0.2
|
||||
cffi==1.5.2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[bdist_wheel]
|
||||
python-tag = py34
|
||||
[pytest]
|
||||
norecursedirs = lib
|
||||
[aliases]
|
||||
test=pytest
|
||||
@@ -1,18 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
from setuptools import setup
|
||||
from subprocess import STDOUT, check_output, CalledProcessError
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info < (3, 4):
|
||||
print("Python 3.4 or newer is required")
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup # nopep8
|
||||
from subprocess import STDOUT, check_output, CalledProcessError # nopep8
|
||||
from collections.abc import Mapping # nopep8
|
||||
import re # nopep8
|
||||
|
||||
|
||||
missing_program = '''
|
||||
The program '{program}' could not be executed or was not found on your
|
||||
system PATH.
|
||||
@@ -149,7 +151,7 @@ command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
|
||||
|
||||
|
||||
if command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install', 'egg_info']:
|
||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
need_version='3.02.02',
|
||||
@@ -206,15 +208,21 @@ setup(
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
],
|
||||
setup_requires=[
|
||||
'setuptools_scm'
|
||||
'setuptools_scm',
|
||||
'cffi>=1.5.0',
|
||||
'pytest-runner'
|
||||
],
|
||||
use_scm_version={'version_scheme': 'post-release'},
|
||||
cffi_modules=[
|
||||
'ocrmypdf/lib/compile_leptonica.py:ffi'
|
||||
],
|
||||
install_requires=[
|
||||
'ruffus',
|
||||
'Pillow',
|
||||
'reportlab',
|
||||
'PyPDF2',
|
||||
'img2pdf'
|
||||
'ruffus==2.6.3',
|
||||
'Pillow>=3.0.0',
|
||||
'reportlab>=3.1.44',
|
||||
'PyPDF2>=1.25.1',
|
||||
'img2pdf>=0.2.0',
|
||||
'cffi>=1.5.0'
|
||||
],
|
||||
tests_require=tests_require,
|
||||
entry_points={
|
||||
@@ -222,5 +230,6 @@ setup(
|
||||
'ocrmypdf = ocrmypdf.main:run_pipeline'
|
||||
],
|
||||
},
|
||||
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
||||
include_package_data=True,
|
||||
zip_safe=False)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
deb http://httpredir.debian.org/debian stretch main
|
||||
deb http://httpredir.debian.org/debian stretch-updates main
|
||||
deb http://security.debian.org stretch/updates main
|
||||
deb http://ftp.de.debian.org/debian sid main contrib non-free
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
pytest>=2.7.2
|
||||
pytest>=2.7.2
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
All test resources must come from free public domain sources for
|
||||
copyright reasons.
|
||||
|
||||
Test files do not necessarily produce perfect (or even good) OCR
|
||||
results.
|
||||
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| File | Source |
|
||||
+=====================+================================================================================+
|
||||
| graph.pdf | Wikimedia |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| c02-22.pdf | Project Gutenberg: https://www.gutenberg.org/files/76/76-h/images/c02-22.jpg |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| LinnSequencer.jpg | Wikimedia_ |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| congress.jpg | http://www.baxleystamps.com/litho/meiji/courts_1871.jpg |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| blank.pdf | Blank page from Adobe Illustrator CC 2015 |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| enormous.pdf | PNG file saved to PDF using img2pdf |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| invalid.pdf | PDF file header followed by EOF marker; not valid |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| multipage.pdf | several other files concatenated |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| skew.pdf | skewed version of c02-22.PDF |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| Test_Issue_28.pdf | file with some syntax errors |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| missing_docinfo.pdf | file missing its DocumentInfo dictionary |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
.. _Wikimedia: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
|
||||
@@ -0,0 +1,66 @@
|
||||
These test files are used in OCRmyPDF's test suite. They do not necessarily produce OCR results
|
||||
at all and are not meant as examples of OCR output. Some are even invalid PDFs that might
|
||||
crash certain PDF viewers.
|
||||
|
||||
|
||||
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`_ |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| graph.pdf | `Wikimedia: Pandas text analysis.png`_ |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| 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 | |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
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.
|
||||
|
||||
- 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)
|
||||
|
||||
|
||||
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)
|
||||
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
||||
- 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)
|
||||
- skew-encrypted.pdf (skew.pdf with encrypted applied)
|
||||
|
||||
|
||||
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
|
||||
|
||||
.. _`Project Gutenberg`: https://www.gutenberg.org/files/76/76-h/76-h.htm#c2
|
||||
|
||||
.. _`US Congressional Records`: http://www.baxleystamps.com/litho/meiji/courts_1871.jpg
|
||||
|
||||
.. _`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
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
The LinnSequencer
|
||||
32 Track MIDI Sequence Recorder
|
||||
|
||||
The LinnSequencer is a state—of—the-art composition and performance tool for the professional musician. It is
|
||||
|
||||
extremely powerful, yet amazingly simple to learn and use. It’s many remarkable features include:
|
||||
|
||||
0 Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
|
||||
FORWARD, REWIND, and LOCATE controls.
|
||||
|
||||
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
|
||||
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
|
||||
|
||||
synthesizers !
|
||||
|
||||
0 Ultra-fast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
|
||||
|
||||
per disk!
|
||||
|
||||
0 One or all tracks may be TRANSPOSED at the touch of a key.
|
||||
0 Exclusive real—time ERASE function makes editing FAST.
|
||||
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
|
||||
|
||||
rhythmic value.
|
||||
|
||||
0 TIMING CORRECTION works during playback and operates without ‘chopping’ notes.
|
||||
|
||||
0 Optional SMPTE time code synchronization.
|
||||
|
||||
0 Optional remote control.
|
||||
|
||||
Recording a Sequence
|
||||
|
||||
To record a sequence, simply press RECORD and PLAY,
|
||||
then play your MIDI keyboard in time to the Sequencer’s
|
||||
click track. When the sequence loops back around to bar 1,
|
||||
you’ll hear what you played—only all timing errors will be
|
||||
|
||||
corrected! (Timing correction may be adjusted 0r defeated).
|
||||
|
||||
Any additional notes played will be added into the track
|
||||
—existing notes are not erased while recording!
|
||||
|
||||
FAST FORWARD, REWIND, and LOCATE controls
|
||||
may be used at any time to quickly access any location in
|
||||
your sequence for spot-recording. To overdub a new part,
|
||||
select a different track and start recording—while you
|
||||
record, the first‘track will play in perfect sync (unless you
|
||||
MUTE it, or SOLO another track). In this way, up to 32
|
||||
tracks may be overdubbed! All MIDI effects are recorded
|
||||
including pitch bend, modulation, velocity, aftertouch,
|
||||
sustain pedal, and program changes!
|
||||
|
||||
Editing
|
||||
|
||||
To erase a wrong note, simply hold ERASE and press
|
||||
the note to be erased just before it plays in the sequence-—
|
||||
when played back, it will be gone. Notes may also be
|
||||
|
||||
added, erased, or changed using the SINGLE STEP func-
|
||||
tion. To overdub notes at specific points within a sequence,
|
||||
|
||||
Additional Features
|
||||
|
||||
simply use LOCATE, FAST FORWARD, or REWIND to
|
||||
find the desired bar number, then start recording.
|
||||
|
||||
The INSERT/ COPY function allows you to move bars
|
||||
from one location to another—in the same sequence or a
|
||||
different one. For example, you might insert a copy of the
|
||||
first verse between the second chorus and the bridge.
|
||||
DELETE BARS operates the same way to remove
|
||||
unwanted sections.
|
||||
|
||||
Creating a Song
|
||||
|
||||
One way to create a song is to record each track all the
|
||||
way through (up to 999 bars). Another way is to record
|
||||
each basic section (verse, chorus, etc.) in individual
|
||||
sequences, then use the CREATE SONG function to “chain”
|
||||
them together. CREATE SONG will then automatically
|
||||
copy all the parts into a new sequence. If desired, you can
|
||||
even set the last few bars to repeat infinitely, for a fadeout.
|
||||
|
||||
Composition Without Compromise
|
||||
|
||||
The technology you use should never be so complex that
|
||||
it interferes with the creative process. That’s precisely why
|
||||
the LinnSequencer is designed to let you compose, record
|
||||
and edit while devoting your undivided attention to your
|
||||
music. See your Linn dealer today for a demonstration!
|
||||
|
||||
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
|
||||
|
||||
HELP button displays additional explanations.
|
||||
|
||||
0 Non-destructive recording—existing notes are not erased while recording.
|
||||
0 Two FOOTSWIT CH INPUTS may be assigned to remotely control many of the commonly used functions, including
|
||||
|
||||
ERASE, REPEAT, PLAY/ STOP, or LOCATE.
|
||||
|
||||
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
|
||||
|
||||
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
|
||||
|
||||
0 Utilizes ultra high—speed, 8 MHZ 80186 16 bit computer internally for FAST operation.
|
||||
0 TEMPO may be specified in BEATS-PER—MINUTE or FRAMES-PER—BEAT at 24, 25, or 30 frames per second,
|
||||
|
||||
(even drop frame!)
|
||||
|
||||
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
|
||||
|
||||
on the TAP TEMPO button.
|
||||
|
||||
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
|
||||
0 Any TIME SIGNATURE may be used, and may be changed within a song.
|
||||
|
||||
EDI]
|
||||
Linn Electronics, Inc.
|
||||
|
||||
18720 Oxnard Street, Tarzana, CA 91356
|
||||
(818) 708-8131 TELEX #298949 LINN UR
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -10,12 +10,16 @@ CACHE_PATH = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', 'cache'))
|
||||
|
||||
|
||||
def real_tesseract():
|
||||
tess_args = ['tesseract'] + sys.argv[1:]
|
||||
os.execvp("tesseract", tess_args)
|
||||
return # Not reachable
|
||||
|
||||
def main():
|
||||
operation = sys.argv[-1]
|
||||
# For anything except a hocr or pdf, defer to real tesseract
|
||||
if operation != 'hocr' and operation != 'pdf':
|
||||
tess_args = ['tesseract'] + sys.argv[1:]
|
||||
os.execvp("tesseract", tess_args)
|
||||
# For anything unexpected operation, defer to real tesseract binary
|
||||
if operation != 'hocr' and operation != 'pdf' and operation != 'stdout':
|
||||
real_tesseract()
|
||||
return # Not reachable
|
||||
|
||||
try:
|
||||
@@ -43,8 +47,16 @@ def main():
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
input_file = sys.argv[-3]
|
||||
output_file = sys.argv[-2]
|
||||
if operation == 'stdout' and psm != '0':
|
||||
real_tesseract()
|
||||
return
|
||||
|
||||
if operation == 'stdout':
|
||||
input_file = sys.argv[-2]
|
||||
output_file = 'stdout'
|
||||
else:
|
||||
input_file = sys.argv[-3]
|
||||
output_file = sys.argv[-2]
|
||||
|
||||
if operation == 'hocr':
|
||||
output_file += '.hocr'
|
||||
@@ -53,25 +65,53 @@ def main():
|
||||
|
||||
with open(input_file, 'rb') as f:
|
||||
m.update(f.read())
|
||||
|
||||
cache_name = os.path.join(CACHE_PATH, m.hexdigest())
|
||||
print(cache_name)
|
||||
if os.path.exists(cache_name):
|
||||
# Cache hit
|
||||
print("Tesseract cache hit", file=sys.stderr)
|
||||
shutil.copy(cache_name, output_file)
|
||||
if operation != 'stdout':
|
||||
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)
|
||||
sys.exit(0)
|
||||
|
||||
# Cache miss
|
||||
print("Tesseract cache miss", file=sys.stderr)
|
||||
|
||||
# Call tesseract
|
||||
subprocess.check_call(['tesseract'] + sys.argv[1:])
|
||||
p = subprocess.Popen(
|
||||
['tesseract'] + sys.argv[1:],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
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)
|
||||
return p.returncode
|
||||
|
||||
with open(cache_name + '.stdout', 'w') as f:
|
||||
f.write(stdout)
|
||||
with open(cache_name + '.stderr', 'w') as f:
|
||||
f.write(stderr)
|
||||
print(stdout, end='')
|
||||
print(stderr, end='', file=sys.stderr)
|
||||
|
||||
# Insert file into cache
|
||||
if os.path.exists(output_file):
|
||||
shutil.copy(output_file, cache_name)
|
||||
if output_file != 'stdout':
|
||||
if os.path.exists(output_file):
|
||||
shutil.copy(output_file, cache_name)
|
||||
else:
|
||||
print("Could not find output file", file=sys.stderr)
|
||||
else:
|
||||
print("Could not find output file", file=sys.stderr)
|
||||
open(cache_name, 'w').close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED: CRASH ON OCR or -psm 0
|
||||
'''
|
||||
|
||||
"""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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
print("libc++abi.dylib: terminating with uncaught exception of type "
|
||||
"std::bad_alloc: std::bad_alloc", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGABRT)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -53,6 +53,13 @@ def main():
|
||||
pdf_bytes = img2pdf.convert([inputf], dpi=300)
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
f.write(pdf_bytes)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
inputf = sys.argv[-2]
|
||||
print("""Orientation: 0
|
||||
Orientation in degrees: 0
|
||||
Orientation confidence: 100.00
|
||||
Script: 1
|
||||
Script confidence: 100.00""", file=sys.stderr)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print('0.5')
|
||||
sys.exit(0)
|
||||
|
||||
print("Only supports --version")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+224
-40
@@ -11,6 +11,7 @@ import pytest
|
||||
from ocrmypdf.pageinfo import pdf_get_all_pageinfo
|
||||
import PyPDF2 as pypdf
|
||||
from ocrmypdf import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
@@ -27,6 +28,11 @@ TEST_OUTPUT = os.environ.get(
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'main'))
|
||||
|
||||
|
||||
def running_in_docker():
|
||||
# Docker creates a file named /.dockerinit
|
||||
return os.path.exists('/.dockerinit')
|
||||
|
||||
|
||||
def setup_module():
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(TEST_OUTPUT)
|
||||
@@ -43,28 +49,33 @@ def run_ocrmypdf_sh(input_file, output_file, *args, env=None):
|
||||
return sh, out, err
|
||||
|
||||
|
||||
def _make_input(input_basename):
|
||||
def _infile(input_basename):
|
||||
return os.path.join(TEST_RESOURCES, input_basename)
|
||||
|
||||
|
||||
def _make_output(output_basename):
|
||||
return os.path.join(TEST_OUTPUT, output_basename)
|
||||
def _outfile(output_basename):
|
||||
return os.path.join(TEST_OUTPUT, os.path.basename(output_basename))
|
||||
|
||||
|
||||
def check_ocrmypdf(input_basename, output_basename, *args, env=None):
|
||||
input_file = _make_input(input_basename)
|
||||
output_file = _make_output(output_basename)
|
||||
input_file = _infile(input_basename)
|
||||
output_file = _outfile(output_basename)
|
||||
|
||||
sh, out, err = run_ocrmypdf_sh(input_file, output_file, *args, env=env)
|
||||
assert sh.returncode == 0, dict(stdout=out, stderr=err)
|
||||
if sh.returncode != 0:
|
||||
print('stdout\n======')
|
||||
print(out)
|
||||
print('stderr\n======')
|
||||
print(err)
|
||||
assert sh.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"
|
||||
return output_file
|
||||
|
||||
|
||||
def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
input_file = _make_input(input_basename)
|
||||
output_file = _make_output(output_basename)
|
||||
input_file = _infile(input_basename)
|
||||
output_file = _outfile(output_basename)
|
||||
|
||||
if env is None:
|
||||
env = os.environ
|
||||
@@ -77,22 +88,41 @@ def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
return p, out, err
|
||||
|
||||
|
||||
def spoof(replace_program, with_spoof):
|
||||
"""Modify environment variables to override subprocess executables
|
||||
|
||||
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
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop():
|
||||
env = os.environ.copy()
|
||||
program = os.path.join(SPOOF_PATH, 'tesseract_noop.py')
|
||||
check_call(['chmod', "+x", program])
|
||||
env['OCRMYPDF_TESSERACT'] = program
|
||||
return env
|
||||
return spoof('tesseract', 'tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache():
|
||||
env = os.environ.copy()
|
||||
program = os.path.join(SPOOF_PATH, "tesseract_cache.py")
|
||||
check_call(['chmod', '+x', program])
|
||||
env['OCRMYPDF_TESSERACT'] = program
|
||||
return env
|
||||
if running_in_docker():
|
||||
return os.environ.copy()
|
||||
return spoof('tesseract', "tesseract_cache.py")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_crash():
|
||||
return spoof('tesseract', 'tesseract_crash.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_big_image_error():
|
||||
return spoof('tesseract', 'tesseract_big_image_error.py')
|
||||
|
||||
|
||||
def test_quick(spoof_tesseract_noop):
|
||||
@@ -102,7 +132,7 @@ def test_quick(spoof_tesseract_noop):
|
||||
def test_deskew(spoof_tesseract_noop):
|
||||
# Run with deskew
|
||||
deskewed_pdf = check_ocrmypdf(
|
||||
'skew.pdf', 'test_deskew.pdf', '-d', env=spoof_tesseract_noop)
|
||||
'skew.pdf', 'test_deskew.pdf', '-d', '-v', '1', env=spoof_tesseract_noop)
|
||||
|
||||
# Now render as an image again and use Leptonica to find the skew angle
|
||||
# to confirm that it was deskewed
|
||||
@@ -110,7 +140,7 @@ def test_deskew(spoof_tesseract_noop):
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
deskewed_png = _make_output('deskewed.png')
|
||||
deskewed_png = _outfile('deskewed.png')
|
||||
|
||||
rasterize_pdf(
|
||||
deskewed_pdf,
|
||||
@@ -120,10 +150,9 @@ def test_deskew(spoof_tesseract_noop):
|
||||
raster_device='pngmono',
|
||||
log=log)
|
||||
|
||||
from ocrmypdf.leptonica import pixRead, pixDestroy, pixFindSkew
|
||||
pix = pixRead(deskewed_png)
|
||||
skew_angle, skew_confidence = pixFindSkew(pix)
|
||||
pix = pixDestroy(pix)
|
||||
from ocrmypdf.leptonica import Pix
|
||||
pix = Pix.read(deskewed_png)
|
||||
skew_angle, skew_confidence = pix.find_skew()
|
||||
|
||||
print(skew_angle)
|
||||
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
|
||||
@@ -141,7 +170,9 @@ def test_clean(spoof_tesseract_noop):
|
||||
('ccitt.pdf', 'hocr'),
|
||||
('ccitt.pdf', 'tesseract'),
|
||||
('jbig2.pdf', 'hocr'),
|
||||
('jbig2.pdf', 'tesseract')
|
||||
('jbig2.pdf', 'tesseract'),
|
||||
('lichtenstein.pdf', 'hocr'),
|
||||
('lichtenstein.pdf', 'tesseract')
|
||||
])
|
||||
def test_exotic_image(spoof_tesseract_cache, pdf, renderer):
|
||||
check_ocrmypdf(
|
||||
@@ -153,7 +184,7 @@ def test_exotic_image(spoof_tesseract_cache, pdf, renderer):
|
||||
|
||||
|
||||
def test_preserve_metadata(spoof_tesseract_noop):
|
||||
pdf_before = pypdf.PdfFileReader(_make_input('graph.pdf'))
|
||||
pdf_before = pypdf.PdfFileReader(_infile('graph.pdf'))
|
||||
|
||||
output = check_ocrmypdf('graph.pdf', 'test_metadata_preserve.pdf',
|
||||
env=spoof_tesseract_noop)
|
||||
@@ -165,8 +196,8 @@ def test_preserve_metadata(spoof_tesseract_noop):
|
||||
|
||||
|
||||
def test_override_metadata(spoof_tesseract_noop):
|
||||
input_file = _make_input('c02-22.pdf')
|
||||
output_file = _make_output('test_override_metadata.pdf')
|
||||
input_file = _infile('c02-22.pdf')
|
||||
output_file = _outfile('test_override_metadata.pdf')
|
||||
|
||||
german = 'Du siehst den Wald vor lauter Bäumen nicht.'
|
||||
chinese = '孔子'
|
||||
@@ -202,14 +233,14 @@ def test_override_metadata(spoof_tesseract_noop):
|
||||
])
|
||||
def test_oversample(spoof_tesseract_cache, renderer):
|
||||
oversampled_pdf = check_ocrmypdf(
|
||||
'skew.pdf', 'test_oversample_%s.pdf' % renderer, '--oversample', '300',
|
||||
'skew.pdf', 'test_oversample_%s.pdf' % renderer, '--oversample', '350',
|
||||
'-f',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
pdfinfo = pdf_get_all_pageinfo(oversampled_pdf)
|
||||
|
||||
print(pdfinfo[0]['xres'])
|
||||
assert abs(pdfinfo[0]['xres'] - 300) < 1
|
||||
assert abs(pdfinfo[0]['xres'] - 350) < 1
|
||||
|
||||
|
||||
def test_repeat_ocr():
|
||||
@@ -230,14 +261,113 @@ def test_skip_ocr(spoof_tesseract_cache):
|
||||
|
||||
|
||||
def test_argsfile(spoof_tesseract_noop):
|
||||
with open(_make_output('test_argsfile.txt'), 'w') as argsfile:
|
||||
with open(_outfile('test_argsfile.txt'), 'w') as argsfile:
|
||||
print('--title', 'ArgsFile Test', '--author', 'Test Cases',
|
||||
sep='\n', end='\n', file=argsfile)
|
||||
check_ocrmypdf('graph.pdf', 'test_argsfile.pdf',
|
||||
'@' + _make_output('test_argsfile.txt'),
|
||||
'@' + _outfile('test_argsfile.txt'),
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def check_monochrome_correlation(
|
||||
reference_pdf, reference_pageno, test_pdf, test_pageno):
|
||||
|
||||
import ocrmypdf.ghostscript as ghostscript
|
||||
import logging
|
||||
|
||||
gslog = logging.getLogger()
|
||||
|
||||
reference_png = _outfile('{}.ref{:04d}.png'.format(
|
||||
reference_pdf, reference_pageno))
|
||||
test_png = _outfile('{}.test{:04d}.png'.format(
|
||||
test_pdf, test_pageno))
|
||||
|
||||
def rasterize(pdf, pageno, png):
|
||||
if os.path.exists(png):
|
||||
print(png)
|
||||
return
|
||||
ghostscript.rasterize_pdf(
|
||||
pdf,
|
||||
png,
|
||||
xres=100, yres=100,
|
||||
raster_device='pngmono', log=gslog, pageno=pageno)
|
||||
|
||||
rasterize(reference_pdf, reference_pageno, reference_png)
|
||||
rasterize(test_pdf, test_pageno, test_png)
|
||||
|
||||
pix_ref = leptonica.Pix.read(reference_png)
|
||||
pix_test = leptonica.Pix.read(test_png)
|
||||
|
||||
return leptonica.Pix.correlation_binary(pix_ref, pix_test)
|
||||
|
||||
|
||||
def test_monochrome_correlation():
|
||||
# Verify leptonica: check that an incorrect rotated image has poor
|
||||
# correlation with reference
|
||||
corr = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1, # north facing page
|
||||
test_pdf=_infile('cardinal.pdf'),
|
||||
test_pageno=3, # south facing page
|
||||
)
|
||||
assert corr < 0.10
|
||||
corr = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=2,
|
||||
test_pdf=_infile('cardinal.pdf'),
|
||||
test_pageno=2,
|
||||
)
|
||||
assert corr > 0.90
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_autorotate(spoof_tesseract_cache, renderer):
|
||||
# cardinal.pdf contains four copies of an image rotated in each cardinal
|
||||
# direction - these ones are "burned in" not tagged with /Rotate
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
for n in range(1, 4+1):
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=n)
|
||||
assert correlation > 0.80
|
||||
|
||||
|
||||
def test_autorotate_threshold_low(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_low.pdf',
|
||||
'--rotate-pages-threshold', '1',
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
|
||||
# Low threshold -> always rotate -> expect high correlation between
|
||||
# reference page and test page
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=3)
|
||||
assert correlation > 0.80
|
||||
|
||||
|
||||
def test_autorotate_threshold_high(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_high.pdf',
|
||||
'--rotate-pages-threshold', '99',
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
|
||||
# High threshold -> never rotate -> expect low correlation since
|
||||
# test page will not be rotated
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=3)
|
||||
assert correlation < 0.10
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
@@ -246,14 +376,14 @@ def test_ocr_timeout(renderer):
|
||||
out = check_ocrmypdf('skew.pdf', 'test_timeout_%s.pdf' % renderer,
|
||||
'--tesseract-timeout', '1.0')
|
||||
pdfinfo = pdf_get_all_pageinfo(out)
|
||||
assert pdfinfo[0]['has_text'] == False
|
||||
assert not pdfinfo[0]['has_text']
|
||||
|
||||
|
||||
def test_skip_big(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('enormous.pdf', 'test_enormous.pdf',
|
||||
'--skip-big', '10', env=spoof_tesseract_cache)
|
||||
pdfinfo = pdf_get_all_pageinfo(out)
|
||||
assert pdfinfo[0]['has_text'] == False
|
||||
assert not pdfinfo[0]['has_text']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
@@ -310,20 +440,22 @@ def test_missing_docinfo(spoof_tesseract_noop):
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
|
||||
|
||||
@pytest.mark.skipif(running_in_docker(),
|
||||
reason="writes to tests/resources")
|
||||
def test_uppercase_extension(spoof_tesseract_noop):
|
||||
shutil.copy(_make_input("skew.pdf"), _make_input("UPPERCASE.PDF"))
|
||||
shutil.copy(_infile("skew.pdf"), _infile("UPPERCASE.PDF"))
|
||||
try:
|
||||
check_ocrmypdf("UPPERCASE.PDF", "UPPERCASE_OUT.PDF",
|
||||
env=spoof_tesseract_noop)
|
||||
finally:
|
||||
os.unlink(_make_input("UPPERCASE.PDF"))
|
||||
os.unlink(_infile("UPPERCASE.PDF"))
|
||||
|
||||
|
||||
def test_input_file_not_found():
|
||||
input_file = "does not exist.pdf"
|
||||
sh, out, err = run_ocrmypdf_sh(
|
||||
_make_input(input_file),
|
||||
_make_output("will not happen.pdf"))
|
||||
_infile(input_file),
|
||||
_outfile("will not happen.pdf"))
|
||||
assert sh.returncode == ExitCode.input_file
|
||||
assert (input_file in out or input_file in err)
|
||||
|
||||
@@ -331,8 +463,8 @@ def test_input_file_not_found():
|
||||
def test_input_file_not_a_pdf():
|
||||
input_file = __file__ # Try to OCR this file
|
||||
sh, out, err = run_ocrmypdf_sh(
|
||||
_make_input(input_file),
|
||||
_make_output("will not happen.pdf"))
|
||||
_infile(input_file),
|
||||
_outfile("will not happen.pdf"))
|
||||
assert sh.returncode == ExitCode.input_file
|
||||
assert (input_file in out or input_file in err)
|
||||
|
||||
@@ -366,4 +498,56 @@ def test_pagesegmode(renderer, spoof_tesseract_cache):
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash):
|
||||
sh, out, err = run_ocrmypdf_env(
|
||||
'ccitt.pdf', 'wontwork.pdf', '-v', '1',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_crash)
|
||||
assert sh.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(
|
||||
'ccitt.pdf', 'wontwork.pdf',
|
||||
'-r', env=spoof_tesseract_crash)
|
||||
assert sh.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
print(out)
|
||||
print(err)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
|
||||
check_ocrmypdf(
|
||||
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
|
||||
'--pdf-renderer', renderer, env=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(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert sh.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(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert sh.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_algo4():
|
||||
sh, _, _ = run_ocrmypdf_env('encrypted_algo4.pdf', 'wontwork.pdf')
|
||||
assert sh.returncode == ExitCode.encrypted_pdf
|
||||
|
||||
+19
-6
@@ -67,13 +67,21 @@ def test_single_page_text():
|
||||
def test_single_page_image():
|
||||
filename = os.path.join(TEST_OUTPUT, 'image-mono.pdf')
|
||||
|
||||
with NamedTemporaryFile() as im_tmp:
|
||||
with NamedTemporaryFile(mode='wb+', suffix='.png') as im_tmp:
|
||||
im = Image.new('1', (8, 8), 0)
|
||||
for n in range(8):
|
||||
im.putpixel((n, n), 1)
|
||||
im.save(im_tmp.name, format='PNG')
|
||||
|
||||
pdf_bytes = img2pdf.convert([im_tmp.name], dpi=8)
|
||||
imgsize = ((img2pdf.ImgSize.dpi, 8), (img2pdf.ImgSize.dpi, 8))
|
||||
layout_fun = img2pdf.get_layout_fun(None, imgsize, None, None, None)
|
||||
|
||||
im_tmp.seek(0)
|
||||
im_bytes = im_tmp.read()
|
||||
pdf_bytes = img2pdf.convert(
|
||||
im_bytes, producer="img2pdf", with_pdfrw=False,
|
||||
layout_fun=layout_fun)
|
||||
|
||||
with open(filename, 'wb') as pdf:
|
||||
pdf.write(pdf_bytes)
|
||||
|
||||
@@ -95,8 +103,8 @@ def test_single_page_image():
|
||||
assert pdfimage['bpc'] == 8
|
||||
|
||||
# DPI in a 1"x1" is the image width
|
||||
assert pdfimage['dpi_w'] == 8
|
||||
assert pdfimage['dpi_h'] == 8
|
||||
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
|
||||
|
||||
|
||||
def test_single_page_inline_image():
|
||||
@@ -112,8 +120,12 @@ def test_single_page_inline_image():
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
pageinfo.pdf_get_all_pageinfo(filename)
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(filename)
|
||||
print(pdfinfo)
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert (pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert pdfimage['color'] != '-'
|
||||
assert pdfimage['width'] == 8
|
||||
|
||||
|
||||
def test_jpeg():
|
||||
@@ -123,4 +135,5 @@ def test_jpeg():
|
||||
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert pdfimage['enc'] == 'jpeg'
|
||||
assert (pdfimage['dpi_w'] - 150) < 1e-5
|
||||
|
||||
|
||||
Reference in New Issue
Block a user