Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b969aad67b | ||
|
|
e70387b1af | ||
|
|
44f47fba21 | ||
|
|
02584094a1 | ||
|
|
91d715ac93 | ||
|
|
35addb8a33 | ||
|
|
d32ea8d0dd | ||
|
|
12575d594a | ||
|
|
0746083301 | ||
|
|
5c99acf6d1 | ||
|
|
2b10df7b74 | ||
|
|
ebe68de4ff | ||
|
|
b17c6a146d | ||
|
|
46d837c866 | ||
|
|
24856b61e4 | ||
|
|
8d0c6ff616 | ||
|
|
0b24f971cd | ||
|
|
bc5d3824bd | ||
|
|
4356983707 | ||
|
|
2414b79ee6 | ||
|
|
968e1546f0 | ||
|
|
48213c9c3f | ||
|
|
f385772d21 | ||
|
|
d257c83520 | ||
|
|
7b72ffec4f | ||
|
|
757f6826dc | ||
|
|
5df83a0d30 | ||
|
|
d70e3d3753 | ||
|
|
0dfceedcfb | ||
|
|
2c30f4bfc5 | ||
|
|
9e7fb52b47 | ||
|
|
bb5fd38e38 | ||
|
|
7c8cf5cfa2 | ||
|
|
fef35e4eb2 | ||
|
|
8f77576dc4 | ||
|
|
b3fcf24a26 | ||
|
|
16e4d342d2 | ||
|
|
8458a51860 | ||
|
|
636d1903b3 | ||
|
|
514efa36fc | ||
|
|
bd48f40d3d | ||
|
|
c02dbc809a | ||
|
|
410111d6fb | ||
|
|
68cf9cbd87 | ||
|
|
c9b2540d9d | ||
|
|
1bacf35a2c | ||
|
|
8aef0d9277 | ||
|
|
b2fa8645ba | ||
|
|
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 |
+10
-11
@@ -1,3 +1,5 @@
|
|||||||
|
sudo: required
|
||||||
|
dist: trusty
|
||||||
language: python
|
language: python
|
||||||
|
|
||||||
cache: pip
|
cache: pip
|
||||||
@@ -5,33 +7,30 @@ cache: pip
|
|||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.ccache
|
- $HOME/.ccache
|
||||||
- tarballs
|
- packages
|
||||||
- tests/cache
|
- tests/cache
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- 3.4
|
- 3.4
|
||||||
|
- 3.5
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -f $HOME/.cache/pip/log/debug.log
|
- rm -f $HOME/.cache/pip/log/debug.log
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
# Ubuntu packages
|
# Ubuntu packages
|
||||||
- sudo add-apt-repository ppa:evl.ms/precise -y # for Ghostscript 9.15
|
- sudo add-apt-repository ppa:vshn/ghostscript -y # for ghostscript 9.16 (trusty has 9.10)
|
||||||
- sudo add-apt-repository ppa:lyrasis/precise-backports -y # for Tesseract 3.03
|
- sudo add-apt-repository ppa:heyarje/libav-11 -y # for libav11, which is for unpaper
|
||||||
- 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 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 libffi-dev
|
- 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
|
||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
|
|
||||||
# Download, make and install unpaper (using ccache)
|
# Download, make and install unpaper (using ccache)
|
||||||
- mkdir -p tarballs
|
- mkdir -p packages
|
||||||
- "[ -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"
|
- "[ -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"
|
||||||
- tar -xvf tarballs/unpaper-6.1.tar.xz
|
- sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||||
- export PATH="/usr/lib/ccache:$PATH"
|
|
||||||
- pushd unpaper-6.1 && ./configure --prefix=/usr && make -j && sudo make install && popd
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
|
|||||||
+156
-47
@@ -8,7 +8,7 @@ Main features
|
|||||||
-------------
|
-------------
|
||||||
|
|
||||||
- Generates a searchable
|
- Generates a searchable
|
||||||
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`__ file from a regular PDF
|
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`_ file from a regular PDF
|
||||||
- Places OCR text accurately below the image to ease copy / paste
|
- Places OCR text accurately below the image to ease copy / paste
|
||||||
- Keeps the exact resolution of the original embedded images
|
- Keeps the exact resolution of the original embedded images
|
||||||
- When possible, inserts OCR information as a "lossless" operation without rendering vector information
|
- When possible, inserts OCR information as a "lossless" operation without rendering vector information
|
||||||
@@ -18,11 +18,11 @@ Main features
|
|||||||
- Provides debug mode to enable easy verification of the OCR results
|
- Provides debug mode to enable easy verification of the OCR results
|
||||||
- Processes pages in parallel when more than one CPU core is
|
- Processes pages in parallel when more than one CPU core is
|
||||||
available
|
available
|
||||||
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`__ engine
|
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`_ engine
|
||||||
- Supports the `39 languages <https://code.google.com/p/tesseract-ocr/downloads/list>`__ recognized by Tesseract
|
- Supports the `39 languages <https://code.google.com/p/tesseract-ocr/downloads/list>`_ recognized by Tesseract
|
||||||
- Battle-tested on thousands of PDFs, a test suite and continuous integration
|
- Battle-tested on thousands of PDFs, a test suite and continuous integration
|
||||||
|
|
||||||
For details: please consult the `release notes <RELEASE_NOTES.rst>`__.
|
For details: please consult the `release notes <RELEASE_NOTES.rst>`_.
|
||||||
|
|
||||||
Motivation
|
Motivation
|
||||||
----------
|
----------
|
||||||
@@ -31,9 +31,9 @@ I searched the web for a free command line tool to OCR PDF files on
|
|||||||
Linux/UNIX: I found many, but none of them were really satisfying.
|
Linux/UNIX: I found many, but none of them were really satisfying.
|
||||||
|
|
||||||
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
||||||
- Or they did not display correctly some escaped HTML characters located in the hOCR file produced by the OCR engine
|
- Or they did not handle accents and multilingual characters
|
||||||
- Or they changed the resolution of the embedded images
|
- Or they changed the resolution of the embedded images
|
||||||
- Or they generated PDF files having a ridiculous big size
|
- Or they generated ridiculously large PDF files
|
||||||
- Or they crashed when trying to OCR some of my PDF files
|
- Or they crashed when trying to OCR some of my PDF files
|
||||||
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
|
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
|
||||||
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
|
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
|
||||||
@@ -46,22 +46,45 @@ Installation
|
|||||||
|
|
||||||
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
|
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
|
||||||
|
|
||||||
You can install it to a Python virtual environment or system-wide.
|
These steps describe how to install OCRmyPDF on your system.
|
||||||
|
|
||||||
|
- `Installing on Debian and Ubuntu`_ (Debian stretch and Ubuntu 16.10 or later)
|
||||||
|
- `Installing the Docker image`_
|
||||||
|
- `Installing on Mac OS X`_
|
||||||
|
- `Installing on Ubuntu 14.04 LTS`_
|
||||||
|
- Installing and running on `Windows`_ using the Docker image
|
||||||
|
|
||||||
|
If you prefer to install from source or install OCRmyPDF to a Python virtual environment, see steps for `Installing HEAD revision from sources`_.
|
||||||
|
|
||||||
|
.. _Windows: `Installing on Windows`_
|
||||||
|
|
||||||
|
|
||||||
|
Installing on Debian and Ubuntu
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||||
|
``apt-get install ocrmypdf``.
|
||||||
|
|
||||||
|
.. _Docker:
|
||||||
|
|
||||||
Installing the Docker image
|
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.
|
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
|
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install
|
||||||
a Docker image 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
|
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::
|
successfully, your system is ready to download and execute the image:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run hello-world
|
docker run hello-world
|
||||||
|
|
||||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs::
|
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
# Optional step for Mac OS X users
|
# Optional step for Mac OS X users
|
||||||
docker-machine stop "yourVM"
|
docker-machine stop "yourVM"
|
||||||
@@ -70,29 +93,41 @@ OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine i
|
|||||||
eval $(docker-machine env "yourVM")
|
eval $(docker-machine env "yourVM")
|
||||||
|
|
||||||
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
||||||
the image::
|
the image:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
docker pull jbarlow83/ocrmypdf
|
docker pull jbarlow83/ocrmypdf
|
||||||
|
|
||||||
Then tag it to give a more convenient name, just ocrmypdf::
|
Then tag it to give a more convenient name, just ocrmypdf:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||||
|
|
||||||
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__::
|
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
# Alternative step: If you need all language packs
|
# Alternative step: If you need all language packs
|
||||||
docker pull jbarlow83/ocrmypdf-polyglot
|
docker pull jbarlow83/ocrmypdf-polyglot
|
||||||
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
|
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
|
||||||
|
|
||||||
You can then run ocrmypdf using the command::
|
You can then run ocrmypdf using the command:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run ocrmypdf --help
|
docker run ocrmypdf --help
|
||||||
|
|
||||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`__, such as this in this template::
|
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, such as this in this template:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||||
|
|
||||||
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``::
|
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||||
|
|
||||||
@@ -104,84 +139,135 @@ Installing on Mac OS X
|
|||||||
|
|
||||||
These instructions probably work on all Mac OS X versions later than 10.7 (Lion). OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
|
These instructions probably work on all Mac OS X versions later than 10.7 (Lion). OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
|
||||||
|
|
||||||
If it's not already present, `install Homebrew <http://brew.sh/>`__.
|
If it's not already present, `install Homebrew <http://brew.sh/>`_.
|
||||||
|
|
||||||
Update Homebrew::
|
Update Homebrew:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
brew update
|
brew update
|
||||||
|
|
||||||
Install or upgrade the required Homebrew packages, if any are missing::
|
Install or upgrade the required Homebrew packages, if any are missing:
|
||||||
|
|
||||||
brew install libpng openjpeg jbig2dec # image libraries
|
.. code-block:: bash
|
||||||
|
|
||||||
|
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||||
brew install qpdf
|
brew install qpdf
|
||||||
brew install ghostscript
|
brew install ghostscript
|
||||||
brew install python3
|
brew install python3
|
||||||
brew install libxml2 libffi leptonica
|
brew install libxml2 libffi leptonica
|
||||||
brew install unpaper # optional
|
brew install unpaper # optional
|
||||||
brew install tesseract
|
|
||||||
|
|
||||||
Update the homebrew pip and install Pillow::
|
Install the required Tesseract OCR engine with the language packs you plan to use:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||||
|
|
||||||
|
Update the homebrew pip and install Pillow:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
pip3 install --upgrade pip
|
pip3 install --upgrade pip
|
||||||
pip3 install --upgrade pillow
|
pip3 install --upgrade pillow
|
||||||
|
|
||||||
You can then install OCRmyPDF from PyPI::
|
You can then install OCRmyPDF from PyPI:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
pip3 install ocrmypdf
|
pip3 install ocrmypdf
|
||||||
|
|
||||||
The command line program should now be available::
|
The command line program should now be available:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
ocrmypdf --help
|
ocrmypdf --help
|
||||||
|
|
||||||
Installing on Ubuntu 14.04 LTS
|
Installing on Ubuntu 14.04 LTS
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than other options, because of certain bugs in Python package installation.
|
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
|
||||||
|
|
||||||
Update apt-get::
|
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||||
|
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||||
|
|
||||||
|
Update apt-get:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get upgrade
|
sudo apt-get upgrade
|
||||||
|
|
||||||
Install system dependencies::
|
Install system dependencies:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo apt-get install \
|
sudo apt-get install \
|
||||||
zlib1g-dev \
|
zlib1g-dev \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libavformat56 libavcodec56 libavutil54 \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
tesseract-ocr \
|
tesseract-ocr \
|
||||||
qpdf \
|
qpdf \
|
||||||
unpaper \
|
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-pil \
|
python3-pil \
|
||||||
python3-pytest \
|
python3-pytest \
|
||||||
python3-reportlab
|
python3-reportlab
|
||||||
|
|
||||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||||
into your system Python, which could interfere with other programs)::
|
into your system Python, which could interfere with other programs):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo pip3 install ocrmypdf
|
sudo pip3 install ocrmypdf
|
||||||
|
|
||||||
If you wish to install OCRmyPDF to a virtual environment to isolate system Python from modified, you can
|
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||||
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
||||||
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`__::
|
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo apt-get install python3-venv
|
sudo apt-get install python3-venv
|
||||||
python3 -m venv venv-ocrmypdf --without-pip
|
python3 -m venv venv-ocrmypdf --without-pip
|
||||||
source venv-ocrmypdf/bin/activate
|
source venv-ocrmypdf/bin/activate
|
||||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||||
deactivate
|
deactivate
|
||||||
pyvenv --system-site-packages venv-ocrmypdf
|
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||||
source venv-ocrmypdf/bin/activate
|
source venv-ocrmypdf/bin/activate
|
||||||
pip install ocrmypdf
|
pip install ocrmypdf
|
||||||
|
|
||||||
Ubuntu 14.04 only installs ``unpaper`` version 0.4.2, which is not supported by OCRmyPDF because it is produces invalid output. This program is an optional dependency, and provides page deskewing and cleaning. See `Dockerfile <Dockerfile>`__ for an example of how to building unpaper 6.1 from source. If you choose to install unpaper later, OCRmyPDF will use the foremost version on the system PATH.
|
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper`` and is not motivated to figure how to set up a Ubuntu PPA to distribute it. You can create a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||||
|
sudo dpkg -i unpaper_6.1-1.deb
|
||||||
|
|
||||||
|
|
||||||
Installing on Windows
|
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.
|
||||||
|
|
||||||
|
Running on Windows
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The command line syntax to run ocrmypdf from a command prompt will resemble:
|
||||||
|
|
||||||
|
.. code-block:: bat
|
||||||
|
|
||||||
|
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
|
||||||
|
|
||||||
|
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
|
||||||
|
|
||||||
Installing HEAD revision from sources
|
Installing HEAD revision from sources
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -189,24 +275,32 @@ 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,
|
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.
|
it will alert you if dependencies are missing.
|
||||||
|
|
||||||
To install the HEAD revision from sources in the current Python 3 environment::
|
To install the HEAD revision from sources in the current Python 3 environment:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||||
|
|
||||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__, allowing customization of OCRmyPDF, use the ``-e`` flag::
|
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||||
|
|
||||||
On certain Linux distributions such as Ubuntu, you may need to use
|
On certain Linux distributions such as Ubuntu, you may need to use
|
||||||
run the install command as superuser::
|
run the install command as superuser:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||||
|
|
||||||
Note that this will alter your system's Python distribution. If you prefer
|
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::
|
to not install as superuser, you can install the package in a Python virtual environment:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||||
pyvenv venv
|
python3 -m venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
cd OCRmyPDF
|
cd OCRmyPDF
|
||||||
pip3 install .
|
pip3 install .
|
||||||
@@ -214,7 +308,9 @@ to not install as superuser, you can install the package in a Python virtual env
|
|||||||
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
||||||
you activate the virtual environment.
|
you activate the virtual environment.
|
||||||
|
|
||||||
To run the program::
|
To run the program:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
ocrmypdf --help
|
ocrmypdf --help
|
||||||
|
|
||||||
@@ -227,10 +323,15 @@ Languages
|
|||||||
---------
|
---------
|
||||||
|
|
||||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
|
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
|
||||||
you can often find packages that provide language packs::
|
you can often find packages that provide language packs:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Display a list of all Tesseract language packs
|
||||||
|
apt-cache search tesseract-ocr
|
||||||
|
|
||||||
# Debian/Ubuntu users
|
# Debian/Ubuntu users
|
||||||
sudo apt-get install tesseract-ocr-chi-sim
|
sudo apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||||
|
|
||||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
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.
|
languages can be requested.
|
||||||
@@ -238,9 +339,17 @@ languages can be requested.
|
|||||||
Support
|
Support
|
||||||
-------
|
-------
|
||||||
|
|
||||||
In case you detect an issue, please:
|
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||||
|
|
||||||
- Check if your issue is already known
|
.. code-block:: bash
|
||||||
|
|
||||||
|
ocrmypdf --help
|
||||||
|
|
||||||
|
The `Wiki <https://github.com/jbarlow83/OCRmyPDF/wiki>`_ page also contains some tips and suggests.
|
||||||
|
|
||||||
|
If you detect an issue, please:
|
||||||
|
|
||||||
|
- Check whether your issue is already known
|
||||||
- If no problem report exists on github, please create one here:
|
- If no problem report exists on github, please create one here:
|
||||||
https://github.com/jbarlow83/OCRmyPDF/issues
|
https://github.com/jbarlow83/OCRmyPDF/issues
|
||||||
- Describe your problem thoroughly
|
- Describe your problem thoroughly
|
||||||
@@ -252,11 +361,11 @@ In case you detect an issue, please:
|
|||||||
Press & Media
|
Press & Media
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
- `c't 1-2014, page 59 <http://heise.de/-2279695>`__:
|
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
|
||||||
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
|
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
|
||||||
magazine c't
|
magazine c't
|
||||||
- `heise Open Source, 09/2014: Texterkennung mit
|
- `heise Open Source, 09/2014: Texterkennung mit
|
||||||
OCRmyPDF <http://heise.de/-2356670>`__
|
OCRmyPDF <http://heise.de/-2356670>`_
|
||||||
|
|
||||||
Disclaimer
|
Disclaimer
|
||||||
----------
|
----------
|
||||||
|
|||||||
+64
-365
@@ -1,9 +1,68 @@
|
|||||||
RELEASE NOTES
|
RELEASE NOTES
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Please always read this file before installing the package
|
|
||||||
|
|
||||||
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
|
v4.2:
|
||||||
|
=====
|
||||||
|
|
||||||
|
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
|
||||||
|
|
||||||
|
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
|
||||||
|
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
|
||||||
|
|
||||||
|
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
|
||||||
|
|
||||||
|
+ ``pdfa`` is the default, consistent with past behavior
|
||||||
|
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
|
||||||
|
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
|
||||||
|
|
||||||
|
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
|
||||||
|
- ``--force-ocr`` rasterizes pages even if they contain no images
|
||||||
|
|
||||||
|
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
|
||||||
|
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
|
||||||
|
+ fixes issue #82
|
||||||
|
|
||||||
|
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
|
||||||
|
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
|
||||||
|
|
||||||
|
+ Some Ubuntu "PPAs" (backports) are needed to make it work
|
||||||
|
|
||||||
|
- Support for some older dependencies dropped
|
||||||
|
|
||||||
|
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
|
||||||
|
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
|
||||||
|
|
||||||
|
- Ghostscript now runs in "safer" mode where possible
|
||||||
|
|
||||||
|
v4.1.4:
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
v4.0.7:
|
||||||
@@ -22,9 +81,6 @@ v4.0.6:
|
|||||||
v4.0.5:
|
v4.0.5:
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Remove some verbose debug messages from v4.0.4
|
- Remove some verbose debug messages from v4.0.4
|
||||||
- Fixed temporary that wasn't being deleted
|
- Fixed temporary that wasn't being deleted
|
||||||
- DPI is now calculated correctly for cropped images, along with other image transformations
|
- DPI is now calculated correctly for cropped images, along with other image transformations
|
||||||
@@ -48,7 +104,7 @@ New features
|
|||||||
Fixes
|
Fixes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
- Show stack trace if unexpect errors occur
|
- Show stack trace if unexpected errors occur
|
||||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||||
abort the file
|
abort the file
|
||||||
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
||||||
@@ -321,363 +377,6 @@ Notes and known issues
|
|||||||
v2.2-stable (2014-09-29):
|
v2.2-stable (2014-09-29):
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
New features
|
OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions.
|
||||||
------------
|
|
||||||
|
|
||||||
- None
|
For details on older versions, see the `final version of its release notes <https://github.com/fritz-hh/OCRmyPDF/blob/7fd3dbdf42ca53a619412ce8add7532c5e81a9d1/RELEASE_NOTES.md>`_.
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- Update to jhove v1.11
|
|
||||||
- Request the python library reportlab v3.0 or newer (So that we could remove a patch to the previous version of reportlab leading to issues for some users)
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Fix bug on Mac OS X (resolution of simlink to OCRmyPDF.sh script) (thanks to jbarlow83)
|
|
||||||
- Check if the input pdf file exists before to continue
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.2
|
|
||||||
- Dependencies:
|
|
||||||
|
|
||||||
- parallel 20140822
|
|
||||||
- poppler-utils 0.24.5
|
|
||||||
- ImageMagick 6.8.9-4 2014-09-17
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.8
|
|
||||||
- ghostcript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0_65"
|
|
||||||
|
|
||||||
|
|
||||||
v2.1-stable (2014-09-20):
|
|
||||||
=========================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- None
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- None
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Allow execution via simlink
|
|
||||||
- Add support for tesseract 3.03
|
|
||||||
- Add support for newer version of reportlab
|
|
||||||
- Lowered minimum version of gnu parallel
|
|
||||||
- Various typo
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- parallel 20130222
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v2.0-stable (2014-01-25):
|
|
||||||
=========================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- Check if the language(s) passed using the -l option is supported by
|
|
||||||
tesseract (fixes #60)
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- Allow OCRmyPDF to be used with tesseract 3.02.01, even though OCR
|
|
||||||
might fail for few PDF file (see issue #28). Rationale: For some
|
|
||||||
linux distribution, no newer version than tesseract 3.02.01 is
|
|
||||||
available
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- More robust algorithm for checking the version of the installed
|
|
||||||
tesseract package
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- parallel 20130222
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v2.0-rc2 (2014-01-16):
|
|
||||||
======================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- None
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- Size reduction of final PDF file: (fixes #50)
|
|
||||||
- Support for monochrome (Black&White) images (massive size reduction
|
|
||||||
in final PDF: >80%)
|
|
||||||
- Reduced size of grayscale images (by 13% on test PDF file)
|
|
||||||
- Preventing fi, fl ligatures does not require anymore to pass an
|
|
||||||
additional config file to tesseract using the -C option (fixes #58)
|
|
||||||
- Location of temporary folder according to content of environment
|
|
||||||
variable TMPDIR.
|
|
||||||
- Dependency to pdftk removed
|
|
||||||
- Check for compatible versions of dependencies: (fixes #51)
|
|
||||||
- parallel and tesseract
|
|
||||||
- python libraries reportlab and lxml
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Improved portability with various shells (dash, bash, tcsh) and OS
|
|
||||||
(FreeBSD, MAC OSX, Linux) (fixes #59)
|
|
||||||
- Corrected bug in case the input PDF file contains a space character
|
|
||||||
(fixes #48)
|
|
||||||
- Prevent spurious error message in case there is no image in a PDF
|
|
||||||
page
|
|
||||||
- Prevent collision of temporary folder names (fixes #57)
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- parallel 20130222
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v2.0-rc1 (2014-01-07):
|
|
||||||
======================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- Huge performance improvement on machines having multiple CPU/cores
|
|
||||||
(processing of several pages concurrently) (fixes #18)
|
|
||||||
- By default prevent from processing a PDF file already containing
|
|
||||||
fonts (i.e. text)(it can be overridden with the -f flag) (fixes #16)
|
|
||||||
- Warn if the resolution is too low to get reasonable OCR results
|
|
||||||
(fixes #37)
|
|
||||||
- New option (-o) to perform automatic oversampling if the image
|
|
||||||
resolution is too low. This can improve OCR results.
|
|
||||||
- Warn if using a tesseract version older than v3.02.02 (as older
|
|
||||||
versions are known to produce invalid output) (fixes #41)
|
|
||||||
- Echo version of the installed dependencies (e.g. tesseract) in debug
|
|
||||||
mode in order to ease support (fixes #35)
|
|
||||||
- Echo the arguments passed to the script in debug mode to ease support
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- In debug mode: The debug page is now placed after the respective
|
|
||||||
"normal" page
|
|
||||||
- Reduced disk space usage in temporary folder if -d (deskew) or -c
|
|
||||||
(cleanup) options are not selected
|
|
||||||
- New file src/config.sh containing various configuration parameters
|
|
||||||
- Documentation of the tesseract config file "tess-cfg/no\_ligature"
|
|
||||||
improved
|
|
||||||
- Improved consistency of the temporary file names
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Improved robustness:
|
|
||||||
- in case vertical resolution differs from horizontal resolution (fixes
|
|
||||||
#38)
|
|
||||||
- in case a PDF page contains more than one image (fixes #36)
|
|
||||||
- Fix a problem occurring if python 3 is the standard interpreter
|
|
||||||
(fixes #33)
|
|
||||||
- Fix a problem occurring if the input PDF file contains special
|
|
||||||
characters like "#" (fixes #34)
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- parallel 20130222
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- pdftk 1.45
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v1.1-stable (2014-01-06):
|
|
||||||
=========================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- N/A
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- N/A
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Fixed syntax error (bashism) leading to an error message on certain
|
|
||||||
systems (fixes #42)
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- pdftk 1.45
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v1.0-stable (2013-05-06):
|
|
||||||
=========================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- In debug mode: compute and echo time required for processing (fixes
|
|
||||||
#26)
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- Removed feature to add metadata in final pdf file (because it lead to
|
|
||||||
to final PDF file that does not comply to the PDF/A-1 format)
|
|
||||||
- Removed feature to set same owner & permissions in final PDF file
|
|
||||||
than in input file
|
|
||||||
- Removed many unused jhove files (e.g. documentation, \*.java and
|
|
||||||
\*.class files)
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Correction to handle correctly path and input PDF files having spaces
|
|
||||||
(fixes #31)
|
|
||||||
- Resolutions (x/y) that are nearly equal are now supported (fixes #25)
|
|
||||||
- Fix compatibility issue with Ubuntu server 12.04 / Ubuntu server
|
|
||||||
10.04 / Linux Mint 13 Maya and probably other Linux distributions
|
|
||||||
(fixes #27)
|
|
||||||
- Commit missing jhove files (\*.jar mainly) due to wrong .gitignore
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- pdftk 1.45
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v1.0-rc2 (2013-04-29):
|
|
||||||
======================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- Keep temporary files if debug mode is set (fixes #22)
|
|
||||||
- Set same owner & permissions in final PDF file than in input file
|
|
||||||
(fixes #9)
|
|
||||||
- Added metadata in final pdf file (fixes #4)
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- N/A
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- Fixed wrong image cropping when deskew option is activated
|
|
||||||
- Exit with error message if page size is not found in hocr file (fixes
|
|
||||||
#21)
|
|
||||||
- Various minor fixes in log messages
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- pdftk 1.45
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
|
|
||||||
v1.0-rc1 (2013-04-26):
|
|
||||||
======================
|
|
||||||
|
|
||||||
New features
|
|
||||||
------------
|
|
||||||
|
|
||||||
- First release candidate
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
|
|
||||||
- N/A
|
|
||||||
|
|
||||||
Fixes
|
|
||||||
-----
|
|
||||||
|
|
||||||
- N/A
|
|
||||||
|
|
||||||
Tested with
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Operating system: FreeBSD 9.1
|
|
||||||
- Dependencies:
|
|
||||||
- poppler-utils 0.22.2
|
|
||||||
- ImageMagick 6.8.0-7 2013-03-30
|
|
||||||
- Unpaper 0.3
|
|
||||||
- tesseract 3.02.02
|
|
||||||
- Python 2.7.3
|
|
||||||
- pdftk 1.45
|
|
||||||
- ghoscript (gs): 9.06
|
|
||||||
- java: openjdk version "1.7.0\_17"
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import os
|
import os
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
|
||||||
class ExitCode(IntEnum):
|
class ExitCode(IntEnum):
|
||||||
@@ -7,11 +8,13 @@ class ExitCode(IntEnum):
|
|||||||
bad_args = 1
|
bad_args = 1
|
||||||
input_file = 2
|
input_file = 2
|
||||||
missing_dependency = 3
|
missing_dependency = 3
|
||||||
invalid_output_pdfa = 4
|
invalid_output_pdf = 4
|
||||||
file_access_error = 5
|
file_access_error = 5
|
||||||
already_done_ocr = 6
|
already_done_ocr = 6
|
||||||
child_process_error = 7
|
child_process_error = 7
|
||||||
|
encrypted_pdf = 8
|
||||||
other_error = 15
|
other_error = 15
|
||||||
|
ctrl_c = 130
|
||||||
|
|
||||||
|
|
||||||
def get_program(name):
|
def get_program(name):
|
||||||
@@ -21,3 +24,7 @@ def get_program(name):
|
|||||||
|
|
||||||
def page_number(input_file):
|
def page_number(input_file):
|
||||||
return int(os.path.basename(input_file)[0:6])
|
return int(os.path.basename(input_file)[0:6])
|
||||||
|
|
||||||
|
|
||||||
|
def is_iterable_notstr(thing):
|
||||||
|
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -14,6 +14,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
|||||||
args_gs = [
|
args_gs = [
|
||||||
get_program('gs'),
|
get_program('gs'),
|
||||||
'-dQUIET',
|
'-dQUIET',
|
||||||
|
'-dSAFER',
|
||||||
'-dBATCH',
|
'-dBATCH',
|
||||||
'-dNOPAUSE',
|
'-dNOPAUSE',
|
||||||
'-sDEVICE=%s' % raster_device,
|
'-sDEVICE=%s' % raster_device,
|
||||||
|
|||||||
+217
-22
@@ -17,6 +17,7 @@ from tempfile import TemporaryFile
|
|||||||
from ctypes.util import find_library
|
from ctypes.util import find_library
|
||||||
from .lib._leptonica import ffi
|
from .lib._leptonica import ffi
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
lept = ffi.dlopen(find_library('lept'))
|
lept = ffi.dlopen(find_library('lept'))
|
||||||
|
|
||||||
@@ -83,6 +84,13 @@ class LeptonicaIOError(LeptonicaError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveColormap(Enum):
|
||||||
|
to_binary = 0
|
||||||
|
to_grayscale = 1
|
||||||
|
to_full_color = 2
|
||||||
|
based_on_src = 3
|
||||||
|
|
||||||
|
|
||||||
class Pix:
|
class Pix:
|
||||||
"""Wrapper around leptonica's PIX object.
|
"""Wrapper around leptonica's PIX object.
|
||||||
|
|
||||||
@@ -98,14 +106,14 @@ class Pix:
|
|||||||
in a threadsafe manner if a Python threading.Lock protects the data.
|
in a threadsafe manner if a Python threading.Lock protects the data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cpix):
|
def __init__(self, pix):
|
||||||
self.cpix = ffi.gc(cpix, Pix._pix_destroy)
|
self._pix = ffi.gc(pix, Pix._pix_destroy)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.cpix:
|
if self._pix:
|
||||||
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
|
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
|
||||||
return s.format(self.cpix.w, self.cpix.h, self.cpix.d,
|
return s.format(self._pix.w, self._pix.h, self._pix.d,
|
||||||
int(ffi.cast("intptr_t", self.cpix)))
|
int(ffi.cast("intptr_t", self._pix)))
|
||||||
else:
|
else:
|
||||||
return "<leptonica.Pix image NULL>"
|
return "<leptonica.Pix image NULL>"
|
||||||
|
|
||||||
@@ -113,12 +121,16 @@ class Pix:
|
|||||||
data = ffi.new('l_uint32 **')
|
data = ffi.new('l_uint32 **')
|
||||||
size = ffi.new('size_t *')
|
size = ffi.new('size_t *')
|
||||||
|
|
||||||
err = lept.pixSerializeToMemory(self.cpix, data, size)
|
err = lept.pixSerializeToMemory(self._pix, data, size)
|
||||||
if err != 0:
|
if err != 0:
|
||||||
raise LeptonicaIOError("pixSerializeToMemory")
|
raise LeptonicaIOError("pixSerializeToMemory")
|
||||||
|
|
||||||
char_data = ffi.cast('char *', data[0])
|
char_data = ffi.cast('char *', data[0])
|
||||||
|
|
||||||
|
# Copy from C bytes to python bytes()
|
||||||
data_bytes = ffi.buffer(char_data, size[0])[:]
|
data_bytes = ffi.buffer(char_data, size[0])[:]
|
||||||
|
|
||||||
|
# Can now free C bytes
|
||||||
lept.lept_free(char_data)
|
lept.lept_free(char_data)
|
||||||
return dict(data=data_bytes)
|
return dict(data=data_bytes)
|
||||||
|
|
||||||
@@ -126,16 +138,41 @@ class Pix:
|
|||||||
cdata_bytes = ffi.new('char[]', state['data'])
|
cdata_bytes = ffi.new('char[]', state['data'])
|
||||||
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
|
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
|
||||||
|
|
||||||
self.cpix = lept.pixDeserializeFromMemory(
|
pix = lept.pixDeserializeFromMemory(
|
||||||
cdata_uint32, len(state['data']))
|
cdata_uint32, len(state['data']))
|
||||||
|
Pix.__init__(self, pix)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width(self):
|
def width(self):
|
||||||
return self.cpix.w
|
return self._pix.w
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def height(self):
|
def height(self):
|
||||||
return self.cpix.h
|
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
|
@classmethod
|
||||||
def read(cls, filename):
|
def read(cls, filename):
|
||||||
@@ -158,7 +195,29 @@ class Pix:
|
|||||||
with LeptonicaErrorTrap():
|
with LeptonicaErrorTrap():
|
||||||
lept.pixWriteImpliedFormat(
|
lept.pixWriteImpliedFormat(
|
||||||
filename.encode(sys.getfilesystemencoding()),
|
filename.encode(sys.getfilesystemencoding()),
|
||||||
self.cpix, jpeg_quality, jpeg_progressive)
|
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):
|
def deskew(self, reduction_factor=0):
|
||||||
"""Returns the deskewed pix object.
|
"""Returns the deskewed pix object.
|
||||||
@@ -170,16 +229,21 @@ class Pix:
|
|||||||
for skew angle
|
for skew angle
|
||||||
"""
|
"""
|
||||||
with LeptonicaErrorTrap():
|
with LeptonicaErrorTrap():
|
||||||
return Pix(lept.pixDeskew(self.cpix, reduction_factor))
|
return Pix(lept.pixDeskew(self._pix, reduction_factor))
|
||||||
|
|
||||||
def scale(self, scalex, scaley):
|
def scale(self, scalex, scaley):
|
||||||
"Returns the pix object rescaled according to the proportions given."
|
"Returns the pix object rescaled according to the proportions given."
|
||||||
with LeptonicaErrorTrap():
|
with LeptonicaErrorTrap():
|
||||||
return Pix(lept.pixScale(self.cpix, scalex, scaley))
|
return Pix(lept.pixScale(self._pix, scalex, scaley))
|
||||||
|
|
||||||
def rotate180(self):
|
def rotate180(self):
|
||||||
with LeptonicaErrorTrap():
|
with LeptonicaErrorTrap():
|
||||||
return Pix(lept.pixRotate180(ffi.NULL, self.cpix))
|
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):
|
def find_skew(self):
|
||||||
"""Returns a tuple (deskew angle in degrees, confidence value).
|
"""Returns a tuple (deskew angle in degrees, confidence value).
|
||||||
@@ -189,12 +253,105 @@ class Pix:
|
|||||||
with LeptonicaErrorTrap():
|
with LeptonicaErrorTrap():
|
||||||
angle = ffi.new('float *', 0.0)
|
angle = ffi.new('float *', 0.0)
|
||||||
confidence = ffi.new('float *', 0.0)
|
confidence = ffi.new('float *', 0.0)
|
||||||
result = lept.pixFindSkew(self.cpix, angle, confidence)
|
result = lept.pixFindSkew(self._pix, angle, confidence)
|
||||||
if result == 0:
|
if result == 0:
|
||||||
return (angle[0], confidence[0])
|
return (angle[0], confidence[0])
|
||||||
else:
|
else:
|
||||||
return (None, None)
|
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
|
@staticmethod
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def make_pixel_sum_tab8():
|
def make_pixel_sum_tab8():
|
||||||
@@ -205,16 +362,17 @@ class Pix:
|
|||||||
if get_leptonica_version() < 'leptonica-1.72':
|
if get_leptonica_version() < 'leptonica-1.72':
|
||||||
# Older versions of Leptonica (pre-1.72) have a buggy
|
# Older versions of Leptonica (pre-1.72) have a buggy
|
||||||
# implementation of pixCorrelationBinary that overflows on larger
|
# implementation of pixCorrelationBinary that overflows on larger
|
||||||
# images.
|
# images. Ubuntu trusty has 1.70. Ubuntu PPA
|
||||||
|
# ppa:rebuntu16/avidemux+unofficial has "leptonlib" 1.73.
|
||||||
pix1_count = ffi.new('l_int32 *')
|
pix1_count = ffi.new('l_int32 *')
|
||||||
pix2_count = ffi.new('l_int32 *')
|
pix2_count = ffi.new('l_int32 *')
|
||||||
pixn_count = ffi.new('l_int32 *')
|
pixn_count = ffi.new('l_int32 *')
|
||||||
tab8 = Pix.make_pixel_sum_tab8()
|
tab8 = Pix.make_pixel_sum_tab8()
|
||||||
|
|
||||||
lept.pixCountPixels(pix1.cpix, pix1_count, tab8)
|
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
|
||||||
lept.pixCountPixels(pix2.cpix, pix2_count, tab8)
|
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
|
||||||
pixn = Pix(lept.pixAnd(ffi.NULL, pix1.cpix, pix2.cpix))
|
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
|
||||||
lept.pixCountPixels(pixn.cpix, pixn_count, tab8)
|
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
|
||||||
|
|
||||||
# Python converts these int32s to larger units as needed
|
# Python converts these int32s to larger units as needed
|
||||||
# to avoid overflow. Overflow happens easily here.
|
# to avoid overflow. Overflow happens easily here.
|
||||||
@@ -225,7 +383,7 @@ class Pix:
|
|||||||
return correlation
|
return correlation
|
||||||
else:
|
else:
|
||||||
correlation = ffi.new('float *', 0.0)
|
correlation = ffi.new('float *', 0.0)
|
||||||
result = lept.pixCorrelationBinary(pix1.cpix, pix2.cpix,
|
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
|
||||||
correlation)
|
correlation)
|
||||||
if result != 0:
|
if result != 0:
|
||||||
raise LeptonicaError("Correlation failed")
|
raise LeptonicaError("Correlation failed")
|
||||||
@@ -233,11 +391,48 @@ class Pix:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _pix_destroy(pix):
|
def _pix_destroy(pix):
|
||||||
ptr_to_pix = ffi.new('PIX **', pix)
|
p_pix = ffi.new('PIX **', pix)
|
||||||
lept.pixDestroy(ptr_to_pix)
|
lept.pixDestroy(p_pix)
|
||||||
# print('pix destroy ' + repr(pix))
|
# print('pix destroy ' + repr(pix))
|
||||||
|
|
||||||
|
|
||||||
|
class Box:
|
||||||
|
"""Wrapper around Leptonica's BOX objects.
|
||||||
|
|
||||||
|
See class Pix for notes about reference counting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, box):
|
||||||
|
self._box = ffi.gc(box, Box._box_destroy)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_leptonica_version():
|
def get_leptonica_version():
|
||||||
"""Get Leptonica version string.
|
"""Get Leptonica version string.
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ struct PixColormap
|
|||||||
l_int32 n; /* number of color entries used */
|
l_int32 n; /* number of color entries used */
|
||||||
};
|
};
|
||||||
typedef struct PixColormap PIXCMAP;
|
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("""
|
ffi.cdef("""
|
||||||
@@ -50,10 +62,17 @@ PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
|
|||||||
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
|
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
|
||||||
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
|
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
|
||||||
void pixDestroy ( PIX **ppix );
|
void pixDestroy ( PIX **ppix );
|
||||||
|
|
||||||
|
PIX *
|
||||||
|
pixEndianByteSwapNew(PIX *pixs);
|
||||||
|
|
||||||
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
|
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
|
||||||
char * getLeptonicaVersion ( );
|
char * getLeptonicaVersion ( );
|
||||||
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
|
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
|
||||||
PIX *pixRotate180(PIX *pixd, PIX *pixs);
|
PIX *pixRotate180(PIX *pixd, PIX *pixs);
|
||||||
|
PIX *
|
||||||
|
pixRotateOrth(PIX *pixs,
|
||||||
|
l_int32 quads);
|
||||||
|
|
||||||
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
|
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
|
||||||
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
|
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
|
||||||
@@ -62,6 +81,59 @@ l_int32 * makePixelSumTab8 ( void );
|
|||||||
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
|
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
|
||||||
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
|
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);
|
void lept_free(void *ptr);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|||||||
+414
-131
@@ -27,11 +27,12 @@ import ruffus.proxy_logger as proxy_logger
|
|||||||
|
|
||||||
from .hocrtransform import HocrTransform
|
from .hocrtransform import HocrTransform
|
||||||
from .pageinfo import pdf_get_all_pageinfo
|
from .pageinfo import pdf_get_all_pageinfo
|
||||||
from .pdfa import generate_pdfa_def
|
from .pdfa import generate_pdfa_def, file_claims_pdfa
|
||||||
from . import ghostscript
|
from . import ghostscript
|
||||||
from . import tesseract
|
from . import tesseract
|
||||||
from . import qpdf
|
from . import qpdf
|
||||||
from . import ExitCode, page_number
|
from . import ExitCode, page_number, is_iterable_notstr
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
@@ -42,6 +43,8 @@ warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
|
|||||||
|
|
||||||
BASEDIR = os.path.dirname(os.path.realpath(__file__))
|
BASEDIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
VECTOR_PAGE_DPI = 400
|
||||||
|
|
||||||
|
|
||||||
# -------------
|
# -------------
|
||||||
# External dependencies
|
# External dependencies
|
||||||
@@ -120,6 +123,16 @@ parser.add_argument(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-j', '--jobs', metavar='N', type=int,
|
'-j', '--jobs', metavar='N', type=int,
|
||||||
help="Use up to N CPU cores simultaneously (default: use all)")
|
help="Use up to N CPU cores simultaneously (default: use all)")
|
||||||
|
parser.add_argument(
|
||||||
|
'--image-dpi', metavar='DPI', type=int,
|
||||||
|
help="for input image instead of PDF, use this DPI instead of file's")
|
||||||
|
parser.add_argument(
|
||||||
|
'--output-type', choices=['pdfa', 'pdf'], default='pdfa',
|
||||||
|
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
|
||||||
|
"long term archiving (default, recommended) but may not suitable "
|
||||||
|
"for users who want their file altered as little as possible. 'pdfa' "
|
||||||
|
"also has problems with full Unicode text. 'pdf' attempts to "
|
||||||
|
"preserve file contents as much as possible.")
|
||||||
|
|
||||||
metadata = parser.add_argument_group(
|
metadata = parser.add_argument_group(
|
||||||
"Metadata options",
|
"Metadata options",
|
||||||
@@ -188,6 +201,10 @@ advanced.add_argument(
|
|||||||
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
||||||
help='give up on OCR after the timeout, but copy the preprocessed page '
|
help='give up on OCR after the timeout, but copy the preprocessed page '
|
||||||
'into the final output')
|
'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 = parser.add_argument_group(
|
||||||
"Debugging",
|
"Debugging",
|
||||||
@@ -234,13 +251,18 @@ if options.pdf_renderer == 'tesseract' and tesseract.version() < '3.04.01' \
|
|||||||
"Some PDF viewers will fail to find searchable text.\n"
|
"Some PDF viewers will fail to find searchable text.\n"
|
||||||
"--pdf-renderer=tesseract is not recommended.")
|
"--pdf-renderer=tesseract is not recommended.")
|
||||||
|
|
||||||
if any((options.deskew, options.clean, options.clean_final)):
|
if any((options.clean, options.clean_final)):
|
||||||
try:
|
try:
|
||||||
from . import unpaper
|
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(
|
complain(
|
||||||
"Install the 'unpaper' program to use --deskew or --clean.")
|
"Install the 'unpaper' program to use --deskew or --clean.")
|
||||||
sys.exit(ExitCode.bad_args)
|
sys.exit(ExitCode.missing_dependency)
|
||||||
else:
|
else:
|
||||||
unpaper = None
|
unpaper = None
|
||||||
|
|
||||||
@@ -260,6 +282,13 @@ if options.clean and not options.clean_final \
|
|||||||
"Tesseract PDF renderer cannot render --clean pages without "
|
"Tesseract PDF renderer cannot render --clean pages without "
|
||||||
"also performing --clean-final, so --clean-final is assumed.")
|
"also performing --clean-final, so --clean-final is assumed.")
|
||||||
|
|
||||||
|
if set(options.language) & {'chi_sim', 'chi_tra'} \
|
||||||
|
and (options.pdf_renderer == 'hocr' or options.output_type == 'pdfa'):
|
||||||
|
complain(
|
||||||
|
"Your settings are known to cause problems with OCR of Chinese text. "
|
||||||
|
"Try adding these arguments: "
|
||||||
|
" ocrmypdf --pdf-renderer tesseract --output-type pdf")
|
||||||
|
|
||||||
lossless_reconstruction = False
|
lossless_reconstruction = False
|
||||||
if options.pdf_renderer == 'hocr':
|
if options.pdf_renderer == 'hocr':
|
||||||
if not options.deskew and not options.clean_final and not options.force_ocr:
|
if not options.deskew and not options.clean_final and not options.force_ocr:
|
||||||
@@ -375,10 +404,87 @@ def cleanup_working_files(*args):
|
|||||||
shutil.rmtree(work_folder)
|
shutil.rmtree(work_folder)
|
||||||
|
|
||||||
|
|
||||||
|
def triage_image_file(input_file, output_file, log):
|
||||||
|
try:
|
||||||
|
log.info("Input file is not a PDF, checking if it is an image...")
|
||||||
|
im = Image.open(input_file)
|
||||||
|
except EnvironmentError as e:
|
||||||
|
log.error(e)
|
||||||
|
sys.exit(ExitCode.input_file)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
log.info("Input file is an image")
|
||||||
|
|
||||||
|
if 'dpi' in im.info:
|
||||||
|
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
|
||||||
|
log.info("Image size: (%d, %d)" % im.size)
|
||||||
|
log.info("Image resolution: (%d, %d)" % im.info['dpi'])
|
||||||
|
log.error(
|
||||||
|
"Input file is an image, but the resolution (DPI) is "
|
||||||
|
"not credible. Estimate the resolution at which the "
|
||||||
|
"image was scanned and specify it using --image-dpi.")
|
||||||
|
sys.exit(ExitCode.input_file)
|
||||||
|
elif not options.image_dpi:
|
||||||
|
log.info("Image size: (%d, %d)" % im.size)
|
||||||
|
log.error(
|
||||||
|
"Input file is an image, but has no resolution (DPI) "
|
||||||
|
"in its metadata. Estimate the resolution at which "
|
||||||
|
"image was scanned and specify it using --image-dpi.")
|
||||||
|
sys.exit(ExitCode.input_file)
|
||||||
|
|
||||||
|
if 'iccprofile' not in im.info:
|
||||||
|
if im.mode == 'RGB':
|
||||||
|
log.info('Input image has no ICC profile, assuming sRGB')
|
||||||
|
elif im.mode == 'CMYK':
|
||||||
|
log.info('Input CMYK image has no ICC profile, not usable')
|
||||||
|
sys.exit(ExitCode.input_file)
|
||||||
|
im.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.info("Image seems valid. Try converting to PDF...")
|
||||||
|
layout_fun = img2pdf.default_layout_fun
|
||||||
|
if options.image_dpi:
|
||||||
|
layout_fun = img2pdf.get_fixed_dpi_layout_fun(
|
||||||
|
(options.image_dpi, options.image_dpi))
|
||||||
|
with open(output_file, 'wb') as outf:
|
||||||
|
img2pdf.convert(
|
||||||
|
input_file,
|
||||||
|
layout_fun=layout_fun,
|
||||||
|
with_pdfrw=False,
|
||||||
|
outputstream=outf)
|
||||||
|
log.info("Successfully converted to PDF, processing...")
|
||||||
|
except img2pdf.ImageOpenError as e:
|
||||||
|
log.error(e)
|
||||||
|
sys.exit(ExitCode.input_file)
|
||||||
|
|
||||||
|
|
||||||
@transform(
|
@transform(
|
||||||
input=options.input_file,
|
input=options.input_file,
|
||||||
filter=formatter('(?i)\.pdf'),
|
filter=formatter('(?i)'),
|
||||||
output=os.path.join(work_folder, '{basename[0]}.repaired.pdf'),
|
output=os.path.join(work_folder, '{basename[0]}.pdf'),
|
||||||
|
extras=[_log])
|
||||||
|
def triage(
|
||||||
|
input_file,
|
||||||
|
output_file,
|
||||||
|
log):
|
||||||
|
try:
|
||||||
|
with open(input_file, 'rb') as f:
|
||||||
|
signature = f.read(4)
|
||||||
|
if signature == b'%PDF':
|
||||||
|
re_symlink(input_file, output_file)
|
||||||
|
return
|
||||||
|
except EnvironmentError as e:
|
||||||
|
log.error(e)
|
||||||
|
sys.exit(ExitCode.input_file)
|
||||||
|
|
||||||
|
triage_image_file(input_file, output_file, log)
|
||||||
|
|
||||||
|
|
||||||
|
@transform(
|
||||||
|
input=triage,
|
||||||
|
filter=suffix('.pdf'),
|
||||||
|
output='.repaired.pdf',
|
||||||
|
output_dir=work_folder,
|
||||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||||
def repair_pdf(
|
def repair_pdf(
|
||||||
input_file,
|
input_file,
|
||||||
@@ -400,31 +506,60 @@ def get_pageinfo(input_file, pdfinfo, pdfinfo_lock):
|
|||||||
return pageinfo
|
return pageinfo
|
||||||
|
|
||||||
|
|
||||||
|
def get_page_dpi(pageinfo):
|
||||||
|
"Get the DPI when nonsquare DPI is tolerable"
|
||||||
|
xres = max(pageinfo.get('xres', VECTOR_PAGE_DPI), options.oversample or 0)
|
||||||
|
yres = max(pageinfo.get('yres', VECTOR_PAGE_DPI), options.oversample or 0)
|
||||||
|
return (float(xres), float(yres))
|
||||||
|
|
||||||
|
|
||||||
|
def get_page_square_dpi(pageinfo):
|
||||||
|
"Get the DPI when we require xres == yres"
|
||||||
|
return float(max(
|
||||||
|
pageinfo.get('xres', VECTOR_PAGE_DPI),
|
||||||
|
pageinfo.get('yres', VECTOR_PAGE_DPI),
|
||||||
|
options.oversample or 0))
|
||||||
|
|
||||||
|
|
||||||
def is_ocr_required(pageinfo, log):
|
def is_ocr_required(pageinfo, log):
|
||||||
page = pageinfo['pageno'] + 1
|
page = pageinfo['pageno'] + 1
|
||||||
ocr_required = True
|
ocr_required = True
|
||||||
if not pageinfo['images']:
|
if not pageinfo['images']:
|
||||||
# If the page has no images, then it contains vector content or text
|
if options.force_ocr and options.oversample:
|
||||||
# or both. It seems quite unlikely that one would find meaningful text
|
# The user really wants to reprocess this file
|
||||||
# from rasterizing vector content. So skip the page.
|
log.info(
|
||||||
log.info(
|
"{0:4d}: page has no images - "
|
||||||
"{0:4d}: page has no images - skipping OCR".format(page)
|
"rasterizing at {1} DPI because "
|
||||||
)
|
"--force-ocr --oversample was specified".format(
|
||||||
ocr_required = False
|
page, options.oversample))
|
||||||
|
elif options.force_ocr:
|
||||||
|
# Warn the user they might not want to do this
|
||||||
|
log.warning(
|
||||||
|
"{0:4d}: page has no images - "
|
||||||
|
"all vector content will be "
|
||||||
|
"rasterized at {1} DPI, losing some resolution and likely "
|
||||||
|
"increasing file size. Use --oversample to adjust the "
|
||||||
|
"DPI.".format(page, VECTOR_PAGE_DPI))
|
||||||
|
else:
|
||||||
|
log.info(
|
||||||
|
"{0:4d}: page has no images - "
|
||||||
|
"skipping all processing on this page".format(page))
|
||||||
|
ocr_required = False
|
||||||
|
|
||||||
elif pageinfo['has_text']:
|
elif pageinfo['has_text']:
|
||||||
s = "{0:4d}: page already has text! – {1}"
|
msg = "{0:4d}: page already has text! – {1}"
|
||||||
|
|
||||||
if not options.force_ocr and not options.skip_text:
|
if not options.force_ocr and not options.skip_text:
|
||||||
log.error(s.format(page,
|
log.error(msg.format(page,
|
||||||
"aborting (use --force-ocr to force OCR)"))
|
"aborting (use --force-ocr to force OCR)"))
|
||||||
sys.exit(ExitCode.already_done_ocr)
|
sys.exit(ExitCode.already_done_ocr)
|
||||||
elif options.force_ocr:
|
elif options.force_ocr:
|
||||||
log.info(s.format(page,
|
log.info(msg.format(page,
|
||||||
"rasterizing text and running OCR anyway"))
|
"rasterizing text and running OCR anyway"))
|
||||||
ocr_required = True
|
ocr_required = True
|
||||||
elif options.skip_text:
|
elif options.skip_text:
|
||||||
log.info(s.format(page,
|
log.info(msg.format(page,
|
||||||
"skipping all processing on this page"))
|
"skipping all processing on this page"))
|
||||||
ocr_required = False
|
ocr_required = False
|
||||||
|
|
||||||
if ocr_required and options.skip_big:
|
if ocr_required and options.skip_big:
|
||||||
@@ -443,12 +578,17 @@ def is_ocr_required(pageinfo, log):
|
|||||||
os.path.join(work_folder, '*.page.pdf'),
|
os.path.join(work_folder, '*.page.pdf'),
|
||||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||||
def split_pages(
|
def split_pages(
|
||||||
input_file,
|
input_files,
|
||||||
output_files,
|
output_files,
|
||||||
log,
|
log,
|
||||||
pdfinfo,
|
pdfinfo,
|
||||||
pdfinfo_lock):
|
pdfinfo_lock):
|
||||||
|
|
||||||
|
if is_iterable_notstr(input_files):
|
||||||
|
input_file = input_files[0]
|
||||||
|
else:
|
||||||
|
input_file = input_files
|
||||||
|
|
||||||
for oo in output_files:
|
for oo in output_files:
|
||||||
with suppress(FileNotFoundError):
|
with suppress(FileNotFoundError):
|
||||||
os.unlink(oo)
|
os.unlink(oo)
|
||||||
@@ -529,15 +669,29 @@ def orient_page(
|
|||||||
270: '⇦'
|
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(
|
log.info(
|
||||||
'{0:4d}: page is facing {1}, confidence {2:.2f}{3}'.format(
|
'{0:4d}: page is facing {1}, confidence {2:.2f}{3}'.format(
|
||||||
page_number(preview),
|
page_number(preview),
|
||||||
direction.get(orient_conf.angle, '?'),
|
direction.get(orient_conf.angle, '?'),
|
||||||
orient_conf.confidence,
|
orient_conf.confidence,
|
||||||
' - correcting rotation' if orient_conf.angle != 0 else '')
|
description)
|
||||||
)
|
)
|
||||||
|
|
||||||
if orient_conf.angle == 0:
|
if not apply_correction:
|
||||||
re_symlink(page_pdf, output_file)
|
re_symlink(page_pdf, output_file)
|
||||||
else:
|
else:
|
||||||
writer = pypdf.PdfFileWriter()
|
writer = pypdf.PdfFileWriter()
|
||||||
@@ -583,11 +737,14 @@ def rasterize_with_ghostscript(
|
|||||||
device = 'pnggray'
|
device = 'pnggray'
|
||||||
|
|
||||||
log.debug("Rasterize {0} with {1}".format(
|
log.debug("Rasterize {0} with {1}".format(
|
||||||
os.path.basename(input_file), device))
|
os.path.basename(input_file), device))
|
||||||
xres = max(pageinfo['xres'], options.oversample or 0)
|
|
||||||
yres = max(pageinfo['yres'], options.oversample or 0)
|
|
||||||
|
|
||||||
ghostscript.rasterize_pdf(input_file, output_file, xres, yres, device, log)
|
# Produce the page image with square resolution or else deskew and OCR
|
||||||
|
# will not work properly
|
||||||
|
dpi = get_page_square_dpi(pageinfo)
|
||||||
|
ghostscript.rasterize_pdf(
|
||||||
|
input_file, output_file, xres=dpi, yres=dpi, raster_device=device,
|
||||||
|
log=log)
|
||||||
|
|
||||||
|
|
||||||
@transform(
|
@transform(
|
||||||
@@ -607,7 +764,7 @@ def preprocess_deskew(
|
|||||||
return
|
return
|
||||||
|
|
||||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||||
dpi = int(pageinfo['xres'])
|
dpi = get_page_square_dpi(pageinfo)
|
||||||
|
|
||||||
from . import leptonica
|
from . import leptonica
|
||||||
leptonica.deskew(input_file, output_file, dpi)
|
leptonica.deskew(input_file, output_file, dpi)
|
||||||
@@ -630,7 +787,7 @@ def preprocess_clean(
|
|||||||
return
|
return
|
||||||
|
|
||||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||||
dpi = int(pageinfo['xres'])
|
dpi = get_page_square_dpi(pageinfo)
|
||||||
|
|
||||||
unpaper.clean(input_file, output_file, dpi, log)
|
unpaper.clean(input_file, output_file, dpi, log)
|
||||||
|
|
||||||
@@ -681,13 +838,20 @@ def select_image_for_pdf(
|
|||||||
image = next(ii for ii in infiles if ii.endswith(image_suffix))
|
image = next(ii for ii in infiles if ii.endswith(image_suffix))
|
||||||
|
|
||||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||||
if all(image['enc'] == 'jpeg' for image in pageinfo['images']):
|
if all(orig_image['enc'] == 'jpeg' for orig_image in pageinfo['images']):
|
||||||
# If all images were JPEGs originally, produce a JPEG as output
|
# If all images were JPEGs originally, produce a JPEG as output
|
||||||
im = Image.open(image)
|
im = Image.open(image)
|
||||||
dpi = im.info.get(
|
|
||||||
'dpi',
|
# At this point the image should be a .png, but deskew, unpaper might
|
||||||
(int(pageinfo['xres']), int(pageinfo['yres']))
|
# have removed the DPI information. In this case, fall back to square
|
||||||
)
|
# DPI used to rasterize. When the preview image was rasterized, it
|
||||||
|
# was also converted to square resolution, which is what we want to
|
||||||
|
# give tesseract, so keep it square.
|
||||||
|
fallback_dpi = get_page_square_dpi(pageinfo)
|
||||||
|
dpi = im.info.get('dpi', (fallback_dpi, fallback_dpi))
|
||||||
|
|
||||||
|
# Pillow requires integer DPI
|
||||||
|
dpi = round(dpi[0]), round(dpi[1])
|
||||||
im.save(output_file, format='JPEG', dpi=dpi)
|
im.save(output_file, format='JPEG', dpi=dpi)
|
||||||
else:
|
else:
|
||||||
re_symlink(image, output_file)
|
re_symlink(image, output_file)
|
||||||
@@ -715,18 +879,16 @@ def select_image_layer(
|
|||||||
re_symlink(page_pdf, output_file)
|
re_symlink(page_pdf, output_file)
|
||||||
else:
|
else:
|
||||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'],
|
dpi = get_page_dpi(pageinfo)
|
||||||
options.oversample))
|
dpi = float(dpi[0]), float(dpi[1])
|
||||||
imgsize = ((img2pdf.ImgSize.dpi, dpi), (img2pdf.ImgSize.dpi, dpi))
|
layout_fun = img2pdf.get_fixed_dpi_layout_fun(dpi)
|
||||||
|
|
||||||
layout_fun = img2pdf.get_layout_fun(None, imgsize, None, None, None)
|
|
||||||
|
|
||||||
with open(image, 'rb') as imfile, \
|
with open(image, 'rb') as imfile, \
|
||||||
open(output_file, 'wb') as pdf:
|
open(output_file, 'wb') as pdf:
|
||||||
rawdata = imfile.read()
|
rawdata = imfile.read()
|
||||||
pdf.write(img2pdf.convert(
|
img2pdf.convert(
|
||||||
rawdata, producer="img2pdf", with_pdfrw=False,
|
rawdata, with_pdfrw=False,
|
||||||
layout_fun=layout_fun))
|
layout_fun=layout_fun, outputstream=pdf)
|
||||||
|
|
||||||
|
|
||||||
@active_if(options.pdf_renderer == 'hocr')
|
@active_if(options.pdf_renderer == 'hocr')
|
||||||
@@ -743,7 +905,7 @@ def render_hocr_page(
|
|||||||
pdfinfo_lock):
|
pdfinfo_lock):
|
||||||
hocr = input_file
|
hocr = input_file
|
||||||
pageinfo = get_pageinfo(hocr, pdfinfo, pdfinfo_lock)
|
pageinfo = get_pageinfo(hocr, pdfinfo, pdfinfo_lock)
|
||||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
|
dpi = get_page_square_dpi(pageinfo)
|
||||||
|
|
||||||
hocrtransform = HocrTransform(hocr, dpi)
|
hocrtransform = HocrTransform(hocr, dpi)
|
||||||
hocrtransform.to_pdf(output_file, imageFileName=None,
|
hocrtransform.to_pdf(output_file, imageFileName=None,
|
||||||
@@ -767,13 +929,17 @@ def render_hocr_debug_page(
|
|||||||
image = next(ii for ii in infiles if ii.endswith('.image'))
|
image = next(ii for ii in infiles if ii.endswith('.image'))
|
||||||
|
|
||||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
|
dpi = get_page_square_dpi(pageinfo)
|
||||||
|
|
||||||
hocrtransform = HocrTransform(hocr, dpi)
|
hocrtransform = HocrTransform(hocr, dpi)
|
||||||
hocrtransform.to_pdf(output_file, imageFileName=None,
|
hocrtransform.to_pdf(output_file, imageFileName=None,
|
||||||
showBoundingboxes=True, invisibleText=False)
|
showBoundingboxes=True, invisibleText=False)
|
||||||
|
|
||||||
|
|
||||||
|
class PdfMergeFailedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@active_if(options.pdf_renderer == 'hocr')
|
@active_if(options.pdf_renderer == 'hocr')
|
||||||
@collate(
|
@collate(
|
||||||
input=[render_hocr_page, select_image_layer],
|
input=[render_hocr_page, select_image_layer],
|
||||||
@@ -813,7 +979,7 @@ def add_text_layer(
|
|||||||
y2 = page_image.mediaBox.getUpperRight_y()
|
y2 = page_image.mediaBox.getUpperRight_y()
|
||||||
|
|
||||||
# Rotation occurs about the page's (0, 0). Most pages will have the media
|
# 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
|
# box at (0, 0) with all content in the first quadrant but some cropped
|
||||||
# files may have an offset mediabox. We translate the page so that its
|
# 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
|
# bottom left corner after rotation is pinned to (0, 0) with the image
|
||||||
# in the first quadrant.
|
# in the first quadrant.
|
||||||
@@ -832,8 +998,12 @@ def add_text_layer(
|
|||||||
log.info("{0:4d}: rotating image layer {1} degrees".format(
|
log.info("{0:4d}: rotating image layer {1} degrees".format(
|
||||||
page_number(image), rotation, tx, ty))
|
page_number(image), rotation, tx, ty))
|
||||||
|
|
||||||
page_text.mergeRotatedScaledTranslatedPage(
|
try:
|
||||||
page_image, rotation, 1.0, tx, ty, expand=False)
|
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 = pypdf.PdfFileWriter()
|
||||||
pdf_output.addPage(page_text)
|
pdf_output.addPage(page_text)
|
||||||
@@ -873,6 +1043,40 @@ def tesseract_ocr_and_render_pdf(
|
|||||||
log=log)
|
log=log)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pdfmark(base_pdf):
|
||||||
|
def from_document_info(key):
|
||||||
|
# pdf.documentInfo.get() DOES NOT behave as expected for a dict-like
|
||||||
|
# object, so call with precautions. TypeError may occur if the PDF
|
||||||
|
# is missing the optional document info section.
|
||||||
|
try:
|
||||||
|
s = base_pdf.documentInfo[key]
|
||||||
|
return str(s)
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
pdfmark = {
|
||||||
|
'/Title': from_document_info('/Title'),
|
||||||
|
'/Author': from_document_info('/Author'),
|
||||||
|
'/Keywords': from_document_info('/Keywords'),
|
||||||
|
'/Subject': from_document_info('/Subject'),
|
||||||
|
}
|
||||||
|
if options.title:
|
||||||
|
pdfmark['/Title'] = options.title
|
||||||
|
if options.author:
|
||||||
|
pdfmark['/Author'] = options.author
|
||||||
|
if options.keywords:
|
||||||
|
pdfmark['/Keywords'] = options.keywords
|
||||||
|
if options.subject:
|
||||||
|
pdfmark['/Subject'] = options.subject
|
||||||
|
|
||||||
|
pdfmark['/Creator'] = '{0} {1} / Tesseract OCR{2} {3}'.format(
|
||||||
|
parser.prog, VERSION,
|
||||||
|
'+PDF' if options.pdf_renderer == 'tesseract' else '',
|
||||||
|
tesseract.version())
|
||||||
|
return pdfmark
|
||||||
|
|
||||||
|
|
||||||
|
@active_if(options.output_type == 'pdfa')
|
||||||
@transform(
|
@transform(
|
||||||
input=repair_pdf,
|
input=repair_pdf,
|
||||||
filter=formatter(r'\.repaired\.pdf'),
|
filter=formatter(r'\.repaired\.pdf'),
|
||||||
@@ -884,37 +1088,7 @@ def generate_postscript_stub(
|
|||||||
log):
|
log):
|
||||||
|
|
||||||
pdf = pypdf.PdfFileReader(input_file)
|
pdf = pypdf.PdfFileReader(input_file)
|
||||||
|
pdfmark = get_pdfmark(pdf)
|
||||||
def from_document_info(key):
|
|
||||||
# pdf.documentInfo.get() DOES NOT behave as expected for a dict-like
|
|
||||||
# object, so call with precautions. TypeError may occur if the PDF
|
|
||||||
# is missing the optional document info section.
|
|
||||||
try:
|
|
||||||
s = pdf.documentInfo[key]
|
|
||||||
return str(s)
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
pdfmark = {
|
|
||||||
'title': from_document_info('/Title'),
|
|
||||||
'author': from_document_info('/Author'),
|
|
||||||
'keywords': from_document_info('/Keywords'),
|
|
||||||
'subject': from_document_info('/Subject'),
|
|
||||||
}
|
|
||||||
if options.title:
|
|
||||||
pdfmark['title'] = options.title
|
|
||||||
if options.author:
|
|
||||||
pdfmark['author'] = options.author
|
|
||||||
if options.keywords:
|
|
||||||
pdfmark['keywords'] = options.keywords
|
|
||||||
if options.subject:
|
|
||||||
pdfmark['subject'] = options.subject
|
|
||||||
|
|
||||||
pdfmark['creator'] = '{0} {1} / Tesseract OCR{2} {3}'.format(
|
|
||||||
parser.prog, VERSION,
|
|
||||||
'+PDF' if options.pdf_renderer == 'tesseract' else '',
|
|
||||||
tesseract.version())
|
|
||||||
|
|
||||||
generate_pdfa_def(output_file, pdfmark)
|
generate_pdfa_def(output_file, pdfmark)
|
||||||
|
|
||||||
|
|
||||||
@@ -935,12 +1109,13 @@ def skip_page(
|
|||||||
re_symlink(input_file, output_file, log)
|
re_symlink(input_file, output_file, log)
|
||||||
|
|
||||||
|
|
||||||
|
@active_if(options.output_type == 'pdfa')
|
||||||
@merge(
|
@merge(
|
||||||
input=[add_text_layer, render_hocr_debug_page, skip_page,
|
input=[add_text_layer, render_hocr_debug_page, skip_page,
|
||||||
tesseract_ocr_and_render_pdf, generate_postscript_stub],
|
tesseract_ocr_and_render_pdf, generate_postscript_stub],
|
||||||
output=os.path.join(work_folder, 'merged.pdf'),
|
output=os.path.join(work_folder, 'merged.pdf'),
|
||||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||||
def merge_pages(
|
def merge_pages_ghostscript(
|
||||||
input_files,
|
input_files,
|
||||||
output_file,
|
output_file,
|
||||||
log,
|
log,
|
||||||
@@ -965,26 +1140,66 @@ def merge_pages(
|
|||||||
ghostscript.generate_pdfa(pdf_pages, output_file, options.jobs or 1)
|
ghostscript.generate_pdfa(pdf_pages, output_file, options.jobs or 1)
|
||||||
|
|
||||||
|
|
||||||
@transform(
|
@active_if(options.output_type == 'pdf')
|
||||||
input=merge_pages,
|
@merge(
|
||||||
filter=formatter(),
|
input=[add_text_layer, render_hocr_debug_page, skip_page,
|
||||||
output=options.output_file,
|
tesseract_ocr_and_render_pdf, repair_pdf],
|
||||||
|
output=os.path.join(work_folder, 'merged.pdf'),
|
||||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||||
def copy_final(
|
def merge_pages_qpdf(
|
||||||
input_file,
|
input_files,
|
||||||
output_file,
|
output_file,
|
||||||
log,
|
log,
|
||||||
pdfinfo,
|
pdfinfo,
|
||||||
pdfinfo_lock):
|
pdfinfo_lock):
|
||||||
|
|
||||||
|
metadata_file = next(
|
||||||
|
(ii for ii in input_files if ii.endswith('.repaired.pdf')))
|
||||||
|
input_files.remove(metadata_file)
|
||||||
|
|
||||||
|
def input_file_order(s):
|
||||||
|
'''Sort order: All rendered pages followed
|
||||||
|
by their debug page.'''
|
||||||
|
key = int(os.path.basename(s)[0:6]) * 10
|
||||||
|
if 'debug' in os.path.basename(s):
|
||||||
|
key += 1
|
||||||
|
return key
|
||||||
|
|
||||||
|
pdf_pages = sorted(input_files, key=input_file_order)
|
||||||
|
log.debug("Final pages: " + "\n".join(pdf_pages))
|
||||||
|
|
||||||
|
reader_metadata = pypdf.PdfFileReader(metadata_file)
|
||||||
|
pdfmark = get_pdfmark(reader_metadata)
|
||||||
|
pdfmark['/Producer'] = 'qpdf ' + qpdf.version()
|
||||||
|
|
||||||
|
first_page = pypdf.PdfFileReader(pdf_pages[0])
|
||||||
|
|
||||||
|
writer = pypdf.PdfFileWriter()
|
||||||
|
writer.appendPagesFromReader(first_page)
|
||||||
|
writer.addMetadata(pdfmark)
|
||||||
|
writer_file = pdf_pages[0].replace('.pdf', '.metadata.pdf')
|
||||||
|
with open(writer_file, 'wb') as f:
|
||||||
|
writer.write(f)
|
||||||
|
|
||||||
|
pdf_pages[0] = writer_file
|
||||||
|
|
||||||
|
qpdf.merge(pdf_pages, output_file)
|
||||||
|
|
||||||
|
|
||||||
|
@merge(
|
||||||
|
input=[merge_pages_ghostscript, merge_pages_qpdf],
|
||||||
|
output=options.output_file,
|
||||||
|
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||||
|
def copy_final(
|
||||||
|
input_files,
|
||||||
|
output_file,
|
||||||
|
log,
|
||||||
|
pdfinfo,
|
||||||
|
pdfinfo_lock):
|
||||||
|
input_file = next((ii for ii in input_files if ii.endswith('.pdf')))
|
||||||
shutil.copy(input_file, output_file)
|
shutil.copy(input_file, output_file)
|
||||||
|
|
||||||
|
|
||||||
def validate_pdfa(
|
|
||||||
input_file,
|
|
||||||
log):
|
|
||||||
return qpdf.check(input_file, log)
|
|
||||||
|
|
||||||
|
|
||||||
def available_cpu_count():
|
def available_cpu_count():
|
||||||
try:
|
try:
|
||||||
return multiprocessing.cpu_count()
|
return multiprocessing.cpu_count()
|
||||||
@@ -1010,58 +1225,126 @@ def cleanup_ruffus_error_message(msg):
|
|||||||
return 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 == 'builtins.KeyboardInterrupt':
|
||||||
|
_log.error("Interrupted by user")
|
||||||
|
return ExitCode.ctrl_c
|
||||||
|
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
|
||||||
|
|
||||||
|
if not options.verbose:
|
||||||
|
_log.error(exc_stack)
|
||||||
|
return ExitCode.other_error
|
||||||
|
|
||||||
|
|
||||||
|
def traverse_ruffus_exception(e_args):
|
||||||
|
"""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_args, Sequence) and isinstance(e_args[0], str) and \
|
||||||
|
len(e_args) == 5:
|
||||||
|
return do_ruffus_exception(e_args)
|
||||||
|
elif is_iterable_notstr(e_args):
|
||||||
|
for exc in e_args:
|
||||||
|
return traverse_ruffus_exception(exc)
|
||||||
|
|
||||||
|
|
||||||
def run_pipeline():
|
def run_pipeline():
|
||||||
if not options.jobs:
|
if not options.jobs:
|
||||||
options.jobs = available_cpu_count()
|
options.jobs = available_cpu_count()
|
||||||
try:
|
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)
|
cmdline.run(options)
|
||||||
except ruffus_exceptions.RethrownJobError as e:
|
except ruffus_exceptions.RethrownJobError as e:
|
||||||
if options.verbose:
|
if options.verbose:
|
||||||
_log.debug(e)
|
_log.debug(str(e)) # stringify exception so logger doesn't have to
|
||||||
|
|
||||||
# Yuck. Hunt through the ruffus exception to find out what the
|
# Ruffus flattens exception to 5 element tuples. Because of a bug
|
||||||
# return code is supposed to be.
|
# in <= 2.6.3 it may present either the single:
|
||||||
# Ruffus flattens the exception to a string, throwing away all kinds
|
# (task, job, exc, value, stack)
|
||||||
# of helpful details
|
# or something like:
|
||||||
# task_name, job_name - ruffus status
|
# [[(task, job, exc, value, stack)]]
|
||||||
# exc_name - class name of exception
|
#
|
||||||
# exc_value - irritating string that makes impossible to recover
|
# Generally cross-process exception marshalling doesn't work well
|
||||||
# exception object
|
# and ruffus doesn't support because BaseException has its own
|
||||||
# exc_stack - string that contains traceback of exception
|
# implementation of __reduce__ that attempts to reconstruct the
|
||||||
for exc in e.args:
|
# exception based on e.__init__(e.args).
|
||||||
task_name, job_name, exc_name, exc_value, exc_stack = exc
|
#
|
||||||
if exc_name == 'builtins.SystemExit':
|
# Attempting to log the exception directly marshalls it to the logger
|
||||||
match = re.search(r"\.(.+?)\)", exc_value)
|
# which is probably in another process, so it's better to log only
|
||||||
exit_code_name = match.groups()[0]
|
# data from the exception at this point.
|
||||||
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 not options.verbose:
|
|
||||||
_log.error(e)
|
|
||||||
|
|
||||||
return ExitCode.other_error
|
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:
|
except Exception as e:
|
||||||
_log.error(e)
|
_log.error(e)
|
||||||
return ExitCode.other_error
|
return ExitCode.other_error
|
||||||
|
|
||||||
if not validate_pdfa(options.output_file, _log):
|
if options.output_type == 'pdfa':
|
||||||
_log.warning('Output file: The generated PDF/A file is INVALID')
|
pdfa_info = file_claims_pdfa(options.output_file)
|
||||||
return ExitCode.invalid_output_pdfa
|
if pdfa_info['pass']:
|
||||||
|
_log.info(pdfa_info['message'])
|
||||||
|
else:
|
||||||
|
_log.warning(pdfa_info['message'])
|
||||||
|
return ExitCode.invalid_output_pdf
|
||||||
|
|
||||||
|
if not qpdf.check(options.output_file, _log):
|
||||||
|
_log.warning('Output file: The generated PDF is INVALID')
|
||||||
|
return ExitCode.invalid_output_pdf
|
||||||
|
|
||||||
with _pdfinfo_lock:
|
with _pdfinfo_lock:
|
||||||
_log.debug(_pdfinfo)
|
_log.debug(_pdfinfo)
|
||||||
|
|||||||
+63
-48
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
|
from math import hypot
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import PyPDF2 as pypdf
|
import PyPDF2 as pypdf
|
||||||
@@ -34,7 +35,12 @@ FRIENDLY_ENCODING = {
|
|||||||
'/JPXDecode': 'jpx',
|
'/JPXDecode': 'jpx',
|
||||||
'/JBIG2Decode': 'jbig2',
|
'/JBIG2Decode': 'jbig2',
|
||||||
'/CCF': 'ccitt', # Abbreviations permitted in inline images
|
'/CCF': 'ccitt', # Abbreviations permitted in inline images
|
||||||
'/DCT': 'jpeg'
|
'/DCT': 'jpeg',
|
||||||
|
'/AHx': 'asciihex',
|
||||||
|
'/A85': 'ascii85',
|
||||||
|
'/LZW': 'lzw',
|
||||||
|
'/Fl': 'flate',
|
||||||
|
'/RL': 'runlength'
|
||||||
}
|
}
|
||||||
|
|
||||||
FRIENDLY_COMP = {
|
FRIENDLY_COMP = {
|
||||||
@@ -67,14 +73,9 @@ def _shorthand_from_matrix(matrix):
|
|||||||
return tuple(map(float, (a, b, c, d, e, f)))
|
return tuple(map(float, (a, b, c, d, e, f)))
|
||||||
|
|
||||||
|
|
||||||
def euclidean_distance(rowvec1, rowvec2):
|
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||||
return ((rowvec1[0] - rowvec2[0]) ** 2
|
|
||||||
+ (rowvec1[1] - rowvec2[1]) ** 2) ** 0.5
|
|
||||||
|
|
||||||
|
|
||||||
ContentsInfo = namedtuple('ContentsInfo',
|
|
||||||
['raster_settings', 'inline_images'])
|
|
||||||
|
|
||||||
def _interpret_contents(contentstream):
|
def _interpret_contents(contentstream):
|
||||||
operations = contentstream.operations
|
operations = contentstream.operations
|
||||||
stack = []
|
stack = []
|
||||||
@@ -86,11 +87,13 @@ def _interpret_contents(contentstream):
|
|||||||
operands, command = op
|
operands, command = op
|
||||||
if command == b'q':
|
if command == b'q':
|
||||||
stack.append(ctm)
|
stack.append(ctm)
|
||||||
|
if len(stack) > 32:
|
||||||
|
raise RuntimeError("PDF graphics stack overflow")
|
||||||
elif command == b'Q':
|
elif command == b'Q':
|
||||||
ctm = stack.pop()
|
ctm = stack.pop()
|
||||||
elif command == b'cm':
|
elif command == b'cm':
|
||||||
ctm = matrix_mult(
|
ctm = matrix_mult(
|
||||||
ctm, _matrix_from_shorthand(operands))
|
_matrix_from_shorthand(operands), ctm)
|
||||||
elif command == b'Do':
|
elif command == b'Do':
|
||||||
image_name = operands[0]
|
image_name = operands[0]
|
||||||
image_raster_settings.append(
|
image_raster_settings.append(
|
||||||
@@ -119,21 +122,31 @@ def _get_dpi(ctm_shorthand, image_size):
|
|||||||
it is not sufficient to assume that the image fills the page, even though
|
it is not sufficient to assume that the image fills the page, even though
|
||||||
that is the most common case.
|
that is the most common case.
|
||||||
|
|
||||||
This code solves the general case where the image may be scaled (always),
|
A PDF image may be scaled (always), cropped, translated, rotated in place
|
||||||
cropped, translated (often), and rotated in place (occasionally) to an
|
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
|
||||||
arbitrary angle (rare). It will work as long as the image is a
|
be expressed, that is, it is not necessary to consider distortions where
|
||||||
parallelogram from the perspective of a rectilinear coordinate system.
|
the effective DPI varies with position.
|
||||||
It does not work for arbitrarily quadrilaterals that might be produced
|
|
||||||
by shearing, but by that point DPI becomes a linear gradient rather than
|
|
||||||
constant over the image.
|
|
||||||
|
|
||||||
The transformation matrix describes the coordinate system at the time of
|
To determine the image scale, transform an offset axis vector v0 (0, 0),
|
||||||
rendering. We transform the image corner locations into the coordinate
|
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
|
||||||
system and measure the width and height within the system, expressed in
|
which gives the dimensions of the image in PDF units. From there we can
|
||||||
PDF units. From there we can compare to the actual image dimensions.
|
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
|
pdfimages -list does calculate the DPI in some way that is not completely
|
||||||
naive, but it does not the DPI of rotated images right, so cannot be
|
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
|
used anymore to validate this. Photoshop works, or using Acrobat to
|
||||||
rotate the image back to normal.
|
rotate the image back to normal.
|
||||||
|
|
||||||
@@ -141,31 +154,12 @@ def _get_dpi(ctm_shorthand, image_size):
|
|||||||
/MediaBox.
|
/MediaBox.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
matrix = _matrix_from_shorthand(ctm_shorthand)
|
|
||||||
|
|
||||||
# Corners of the image in untransformed unit space; last
|
a, b, c, d, _, _ = ctm_shorthand
|
||||||
# column is a dummy to assist matrix math
|
|
||||||
corners = [[0, 0, 1],
|
|
||||||
[1, 0, 1],
|
|
||||||
[0, 1, 1],
|
|
||||||
[1, 1, 1]]
|
|
||||||
|
|
||||||
# Rotate/translate/scale the corners into PDF coords (1/72")
|
# Calculate the width and height of the image in PDF units
|
||||||
# ordering of points may change, e.g. if rotation is 180 then
|
image_drawn_width = hypot(a, b)
|
||||||
# the point (0, 0) may become the top right
|
image_drawn_height = hypot(c, d)
|
||||||
# The row vectors can all be transformed together here by building
|
|
||||||
# a matrix of them
|
|
||||||
page_unit_corners = matrix_mult(corners, matrix)
|
|
||||||
|
|
||||||
# Calculate the width and height of the rotated image
|
|
||||||
# the transformation matrix so the corner that was originally
|
|
||||||
# (1, 1) can be ignored
|
|
||||||
image_drawn_width = euclidean_distance(
|
|
||||||
page_unit_corners[0], page_unit_corners[1])
|
|
||||||
image_drawn_height = euclidean_distance(
|
|
||||||
page_unit_corners[0], page_unit_corners[2])
|
|
||||||
|
|
||||||
# print((image_drawn_width, image_drawn_height))
|
|
||||||
|
|
||||||
# The scale of the image is pixels per PDF unit (1/72")
|
# The scale of the image is pixels per PDF unit (1/72")
|
||||||
scale_w = image_size[0] / image_drawn_width
|
scale_w = image_size[0] / image_drawn_width
|
||||||
@@ -178,7 +172,8 @@ def _get_dpi(ctm_shorthand, image_size):
|
|||||||
return (dpi_w, dpi_h)
|
return (dpi_w, dpi_h)
|
||||||
|
|
||||||
|
|
||||||
def _find_page_images(page, pageinfo, contentsinfo):
|
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||||
|
"Find inline images on the page"
|
||||||
|
|
||||||
for n, im in enumerate(contentsinfo.inline_images):
|
for n, im in enumerate(contentsinfo.inline_images):
|
||||||
settings, shorthand = im
|
settings, shorthand = im
|
||||||
@@ -189,12 +184,22 @@ def _find_page_images(page, pageinfo, contentsinfo):
|
|||||||
image['bpc'] = settings['/BPC']
|
image['bpc'] = settings['/BPC']
|
||||||
image['color'] = FRIENDLY_COLORSPACE.get(settings['/CS'], '-')
|
image['color'] = FRIENDLY_COLORSPACE.get(settings['/CS'], '-')
|
||||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
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']))
|
dpi_w, dpi_h = _get_dpi(shorthand, (image['width'], image['height']))
|
||||||
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
||||||
yield image
|
yield image
|
||||||
|
|
||||||
# Look for XObject (out of line images)
|
|
||||||
|
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||||
|
"Find images stored in XObject resources"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page['/Resources']['/XObject']
|
page['/Resources']['/XObject']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -204,9 +209,6 @@ def _find_page_images(page, pageinfo, contentsinfo):
|
|||||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
pdfimage = page['/Resources']['/XObject'][xobj]
|
||||||
if pdfimage['/Subtype'] != '/Image':
|
if pdfimage['/Subtype'] != '/Image':
|
||||||
continue
|
continue
|
||||||
if '/ImageMask' in pdfimage:
|
|
||||||
if pdfimage['/ImageMask']:
|
|
||||||
continue
|
|
||||||
image = {}
|
image = {}
|
||||||
image['name'] = str(xobj)
|
image['name'] = str(xobj)
|
||||||
image['width'] = pdfimage['/Width']
|
image['width'] = pdfimage['/Width']
|
||||||
@@ -228,6 +230,14 @@ def _find_page_images(page, pageinfo, contentsinfo):
|
|||||||
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
|
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
|
||||||
|
|
||||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||||
|
|
||||||
|
# Bit of a hack... infer grayscale if component count is uncertain
|
||||||
|
# but encoding must be monochrome. This happens if a monochrome image
|
||||||
|
# has an ICC profile attached. Better solution would be to examine
|
||||||
|
# the ICC profile.
|
||||||
|
if image['comp'] == '?' and image['enc'] in ('ccitt', 'jbig2'):
|
||||||
|
image['comp'] = FRIENDLY_COMP['gray']
|
||||||
|
|
||||||
image['dpi_w'] = image['dpi_h'] = 0
|
image['dpi_w'] = image['dpi_h'] = 0
|
||||||
|
|
||||||
for raster in contentsinfo.raster_settings:
|
for raster in contentsinfo.raster_settings:
|
||||||
@@ -250,6 +260,11 @@ def _find_page_images(page, pageinfo, contentsinfo):
|
|||||||
yield image
|
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):
|
def _page_has_text(pdf, page):
|
||||||
# Simple test
|
# Simple test
|
||||||
text = page.extractText()
|
text = page.extractText()
|
||||||
|
|||||||
+56
-8
@@ -7,8 +7,9 @@ from __future__ import print_function, absolute_import, division
|
|||||||
from string import Template
|
from string import Template
|
||||||
import codecs
|
import codecs
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import PyPDF2 as pypdf
|
||||||
|
|
||||||
ICC_PROFILE_RELPATH = 'data/sRGB_IEC61966-2-1_black_scaled.icc'
|
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
|
||||||
|
|
||||||
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
|
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
|
||||||
'ocrmypdf', ICC_PROFILE_RELPATH)
|
'ocrmypdf', ICC_PROFILE_RELPATH)
|
||||||
@@ -74,8 +75,14 @@ def encode_text_string(s: str) -> str:
|
|||||||
Postscript file to be completely ASCII and no escaping of Postscript
|
Postscript file to be completely ASCII and no escaping of Postscript
|
||||||
characters is necessary.
|
characters is necessary.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# Sometimes lazy C programmer leave their NULs at the end of strings
|
||||||
|
# tests/resources/aspect.pdf is one example (created by ImageMagick)
|
||||||
|
s = s.replace('\x00', '')
|
||||||
|
|
||||||
if s == '':
|
if s == '':
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
utf16_bytes = s.encode('utf-16be')
|
utf16_bytes = s.encode('utf-16be')
|
||||||
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
|
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
|
||||||
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
|
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
|
||||||
@@ -88,11 +95,11 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
|||||||
t = Template(pdfa_def_template)
|
t = Template(pdfa_def_template)
|
||||||
result = t.substitute(icc_profile=icc_profile,
|
result = t.substitute(icc_profile=icc_profile,
|
||||||
icc_identifier=icc_identifier,
|
icc_identifier=icc_identifier,
|
||||||
title=pdfmark_utf16.get('title', ''),
|
title=pdfmark_utf16.get('/Title', ''),
|
||||||
author=pdfmark_utf16.get('author', ''),
|
author=pdfmark_utf16.get('/Author', ''),
|
||||||
subject=pdfmark_utf16.get('subject', ''),
|
subject=pdfmark_utf16.get('/Subject', ''),
|
||||||
creator=pdfmark_utf16.get('creator', ''),
|
creator=pdfmark_utf16.get('/Creator', ''),
|
||||||
keywords=pdfmark_utf16.get('keywords', ''))
|
keywords=pdfmark_utf16.get('/Keywords', ''))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +111,48 @@ def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
|
|||||||
|
|
||||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
||||||
|
|
||||||
# Since PostScript might not handle UTF-8 (it's hard to get a clear
|
# We should have encoded everything to pure ASCII by this point, and
|
||||||
# answer), insist on ascii
|
# to be safe, only allow ASCII in PostScript
|
||||||
with open(target_filename, 'w', encoding='ascii') as f:
|
with open(target_filename, 'w', encoding='ascii') as f:
|
||||||
f.write(ps)
|
f.write(ps)
|
||||||
|
|
||||||
|
|
||||||
|
def file_claims_pdfa(filename):
|
||||||
|
"""Determines if the file claims to be PDF/A compliant
|
||||||
|
|
||||||
|
Checking if a file is a truly compliant PDF/A is a massive undertaking
|
||||||
|
that no open source tool does properly. Some commercial tools are
|
||||||
|
generally reliable (Acrobat).
|
||||||
|
|
||||||
|
This checks if the XMP metadata contains a PDF/A marker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pdf = pypdf.PdfFileReader(filename)
|
||||||
|
xmp = pdf.getXmpMetadata()
|
||||||
|
|
||||||
|
try:
|
||||||
|
pdfa_nodes = xmp.getNodesInNamespace(
|
||||||
|
aboutUri='',
|
||||||
|
namespace='http://www.aiim.org/pdfa/ns/id/')
|
||||||
|
except AttributeError:
|
||||||
|
return {'pass': False, 'output': 'pdf', 'message': 'No XMP metadata'}
|
||||||
|
|
||||||
|
pdfa_dict = {attr.localName: attr.value for attr in pdfa_nodes}
|
||||||
|
pdfa_dict['pass'] = False
|
||||||
|
pdfa_dict['output'] = 'pdf'
|
||||||
|
if pdfa_dict:
|
||||||
|
part_conformance = pdfa_dict['part'] + pdfa_dict['conformance']
|
||||||
|
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||||
|
|
||||||
|
message = 'File claims to be PDF/A-{}'.format(
|
||||||
|
part_conformance)
|
||||||
|
|
||||||
|
if part_conformance in valid_part_conforms:
|
||||||
|
pdfa_dict['pass'] = True
|
||||||
|
pdfa_dict['output'] = 'pdfa'
|
||||||
|
pdfa_dict['message'] = message
|
||||||
|
else:
|
||||||
|
pdfa_dict['message'] = 'File is a regular PDF'
|
||||||
|
|
||||||
|
return pdfa_dict
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -2,12 +2,32 @@
|
|||||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||||
|
|
||||||
from subprocess import CalledProcessError, check_output, STDOUT, check_call
|
from subprocess import CalledProcessError, check_output, STDOUT, check_call
|
||||||
|
from functools import lru_cache
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from . import ExitCode, get_program
|
from . import ExitCode, get_program
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def version():
|
||||||
|
args_qpdf = [
|
||||||
|
get_program('qpdf'),
|
||||||
|
'--version'
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
versions = check_output(
|
||||||
|
args_qpdf, close_fds=True, universal_newlines=True,
|
||||||
|
stderr=STDOUT)
|
||||||
|
except CalledProcessError:
|
||||||
|
print("Could not find qpdf executable on system PATH.")
|
||||||
|
sys.exit(ExitCode.missing_dependency)
|
||||||
|
|
||||||
|
qpdf_version = re.match(r'qpdf version (.+)', versions).group(1)
|
||||||
|
return qpdf_version
|
||||||
|
|
||||||
|
|
||||||
def check(input_file, log):
|
def check(input_file, log):
|
||||||
args_qpdf = [
|
args_qpdf = [
|
||||||
get_program('qpdf'),
|
get_program('qpdf'),
|
||||||
@@ -20,7 +40,7 @@ def check(input_file, log):
|
|||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
if e.returncode == 2:
|
if e.returncode == 2:
|
||||||
log.error("{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))
|
input_file))
|
||||||
log.error("Details:")
|
log.error("Details:")
|
||||||
log.error(e.output)
|
log.error(e.output)
|
||||||
elif e.returncode == 3:
|
elif e.returncode == 3:
|
||||||
@@ -85,3 +105,15 @@ def split_pages(input_file, work_folder, npages):
|
|||||||
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
||||||
]
|
]
|
||||||
check_call(args_qpdf)
|
check_call(args_qpdf)
|
||||||
|
|
||||||
|
|
||||||
|
def merge(input_files, output_file):
|
||||||
|
"""Merge the list of input files (all filenames) into the output file.
|
||||||
|
|
||||||
|
The input files may contain one or more pages.
|
||||||
|
"""
|
||||||
|
args_qpdf = [
|
||||||
|
get_program('qpdf'), input_files[0], '--pages'
|
||||||
|
] + input_files + ['--', output_file]
|
||||||
|
check_call(args_qpdf)
|
||||||
|
|
||||||
|
|||||||
+15
-14
@@ -3,7 +3,7 @@
|
|||||||
# unpaper documentation:
|
# unpaper documentation:
|
||||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
# 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
|
from tempfile import NamedTemporaryFile
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -17,10 +17,9 @@ def version():
|
|||||||
get_program('unpaper'),
|
get_program('unpaper'),
|
||||||
'--version'
|
'--version'
|
||||||
]
|
]
|
||||||
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
|
version = check_output(
|
||||||
stdout=PIPE, stderr=PIPE)
|
args_unpaper, close_fds=True, universal_newlines=True,
|
||||||
version, _ = p_unpaper.communicate(timeout=5)
|
stderr=STDOUT, timeout=5)
|
||||||
|
|
||||||
return version.strip()
|
return version.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
|
|||||||
os.unlink(output_pnm.name)
|
os.unlink(output_pnm.name)
|
||||||
|
|
||||||
args_unpaper.extend([input_pnm.name, output_pnm.name])
|
args_unpaper.extend([input_pnm.name, output_pnm.name])
|
||||||
p_unpaper = Popen(
|
try:
|
||||||
args_unpaper, close_fds=True,
|
stdout = check_output(
|
||||||
universal_newlines=True, stdout=PIPE, stderr=PIPE
|
args_unpaper, close_fds=True,
|
||||||
)
|
universal_newlines=True, stderr=STDOUT,
|
||||||
out, err = p_unpaper.communicate()
|
)
|
||||||
log.debug(out)
|
except CalledProcessError as e:
|
||||||
log.debug(err)
|
log.debug(e.output)
|
||||||
|
raise e from e
|
||||||
Image.open(output_pnm.name).save(output_file)
|
else:
|
||||||
|
log.debug(stdout)
|
||||||
|
Image.open(output_pnm.name).save(output_file)
|
||||||
|
|
||||||
|
|
||||||
def deskew(input_file, output_file, dpi, log):
|
def deskew(input_file, output_file, dpi, log):
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,9 @@
|
|||||||
|
# requirements.txt can be used to replicate the developer's build environment
|
||||||
|
# setup.py lists a separate set of requirements that are looser to simplify
|
||||||
|
# installation
|
||||||
ruffus==2.6.3
|
ruffus==2.6.3
|
||||||
Pillow==3.1.1
|
Pillow==3.3.0
|
||||||
reportlab==3.2.0
|
reportlab==3.2.0
|
||||||
PyPDF2==1.25.1
|
PyPDF2==1.26
|
||||||
img2pdf==0.2
|
img2pdf==0.2.1
|
||||||
cffi==1.5.2
|
cffi==1.5.2
|
||||||
|
|||||||
@@ -154,23 +154,23 @@ if command.startswith('install') or \
|
|||||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||||
check_external_program(
|
check_external_program(
|
||||||
program='tesseract',
|
program='tesseract',
|
||||||
need_version='3.02.02',
|
need_version='3.03', # limited by Travis CI / Ubuntu 12.04 backports
|
||||||
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
|
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
|
||||||
)
|
)
|
||||||
check_external_program(
|
check_external_program(
|
||||||
program='gs',
|
program='gs',
|
||||||
need_version='9.14',
|
need_version='9.15', # limited by Travis CI / Ubuntu 12.04 backports
|
||||||
package='ghostscript'
|
package='ghostscript'
|
||||||
)
|
)
|
||||||
check_external_program(
|
check_external_program(
|
||||||
program='unpaper',
|
program='unpaper',
|
||||||
need_version='6.1',
|
need_version='6.1', # latest sane version
|
||||||
package='unpaper',
|
package='unpaper',
|
||||||
optional=True
|
optional=True
|
||||||
)
|
)
|
||||||
check_external_program(
|
check_external_program(
|
||||||
program='qpdf',
|
program='qpdf',
|
||||||
need_version='5.0.0',
|
need_version='5.0.0', # limited by Travis CI / Ubuntu 12.04 backports
|
||||||
package='qpdf',
|
package='qpdf',
|
||||||
version_check_args=['--version']
|
version_check_args=['--version']
|
||||||
)
|
)
|
||||||
@@ -217,12 +217,12 @@ setup(
|
|||||||
'ocrmypdf/lib/compile_leptonica.py:ffi'
|
'ocrmypdf/lib/compile_leptonica.py:ffi'
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'ruffus>=2.6.3',
|
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
|
||||||
'Pillow>=3.0.0',
|
'Pillow>=3.1.0', # Pillow is pretty stable
|
||||||
'reportlab>=3.1.44',
|
'reportlab>=3.2.0', # oldest released version with sane image handling
|
||||||
'PyPDF2>=1.25.1',
|
'PyPDF2>=1.26', # pure Python, so track HEAD closely
|
||||||
'img2pdf>=0.2.0',
|
'img2pdf>=0.2.1', # pure Python, so track HEAD closely
|
||||||
'cffi>=1.5.0'
|
'cffi>=1.5.0' # oldest version ever tested
|
||||||
],
|
],
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
entry_points={
|
entry_points={
|
||||||
@@ -230,6 +230,6 @@ setup(
|
|||||||
'ocrmypdf = ocrmypdf.main:run_pipeline'
|
'ocrmypdf = ocrmypdf.main:run_pipeline'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
package_data={'ocrmypdf': ['data/sRGB_IEC61966-2-1_black_scaled.icc']},
|
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False)
|
zip_safe=False)
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ In some cases they were converted from one image format to another without other
|
|||||||
+=====================+================================================================================+
|
+=====================+================================================================================+
|
||||||
| c02-22.pdf | `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22 |
|
| c02-22.pdf | `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22 |
|
||||||
+---------------------+--------------------------------------------------------------------------------+
|
+---------------------+--------------------------------------------------------------------------------+
|
||||||
| congress.jpg | `US Congressional Records`_ |
|
| congress.jpg | `US Congressional Records`_ (Public Domain) |
|
||||||
+---------------------+--------------------------------------------------------------------------------+
|
+---------------------+--------------------------------------------------------------------------------+
|
||||||
| graph.pdf | `Wikimedia: Pandas text analysis.png`_ |
|
| graph.pdf | `Wikimedia: Pandas text analysis.png`_ (Public Domain) |
|
||||||
+---------------------+--------------------------------------------------------------------------------+
|
+---------------------+--------------------------------------------------------------------------------+
|
||||||
| lichtenstein.pdf | `Wikimedia: JPEG2000 Lichtenstein`_ (Creative Commons BY-SA 3.0) |
|
| lichtenstein.pdf | `Wikimedia: JPEG2000 Lichtenstein`_ (Creative Commons BY-SA 3.0) |
|
||||||
+---------------------+--------------------------------------------------------------------------------+
|
+---------------------+--------------------------------------------------------------------------------+
|
||||||
@@ -31,6 +31,7 @@ Files generated for this project
|
|||||||
The following test resources were crafted specifically for this project, and can be used
|
The following test resources were crafted specifically for this project, and can be used
|
||||||
under the terms of the license in LICENSE.rst.
|
under the terms of the license in LICENSE.rst.
|
||||||
|
|
||||||
|
- aspect.pdf (a page with an image with 200 x 100 DPI resolution)
|
||||||
- blank.pdf (a blank PDF page)
|
- blank.pdf (a blank PDF page)
|
||||||
- cmyk.pdf (a CMYK image created in Photoshop)
|
- cmyk.pdf (a CMYK image created in Photoshop)
|
||||||
- enormous.pdf (a very lage page)
|
- enormous.pdf (a very lage page)
|
||||||
@@ -47,12 +48,13 @@ These test resources are assemblies from other previously mentioned files, relea
|
|||||||
|
|
||||||
- cardinal.pdf (four cardinal directions, rotated copies of LinnSequencer.jpg)
|
- cardinal.pdf (four cardinal directions, rotated copies of LinnSequencer.jpg)
|
||||||
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
||||||
|
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
|
||||||
- graph_ocred.pdf (from graph.pdf)
|
- graph_ocred.pdf (from graph.pdf)
|
||||||
- jbig2.pdf (congress.jpg, converted to JBIG2 encoding)
|
- jbig2.pdf (congress.jpg, converted to JBIG2 encoding)
|
||||||
- multipage.pdf (from several other files)
|
- multipage.pdf (from several other files)
|
||||||
- palette.pdf (congress.jpg, converted to a 256-color palette)
|
- palette.pdf (congress.jpg, converted to a 256-color palette)
|
||||||
- skew.pdf (from c02-22.pdf)
|
- skew.pdf (from c02-22.pdf)
|
||||||
- skew-encrypted.pdf (skew.pdf with encrypted applied)
|
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2)
|
||||||
|
|
||||||
|
|
||||||
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
|
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||||
+120
-26
@@ -12,6 +12,7 @@ from ocrmypdf.pageinfo import pdf_get_all_pageinfo
|
|||||||
import PyPDF2 as pypdf
|
import PyPDF2 as pypdf
|
||||||
from ocrmypdf import ExitCode
|
from ocrmypdf import ExitCode
|
||||||
from ocrmypdf import leptonica
|
from ocrmypdf import leptonica
|
||||||
|
from ocrmypdf.pdfa import file_claims_pdfa
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info.major < 3:
|
if sys.version_info.major < 3:
|
||||||
@@ -159,34 +160,34 @@ def test_deskew(spoof_tesseract_noop):
|
|||||||
|
|
||||||
|
|
||||||
def test_clean(spoof_tesseract_noop):
|
def test_clean(spoof_tesseract_noop):
|
||||||
check_ocrmypdf('skew.pdf', 'test_clean.pdf', '-c', env=spoof_tesseract_noop)
|
check_ocrmypdf('skew.pdf', 'test_clean.pdf', '-c',
|
||||||
|
env=spoof_tesseract_noop)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pdf,renderer", [
|
# This will run 5 * 2 * 2 = 20 test cases
|
||||||
('palette.pdf', 'hocr'),
|
@pytest.mark.parametrize(
|
||||||
('palette.pdf', 'tesseract'),
|
"pdf",
|
||||||
('cmyk.pdf', 'hocr'),
|
['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'])
|
||||||
('cmyk.pdf', 'tesseract'),
|
@pytest.mark.parametrize("renderer", ['hocr', 'tesseract'])
|
||||||
('ccitt.pdf', 'hocr'),
|
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
|
||||||
('ccitt.pdf', 'tesseract'),
|
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type):
|
||||||
('jbig2.pdf', 'hocr'),
|
|
||||||
('jbig2.pdf', 'tesseract'),
|
|
||||||
('lichtenstein.pdf', 'hocr'),
|
|
||||||
('lichtenstein.pdf', 'tesseract')
|
|
||||||
])
|
|
||||||
def test_exotic_image(spoof_tesseract_cache, pdf, renderer):
|
|
||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
pdf,
|
pdf,
|
||||||
'test_{0}_{1}.pdf'.format(pdf, renderer),
|
'test_{0}_{1}.pdf'.format(pdf, renderer),
|
||||||
'-dc',
|
'-dc',
|
||||||
'-v', '1',
|
'-v', '1',
|
||||||
|
'--output-type', output_type,
|
||||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||||
|
|
||||||
|
|
||||||
def test_preserve_metadata(spoof_tesseract_noop):
|
@pytest.mark.parametrize("output_type", [
|
||||||
|
'pdfa', 'pdf'
|
||||||
|
])
|
||||||
|
def test_preserve_metadata(spoof_tesseract_noop, output_type):
|
||||||
pdf_before = pypdf.PdfFileReader(_infile('graph.pdf'))
|
pdf_before = pypdf.PdfFileReader(_infile('graph.pdf'))
|
||||||
|
|
||||||
output = check_ocrmypdf('graph.pdf', 'test_metadata_preserve.pdf',
|
output = check_ocrmypdf('graph.pdf', 'test_metadata_preserve.pdf',
|
||||||
|
'--output-type', output_type,
|
||||||
env=spoof_tesseract_noop)
|
env=spoof_tesseract_noop)
|
||||||
|
|
||||||
pdf_after = pypdf.PdfFileReader(output)
|
pdf_after = pypdf.PdfFileReader(output)
|
||||||
@@ -194,8 +195,14 @@ def test_preserve_metadata(spoof_tesseract_noop):
|
|||||||
for key in ('/Title', '/Author'):
|
for key in ('/Title', '/Author'):
|
||||||
assert pdf_before.documentInfo[key] == pdf_after.documentInfo[key]
|
assert pdf_before.documentInfo[key] == pdf_after.documentInfo[key]
|
||||||
|
|
||||||
|
pdfa_info = file_claims_pdfa(output)
|
||||||
|
assert pdfa_info['output'] == output_type
|
||||||
|
|
||||||
def test_override_metadata(spoof_tesseract_noop):
|
|
||||||
|
@pytest.mark.parametrize("output_type", [
|
||||||
|
'pdfa', 'pdf'
|
||||||
|
])
|
||||||
|
def test_override_metadata(spoof_tesseract_noop, output_type):
|
||||||
input_file = _infile('c02-22.pdf')
|
input_file = _infile('c02-22.pdf')
|
||||||
output_file = _outfile('test_override_metadata.pdf')
|
output_file = _outfile('test_override_metadata.pdf')
|
||||||
|
|
||||||
@@ -208,6 +215,7 @@ def test_override_metadata(spoof_tesseract_noop):
|
|||||||
'--title', german,
|
'--title', german,
|
||||||
'--author', chinese,
|
'--author', chinese,
|
||||||
'--subject', high_unicode,
|
'--subject', high_unicode,
|
||||||
|
'--output-type', output_type,
|
||||||
env=spoof_tesseract_noop)
|
env=spoof_tesseract_noop)
|
||||||
|
|
||||||
assert p.returncode == ExitCode.ok
|
assert p.returncode == ExitCode.ok
|
||||||
@@ -226,6 +234,9 @@ def test_override_metadata(spoof_tesseract_noop):
|
|||||||
assert pdfinfo['Subject'] == high_unicode
|
assert pdfinfo['Subject'] == high_unicode
|
||||||
assert pdfinfo.get('Keywords', '') == ''
|
assert pdfinfo.get('Keywords', '') == ''
|
||||||
|
|
||||||
|
pdfa_info = file_claims_pdfa(output_file)
|
||||||
|
assert pdfa_info['output'] == output_type
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('renderer', [
|
@pytest.mark.parametrize('renderer', [
|
||||||
'hocr',
|
'hocr',
|
||||||
@@ -325,11 +336,6 @@ def test_monochrome_correlation():
|
|||||||
'tesseract',
|
'tesseract',
|
||||||
])
|
])
|
||||||
def test_autorotate(spoof_tesseract_cache, renderer):
|
def test_autorotate(spoof_tesseract_cache, renderer):
|
||||||
import ocrmypdf.ghostscript as ghostscript
|
|
||||||
import logging
|
|
||||||
|
|
||||||
gslog = logging.getLogger()
|
|
||||||
|
|
||||||
# cardinal.pdf contains four copies of an image rotated in each cardinal
|
# cardinal.pdf contains four copies of an image rotated in each cardinal
|
||||||
# direction - these ones are "burned in" not tagged with /Rotate
|
# direction - these ones are "burned in" not tagged with /Rotate
|
||||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
|
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
|
||||||
@@ -343,6 +349,36 @@ def test_autorotate(spoof_tesseract_cache, renderer):
|
|||||||
assert correlation > 0.80
|
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', [
|
@pytest.mark.parametrize('renderer', [
|
||||||
'hocr',
|
'hocr',
|
||||||
'tesseract',
|
'tesseract',
|
||||||
@@ -361,16 +397,15 @@ def test_skip_big(spoof_tesseract_cache):
|
|||||||
assert not pdfinfo[0]['has_text']
|
assert not pdfinfo[0]['has_text']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('renderer', [
|
@pytest.mark.parametrize('renderer', ['hocr', 'tesseract'])
|
||||||
'hocr',
|
@pytest.mark.parametrize('output_type', ['pdf', 'pdfa'])
|
||||||
'tesseract',
|
def test_maximum_options(spoof_tesseract_cache, renderer, output_type):
|
||||||
])
|
|
||||||
def test_maximum_options(spoof_tesseract_cache, renderer):
|
|
||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
'multipage.pdf', 'test_multipage%s.pdf' % renderer,
|
'multipage.pdf', 'test_multipage%s.pdf' % renderer,
|
||||||
'-d', '-c', '-i', '-g', '-f', '-k', '--oversample', '300',
|
'-d', '-c', '-i', '-g', '-f', '-k', '--oversample', '300',
|
||||||
'--skip-big', '10', '--title', 'Too Many Weird Files',
|
'--skip-big', '10', '--title', 'Too Many Weird Files',
|
||||||
'--author', 'py.test', '--pdf-renderer', renderer,
|
'--author', 'py.test', '--pdf-renderer', renderer,
|
||||||
|
'--output-type', output_type,
|
||||||
env=spoof_tesseract_cache)
|
env=spoof_tesseract_cache)
|
||||||
|
|
||||||
|
|
||||||
@@ -395,6 +430,17 @@ def test_blank_input_pdf():
|
|||||||
assert p.returncode == ExitCode.ok
|
assert p.returncode == ExitCode.ok
|
||||||
|
|
||||||
|
|
||||||
|
def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash):
|
||||||
|
# As a correctness test, make sure that --force-ocr on a PDF with no
|
||||||
|
# content still triggers tesseract. If tesseract crashes, then it was
|
||||||
|
# called.
|
||||||
|
p, _, err = run_ocrmypdf_env(
|
||||||
|
'blank.pdf', 'wont_be_created.pdf', '--force-ocr',
|
||||||
|
env=spoof_tesseract_crash)
|
||||||
|
assert p.returncode == ExitCode.child_process_error, err
|
||||||
|
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||||
|
|
||||||
|
|
||||||
def test_french(spoof_tesseract_cache):
|
def test_french(spoof_tesseract_cache):
|
||||||
p, out, err = run_ocrmypdf_env(
|
p, out, err = run_ocrmypdf_env(
|
||||||
'francais.pdf', 'francais.pdf', '-l', 'fra', env=spoof_tesseract_cache)
|
'francais.pdf', 'francais.pdf', '-l', 'fra', env=spoof_tesseract_cache)
|
||||||
@@ -493,6 +539,8 @@ def test_tesseract_crash_autorotate(spoof_tesseract_crash):
|
|||||||
assert sh.returncode == ExitCode.child_process_error
|
assert sh.returncode == ExitCode.child_process_error
|
||||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||||
assert "ERROR" in err
|
assert "ERROR" in err
|
||||||
|
print(out)
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('renderer', [
|
@pytest.mark.parametrize('renderer', [
|
||||||
@@ -503,3 +551,49 @@ def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
|
|||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
|
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
|
||||||
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
|
'--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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('renderer', [
|
||||||
|
'hocr']) # tesseract cannot pass this test yet
|
||||||
|
def test_non_square_resolution(renderer, spoof_tesseract_cache):
|
||||||
|
# Confirm input image is non-square resolution
|
||||||
|
in_pageinfo = pdf_get_all_pageinfo(_infile('aspect.pdf'))
|
||||||
|
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
|
||||||
|
|
||||||
|
out = 'aspect_%s.pdf' % renderer
|
||||||
|
check_ocrmypdf(
|
||||||
|
'aspect.pdf', out,
|
||||||
|
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||||
|
|
||||||
|
out_pageinfo = pdf_get_all_pageinfo(_outfile(out))
|
||||||
|
|
||||||
|
# Confirm resolution was kept the same
|
||||||
|
assert in_pageinfo[0]['xres'] == out_pageinfo[0]['xres']
|
||||||
|
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_to_pdf(spoof_tesseract_noop):
|
||||||
|
check_ocrmypdf(
|
||||||
|
'LinnSequencer.jpg', 'image_to_pdf.pdf', '--image-dpi', '200',
|
||||||
|
env=spoof_tesseract_noop)
|
||||||
|
|||||||
Reference in New Issue
Block a user