Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3097a2384 | ||
|
|
6d9ddbe98b | ||
|
|
9bb42c0229 | ||
|
|
bd7226b27a | ||
|
|
5b413e3873 | ||
|
|
be5831a629 | ||
|
|
084d2bf8e2 | ||
|
|
da79e6bac7 | ||
|
|
c4831ac00c | ||
|
|
93a954ef9f | ||
|
|
f7ce8f44e9 | ||
|
|
0b012697e5 | ||
|
|
58e357c992 | ||
|
|
71fbad83ad | ||
|
|
52483072dc | ||
|
|
7f0b8621f3 | ||
|
|
cd8db60b06 | ||
|
|
1aa34f5d2e | ||
|
|
dfa1d88ce9 | ||
|
|
dd38519f07 | ||
|
|
098f5d4f0b | ||
|
|
ffc685d536 | ||
|
|
cd1a99a0de | ||
|
|
48e3b267fc | ||
|
|
3a7c3417bb | ||
|
|
d792ef7222 | ||
|
|
2c24f67deb | ||
|
|
9e75e28d0c | ||
|
|
3232643809 | ||
|
|
f7ee9e90ce | ||
|
|
47298be132 | ||
|
|
a88fa83515 | ||
|
|
12bfe20385 | ||
|
|
3d2f6f0772 | ||
|
|
1cb607f64b | ||
|
|
d3c54fbbde | ||
|
|
28341b755f | ||
|
|
4b5cd420e1 | ||
|
|
1d57bcc99e | ||
|
|
facdd13879 | ||
|
|
6e891f91d3 | ||
|
|
9b50ede977 | ||
|
|
82cf010333 | ||
|
|
6ff6c8614f | ||
|
|
eb1cd38f6c | ||
|
|
148b632b4f | ||
|
|
591e213713 | ||
|
|
75f2262659 | ||
|
|
d9005a1074 | ||
|
|
3e73fa81bf | ||
|
|
ba6e290231 | ||
|
|
08e47117a3 | ||
|
|
532ef38157 | ||
|
|
4c09875890 | ||
|
|
0e98139712 | ||
|
|
4c04d802d7 | ||
|
|
b3dc404571 | ||
|
|
8694f8d2eb | ||
|
|
263f9b79f4 | ||
|
|
56d2aae963 | ||
|
|
127706153d | ||
|
|
caee5b1428 | ||
|
|
6c12e7e944 | ||
|
|
cd04ae6949 | ||
|
|
6a0b68298f | ||
|
|
0a2f732267 | ||
|
|
4bade99f27 | ||
|
|
0b048cd24e | ||
|
|
c69ee63d82 | ||
|
|
744fa104d7 | ||
|
|
e24ff0fd64 | ||
|
|
5de107d44c | ||
|
|
131a5b741d | ||
|
|
65b89687a9 | ||
|
|
048ae40e75 | ||
|
|
234183ecd2 | ||
|
|
fb067dc97b | ||
|
|
a1fea0ce16 | ||
|
|
e1e9135e93 | ||
|
|
aff982036b | ||
|
|
d087649eab | ||
|
|
7f3fa46a40 | ||
|
|
b1f79e4d97 | ||
|
|
115d6df94f | ||
|
|
559af9635f | ||
|
|
cb06359c0b | ||
|
|
5e26bb29d9 | ||
|
|
b0e95842b8 | ||
|
|
08e678f21f | ||
|
|
c17817810f | ||
|
|
ff5c38b1f7 | ||
|
|
64314c1b82 | ||
|
|
83230097ae | ||
|
|
8f91acf956 | ||
|
|
d211722a2f | ||
|
|
56e6ed1249 | ||
|
|
21982cf1cb | ||
|
|
edc01408da | ||
|
|
aee33c87ed | ||
|
|
0dae1602c7 | ||
|
|
d926f07ac1 | ||
|
|
96045e98f4 | ||
|
|
01b7205e2c | ||
|
|
c8a4cbcf17 | ||
|
|
16b6442b23 | ||
|
|
183eafa587 | ||
|
|
47a2997538 | ||
|
|
37ebcadfa1 | ||
|
|
74d98216f1 | ||
|
|
4bdebf573e | ||
|
|
1606b6a383 | ||
|
|
2a61902df5 | ||
|
|
01a1c2b576 | ||
|
|
c4f01de231 | ||
|
|
63a4a761dd | ||
|
|
d077c03686 | ||
|
|
c97ea1f2a9 | ||
|
|
fd27df2abb | ||
|
|
bf04f03c4c | ||
|
|
93e802f473 | ||
|
|
1464b9087a | ||
|
|
e8cc8fc879 | ||
|
|
fae2119b1e | ||
|
|
aa859a4139 | ||
|
|
b9b12e2879 | ||
|
|
cf643c9f43 | ||
|
|
5b1a7880a9 | ||
|
|
474b6b0500 | ||
|
|
6c8c1d8173 | ||
|
|
6a91fa637f | ||
|
|
2846fb4e31 | ||
|
|
a1033cdc64 | ||
|
|
204336e1a5 | ||
|
|
8954e6c3b9 | ||
|
|
fee22b6b0b | ||
|
|
2b82c31b85 | ||
|
|
9a4813089c | ||
|
|
554fcc8b9d | ||
|
|
345256ee99 | ||
|
|
6e907856f2 | ||
|
|
8bc6019172 | ||
|
|
059f79242e | ||
|
|
89599b4812 | ||
|
|
a9f4047a97 | ||
|
|
23227ae763 |
@@ -6,5 +6,8 @@
|
||||
*.jar binary
|
||||
*.pdf binary
|
||||
*.PDF binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
|
||||
.git_archival.txt export-subst
|
||||
|
||||
@@ -6,6 +6,7 @@ pyvenv.cfg
|
||||
tasks.py
|
||||
.bash_history
|
||||
.ruffus_history.sqlite
|
||||
.idea/
|
||||
|
||||
# Package building
|
||||
*.egg-info/
|
||||
@@ -21,6 +22,7 @@ ocrmypdf/version.py
|
||||
docs/_build/
|
||||
docs/_static/
|
||||
docs/_templates/
|
||||
docs/Makefile
|
||||
|
||||
# Code coverage
|
||||
.coverage
|
||||
|
||||
+51
-16
@@ -7,12 +7,14 @@ cache:
|
||||
- tests/cache
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: "oyX5xesoHD7qcDXKIxMyGZPi+H/WxcvfFkaviEmq84K1DDyHk48+9e92IKgrw8/lcTADnEo/AgVKfnhCPflFimk1xTkgaK4sUg1WLI2YjmaHcwl5SlBHa2rN3uGBwy1hyP92qyv/mMc9R59NtRJ8u76lbn6eN9wi7lkFWdE6BTw=" # DOCKERHUB_OCRMYPDF_TOKEN
|
||||
- secure: "WlyII8YLsiUUyLtEA563GvEZmbneDb/T8q/P1uNbyQ2ps1U82tH0nSUV2CspSMxOFtZzPHCrRvnAmuTYKshBj+GNnBb1J9FKQmFwF+4NPeqsFdUkQ1NeeCmfIRShuNC3Otg2GGwj4Zssdg+QnVy43t2L11qizzfY+lY+MVzAYcM=" # DOCKERHUB_OCRMYPDF_TESS4_TOKEN
|
||||
- secure: "hsf6MT+n2x3OiDM2fQyJZdV0/PWYmv81LdVqC6cfnHBE/8N3DloJRqQ7WfO14TxhiK9PEC7MpyCj0lSabUHEO7gSH6Vks6I1asoSkt8S9/bSMlhT4hei+pwVpeGEiU5xHVATNjY+D919VC3IFvc3XmjT74h/2SLhaZ+jhEmDggM=" # HOMEBREW_OCRMYPDF_TOKEN
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
sudo: required
|
||||
language: python
|
||||
python: 3.4
|
||||
- os: linux
|
||||
sudo: required
|
||||
language: python
|
||||
@@ -28,9 +30,12 @@ matrix:
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then bash .travis/linux_before_install.sh ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash .travis/osx_before_install.sh ; fi
|
||||
before_install: |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||
bash .travis/linux_before_install.sh
|
||||
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
bash .travis/osx_before_install.sh
|
||||
fi
|
||||
|
||||
install:
|
||||
- pip3 install .
|
||||
@@ -38,23 +43,53 @@ install:
|
||||
|
||||
script:
|
||||
- mv ocrmypdf dont_import_this_ocrmypdf
|
||||
- pytest
|
||||
- pytest -n auto
|
||||
- mv dont_import_this_ocrmypdf ocrmypdf
|
||||
|
||||
after_success:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash .travis/osx_brew.sh ; fi
|
||||
|
||||
# See https://www.appneta.com/blog/pypi-deployment-with-travis-ci/ for
|
||||
# steps to set up testpypi deploy for untagged builds if desired
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
# release for main pypi
|
||||
# 3.6 is considered the build leader and does the deploy, otherwise there is
|
||||
# a race and all versions will try to deploy
|
||||
# OTOH if we ever need separate binary wheels then each version needs its
|
||||
# own deploy
|
||||
- provider: pypi
|
||||
server: https://upload.pypi.org/legacy/
|
||||
user: ocrmypdf-travis
|
||||
password:
|
||||
secure: DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo=
|
||||
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
|
||||
distributions: "sdist bdist_wheel"
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux"
|
||||
skip_upload_docs: true
|
||||
|
||||
# test pypi
|
||||
- provider: pypi
|
||||
server: https://testpypi.pypi.org/legacy/
|
||||
user: ocrmypdf-travis
|
||||
password:
|
||||
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
|
||||
distributions: "sdist"
|
||||
on:
|
||||
branch: develop
|
||||
tags: false
|
||||
condition: $TRAVIS_OS_NAME == "osx"
|
||||
skip_upload_docs: true
|
||||
|
||||
# null deploy for osx
|
||||
# we really just want to run after_deploy *after* pypi upload is done, but
|
||||
# after_deploy on runs if a given box deployed
|
||||
- provider: script
|
||||
script: /usr/bin/true
|
||||
on:
|
||||
branch: master
|
||||
condition: $TRAVIS_OS_NAME == "osx"
|
||||
|
||||
after_deploy: |
|
||||
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
bash .travis/osx_brew.sh
|
||||
elif [[ "$TRAVIS_PYTHON_VERSION" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then
|
||||
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf/trigger/$DOCKERHUB_OCRMYPDF_TOKEN/
|
||||
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf-tess4/trigger/$DOCKERHUB_OCRMYPDF_TESS4_TOKEN/
|
||||
fi
|
||||
+7
-2
@@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from string import Template
|
||||
from subprocess import run, PIPE
|
||||
import re
|
||||
@@ -12,8 +15,8 @@ class Ocrmypdf < Formula
|
||||
${ocrmypdf_sha256}
|
||||
|
||||
depends_on :python3
|
||||
depends_on :x11 # Pillow needs XQuartz
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "zlib"
|
||||
depends_on "libffi"
|
||||
depends_on "tesseract"
|
||||
depends_on "ghostscript"
|
||||
@@ -24,9 +27,11 @@ class Ocrmypdf < Formula
|
||||
depends_on "openjpeg"
|
||||
depends_on "freetype"
|
||||
depends_on "libpng"
|
||||
depends_on "libjpeg"
|
||||
depends_on "webp"
|
||||
depends_on "little-cms2"
|
||||
|
||||
${resources}
|
||||
|
||||
def install
|
||||
ENV.append ["SETUPTOOLS_SCM_PRETEND_VERSION"], "v${ocrmypdf_version}"
|
||||
ENV.each do |key, value|
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
@@ -7,10 +8,6 @@ sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-fra \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
libavformat56 \
|
||||
@@ -18,7 +15,17 @@ sudo apt-get install -y \
|
||||
libavutil54 \
|
||||
libffi-dev
|
||||
|
||||
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-deu
|
||||
|
||||
pip install --upgrade pip
|
||||
mkdir -p packages
|
||||
[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb
|
||||
[ -f packages/unpaper_6.1-1.deb ] || wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb
|
||||
sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
|
||||
+16
-1
@@ -1,8 +1,23 @@
|
||||
#!/bin/bash
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
pip3 install homebrew-pypi-poet
|
||||
python3 .travis/autobrew.py
|
||||
brew audit ocrmypdf.rb
|
||||
cat ocrmypdf.rb
|
||||
|
||||
# brew audit crashes Travis
|
||||
#brew audit ocrmypdf.rb
|
||||
|
||||
# Important: disable debug output so token is hidden
|
||||
set +x
|
||||
git clone https://$HOMEBREW_OCRMYPDF_TOKEN@github.com/jbarlow83/homebrew-ocrmypdf.git
|
||||
set -x
|
||||
|
||||
pushd homebrew-ocrmypdf
|
||||
cp ../ocrmypdf.rb Formula/ocrmypdf.rb
|
||||
git add Formula/ocrmypdf.rb
|
||||
git commit -m "homebrew-ocrmypdf: automatic release $TRAVIS_BUILD_NUMBER $TRAVIS_TAG"
|
||||
git push origin master
|
||||
popd
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-14: fritz-hh from Github (https://github.com/fritz-hh)
|
||||
##############################################################################
|
||||
|
||||
echo "This script is deprecated. Use 'ocrmypdf' instead."
|
||||
|
||||
python3 -m ocrmypdf.main "$@"
|
||||
+10
-1
@@ -1,6 +1,15 @@
|
||||
OCRmyPDF
|
||||
========
|
||||
|
||||
.. image:: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master
|
||||
:target: https://travis-ci.org/jbarlow83/OCRmyPDF
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||
:target: https://pypi.org/project/ocrmypdf/
|
||||
|
||||
.. image:: https://img.shields.io/docker/build/jbarlow83/ocrmypdf.svg
|
||||
:target: https://hub.docker.com/r/jbarlow83/ocrmypdf/
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched or copy-pasted.
|
||||
|
||||
@@ -35,7 +44,7 @@ Main features
|
||||
- Supports more than `100 languages <https://github.com/tesseract-ocr/tessdata>`_ recognized by Tesseract
|
||||
- Battle-tested on thousands of PDFs, a test suite and continuous integration
|
||||
|
||||
For details: please consult the `release notes <RELEASE_NOTES.rst>`_.
|
||||
For details: please consult the `documentation <https://ocrmypdf.readthedocs.io/en/latest/>`_.
|
||||
|
||||
Motivation
|
||||
----------
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
check-manifest >= 0.34
|
||||
check-manifest >= 0.35
|
||||
twine >= 1.8.1
|
||||
coverage >= 4.3.1
|
||||
pytest-xdist >= 1.15.0
|
||||
GitPython == 2.1.0
|
||||
coverage >= 4.4
|
||||
pytest-xdist >= 1.16.0
|
||||
GitPython == 2.1.3
|
||||
|
||||
# Known good versions: 1.11.1
|
||||
# Known broken versions: 1.15.0
|
||||
|
||||
+42
-26
@@ -33,20 +33,27 @@ If you want to adjust the amount of time spent on OCR, change ``--tesseract-time
|
||||
Overriding default tesseract
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
OCRmyPDF checks the environment variable ``OCRMYPDF_TESSERACT`` for the full path *to the tesseract executable* first.
|
||||
OCRmyPDF checks the environment variable ``OCRMYPDF_TESSERACT`` for the full path *to the tesseract binary* first.
|
||||
|
||||
For example, if you are testing tesseract 4.00 and don't wish to disturb your tesseract 3.04 installation, you can launch OCRmyPDF as follows:
|
||||
.. envvar:: OCRMYPDF_TESSERACT
|
||||
|
||||
Specify the location of the Tesseract binary.
|
||||
|
||||
.. envvar:: TESSDATA_PREFIX
|
||||
|
||||
A Tesseract environment variable that overrides the path to Tesseract's data files.
|
||||
|
||||
For example, if you are testing tesseract 4.00 and don't wish to disturb use an existing tesseract 3.04 installation, you can launch OCRmyPDF as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
env \
|
||||
OCRMYPDF_TESSERACT=/home/user/src/tesseract4/api/tesseract \
|
||||
TESSDATA_PREFIX=/home/user/src/tesseract4 \
|
||||
ocrmypdf --pdf-renderer tess4 --tesseract-oem 2 input.pdf output.pdf
|
||||
ocrmypdf --tesseract-oem 2 input.pdf output.pdf
|
||||
|
||||
In this example ``TESSDATA_PREFIX`` directs tesseract 4.0 to use LSTM training data. ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
|
||||
|
||||
* ``TESSDATA_PREFIX`` directs tesseract 4.0 to use LSTM training data. This is a tesseract environment variable.
|
||||
* ``--pdf-renderer tess4`` takes advantage of new tesseract 4.0 PDF renderer in OCRmyPDF. (Tesseract 4.0 only.)
|
||||
* ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
|
||||
|
||||
Overriding other support programs
|
||||
"""""""""""""""""""""""""""""""""
|
||||
@@ -57,7 +64,20 @@ In addition to tesseract, OCRmyPDF uses the following external binaries:
|
||||
* ``unpaper``
|
||||
* ``qpdf``
|
||||
|
||||
In each case OCRmyPDF will check the environment variable ``OCRMYPDF_{program}`` before asking the system to find ``{program}`` on the PATH. For example, you could redirect OCRmyPDF to ``OCRMYPDF_GS`` to override Ghostscript.
|
||||
In each case OCRmyPDF will check the environment variable ``OCRMYPDF_{program}`` before asking the system to find ``{program}`` on the PATH. For example, you could redirect OCRmyPDF to ``OCRMYPDF_GS`` to override Ghostscript. The full list is below:
|
||||
|
||||
.. envvar:: OCRMYPDF_GS
|
||||
|
||||
Specify the location of the Ghostscript binary.
|
||||
|
||||
.. envvar:: OCRMYPDF_UNPAPER
|
||||
|
||||
Specify the location of the unpaper binary.
|
||||
|
||||
.. envvar:: OCRMYPDF_QPDF
|
||||
|
||||
Specify the location of the qpdf binary.
|
||||
|
||||
|
||||
Changing tesseract configuration variables
|
||||
""""""""""""""""""""""""""""""""""""""""""
|
||||
@@ -95,33 +115,29 @@ rendering
|
||||
Creating a new PDF from other data (such as an existing PDF).
|
||||
|
||||
|
||||
OCRmyPDF has three PDF renderers: ``hocr``, ``tesseract`` and ``tess4``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` always selects ``hocr``.
|
||||
OCRmyPDF has three PDF renderers: ``sandwich``, ``hocr``, ``tesseract``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` selects ``sandwich`` for Tesseract 3.05.01, and newer, ``hocr`` for older versions of Tesseract.
|
||||
|
||||
The ``sandwich`` renderer
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
The ``sandwich`` renderer uses Tesseract's new text-only PDF feature, which produces a PDF page that lays out the OCR in invisible text. This page is then "sandwiched" onto the original PDF page, allowing lossless application of OCR even to PDF pages that contain other vector objects.
|
||||
|
||||
When image preprocessing features like ``--deskew`` are used, the original PDF will be rendered as a full page and the OCR layer will be placed on top.
|
||||
|
||||
This renderer requires Tesseract 3.05.01 or newer.
|
||||
|
||||
The ``hocr`` renderer
|
||||
"""""""""""""""""""""
|
||||
|
||||
The ``hocr`` renderer is the default because it works in most cases. In this mode the whole PDF is rasterized, the raster image is run through OCR to generate a .hocr file, which is an HTML-like file that specifies the location of all identified words.
|
||||
|
||||
The .hocr file is then rendered as a PDF and merged with the image layer.
|
||||
|
||||
The image layer is copied from the original PDF page if possible, avoiding potentially lossy transcoding or loss of other PDF information. If preprocessing is specified, then the image layer is a new PDF.
|
||||
|
||||
This is the only option for tesseract 3.02 and older.
|
||||
The ``hocr`` renderer works with older versions of Tesseract. The image layer is copied from the original PDF page if possible, avoiding potentially lossy transcoding or loss of other PDF information. If preprocessing is specified, then the image layer is a new PDF.
|
||||
|
||||
This works in all versions of Tesseract.
|
||||
|
||||
The ``tesseract`` renderer
|
||||
""""""""""""""""""""""""""
|
||||
|
||||
The tesseract renderer uses tesseract's capability to produce a PDF directly. In version 3, tesseract automatically combined the image layer and text, meaning that this mode *always* transcodes and loses potentially loses image quality and other PDF information.
|
||||
The ``tesseract`` renderer creates a PDF with the image and text layers precomposed, meaning that it always transcodes, loses image quality and rasterizes and vector objects. It does a better job on non-Latin text and document structure than ``hocr``.
|
||||
|
||||
It does a much better job on non-Latin text.
|
||||
If a PDF created with this renderer using Tesseract versions older than 3.05.00 is then passed through Ghostscript's pdfwrite feature, the OCR text *may* be corrupted. The ``--output-type=pdfa`` argument will produce a warning in this situation.
|
||||
|
||||
In a future release this will become the "tess3" renderer and ultimately will be dropped.
|
||||
|
||||
|
||||
The ``tess4`` renderer
|
||||
""""""""""""""""""""""
|
||||
|
||||
The tess4 renderer uses tesseract 4.00 alpha's text-only PDF feature added in January 2017. This combines the advantages of the tesseract and hocr renderers, transcoding the image layer only if required by preprocessing options.
|
||||
|
||||
Ghostscript PDF/A still sometimes inserts spaces between words when the tess4 renderer is used, affecting search quality. ``--output-pdf pdf`` may be used to avoid this issue.
|
||||
*This renderer is deprecated and will be removed whenever support for older versions of Tesseract is dropped.*
|
||||
|
||||
+50
-26
@@ -11,7 +11,7 @@ ocrmypdf has built-in help.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
ocrmypdf --help
|
||||
|
||||
|
||||
Add an OCR layer and convert to PDF/A
|
||||
@@ -19,14 +19,21 @@ Add an OCR layer and convert to PDF/A
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf input.pdf output.pdf
|
||||
ocrmypdf input.pdf output.pdf
|
||||
|
||||
Add an OCR layer and output a standard PDF
|
||||
""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
|
||||
Create a PDF/A with all color and grayscale images converted to JPEG
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
|
||||
|
||||
Modify a file in place
|
||||
""""""""""""""""""""""
|
||||
@@ -35,7 +42,7 @@ The file will only be overwritten if OCRmyPDF is successful.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf myfile.pdf myfile.pdf
|
||||
ocrmypdf myfile.pdf myfile.pdf
|
||||
|
||||
Correct page rotation
|
||||
"""""""""""""""""""""
|
||||
@@ -44,7 +51,7 @@ OCR will attempt to automatic correct the rotation of each page. This can help f
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
|
||||
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
|
||||
|
||||
You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive.
|
||||
|
||||
@@ -56,8 +63,8 @@ By default OCRmyPDF assumes the document is English.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
|
||||
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
|
||||
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
|
||||
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
|
||||
|
||||
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
|
||||
|
||||
@@ -65,19 +72,11 @@ Language packs must be installed for all languages specified. See :ref:`Installi
|
||||
Produce PDF and text file containing OCR text
|
||||
"""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
This produces a file named "output.pdf" and a companion text file named "output.txt". The ``pdftotext`` program from
|
||||
`Poppler <https://poppler.freedesktop.org/>`_ is used to extract text from the finished PDF.
|
||||
|
||||
This produces a file named "output.pdf" and a companion text file named "output.txt".
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf input.pdf - | tee output.pdf | pdftotext - output.txt
|
||||
|
||||
.. note::
|
||||
|
||||
To get pdftotext, Debian/Ubuntu users may ``apt-get install poppler-utils``
|
||||
and macOS users may ``brew install poppler`` respectively.
|
||||
|
||||
ocrmypdf --sidecar output.txt input.pdf output.pdf
|
||||
|
||||
OCR images, not PDFs
|
||||
--------------------
|
||||
@@ -86,21 +85,27 @@ Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
|
||||
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
|
||||
|
||||
If given a single image as input, OCRmyPDF will try converting it to a PDF on its own. If the DPI specified in the image is incorrect, it can be overridden with ``--image-dpi``:
|
||||
``img2pdf`` also has features to control the position of images on a page, if desired.
|
||||
|
||||
For convenience, OCRmyPDF can convert single images to PDFs on its own. If the resolution (dots per inch, DPI) of an image is not set or is incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54 cm, 1 dpi = 0.39 dpcm).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --image-dpi 300 image.png myfile.pdf
|
||||
ocrmypdf --image-dpi 300 image.png myfile.pdf
|
||||
|
||||
This feature may be removed at some point, because OCRmyPDF does not specialize in converting images to PDFs.
|
||||
If you have multiple images, you must use ``img2pdf`` to convert the images to PDF.
|
||||
|
||||
.. note::
|
||||
|
||||
ImageMagick ``convert`` can also convert a group of images to PDF, but in the author's experience it takes a long time, transcodes unnecessarily and gives poor results.
|
||||
|
||||
You can also use Tesseract 3.04+ directly to convert single page images or multi-page TIFFs to PDF:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tesseract my-image.jpg output-prefix pdf
|
||||
tesseract my-image.jpg output-prefix pdf
|
||||
|
||||
Image processing
|
||||
----------------
|
||||
@@ -119,11 +124,11 @@ OCRmyPDF perform some image processing on each page of a PDF, if desired. The s
|
||||
|
||||
.. note::
|
||||
|
||||
In many cases image processing will rasterize PDF pages as images, potentially losing quality.
|
||||
In many cases image processing will rasterize PDF pages as images, potentially losing quality.
|
||||
|
||||
.. warning::
|
||||
|
||||
``--clean-final`` and ``-remove-background`` may leave undesirable visual artifacts in some images where their algorithms have shortcomings. Files should be visually reviewed after using these options.
|
||||
``--clean-final`` and ``-remove-background`` may leave undesirable visual artifacts in some images where their algorithms have shortcomings. Files should be visually reviewed after using these options.
|
||||
|
||||
|
||||
OCR and correct document skew (crooked scan)
|
||||
@@ -133,12 +138,31 @@ Deskew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --deskew input.pdf output.pdf
|
||||
ocrmypdf --deskew input.pdf output.pdf
|
||||
|
||||
Image processing commands can be combined. The order in which options are given does not matter. OCRmyPDF always applies the steps of the image processing pipeline in the same order (rotate, remove background, deskew, clean).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --deskew --clean --rotate-pages input.pdf output.pdf
|
||||
ocrmypdf --deskew --clean --rotate-pages input.pdf output.pdf
|
||||
|
||||
|
||||
Don't actually OCR my PDF
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image processing without performing OCR, if all you want to is to apply image processing or PDF/A conversion.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
|
||||
|
||||
|
||||
Improving OCR quality
|
||||
---------------------
|
||||
|
||||
The `Image processing`_ features can improve OCR quality.
|
||||
|
||||
Rotating pages and deskewing helps to ensure that the page orientation is correct before OCR begins. Removing the background and/or cleaning the page can also improve results. The ``--oversample DPI`` argument can be specified to resample images to higher resolution before attempting OCR; this can improve results as well.
|
||||
|
||||
OCR quality will suffer if the resolution of input images is not correct (since the range of pixel sizes that will be checked for possible fonts will also be incorrect).
|
||||
|
||||
|
||||
+7
-4
@@ -11,14 +11,18 @@ be searched.
|
||||
|
||||
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 1
|
||||
|
||||
introduction
|
||||
release_notes
|
||||
installation
|
||||
languages
|
||||
|
||||
.. toctree::
|
||||
:caption: Usage
|
||||
:maxdepth: 2
|
||||
|
||||
cookbook
|
||||
advanced
|
||||
batch
|
||||
@@ -26,7 +30,6 @@ Contents:
|
||||
errors
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
+119
-97
@@ -1,28 +1,33 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Installing on Debian and Ubuntu
|
||||
-------------------------------
|
||||
OCRmyPDF requires Python 3.5 (or newer) and Tesseract 3.04 (or newer).
|
||||
|
||||
Installing on Debian and Ubuntu 16.10 or newer
|
||||
----------------------------------------------
|
||||
|
||||
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
apt-get install ocrmypdf
|
||||
|
||||
apt-get install ocrmypdf
|
||||
|
||||
Installing on macOS
|
||||
-------------------
|
||||
|
||||
A `Homebrew <https://brew.sh>`_ tap is available for macOS:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew tap jbarlow83/ocrmypdf
|
||||
brew install ocrmypdf
|
||||
brew tap jbarlow83/ocrmypdf
|
||||
brew install ocrmypdf
|
||||
|
||||
|
||||
Users who previously installed OCRmyPDF on macOS may need to remove the ``pip`` based installation (``pip3 uninstall ocrmypdf``).
|
||||
.. warning::
|
||||
|
||||
.. _Docker:
|
||||
Users who previously installed OCRmyPDF on macOS using ``pip install ocrmypdf`` should remove the pip version (``pip3 uninstall ocrmypdf``) before switching to the Homebrew version.
|
||||
|
||||
.. _Docker-install:
|
||||
|
||||
Installing the Docker image
|
||||
---------------------------
|
||||
@@ -37,17 +42,17 @@ 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 macOS has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
Assuming you have a Docker engine running, you can download one of the three available images:
|
||||
|
||||
@@ -66,13 +71,13 @@ For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf-tess4
|
||||
docker pull jbarlow83/ocrmypdf-tess4
|
||||
|
||||
Then tag it to give a more convenient name, just ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf-tess4 ocrmypdf
|
||||
docker tag jbarlow83/ocrmypdf-tess4 ocrmypdf
|
||||
|
||||
.. _docker-polyglot:
|
||||
|
||||
@@ -82,19 +87,19 @@ You can then run ocrmypdf using the command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm ocrmypdf --help
|
||||
docker run --rm ocrmypdf --help
|
||||
|
||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
|
||||
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
.. note:: The working directory should be a writable local volume or Docker may not have permission to access it.
|
||||
|
||||
@@ -104,15 +109,15 @@ For convenience, a shell alias can hide the docker command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
|
||||
ocrmypdf --version # runs docker version
|
||||
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
|
||||
ocrmypdf --version # runs docker version
|
||||
|
||||
Or in the wonderful `fish shell <https://fishshell.com/>`_:
|
||||
|
||||
.. code-block:: fish
|
||||
|
||||
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
|
||||
funcsave ocrmypdf
|
||||
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
|
||||
funcsave ocrmypdf
|
||||
|
||||
|
||||
Manual installation on macOS
|
||||
@@ -126,51 +131,51 @@ Update Homebrew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew update
|
||||
|
||||
brew update
|
||||
|
||||
Install or upgrade the required Homebrew packages, if any are missing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
|
||||
Python 3.4, 3.5 and 3.6 are supported.
|
||||
Python 3.5 and 3.6 are supported.
|
||||
|
||||
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
|
||||
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||
|
||||
.. _macos-all-languages:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
Update the homebrew pip and install Pillow:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
You can then install OCRmyPDF from PyPI:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
pip3 install ocrmypdf
|
||||
|
||||
The command line program should now be available:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
ocrmypdf --help
|
||||
|
||||
|
||||
Installing on Ubuntu 16.04 LTS
|
||||
@@ -180,95 +185,110 @@ No package is currently available for Ubuntu 16.04, but you can install the depe
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
unpaper \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-cffi
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
unpaper \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-cffi
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m venv venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip3 install ocrmypdf
|
||||
python3 -m venv venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip3 install ocrmypdf
|
||||
|
||||
|
||||
Installing on Ubuntu 14.04 LTS
|
||||
------------------------------
|
||||
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation and because OCRmyPDF depends on some packages newer than are available in the main distribution.
|
||||
|
||||
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.
|
||||
Add new "apt" repositories needed for backports of Ghostscript 9.16, libav-11 (for unpaper 6.1) and Tesseract 4.00 (alpha). This will replace Ghostscript and Tesseract 3.x on your system. If you prefer to not modify your system in this matter, consider using a Docker container.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
sudo add-apt-repository ppa:alex-p/tesseract-ocr
|
||||
|
||||
Update apt-get:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
Install system dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab
|
||||
sudo apt-get install \
|
||||
software-properties-common python-software-properties \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab \
|
||||
python3-wheel \
|
||||
python3-venv \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
||||
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
|
||||
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
|
||||
|
||||
Installing on ArchLinux
|
||||
-----------------------
|
||||
|
||||
The author is aware of an `ArchLinux package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. It seems like the following command might work.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pacman -S ocrmypdf
|
||||
|
||||
|
||||
Installing on Windows
|
||||
@@ -283,45 +303,47 @@ 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
|
||||
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.
|
||||
|
||||
|
||||
`Bash on Ubuntu on Windows <https://github.com/Microsoft/BashOnWindows>`_ should also be a viable route for running the OCRmyPDF Docker container.
|
||||
|
||||
Installing HEAD revision from sources
|
||||
-------------------------------------
|
||||
|
||||
If you have ``git`` and Python 3.4 or newer installed, you can install from source. When the ``pip`` installer runs,
|
||||
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs,
|
||||
it will alert you if dependencies are missing.
|
||||
|
||||
To install the HEAD revision from sources in the current Python 3 environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
On certain Linux distributions such as Ubuntu, you may need to use
|
||||
run the install command as superuser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Note that this will alter your system's Python distribution. If you prefer
|
||||
to not install as superuser, you can install the package in a Python virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
|
||||
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
||||
you activate the virtual environment.
|
||||
@@ -329,8 +351,8 @@ you activate the virtual environment.
|
||||
To run the program:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
If not yet installed, the script will notify you about dependencies that
|
||||
need to be installed. The script requires specific versions of the
|
||||
|
||||
+34
-1
@@ -53,6 +53,22 @@ By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be dis
|
||||
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
|
||||
|
||||
|
||||
Why you shouldn't do this manually
|
||||
----------------------------------
|
||||
|
||||
A PDF is similar to an HTML file, in that it contains document structure along with images. Sometimes a PDF does nothing more than present a full page image, but often there is additional content that would be lost.
|
||||
|
||||
A manual process could work like either of these:
|
||||
|
||||
1. Rasterize each page as an image, OCR the images, and combine the output into a PDF. This preserves the layout of each page, but resamples all images (possibly losing quality, increasing file size, introducing compression artifacts, etc.).
|
||||
|
||||
2. Extract each image, OCR, and combine the output into a PDF. This loses the context in which images are used in the PDF, meaning that cropping, rotation and scaling of pages may be lost. Some scanned PDFs use multiple images segmented into black and white, grayscale and color regions, with stencil masks to prevent overlap, as this can enhance the appearance of a file while reducing file size. Clearly, reassembling these images will be easy. This also loses and text or vector art on any pages in a PDF with both scanned and pure digital content.
|
||||
|
||||
In the case of a PDF that is nothing other than a container of images (no rotation, scaling, cropping, one image per page), the second approach can be lossless.
|
||||
|
||||
OCRmyPDF uses several strategies depending on input options and the input PDF itself, but generally speaking it rasterizes a page for OCR and then grafts the OCR back onto the original. As such it can handle complex PDFs and still preserve their contents as much as possible.
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
@@ -64,6 +80,7 @@ OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these l
|
||||
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
|
||||
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
|
||||
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
|
||||
* PDFs that use transparent layers are not currently checked in the test suite, so they may not work correctly.
|
||||
|
||||
OCRmyPDF is also limited by the PDF specification:
|
||||
|
||||
@@ -72,5 +89,21 @@ OCRmyPDF is also limited by the PDF specification:
|
||||
Ghostscript also imposes some limitations:
|
||||
|
||||
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
|
||||
* PDFs containing JPEG 2000-encoded content will be converted to JPEG encoding, which may introduce compression artifacts, if Ghostscript PDF/A is enabled.
|
||||
* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format.
|
||||
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
|
||||
|
||||
Similar programs
|
||||
----------------
|
||||
|
||||
To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly tested command line OCR PDF conversion tool. If it does not meet your needs, contributions and suggestions are welcome. If not, consider one of these similar open source programs:
|
||||
|
||||
* pdf2pdfocr
|
||||
* pdfsandwich
|
||||
* pypdfocr
|
||||
* pdfbeads
|
||||
|
||||
If you are looking for a micro web-frontend for OCRmyPDF, consider the third-party `OCRmyPDF-web <https://github.com/sseemayer/OCRmyPDF-web>`_. Bear in mind that OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online`_).
|
||||
|
||||
|
||||
@@ -1,11 +1,93 @@
|
||||
RELEASE NOTES
|
||||
Release notes
|
||||
=============
|
||||
|
||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_ for its command line interface.
|
||||
|
||||
The OCRmyPDF package itself does not contain a public API, although it is fairly stable and breaking changes are usually timed with a major release. A future release will clearly define the stable public API.
|
||||
|
||||
|
||||
v5.3.2
|
||||
------
|
||||
|
||||
- Fixed a broken test case related to language packs
|
||||
|
||||
|
||||
v5.3.1
|
||||
------
|
||||
|
||||
- Fixed wrong return code given for missing Tesseract language packs
|
||||
- Fixed "brew audit" crashing on Travis when trying to auto-brew
|
||||
|
||||
|
||||
v5.3
|
||||
----
|
||||
|
||||
- Added ``--user-words`` and ``--user-patterns`` arguments which are forwarded to Tesseract OCR as words and regular expressions respective to use to guide OCR. Supplying a list of subject-domain words should assist Tesseract with resolving words. (#165)
|
||||
- Using a non Latin-1 language with the "hocr" renderer now warns about possible OCR quality and recommends workarounds (#176)
|
||||
- Output file path added to error message when that location is not writable (#175)
|
||||
- Otherwise valid PDFs with leading whitespace at the beginning of the file are now accepted
|
||||
|
||||
|
||||
v5.2
|
||||
----
|
||||
|
||||
- When using Tesseract 3.05.01 or newer, OCRmyPDF will select the "sandwich" PDF renderer by default, unless another PDF renderer is specified with the ``--pdf-renderer`` argument. The previous behavior was to select ``--pdf-renderer=hocr``.
|
||||
- The "tesseract" PDF renderer is now deprecated, since it can cause problems with Ghostscript on Tesseract 3.05.00
|
||||
- The "tess4" PDF renderer has been renamed to "sandwich". "tess4" is now a deprecated alias for "sandwich".
|
||||
|
||||
|
||||
v5.1
|
||||
----
|
||||
|
||||
- Files with pages larger than 200" (5080 mm) in either dimension are now supported with ``--output-type=pdf`` with the page size preserved (in the PDF specification this feature is called UserUnit scaling). Due to Ghostscript limitations this is not available in conjunction with PDF/A output.
|
||||
|
||||
|
||||
v5.0.1
|
||||
------
|
||||
|
||||
- Fixed issue #169, exception due to failure to create sidecar text files on some versions of Tesseract 3.04, including the jbarlow83/ocrmypdf Docker image
|
||||
|
||||
|
||||
v5.0
|
||||
----
|
||||
|
||||
- Backward incompatible changes
|
||||
|
||||
+ Support for Python 3.4 dropped. Python 3.5 is now required.
|
||||
+ Support for Tesseract 3.02 and 3.03 dropped. Tesseract 3.04 or newer is required. Tesseract 4.00 (alpha) is supported.
|
||||
+ The OCRmyPDF.sh script was removed.
|
||||
|
||||
- Add a new feature, ``--sidecar``, which allows creating "sidecar" text files which contain the OCR results in plain text. These OCR text is more reliable than extracting text from PDFs. Closes #126.
|
||||
- New feature: ``--pdfa-image-compression``, which allows overriding Ghostscript's lossy-or-lossless image encoding heuristic and making all images JPEG encoded or lossless encoded as desired. Fixes #163.
|
||||
- Fixed issue #143, added ``--quiet`` to suppress "INFO" messages
|
||||
- Fixed issue #164, a typo
|
||||
- Removed the command line parameters ``-n`` and ``--just-print`` since they have not worked for some time (reported as Ubuntu bug `#1687308 <https://bugs.launchpad.net/ubuntu/+source/ocrmypdf/+bug/1687308>`_)
|
||||
|
||||
|
||||
v4.5.6
|
||||
------
|
||||
|
||||
- Fixed issue #156, 'NoneType' object has no attribute 'getObject' on pages with no optional /Contents record. This should resolve all issues related to pages with no /Contents record.
|
||||
- Fixed issue #158, ocrmypdf now stops and terminates if Ghostscript fails on an intermediate step, as it is not possible to proceed.
|
||||
- Fixed issue #160, exception thrown on certain invalid arguments instead of error message
|
||||
|
||||
|
||||
v4.5.5
|
||||
------
|
||||
|
||||
- Automated update of macOS homebrew tap
|
||||
- Fixed issue #154, KeyError '/Contents' when searching for text on blank pages that have no /Contents record. Note: incomplete fix for this issue.
|
||||
|
||||
|
||||
v4.5.4
|
||||
------
|
||||
|
||||
- Fix ``--skip-big`` raising an exception if a page contains no images (#152) (thanks to @TomRaz)
|
||||
- Fix an issue where pages with no images might trigger "cannot write mode P as JPEG" (#151)
|
||||
|
||||
|
||||
v4.5.3
|
||||
======
|
||||
------
|
||||
|
||||
- Added a workaround for Ghostscript 9.21 and probably earlier versions would fail with the error message "VMerror -25", due to a Ghostscript bug in XMP metadata handling
|
||||
- High Unicode characters (U+10000 and up) are no longer accepted for setting metadata on the command line, as Ghostscript may not handle them correctly.
|
||||
@@ -14,7 +96,7 @@ v4.5.3
|
||||
|
||||
|
||||
v4.5.2
|
||||
======
|
||||
------
|
||||
|
||||
- Fix issue #147. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information.
|
||||
- Make "using Tesseract 4.0" warning less ominous
|
||||
@@ -22,13 +104,13 @@ v4.5.2
|
||||
|
||||
|
||||
v4.5.1
|
||||
======
|
||||
------
|
||||
|
||||
- Fix issue #137, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags
|
||||
|
||||
|
||||
v4.5
|
||||
====
|
||||
----
|
||||
|
||||
- Exotic PDFs containing "Form XObjects" are now supported (issue #134; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
|
||||
- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
|
||||
@@ -36,7 +118,7 @@ v4.5
|
||||
|
||||
|
||||
v4.4.2
|
||||
======
|
||||
------
|
||||
|
||||
- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch
|
||||
|
||||
@@ -47,7 +129,7 @@ v4.4.2
|
||||
|
||||
|
||||
v4.4.1
|
||||
======
|
||||
------
|
||||
|
||||
- To prevent a `TIFF output error <https://github.com/python-pillow/Pillow/issues/2206>`_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened
|
||||
- The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance
|
||||
@@ -56,7 +138,7 @@ v4.4.1
|
||||
- Tweaks to setup.py to deal with issues in the v4.4 release
|
||||
|
||||
v4.4
|
||||
====
|
||||
----
|
||||
|
||||
- Tesseract 4.00 is now supported on an experimental basis.
|
||||
|
||||
@@ -74,32 +156,32 @@ v4.4
|
||||
|
||||
|
||||
v4.3.5
|
||||
======
|
||||
------
|
||||
|
||||
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
|
||||
|
||||
|
||||
v4.3.4
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
|
||||
|
||||
|
||||
v4.3.3
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed PDF/A creation with Ghostscript 9.20 properly
|
||||
- Fixed an exception on inline stencil masks with a missing optional parameter
|
||||
|
||||
|
||||
v4.3.2
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
|
||||
|
||||
|
||||
v4.3.1
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
|
||||
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
|
||||
@@ -110,7 +192,7 @@ v4.3.1
|
||||
|
||||
|
||||
v4.3
|
||||
====
|
||||
----
|
||||
|
||||
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
|
||||
- Better documentation
|
||||
@@ -121,20 +203,20 @@ v4.3
|
||||
+ Some output validation is disabled in this mode
|
||||
|
||||
v4.2.5
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
|
||||
- Removed non-free file milk.pdf
|
||||
|
||||
|
||||
v4.2.4
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed an error (#90) caused by PDFs that use stencil masks properly
|
||||
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
|
||||
|
||||
v4.2.3
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
|
||||
- Integrated a few fixes to simplify downstream packaging (Debian)
|
||||
@@ -148,20 +230,20 @@ v4.2.3
|
||||
|
||||
|
||||
v4.2.2
|
||||
======
|
||||
------
|
||||
|
||||
- Improvements to documentation
|
||||
|
||||
|
||||
v4.2.1
|
||||
======
|
||||
------
|
||||
|
||||
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
|
||||
- Implemented stdin streaming
|
||||
|
||||
|
||||
v4.2
|
||||
====
|
||||
----
|
||||
|
||||
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
|
||||
|
||||
@@ -194,13 +276,13 @@ v4.2
|
||||
- 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
|
||||
@@ -209,14 +291,14 @@ v4.1.3
|
||||
|
||||
|
||||
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
|
||||
@@ -224,20 +306,20 @@ v4.1
|
||||
|
||||
|
||||
v4.0.7
|
||||
======
|
||||
------
|
||||
|
||||
- Minor correction to Ghostscript output settings
|
||||
|
||||
|
||||
v4.0.6
|
||||
======
|
||||
------
|
||||
|
||||
- Update install instructions
|
||||
- Provide a sRGB profile instead of using Ghostscript's
|
||||
|
||||
|
||||
v4.0.5
|
||||
======
|
||||
------
|
||||
|
||||
- Remove some verbose debug messages from v4.0.4
|
||||
- Fixed temporary that wasn't being deleted
|
||||
@@ -245,22 +327,22 @@ v4.0.5
|
||||
- Inline images are now checked during DPI calculation instead of rejecting the image
|
||||
|
||||
v4.0.4
|
||||
======
|
||||
------
|
||||
|
||||
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
|
||||
|
||||
|
||||
v4.0.3
|
||||
======
|
||||
------
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Page orientations detected are now reported in a summary comment
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Show stack trace if unexpected errors occur
|
||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||
@@ -269,10 +351,10 @@ Fixes
|
||||
|
||||
|
||||
v4.0.2
|
||||
======
|
||||
------
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
@@ -281,19 +363,19 @@ Fixes
|
||||
|
||||
|
||||
v4.0.1
|
||||
======
|
||||
------
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
|
||||
v4.0
|
||||
====
|
||||
----
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
|
||||
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
|
||||
@@ -303,7 +385,7 @@ New features
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
|
||||
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
|
||||
@@ -312,7 +394,7 @@ Fixes
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Logging output is now much easier to read
|
||||
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
|
||||
@@ -325,20 +407,20 @@ Changes
|
||||
|
||||
|
||||
v3.2.1
|
||||
======
|
||||
------
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
|
||||
- Tweaked the Dockerfiles
|
||||
|
||||
|
||||
v3.2
|
||||
====
|
||||
----
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without
|
||||
otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
|
||||
@@ -350,25 +432,25 @@ New features
|
||||
for the polyglots among us. It is much larger.
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
|
||||
|
||||
|
||||
|
||||
v3.1.1
|
||||
======
|
||||
------
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
|
||||
|
||||
v3.1
|
||||
====
|
||||
----
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- Default output format is now PDF/A-2b instead of PDF/A-1b
|
||||
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
|
||||
@@ -383,10 +465,10 @@ Changes
|
||||
- Set up Travis CI automatic integration testing
|
||||
|
||||
v3.0
|
||||
====
|
||||
----
|
||||
|
||||
New features
|
||||
------------
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Easier installation with a Docker container or Python's ``pip`` package manager
|
||||
- Eliminated many external dependencies, so it's easier to setup
|
||||
@@ -409,7 +491,7 @@ New features
|
||||
- Multiple images on the same PDF page are now supported
|
||||
|
||||
Changes
|
||||
-------
|
||||
^^^^^^^
|
||||
|
||||
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines
|
||||
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
|
||||
@@ -447,7 +529,7 @@ Changes
|
||||
.. _JHOVE: http://jhove.sourceforge.net/
|
||||
|
||||
Release candidates
|
||||
------------------
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- rc9:
|
||||
|
||||
@@ -515,12 +597,12 @@ where ``settings.txt`` contains *one argument per line*, for example:
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
^^^^^
|
||||
|
||||
- Handling of filenames containing spaces: fixed
|
||||
|
||||
Notes and known issues
|
||||
----------------------
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- Some dependencies may work with lower versions than tested, so try
|
||||
overriding dependencies if they are "in the way" to see if they work.
|
||||
@@ -533,7 +615,7 @@ Notes and known issues
|
||||
|
||||
|
||||
v2.2-stable (2014-09-29)
|
||||
========================
|
||||
------------------------
|
||||
|
||||
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.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
PDF Security Issues
|
||||
PDF security issues
|
||||
===================
|
||||
|
||||
OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import pkg_resources
|
||||
|
||||
PROGRAM_NAME = 'ocrmypdf'
|
||||
@@ -5,11 +7,3 @@ PROGRAM_NAME = 'ocrmypdf'
|
||||
VERSION = pkg_resources.get_distribution('ocrmypdf').version
|
||||
|
||||
|
||||
# These imports are for v4.x backward compatibility for consumers of ocrmypdf
|
||||
# (if any). They are deprecated and will be removed in v5.x.
|
||||
from .exec import ghostscript, qpdf, tesseract, unpaper, get_program
|
||||
from .exceptions import ExitCode
|
||||
from .helpers import page_number, is_iterable_notstr
|
||||
|
||||
|
||||
|
||||
|
||||
+182
-83
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015-16 James R. Barlow: github.com/jbarlow83
|
||||
# © 2015-17 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from contextlib import suppress
|
||||
from tempfile import mkdtemp
|
||||
@@ -39,13 +39,20 @@ warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
|
||||
# -------------
|
||||
# External dependencies
|
||||
|
||||
MINIMUM_TESS_VERSION = '3.02.02'
|
||||
MINIMUM_TESS_VERSION = '3.04'
|
||||
|
||||
HOCR_OK_LANGS = frozenset([
|
||||
'eng', 'deu', 'spa', 'ita', 'por'
|
||||
])
|
||||
|
||||
def complain(message):
|
||||
print(*textwrap.wrap(message), file=sys.stderr)
|
||||
|
||||
|
||||
# Hack to help debugger context find /usr/local/bin
|
||||
if 'IDE_PROJECT_ROOTS' in os.environ:
|
||||
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
|
||||
|
||||
if tesseract.version() < MINIMUM_TESS_VERSION:
|
||||
complain(
|
||||
"Please install tesseract {0} or newer "
|
||||
@@ -56,14 +63,9 @@ if tesseract.version() < MINIMUM_TESS_VERSION:
|
||||
# -------------
|
||||
# Parser
|
||||
|
||||
parser = cmdline.get_argparse(
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=PROGRAM_NAME,
|
||||
version=VERSION,
|
||||
fromfile_prefix_chars='@',
|
||||
ignored_args=[
|
||||
'touch_files_only', 'recreate_database', 'checksum_file_name',
|
||||
'key_legend_in_graph', 'draw_graph_horizontally', 'flowchart_format',
|
||||
'forced_tasks', 'target_tasks', 'use_threads', 'jobs', 'log_file'],
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description="""\
|
||||
Generates a searchable PDF or PDF/A from a regular PDF.
|
||||
@@ -115,24 +117,22 @@ Online documentation is located at:
|
||||
""")
|
||||
|
||||
parser.add_argument(
|
||||
'input_file',
|
||||
'input_file', metavar="input_pdf_or_image",
|
||||
help="PDF file containing the images to be OCRed (or '-' to read from "
|
||||
"standard input)")
|
||||
parser.add_argument(
|
||||
'output_file',
|
||||
help="output searchable PDF file (or '-' to write to standard output)")
|
||||
'output_file', metavar="output_pdf",
|
||||
help="Output searchable PDF file (or '-' to write to standard output). "
|
||||
"Existing files will be ovewritten. If same as input file, the "
|
||||
"input file will be updated only if processing is successful.")
|
||||
parser.add_argument(
|
||||
'-l', '--language', action='append',
|
||||
help="Language(s) of the file to be OCRed (see tesseract --list-langs for "
|
||||
"all language packs installed in your system). To specify multiple "
|
||||
"languages, join them with '+' or issue this argument once for each "
|
||||
"language.")
|
||||
parser.add_argument(
|
||||
'-j', '--jobs', metavar='N', type=int,
|
||||
help="Use up to N CPU cores simultaneously (default: use all)")
|
||||
"all language packs installed in your system). Use -l eng+deu for "
|
||||
"multiple languages.")
|
||||
parser.add_argument(
|
||||
'--image-dpi', metavar='DPI', type=int,
|
||||
help="for input image instead of PDF, use this DPI instead of file's")
|
||||
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 "
|
||||
@@ -141,47 +141,74 @@ parser.add_argument(
|
||||
"also has problems with full Unicode text. 'pdf' attempts to "
|
||||
"preserve file contents as much as possible.")
|
||||
|
||||
# Use null string '\0' as sentinel to indicate the user supplied no argument,
|
||||
# since that is the only invalid character for filepaths on all platforms
|
||||
# bool('\0') is True in Python
|
||||
parser.add_argument(
|
||||
'--sidecar', nargs='?', const='\0', default=None, metavar='FILE',
|
||||
help="Generate sidecar text files that contain the same text recognized "
|
||||
"by Tesseract. This may be useful for building a OCR text database. "
|
||||
"If FILE is omitted, the sidecar file be named {output_file}.txt "
|
||||
"If FILE is set to '-', the sidecar is written to stdout (a "
|
||||
"convenient way to preview OCR quality). The output file and sidecar "
|
||||
"may not both use stdout at the same time.")
|
||||
|
||||
parser.add_argument(
|
||||
'--version', action='version', version=VERSION,
|
||||
help="Print program version and exit")
|
||||
|
||||
jobcontrol = parser.add_argument_group(
|
||||
"Job control options")
|
||||
jobcontrol.add_argument(
|
||||
'-j', '--jobs', metavar='N', type=int,
|
||||
help="Use up to N CPU cores simultaneously (default: use all).")
|
||||
jobcontrol.add_argument(
|
||||
'-q', '--quiet', action='store_true', help="Suppress INFO messages")
|
||||
jobcontrol.add_argument(
|
||||
'-v', '--verbose', const="+", default=[], nargs='?', action="append",
|
||||
help="Print more verbose messages for each additional verbose level")
|
||||
|
||||
metadata = parser.add_argument_group(
|
||||
"Metadata options",
|
||||
"Set output PDF/A metadata (default: use input document's metadata)")
|
||||
"Set output PDF/A metadata (default: copy input document's metadata)")
|
||||
metadata.add_argument(
|
||||
'--title', type=str,
|
||||
help="set document title (place multiple words in quotes)")
|
||||
help="Set document title (place multiple words in quotes)")
|
||||
metadata.add_argument(
|
||||
'--author', type=str,
|
||||
help="set document author")
|
||||
help="Set document author")
|
||||
metadata.add_argument(
|
||||
'--subject', type=str,
|
||||
help="set document subject description")
|
||||
help="Set document subject description")
|
||||
metadata.add_argument(
|
||||
'--keywords', type=str,
|
||||
help="set document keywords")
|
||||
help="Set document keywords")
|
||||
|
||||
preprocessing = parser.add_argument_group(
|
||||
"Image preprocessing options",
|
||||
"Options to improve the quality of the final PDF and OCR")
|
||||
preprocessing.add_argument(
|
||||
'-r', '--rotate-pages', action='store_true',
|
||||
help="automatically rotate pages based on detected text orientation")
|
||||
help="Automatically rotate pages based on detected text orientation")
|
||||
preprocessing.add_argument(
|
||||
'--remove-background', action='store_true',
|
||||
help="attempt to remove background from gray or color pages, setting it "
|
||||
help="Attempt to remove background from gray or color pages, setting it "
|
||||
"to white ")
|
||||
preprocessing.add_argument(
|
||||
'-d', '--deskew', action='store_true',
|
||||
help="deskew each page before performing OCR")
|
||||
help="Deskew each page before performing OCR")
|
||||
preprocessing.add_argument(
|
||||
'-c', '--clean', action='store_true',
|
||||
help="clean pages from scanning artifacts before performing OCR, and send "
|
||||
help="Clean pages from scanning artifacts before performing OCR, and send "
|
||||
"the cleaned page to OCR, but do not include the cleaned page in "
|
||||
"the output ")
|
||||
"the output")
|
||||
preprocessing.add_argument(
|
||||
'-i', '--clean-final', action='store_true',
|
||||
help="clean page as above, and incorporate the cleaned image in the final "
|
||||
"PDF")
|
||||
help="Clean page as above, and incorporate the cleaned image in the final "
|
||||
"PDF. Might remove desired content.")
|
||||
preprocessing.add_argument(
|
||||
'--oversample', metavar='DPI', type=int, default=0,
|
||||
help="oversample images to at least the specified DPI, to improve OCR "
|
||||
help="Oversample images to at least the specified DPI, to improve OCR "
|
||||
"results slightly")
|
||||
|
||||
ocrsettings = parser.add_argument_group(
|
||||
@@ -189,65 +216,96 @@ ocrsettings = parser.add_argument_group(
|
||||
"Control how OCR is applied")
|
||||
ocrsettings.add_argument(
|
||||
'-f', '--force-ocr', action='store_true',
|
||||
help="rasterize any fonts or vector objects on each page, apply OCR, and "
|
||||
help="Rasterize any fonts or vector objects on each page, apply OCR, and "
|
||||
"save the rastered output (this rewrites the PDF)")
|
||||
ocrsettings.add_argument(
|
||||
'-s', '--skip-text', action='store_true',
|
||||
help="skip OCR on any pages that already contain text, but include the "
|
||||
help="Skip OCR on any pages that already contain text, but include the "
|
||||
"page in final output; useful for PDFs that contain a mix of "
|
||||
"images, text pages, and/or previously OCRed pages")
|
||||
# ocrsettings.add_argument(
|
||||
# '--redo-ocr', action='store_true',
|
||||
# help="removing any existing OCR text, but otherwise preserve mixed PDF "
|
||||
# "pages")
|
||||
|
||||
ocrsettings.add_argument(
|
||||
'--skip-big', type=float, metavar='MPixels',
|
||||
help="skip OCR on pages larger than the specified amount of megapixels, "
|
||||
help="Skip OCR on pages larger than the specified amount of megapixels, "
|
||||
"but include skipped pages in final output")
|
||||
|
||||
advanced = parser.add_argument_group(
|
||||
"Advanced",
|
||||
"Advanced options for power users")
|
||||
"Advanced options to control Tesseract's OCR behavior")
|
||||
advanced.add_argument(
|
||||
'--tesseract-config', action='append', metavar='CFG', default=[],
|
||||
help="additional Tesseract configuration files -- see documentation")
|
||||
help="Additional Tesseract configuration files -- see documentation")
|
||||
advanced.add_argument(
|
||||
'--tesseract-pagesegmode', action='store', type=int, metavar='PSM',
|
||||
choices=range(0, 14),
|
||||
help="set Tesseract page segmentation mode (see tesseract --help)")
|
||||
help="Set Tesseract page segmentation mode (see tesseract --help)")
|
||||
advanced.add_argument(
|
||||
'--tesseract-oem', action='store', type=int, metavar='MODE',
|
||||
choices=range(0, 4),
|
||||
help=("set Tesseract 4.0 OCR engine mode: "
|
||||
help=("Set Tesseract 4.0 OCR engine mode: "
|
||||
"0 - original Tesseract only; "
|
||||
"1 - neural nets LSTM only; "
|
||||
"2 - Tesseract + LSTM; "
|
||||
"3 - default.")
|
||||
)
|
||||
advanced.add_argument(
|
||||
'--pdf-renderer', choices=['auto', 'tesseract', 'hocr', 'tess4'], default='auto',
|
||||
help="choose OCR PDF renderer - the default option is to let OCRmyPDF "
|
||||
"choose. The 'tesseract' PDF renderer is more accurate and does a "
|
||||
"better job and document structure such as recognizing columns. It "
|
||||
"also does a better job on non-Latin languages. However, it does "
|
||||
"not work as well when older versions of Tesseract or Ghostscript "
|
||||
"are installed, and some combinations of arguments to do not work "
|
||||
"with --pdf-renderer tesseract. The 'tess4' PDF renderer is similar "
|
||||
"to 'tesseract', requires tesseract 4, and gives superior results.")
|
||||
'--pdf-renderer',
|
||||
choices=['auto', 'tesseract', 'hocr', 'tess4', 'sandwich'], default='auto',
|
||||
help="Choose OCR PDF renderer - the default option is to let OCRmyPDF "
|
||||
"choose."
|
||||
"auto - let OCRmyPDF choose; "
|
||||
"sandwich - default renderer for Tesseract 3.05.01 and newer; "
|
||||
"hocr - default renderer for older versions of Tesseract; "
|
||||
"tesseract - gives better results for non-Latin languages and "
|
||||
"Tesseract older than 3.05.01 but has problems with some versions "
|
||||
" of Ghostscript; deprecated"
|
||||
"tess4 - deprecated alias for 'sandwich'"
|
||||
)
|
||||
advanced.add_argument(
|
||||
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
|
||||
help='give up on OCR after the timeout, but copy the preprocessed page '
|
||||
help='Give up on OCR after the timeout, but copy the preprocessed page '
|
||||
'into the final output')
|
||||
advanced.add_argument(
|
||||
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
|
||||
help="only rotate pages when confidence is above this value (arbitrary "
|
||||
help="Only rotate pages when confidence is above this value (arbitrary "
|
||||
"units reported by tesseract)")
|
||||
advanced.add_argument(
|
||||
'--pdfa-image-compression', choices=['auto', 'jpeg', 'lossless'],
|
||||
default='auto',
|
||||
help="Specify how to compress images in the output PDF/A. 'auto' lets "
|
||||
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
|
||||
"JPEG compression. 'lossless' uses PNG-style lossless compression "
|
||||
"for all images. Monochrome images are always compressed using a "
|
||||
"lossless codec. Compression settings "
|
||||
"are applied to all pages, including those for which OCR was "
|
||||
"skipped. Not supported for --output-type=pdf ; that setting "
|
||||
"preserves the original compression of all images.")
|
||||
advanced.add_argument(
|
||||
'--user-words', metavar='FILE',
|
||||
help="Specify the location of the Tesseract user words file. This is a "
|
||||
"list of words Tesseract should consider while performing OCR in "
|
||||
"addition to its standard language dictionaries. This can improve "
|
||||
"OCR quality especially for specialized and technical documents.")
|
||||
advanced.add_argument(
|
||||
'--user-patterns', metavar='FILE',
|
||||
help="Specify the location of the Tesseract user patterns file.")
|
||||
|
||||
debugging = parser.add_argument_group(
|
||||
"Debugging",
|
||||
"Arguments to help with troubleshooting and debugging")
|
||||
debugging.add_argument(
|
||||
'-k', '--keep-temporary-files', action='store_true',
|
||||
help="keep temporary files (helpful for debugging)")
|
||||
help="Keep temporary files (helpful for debugging)")
|
||||
debugging.add_argument(
|
||||
'-g', '--debug-rendering', action='store_true',
|
||||
help="render each page twice with debug information on second page")
|
||||
help="Render each page twice with debug information on second page")
|
||||
debugging.add_argument(
|
||||
'--flowchart', type=str,
|
||||
help="Generate the pipeline execution flowchart")
|
||||
|
||||
|
||||
def check_options_languages(options, _log):
|
||||
@@ -264,39 +322,52 @@ def check_options_languages(options, _log):
|
||||
"data for the following requested languages: \n")
|
||||
for lang in (set(options.language) - tesseract.languages()):
|
||||
msg += lang + '\n'
|
||||
raise argparse.ArgumentError(msg)
|
||||
raise MissingDependencyError(msg)
|
||||
|
||||
|
||||
def check_options_output(options, log):
|
||||
if options.pdf_renderer == 'auto':
|
||||
options.pdf_renderer = 'hocr'
|
||||
if tesseract.has_textonly_pdf():
|
||||
options.pdf_renderer = 'sandwich'
|
||||
else:
|
||||
options.pdf_renderer = 'hocr'
|
||||
|
||||
if options.pdf_renderer in ('tesseract', 'tess4'):
|
||||
if tesseract.version() < '3.05':
|
||||
log.warning(
|
||||
"The setting --pdf-renderer=tesseract is not recommend for "
|
||||
" use with tesseract versions less than 3.05, because it "
|
||||
" produces OCR text that is incompatible with Ghostscript and "
|
||||
" some other software.")
|
||||
elif tesseract.version() == '4.00.00alpha':
|
||||
log.warning(
|
||||
"The setting --pdf-renderer={tesseract,tess4} is not"
|
||||
" recommended for builds of tesseract 4.00.00alpha older than"
|
||||
" February 2017. Make sure you are using a recent build.")
|
||||
if options.pdf_renderer == 'tess4':
|
||||
log.warning("The 'tess4' PDF renderer has been renamed to 'sandwich'. "
|
||||
"Please use --pdf-renderer=sandwich.")
|
||||
options.pdf_renderer = 'sandwich'
|
||||
|
||||
if options.debug_rendering and options.pdf_renderer == 'tesseract':
|
||||
if options.pdf_renderer == 'tesseract':
|
||||
log.warning("The 'tesseract' PDF renderer is deprecated.")
|
||||
if tesseract.version() < '3.05' and options.output_type == 'pdfa':
|
||||
log.warning(
|
||||
"For best results use --pdf-renderer=tesseract "
|
||||
"--output-type=pdf to disable PDF/A generation via "
|
||||
"Ghostscript, which is known to corrupt the OCR text of "
|
||||
"some PDFs produced your version of Tesseract.")
|
||||
|
||||
if options.debug_rendering and options.pdf_renderer != 'hocr':
|
||||
log.info(
|
||||
"Ignoring --debug-rendering because it is not supported with"
|
||||
"--pdf-renderer=tesseract.")
|
||||
"Ignoring --debug-rendering because it requires --pdf-renderer=hocr")
|
||||
|
||||
lossless_reconstruction = False
|
||||
if options.pdf_renderer in ('hocr', 'tess4'):
|
||||
if options.pdf_renderer in ('hocr', 'sandwich'):
|
||||
if not any((options.deskew, options.clean_final, options.force_ocr,
|
||||
options.remove_background)):
|
||||
lossless_reconstruction = True
|
||||
options.lossless_reconstruction = lossless_reconstruction
|
||||
|
||||
|
||||
def check_options_sidecar(options, log):
|
||||
if options.sidecar == '\0':
|
||||
if options.output_file == '-':
|
||||
raise argparse.ArgumentError(
|
||||
None,
|
||||
"--sidecar filename must be specified when output file is "
|
||||
"stdout.")
|
||||
options.sidecar = options.output_file + '.txt'
|
||||
|
||||
|
||||
def check_options_preprocessing(options, log):
|
||||
if any((options.clean, options.clean_final)):
|
||||
from .exec import unpaper
|
||||
@@ -320,14 +391,27 @@ def check_options_preprocessing(options, log):
|
||||
def check_options_ocr_behavior(options, log):
|
||||
if options.force_ocr and options.skip_text:
|
||||
raise argparse.ArgumentError(
|
||||
None,
|
||||
"Error: --force-ocr and --skip-text are mutually incompatible.")
|
||||
|
||||
if set(options.language) & {'chi_sim', 'chi_tra'} and \
|
||||
(options.pdf_renderer == 'hocr' or options.output_type == 'pdfa'):
|
||||
log.warning(
|
||||
"Your settings are known to cause problems with OCR of Chinese text. "
|
||||
"Try adding these arguments: "
|
||||
" ocrmypdf --pdf-renderer tesseract --output-type pdf")
|
||||
# if options.redo_ocr and (options.skip_text or options.force_ocr):
|
||||
# raise argparse.ArgumentError(
|
||||
# "Error: --redo-ocr and other OCR options are incompatible.")
|
||||
|
||||
if options.pdf_renderer == 'hocr' and \
|
||||
not set(options.language).issubset(HOCR_OK_LANGS):
|
||||
msg = (
|
||||
"The 'hocr' PDF renderer is known to cause problems with one "
|
||||
"or more of the languages in your document. ")
|
||||
|
||||
if tesseract.has_textonly_pdf():
|
||||
msg += (
|
||||
"Use --pdf-renderer auto (the default) to avoid this issue.")
|
||||
else:
|
||||
msg += (
|
||||
"Use --pdf-renderer tesseract --output-type pdf to avoid "
|
||||
"this issue")
|
||||
log.warning(msg)
|
||||
|
||||
|
||||
def check_options_advanced(options, log):
|
||||
@@ -341,6 +425,12 @@ def check_options_advanced(options, log):
|
||||
raise MissingDependencyError(
|
||||
"--pdf-renderer tess4 requires Tesseract 4.x "
|
||||
"commit 3d9fb3b or later")
|
||||
if options.pdfa_image_compression != 'auto' and \
|
||||
options.output_type != 'pdfa':
|
||||
log.warning(
|
||||
"--pdfa-image-compression argument has no effect when "
|
||||
"--output-type is not 'pdfa'"
|
||||
)
|
||||
|
||||
|
||||
def check_options_metadata(options, log):
|
||||
@@ -362,6 +452,7 @@ def check_options(options, log):
|
||||
check_options_languages(options, log)
|
||||
check_options_metadata(options, log)
|
||||
check_options_output(options, log)
|
||||
check_options_sidecar(options, log)
|
||||
check_options_preprocessing(options, log)
|
||||
check_options_ocr_behavior(options, log)
|
||||
check_options_advanced(options, log)
|
||||
@@ -380,8 +471,9 @@ def check_options(options, log):
|
||||
# Logging
|
||||
|
||||
|
||||
def logging_factory(logger_name, listargs):
|
||||
log_file_name, verbose = listargs
|
||||
def logging_factory(logger_name, logger_args):
|
||||
verbose = logger_args['verbose']
|
||||
quiet = logger_args['quiet']
|
||||
|
||||
root_logger = logging.getLogger(logger_name)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
@@ -391,6 +483,8 @@ def logging_factory(logger_name, listargs):
|
||||
handler.setFormatter(formatter_)
|
||||
if verbose:
|
||||
handler.setLevel(logging.DEBUG)
|
||||
elif quiet:
|
||||
handler.setLevel(logging.WARNING)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
root_logger.addHandler(handler)
|
||||
@@ -416,7 +510,7 @@ def available_cpu_count():
|
||||
|
||||
|
||||
def cleanup_ruffus_error_message(msg):
|
||||
msg = re.sub(r'\s+', r' ', msg, re.MULTILINE)
|
||||
msg = re.sub(r'\s+', r' ', msg)
|
||||
msg = re.sub(r"\((.+?)\)", r'\1', msg)
|
||||
msg = msg.strip()
|
||||
return msg
|
||||
@@ -550,8 +644,10 @@ def run_pipeline():
|
||||
if not check_closed_streams(options):
|
||||
return ExitCode.bad_args
|
||||
|
||||
logger_args = {'verbose': options.verbose, 'quiet': options.quiet}
|
||||
|
||||
_log, _log_mutex = proxy_logger.make_shared_logger_and_proxy(
|
||||
logging_factory, __name__, [None, options.verbose])
|
||||
logging_factory, __name__, logger_args)
|
||||
_log.debug('ocrmypdf ' + VERSION)
|
||||
_log.debug('tesseract ' + tesseract.version())
|
||||
|
||||
@@ -590,8 +686,9 @@ def run_pipeline():
|
||||
file."""))
|
||||
return ExitCode.bad_args
|
||||
elif not is_file_writable(options.output_file):
|
||||
_log.error(textwrap.dedent("""\
|
||||
Cutput file location is not writable."""))
|
||||
_log.error(
|
||||
"Output file location (" + options.output_file + ") " +
|
||||
"is not writable.")
|
||||
return ExitCode.file_access_error
|
||||
|
||||
manager = JobContextManager()
|
||||
@@ -637,7 +734,9 @@ def run_pipeline():
|
||||
_log.error(e)
|
||||
return ExitCode.other_error
|
||||
|
||||
if options.output_file != '-':
|
||||
if options.flowchart:
|
||||
_log.info("Flowchart saved to {}".format(options.flowchart))
|
||||
elif options.output_file != '-':
|
||||
if options.output_type == 'pdfa':
|
||||
pdfa_info = file_claims_pdfa(options.output_file)
|
||||
if pdfa_info['pass']:
|
||||
@@ -662,7 +761,7 @@ def run_pipeline():
|
||||
180: 's', 270: 'w'}
|
||||
orientations = []
|
||||
for n, page in enumerate(pdfinfo):
|
||||
angle = pdfinfo[n].get('rotated', 0)
|
||||
angle = pdfinfo[n].rotation or 0
|
||||
if angle != 0:
|
||||
orientations.append('{0}{1}'.format(
|
||||
n + 1,
|
||||
|
||||
@@ -48,7 +48,7 @@ class InputFileError(ExitCodeException):
|
||||
|
||||
|
||||
class SubprocessOutputError(ExitCodeException):
|
||||
exit_code = ExitCode.other_error
|
||||
exit_code = ExitCode.child_process_error
|
||||
|
||||
|
||||
class EncryptedPdfError(ExitCodeException):
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError, \
|
||||
check_output
|
||||
from subprocess import run, PIPE, STDOUT, CalledProcessError
|
||||
from shutil import copy
|
||||
from functools import lru_cache
|
||||
import re
|
||||
import sys
|
||||
from . import get_program
|
||||
from ..pdfa import SRGB_ICC_PROFILE
|
||||
from ..exceptions import SubprocessOutputError
|
||||
from PIL import Image
|
||||
from ..helpers import fspath
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -28,8 +31,33 @@ def version():
|
||||
return version.strip()
|
||||
|
||||
|
||||
def _gs_error_reported(stream):
|
||||
return re.search(r'error', stream, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
pageno=1):
|
||||
pageno=1, page_dpi=None):
|
||||
"""
|
||||
Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
|
||||
|
||||
The image is sized to match the integer pixels dimensions implied by
|
||||
(xres, yres) even if those numbers are noninteger. The image's DPI will
|
||||
be overridden with the values in page_dpi.
|
||||
|
||||
:param input_file: pathlike
|
||||
:param output_file: pathlike
|
||||
:param xres: resolution at which to rasterize page
|
||||
:param yres:
|
||||
:param raster_device:
|
||||
:param log:
|
||||
:param pageno: page number to rasterize
|
||||
:param page_dpi: resolution tuple (x, y) overriding output image DPI
|
||||
:return:
|
||||
"""
|
||||
res = xres, yres
|
||||
int_res = round(xres), round(yres)
|
||||
if not page_dpi:
|
||||
page_dpi = res
|
||||
with NamedTemporaryFile(delete=True) as tmp:
|
||||
args_gs = [
|
||||
get_program('gs'),
|
||||
@@ -41,49 +69,87 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
'-dFirstPage=%i' % pageno,
|
||||
'-dLastPage=%i' % pageno,
|
||||
'-o', tmp.name,
|
||||
'-r{0}x{1}'.format(str(round(xres)), str(round(yres))),
|
||||
input_file
|
||||
'-r{0}x{1}'.format(str(int_res[0]), str(int_res[1])),
|
||||
fspath(input_file)
|
||||
]
|
||||
|
||||
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
stdout, _ = p.communicate()
|
||||
if 'error' in stdout:
|
||||
log.error(stdout) # Ghostscript puts errors in stdout
|
||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
if _gs_error_reported(p.stdout):
|
||||
log.error(p.stdout)
|
||||
else:
|
||||
log.debug(stdout)
|
||||
log.debug(p.stdout)
|
||||
|
||||
if p.returncode == 0:
|
||||
copy(tmp.name, output_file)
|
||||
else:
|
||||
log.error('Ghostscript rendering failed')
|
||||
if p.returncode != 0:
|
||||
log.error('Ghostscript rasterizing failed')
|
||||
raise SubprocessOutputError()
|
||||
|
||||
# Ghostscript only accepts integers for output resolution
|
||||
# if the resolution happens to be fractional, then the discrepancy
|
||||
# would change the size of the output page, especially if the DPI
|
||||
# is quite low. Resize the image to the expected size
|
||||
tmp.seek(0)
|
||||
with Image.open(tmp) as im:
|
||||
expected_size = round(im.size[0] / int_res[0] * res[0]), \
|
||||
round(im.size[1] / int_res[1] * res[1])
|
||||
if expected_size != im.size or page_dpi != (xres, yres):
|
||||
log.debug(
|
||||
"Ghostscript: resize output image {} -> {}".format(
|
||||
im.size, expected_size))
|
||||
im.resize(expected_size).save(
|
||||
fspath(output_file), dpi=page_dpi)
|
||||
else:
|
||||
copy(tmp.name, fspath(output_file))
|
||||
|
||||
|
||||
def generate_pdfa(pdf_pages, output_file, log, threads=1):
|
||||
def generate_pdfa(pdf_pages, output_file, compression, log,
|
||||
threads=1, pdf_version='1.5'):
|
||||
compression_args = []
|
||||
if compression == 'jpeg':
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=false",
|
||||
"-dColorImageFilter=/DCTEncode",
|
||||
"-dAutoFilterGrayImages=false",
|
||||
"-dGrayImageFilter=/DCTEncode",
|
||||
]
|
||||
elif compression == 'lossless':
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=false",
|
||||
"-dColorImageFilter=/FlateEncode",
|
||||
"-dAutoFilterGrayImages=false",
|
||||
"-dGrayImageFilter=/FlateEncode",
|
||||
]
|
||||
else:
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=true",
|
||||
"-dAutoFilterGrayImages=true",
|
||||
]
|
||||
|
||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||
args_gs = [
|
||||
get_program("gs"),
|
||||
"-dQUIET",
|
||||
"-dBATCH",
|
||||
"-dNOPAUSE",
|
||||
'-dNumRenderingThreads=' + str(threads),
|
||||
"-dCompatibilityLevel=" + str(pdf_version),
|
||||
"-dNumRenderingThreads=" + str(threads),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=/RGB",
|
||||
"-sProcessColorModel=DeviceRGB",
|
||||
"-sProcessColorModel=DeviceRGB"
|
||||
] + compression_args + [
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
stdout, _ = p.communicate()
|
||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
|
||||
universal_newlines=True)
|
||||
|
||||
if 'error' in stdout or 'ERROR' in stdout:
|
||||
log.error(stdout)
|
||||
elif 'overprint mode not set' in stdout:
|
||||
if _gs_error_reported(p.stdout):
|
||||
log.error(p.stdout)
|
||||
elif 'overprint mode not set' in p.stdout:
|
||||
# Unless someone is going to print PDF/A documents on a
|
||||
# magical sRGB printer I can't see the removal of overprinting
|
||||
# being a problem....
|
||||
@@ -92,11 +158,12 @@ def generate_pdfa(pdf_pages, output_file, log, threads=1):
|
||||
"input file to complete PDF/A conversion. "
|
||||
)
|
||||
else:
|
||||
log.debug(stdout)
|
||||
log.debug(p.stdout)
|
||||
|
||||
if p.returncode == 0:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
# PDF/A - check PDF/A status elsewhere
|
||||
copy(gs_pdf.name, output_file)
|
||||
else:
|
||||
log.error('Ghostscript PDF/A failed')
|
||||
log.error('Ghostscript PDF/A rendering failed')
|
||||
raise SubprocessOutputError()
|
||||
+13
-9
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import CalledProcessError, check_output, STDOUT, check_call
|
||||
from subprocess import CalledProcessError, STDOUT, PIPE, run, check_output
|
||||
from functools import lru_cache
|
||||
import sys
|
||||
import os
|
||||
@@ -19,15 +19,14 @@ def version():
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_qpdf, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
p = run(args_qpdf, universal_newlines=True, stderr=STDOUT,
|
||||
stdout=PIPE)
|
||||
except CalledProcessError as e:
|
||||
print("Could not find qpdf executable on system PATH.",
|
||||
file=sys.stderr)
|
||||
raise MissingDependencyError() from e
|
||||
|
||||
qpdf_version = re.match(r'qpdf version (.+)', versions).group(1)
|
||||
qpdf_version = re.match(r'qpdf version (.+)', p.stdout).group(1)
|
||||
return qpdf_version
|
||||
|
||||
|
||||
@@ -118,16 +117,21 @@ def split_pages(input_file, work_folder, npages):
|
||||
'--pages', input_file, '{0}'.format(n + 1), '--',
|
||||
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
||||
]
|
||||
check_call(args_qpdf)
|
||||
run(args_qpdf, check=True)
|
||||
|
||||
|
||||
def merge(input_files, output_file):
|
||||
def merge(input_files, output_file, min_version=None):
|
||||
"""Merge the list of input files (all filenames) into the output file.
|
||||
|
||||
The input files may contain one or more pages.
|
||||
"""
|
||||
version_arg = ['--min-version={}'.format(min_version)] \
|
||||
if min_version else []
|
||||
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_files[0], '--pages'
|
||||
get_program('qpdf')
|
||||
] + version_arg + [
|
||||
input_files[0], '--pages'
|
||||
] + input_files + ['--', output_file]
|
||||
check_call(args_qpdf)
|
||||
run(args_qpdf, check=True)
|
||||
|
||||
|
||||
+85
-69
@@ -21,27 +21,22 @@ OrientationConfidence = namedtuple(
|
||||
'OrientationConfidence',
|
||||
('angle', 'confidence'))
|
||||
|
||||
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name='ocr-system' content='tesseract 3.02.02' />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<meta name='ocr-system' content='tesseract 3.05.00' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "x.tif"; bbox 0 0 {0} {1}; ppageno 0'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 0 1 {0} {1}">
|
||||
<p class='ocr_par' dir='ltr' id='par_1' title="bbox 0 1 {0} {1}">
|
||||
<span class='ocr_line' id='line_1' title="bbox 0 1 {0} {1}"><span class='ocrx_word' id='word_1' title="bbox 0 1 {0} {1}"> </span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "_blank.png"; bbox 0 0 {0} {1}; ppageno 0'>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -68,26 +63,28 @@ def v4():
|
||||
return (version() >= '4')
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def has_textonly_pdf():
|
||||
if version() == '4.00.00alpha':
|
||||
# textonly_pdf added during the 4.00.00alpha cycle, so we must test
|
||||
# more carefully to see if it is present
|
||||
args_tess = [
|
||||
get_program('tesseract'),
|
||||
'--print-parameters'
|
||||
]
|
||||
try:
|
||||
params = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError as e:
|
||||
print("Could not --print-parameters from tesseract",
|
||||
file=sys.stderr)
|
||||
raise MissingDependencyError from e
|
||||
if 'textonly_pdf' in params:
|
||||
return True
|
||||
else:
|
||||
return v4()
|
||||
"""Does Tesseract have textonly_pdf capability?
|
||||
|
||||
Available in 3.05.01, and v4.00.00alpha since January 2017. Best to
|
||||
parse the parameter list
|
||||
"""
|
||||
args_tess = [
|
||||
get_program('tesseract'),
|
||||
'--print-parameters'
|
||||
]
|
||||
try:
|
||||
params = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError as e:
|
||||
print("Could not --print-parameters from tesseract",
|
||||
file=sys.stderr)
|
||||
raise MissingDependencyError from e
|
||||
if 'textonly_pdf' in params:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def psm():
|
||||
@@ -200,7 +197,7 @@ def page_timedout(log, input_file):
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, image):
|
||||
def _generate_null_hocr(output_hocr, output_sidecar, image):
|
||||
"""Produce a .hocr file that reports no text detected on a page that is
|
||||
the same size as the input image."""
|
||||
from PIL import Image
|
||||
@@ -210,23 +207,40 @@ def _generate_null_hocr(output_hocr, image):
|
||||
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(w, h))
|
||||
with open(output_sidecar, 'w', encoding='utf-8') as f:
|
||||
f.write('[skipped page]')
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, engine_mode,
|
||||
def generate_hocr(input_file, output_files, language: list, engine_mode,
|
||||
tessconfig: list,
|
||||
timeout: float, pagesegmode: int, log):
|
||||
timeout: float, pagesegmode: int, user_words, user_patterns,
|
||||
log):
|
||||
|
||||
badxml = os.path.splitext(output_hocr)[0] + '.badxml'
|
||||
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
|
||||
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
|
||||
prefix = os.path.splitext(output_hocr)[0]
|
||||
|
||||
args_tesseract = tess_base_args(language, engine_mode)
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend([psm(), str(pagesegmode)])
|
||||
|
||||
if user_words:
|
||||
args_tesseract.extend(['--user-words', user_words])
|
||||
|
||||
if user_patterns:
|
||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||
|
||||
# Reminder: test suite tesseract spoofers will break after any changes
|
||||
# to the number of order parameters here
|
||||
# Tesseract 3.04 requires the order here to be "hocr txt" and will fail
|
||||
# on "txt hocr"
|
||||
|
||||
args_tesseract.extend([
|
||||
input_file,
|
||||
badxml,
|
||||
'hocr'
|
||||
prefix,
|
||||
'hocr',
|
||||
'txt'
|
||||
] + tessconfig)
|
||||
try:
|
||||
log.debug(args_tesseract)
|
||||
@@ -238,42 +252,28 @@ def generate_hocr(input_file, output_hocr, language: list, engine_mode,
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, input_file)
|
||||
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if 'read_params_file: parameter not found' in e.output:
|
||||
raise TesseractConfigError() from e
|
||||
if 'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, input_file)
|
||||
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
||||
return
|
||||
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_file)
|
||||
|
||||
if os.path.exists(badxml + '.html'):
|
||||
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
|
||||
shutil.move(badxml + '.html', badxml)
|
||||
elif os.path.exists(badxml + '.hocr'):
|
||||
# Tesseract 3.03 appends suffix ".hocr" on its own (.badxml.hocr)
|
||||
shutil.move(badxml + '.hocr', badxml)
|
||||
|
||||
# Tesseract 3.03 inserts source filename into hocr file without
|
||||
# escaping it, creating invalid XML and breaking the parser.
|
||||
# As a workaround, rewrite the hocr file, replacing the filename
|
||||
# with a space. Don't know if Tesseract 3.02 does the same.
|
||||
|
||||
regex_nested_single_quotes = re.compile(
|
||||
r"""title='image "([^"]*)";""")
|
||||
with open(badxml, mode='r', encoding='utf-8') as f_in, \
|
||||
open(output_hocr, mode='w', encoding='utf-8') as f_out:
|
||||
for line in f_in:
|
||||
line = regex_nested_single_quotes.sub(
|
||||
r"""title='image " ";""", line)
|
||||
f_out.write(line)
|
||||
# The sidecar text file will get the suffix .txt; rename it to
|
||||
# whatever caller wants it named
|
||||
if os.path.exists(prefix + '.txt'):
|
||||
shutil.move(prefix + '.txt', output_sidecar)
|
||||
|
||||
|
||||
def use_skip_page(text_only, skip_pdf, output_pdf):
|
||||
def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
|
||||
with open(output_text, 'w') as f:
|
||||
f.write('[skipped page]')
|
||||
|
||||
if not text_only:
|
||||
os.symlink(skip_pdf, output_pdf)
|
||||
return
|
||||
@@ -291,14 +291,16 @@ def use_skip_page(text_only, skip_pdf, output_pdf):
|
||||
pdf_out.write(out)
|
||||
|
||||
|
||||
def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
engine_mode, text_only: bool,
|
||||
tessconfig: list, timeout: float, pagesegmode: int, log):
|
||||
def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
|
||||
language: list, engine_mode, text_only: bool,
|
||||
tessconfig: list, timeout: float, pagesegmode: int,
|
||||
user_words, user_patterns, log):
|
||||
'''Use Tesseract to render a PDF.
|
||||
|
||||
input_image -- image to analyze
|
||||
skip_pdf -- if we time out, use this file as output
|
||||
output_pdf -- file to generate
|
||||
output_text -- OCR text file
|
||||
language -- list of languages to consider
|
||||
engine_mode -- engine mode argument for tess v4
|
||||
text_only -- enable tesseract text only mode?
|
||||
@@ -315,10 +317,22 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
if text_only:
|
||||
args_tesseract.extend(['-c', 'textonly_pdf=1'])
|
||||
|
||||
if user_words:
|
||||
args_tesseract.extend(['--user-words', user_words])
|
||||
|
||||
if user_patterns:
|
||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||
|
||||
prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes
|
||||
|
||||
# Reminder: test suite tesseract spoofers might break after any changes
|
||||
# to the number of order parameters here
|
||||
|
||||
args_tesseract.extend([
|
||||
input_image,
|
||||
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
prefix,
|
||||
'pdf',
|
||||
'txt'
|
||||
] + tessconfig)
|
||||
|
||||
try:
|
||||
@@ -326,16 +340,18 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
if os.path.exists(prefix + '.txt'):
|
||||
shutil.move(prefix + '.txt', output_text)
|
||||
except TimeoutExpired:
|
||||
page_timedout(log, input_image)
|
||||
use_skip_page(text_only, skip_pdf, output_pdf)
|
||||
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_image)
|
||||
if 'read_params_file: parameter not found' in e.output:
|
||||
raise TesseractConfigError() from e
|
||||
|
||||
if 'Image too large' in e.output:
|
||||
use_skip_page(text_only, skip_pdf, output_pdf)
|
||||
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
||||
return
|
||||
raise e from e
|
||||
else:
|
||||
|
||||
+43
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
from functools import partial
|
||||
from collections.abc import Iterable
|
||||
from contextlib import suppress
|
||||
from contextlib import suppress, contextmanager
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -77,3 +77,45 @@ def is_file_writable(test_file):
|
||||
with suppress(OSError):
|
||||
os.unlink(test_file)
|
||||
return True
|
||||
|
||||
|
||||
if sys.version_info[0:2] <= (3, 5):
|
||||
def universal_open(p, *args, **kwargs):
|
||||
"Work around Python 3.5's inability to open(pathlib.Path())"
|
||||
try:
|
||||
return p.open(*args, **kwargs)
|
||||
except AttributeError:
|
||||
return open(p, *args, **kwargs)
|
||||
|
||||
|
||||
def fspath(path):
|
||||
import pathlib
|
||||
'''https://www.python.org/dev/peps/pep-0519/#os'''
|
||||
if isinstance(path, (str, bytes)):
|
||||
return path
|
||||
|
||||
# Work from the object's type to match method resolution of other magic
|
||||
# methods.
|
||||
path_type = type(path)
|
||||
try:
|
||||
path = path_type.__fspath__(path)
|
||||
except AttributeError:
|
||||
# Added for Python 3.5 support.
|
||||
if isinstance(path, pathlib.Path):
|
||||
return str(path)
|
||||
elif hasattr(path_type, '__fspath__'):
|
||||
raise
|
||||
else:
|
||||
if isinstance(path, (str, bytes)):
|
||||
return path
|
||||
else:
|
||||
raise TypeError("expected __fspath__() to return str or bytes, "
|
||||
"not " + type(path).__name__)
|
||||
|
||||
raise TypeError(
|
||||
"expected str, bytes, pathlib.Path or os.PathLike object, not "
|
||||
+ path_type.__name__)
|
||||
|
||||
else:
|
||||
universal_open = open
|
||||
fspath = os.fspath
|
||||
@@ -14,6 +14,7 @@ from ctypes.util import find_library
|
||||
from .lib._leptonica import ffi
|
||||
from functools import lru_cache
|
||||
from enum import Enum
|
||||
from .helpers import fspath
|
||||
|
||||
lept = ffi.dlopen(find_library('lept'))
|
||||
|
||||
@@ -208,26 +209,27 @@ class Pix:
|
||||
return 'P'
|
||||
|
||||
@classmethod
|
||||
def read(cls, filename):
|
||||
def read(cls, path):
|
||||
"""Load an image file into a PIX object.
|
||||
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
|
||||
loading fails then the object will wrap a C null pointer.
|
||||
"""
|
||||
filename = fspath(path)
|
||||
with LeptonicaErrorTrap():
|
||||
return cls(lept.pixRead(
|
||||
filename.encode(sys.getfilesystemencoding())))
|
||||
return cls(lept.pixRead(os.fsencode(filename)))
|
||||
|
||||
def write_implied_format(
|
||||
self, filename, jpeg_quality=0, jpeg_progressive=0):
|
||||
self, path, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
"""
|
||||
filename = fspath(path)
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
os.fsencode(filename),
|
||||
self._pix, jpeg_quality, jpeg_progressive)
|
||||
|
||||
def topil(self):
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
"""Bindings to external libraries"""
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
@@ -1,3 +1,5 @@
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from cffi import FFI
|
||||
|
||||
ffibuilder = FFI()
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015-16 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
# This file is now an alias for __main__
|
||||
# Consider removing in future releases
|
||||
|
||||
from ocrmypdf.__main__ import *
|
||||
@@ -3,19 +3,16 @@
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal
|
||||
from math import hypot
|
||||
from math import hypot, isclose
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
import warnings
|
||||
|
||||
try:
|
||||
from math import isclose
|
||||
except ImportError:
|
||||
def isclose(a, b, rel_tol=1e-9):
|
||||
"Python 3.4 does not have math.isclose()"
|
||||
diff = abs(b - a)
|
||||
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
|
||||
|
||||
warnings.warn("ocrmypdf.pageinfo is deprecated'; use ocrmypdf.pdfinfo",
|
||||
DeprecationWarning)
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
@@ -430,7 +427,7 @@ def _find_images(pdf, container, shorthand=None):
|
||||
|
||||
"""
|
||||
|
||||
if container.get('/Type') == '/Page':
|
||||
if container.get('/Type') == '/Page' and '/Contents' in container:
|
||||
# For a /Page the content stream is attached to the page's /Contents
|
||||
page = container
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
@@ -464,6 +461,9 @@ def _find_images(pdf, container, shorthand=None):
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
if not '/Contents' in page:
|
||||
return False
|
||||
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
if text.strip() != '':
|
||||
|
||||
@@ -122,10 +122,6 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
|
||||
f.write(ps)
|
||||
|
||||
|
||||
# The old name is generate_pdfa_def -- now deprecated
|
||||
generate_pdfa_def = generate_pdfa_ps
|
||||
|
||||
|
||||
def file_claims_pdfa(filename):
|
||||
"""Determines if the file claims to be PDF/A compliant
|
||||
|
||||
|
||||
@@ -0,0 +1,729 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal
|
||||
from math import hypot, isclose
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
from collections.abc import MutableMapping, Mapping
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from .helpers import universal_open
|
||||
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
Colorspace = Enum('Colorspace',
|
||||
'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
|
||||
|
||||
Encoding = Enum('Encoding',
|
||||
'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + \
|
||||
'runlength')
|
||||
|
||||
|
||||
FRIENDLY_COLORSPACE = {
|
||||
'/DeviceGray': Colorspace.gray,
|
||||
'/CalGray': Colorspace.gray,
|
||||
'/DeviceRGB': Colorspace.rgb,
|
||||
'/CalRGB': Colorspace.rgb,
|
||||
'/DeviceCMYK': Colorspace.cmyk,
|
||||
'/Lab': Colorspace.lab,
|
||||
'/ICCBased': Colorspace.icc,
|
||||
'/Indexed': Colorspace.index,
|
||||
'/Separation': Colorspace.sep,
|
||||
'/DeviceN': Colorspace.devn,
|
||||
'/Pattern': Colorspace.pattern,
|
||||
'/G': Colorspace.gray, # Abbreviations permitted in inline images
|
||||
'/RGB': Colorspace.rgb,
|
||||
'/CMYK': Colorspace.cmyk,
|
||||
'/I': Colorspace.index,
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING = {
|
||||
'/CCITTFaxDecode': Encoding.ccitt,
|
||||
'/DCTDecode': Encoding.jpeg,
|
||||
'/JPXDecode': Encoding.jpeg2000,
|
||||
'/JBIG2Decode': Encoding.jbig2,
|
||||
'/CCF': Encoding.ccitt, # Abbreviations permitted in inline images
|
||||
'/DCT': Encoding.jpeg,
|
||||
'/AHx': Encoding.asciihex,
|
||||
'/A85': Encoding.ascii85,
|
||||
'/LZW': Encoding.lzw,
|
||||
'/Fl': Encoding.flate,
|
||||
'/RL': Encoding.runlength
|
||||
}
|
||||
|
||||
FRIENDLY_COMP = {
|
||||
Colorspace.gray: 1,
|
||||
Colorspace.rgb: 3,
|
||||
Colorspace.cmyk: 4,
|
||||
Colorspace.lab: 3,
|
||||
Colorspace.index: 1
|
||||
}
|
||||
|
||||
|
||||
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def _matrix_from_shorthand(shorthand):
|
||||
"""Convert from PDF matrix shorthand to full matrix
|
||||
|
||||
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
|
||||
since the last column is always (0, 0, 1).
|
||||
"""
|
||||
|
||||
a, b, c, d, e, f = map(float, shorthand)
|
||||
return ((a, b, 0),
|
||||
(c, d, 0),
|
||||
(e, f, 1))
|
||||
|
||||
|
||||
def _shorthand_from_matrix(matrix):
|
||||
"""Convert from transformation matrix to PDF shorthand."""
|
||||
a, b = matrix[0][0], matrix[0][1]
|
||||
c, d = matrix[1][0], matrix[1][1]
|
||||
e, f = matrix[2][0], matrix[2][1]
|
||||
return tuple(map(float, (a, b, c, d, e, f)))
|
||||
|
||||
|
||||
def _is_unit_square(shorthand):
|
||||
values = map(float, shorthand)
|
||||
pairwise = zip(values, UNIT_SQUARE)
|
||||
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
|
||||
|
||||
XobjectSettings = namedtuple('XobjectSettings',
|
||||
['name', 'shorthand', 'stack_depth'])
|
||||
|
||||
InlineSettings = namedtuple('InlineSettings',
|
||||
['settings', 'shorthand', 'stack_depth'])
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['xobject_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _normalize_stack(operations):
|
||||
"""Fix runs of qQ's in the stack
|
||||
|
||||
For some reason PyPDF2 converts runs of qqq, QQ, QQQq, etc. into single
|
||||
operations. Break this silliness up and issue each stack operation
|
||||
individually so we don't lose count.
|
||||
|
||||
"""
|
||||
for operands, command in operations:
|
||||
if re.match(br'Q*q+$', command): # Zero or more Q, one or more q
|
||||
for char in command: # Split into individual bytes
|
||||
yield ([], bytes([char])) # Yield individual bytes
|
||||
else:
|
||||
yield (operands, command)
|
||||
|
||||
|
||||
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
|
||||
"""Interpret the PDF content stream
|
||||
|
||||
The stack represents the state of the PDF graphics stack. We are only
|
||||
interested in the current transformation matrix (CTM) so we only track
|
||||
this object; a full implementation would need to track many other items.
|
||||
|
||||
The CTM is initialized to the mapping from user space to device space.
|
||||
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
|
||||
to the transformation to device space. For example if set to
|
||||
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
|
||||
|
||||
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
|
||||
image there should be a 'cm' that sets up an image coordinate system
|
||||
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
|
||||
page.
|
||||
|
||||
PDF units suit our needs so we initialize ctm to the identity matrix.
|
||||
|
||||
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
|
||||
|
||||
"""
|
||||
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand(initial_shorthand)
|
||||
xobject_settings = []
|
||||
inline_images = []
|
||||
|
||||
for n, op in enumerate(_normalize_stack(operations)):
|
||||
operands, command = op
|
||||
if command == b'q':
|
||||
stack.append(ctm)
|
||||
if len(stack) > 32:
|
||||
raise RuntimeError(
|
||||
"PDF graphics stack overflow, command %i" % n)
|
||||
elif command == b'Q':
|
||||
try:
|
||||
ctm = stack.pop()
|
||||
except IndexError:
|
||||
raise RuntimeError(
|
||||
"PDF graphics stack underflow, command %i" % n)
|
||||
elif command == b'cm':
|
||||
ctm = matrix_mult(
|
||||
_matrix_from_shorthand(operands), ctm)
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
settings = XobjectSettings(
|
||||
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
xobject_settings.append(settings)
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline = InlineSettings(
|
||||
settings=settings, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
inline_images.append(inline)
|
||||
|
||||
return ContentsInfo(
|
||||
xobject_settings=xobject_settings,
|
||||
inline_images=inline_images)
|
||||
|
||||
|
||||
def _get_dpi(ctm_shorthand, image_size):
|
||||
"""Given the transformation matrix and image size, find the image DPI.
|
||||
|
||||
PDFs do not include image resolution information within image data.
|
||||
Instead, the PDF page content stream describes the location where the
|
||||
image will be rasterized, and the effective resolution is the ratio of the
|
||||
pixel size to raster target size.
|
||||
|
||||
Normally a scanned PDF has the paper size set appropriately but this is
|
||||
not guaranteed. The most common case is a cropped image will change the
|
||||
page size (/CropBox) without altering the page content stream. That means
|
||||
it is not sufficient to assume that the image fills the page, even though
|
||||
that is the most common case.
|
||||
|
||||
A PDF image may be scaled (always), cropped, translated, rotated in place
|
||||
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
|
||||
be expressed, that is, it is not necessary to consider distortions where
|
||||
the effective DPI varies with position.
|
||||
|
||||
To determine the image scale, transform an offset axis vector v0 (0, 0),
|
||||
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
|
||||
which gives the dimensions of the image in PDF units. From there we can
|
||||
compare to actual image dimensions. PDF uses
|
||||
row vector * matrix_tranposed unlike the traditional
|
||||
matrix * column vector.
|
||||
|
||||
The offset, width and height vectors can be combined in a matrix and
|
||||
multiplied by the transform matrix. Then we want to calculated
|
||||
magnitude(width_vector - offset_vector)
|
||||
and
|
||||
magnitude(height_vector - offset_vector)
|
||||
|
||||
When the above is worked out algebraically, the effect of translation
|
||||
cancels out, and the vector magnitudes become functions of the nonzero
|
||||
transformation matrix indices. The results of the derivation are used
|
||||
in this code.
|
||||
|
||||
pdfimages -list does calculate the DPI in some way that is not completely
|
||||
naive, but it does not get the DPI of rotated images right, so cannot be
|
||||
used anymore to validate this. Photoshop works, or using Acrobat to
|
||||
rotate the image back to normal.
|
||||
|
||||
It does not matter if the image is partially cropped, or even out of the
|
||||
/MediaBox.
|
||||
|
||||
"""
|
||||
|
||||
a, b, c, d, _, _ = ctm_shorthand
|
||||
|
||||
# Calculate the width and height of the image in PDF units
|
||||
image_drawn_width = hypot(a, b)
|
||||
image_drawn_height = hypot(c, d)
|
||||
|
||||
# The scale of the image is pixels per unit of default user space (1/72")
|
||||
scale_w = image_size[0] / image_drawn_width
|
||||
scale_h = image_size[1] / image_drawn_height
|
||||
|
||||
# DPI = scale * 72
|
||||
dpi_w = scale_w * 72.0
|
||||
dpi_h = scale_h * 72.0
|
||||
|
||||
return dpi_w, dpi_h
|
||||
|
||||
|
||||
class ImageInfo:
|
||||
DPI_PREC = Decimal('1.000')
|
||||
|
||||
def __init__(self, *, name='', pdfimage=None, inline=None,
|
||||
shorthand=None):
|
||||
|
||||
self._name = name
|
||||
self._shorthand = shorthand
|
||||
if inline:
|
||||
# Fixme does not work for inline images with non abbreviated
|
||||
# fields
|
||||
self._origin = 'inline'
|
||||
self._width = inline.settings['/W']
|
||||
self._height = inline.settings['/H']
|
||||
self._bpc = inline.settings.get('/BPC', 8)
|
||||
try:
|
||||
self._color = FRIENDLY_COLORSPACE[inline.settings['/CS']]
|
||||
except Exception:
|
||||
self._color = '-'
|
||||
self._comp = FRIENDLY_COMP.get(self._color, '?')
|
||||
if '/F' in inline.settings:
|
||||
filter_ = inline.settings['/F']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
self._enc = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
self._enc = 'image'
|
||||
elif pdfimage:
|
||||
self._origin = 'xobject'
|
||||
self._width = pdfimage['/Width']
|
||||
self._height = pdfimage['/Height']
|
||||
if '/BitsPerComponent' in pdfimage:
|
||||
self._bpc = pdfimage['/BitsPerComponent']
|
||||
else:
|
||||
self._bpc = 8
|
||||
|
||||
# Fixme: this is incorrectly treats explicit masks as stencil masks,
|
||||
# but good enough for now. Explicit masks have /ImageMask true but are
|
||||
# never called for in content stream, instead are drawn as a /Mask on
|
||||
# other images. For our purposes finding out the details of /Mask
|
||||
# will seldom matter.
|
||||
if '/ImageMask' in pdfimage:
|
||||
self._type = 'stencil' if pdfimage['/ImageMask'].value \
|
||||
else 'image'
|
||||
else:
|
||||
self._type = 'image'
|
||||
if '/Filter' in pdfimage:
|
||||
filter_ = pdfimage['/Filter']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
self._enc = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
self._enc = 'image'
|
||||
if '/ColorSpace' in pdfimage:
|
||||
cs = pdfimage['/ColorSpace']
|
||||
if isinstance(cs, pypdf.generic.ArrayObject):
|
||||
cs = cs[0]
|
||||
self._color = FRIENDLY_COLORSPACE.get(cs, '-')
|
||||
else:
|
||||
self._color = FRIENDLY_COLORSPACE[Colorspace.jpeg2000] \
|
||||
if self._enc == Encoding.jpeg2000 else '?'
|
||||
|
||||
self._comp = FRIENDLY_COMP.get(self._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 self._comp == '?' and self._enc in (Encoding.ccitt, 'jbig2'):
|
||||
self._comp = FRIENDLY_COMP[Colorspace.gray]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def type_(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def bpc(self):
|
||||
return self._bpc
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
@property
|
||||
def comp(self):
|
||||
return self._comp
|
||||
|
||||
@property
|
||||
def enc(self):
|
||||
return self._enc
|
||||
|
||||
@property
|
||||
def xres(self):
|
||||
return _get_dpi(self._shorthand, (self._width, self._height))[0]
|
||||
|
||||
@property
|
||||
def yres(self):
|
||||
return _get_dpi(self._shorthand, (self._width, self._height))[1]
|
||||
|
||||
def __getitem__(self, item):
|
||||
warnings.warn("ImageInfo.__getitem__", DeprecationWarning)
|
||||
if item in ('name', 'width', 'height', 'bpc', 'color', 'comp', 'enc'):
|
||||
return getattr(self, item)
|
||||
elif item == 'dpi_w':
|
||||
return Decimal(self.xres).quantize(self.DPI_PREC)
|
||||
elif item == 'dpi_h':
|
||||
return Decimal(self.yres).quantize(self.DPI_PREC)
|
||||
elif item == 'dpi':
|
||||
return Decimal(self.xres * self.yres).sqrt().quantize(
|
||||
self.DPI_PREC)
|
||||
else:
|
||||
raise KeyError(item)
|
||||
|
||||
def __repr__(self):
|
||||
class_locals = {attr: getattr(self, attr, None) for attr in dir(self)
|
||||
if not attr.startswith('_')}
|
||||
return (
|
||||
"<ImageInfo '{name}' {type_} {width}x{height} {color} "
|
||||
"{comp} {bpc} {enc} {xres}x{yres}>").format(**class_locals)
|
||||
|
||||
|
||||
def _find_inline_images(contentsinfo):
|
||||
"Find inline images in the contentstream"
|
||||
|
||||
for n, inline in enumerate(contentsinfo.inline_images):
|
||||
yield ImageInfo(name='inline-%02d' % n, shorthand=inline.shorthand,
|
||||
inline=inline)
|
||||
|
||||
|
||||
def _image_xobjects(container):
|
||||
"""Search for all XObject-based images in the container
|
||||
|
||||
Usually the container is a page, but it could also be a Form XObject
|
||||
that contains images. Filter out the Form XObjects which are dealt with
|
||||
elsewhere.
|
||||
|
||||
Generate a sequence of tuples (image, xobj container), where container,
|
||||
where xobj is the name of the object and image is the object itself,
|
||||
since the object does not know its own name.
|
||||
|
||||
"""
|
||||
|
||||
if '/Resources' not in container:
|
||||
return
|
||||
resources = container['/Resources']
|
||||
if '/XObject' not in resources:
|
||||
return
|
||||
for xobj in resources['/XObject']:
|
||||
candidate = resources['/XObject'][xobj]
|
||||
if candidate['/Subtype'] == '/Image':
|
||||
pdfimage = candidate
|
||||
yield (pdfimage, xobj)
|
||||
|
||||
|
||||
def _find_regular_images(container, contentsinfo):
|
||||
"""Find images stored in the container's /Resources /XObject
|
||||
|
||||
Usually the container is a page, but it could also be a Form XObject
|
||||
that contains images.
|
||||
|
||||
Generates images with their DPI at time of drawing.
|
||||
|
||||
"""
|
||||
|
||||
for pdfimage, xobj in _image_xobjects(container):
|
||||
|
||||
# For each image that is drawn on this, check if we drawing the
|
||||
# current image - yes this is O(n^2), but n == 1 almost always
|
||||
for draw in contentsinfo.xobject_settings:
|
||||
if draw.name != xobj:
|
||||
continue
|
||||
|
||||
if draw.stack_depth == 0 and _is_unit_square(draw.shorthand):
|
||||
# At least one PDF in the wild (and test suite) draws an image
|
||||
# when the graphics stack depth is 0, meaning that the image
|
||||
# gets drawn into a square of 1x1 PDF units (or 1/72",
|
||||
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
|
||||
# these from our DPI calculation for the page.
|
||||
continue
|
||||
|
||||
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=
|
||||
draw.shorthand)
|
||||
|
||||
|
||||
def _find_form_xobject_images(pdf, container, contentsinfo):
|
||||
"""Find any images that are in Form XObjects in the container
|
||||
|
||||
The container may be a page, or a parent Form XObject.
|
||||
|
||||
"""
|
||||
if '/Resources' not in container:
|
||||
return
|
||||
resources = container['/Resources']
|
||||
if '/XObject' not in resources:
|
||||
return
|
||||
for xobj in resources['/XObject']:
|
||||
candidate = resources['/XObject'][xobj]
|
||||
if candidate['/Subtype'] != '/Form':
|
||||
continue
|
||||
|
||||
form_xobject = candidate
|
||||
for settings in contentsinfo.xobject_settings:
|
||||
if settings.name != xobj:
|
||||
continue
|
||||
|
||||
# Find images once for each time this Form XObject is drawn.
|
||||
# This could be optimized to cache the multiple drawing events
|
||||
# but in practice both Form XObjects and multiple drawing of the
|
||||
# same object are both very rare.
|
||||
ctm_shorthand = settings.shorthand
|
||||
yield from _find_images(
|
||||
pdf=pdf, container=form_xobject, shorthand=ctm_shorthand)
|
||||
|
||||
|
||||
def _find_images(*, pdf, container, shorthand=None):
|
||||
"""Find all individual instances of images drawn in the container
|
||||
|
||||
Usually the container is a page, but it may also be a Form XObject.
|
||||
|
||||
On a typical page images are stored inline or as regular images
|
||||
in an XObject.
|
||||
|
||||
Form XObjects may include inline images, XObject images,
|
||||
and recursively, other Form XObjects; and also vector drawing commands.
|
||||
|
||||
Every instance of an image being drawn somewhere is flattened and
|
||||
treated as a unique image, since if the same image is drawn multiple times
|
||||
on one page it may be drawn at differing resolutions, and our objective
|
||||
is to find the resolution at which the page can be rastered without
|
||||
downsampling.
|
||||
|
||||
"""
|
||||
|
||||
if container.get('/Type') == '/Page' and '/Contents' in container:
|
||||
# For a /Page the content stream is attached to the page's /Contents
|
||||
page = container
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
initial_shorthand = shorthand or UNIT_SQUARE
|
||||
elif container.get('/Type') == '/XObject' and \
|
||||
container['/Subtype'] == '/Form':
|
||||
# For a Form XObject that content stream is attached to the XObject
|
||||
contentstream = pypdf.pdf.ContentStream(container, pdf)
|
||||
|
||||
# Set the CTM to the state it was when the "Do" operator was
|
||||
# encountered that is drawing this instance of the Form XObject
|
||||
ctm = _matrix_from_shorthand(shorthand or UNIT_SQUARE)
|
||||
|
||||
# A Form XObject may provide its own matrix to map form space into
|
||||
# user space. Get this if one exists
|
||||
form_matrix = _matrix_from_shorthand(
|
||||
container.get('/Matrix', UNIT_SQUARE))
|
||||
|
||||
# Concatenate form matrix with CTM to ensure CTM is correct for
|
||||
# drawing this instance of the XObject
|
||||
ctm = matrix_mult(form_matrix, ctm)
|
||||
initial_shorthand = _shorthand_from_matrix(ctm)
|
||||
else:
|
||||
return
|
||||
|
||||
contentsinfo = _interpret_contents(contentstream, initial_shorthand)
|
||||
|
||||
yield from _find_inline_images(contentsinfo)
|
||||
yield from _find_regular_images(container, contentsinfo)
|
||||
yield from _find_form_xobject_images(pdf, container, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
if not '/Contents' in page:
|
||||
return False
|
||||
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
if text.strip() != '':
|
||||
return True
|
||||
|
||||
# More nuanced test to deal with quirks of Tesseract PDF generation
|
||||
# Check if there's a Glyphless font
|
||||
try:
|
||||
font = page['/Resources']['/Font']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
font_objects = list(font.keys())
|
||||
for font_object in font_objects:
|
||||
basefont = font[font_object]['/BaseFont']
|
||||
if basefont.endswith('GlyphLessFont'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _pdf_get_pageinfo(pdf, pageno: int):
|
||||
pageinfo = {}
|
||||
pageinfo['pageno'] = pageno
|
||||
pageinfo['images'] = []
|
||||
|
||||
if isinstance(pdf, Path):
|
||||
pdf = pypdf.PdfFileReader(str(pdf))
|
||||
elif isinstance(pdf, str):
|
||||
pdf = pypdf.PdfFileReader(pdf)
|
||||
|
||||
page = pdf.pages[pageno]
|
||||
|
||||
pageinfo['has_text'] = _page_has_text(pdf, page)
|
||||
|
||||
width_pt = page.mediaBox.getWidth()
|
||||
height_pt = page.mediaBox.getHeight()
|
||||
|
||||
userunit = page.get('/UserUnit', Decimal(1.0))
|
||||
pageinfo['userunit'] = userunit
|
||||
pageinfo['width_inches'] = width_pt * userunit / Decimal(72.0)
|
||||
pageinfo['height_inches'] = height_pt * userunit / Decimal(72.0)
|
||||
|
||||
try:
|
||||
pageinfo['rotate'] = int(page['/Rotate'])
|
||||
except KeyError:
|
||||
pageinfo['rotate'] = 0
|
||||
|
||||
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
|
||||
pageinfo['images'] = [im for im in
|
||||
_find_images(pdf=pdf, container=page,
|
||||
shorthand=userunit_shorthand)]
|
||||
if pageinfo['images']:
|
||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||
yres = max(image['dpi_h'] for image in pageinfo['images'])
|
||||
pageinfo['xres'], pageinfo['yres'] = xres, yres
|
||||
pageinfo['width_pixels'] = \
|
||||
int(round(xres * pageinfo['width_inches']))
|
||||
pageinfo['height_pixels'] = \
|
||||
int(round(yres * pageinfo['height_inches']))
|
||||
|
||||
return pageinfo
|
||||
|
||||
|
||||
def _pdf_get_all_pageinfo(infile):
|
||||
with universal_open(infile, 'rb') as f:
|
||||
pdf = pypdf.PdfFileReader(f)
|
||||
return [PageInfo(pdf, n) for n in range(pdf.numPages)]
|
||||
|
||||
|
||||
class PageInfo:
|
||||
def __init__(self, pdf, pageno):
|
||||
self._pageno = pageno
|
||||
self._pageinfo = _pdf_get_pageinfo(pdf, pageno)
|
||||
|
||||
@property
|
||||
def pageno(self):
|
||||
return self._pageno
|
||||
|
||||
@property
|
||||
def has_text(self):
|
||||
return self._pageinfo['has_text']
|
||||
|
||||
@property
|
||||
def width_inches(self):
|
||||
return self._pageinfo['width_inches']
|
||||
|
||||
@property
|
||||
def height_inches(self):
|
||||
return self._pageinfo['height_inches']
|
||||
|
||||
@property
|
||||
def width_pixels(self):
|
||||
return int(round(self.width_inches * self.xres))
|
||||
|
||||
@property
|
||||
def height_pixels(self):
|
||||
return int(round(self.height_inches * self.yres))
|
||||
|
||||
@property
|
||||
def rotation(self):
|
||||
return self._pageinfo.get('rotate', None)
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, value):
|
||||
if value in (0, 90, 180, 270, 360, -90, -180, -270):
|
||||
self._pageinfo['rotate'] = value
|
||||
else:
|
||||
raise ValueError("rotation must be a cardinal angle")
|
||||
|
||||
@property
|
||||
def images(self):
|
||||
return self._pageinfo['images']
|
||||
|
||||
@property
|
||||
def xres(self):
|
||||
return self._pageinfo.get('xres', None)
|
||||
|
||||
@property
|
||||
def yres(self):
|
||||
return self._pageinfo.get('yres', None)
|
||||
|
||||
@property
|
||||
def userunit(self):
|
||||
return self._pageinfo.get('userunit', None)
|
||||
|
||||
@property
|
||||
def min_version(self):
|
||||
if self.userunit is not None:
|
||||
return '1.6'
|
||||
else:
|
||||
return '1.5'
|
||||
|
||||
@property
|
||||
def images(self):
|
||||
return self._pageinfo['images']
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
'<PageInfo '
|
||||
'pageno={} {}"x{}" rotation={} res={}x{} has_text={}>').format(
|
||||
self.pageno, self.width_inches, self.height_inches,
|
||||
self.rotation,
|
||||
self.xres, self.yres, self.has_text
|
||||
)
|
||||
|
||||
|
||||
class PdfInfo:
|
||||
"""Get summary information about a PDF
|
||||
|
||||
"""
|
||||
def __init__(self, infile):
|
||||
self._infile = infile
|
||||
self._pages = _pdf_get_all_pageinfo(infile)
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
return self._pages
|
||||
|
||||
@property
|
||||
def min_version(self):
|
||||
# The minimum PDF is the maximum version that any particular page needs
|
||||
return max(page.min_version for page in self.pages)
|
||||
|
||||
@property
|
||||
def has_userunit(self):
|
||||
return any(page.userunit != 1.0 for page in self.pages)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._pages[item]
|
||||
|
||||
def __len__(self):
|
||||
return len(self._pages)
|
||||
|
||||
def __repr__(self):
|
||||
return "<PdfInfo('...'), page count={}>".format(len(self))
|
||||
|
||||
# def __getstate__(self):
|
||||
# state = {'_infile': self._infile}
|
||||
# return state
|
||||
#
|
||||
# def __setstate__(self, state):
|
||||
# self._infile = state['_infile']
|
||||
# self._pages = _pdf_get_all_pageinfo(self._infile)
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('infile')
|
||||
args = parser.parse_args()
|
||||
info = _pdf_get_all_pageinfo(args.infile)
|
||||
from pprint import pprint
|
||||
pprint(info)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+207
-76
@@ -22,7 +22,7 @@ from PIL import Image
|
||||
from ruffus import formatter, regex, Pipeline, suffix
|
||||
|
||||
from .hocrtransform import HocrTransform
|
||||
from .pageinfo import pdf_get_all_pageinfo
|
||||
from .pdfinfo import PdfInfo, Encoding, Colorspace
|
||||
from .pdfa import generate_pdfa_ps, file_claims_pdfa
|
||||
from .helpers import re_symlink, is_iterable_notstr, page_number
|
||||
from .exec import ghostscript, tesseract, qpdf
|
||||
@@ -50,7 +50,10 @@ class JobContext:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pdfinfo = []
|
||||
self.pdfinfo = None
|
||||
|
||||
def generate_pdfinfo(self, infile):
|
||||
self.pdfinfo = PdfInfo(infile)
|
||||
|
||||
def get_pdfinfo(self):
|
||||
"What we know about the input PDF"
|
||||
@@ -72,8 +75,8 @@ class JobContext:
|
||||
self.work_folder = work_folder
|
||||
|
||||
|
||||
from multiprocessing.managers import BaseManager
|
||||
class JobContextManager(BaseManager):
|
||||
from multiprocessing.managers import SyncManager
|
||||
class JobContextManager(SyncManager):
|
||||
pass
|
||||
|
||||
|
||||
@@ -159,17 +162,20 @@ def triage(
|
||||
log,
|
||||
context):
|
||||
|
||||
options = context.get_options()
|
||||
try:
|
||||
with open(input_file, 'rb') as f:
|
||||
signature = f.read(4)
|
||||
if signature == b'%PDF':
|
||||
signature = f.read(1024)
|
||||
if b'%PDF' in signature:
|
||||
if options.image_dpi:
|
||||
log.warning("Argument --image-dpi ignored because the "
|
||||
"input file is a PDF, not an image.")
|
||||
re_symlink(input_file, output_file, log)
|
||||
return
|
||||
except EnvironmentError as e:
|
||||
log.error(e)
|
||||
raise InputFileError() from e
|
||||
|
||||
options = context.get_options()
|
||||
triage_image_file(input_file, output_file, log, options)
|
||||
|
||||
|
||||
@@ -178,38 +184,59 @@ def repair_pdf(
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
|
||||
options = context.get_options()
|
||||
qpdf.repair(input_file, output_file, log)
|
||||
pdfinfo = pdf_get_all_pageinfo(output_file)
|
||||
pdfinfo = PdfInfo(output_file)
|
||||
|
||||
if pdfinfo.has_userunit and options.output_type == 'pdfa':
|
||||
log.error("This input file uses a PDF feature that is not supported "
|
||||
"by Ghostscript, so you cannot use --output-type=pdfa for this "
|
||||
"file. (Specifically, it uses the PDF-1.6 /UserUnit feature to "
|
||||
"support very large or small page sizes, and Ghostscript cannot "
|
||||
"output these files.) Use --output-type=pdf instead."
|
||||
)
|
||||
raise InputFileError()
|
||||
|
||||
context.set_pdfinfo(pdfinfo)
|
||||
log.debug(pdfinfo)
|
||||
|
||||
|
||||
def get_pageinfo(input_file, context):
|
||||
pageno = int(os.path.basename(input_file)[0:6]) - 1
|
||||
pageno = page_number(input_file) - 1
|
||||
pageinfo = context.get_pdfinfo()[pageno]
|
||||
return pageinfo
|
||||
|
||||
|
||||
def get_page_dpi(pageinfo, options):
|
||||
"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)
|
||||
xres = max(pageinfo.xres or VECTOR_PAGE_DPI, options.oversample or 0)
|
||||
yres = max(pageinfo.yres or VECTOR_PAGE_DPI, options.oversample or 0)
|
||||
return (float(xres), float(yres))
|
||||
|
||||
|
||||
def get_page_square_dpi(pageinfo, options):
|
||||
"Get the DPI when we require xres == yres"
|
||||
"Get the DPI when we require xres == yres, scaled to physical units"
|
||||
xres = pageinfo.xres or 0
|
||||
yres = pageinfo.yres or 0
|
||||
userunit = pageinfo.userunit or 1
|
||||
return float(max(
|
||||
pageinfo.get('xres', VECTOR_PAGE_DPI),
|
||||
pageinfo.get('yres', VECTOR_PAGE_DPI),
|
||||
(xres * userunit) or VECTOR_PAGE_DPI,
|
||||
(yres * userunit) or VECTOR_PAGE_DPI,
|
||||
options.oversample or 0))
|
||||
|
||||
|
||||
def get_canvas_square_dpi(pageinfo, options):
|
||||
"""Get the DPI when we require xres == yres, in Postscript units"""
|
||||
return float(max(
|
||||
(pageinfo.xres) or VECTOR_PAGE_DPI,
|
||||
(pageinfo.yres) or VECTOR_PAGE_DPI,
|
||||
options.oversample or 0))
|
||||
|
||||
|
||||
def is_ocr_required(pageinfo, log, options):
|
||||
page = pageinfo['pageno'] + 1
|
||||
page = pageinfo.pageno + 1
|
||||
ocr_required = True
|
||||
if not pageinfo['images']:
|
||||
if not pageinfo.images:
|
||||
if options.force_ocr and options.oversample:
|
||||
# The user really wants to reprocess this file
|
||||
log.info(
|
||||
@@ -231,7 +258,7 @@ def is_ocr_required(pageinfo, log, options):
|
||||
"skipping all processing on this page".format(page))
|
||||
ocr_required = False
|
||||
|
||||
elif pageinfo['has_text']:
|
||||
elif pageinfo.has_text:
|
||||
msg = "{0:4d}: page already has text! – {1}"
|
||||
|
||||
if not options.force_ocr and not options.skip_text:
|
||||
@@ -247,8 +274,8 @@ def is_ocr_required(pageinfo, log, options):
|
||||
"skipping all processing on this page"))
|
||||
ocr_required = False
|
||||
|
||||
if ocr_required and options.skip_big:
|
||||
pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels']
|
||||
if ocr_required and options.skip_big and pageinfo.images:
|
||||
pixel_count = pageinfo.width_pixels * pageinfo.height_pixels
|
||||
if pixel_count > (options.skip_big * 1000000):
|
||||
ocr_required = False
|
||||
log.warning(
|
||||
@@ -306,13 +333,14 @@ def rasterize_preview(
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
pageinfo = get_pageinfo(input_file, context)
|
||||
options = context.get_options()
|
||||
canvas_dpi = get_canvas_square_dpi(pageinfo, options) / 2
|
||||
page_dpi = get_page_square_dpi(pageinfo, options) / 2
|
||||
|
||||
ghostscript.rasterize_pdf(
|
||||
input_file=input_file,
|
||||
output_file=output_file,
|
||||
xres=200,
|
||||
yres=200,
|
||||
raster_device='jpeggray',
|
||||
log=log)
|
||||
input_file, output_file, xres=canvas_dpi, yres=canvas_dpi,
|
||||
raster_device='jpeggray', log=log, page_dpi=(page_dpi, page_dpi))
|
||||
|
||||
|
||||
def orient_page(
|
||||
@@ -378,9 +406,9 @@ def orient_page(
|
||||
with open(output_file, 'wb') as out:
|
||||
writer.write(out)
|
||||
|
||||
pageno = int(os.path.basename(page_pdf)[0:6]) - 1
|
||||
pageno = page_number(page_pdf) - 1
|
||||
pdfinfo = context.get_pdfinfo()
|
||||
pdfinfo[pageno]['rotated'] = orient_conf.angle
|
||||
pdfinfo[pageno].rotation = orient_conf.angle
|
||||
context.set_pdfinfo(pdfinfo)
|
||||
|
||||
|
||||
@@ -393,25 +421,28 @@ def rasterize_with_ghostscript(
|
||||
pageinfo = get_pageinfo(input_file, context)
|
||||
|
||||
device = 'png16m' # 24-bit
|
||||
if all(image['comp'] == 1 for image in pageinfo['images']):
|
||||
if all(image['bpc'] == 1 for image in pageinfo['images']):
|
||||
device = 'pngmono'
|
||||
elif all(image['bpc'] > 1 and image['color'] == 'index'
|
||||
for image in pageinfo['images']):
|
||||
device = 'png256'
|
||||
elif all(image['bpc'] > 1 and image['color'] == 'gray'
|
||||
for image in pageinfo['images']):
|
||||
device = 'pnggray'
|
||||
if pageinfo.images:
|
||||
if all(image.comp == 1 for image in pageinfo.images):
|
||||
if all(image.bpc == 1 for image in pageinfo.images):
|
||||
device = 'pngmono'
|
||||
elif all(image.bpc > 1 and image.color == Colorspace.index
|
||||
for image in pageinfo.images):
|
||||
device = 'png256'
|
||||
elif all(image.bpc > 1 and image.color == Colorspace.gray
|
||||
for image in pageinfo.images):
|
||||
device = 'pnggray'
|
||||
|
||||
log.debug("Rasterize {0} with {1}".format(
|
||||
os.path.basename(input_file), device))
|
||||
|
||||
# Produce the page image with square resolution or else deskew and OCR
|
||||
# will not work properly
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
# will not work properly.
|
||||
canvas_dpi = get_canvas_square_dpi(pageinfo, options)
|
||||
page_dpi = get_page_square_dpi(pageinfo, options)
|
||||
|
||||
ghostscript.rasterize_pdf(
|
||||
input_file, output_file, xres=dpi, yres=dpi, raster_device=device,
|
||||
log=log)
|
||||
input_file, output_file, xres=canvas_dpi, yres=canvas_dpi,
|
||||
raster_device=device, log=log, page_dpi=(page_dpi, page_dpi))
|
||||
|
||||
|
||||
def preprocess_remove_background(
|
||||
@@ -426,11 +457,11 @@ def preprocess_remove_background(
|
||||
|
||||
pageinfo = get_pageinfo(input_file, context)
|
||||
|
||||
if any(image['bpc'] > 1 for image in pageinfo['images']):
|
||||
if any(image.bpc > 1 for image in pageinfo.images):
|
||||
leptonica.remove_background(input_file, output_file)
|
||||
else:
|
||||
log.info("{0:4d}: background removal skipped on mono page".format(
|
||||
pageinfo['pageno']))
|
||||
pageinfo.pageno))
|
||||
re_symlink(input_file, output_file, log)
|
||||
|
||||
|
||||
@@ -471,7 +502,7 @@ def select_ocr_image(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
contenxt):
|
||||
context):
|
||||
"""Select the image we send for OCR. May not be the same as the display
|
||||
image depending on preprocessing."""
|
||||
|
||||
@@ -482,18 +513,20 @@ def select_ocr_image(
|
||||
|
||||
def ocr_tesseract_hocr(
|
||||
input_file,
|
||||
output_file,
|
||||
output_files,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
tesseract.generate_hocr(
|
||||
input_file=input_file,
|
||||
output_hocr=output_file,
|
||||
output_files=output_files,
|
||||
language=options.language,
|
||||
engine_mode=options.tesseract_oem,
|
||||
tessconfig=options.tesseract_config,
|
||||
timeout=options.tesseract_timeout,
|
||||
pagesegmode=options.tesseract_pagesegmode,
|
||||
user_words=options.user_words,
|
||||
user_patterns=options.user_patterns,
|
||||
log=log
|
||||
)
|
||||
|
||||
@@ -517,7 +550,10 @@ def select_visible_page_image(
|
||||
image = next(ii for ii in infiles if ii.endswith(image_suffix))
|
||||
|
||||
pageinfo = get_pageinfo(image, context)
|
||||
if all(orig_image['enc'] == 'jpeg' for orig_image in pageinfo['images']):
|
||||
if pageinfo.images and \
|
||||
all(im['enc'] == 'jpeg' for im in pageinfo.images):
|
||||
log.debug('{:4d}: JPEG input -> JPEG output'.format(
|
||||
page_number(image)))
|
||||
# If all images were JPEGs originally, produce a JPEG as output
|
||||
im = Image.open(image)
|
||||
|
||||
@@ -561,7 +597,7 @@ def select_image_layer(
|
||||
# as it accurately describes the image. It would be possible to
|
||||
# resample the image at this stage back to non-square DPI to more
|
||||
# closely resemble the input, except that the hocr renderer does not
|
||||
# understand non-square DPI. The tess4 renderer would be fine.
|
||||
# understand non-square DPI. The sandwich renderer would be fine.
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi))
|
||||
|
||||
@@ -575,12 +611,12 @@ def select_image_layer(
|
||||
|
||||
|
||||
def render_hocr_page(
|
||||
input_file,
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
hocr = input_file
|
||||
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
|
||||
pageinfo = get_pageinfo(hocr, context)
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
|
||||
@@ -589,14 +625,22 @@ def render_hocr_page(
|
||||
showBoundingboxes=False, invisibleText=True)
|
||||
|
||||
|
||||
def flatten_groups(groups):
|
||||
for obj in groups:
|
||||
if is_iterable_notstr(obj):
|
||||
yield from obj
|
||||
else:
|
||||
yield obj
|
||||
|
||||
|
||||
def render_hocr_debug_page(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image'))
|
||||
hocr = next(ii for ii in flatten_groups(infiles) if ii.endswith('.hocr'))
|
||||
image = next(ii for ii in flatten_groups(infiles) if ii.endswith('.image'))
|
||||
|
||||
pageinfo = get_pageinfo(image, context)
|
||||
dpi = get_page_square_dpi(pageinfo, options)
|
||||
@@ -611,8 +655,10 @@ def combine_layers(
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
text = next(ii for ii in infiles if ii.endswith('.text.pdf'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image-layer.pdf'))
|
||||
text = next(ii for ii in flatten_groups(infiles)
|
||||
if ii.endswith('.text.pdf'))
|
||||
image = next(ii for ii in flatten_groups(infiles)
|
||||
if ii.endswith('.image-layer.pdf'))
|
||||
|
||||
pdf_text = pypdf.PdfFileReader(open(text, "rb"))
|
||||
pdf_image = pypdf.PdfFileReader(open(image, "rb"))
|
||||
@@ -672,57 +718,77 @@ def combine_layers(
|
||||
pdf_output = pypdf.PdfFileWriter()
|
||||
pdf_output.addPage(page_text)
|
||||
|
||||
# If the input was scaled, re-apply the scaling
|
||||
pageinfo = get_pageinfo(text, context)
|
||||
if pageinfo.userunit != 1:
|
||||
page_text[pypdf.generic.NameObject('/UserUnit')] = pageinfo.userunit
|
||||
pdf_output._header = b'%PDF-1.6' # Hack header to correct version
|
||||
|
||||
with open(output_file, "wb") as out:
|
||||
pdf_output.write(out)
|
||||
|
||||
|
||||
def ocr_tesseract_and_render_pdf(
|
||||
infiles,
|
||||
output_file,
|
||||
outfiles,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
input_image = next((ii for ii in infiles if ii.endswith('.image')), '')
|
||||
input_pdf = next((ii for ii in infiles if ii.endswith('.pdf')))
|
||||
output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf')))
|
||||
output_text = next((ii for ii in outfiles if ii.endswith('.txt')))
|
||||
|
||||
if not input_image:
|
||||
# Skipping this page
|
||||
re_symlink(input_pdf, output_file, log)
|
||||
re_symlink(input_pdf, output_pdf, log)
|
||||
with open(output_text, 'w') as f:
|
||||
f.write('[skipped page]')
|
||||
return
|
||||
|
||||
tesseract.generate_pdf(
|
||||
input_image=input_image,
|
||||
skip_pdf=input_pdf,
|
||||
output_pdf=output_file,
|
||||
output_pdf=output_pdf,
|
||||
output_text=output_text,
|
||||
language=options.language,
|
||||
engine_mode=options.tesseract_oem,
|
||||
text_only=False,
|
||||
tessconfig=options.tesseract_config,
|
||||
timeout=options.tesseract_timeout,
|
||||
pagesegmode=options.tesseract_pagesegmode,
|
||||
user_words=options.user_words,
|
||||
user_patterns=options.user_patterns,
|
||||
log=log)
|
||||
|
||||
|
||||
def ocr_tesseract_textonly_pdf(
|
||||
infiles,
|
||||
output_file,
|
||||
outfiles,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
input_image = next((ii for ii in infiles if ii.endswith('.ocr.png')), '')
|
||||
if not input_image:
|
||||
raise ValueError("No image rendered?")
|
||||
|
||||
skip_pdf = next((ii for ii in infiles if ii.endswith('.pdf')))
|
||||
|
||||
output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf')))
|
||||
output_text = next((ii for ii in outfiles if ii.endswith('.txt')))
|
||||
|
||||
tesseract.generate_pdf(
|
||||
input_image=input_image,
|
||||
skip_pdf=skip_pdf,
|
||||
output_pdf=output_file,
|
||||
output_pdf=output_pdf,
|
||||
output_text=output_text,
|
||||
language=options.language,
|
||||
engine_mode=options.tesseract_oem,
|
||||
text_only=True,
|
||||
tessconfig=options.tesseract_config,
|
||||
timeout=options.tesseract_timeout,
|
||||
pagesegmode=options.tesseract_pagesegmode,
|
||||
user_words=options.user_words,
|
||||
user_patterns=options.user_patterns,
|
||||
log=log)
|
||||
|
||||
|
||||
@@ -752,9 +818,16 @@ def get_pdfmark(base_pdf, options):
|
||||
if options.subject:
|
||||
pdfmark['/Subject'] = options.subject
|
||||
|
||||
pdfmark['/Creator'] = '{0} {1} / Tesseract OCR{2} {3}'.format(
|
||||
if options.pdf_renderer == 'tesseract':
|
||||
renderer_tag = 'OCR+PDF'
|
||||
elif options.pdf_renderer == 'sandwich':
|
||||
renderer_tag = 'OCR-PDF'
|
||||
else:
|
||||
renderer_tag = 'OCR'
|
||||
|
||||
pdfmark['/Creator'] = '{0} {1} / Tesseract {2} {3}'.format(
|
||||
PROGRAM_NAME, VERSION,
|
||||
'+PDF' if options.pdf_renderer == 'tesseract' else '',
|
||||
renderer_tag,
|
||||
tesseract.version())
|
||||
return pdfmark
|
||||
|
||||
@@ -783,7 +856,7 @@ def skip_page(
|
||||
|
||||
|
||||
def merge_pages_ghostscript(
|
||||
input_files,
|
||||
input_files_groups,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
@@ -796,22 +869,34 @@ def merge_pages_ghostscript(
|
||||
right that way.'''
|
||||
if s.endswith('.ps'):
|
||||
return 99999999
|
||||
key = int(os.path.basename(s)[0:6]) * 10
|
||||
key = page_number(s) * 10
|
||||
if 'debug' in os.path.basename(s):
|
||||
key += 1
|
||||
return key
|
||||
|
||||
input_files = (f for f in flatten_groups(input_files_groups)
|
||||
if not f.endswith('.txt'))
|
||||
pdf_pages = sorted(input_files, key=input_file_order)
|
||||
log.debug("Final pages: " + "\n".join(pdf_pages))
|
||||
ghostscript.generate_pdfa(pdf_pages, output_file, log, options.jobs or 1)
|
||||
input_pdfinfo = context.get_pdfinfo()
|
||||
ghostscript.generate_pdfa(
|
||||
pdf_version=input_pdfinfo.min_version,
|
||||
pdf_pages=pdf_pages,
|
||||
output_file=output_file,
|
||||
compression=options.pdfa_image_compression,
|
||||
log=log,
|
||||
threads=options.jobs or 1)
|
||||
|
||||
|
||||
def merge_pages_qpdf(
|
||||
input_files,
|
||||
input_files_groups,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
|
||||
input_files = list(f for f in flatten_groups(input_files_groups)
|
||||
if not f.endswith('.txt'))
|
||||
metadata_file = next(
|
||||
(ii for ii in input_files if ii.endswith('.repaired.pdf')))
|
||||
input_files.remove(metadata_file)
|
||||
@@ -819,7 +904,7 @@ def merge_pages_qpdf(
|
||||
def input_file_order(s):
|
||||
'''Sort order: All rendered pages followed
|
||||
by their debug page.'''
|
||||
key = int(os.path.basename(s)[0:6]) * 10
|
||||
key = page_number(s) * 10
|
||||
if 'debug' in os.path.basename(s):
|
||||
key += 1
|
||||
return key
|
||||
@@ -842,7 +927,42 @@ def merge_pages_qpdf(
|
||||
|
||||
pdf_pages[0] = writer_file
|
||||
|
||||
qpdf.merge(pdf_pages, output_file)
|
||||
qpdf.merge(input_files=pdf_pages, output_file=output_file,
|
||||
min_version=context.get_pdfinfo().min_version)
|
||||
|
||||
|
||||
def merge_sidecars(
|
||||
input_files_groups,
|
||||
output_file,
|
||||
log,
|
||||
context):
|
||||
options = context.get_options()
|
||||
pdfinfo = context.get_pdfinfo()
|
||||
|
||||
txt_files = [None] * len(pdfinfo)
|
||||
|
||||
for infile in flatten_groups(input_files_groups):
|
||||
if infile.endswith('.txt'):
|
||||
idx = page_number(infile) - 1
|
||||
txt_files[idx] = infile
|
||||
|
||||
def write_pages(stream):
|
||||
for page_number, txt_file in enumerate(txt_files):
|
||||
if page_number != 0:
|
||||
stream.write('\f') # Form feed between pages
|
||||
if txt_file:
|
||||
with open(txt_file, 'r') as in_:
|
||||
stream.write(in_.read())
|
||||
else:
|
||||
stream.write('[OCR skipped on page {}]'.format(
|
||||
page_number + 1))
|
||||
|
||||
if output_file == '-':
|
||||
write_pages(sys.stdout)
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
with open(output_file, 'w', encoding='utf-8') as out:
|
||||
write_pages(out)
|
||||
|
||||
|
||||
def copy_final(
|
||||
@@ -949,7 +1069,7 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_func=ocr_tesseract_hocr,
|
||||
input=task_select_ocr_image,
|
||||
filter=suffix(".ocr.png"),
|
||||
output=".hocr",
|
||||
output=[".hocr", ".txt"],
|
||||
extras=[log, context])
|
||||
task_ocr_tesseract_hocr.graphviz(fillcolor='"#00cc66"')
|
||||
task_ocr_tesseract_hocr.active_if(options.pdf_renderer == 'hocr')
|
||||
@@ -976,13 +1096,13 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_select_image_layer.graphviz(
|
||||
fillcolor='"#00cc66"', shape='diamond')
|
||||
task_select_image_layer.active_if(
|
||||
options.pdf_renderer == 'hocr' or options.pdf_renderer == 'tess4')
|
||||
options.pdf_renderer == 'hocr' or options.pdf_renderer == 'sandwich')
|
||||
|
||||
task_render_hocr_page = main_pipeline.transform(
|
||||
task_func=render_hocr_page,
|
||||
input=task_ocr_tesseract_hocr,
|
||||
filter=suffix('.hocr'),
|
||||
output='.text.pdf',
|
||||
filter=regex(r".*/(\d{6})(?:\.hocr)"),
|
||||
output=os.path.join(work_folder, r'\1.text.pdf'),
|
||||
extras=[log, context])
|
||||
task_render_hocr_page.graphviz(fillcolor='"#00cc66"')
|
||||
task_render_hocr_page.active_if(options.pdf_renderer == 'hocr')
|
||||
@@ -1002,10 +1122,11 @@ def build_pipeline(options, work_folder, log, context):
|
||||
task_func=ocr_tesseract_textonly_pdf,
|
||||
input=[task_select_ocr_image, task_orient_page],
|
||||
filter=regex(r".*/(\d{6})(?:\.ocr.png|\.ocr\.oriented\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.text.pdf'),
|
||||
output=[os.path.join(work_folder, r'\1.text.pdf'),
|
||||
os.path.join(work_folder, r'\1.text.txt')],
|
||||
extras=[log, context])
|
||||
task_ocr_tesseract_textonly_pdf.graphviz(fillcolor='"#ff69b4"')
|
||||
task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'tess4')
|
||||
task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'sandwich')
|
||||
if tesseract.v4():
|
||||
task_ocr_tesseract_textonly_pdf.jobs_limit(2)
|
||||
|
||||
@@ -1018,14 +1139,15 @@ def build_pipeline(options, work_folder, log, context):
|
||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
extras=[log, context])
|
||||
task_combine_layers.graphviz(fillcolor='"#00cc66"')
|
||||
task_combine_layers.active_if(options.pdf_renderer == 'hocr' or options.pdf_renderer == 'tess4')
|
||||
task_combine_layers.active_if(options.pdf_renderer == 'hocr' or options.pdf_renderer == 'sandwich')
|
||||
|
||||
# Tesseract OCR+PDF
|
||||
task_ocr_tesseract_and_render_pdf = main_pipeline.collate(
|
||||
task_func=ocr_tesseract_and_render_pdf,
|
||||
input=[task_select_visible_page_image, task_orient_page],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.oriented\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
output=[os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
os.path.join(work_folder, r'\1.rendered.txt')],
|
||||
extras=[log, context])
|
||||
task_ocr_tesseract_and_render_pdf.graphviz(fillcolor='"#66ccff"')
|
||||
task_ocr_tesseract_and_render_pdf.active_if(options.pdf_renderer == 'tesseract')
|
||||
@@ -1074,6 +1196,15 @@ def build_pipeline(options, work_folder, log, context):
|
||||
extras=[log, context])
|
||||
task_merge_pages_qpdf.active_if(options.output_type == 'pdf')
|
||||
|
||||
task_merge_sidecars = main_pipeline.merge(
|
||||
task_func=merge_sidecars,
|
||||
input=[task_ocr_tesseract_hocr,
|
||||
task_ocr_tesseract_and_render_pdf,
|
||||
task_ocr_tesseract_textonly_pdf],
|
||||
output=options.sidecar,
|
||||
extras=[log, context])
|
||||
task_merge_sidecars.active_if(options.sidecar)
|
||||
|
||||
# Finalize
|
||||
task_copy_final = main_pipeline.merge(
|
||||
task_func=copy_final,
|
||||
|
||||
+4
-4
@@ -2,8 +2,8 @@
|
||||
# setup.py lists a separate set of requirements that are looser to simplify
|
||||
# installation
|
||||
ruffus == 2.6.3
|
||||
Pillow == 4.0.0
|
||||
reportlab == 3.3.0
|
||||
PyPDF2 == 1.26
|
||||
Pillow == 4.1.1
|
||||
reportlab == 3.4.0
|
||||
PyPDF2 == 1.26.0
|
||||
img2pdf == 0.2.3
|
||||
cffi == 1.9.1
|
||||
cffi == 1.10.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[bdist_wheel]
|
||||
python-tag = py34
|
||||
python-tag = py35
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
@@ -11,3 +11,4 @@ ignore =
|
||||
[tool:pytest]
|
||||
norecursedirs = lib .pc .git output cache resources
|
||||
testpaths = tests
|
||||
addopts = -n auto
|
||||
@@ -5,8 +5,8 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
if sys.version_info < (3, 4):
|
||||
print("Python 3.4 or newer is required", file=sys.stderr)
|
||||
if sys.version_info < (3, 5):
|
||||
print("Python 3.5 or newer is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup, find_packages # nopep8
|
||||
@@ -155,7 +155,7 @@ if not forced and command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
need_version='3.03', # limited by Travis CI / Ubuntu 14.04 backports
|
||||
need_version='3.04', # using backport for Travis CI
|
||||
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
|
||||
)
|
||||
check_external_program(
|
||||
@@ -185,9 +185,15 @@ if 'upload' in sys.argv[1:]:
|
||||
|
||||
tests_require = open('test_requirements.txt').read().splitlines()
|
||||
|
||||
|
||||
def readme():
|
||||
with open('README.rst') as f:
|
||||
return f.read()
|
||||
|
||||
setup(
|
||||
name='ocrmypdf',
|
||||
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
|
||||
long_description=readme(),
|
||||
url='https://github.com/jbarlow83/OCRmyPDF',
|
||||
author='James R. Barlow',
|
||||
author_email='jim@purplerock.ca',
|
||||
@@ -195,8 +201,6 @@ setup(
|
||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -224,7 +228,7 @@ setup(
|
||||
install_requires=[
|
||||
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
|
||||
'Pillow>=4.0.0', # Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
||||
'reportlab>=3.2.0', # oldest released version with sane image handling
|
||||
'reportlab>=3.3.0', # oldest released version with sane image handling
|
||||
'PyPDF2>=1.26', # pure Python, so track HEAD closely
|
||||
'img2pdf>=0.2.3', # pure Python, so track HEAD closely
|
||||
'cffi>=1.9.1' # must be a setup and install requirement
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
pytest >= 3.0
|
||||
pytest-helpers-namespace
|
||||
pytest-helpers-namespace
|
||||
pytest-xdist
|
||||
pytest-cov
|
||||
+17
-4
@@ -66,6 +66,18 @@ def spoof(**kwargs):
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop():
|
||||
return spoof(tesseract='tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache():
|
||||
if running_in_docker():
|
||||
return os.environ.copy()
|
||||
return spoof(tesseract="tesseract_cache.py")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resources():
|
||||
return Path(TESTS_ROOT) / 'resources'
|
||||
@@ -117,7 +129,8 @@ def run_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
if env is None:
|
||||
env = os.environ
|
||||
|
||||
p_args = OCRMYPDF + list(args) + [str(input_file), str(output_file)]
|
||||
p_args = OCRMYPDF + [str(arg) for arg in args] + \
|
||||
[str(input_file), str(output_file)]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True, env=env)
|
||||
@@ -129,7 +142,7 @@ def run_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
|
||||
@pytest.helpers.register
|
||||
def first_page_dimensions(pdf):
|
||||
from ocrmypdf import pageinfo
|
||||
info = pageinfo.pdf_get_all_pageinfo(str(pdf))
|
||||
from ocrmypdf import pdfinfo
|
||||
info = pdfinfo.PdfInfo(pdf)
|
||||
page0 = info[0]
|
||||
return (page0['width_inches'], page0['height_inches'])
|
||||
return (page0.width_inches, page0.height_inches)
|
||||
|
||||
@@ -34,6 +34,9 @@ In some cases they were converted from one image format to another without other
|
||||
* - typewriter.png, 2400dpi.pdf
|
||||
- `Wikimedia: Triumph typewrtier text Linzensoep`_
|
||||
* Creative Commons BY-SA 2.5
|
||||
* - baiona.png
|
||||
- `Wikimedia: Baionako udalerri mugakideak`_
|
||||
- Creative Commons BY-SA 4.0
|
||||
|
||||
|
||||
Files generated for this project
|
||||
@@ -85,6 +88,9 @@ under the terms of the license in LICENSE.rst.
|
||||
* - overlay.pdf
|
||||
- @maxandersen
|
||||
- PDF file generated by PDFPen pro that triggered content stream parse errors
|
||||
* - no_conentes.pdf
|
||||
- @jbarlow83
|
||||
- synthetic PDF with a blank page that has no /Contents entry
|
||||
|
||||
Assemblies
|
||||
==========
|
||||
@@ -115,4 +121,6 @@ These test resources are assemblies from other previously mentioned files, relea
|
||||
|
||||
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
|
||||
|
||||
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
|
||||
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
|
||||
|
||||
.. _`Wikimedia: Baionako udalerri mugakideak`: https://commons.wikimedia.org/wiki/File:Baionako_udalerri_mugakideak.png
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,21 @@
|
||||
%PDF-1.3
|
||||
%¿÷¢þ
|
||||
1 0 obj
|
||||
<< /Pages 2 0 R /Type /Catalog >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /MediaBox [ 0 0 720 720 ] /Parent 2 0 R /Resources << >> /Type /Page >>
|
||||
endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000064 00000 n
|
||||
0000000123 00000 n
|
||||
trailer << /Root 1 0 R /Size 4 /ID [<52bba3c78160d0c6e851b59110e5d076><52bba3c78160d0c6e851b59110e5d076>] >>
|
||||
startxref
|
||||
213
|
||||
%%EOF
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
import os
|
||||
from subprocess import check_call
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
"""Replicate Ghostscript raster failure while allowing rendering"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__filename__))
|
||||
sys.exit(0)
|
||||
|
||||
# For any rendering calls (device == pdfwrite) call real ghostscript
|
||||
if '-sDEVICE=pdfwrite' in sys.argv:
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Ghost story archive not found")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
"""Replicate Ghostscript render failure while allowing rasterizing"""
|
||||
|
||||
|
||||
def real_ghostscript(argv):
|
||||
gs_args = ['gs'] + argv[1:]
|
||||
os.execvp("gs", gs_args)
|
||||
return # Not reachable
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print('9.20')
|
||||
print('SPOOFED: ' + os.path.basename(__filename__))
|
||||
sys.exit(0)
|
||||
|
||||
# For any rasterize calls (device != pdfwrite) call real ghostscript
|
||||
if '-sDEVICE=pdfwrite' not in sys.argv:
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Casper is not a friendly ghost")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
@@ -20,11 +22,14 @@ def main():
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
elif sys.argv[1] == '--print-parameters':
|
||||
print('A parameter list would go here', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-2] == 'hocr':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
elif sys.argv[-2] == 'pdf':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
@@ -9,18 +11,27 @@ import subprocess
|
||||
"""Cache output of tesseract to speed up test suite
|
||||
|
||||
The cache is keyed by a hash that includes the tesseract version, some of
|
||||
the command line, and the binary dump of the input file. The output file,
|
||||
stdout, and stderr are replicated on a cache hit.
|
||||
the command line, and the binary dump of the input file, and this file itself.
|
||||
Therefore any updates to this file invalidate cache. Uses SHA-1 because it is
|
||||
fast and defeating a hash collision here is not exactly a priority. :P
|
||||
|
||||
The output files, stdout, and stderr are replicated on a cache hit. The output
|
||||
files are either a .pdf and .txt or .hocr and .txt.
|
||||
|
||||
Page orientation checks are also cached (-psm 0 stdout)
|
||||
|
||||
Errors and crashes are not cached.
|
||||
Errors and crashes are not cached. If the arguments don't match a known
|
||||
caching template then real tesseract is called with the same arguments.
|
||||
|
||||
Things not checked:
|
||||
-changes to tesseract installation that don't affect --version
|
||||
|
||||
Assumes Tesseract 3.04 or higher.
|
||||
|
||||
Will fail on Tesseract 3.02.02 in "hocr" mode because it doesn't produce
|
||||
the incorrect file extension.
|
||||
the incorrect file extension. Will fail on 3.03 because that has no sidecar
|
||||
text support. Will fail to replicate a 3.04 bug if wrong parameter order is
|
||||
given.
|
||||
|
||||
"""
|
||||
|
||||
@@ -35,7 +46,13 @@ def real_tesseract():
|
||||
return # Not reachable
|
||||
|
||||
def main():
|
||||
operation = sys.argv[-1]
|
||||
operation = sys.argv[-2]
|
||||
sidecar = False
|
||||
if sys.argv[-1] == 'txt':
|
||||
sidecar = True
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
operation = 'stdout'
|
||||
|
||||
# For anything unexpected operation, defer to real tesseract binary
|
||||
# Currently this includes all use of "--tesseract-config"
|
||||
if operation != 'hocr' and operation != 'pdf' and operation != 'stdout':
|
||||
@@ -53,11 +70,6 @@ def main():
|
||||
['tesseract', '--version'],
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
if b'4.00.00alpha' in tess_version:
|
||||
# Tesseract 4.x alpha is a moving target, don't cache it
|
||||
real_tesseract()
|
||||
return
|
||||
|
||||
m.update(tess_version)
|
||||
|
||||
# Insert this source file into the hash function, to ensure that any
|
||||
@@ -73,6 +85,12 @@ def main():
|
||||
except ValueError:
|
||||
m.update(b'default-lang')
|
||||
|
||||
try:
|
||||
textonly = sys.argv[sys.argv.index('-c') + 1]
|
||||
m.update(textonly.encode())
|
||||
except ValueError:
|
||||
m.update(b'textonly_pdf=0')
|
||||
|
||||
psm_arg = ''
|
||||
if '--psm' in sys.argv:
|
||||
psm_arg = '--psm'
|
||||
@@ -92,16 +110,22 @@ def main():
|
||||
return
|
||||
|
||||
if operation == 'stdout':
|
||||
# tesseract [--options] ... input stdout
|
||||
input_file = sys.argv[-2]
|
||||
output_file = 'stdout'
|
||||
sidecar_file = ''
|
||||
else:
|
||||
input_file = sys.argv[-3]
|
||||
output_file = sys.argv[-2]
|
||||
# tesseract [--options] ... input output txt hocr|pdf
|
||||
input_file = sys.argv[-4]
|
||||
output_file = sys.argv[-3]
|
||||
sidecar_file = sys.argv[-3]
|
||||
|
||||
if operation == 'hocr':
|
||||
output_file += '.hocr'
|
||||
sidecar_file += '.txt'
|
||||
elif operation == 'pdf':
|
||||
output_file += '.pdf'
|
||||
sidecar_file += '.txt'
|
||||
|
||||
with open(input_file, 'rb') as f:
|
||||
m.update(f.read())
|
||||
@@ -112,6 +136,8 @@ def main():
|
||||
print("Tesseract cache hit", file=sys.stderr)
|
||||
if operation != 'stdout':
|
||||
shutil.copy(cache_name, output_file)
|
||||
if sidecar:
|
||||
shutil.copy(cache_name + '.sidecar', sidecar_file)
|
||||
|
||||
# Replicate output
|
||||
with open(cache_name + '.stdout', 'rb') as f:
|
||||
@@ -149,6 +175,8 @@ def main():
|
||||
shutil.copy(output_file, cache_name)
|
||||
else:
|
||||
print("Could not find output file", file=sys.stderr)
|
||||
if sidecar and os.path.exists(sidecar_file):
|
||||
shutil.copy(sidecar_file, cache_name + '.sidecar')
|
||||
else:
|
||||
open(cache_name, 'w').close()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
@@ -28,10 +29,13 @@ def main():
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
elif sys.argv[1] == '--print-parameters':
|
||||
print('List of parameters would go here', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-2] == 'hocr':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
elif sys.argv[-2] == 'pdf':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
import sys
|
||||
import img2pdf
|
||||
import PyPDF2 as pypdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -16,7 +18,7 @@ In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
VERSION_STRING = '''tesseract 3.05.01
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED
|
||||
@@ -52,19 +54,41 @@ def main():
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
inputf = sys.argv[-3]
|
||||
output = sys.argv[-2]
|
||||
elif sys.argv[1] == '--print-parameters':
|
||||
print("Some parameters", file=sys.stderr)
|
||||
print("textonly_pdf\t1\tSome help text")
|
||||
sys.exit(0)
|
||||
elif sys.argv[-2] == 'hocr':
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
with Image.open(inputf) as im, \
|
||||
open(output + '.hocr', 'w', encoding='utf-8') as f:
|
||||
w, h = im.size
|
||||
f.write(HOCR_TEMPLATE.format(str(w), str(h)))
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
inputf = sys.argv[-3]
|
||||
output = sys.argv[-2]
|
||||
pdf_bytes = img2pdf.convert([inputf], dpi=300)
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
f.write(pdf_bytes)
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
elif sys.argv[-2] == 'pdf':
|
||||
if 'textonly_pdf=1' in sys.argv:
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
with Image.open(inputf) as im:
|
||||
dpi = im.info['dpi']
|
||||
imsize = im.size[0] * dpi[0] / 72, im.size[1] * dpi[1] / 72
|
||||
|
||||
pdf_out = pypdf.PdfFileWriter()
|
||||
pdf_out.addBlankPage(imsize[0], imsize[1])
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
pdf_out.write(f)
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
else:
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
pdf_bytes = img2pdf.convert([inputf], dpi=300)
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
f.write(pdf_bytes)
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
inputf = sys.argv[-2]
|
||||
print("""Orientation: 0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
import sys
|
||||
|
||||
def main():
|
||||
|
||||
+252
-111
@@ -4,31 +4,25 @@
|
||||
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import suppress
|
||||
import sys
|
||||
import pytest
|
||||
from ocrmypdf.pageinfo import pdf_get_all_pageinfo
|
||||
from ocrmypdf.pdfinfo import PdfInfo, Colorspace, Encoding
|
||||
import PyPDF2 as pypdf
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
from ocrmypdf.exec import ghostscript
|
||||
from ocrmypdf.exec import ghostscript, tesseract
|
||||
import logging
|
||||
from math import isclose
|
||||
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop():
|
||||
return spoof(tesseract='tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache():
|
||||
if pytest.helpers.running_in_docker():
|
||||
return os.environ.copy()
|
||||
return spoof(tesseract="tesseract_cache.py")
|
||||
RENDERERS = ['hocr', 'tesseract']
|
||||
if tesseract.has_textonly_pdf():
|
||||
RENDERERS.append('sandwich')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -51,6 +45,16 @@ def spoof_no_tess_pdfa_warning():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_render_fail():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_render_failure.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_raster_fail():
|
||||
return spoof(tesseract='tesseract_noop.py', gs='gs_raster_failure.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_qpdf_always_error():
|
||||
return spoof(qpdf='qpdf_dummy_return2.py')
|
||||
@@ -68,15 +72,13 @@ def test_deskew(spoof_tesseract_noop, resources, outdir):
|
||||
|
||||
# Now render as an image again and use Leptonica to find the skew angle
|
||||
# to confirm that it was deskewed
|
||||
from ocrmypdf.exec.ghostscript import rasterize_pdf
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
deskewed_png = outdir / 'deskewed.png'
|
||||
|
||||
rasterize_pdf(
|
||||
str(deskewed_pdf),
|
||||
str(deskewed_png),
|
||||
ghostscript.rasterize_pdf(
|
||||
deskewed_pdf,
|
||||
deskewed_png,
|
||||
xres=150,
|
||||
yres=150,
|
||||
raster_device='pngmono',
|
||||
@@ -109,15 +111,13 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
|
||||
'--image-dpi', '150',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
from ocrmypdf.exec.ghostscript import rasterize_pdf
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
output_png = outdir / 'remove_bg.png'
|
||||
|
||||
rasterize_pdf(
|
||||
str(output_pdf),
|
||||
str(output_png),
|
||||
ghostscript.rasterize_pdf(
|
||||
output_pdf,
|
||||
output_png,
|
||||
xres=100,
|
||||
yres=100,
|
||||
raster_device='png16m',
|
||||
@@ -133,18 +133,22 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
|
||||
@pytest.mark.parametrize(
|
||||
"pdf",
|
||||
['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'])
|
||||
@pytest.mark.parametrize("renderer", ['hocr', 'tesseract'])
|
||||
@pytest.mark.parametrize("renderer", ['auto', 'tesseract'])
|
||||
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
|
||||
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type,
|
||||
resources, outdir):
|
||||
outfile = outdir / 'test_{0}_{1}.pdf'.format(pdf, renderer)
|
||||
check_ocrmypdf(
|
||||
resources / pdf,
|
||||
outdir / 'test_{0}_{1}.pdf'.format(pdf, renderer),
|
||||
outfile,
|
||||
'-dc',
|
||||
'-v', '1',
|
||||
'--output-type', output_type,
|
||||
'--sidecar',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
assert outfile.with_suffix('.pdf.txt').exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("output_type", [
|
||||
'pdfa', 'pdf'
|
||||
@@ -211,20 +215,17 @@ def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf):
|
||||
assert p.returncode == ExitCode.bad_args, err
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
|
||||
oversampled_pdf = check_ocrmypdf(
|
||||
resources / 'skew.pdf', outpdf, '--oversample', '350',
|
||||
'-f',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
pdfinfo = pdf_get_all_pageinfo(str(oversampled_pdf))
|
||||
pdfinfo = PdfInfo(oversampled_pdf)
|
||||
|
||||
print(pdfinfo[0]['xres'])
|
||||
assert abs(pdfinfo[0]['xres'] - 350) < 1
|
||||
print(pdfinfo[0].xres)
|
||||
assert abs(pdfinfo[0].xres - 350) < 1
|
||||
|
||||
|
||||
def test_repeat_ocr(resources, no_outpdf):
|
||||
@@ -235,8 +236,8 @@ def test_repeat_ocr(resources, no_outpdf):
|
||||
def test_force_ocr(spoof_tesseract_cache, resources, outpdf):
|
||||
out = check_ocrmypdf(resources / 'graph_ocred.pdf', outpdf, '-f',
|
||||
env=spoof_tesseract_cache)
|
||||
pdfinfo = pdf_get_all_pageinfo(out)
|
||||
assert pdfinfo[0]['has_text']
|
||||
pdfinfo = PdfInfo(out)
|
||||
assert pdfinfo[0].has_text
|
||||
|
||||
|
||||
def test_skip_ocr(spoof_tesseract_cache, resources, outpdf):
|
||||
@@ -257,10 +258,6 @@ def test_argsfile(spoof_tesseract_noop, resources, outdir):
|
||||
def check_monochrome_correlation(
|
||||
outdir,
|
||||
reference_pdf, reference_pageno, test_pdf, test_pageno):
|
||||
|
||||
import ocrmypdf.exec.ghostscript as ghostscript
|
||||
import logging
|
||||
|
||||
gslog = logging.getLogger()
|
||||
|
||||
reference_png = outdir / '{}.ref{:04d}.png'.format(
|
||||
@@ -273,9 +270,7 @@ def check_monochrome_correlation(
|
||||
print(png)
|
||||
return
|
||||
ghostscript.rasterize_pdf(
|
||||
str(pdf),
|
||||
str(png),
|
||||
xres=100, yres=100,
|
||||
pdf, png, xres=100, yres=100,
|
||||
raster_device='pngmono', log=gslog, pageno=pageno)
|
||||
|
||||
rasterize(reference_pdf, reference_pageno, reference_png)
|
||||
@@ -308,10 +303,7 @@ def test_monochrome_correlation(resources, outdir):
|
||||
assert corr > 0.90
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_autorotate(spoof_tesseract_cache, renderer, resources, outdir):
|
||||
# cardinal.pdf contains four copies of an image rotated in each cardinal
|
||||
# direction - these ones are "burned in" not tagged with /Rotate
|
||||
@@ -346,25 +338,22 @@ def test_autorotate_threshold(
|
||||
assert eval(correlation_test)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer',RENDERERS)
|
||||
def test_ocr_timeout(renderer, resources, outpdf):
|
||||
out = check_ocrmypdf(resources / 'skew.pdf', outpdf,
|
||||
'--tesseract-timeout', '1.0')
|
||||
pdfinfo = pdf_get_all_pageinfo(str(out))
|
||||
assert not pdfinfo[0]['has_text']
|
||||
pdfinfo = PdfInfo(out)
|
||||
assert not pdfinfo[0].has_text
|
||||
|
||||
|
||||
def test_skip_big(spoof_tesseract_cache, resources, outpdf):
|
||||
out = check_ocrmypdf(resources / 'enormous.pdf', outpdf,
|
||||
'--skip-big', '10', env=spoof_tesseract_cache)
|
||||
pdfinfo = pdf_get_all_pageinfo(str(out))
|
||||
assert not pdfinfo[0]['has_text']
|
||||
pdfinfo = PdfInfo(out)
|
||||
assert not pdfinfo[0].has_text
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', ['hocr', 'tesseract'])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
@pytest.mark.parametrize('output_type', ['pdf', 'pdfa'])
|
||||
def test_maximum_options(spoof_tesseract_cache, renderer, output_type,
|
||||
resources, outpdf):
|
||||
@@ -427,7 +416,7 @@ def test_french(spoof_tesseract_cache, resources, outpdf):
|
||||
def test_klingon(resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'francais.pdf', outpdf, '-l', 'klz')
|
||||
assert p.returncode == ExitCode.bad_args
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf):
|
||||
@@ -494,10 +483,7 @@ def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf):
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash,
|
||||
resources, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
@@ -520,10 +506,7 @@ def test_tesseract_crash_autorotate(spoof_tesseract_crash,
|
||||
print(err)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error,
|
||||
resources, outpdf):
|
||||
check_ocrmypdf(
|
||||
@@ -557,32 +540,28 @@ def test_algo4(resources, no_outpdf):
|
||||
def test_non_square_resolution(renderer, spoof_tesseract_cache,
|
||||
resources, outpdf):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = pdf_get_all_pageinfo(str(resources / 'aspect.pdf'))
|
||||
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
|
||||
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
|
||||
assert in_pageinfo[0].xres != in_pageinfo[0].yres
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'aspect.pdf', outpdf,
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(str(outpdf))
|
||||
out_pageinfo = PdfInfo(outpdf)
|
||||
|
||||
# Confirm resolution was kept the same
|
||||
assert in_pageinfo[0]['xres'] == out_pageinfo[0]['xres']
|
||||
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
|
||||
assert in_pageinfo[0].xres == out_pageinfo[0].xres
|
||||
assert in_pageinfo[0].yres == out_pageinfo[0].yres
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract'
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_convert_to_square_resolution(renderer, spoof_tesseract_cache,
|
||||
resources, outpdf):
|
||||
from math import isclose
|
||||
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = pdf_get_all_pageinfo(str(resources / 'aspect.pdf'))
|
||||
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
|
||||
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
|
||||
assert in_pageinfo[0].xres != in_pageinfo[0].yres
|
||||
|
||||
# --force-ocr requires means forced conversion to square resolution
|
||||
check_ocrmypdf(
|
||||
@@ -590,25 +569,25 @@ def test_convert_to_square_resolution(renderer, spoof_tesseract_cache,
|
||||
'--force-ocr',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(str(outpdf))
|
||||
out_pageinfo = PdfInfo(outpdf)
|
||||
|
||||
in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0]
|
||||
|
||||
# Resolution show now be equal
|
||||
assert out_p0['xres'] == out_p0['yres']
|
||||
assert out_p0.xres == out_p0.yres
|
||||
|
||||
# Page size should match input page size
|
||||
assert isclose(in_p0['width_inches'],
|
||||
out_p0['width_inches'])
|
||||
assert isclose(in_p0['height_inches'],
|
||||
out_p0['height_inches'])
|
||||
assert isclose(in_p0.width_inches,
|
||||
out_p0.width_inches)
|
||||
assert isclose(in_p0.height_inches,
|
||||
out_p0.height_inches)
|
||||
|
||||
# Because we rasterized the page to produce a new image, it should occupy
|
||||
# the entire page
|
||||
out_im_w = out_p0['images'][0]['width'] / out_p0['images'][0]['dpi_w']
|
||||
out_im_h = out_p0['images'][0]['height'] / out_p0['images'][0]['dpi_h']
|
||||
assert isclose(out_p0['width_inches'], out_im_w)
|
||||
assert isclose(out_p0['height_inches'], out_im_h)
|
||||
out_im_w = out_p0.images[0]['width'] / out_p0.images[0]['dpi_w']
|
||||
out_im_h = out_p0.images[0]['height'] / out_p0.images[0]['dpi_h']
|
||||
assert isclose(out_p0.width_inches, out_im_w)
|
||||
assert isclose(out_p0.height_inches, out_im_h)
|
||||
|
||||
|
||||
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
|
||||
@@ -624,8 +603,8 @@ def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf):
|
||||
'--pdf-renderer', 'hocr',
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(str(out))
|
||||
assert out_pageinfo[0]['images'][0]['enc'] == 'jbig2'
|
||||
out_pageinfo = PdfInfo(out)
|
||||
assert out_pageinfo[0].images[0].enc == Encoding.jbig2
|
||||
|
||||
|
||||
def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
@@ -698,27 +677,27 @@ def test_rotated_skew_timeout(resources, outpdf):
|
||||
"""
|
||||
|
||||
input_file = str(resources / 'rotated_skew.pdf')
|
||||
in_pageinfo = pdf_get_all_pageinfo(input_file)[0]
|
||||
in_pageinfo = PdfInfo(input_file)[0]
|
||||
|
||||
assert in_pageinfo['height_pixels'] < in_pageinfo['width_pixels'], \
|
||||
assert in_pageinfo.height_pixels < in_pageinfo.width_pixels, \
|
||||
"Expected the input page to be landscape"
|
||||
assert in_pageinfo['rotate'] == 90, "Expected a rotated page"
|
||||
assert in_pageinfo.rotation == 90, "Expected a rotated page"
|
||||
|
||||
out = check_ocrmypdf(
|
||||
input_file, outpdf,
|
||||
'--pdf-renderer', 'hocr',
|
||||
'--deskew', '--tesseract-timeout', '0')
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(str(out))[0]
|
||||
out_pageinfo = PdfInfo(out)[0]
|
||||
|
||||
assert out_pageinfo['height_pixels'] > out_pageinfo['width_pixels'], \
|
||||
assert out_pageinfo.height_pixels > out_pageinfo.width_pixels, \
|
||||
"Expected the output page to be portrait"
|
||||
|
||||
assert out_pageinfo['rotate'] == 0, \
|
||||
assert out_pageinfo.rotation == 0, \
|
||||
"Expected no page rotation for output"
|
||||
|
||||
assert in_pageinfo['width_pixels'] == out_pageinfo['height_pixels'] and \
|
||||
in_pageinfo['height_pixels'] == out_pageinfo['width_pixels'], \
|
||||
assert in_pageinfo.width_pixels == out_pageinfo.height_pixels and \
|
||||
in_pageinfo.height_pixels == out_pageinfo.width_pixels, \
|
||||
"Expected page rotation to be baked in"
|
||||
|
||||
|
||||
@@ -739,6 +718,11 @@ def test_very_high_dpi(spoof_tesseract_cache, resources, outpdf):
|
||||
"Checks for a Decimal quantize error with high DPI, etc"
|
||||
check_ocrmypdf(resources / '2400dpi.pdf', outpdf,
|
||||
env=spoof_tesseract_cache)
|
||||
pdfinfo = PdfInfo(outpdf)
|
||||
|
||||
image = pdfinfo[0].images[0]
|
||||
assert isclose(image.xres, image.yres)
|
||||
assert isclose(image.xres, 2400)
|
||||
|
||||
|
||||
def test_overlay(spoof_tesseract_noop, resources, outpdf):
|
||||
@@ -771,28 +755,22 @@ language_model_penalty_non_freq_dict_word 0
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outdir / 'out.pdf',
|
||||
'--tesseract-config', str(cfg_file))
|
||||
'--tesseract-config', cfg_file)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_config_notfound(renderer, resources, outdir):
|
||||
cfg_file = outdir / 'nofile.cfg'
|
||||
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outdir / 'out.pdf',
|
||||
'--pdf-renderer', renderer,
|
||||
'--tesseract-config', str(cfg_file))
|
||||
'--tesseract-config', cfg_file)
|
||||
assert "Can't open" in err, "No error message about missing config file"
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_tesseract_config_invalid(renderer, resources, outdir):
|
||||
cfg_file = outdir / 'test.cfg'
|
||||
with cfg_file.open('w') as f:
|
||||
@@ -803,22 +781,50 @@ THIS FILE IS INVALID
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outdir / 'out.pdf',
|
||||
'--pdf-renderer', renderer,
|
||||
'--tesseract-config', str(cfg_file))
|
||||
'--tesseract-config', cfg_file)
|
||||
assert "parameter not found" in err, "No error message"
|
||||
assert p.returncode == ExitCode.invalid_config
|
||||
|
||||
|
||||
def test_user_words(resources, outdir):
|
||||
word_list = outdir / 'wordlist.txt'
|
||||
sidecar_before = outdir / 'sidecar_before.txt'
|
||||
sidecar_after = outdir / 'sidecar_after.txt'
|
||||
|
||||
# Don't know how to make this test pass on various versions and platforms
|
||||
# so weaken to merely testing that the argument is accepted
|
||||
consistent = False
|
||||
|
||||
if consistent:
|
||||
check_ocrmypdf(
|
||||
resources / 'crom.png', outdir / 'out.pdf',
|
||||
'--image-dpi', 150,
|
||||
'--sidecar', sidecar_before
|
||||
)
|
||||
|
||||
assert 'cromulent' not in sidecar_before.open().read()
|
||||
|
||||
with word_list.open('w') as f:
|
||||
f.write('cromulent\n') # a perfectly cromulent word
|
||||
|
||||
check_ocrmypdf(
|
||||
resources / 'crom.png', outdir / 'out.pdf',
|
||||
'--image-dpi', 150,
|
||||
'--sidecar', sidecar_after,
|
||||
'--user-words', word_list
|
||||
)
|
||||
|
||||
if consistent:
|
||||
assert 'cromulent' in sidecar_after.open().read()
|
||||
|
||||
|
||||
def test_form_xobject(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'formxobject.pdf', outpdf,
|
||||
'--force-ocr',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
@pytest.mark.parametrize('renderer', RENDERERS)
|
||||
def test_pagesize_consistency(renderer, resources, outpdf):
|
||||
from math import isclose
|
||||
|
||||
@@ -837,3 +843,138 @@ def test_pagesize_consistency(renderer, resources, outpdf):
|
||||
|
||||
assert isclose(before_dims[0], after_dims[0])
|
||||
assert isclose(before_dims[1], after_dims[1])
|
||||
|
||||
|
||||
def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'blank.pdf', outpdf,
|
||||
'--skip-big', '5',
|
||||
'--force-ocr',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'blank.pdf', outpdf,
|
||||
env=spoof_no_tess_gs_render_fail)
|
||||
print(err)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outpdf,
|
||||
env=spoof_no_tess_gs_raster_fail)
|
||||
print(err)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_no_contents(spoof_tesseract_noop, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'no_contents.pdf', outpdf, '--force-ocr',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('image', [
|
||||
'baiona.png',
|
||||
'baiona_gray.png',
|
||||
'congress.jpg'
|
||||
])
|
||||
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
||||
resources, image, outpdf):
|
||||
from PIL import Image
|
||||
|
||||
input_file = str(resources / image)
|
||||
output_file = str(outpdf)
|
||||
|
||||
im = Image.open(input_file)
|
||||
|
||||
# Runs: ocrmypdf - output.pdf < testfile
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
p_args = ocrmypdf_exec + [
|
||||
'--image-dpi', '150', '--output-type', 'pdf', '-', output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
stdin=input_stream, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
pdfinfo = PdfInfo(output_file)
|
||||
|
||||
pdfimage = pdfinfo[0].images[0]
|
||||
|
||||
if input_file.endswith('.png'):
|
||||
assert pdfimage.enc != Encoding.jpeg, \
|
||||
"Lossless compression changed to lossy!"
|
||||
elif input_file.endswith('.jpg'):
|
||||
assert pdfimage.enc == Encoding.jpeg, \
|
||||
"Lossy compression changed to lossless!"
|
||||
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
||||
assert pdfimage.color == Colorspace.rgb, \
|
||||
"Colorspace changed"
|
||||
elif im.mode.startswith('L'):
|
||||
assert pdfimage.color == Colorspace.gray, \
|
||||
"Colorspace changed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('image,compression', [
|
||||
('baiona.png', 'jpeg'),
|
||||
('baiona_gray.png', 'lossless'),
|
||||
('congress.jpg', 'lossless')
|
||||
])
|
||||
def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
|
||||
resources, image, compression, outpdf):
|
||||
from PIL import Image
|
||||
|
||||
input_file = str(resources / image)
|
||||
output_file = str(outpdf)
|
||||
|
||||
im = Image.open(input_file)
|
||||
|
||||
# Runs: ocrmypdf - output.pdf < testfile
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
p_args = ocrmypdf_exec + [
|
||||
'--image-dpi', '150', '--output-type', 'pdfa',
|
||||
'--pdfa-image-compression', compression,
|
||||
'-', output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
stdin=input_stream, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
pdfinfo = PdfInfo(output_file)
|
||||
|
||||
pdfimage = pdfinfo[0].images[0]
|
||||
|
||||
if compression == "jpeg":
|
||||
assert pdfimage.enc == Encoding.jpeg
|
||||
elif compression == 'lossless':
|
||||
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
|
||||
|
||||
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
||||
assert pdfimage.color == Colorspace.rgb, \
|
||||
"Colorspace changed"
|
||||
elif im.mode.startswith('L'):
|
||||
assert pdfimage.color == Colorspace.gray, \
|
||||
"Colorspace changed"
|
||||
|
||||
|
||||
def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
|
||||
sidecar = outpdf + '.txt'
|
||||
check_ocrmypdf(
|
||||
resources / 'multipage.pdf', outpdf,
|
||||
'--skip-text',
|
||||
'--sidecar', sidecar,
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
pdfinfo = PdfInfo(resources / 'multipage.pdf')
|
||||
num_pages = len(pdfinfo)
|
||||
|
||||
with open(sidecar, 'r') as f:
|
||||
ocr_text = f.read()
|
||||
|
||||
# There should a formfeed between each pair of pages, so the count of
|
||||
# formfeeds is the page count less one
|
||||
assert ocr_text.count('\f') == num_pages - 1, \
|
||||
"Sidecar page count does not match PDF page count"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
from ocrmypdf.pdfinfo import PdfInfo, PageInfo
|
||||
from ocrmypdf.pipeline import JobContext, JobContextManager
|
||||
from multiprocessing import Process
|
||||
from multiprocessing.managers import BaseProxy
|
||||
|
||||
|
||||
def test_jobcontext_proxy(resources):
|
||||
# Prove that managers are set up correctly to share state among processes
|
||||
manager = JobContextManager()
|
||||
manager.register('JobContext', JobContext)
|
||||
|
||||
# Start the manager in a child process (or maybe thread)
|
||||
manager.start()
|
||||
|
||||
# Tell the manager process to retrieve pdf info
|
||||
context = manager.JobContext()
|
||||
context.generate_pdfinfo(resources / 'graph.pdf')
|
||||
|
||||
# Get a copy of that information for this process
|
||||
pdfinfo = context.get_pdfinfo()
|
||||
assert len(pdfinfo) == 1
|
||||
assert pdfinfo[0].rotation == 0
|
||||
|
||||
# Update information and send back to manager
|
||||
pdfinfo[0].rotation = 90
|
||||
context.set_pdfinfo(pdfinfo)
|
||||
|
||||
# Retrieve again, ensure it stayed changed
|
||||
pdfinfo2 = context.get_pdfinfo()
|
||||
assert pdfinfo2[0].rotation == 90
|
||||
|
||||
# Start a new process which gets its own proxy object
|
||||
def client(context):
|
||||
assert isinstance(context, BaseProxy)
|
||||
pdfinfo = context.get_pdfinfo()
|
||||
page = pdfinfo[0]
|
||||
assert page.rotation == 90
|
||||
page.rotation += 90
|
||||
context.set_pdfinfo(pdfinfo)
|
||||
|
||||
p = Process(target=client, args=(context,))
|
||||
p.start()
|
||||
p.join()
|
||||
assert p.exitcode == 0, "Child process failed"
|
||||
|
||||
assert context.get_pdfinfo()[0].rotation == 180
|
||||
+45
-30
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from ocrmypdf import pageinfo
|
||||
from ocrmypdf import pdfinfo
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from PIL import Image
|
||||
from tempfile import NamedTemporaryFile
|
||||
from math import isclose
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding
|
||||
from contextlib import suppress
|
||||
import os
|
||||
import shutil
|
||||
@@ -26,16 +28,15 @@ def test_single_page_text(outdir):
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
|
||||
assert len(pdfinfo) == 1
|
||||
page = pdfinfo[0]
|
||||
assert len(info) == 1
|
||||
page = info[0]
|
||||
|
||||
assert page['has_text']
|
||||
assert len(page['images']) == 0
|
||||
assert page.has_text
|
||||
assert len(page.images) == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs Path.read_bytes")
|
||||
def test_single_page_image(outdir):
|
||||
filename = outdir / 'image-mono.pdf'
|
||||
|
||||
@@ -54,21 +55,21 @@ def test_single_page_image(outdir):
|
||||
layout_fun=layout_fun)
|
||||
filename.write_bytes(pdf_bytes)
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
|
||||
assert len(pdfinfo) == 1
|
||||
page = pdfinfo[0]
|
||||
assert len(info) == 1
|
||||
page = info[0]
|
||||
|
||||
assert not page['has_text']
|
||||
assert len(page['images']) == 1
|
||||
assert not page.has_text
|
||||
assert len(page.images) == 1
|
||||
|
||||
pdfimage = page['images'][0]
|
||||
assert pdfimage['width'] == 8
|
||||
assert pdfimage['color'] == 'gray'
|
||||
pdfimage = page.images[0]
|
||||
assert pdfimage.width == 8
|
||||
assert pdfimage.color == Colorspace.gray
|
||||
|
||||
# DPI in a 1"x1" is the image width
|
||||
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
|
||||
assert isclose(pdfimage.xres, 8)
|
||||
assert isclose(pdfimage.yres, 8)
|
||||
|
||||
|
||||
def test_single_page_inline_image(outdir):
|
||||
@@ -84,27 +85,41 @@ def test_single_page_inline_image(outdir):
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
print(pdfinfo)
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert (pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert pdfimage['color'] != '-'
|
||||
assert pdfimage['width'] == 8
|
||||
pdf = pdfinfo.PdfInfo(filename)
|
||||
print(pdf)
|
||||
pdfimage = pdf[0].images[0]
|
||||
assert isclose(pdfimage.xres, 8)
|
||||
assert pdfimage.color == Colorspace.rgb # reportlab produces color image
|
||||
assert pdfimage.width == 8
|
||||
|
||||
|
||||
def test_jpeg(resources, outdir):
|
||||
filename = resources / 'c02-22.pdf'
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
pdf = pdfinfo.PdfInfo(filename)
|
||||
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert pdfimage['enc'] == 'jpeg'
|
||||
assert (pdfimage['dpi_w'] - 150) < 1e-5
|
||||
pdfimage = pdf[0].images[0]
|
||||
assert pdfimage.enc == Encoding.jpeg
|
||||
assert isclose(pdfimage.xres, 150)
|
||||
|
||||
|
||||
def test_form_xobject(resources):
|
||||
filename = resources / 'formxobject.pdf'
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert pdfimage['width'] == 50
|
||||
pdf = pdfinfo.PdfInfo(filename)
|
||||
pdfimage = pdf[0].images[0]
|
||||
assert pdfimage.width == 50
|
||||
|
||||
|
||||
def test_no_contents(resources):
|
||||
filename = resources / 'no_contents.pdf'
|
||||
|
||||
pdf = pdfinfo.PdfInfo(filename)
|
||||
assert len(pdf[0].images) == 0
|
||||
assert pdf[0].has_text == False
|
||||
|
||||
|
||||
def test_oversized_page(resources):
|
||||
pdf = pdfinfo.PdfInfo(resources / 'poster.pdf')
|
||||
image = pdf[0].images[0]
|
||||
assert image.width * image.xres > 200, "this is supposed to be oversized"
|
||||
+14
-2
@@ -11,14 +11,26 @@ pytestmark = pytest.mark.skipif(tesseract.v4(),
|
||||
reason="tesseract 3.x required")
|
||||
|
||||
|
||||
def test_textonly_pdf_on_tess3(resources, no_outpdf):
|
||||
@pytest.mark.skipif(tesseract.has_textonly_pdf(),
|
||||
reason="check that missing dep is reported on old tess3")
|
||||
def test_textonly_pdf_on_older_tess3(resources, no_outpdf):
|
||||
p, _, _ = pytest.helpers.run_ocrmypdf(
|
||||
resources / 'linn.pdf',
|
||||
no_outpdf, '--pdf-renderer', 'tess4')
|
||||
no_outpdf, '--pdf-renderer', 'sandwich')
|
||||
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
@pytest.mark.skipif(not tesseract.has_textonly_pdf(),
|
||||
reason="check that feature is exercised on new test3")
|
||||
def test_textonly_pdf_on_newer_tess3(resources, no_outpdf):
|
||||
p, _, _ = pytest.helpers.run_ocrmypdf(
|
||||
resources / 'linn.pdf',
|
||||
no_outpdf, '--pdf-renderer', 'sandwich')
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
|
||||
def test_oem_on_tess3(resources, no_outpdf):
|
||||
p, _, err = pytest.helpers.run_ocrmypdf(
|
||||
resources / 'aspect.pdf',
|
||||
|
||||
+40
-22
@@ -4,26 +4,41 @@
|
||||
import pytest
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf.exec import tesseract
|
||||
from ocrmypdf import pageinfo
|
||||
from ocrmypdf import pdfinfo
|
||||
import sys
|
||||
import os
|
||||
import PyPDF2 as pypdf
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
def tess4_possible_location():
|
||||
"""The location of tesseract 4 may be OCRMYPDF_TESS4, OCRMYPDF_TESSERACT,
|
||||
or the installed version on PATH."""
|
||||
return os.environ.get('OCRMYPDF_TESS4') or \
|
||||
os.environ.get('OCRMYPDF_TESSERACT') or \
|
||||
'tesseract'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ensure_tess4():
|
||||
return spoof(tesseract=tess4_possible_location())
|
||||
if tesseract.v4():
|
||||
# Either "tesseract" on $PATH is already v4, or
|
||||
# OCRMYPDF_TESSERACT is tess4 already
|
||||
return os.environ.copy()
|
||||
|
||||
if os.environ.get('OCRMYPDF_TESS4'):
|
||||
# OCRMYPDF_TESS4 is a hint environment variable that tells us to look
|
||||
# somewhere special for tess4 if and only if we need it. This allows
|
||||
# setting OCRMYPDF_TESS4 to test tess4 and PATH to point to tess3
|
||||
# on a system with both installed.
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_TESSERACT'] = env['OCRMYPDF_TESS4']
|
||||
return env
|
||||
|
||||
raise EnvironmentError("Can't find Tesseract 4")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def modified_os_environ(env):
|
||||
old_env = os.environ.copy()
|
||||
os.environ = env
|
||||
yield
|
||||
os.environ = old_env
|
||||
|
||||
|
||||
def tess4_available():
|
||||
@@ -31,13 +46,16 @@ def tess4_available():
|
||||
official "tesseract" on PATH
|
||||
|
||||
"""
|
||||
old_environ = os.environ.copy()
|
||||
try:
|
||||
os.environ['OCRMYPDF_TESSERACT'] = tess4_possible_location()
|
||||
return tesseract.v4() and tesseract.has_textonly_pdf()
|
||||
finally:
|
||||
os.environ = old_environ
|
||||
# ensure_tess4 locates the tess4 binary we are going to check
|
||||
env = ensure_tess4()
|
||||
with modified_os_environ(env):
|
||||
# Now jump into this environment and make sure it really is Tess4
|
||||
return tesseract.v4() and tesseract.has_textonly_pdf()
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
# Skip all tests in this file if not tesseract 4
|
||||
pytestmark = pytest.mark.skipif(
|
||||
@@ -53,10 +71,10 @@ def test_textonly_pdf(ensure_tess4, resources, outdir):
|
||||
check_ocrmypdf(
|
||||
resources / 'linn.pdf',
|
||||
outdir / 'linn_textonly.pdf', '--pdf-renderer', 'tess4',
|
||||
'--sidecar', 'foo',
|
||||
env=ensure_tess4)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
|
||||
def test_pagesize_consistency_tess4(ensure_tess4, resources, outpdf):
|
||||
from math import isclose
|
||||
|
||||
@@ -89,14 +107,14 @@ def test_skip_pages_does_not_replicate(
|
||||
env=ensure_tess4
|
||||
)
|
||||
|
||||
info_in = pageinfo.pdf_get_all_pageinfo(str(infile))
|
||||
info_in = pdfinfo.PdfInfo(infile)
|
||||
|
||||
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
|
||||
info = pdfinfo.PdfInfo(outpdf)
|
||||
for page in info:
|
||||
assert len(page['images']) == 1, "skipped page was replicated"
|
||||
assert len(page.images) == 1, "skipped page was replicated"
|
||||
|
||||
for n in range(len(info_in)):
|
||||
assert info[n]['width_inches'] == info_in[n]['width_inches']
|
||||
assert info[n].width_inches == info_in[n].width_inches
|
||||
|
||||
|
||||
def test_content_preservation(ensure_tess4, resources, outpdf):
|
||||
@@ -108,6 +126,6 @@ def test_content_preservation(ensure_tess4, resources, outpdf):
|
||||
env=ensure_tess4
|
||||
)
|
||||
|
||||
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
|
||||
info = pdfinfo.PdfInfo(outpdf)
|
||||
page = info[0]
|
||||
assert len(page['images']) > 1, "masked were rasterized"
|
||||
assert len(page.images) > 1, "masks were rasterized"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
|
||||
import os
|
||||
import shutil
|
||||
import pytest
|
||||
from ocrmypdf.pdfinfo import PdfInfo, Colorspace, Encoding
|
||||
import PyPDF2 as pypdf
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
from ocrmypdf.exec import ghostscript
|
||||
import logging
|
||||
from math import isclose
|
||||
|
||||
|
||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||
run_ocrmypdf = pytest.helpers.run_ocrmypdf
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def poster(resources):
|
||||
return resources / 'poster.pdf'
|
||||
|
||||
|
||||
def test_userunit_ghostscript_fails(poster, no_outpdf):
|
||||
p, out, err = run_ocrmypdf(poster, no_outpdf, '--output-type=pdfa')
|
||||
assert p.returncode == ExitCode.input_file
|
||||
|
||||
|
||||
def test_userunit_qpdf_passes(spoof_tesseract_cache, poster, outpdf):
|
||||
before = PdfInfo(poster)
|
||||
check_ocrmypdf(poster, outpdf, '--output-type=pdf',
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
after = PdfInfo(outpdf)
|
||||
assert isclose(before[0].width_inches, after[0].width_inches)
|
||||
|
||||
|
||||
def test_rotate_interaction(spoof_tesseract_cache, poster, outpdf):
|
||||
check_ocrmypdf(poster, outpdf, '--output-type=pdf',
|
||||
'--rotate-pages',
|
||||
env=spoof_tesseract_cache)
|
||||
Reference in New Issue
Block a user