Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f24fb0e0c5 | ||
|
|
73b88a0a6f | ||
|
|
c42f39e2d4 | ||
|
|
5e5fe3175f | ||
|
|
cab65d1f11 | ||
|
|
245f05d5f4 | ||
|
|
dda751f9e3 | ||
|
|
3d37ae988a | ||
|
|
717acd9855 | ||
|
|
2e4431cc63 | ||
|
|
f7387b0859 | ||
|
|
a09f6b8977 | ||
|
|
d63449c214 | ||
|
|
a86805f0d9 | ||
|
|
7d2009ccef | ||
|
|
18ae5db06d | ||
|
|
9a1838f102 | ||
|
|
e20346032d | ||
|
|
693a27d76c | ||
|
|
203966d86b | ||
|
|
7eca8508fd | ||
|
|
b85270df1c | ||
|
|
aff597cef4 | ||
|
|
61b05b3dee | ||
|
|
453c4ef602 | ||
|
|
cf4b04f92d | ||
|
|
06c6999987 | ||
|
|
013c5a369f | ||
|
|
07891d994a | ||
|
|
6baf8668a6 | ||
|
|
4ba2962c56 | ||
|
|
7ad92f5db4 | ||
|
|
4dad09cc91 | ||
|
|
7b2e0c7a7a | ||
|
|
7f08f15fc9 | ||
|
|
825c0f8b2a | ||
|
|
dbe880bc41 | ||
|
|
2ec516b6ff | ||
|
|
7942a01e50 | ||
|
|
df684f9344 | ||
|
|
ae16e95e42 | ||
|
|
2ac8e8a0cc | ||
|
|
0a0ceda71f | ||
|
|
220f1ce161 | ||
|
|
c62a8a97c9 | ||
|
|
f8a1136979 | ||
|
|
9ca29c787b | ||
|
|
6af748a251 | ||
|
|
9041867f86 | ||
|
|
04099b087c | ||
|
|
6d6234714c | ||
|
|
520be23481 | ||
|
|
346c3c8dd3 | ||
|
|
bd534c3313 | ||
|
|
2625368aed | ||
|
|
8ac94879f1 | ||
|
|
dd8c0f3756 | ||
|
|
010f353a5e | ||
|
|
e0a18edb92 | ||
|
|
c6f2eea058 | ||
|
|
bf89e38c69 | ||
|
|
e1f0640d42 | ||
|
|
71b54035ba | ||
|
|
325cc0beca | ||
|
|
1a9f09c4d5 | ||
|
|
4fed4e2af3 | ||
|
|
74cc2346a5 | ||
|
|
cc7e328358 | ||
|
|
d25397e2b0 | ||
|
|
bc11454e1c | ||
|
|
2025a096c3 | ||
|
|
38fe14b108 | ||
|
|
1b7b2f3695 | ||
|
|
5d67cc76cc | ||
|
|
27a3813207 | ||
|
|
b06e0bfdcd | ||
|
|
d616f25324 | ||
|
|
b03028e31f | ||
|
|
e08c42fd3d | ||
|
|
16901f7134 | ||
|
|
dffceedd85 | ||
|
|
e5541e435c | ||
|
|
b969aad67b | ||
|
|
e70387b1af | ||
|
|
44f47fba21 | ||
|
|
02584094a1 | ||
|
|
91d715ac93 | ||
|
|
35addb8a33 | ||
|
|
d32ea8d0dd | ||
|
|
12575d594a | ||
|
|
0746083301 | ||
|
|
5c99acf6d1 | ||
|
|
2b10df7b74 | ||
|
|
ebe68de4ff | ||
|
|
b17c6a146d | ||
|
|
46d837c866 | ||
|
|
24856b61e4 | ||
|
|
8d0c6ff616 | ||
|
|
0b24f971cd | ||
|
|
bc5d3824bd | ||
|
|
4356983707 | ||
|
|
2414b79ee6 | ||
|
|
968e1546f0 | ||
|
|
48213c9c3f | ||
|
|
f385772d21 | ||
|
|
d257c83520 | ||
|
|
7b72ffec4f | ||
|
|
757f6826dc | ||
|
|
5df83a0d30 | ||
|
|
d70e3d3753 | ||
|
|
0dfceedcfb | ||
|
|
2c30f4bfc5 | ||
|
|
9e7fb52b47 | ||
|
|
bb5fd38e38 | ||
|
|
7c8cf5cfa2 | ||
|
|
fef35e4eb2 | ||
|
|
8f77576dc4 | ||
|
|
b3fcf24a26 | ||
|
|
16e4d342d2 | ||
|
|
8458a51860 | ||
|
|
636d1903b3 | ||
|
|
514efa36fc | ||
|
|
bd48f40d3d | ||
|
|
c02dbc809a | ||
|
|
410111d6fb | ||
|
|
68cf9cbd87 | ||
|
|
c9b2540d9d | ||
|
|
1bacf35a2c | ||
|
|
8aef0d9277 | ||
|
|
b2fa8645ba | ||
|
|
c96823a648 | ||
|
|
3807b7d655 | ||
|
|
a45505cf1d | ||
|
|
b4a734fc0d | ||
|
|
bbd02926e1 | ||
|
|
5022ded276 | ||
|
|
c7612152ef | ||
|
|
af91642cd1 | ||
|
|
9c66334c38 | ||
|
|
8d79b94b84 | ||
|
|
d7f60b96c1 | ||
|
|
b964999427 | ||
|
|
3473345ea6 | ||
|
|
349ec5c81f | ||
|
|
ff78d7c56c | ||
|
|
ff092c8629 | ||
|
|
fe14cb57c0 | ||
|
|
507fbc01d5 | ||
|
|
325479e5be | ||
|
|
e926ecb8b2 | ||
|
|
d0cb6c0e92 | ||
|
|
5b7c8cf5d3 | ||
|
|
40baab32ac | ||
|
|
e877d37ac8 | ||
|
|
5a9f77e438 | ||
|
|
8ddd67d1e2 | ||
|
|
1605408c23 | ||
|
|
2d3b1ebf6e | ||
|
|
c74eaab7f5 | ||
|
|
c21d231388 | ||
|
|
a73afc4e76 | ||
|
|
76c364150d | ||
|
|
94a3e447cc | ||
|
|
12868b461a | ||
|
|
322085933b | ||
|
|
3fed94bb79 | ||
|
|
8c877482bd | ||
|
|
b17d589e84 | ||
|
|
368252a243 | ||
|
|
ccefda1bee | ||
|
|
3d0e8c9629 | ||
|
|
313bbbb94c | ||
|
|
0360f078de | ||
|
|
c8901666c4 | ||
|
|
7430006596 | ||
|
|
f3e06b2dbd | ||
|
|
e97df307ff | ||
|
|
1443354aa2 | ||
|
|
250e68c1cd | ||
|
|
6a380ee99c | ||
|
|
3c90bd96a9 | ||
|
|
06a7ceb25a | ||
|
|
733a8e7d58 | ||
|
|
570bbe9a05 | ||
|
|
5cc3adb39a | ||
|
|
3957a0606c | ||
|
|
11a561dbce | ||
|
|
dad2198394 | ||
|
|
e40fdc502d | ||
|
|
d446fe5922 | ||
|
|
4ca90c106d | ||
|
|
7c5e58a497 | ||
|
|
323b9a5f8e | ||
|
|
cab381a339 | ||
|
|
fe4d4c39cd | ||
|
|
ad188d7ae1 | ||
|
|
8246cc0538 | ||
|
|
6f3ac46b1c | ||
|
|
ac71c3be63 | ||
|
|
ecc0ac9b19 | ||
|
|
ea4e6bf67d | ||
|
|
46c204f533 | ||
|
|
71fbda8bf6 | ||
|
|
9b79b4a7c8 | ||
|
|
c04cc853d7 | ||
|
|
dd41e70ccc | ||
|
|
4206e74f42 | ||
|
|
68c3ce56a9 | ||
|
|
ab0e5fa425 | ||
|
|
f3b0434a87 | ||
|
|
aa394440db | ||
|
|
3b98a1a04b | ||
|
|
fcb89b0c58 | ||
|
|
ac65d6a03a | ||
|
|
2103f60906 | ||
|
|
e3c3d848c1 | ||
|
|
d4ef3411e0 | ||
|
|
71d616e413 | ||
|
|
fe651d1bf5 | ||
|
|
582ba8cfad | ||
|
|
d23291650a | ||
|
|
812fd745b6 | ||
|
|
a87aa71d85 | ||
|
|
60b2eb1455 | ||
|
|
ab3c1988c1 | ||
|
|
ee5223eea8 | ||
|
|
edd2185268 | ||
|
|
35b1ca2be2 | ||
|
|
71e493a810 | ||
|
|
6178e22e7f | ||
|
|
ef0aab060a | ||
|
|
d70ce61cfd | ||
|
|
8cd84afac8 | ||
|
|
ec2c6c312b | ||
|
|
3946bba318 | ||
|
|
2ed0b78a7b | ||
|
|
ed346d032c | ||
|
|
acd645f192 | ||
|
|
88433e4c34 | ||
|
|
1224af1780 | ||
|
|
ab13342931 | ||
|
|
d7913da484 | ||
|
|
c50e3f1329 | ||
|
|
a62f86dbd7 | ||
|
|
33b88b18db | ||
|
|
7c691c21ab | ||
|
|
4ec51729d8 | ||
|
|
07b41e479a | ||
|
|
6510bcad19 | ||
|
|
265d2ce39b | ||
|
|
1928a64cae | ||
|
|
11e575a5a3 | ||
|
|
7fbc0d6460 | ||
|
|
1ba8b1aa4b | ||
|
|
3569c76c0f | ||
|
|
16c7ac2582 | ||
|
|
4ceb59215f | ||
|
|
2e6879ee51 | ||
|
|
66fc2e9d7d | ||
|
|
2c7a6e574f | ||
|
|
78c3bf5dba | ||
|
|
98c115e3bb | ||
|
|
2752bda80b | ||
|
|
7c0940609a | ||
|
|
d30a879e2d | ||
|
|
b907234d5c | ||
|
|
b0114c9174 | ||
|
|
d2ba8c501f | ||
|
|
6a7ed7d359 | ||
|
|
6289afa1a6 | ||
|
|
9bb6fa04cb | ||
|
|
afb6f6f5c9 | ||
|
|
8a69671dbd | ||
|
|
178aee4687 | ||
|
|
8484caddfb | ||
|
|
08313316de | ||
|
|
1d0eca5c63 | ||
|
|
fe89232a30 | ||
|
|
4b51b521e2 | ||
|
|
e9ec458304 | ||
|
|
54b0ddd787 | ||
|
|
93bec22f9c | ||
|
|
0dc96442d8 | ||
|
|
58f4582517 | ||
|
|
2d15c09cca | ||
|
|
04cb8865b0 | ||
|
|
6fe32bbaf7 | ||
|
|
4abb20390d | ||
|
|
daa3916430 | ||
|
|
e9b87cefcc | ||
|
|
60593b5ad3 | ||
|
|
f708b11ea4 | ||
|
|
7982f58b2e | ||
|
|
e805c1908a | ||
|
|
cb3ba8e973 | ||
|
|
344fc40cbc | ||
|
|
7e5c37137b | ||
|
|
1aae11714b | ||
|
|
d82f14a7aa | ||
|
|
4b65e0b093 | ||
|
|
43b0faa830 | ||
|
|
8674c9fb20 | ||
|
|
ccfbb54e8c | ||
|
|
9893ebf889 | ||
|
|
303eb3e93a | ||
|
|
ca546d70e5 | ||
|
|
6a5ea2d64a | ||
|
|
ec3d92ad8e | ||
|
|
66a095d7de | ||
|
|
411981efbc | ||
|
|
350ad5210e | ||
|
|
f3b588764e | ||
|
|
b49f5a7d77 | ||
|
|
bacbcba58a | ||
|
|
52e8aa434f | ||
|
|
37c508f3f8 | ||
|
|
26e36422cc | ||
|
|
f82cb002bc | ||
|
|
c1eb047a4b | ||
|
|
626ca18f5c | ||
|
|
9058dedfbe | ||
|
|
a0952bfca3 | ||
|
|
354e61946e | ||
|
|
fd6d1d748a | ||
|
|
360acd1e2c | ||
|
|
fc0479f110 | ||
|
|
62728205b6 | ||
|
|
dc0fb25e64 | ||
|
|
f3e04cce56 | ||
|
|
7067110308 | ||
|
|
599d889703 | ||
|
|
2fa8366632 | ||
|
|
c368c51bad | ||
|
|
7c558b3713 | ||
|
|
8d323ae510 | ||
|
|
3b53e9adac | ||
|
|
074c1d71b4 | ||
|
|
1fca9a004d | ||
|
|
b485a1ef78 | ||
|
|
326ef7a3ac | ||
|
|
12bc58b5b6 | ||
|
|
6af0815681 | ||
|
|
66c2b9b78e | ||
|
|
d03c056cb1 | ||
|
|
3f94d628fa | ||
|
|
a64c7dbe99 | ||
|
|
61b3ccb57c | ||
|
|
424b4b33b1 | ||
|
|
e510f89792 | ||
|
|
49cd6cc619 | ||
|
|
9aa3d340d4 | ||
|
|
09782242c8 | ||
|
|
9ec4aa039d | ||
|
|
ecebe2f24b | ||
|
|
7313a77c2a | ||
|
|
45113676a3 | ||
|
|
102bd07019 | ||
|
|
9622e31da9 | ||
|
|
1731ce2a44 | ||
|
|
276f421c44 | ||
|
|
133357779a | ||
|
|
5d8167b232 | ||
|
|
e76ae8c46c | ||
|
|
53a7c0e668 | ||
|
|
4ca243e490 | ||
|
|
9f37446155 | ||
|
|
d7c7559b05 | ||
|
|
b2b66d1344 | ||
|
|
5d111a3c04 | ||
|
|
10416f847f | ||
|
|
79b3472b26 | ||
|
|
f1b2f1ae08 | ||
|
|
ee7d97ae8c | ||
|
|
7d9f473bb1 | ||
|
|
e77a5e5e75 | ||
|
|
6ab19af122 | ||
|
|
276fe49867 | ||
|
|
acb31abe86 | ||
|
|
4f964a3c8a | ||
|
|
df1fda7438 | ||
|
|
d6124c1787 | ||
|
|
80d89b5420 | ||
|
|
74059eecf1 | ||
|
|
78697341a2 | ||
|
|
cfb56dd8ff | ||
|
|
b1769cbe18 | ||
|
|
955b801e7f | ||
|
|
3cea3f1afe | ||
|
|
fd4a227ccb | ||
|
|
19c3097483 | ||
|
|
cdd1a6d03c | ||
|
|
5fb8411571 | ||
|
|
334a15b8c7 | ||
|
|
6390736577 | ||
|
|
d55a214516 | ||
|
|
0994164b9a | ||
|
|
54ee0dd147 | ||
|
|
47c7990fb3 | ||
|
|
997e95de4d | ||
|
|
44204be256 | ||
|
|
9b1d9aa88a | ||
|
|
b775762f6a | ||
|
|
df1a28e319 | ||
|
|
c300b2802a | ||
|
|
01040ace4c | ||
|
|
8367172e0b |
+23
-13
@@ -1,21 +1,31 @@
|
||||
bin/
|
||||
build/
|
||||
dist/
|
||||
include/
|
||||
lib/
|
||||
ocrmypdf.egg-info/
|
||||
staging/
|
||||
.git/
|
||||
.ruffus_history.sqlite
|
||||
MANIFEST.in
|
||||
*.sublime*
|
||||
*.ipynb
|
||||
*.pdf
|
||||
*.rst
|
||||
*.pyc
|
||||
*.rst
|
||||
*.sublime*
|
||||
*/*.pyc
|
||||
*/*/*.pyc
|
||||
*/*/*/*.pyc
|
||||
*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*/*/*.pyc
|
||||
.cache/
|
||||
.git/
|
||||
.ipynb_checkpoints/
|
||||
.ruffus_history.sqlite
|
||||
bin/
|
||||
build/
|
||||
dist/
|
||||
htmlcov/
|
||||
include/
|
||||
lib/
|
||||
MANIFEST.in
|
||||
ocrmypdf.egg-info/
|
||||
staging/
|
||||
tests/cache/
|
||||
tests/output/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
venv-3.4/
|
||||
venv-3.5/
|
||||
@@ -0,0 +1 @@
|
||||
ref-names: $Format:%D$
|
||||
+3
-1
@@ -5,4 +5,6 @@
|
||||
# (binary is a macro for -text -diff)
|
||||
*.jar binary
|
||||
*.pdf binary
|
||||
*.PDF binary
|
||||
*.PDF binary
|
||||
|
||||
.git_archival.txt export-subst
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Please include the command line and (if needed) a test file with your issue report.
|
||||
|
||||
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
|
||||
+30
-13
@@ -1,17 +1,34 @@
|
||||
tmp/
|
||||
log/
|
||||
# Development environment
|
||||
*.pyc
|
||||
tests/output/
|
||||
.ruffus_history.sqlite
|
||||
*.sublime-*
|
||||
/*.pdf
|
||||
venv-*/
|
||||
pyvenv.cfg
|
||||
tasks.py
|
||||
|
||||
# Package building
|
||||
*.egg-info/
|
||||
.cache/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
venv/
|
||||
*/test/output
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
pip-selfcheck.json
|
||||
pyvenv.cfg
|
||||
wheelhouse/
|
||||
|
||||
# Automatically generated files
|
||||
ocrmypdf/lib/_*.py
|
||||
ocrmypdf/version.py
|
||||
docs/_build/
|
||||
docs/_static/
|
||||
docs/_templates/
|
||||
|
||||
# Code coverage
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Testing
|
||||
log/
|
||||
/*.pdf
|
||||
.ipynb_checkpoints/
|
||||
tests/cache/
|
||||
tests/output/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: python
|
||||
|
||||
cache: pip
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.ccache
|
||||
- packages
|
||||
- tests/cache
|
||||
|
||||
python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
before_install:
|
||||
# Ubuntu packages
|
||||
- sudo add-apt-repository ppa:vshn/ghostscript -y # for ghostscript 9.16 (trusty has 9.10)
|
||||
- sudo add-apt-repository ppa:heyarje/libav-11 -y # for libav11, which is for unpaper
|
||||
- sudo apt-get update -qq # must go after all add-apt-repo
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
|
||||
|
||||
# pip
|
||||
- pip install --upgrade pip
|
||||
|
||||
# Download, make and install unpaper (using ccache)
|
||||
- mkdir -p packages
|
||||
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
|
||||
- sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r test_requirements.txt
|
||||
|
||||
script:
|
||||
- python setup.py clean
|
||||
- python setup.py install
|
||||
- python setup.py test
|
||||
|
||||
os:
|
||||
- linux
|
||||
+44
-40
@@ -1,6 +1,6 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
# VERSION 3.0.0
|
||||
# VERSION 3.2
|
||||
FROM debian:stretch
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
@@ -10,18 +10,36 @@ RUN useradd docker \
|
||||
&& chown docker:docker /home/docker
|
||||
|
||||
# Update system and install our dependencies
|
||||
# If this command takes too Docker hub's automated build will timeout,
|
||||
# so try it in portions
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
locales \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-reportlab \
|
||||
python3-pil
|
||||
python3-pil \
|
||||
python3-wheel
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
unpaper \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
|
||||
|
||||
RUN apt-get install -qy --no-install-recommends \
|
||||
libffi-dev \
|
||||
libpython3-dev \
|
||||
gcc
|
||||
|
||||
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
|
||||
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
|
||||
|
||||
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghostscript/sid
|
||||
|
||||
|
||||
# Enforce UTF-8
|
||||
# Borrowed from https://index.docker.io/u/crosbymichael/python/
|
||||
@@ -30,36 +48,6 @@ RUN dpkg-reconfigure locales && \
|
||||
/usr/sbin/update-locale LANG=C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
# Build unpaper 6.1
|
||||
RUN apt-get install -y \
|
||||
wget \
|
||||
gcc \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
libavutil-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
make \
|
||||
pkg-config \
|
||||
xsltproc
|
||||
|
||||
WORKDIR /root
|
||||
RUN wget https://github.com/Flameeyes/unpaper/archive/unpaper-6.1.tar.gz
|
||||
RUN tar xf unpaper-6.1.tar.gz
|
||||
WORKDIR /root/unpaper-unpaper-6.1
|
||||
RUN autoreconf -i
|
||||
RUN ./configure CFLAGS="-O2 -march=native -pipe -flto"
|
||||
RUN make -j install
|
||||
|
||||
RUN apt-get remove -y \
|
||||
gcc \
|
||||
autoconf \
|
||||
automake \
|
||||
pkg-config \
|
||||
xsltproc \
|
||||
make
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Set up a Python virtualenv and take all of the system packages, so we can
|
||||
# rely on the platform packages rather than importing GCC and compiling them
|
||||
@@ -68,19 +56,35 @@ RUN pyvenv /appenv \
|
||||
|
||||
COPY . /application/
|
||||
|
||||
# Replace stock Tesseract 3.04.00 font with improved sharp2.ttf that resolves
|
||||
# issues in many PDF viewers.
|
||||
# Discussion is in https://github.com/tesseract-ocr/tesseract/issues/182
|
||||
COPY ./share/sharp2.ttf /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
RUN chmod 644 /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
|
||||
# Set this here to force a docker version, allowing non-tagged versions to
|
||||
# be built
|
||||
# ENV SETUPTOOLS_SCM_PRETEND_VERSION=v3.3.0
|
||||
|
||||
# Install application and dependencies
|
||||
# In this arrangement Pillow and reportlab will be provided by the system
|
||||
# Even though ocrmypdf is locally present, pull from PyPI because
|
||||
# Dockerhub and setuptools_scm clash
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install --no-cache-dir /application \
|
||||
&& pip install ocrmypdf \
|
||||
&& pip install --no-cache-dir -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk
|
||||
RUN apt-get remove -qy gcc
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
|
||||
|
||||
USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
ENV DEFAULT_RUFFUS_HISTORY_FILE=/tmp/.{basename}.ruffus_history.sqlite
|
||||
ENV OCRMYPDF_TEST_OUTPUT=/tmp/test-output
|
||||
ENV OCRMYPDF_IN_DOCKER=1
|
||||
ENV OCRMYPDF_SHARP_TTF=1
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# OCRmyPDF polyglot
|
||||
#
|
||||
# VERSION 3.2
|
||||
FROM jbarlow83/ocrmypdf:latest
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
# Update system and install our dependencies
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr-all
|
||||
|
||||
USER docker
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
ENTRYPOINT ["/application/docker-wrapper.sh"]
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2015, The OCRmyPDF Authors
|
||||
Copyright (c) 2013-2016, The OCRmyPDF Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
|
||||
+47
-2
@@ -1,3 +1,48 @@
|
||||
recursive-exclude tests/output *
|
||||
# requirements
|
||||
include requirements.txt
|
||||
include test_requirements.txt
|
||||
include test_requirements.txt
|
||||
include dev_requirements.txt
|
||||
|
||||
# git
|
||||
include .git_archival.txt
|
||||
|
||||
# docker
|
||||
include Dockerfile
|
||||
include Dockerfile.polyglot
|
||||
include .dockerignore
|
||||
|
||||
# tests
|
||||
include pytest.ini
|
||||
recursive-include tests *.jpg
|
||||
recursive-include tests *.pdf
|
||||
recursive-include tests *.py
|
||||
recursive-include tests *.rst
|
||||
recursive-include tests *.txt
|
||||
recursive-exclude tests/output *
|
||||
recursive-exclude tests/output_pageinfo *
|
||||
recursive-exclude tests/resources/private *
|
||||
recursive-exclude tests/cache *
|
||||
|
||||
# documentation
|
||||
include *.rst
|
||||
include pipeline.svg
|
||||
recursive-exclude .github *
|
||||
recursive-include docs *.py
|
||||
recursive-include docs *.rst
|
||||
recursive-include docs *.svg
|
||||
recursive-exclude docs/_build *
|
||||
|
||||
|
||||
# support files
|
||||
recursive-include ocrmypdf/data *
|
||||
recursive-include share *
|
||||
include *.py
|
||||
exclude tasks.py
|
||||
|
||||
# code
|
||||
recursive-include ocrmypdf *.py
|
||||
exclude ocrmypdf/lib/_leptonica.py
|
||||
include OCRmyPDF.sh
|
||||
include docker-wrapper.sh
|
||||
|
||||
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
# Copyright (c) 2013-14: fritz-hh from Github (https://github.com/fritz-hh)
|
||||
##############################################################################
|
||||
|
||||
echo "This script is deprecated. Use 'ocrmypdf' instead."
|
||||
|
||||
python3 -m ocrmypdf.main "$@"
|
||||
|
||||
+54
-180
@@ -2,31 +2,40 @@ OCRmyPDF
|
||||
========
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
be searched or copy-pasted.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf # it's a scriptable command line program
|
||||
-l eng+fra # it supports multiple languages
|
||||
--rotate-pages # it can fix pages that are misrotated
|
||||
--deskew # it can deskew crooked PDFs!
|
||||
--title "My PDF" # it can change output metadata
|
||||
--jobs 4 # it uses multiple cores by default
|
||||
--output-type pdfa # it produces PDF/A by default
|
||||
input_scanned.pdf # takes PDF input (or images)
|
||||
output_searchable.pdf # produces validated PDF output
|
||||
|
||||
|
||||
Main features
|
||||
-------------
|
||||
|
||||
- Generates a searchable
|
||||
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`__ file from a regular PDF
|
||||
only containing images
|
||||
- Places OCRed text accurately below the image to ease copy / paste
|
||||
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`_ file from a regular PDF
|
||||
- Places OCR text accurately below the image to ease copy / paste
|
||||
- Keeps the exact resolution of the original embedded images
|
||||
|
||||
- or if requested oversamples the images before OCRing so as to get
|
||||
better results
|
||||
|
||||
- When possible, copies input images directly to output without transcoding them,
|
||||
to preserve image quality
|
||||
- When possible, inserts OCR information as a "lossless" operation without rendering vector information
|
||||
- Keeps file size about the same
|
||||
- If requested deskews and/or cleans the image before performing OCR
|
||||
- Validates input and output files
|
||||
- Provides debug mode to enable easy verification of the OCR results
|
||||
- Processes several pages in parallel when more than one CPU core is
|
||||
- Processes pages in parallel when more than one CPU core is
|
||||
available
|
||||
- Uses Tesseract OCR engine
|
||||
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`_ engine
|
||||
- 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 `release notes <RELEASE_NOTES.rst>`_.
|
||||
|
||||
Motivation
|
||||
----------
|
||||
@@ -35,208 +44,73 @@ I searched the web for a free command line tool to OCR PDF files on
|
||||
Linux/UNIX: I found many, but none of them were really satisfying.
|
||||
|
||||
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
||||
- Or they did not display correctly some escaped HTML characters located in the hOCR file produced by the OCR engine
|
||||
- Or they did not handle accents and multilingual characters
|
||||
- Or they changed the resolution of the embedded images
|
||||
- Or they generated PDF files having a ridiculous big size
|
||||
- Or they generated ridiculously large PDF files
|
||||
- Or they crashed when trying to OCR some of my PDF files
|
||||
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
|
||||
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
|
||||
|
||||
... so I decided to develop my own tool (using various existing scripts
|
||||
as an inspiration)
|
||||
...so I decided to develop my own tool (using various existing scripts
|
||||
as an inspiration).
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
|
||||
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
|
||||
|
||||
You can install it to a Python virtual environment or system-wide.
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
``apt-get install ocrmypdf``.
|
||||
|
||||
Installing the Docker container
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
|
||||
|
||||
For many users, installing the Docker container will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
|
||||
Languages
|
||||
---------
|
||||
|
||||
If you have `Docker <https://docs.docker.com/>`__ installed on your system, you can install
|
||||
a Docker container of the latest release.
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
|
||||
you can often find packages that provide language packs:
|
||||
|
||||
Follow the Docker installation instructions for your platform. If you can run this command
|
||||
successfully, your system is ready to download and execute the image::
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
# Display a list of all Tesseract language packs
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker container host, and then follow these optional steps to enable multiple CPUs::
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
||||
languages can be requested.
|
||||
|
||||
# Optional
|
||||
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")
|
||||
Documentation and support
|
||||
-------------------------
|
||||
|
||||
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
||||
the image::
|
||||
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
Then tag it to give a more convenient name, just ocrmypdf::
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
|
||||
You can then run using the command::
|
||||
|
||||
docker run ocrmypdf --help
|
||||
|
||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`__, such as this in this template::
|
||||
|
||||
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
|
||||
In this worked example, the current working directory contains an input file called `test.pdf` and the output will go to `output.pdf`::
|
||||
|
||||
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
Note that `ocrmypdf` has its own separate -v argument to control debug verbosity. All Docker arguments should before the `ocrmypdf` container name and all arguments to `ocrmypdf` should be listed after.
|
||||
|
||||
Installing on Mac OS X Yosemite
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If it's not already present, `install Homebrew <http://brew.sh/>`__
|
||||
|
||||
Update Homebrew::
|
||||
|
||||
brew update
|
||||
|
||||
Install or upgrade the required Homebrew packages, if any are missing::
|
||||
|
||||
brew install libpng openjpeg jbig2dec # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2
|
||||
brew install leptonica
|
||||
brew install tesseract
|
||||
|
||||
It is also recommended that install Pillow and confirm it can read and write JPEG and PNG files::
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
To test that your Python imaging library (Pillow) can access JPEG and PNG files, try this command::
|
||||
|
||||
python3 -c "from PIL import Image; im = Image.new('1', (1, 1)); im.save('test.png'); im.save('test.jpg')"
|
||||
|
||||
If you have trouble getting Pillow to access JPEG and PNG files, `review the installation instructions <https://pillow.readthedocs.org/installation.html>`__.
|
||||
|
||||
You can then install OCRmyPDF from PyPI::
|
||||
|
||||
pip3 install ocrmypdf
|
||||
|
||||
The command line program should now be available::
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
Installing on Ubuntu 14.04 LTS
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Our `documentation is served on Read the Docs <https://ocrmypdf.readthedocs.io/en/latest/index.html>`_.
|
||||
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than other options, because of certain bugs in package installation.
|
||||
If you detect an issue, please:
|
||||
|
||||
Update apt-get::
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
|
||||
Install system dependencies::
|
||||
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
unpaper \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs)::
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate system Python from modified, 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>`__::
|
||||
|
||||
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
|
||||
pyvenv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
|
||||
Ubuntu 14.04 only installs `unpaper` version 0.4.2, which is not supported by OCRmyPDF because it is produces invalid output. This program is an optional dependency, and provides page deskewing and cleaning. See `Dockerfile <Dockerfile>`__ for an example of how to building unpaper 6.1 from source. If you choose to install unpaper later, OCRmyPDF will use the foremost version on the system PATH.
|
||||
|
||||
|
||||
Installing HEAD revision from sources
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To install the HEAD revision from sources in development mode::
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
cd OCRmyPDF
|
||||
pip3 install -e .
|
||||
|
||||
On certain Linux/UNIX platforms such as Ubuntu, you may need to use
|
||||
run the install command as superuser::
|
||||
|
||||
sudo pip3 install -e .
|
||||
|
||||
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::
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
pyvenv venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install -e .
|
||||
|
||||
If your platform does not have ``pip3``, make sure that Python 3.4+ and the `pip`
|
||||
package are installed.
|
||||
|
||||
To run the program::
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
If not yet installed, the script will notify you about dependencies that
|
||||
need to be installed. The script requires specific versions of the
|
||||
dependencies. Older version than the ones mentioned in the release notes
|
||||
are likely not to be compatible to OCRmyPDF.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
In case you detect an issue, please:
|
||||
|
||||
- Check if your issue is already known
|
||||
- Check whether your issue is already known
|
||||
- If no problem report exists on github, please create one here:
|
||||
https://github.com/fritz-hh/OCRmyPDF/issues
|
||||
https://github.com/jbarlow83/OCRmyPDF/issues
|
||||
- Describe your problem thoroughly
|
||||
- Append the console output of the script when running the debug mode
|
||||
(-v 1 option)
|
||||
(``-v 1`` option)
|
||||
- If possible provide your input PDF file as well as the content of the
|
||||
temporary folder (using a file sharing service like
|
||||
www.file-upload.net)
|
||||
temporary folder (using a file sharing service like Dropbox)
|
||||
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
- `c't 1-2014, page 59 <http://www.heise.de/ct/inhalt/2014/1/58/>`__:
|
||||
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
|
||||
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
|
||||
magazine c't
|
||||
- `heise Open Source, 09/2014: Texterkennung mit
|
||||
OCRmyPDF <http://www.heise.de/-2356670>`__
|
||||
OCRmyPDF <http://heise.de/-2356670>`_
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
+278
-361
@@ -1,9 +1,281 @@
|
||||
RELEASE NOTES
|
||||
=============
|
||||
|
||||
Please always read this file before installing the package
|
||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
||||
|
||||
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
|
||||
|
||||
v4.3:
|
||||
=====
|
||||
|
||||
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
|
||||
- Better documentation
|
||||
- Fixed an issue with PDFs that draw images when the raster stack depth is zero
|
||||
- ocrmypdf can now redirect its output to stdout for use in a shell pipeline
|
||||
|
||||
+ This does not improve performance since temporary files are still used for buffering
|
||||
+ Some output validation is disabled in this mode
|
||||
|
||||
v4.2.5:
|
||||
=======
|
||||
|
||||
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
|
||||
- Removed non-free file milk.pdf
|
||||
|
||||
|
||||
v4.2.4:
|
||||
=======
|
||||
|
||||
- Fixed an error (#90) caused by PDFs that use stencil masks properly
|
||||
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
|
||||
|
||||
v4.2.3:
|
||||
=======
|
||||
|
||||
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
|
||||
- Integrated a few fixes to simplify downstream packaging (Debian)
|
||||
|
||||
+ The test suite no longer assumes it is installed
|
||||
+ If running Linux, skip a test that passes Unicode on the command line
|
||||
|
||||
- Added a test case to check explicit masks and stencil masks
|
||||
- Added a test case for indirect objects and linearized PDFs
|
||||
- Deprecated the OCRmyPDF.sh shell script
|
||||
|
||||
|
||||
v4.2.2:
|
||||
=======
|
||||
|
||||
- Improvements to documentation
|
||||
|
||||
|
||||
v4.2.1:
|
||||
=======
|
||||
|
||||
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
|
||||
- Implemented stdin streaming
|
||||
|
||||
|
||||
v4.2:
|
||||
=====
|
||||
|
||||
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
|
||||
|
||||
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
|
||||
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
|
||||
|
||||
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
|
||||
|
||||
+ ``pdfa`` is the default, consistent with past behavior
|
||||
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
|
||||
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
|
||||
|
||||
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
|
||||
- ``--force-ocr`` rasterizes pages even if they contain no images
|
||||
|
||||
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
|
||||
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
|
||||
+ fixes issue #82
|
||||
|
||||
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
|
||||
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
|
||||
|
||||
+ Some Ubuntu "PPAs" (backports) are needed to make it work
|
||||
|
||||
- Support for some older dependencies dropped
|
||||
|
||||
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
|
||||
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
|
||||
|
||||
- Ghostscript now runs in "safer" mode where possible
|
||||
|
||||
v4.1.4:
|
||||
=======
|
||||
|
||||
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
|
||||
|
||||
|
||||
v4.1.3:
|
||||
=======
|
||||
|
||||
- More helpful error message for PDFs with version 4 security handler
|
||||
- Update usage instructions for Windows/Docker users
|
||||
- Fix order of operations for matrix multiplication (no effect on most users)
|
||||
- Add a few leptonica wrapper functions (no effect on most users)
|
||||
|
||||
|
||||
v4.1.2:
|
||||
=======
|
||||
|
||||
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
|
||||
- More helpful error message for an error related to certain types of malformed PDFs
|
||||
|
||||
|
||||
v4.1:
|
||||
=====
|
||||
|
||||
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
|
||||
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
|
||||
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
|
||||
|
||||
|
||||
v4.0.7:
|
||||
=======
|
||||
|
||||
- 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
|
||||
- DPI is now calculated correctly for cropped images, along with other image transformations
|
||||
- Inline images are now checked during DPI calculation instead of rejecting the image
|
||||
|
||||
v4.0.4:
|
||||
=======
|
||||
|
||||
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
|
||||
|
||||
|
||||
v4.0.3:
|
||||
=======
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Page orientations detected are now reported in a summary comment
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Show stack trace if unexpected errors occur
|
||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||
abort the file
|
||||
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
||||
|
||||
|
||||
v4.0.2:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
- Improved handling of Tesseract errors and crashes
|
||||
- Fixed use of chmod on Docker that broke most test cases
|
||||
|
||||
|
||||
v4.0.1:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
|
||||
v4.0:
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
|
||||
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
|
||||
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
|
||||
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
|
||||
at image deskewing than unpaper.
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
|
||||
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
|
||||
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
|
||||
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Logging output is now much easier to read
|
||||
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
|
||||
- libffi is now required
|
||||
- Some changes were made to the Docker and Travis build environments to support libffi
|
||||
- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01,
|
||||
the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00.
|
||||
You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder
|
||||
to correct the problem.
|
||||
|
||||
|
||||
v3.2.1:
|
||||
=======
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
|
||||
- Tweaked the Dockerfiles
|
||||
|
||||
|
||||
v3.2:
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- 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
|
||||
of vector and raster content would see the vector content preserved. Images may still be transcoded
|
||||
during PDF/A conversion. (``--deskew`` and ``--clean-final`` disable this mode, necessarily.)
|
||||
- New argument ``--tesseract-pagesegmode`` allows you to pass page segmentation arguments to Tesseract OCR.
|
||||
This helps for two column text and other situations that confuse Tesseract.
|
||||
- Added a new "polyglot" version of the Docker image, that generates Tesseract with all languages packs installed,
|
||||
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 OS X El Capitan are now supported platforms - no changes were
|
||||
needed to implement support
|
||||
- Improved some error messages related to missing input files
|
||||
- Fixed issue #20 - uppercase .PDF extension not accepted
|
||||
- Fixed an issue where OCRmyPDF failed to text that certain pages contained previously OCR'ed text,
|
||||
such as OCR text produced by Tesseract 3.04
|
||||
- Inserts /Creator tag into PDFs so that errors can be traced back to this project
|
||||
- Added new option ``--pdf-renderer=auto``, to let OCRmyPDF pick the best PDF renderer.
|
||||
Currently it always chooses the 'hocrtransform' renderer but that behavior may change.
|
||||
- Set up Travis CI automatic integration testing
|
||||
|
||||
v3.0:
|
||||
=====
|
||||
@@ -36,6 +308,8 @@ 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
|
||||
- OCR text is now rendered in the PDF as invisible text. Previous versions of OCRmyPDF
|
||||
incorrectly rendered visible text with an image on top.
|
||||
- All "tasks" in the pipeline can be executed in parallel on any
|
||||
available CPUs, increasing performance
|
||||
- The ``-o DPI`` argument has been phased out, in favor of ``--oversample DPI``, in
|
||||
@@ -156,363 +430,6 @@ Notes and known issues
|
||||
v2.2-stable (2014-09-29):
|
||||
=========================
|
||||
|
||||
New features
|
||||
------------
|
||||
OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions.
|
||||
|
||||
- None
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Update to jhove v1.11
|
||||
- Request the python library reportlab v3.0 or newer (So that we could remove a patch to the previous version of reportlab leading to issues for some users)
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fix bug on Mac OS X (resolution of simlink to OCRmyPDF.sh script) (thanks to jbarlow83)
|
||||
- Check if the input pdf file exists before to continue
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.2
|
||||
- Dependencies:
|
||||
|
||||
- parallel 20140822
|
||||
- poppler-utils 0.24.5
|
||||
- ImageMagick 6.8.9-4 2014-09-17
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.8
|
||||
- ghostcript (gs): 9.06
|
||||
- java: openjdk version "1.7.0_65"
|
||||
|
||||
|
||||
v2.1-stable (2014-09-20):
|
||||
=========================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- None
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- None
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Allow execution via simlink
|
||||
- Add support for tesseract 3.03
|
||||
- Add support for newer version of reportlab
|
||||
- Lowered minimum version of gnu parallel
|
||||
- Various typo
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- parallel 20130222
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v2.0-stable (2014-01-25):
|
||||
=========================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Check if the language(s) passed using the -l option is supported by
|
||||
tesseract (fixes #60)
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Allow OCRmyPDF to be used with tesseract 3.02.01, even though OCR
|
||||
might fail for few PDF file (see issue #28). Rationale: For some
|
||||
linux distribution, no newer version than tesseract 3.02.01 is
|
||||
available
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- More robust algorithm for checking the version of the installed
|
||||
tesseract package
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- parallel 20130222
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v2.0-rc2 (2014-01-16):
|
||||
======================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- None
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Size reduction of final PDF file: (fixes #50)
|
||||
- Support for monochrome (Black&White) images (massive size reduction
|
||||
in final PDF: >80%)
|
||||
- Reduced size of grayscale images (by 13% on test PDF file)
|
||||
- Preventing fi, fl ligatures does not require anymore to pass an
|
||||
additional config file to tesseract using the -C option (fixes #58)
|
||||
- Location of temporary folder according to content of environment
|
||||
variable TMPDIR.
|
||||
- Dependency to pdftk removed
|
||||
- Check for compatible versions of dependencies: (fixes #51)
|
||||
- parallel and tesseract
|
||||
- python libraries reportlab and lxml
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Improved portability with various shells (dash, bash, tcsh) and OS
|
||||
(FreeBSD, MAC OSX, Linux) (fixes #59)
|
||||
- Corrected bug in case the input PDF file contains a space character
|
||||
(fixes #48)
|
||||
- Prevent spurious error message in case there is no image in a PDF
|
||||
page
|
||||
- Prevent collision of temporary folder names (fixes #57)
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- parallel 20130222
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v2.0-rc1 (2014-01-07):
|
||||
======================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Huge performance improvement on machines having multiple CPU/cores
|
||||
(processing of several pages concurrently) (fixes #18)
|
||||
- By default prevent from processing a PDF file already containing
|
||||
fonts (i.e. text)(it can be overridden with the -f flag) (fixes #16)
|
||||
- Warn if the resolution is too low to get reasonable OCR results
|
||||
(fixes #37)
|
||||
- New option (-o) to perform automatic oversampling if the image
|
||||
resolution is too low. This can improve OCR results.
|
||||
- Warn if using a tesseract version older than v3.02.02 (as older
|
||||
versions are known to produce invalid output) (fixes #41)
|
||||
- Echo version of the installed dependencies (e.g. tesseract) in debug
|
||||
mode in order to ease support (fixes #35)
|
||||
- Echo the arguments passed to the script in debug mode to ease support
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- In debug mode: The debug page is now placed after the respective
|
||||
"normal" page
|
||||
- Reduced disk space usage in temporary folder if -d (deskew) or -c
|
||||
(cleanup) options are not selected
|
||||
- New file src/config.sh containing various configuration parameters
|
||||
- Documentation of the tesseract config file "tess-cfg/no\_ligature"
|
||||
improved
|
||||
- Improved consistency of the temporary file names
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Improved robustness:
|
||||
- in case vertical resolution differs from horizontal resolution (fixes
|
||||
#38)
|
||||
- in case a PDF page contains more than one image (fixes #36)
|
||||
- Fix a problem occurring if python 3 is the standard interpreter
|
||||
(fixes #33)
|
||||
- Fix a problem occurring if the input PDF file contains special
|
||||
characters like "#" (fixes #34)
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- parallel 20130222
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- pdftk 1.45
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v1.1-stable (2014-01-06):
|
||||
=========================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- N/A
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- N/A
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed syntax error (bashism) leading to an error message on certain
|
||||
systems (fixes #42)
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- pdftk 1.45
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v1.0-stable (2013-05-06):
|
||||
=========================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- In debug mode: compute and echo time required for processing (fixes
|
||||
#26)
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Removed feature to add metadata in final pdf file (because it lead to
|
||||
to final PDF file that does not comply to the PDF/A-1 format)
|
||||
- Removed feature to set same owner & permissions in final PDF file
|
||||
than in input file
|
||||
- Removed many unused jhove files (e.g. documentation, \*.java and
|
||||
\*.class files)
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Correction to handle correctly path and input PDF files having spaces
|
||||
(fixes #31)
|
||||
- Resolutions (x/y) that are nearly equal are now supported (fixes #25)
|
||||
- Fix compatibility issue with Ubuntu server 12.04 / Ubuntu server
|
||||
10.04 / Linux Mint 13 Maya and probably other Linux distributions
|
||||
(fixes #27)
|
||||
- Commit missing jhove files (\*.jar mainly) due to wrong .gitignore
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- pdftk 1.45
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v1.0-rc2 (2013-04-29):
|
||||
======================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Keep temporary files if debug mode is set (fixes #22)
|
||||
- Set same owner & permissions in final PDF file than in input file
|
||||
(fixes #9)
|
||||
- Added metadata in final pdf file (fixes #4)
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- N/A
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed wrong image cropping when deskew option is activated
|
||||
- Exit with error message if page size is not found in hocr file (fixes
|
||||
#21)
|
||||
- Various minor fixes in log messages
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- pdftk 1.45
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
|
||||
v1.0-rc1 (2013-04-26):
|
||||
======================
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- First release candidate
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- N/A
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- N/A
|
||||
|
||||
Tested with
|
||||
-----------
|
||||
|
||||
- Operating system: FreeBSD 9.1
|
||||
- Dependencies:
|
||||
- poppler-utils 0.22.2
|
||||
- ImageMagick 6.8.0-7 2013-03-30
|
||||
- Unpaper 0.3
|
||||
- tesseract 3.02.02
|
||||
- Python 2.7.3
|
||||
- pdftk 1.45
|
||||
- ghoscript (gs): 9.06
|
||||
- java: openjdk version "1.7.0\_17"
|
||||
For details on older versions, see the `final version of its release notes <https://github.com/fritz-hh/OCRmyPDF/blob/7fd3dbdf42ca53a619412ce8add7532c5e81a9d1/RELEASE_NOTES.md>`_.
|
||||
@@ -0,0 +1,4 @@
|
||||
check-manifest>=0.33
|
||||
setuptools-scm>=1.11.1
|
||||
twine>=1.8.1
|
||||
coverage>=4.2
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 915 585" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="b" y2="445" gradientUnits="userSpaceOnUse" y1="179" gradientTransform="translate(0 -2.06)" x2="-29.7" x1="322">
|
||||
<stop stop-color="#333" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="a" y2="414" gradientUnits="userSpaceOnUse" y1="159" x2="490" x1="815">
|
||||
<stop stop-color="#33f" offset="0"/>
|
||||
<stop stop-color="#3f3fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#b)" d="m403 247c-12 115-135 122-368 123-4.3-1.07-7.32-7.33-6-41 76-37 151-124 167-236 93 123 201 40.9 207 154z"/>
|
||||
<g stroke-width="3.45" fill="none">
|
||||
<path stroke="#000" d="m11.8 11.8h411v411l-411 0.01v-411z"/>
|
||||
<path stroke="#448" d="m489 11.7h415v411h-415v-411z"/>
|
||||
</g>
|
||||
<path d="m876 244c-12 115-133 120-366 121-6-14-10-40-3-43 76-37.3 136-106 152-218 38 48 209 101 217 140z" fill="url(#a)"/>
|
||||
<g id="RasterLarge" transform="matrix(1.36 0 0 1.28 -161 -636)">
|
||||
<path fill="#999" d="m287 730h-30v-60h20v20h10v10h10v10h20v10h40v-10h20v-20h10v-20h-10v-10h-10v-10h-40v-10h-30v-10h-20v-10h-10v-10h-10v-60h10v-10h10v-10h10v-10h90v10h30v60h-20v-10h-10v-20h-10v-10h-20v-10h-30v10h-20v10h-10v20h10v20h30v10h30v10h30v10h10v10h10v10h10v60h-10v10h-10v10h-10v10h-100v-10z"/>
|
||||
<path fill="#555" d="m297 730h-30v-10h-10v-50h20v20h10v20h10 10v10h60v-10h10v-10h10v-40h-20v-10h-20v-10h-40v-10h-20v-10h-20v-20h-10v-50h10v-10h10v-10h20v-10h70v10h30v10h10v50h-20v-20h-10v-20h-20v-10h-50v10h-10v10h-10v30h10v10h20v10h30v10h30v10h20v10h10v10h10v50h-10v20h-10v10h-20v10h-80z"/>
|
||||
<path d="m307 730h-30v-10h-20v-50h20v30h10v10h10v10h70v-10h20v-50h-10v-10h-20v-10h-40v-10h-30v-10h-10v-10h-10v-20h-10v-30h10v-20h20v-10h20v-10h50v10h30v10h20v50h-20v-30h-10v-10h-10v-10h-70v10h-10v40h10v10h10v10h30v10h40v10h20v10h10v20h10v40h-10v20h-20v10h-30v10h-50v-10z"/>
|
||||
</g>
|
||||
<g font-size="40" font-family="sans-serif" text-anchor="middle">
|
||||
<g font-size="100">
|
||||
<text y="518" x="210">Raster</text>
|
||||
<text y="518" x="695" fill="#338">Vector</text>
|
||||
</g>
|
||||
<text y="563" x="210">.jpeg .gif .png</text>
|
||||
<text y="563" x="696" fill="#338">.svg</text>
|
||||
</g>
|
||||
<path id="VectorLarge" fill="#005" d="m661 294v-62.5l23.4 0.184c0.678 20.8 7.32 36.3 19.9 46.4 12.7 9.93 32.1 14.9 57.9 14.9 24.1 0 42.5-4.29 55.1-12.9 12.7-8.71 19.1-21.3 19.1-37.9 0-13.2-3.86-23.4-11.6-30.5-7.59-7.11-23.7-14-48.4-20.8l-40.1-10.9c-29-7.97-49.5-17.9-61.4-29.8-11.8-11.9-17.7-28.2-17.7-48.9 0-23.3 9.15-41.4 27.4-54.3 18.3-12.9 44-19.3 77.1-19.3 14.1 0 29.6 1.41 46.4 4.23 16.8 2.7 34.7 6.68 53.7 12v58.5h-23c-2.3-19.4-9.49-33.4-21.6-41.9-11.9-8.71-30.2-13.1-54.7-13.1-21.4 0-37.7 3.99-49 12-11.1 7.85-16.7 19.3-16.7 34.4 0 13.1 4.2 23.4 12.6 30.9 8.4 7.48 26.2 14.9 53.5 22.3l37.6 10.1c27.5 7.48 47.1 17 58.8 28.7 11.8 11.5 17.7 27 17.7 46.5 0 26.6-9.42 46.7-28.3 60.2s-46.9 20.2-84.2 20.2c-16.7 0-33.7-1.53-51-4.6-18-3-35-7-53-13z"/>
|
||||
<use xlink:href="#VectorLarge" transform="matrix(.17 0 0 .17 392 313)"/>
|
||||
<use xlink:href="#RasterLarge" transform="matrix(.173 0 0 .173 -8.25 314)" height="100%" width="100%" y="0" x="0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
+350
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ocrmypdf documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Sep 4 14:29:43 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.mathjax',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'ocrmypdf'
|
||||
copyright = '2016, James R. Barlow'
|
||||
author = 'James R. Barlow'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
from ocrmypdf import VERSION as OCRMYPDF_VERSION
|
||||
|
||||
_version_parts = OCRMYPDF_VERSION.split('.')
|
||||
|
||||
version = '.'.join(_version_parts[0:2])
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = OCRMYPDF_VERSION
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#
|
||||
# today = ''
|
||||
#
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#
|
||||
today_fmt = '%Y-%m-%d'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
html_theme_options = {
|
||||
'display_version': False
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents.
|
||||
# "<project> v<release> documentation" by default.
|
||||
#
|
||||
# html_title = 'ocrmypdf v4.2'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#
|
||||
# html_extra_path = []
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# bottom, using the given strftime format.
|
||||
# The empty string is equivalent to '%b %d, %Y'.
|
||||
#
|
||||
# html_last_updated_fmt = None
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
|
||||
#
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# 'ja' uses this config value.
|
||||
# 'zh' user can custom change `jieba` dictionary path.
|
||||
#
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'ocrmypdfdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation',
|
||||
'James R. Barlow', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# latex_appendices = []
|
||||
|
||||
# It false, will not define \strong, \code, itleref, \crossref ... but only
|
||||
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
|
||||
# packages.
|
||||
#
|
||||
# latex_keep_old_macro_names = True
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
|
||||
author, 'ocrmypdf', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#
|
||||
# texinfo_no_detailmenu = False
|
||||
@@ -0,0 +1,139 @@
|
||||
Cookbook
|
||||
========
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
|
||||
Help!
|
||||
"""""
|
||||
|
||||
ocrmypdf has built-in help.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
|
||||
Add an OCR layer and convert to PDF/A
|
||||
"""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf input.pdf output.pdf
|
||||
|
||||
Add an OCR layer and output a standard PDF
|
||||
""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
|
||||
Modify a file in place
|
||||
""""""""""""""""""""""
|
||||
|
||||
The file will only be overwritten if OCRmyPDF is successful.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf myfile.pdf myfile.pdf
|
||||
|
||||
Correct page rotation
|
||||
"""""""""""""""""""""
|
||||
|
||||
OCR will attempt to automatic correct the rotation of each page. This can help fix a scanning job that contains a mix of landscape and portrait pages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
|
||||
|
||||
You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive.
|
||||
|
||||
|
||||
OCR images, not PDFs
|
||||
--------------------
|
||||
|
||||
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the resutls to run ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
|
||||
|
||||
If given a single image as input, OCRmyPDF will try converting it to a PDF on its own. This feature may be removed at some point, because OCRmyPDF does not specialize in converting images to PDFs.
|
||||
|
||||
You can also use Tesseract 3.04+ directly to convert single page images or multi-page TIFFs to PDF:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tesseract my-image.jpg output-prefix pdf
|
||||
|
||||
Image processing
|
||||
----------------
|
||||
|
||||
OCRmyPDF perform some image processing on each page of a PDF, if desired. The same processing is applied to each page. It is suggested that the user review files after image processing as these commands might remove desirable content, especially from poor quality scans.
|
||||
|
||||
* ``--rotate-pages`` attempts to determine the correct orientation for each page and rotates the page if necessary.
|
||||
|
||||
* ``--remove-background`` attempts to detect and remove a noisy background from grayscale or color images. Monochrome images are ignored. This should not be used on documents that contain color photos as it may remove them.
|
||||
|
||||
* ``--deskew`` will correct pages were scanned at a skewed angle by rotating them back into place. Skew determination and correction is performed using `Postl's variance of line sums <http://www.leptonica.com/skew-measurement.html>`_ algorithm as implemented in `Leptonica <http://www.leptonica.com/index.html>`_.
|
||||
|
||||
* ``--clean`` uses `unpaper <https://www.flameeyes.eu/projects/unpaper>`_ to clean up pages before OCR, but does not alter the final output. This makes it less likely that OCR will try to find text in background noise.
|
||||
|
||||
* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important.
|
||||
|
||||
|
||||
OCR and correct document skew (crooked scan)
|
||||
""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --deskew input.pdf output.pdf
|
||||
|
||||
|
||||
Hot (watched) folders
|
||||
---------------------
|
||||
|
||||
To set up a "hot folder" that will trigger an OCR operation for every file inserted, use a program like Python `watchdog <https://pypi.python.org/pypi/watchdog>`_ (supports all major OS).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install watchdog
|
||||
|
||||
watchdog installs the command line program ``watchmedo``, which can be told to run ``ocrmypdf`` on any .pdf added to the current directory (``.``) and place the result in the previously created ``out/`` folder.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd hot-folder
|
||||
mkdir out
|
||||
watchmedo shell-command \
|
||||
--patterns="*.pdf" \
|
||||
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
||||
. # don't forget the final dot
|
||||
|
||||
For more complex behavior you can write a Python script around to use the watchdog API.
|
||||
|
||||
On file servers, you could configure watchmedo as a system service so it will run all the time.
|
||||
|
||||
|
||||
Caveats
|
||||
"""""""
|
||||
|
||||
* ``watchmedo`` may not work properly on a networked file system, depending on the capabilities of the file system client and server.
|
||||
* This simple recipe does not filter for the type of file system event, so file copies, deletes and moves, and directory operations, will all be sent to ocrmypdf, producing errors in several cases. Disable your watched folder if you are doing anything other than copying files to it.
|
||||
|
||||
|
||||
Batch jobs
|
||||
----------
|
||||
|
||||
Consider using the excellent `GNU Parallel <https://www.gnu.org/software/parallel/>`_ to apply OCRmyPDF to multiple files at once.
|
||||
|
||||
Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once.
|
||||
|
||||
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
parallel -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
|
||||
|
||||
If you have thousands of files to work with, contact the author.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
Common error messages
|
||||
=====================
|
||||
|
||||
Page already has text
|
||||
---------------------
|
||||
|
||||
.. code::
|
||||
|
||||
ERROR - 1: page already has text! – aborting (use --force-ocr to force OCR)
|
||||
|
||||
You ran ocrmypdf on a file that already contains printable text or a hidden OCR text layer (it can't quite tell the difference). You probably don't want to do this, because the file is already searchable.
|
||||
|
||||
As the error message suggests, your options are:
|
||||
|
||||
- ``ocrmypdf --force-ocr`` to :ref:`rasterize <raster-vector>` all vector content and run OCR on the images. This is useful if a previous OCR program failed, or if the document contains a text watermark.
|
||||
|
||||
- ``ocrmypdf --skip-text`` to skip OCR and other processing on any pages that contain text. Text pages will be copied into the output PDF without modification.
|
||||
|
||||
|
||||
Input file 'filename' is not a valid PDF
|
||||
----------------------------------------
|
||||
|
||||
OCRmyPDF passes files through qpdf, a program that fixes errors in PDFs, before it tries to work on them. In most cases this happens because the PDF is corrupt and
|
||||
truncated (incomplete file copying) and not much can be done.
|
||||
|
||||
You can try rewriting the file with Ghostscript or pdftk:
|
||||
|
||||
- ``gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf``
|
||||
|
||||
- ``pdftk input.pdf cat output output.pdf``
|
||||
|
||||
Sometimes Acrobat can repair PDFs with its `Preflight tool <https://helpx.adobe.com/acrobat/using/correcting-problem-areas-preflight-tool.html>`_.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.. ocrmypdf documentation master file, created by
|
||||
sphinx-quickstart on Sun Sep 4 14:29:43 2016.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
OCRmyPDF documentation
|
||||
======================
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
|
||||
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
introduction
|
||||
installation
|
||||
languages
|
||||
cookbook
|
||||
errors
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Installing on Debian and Ubuntu
|
||||
-------------------------------
|
||||
|
||||
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
apt-get install ocrmypdf
|
||||
|
||||
.. _Docker:
|
||||
|
||||
Installing the Docker image
|
||||
---------------------------
|
||||
|
||||
For many users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
|
||||
|
||||
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install
|
||||
a Docker image of the latest release.
|
||||
|
||||
Follow the Docker installation instructions for your platform. If you can run this command
|
||||
successfully, your system is ready to download and execute the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
Assuming you have a Docker engine running somewhere, you can run these commands to download
|
||||
the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
Then tag it to give a more convenient name, just ocrmypdf:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
|
||||
.. _docker-polyglot:
|
||||
|
||||
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Alternative step: If you need all language packs
|
||||
docker pull jbarlow83/ocrmypdf-polyglot
|
||||
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
|
||||
|
||||
You can then run ocrmypdf using the command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm ocrmypdf --help
|
||||
|
||||
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
|
||||
|
||||
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
|
||||
|
||||
|
||||
Installing on macOS (formerly Mac OS X)
|
||||
---------------------------------------
|
||||
|
||||
These instructions probably work on all macOS supported by Homebrew. OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
|
||||
|
||||
If it's not already present, `install Homebrew <http://brew.sh/>`_.
|
||||
|
||||
Update Homebrew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew update
|
||||
|
||||
Install or upgrade the required Homebrew packages, if any are missing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install libpng openjpeg jbig2dec libtiff # image libraries
|
||||
brew install qpdf
|
||||
brew install ghostscript
|
||||
brew install python3
|
||||
brew install libxml2 libffi leptonica
|
||||
brew install unpaper # optional
|
||||
|
||||
Install the required Tesseract OCR engine with the language packs you plan to use:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract # Option 1: for English, French, German, Spanish
|
||||
|
||||
.. _macos-all-languages:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
||||
|
||||
Update the homebrew pip and install Pillow:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade pillow
|
||||
|
||||
You can then install OCRmyPDF from PyPI:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
|
||||
The command line program should now be available:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
Installing on Ubuntu 14.04 LTS
|
||||
------------------------------
|
||||
|
||||
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
|
||||
|
||||
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo add-apt-repository ppa:vshn/ghostscript -y
|
||||
sudo add-apt-repository ppa:heyarje/libav-11 -y
|
||||
|
||||
Update apt-get:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
|
||||
Install system dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install \
|
||||
zlib1g-dev \
|
||||
libjpeg-dev \
|
||||
libffi-dev \
|
||||
libavformat56 libavcodec56 libavutil54 \
|
||||
ghostscript \
|
||||
tesseract-ocr \
|
||||
qpdf \
|
||||
python3-pip \
|
||||
python3-pil \
|
||||
python3-pytest \
|
||||
python3-reportlab
|
||||
|
||||
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
|
||||
into your system Python, which could interfere with other programs):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install ocrmypdf
|
||||
|
||||
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
|
||||
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
|
||||
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python3-venv
|
||||
python3 -m venv venv-ocrmypdf --without-pip
|
||||
source venv-ocrmypdf/bin/activate
|
||||
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
|
||||
deactivate
|
||||
python3 -m venv --system-site-packages venv-ocrmypdf
|
||||
source venv-ocrmypdf/bin/activate
|
||||
pip install ocrmypdf
|
||||
|
||||
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
|
||||
sudo dpkg -i unpaper_6.1-1.deb
|
||||
|
||||
|
||||
Installing on Windows
|
||||
---------------------
|
||||
|
||||
Direct installation on Windows is not possible. Install the _`Docker` container as described above. Ensure that your command prompt can run the docker "hello world" container.
|
||||
|
||||
Running on Windows
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The command line syntax to run ocrmypdf from a command prompt will resemble:
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
|
||||
|
||||
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
|
||||
|
||||
Installing HEAD revision from sources
|
||||
-------------------------------------
|
||||
|
||||
If you have ``git`` and ``python3.4`` or ``python3.5`` installed, you can install from source. When the ``pip`` installer runs,
|
||||
it will alert you if dependencies are missing.
|
||||
|
||||
To install the HEAD revision from sources in the current Python 3 environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
On certain Linux distributions such as Ubuntu, you may need to use
|
||||
run the install command as superuser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
|
||||
|
||||
Note that this will alter your system's Python distribution. If you prefer
|
||||
to not install as superuser, you can install the package in a Python virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
|
||||
However, ``ocrmypdf`` will only be accessible on the system PATH after
|
||||
you activate the virtual environment.
|
||||
|
||||
To run the program:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
If not yet installed, the script will notify you about dependencies that
|
||||
need to be installed. The script requires specific versions of the
|
||||
dependencies. Older version than the ones mentioned in the release notes
|
||||
are likely not to be compatible to OCRmyPDF.
|
||||
@@ -0,0 +1,71 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
|
||||
|
||||
|
||||
About OCR
|
||||
---------
|
||||
|
||||
`Optical character recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`_ is technology that converts images of typed or handwritten text, such as in a scanned document, to computer text that can be searched and copied.
|
||||
|
||||
OCRmyPDF uses `Tesseract <https://github.com/tesseract-ocr/tesseract>`_, the best available open source OCR engine, to perform OCR.
|
||||
|
||||
.. _raster-vector:
|
||||
|
||||
About PDFs
|
||||
----------
|
||||
|
||||
PDFs are page description files that attempts to preserve a layout exactly. They can contain `vector graphic files <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
|
||||
|
||||
.. image:: bitmap_vs_svg.svg
|
||||
|
||||
A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page.
|
||||
|
||||
Rasterizing a PDF is the process of generating an image suitable for display or analyzing with an OCR engine. OCR engines like Tesseract work with images, not vector objects.
|
||||
|
||||
|
||||
About PDF/A
|
||||
-----------
|
||||
|
||||
`PDF/A <https://en.wikipedia.org/wiki/PDF/A>`_ is a standardized subset of the full PDF specification that is designed for archiving. PDF/A differs from PDF primarily by omitting features that would make it difficult to read the file in the future, such as embedded Javascript or references to external fonts. All fonts and resources needed to interpret the PDF must be contained within it. Generally speaking, scanned documents should be converted to PDF/A. There are various conformance levels and versions, such as "PDF/A-2b".
|
||||
|
||||
Since most people who scan documents are interested in reading them in the future, OCRmyPDF generates PDF/A-2b by default.
|
||||
|
||||
PDF/A has a few drawbacks. Some PDF viewers include an alert that the file is a PDF/A, which may confuse some users. It also tends to produce larger files than PDF, because it embeds certain resources even if they are commonly available.
|
||||
|
||||
What OCRmyPDF does
|
||||
------------------
|
||||
|
||||
OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript <http://ghostscript.com/>`_ to rasterize the page, and then performs on OCR on the rasterized image. It is not enough to simply extract the images from each page and run OCR on them individually. Of course one could use Ghostscript or another PDF rasterizer and then pass the image to Tesseract. OCRmyPDF automates this process and produces a minimally changed output file that contains the same information, colorspace and resolution.
|
||||
|
||||
The Tesseract OCR engine can output 'hOCR' files, which are XML files that contain a description of the text it found on the page. OCRmyPDF will render a new PDF that contains only the hidden text layer, and merge this with the original page.
|
||||
|
||||
Alternately, OCRmyPDF can use the Tesseract OCR engine to directly output PDFs for each page, then merge them.
|
||||
|
||||
By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be disabled with the ``--output-type pdf`` argument.
|
||||
|
||||
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these limitations, as do any other programs that rely on Tesseract:
|
||||
|
||||
* The OCR is not as accurate as commercial solutions such as Abbyy.
|
||||
* It is not capable of recognizing handwriting.
|
||||
* It may find gibberish and report this as OCR output.
|
||||
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
|
||||
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
|
||||
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
|
||||
|
||||
OCRmyPDF is also limited by the PDF specification:
|
||||
|
||||
* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others.
|
||||
|
||||
Ghostscript also imposes some limitations:
|
||||
|
||||
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
|
||||
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
@@ -0,0 +1,46 @@
|
||||
Installing additional language packs
|
||||
====================================
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English.
|
||||
|
||||
Tesseract supports `most languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
|
||||
|
||||
For Linux users, you can often find packages that provide language packs:
|
||||
|
||||
Debian and Ubuntu users
|
||||
-----------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Display a list of all Tesseract language packs
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
||||
languages can be requested.
|
||||
|
||||
Mac OS X (macOS) users
|
||||
----------------------
|
||||
|
||||
You can install additional language packs by :ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
|
||||
|
||||
Docker users
|
||||
------------
|
||||
|
||||
Users of the Docker image may use the alternative :ref:`"polyglot" container <docker-polyglot>` which includes all languages.
|
||||
|
||||
Known limitations
|
||||
-----------------
|
||||
|
||||
As of v4.2, users of ocrmypdf working languages outside the Latin alphabet should use the following syntax:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdf --pdf-renderer tesseract
|
||||
|
||||
The reasons for this are:
|
||||
|
||||
* The latest version of Ghostscript (9.19 as of this writing) has unfixed bugs in Unicode handling that generate invalid character maps, so Ghostscript cannot be used for PDF/A conversion
|
||||
* The default "hocr" PDF renderer does not handle Asian fonts properly
|
||||
+23
-1
@@ -1,4 +1,9 @@
|
||||
from enum import IntEnum
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
import pkg_resources
|
||||
|
||||
VERSION = pkg_resources.get_distribution('ocrmypdf').version
|
||||
|
||||
|
||||
class ExitCode(IntEnum):
|
||||
@@ -6,7 +11,24 @@ class ExitCode(IntEnum):
|
||||
bad_args = 1
|
||||
input_file = 2
|
||||
missing_dependency = 3
|
||||
invalid_output_pdfa = 4
|
||||
invalid_output_pdf = 4
|
||||
file_access_error = 5
|
||||
already_done_ocr = 6
|
||||
child_process_error = 7
|
||||
encrypted_pdf = 8
|
||||
other_error = 15
|
||||
ctrl_c = 130
|
||||
|
||||
|
||||
def get_program(name):
|
||||
envvar = 'OCRMYPDF_' + name.upper()
|
||||
return os.environ.get(envvar, name)
|
||||
|
||||
|
||||
def page_number(input_file):
|
||||
return int(os.path.basename(input_file)[0:6])
|
||||
|
||||
|
||||
def is_iterable_notstr(thing):
|
||||
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
||||
|
||||
|
||||
Executable
+1521
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+17
-7
@@ -4,18 +4,24 @@
|
||||
from tempfile import NamedTemporaryFile
|
||||
from subprocess import Popen, PIPE, check_call
|
||||
from shutil import copy
|
||||
from . import get_program
|
||||
from .pdfa import SRGB_ICC_PROFILE
|
||||
|
||||
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log):
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
pageno=1):
|
||||
with NamedTemporaryFile(delete=True) as tmp:
|
||||
args_gs = [
|
||||
'gs',
|
||||
get_program('gs'),
|
||||
'-dQUIET',
|
||||
'-dSAFER',
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=%s' % raster_device,
|
||||
'-dFirstPage=%i' % pageno,
|
||||
'-dLastPage=%i' % pageno,
|
||||
'-o', tmp.name,
|
||||
'-r{0}x{1}'.format(str(xres), str(yres)),
|
||||
'-r{0}x{1}'.format(str(round(xres)), str(round(yres))),
|
||||
input_file
|
||||
]
|
||||
|
||||
@@ -23,7 +29,10 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log):
|
||||
universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
if stdout:
|
||||
log.debug(stdout)
|
||||
if 'error' in stdout:
|
||||
log.error(stdout) # Ghostscript puts errors in stdout
|
||||
else:
|
||||
log.debug(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
|
||||
@@ -36,17 +45,18 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log):
|
||||
def generate_pdfa(pdf_pages, output_file, threads=1):
|
||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||
args_gs = [
|
||||
"gs",
|
||||
get_program("gs"),
|
||||
"-dQUIET",
|
||||
"-dBATCH",
|
||||
"-dNOPAUSE",
|
||||
'-dNumRenderingThreads=' + str(threads),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=/RGB",
|
||||
"-sProcessColorModel=DeviceRGB",
|
||||
"-dPDFA",
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-sPDFACompatibilityPolicy=2",
|
||||
"-sOutputICCProfile=srgb.icc",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/local/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-14: fritz-hh from Github
|
||||
# (https://github.com/fritz-hh)
|
||||
|
||||
+438
-177
@@ -1,20 +1,22 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# © 2013-15: jbarlow83 from Github (https://github.com/jbarlow83)
|
||||
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
|
||||
#
|
||||
#
|
||||
# Use Leptonica to detect find and remove page skew. Leptonica uses the method
|
||||
# of differential square sums, which its author claim is faster and more robust
|
||||
# than the Hough transform used by ImageMagick.
|
||||
# Python FFI wrapper for Leptonica library
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
import argparse
|
||||
import ctypes as C
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from tempfile import TemporaryFile
|
||||
from ctypes.util import find_library
|
||||
from .lib._leptonica import ffi
|
||||
from functools import lru_cache
|
||||
from enum import Enum
|
||||
|
||||
lept = ffi.dlopen(find_library('lept'))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,67 +27,6 @@ def stderr(*objs):
|
||||
print("leptonica.py:", *objs, file=sys.stderr)
|
||||
|
||||
|
||||
from ctypes.util import find_library
|
||||
lept_lib = find_library('lept')
|
||||
if not lept_lib:
|
||||
stderr("Could not find the Leptonica library")
|
||||
sys.exit(3)
|
||||
try:
|
||||
lept = C.cdll.LoadLibrary(lept_lib)
|
||||
except Exception:
|
||||
stderr("Could not load the Leptonica library from %s", lept_lib)
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
class _PIXCOLORMAP(C.Structure):
|
||||
"""struct PixColormap from Leptonica src/pix.h
|
||||
"""
|
||||
|
||||
_fields_ = [
|
||||
("array", C.c_void_p),
|
||||
("depth", C.c_int32),
|
||||
("nalloc", C.c_int32),
|
||||
("n", C.c_int32)
|
||||
]
|
||||
|
||||
|
||||
class _PIX(C.Structure):
|
||||
"""struct Pix from Leptonica src/pix.h
|
||||
"""
|
||||
|
||||
_fields_ = [
|
||||
("w", C.c_uint32),
|
||||
("h", C.c_uint32),
|
||||
("d", C.c_uint32),
|
||||
("wpl", C.c_uint32),
|
||||
("refcount", C.c_uint32),
|
||||
("xres", C.c_int32),
|
||||
("yres", C.c_int32),
|
||||
("informat", C.c_int32),
|
||||
("text", C.POINTER(C.c_char)),
|
||||
("colormap", C.POINTER(_PIXCOLORMAP)),
|
||||
("data", C.POINTER(C.c_uint32))
|
||||
]
|
||||
|
||||
|
||||
PIX = C.POINTER(_PIX)
|
||||
|
||||
lept.pixRead.argtypes = [C.c_char_p]
|
||||
lept.pixRead.restype = PIX
|
||||
lept.pixScale.argtypes = [PIX, C.c_float, C.c_float]
|
||||
lept.pixScale.restype = PIX
|
||||
lept.pixDeskew.argtypes = [PIX, C.c_int32]
|
||||
lept.pixDeskew.restype = PIX
|
||||
lept.pixFindSkew.argtypes = [PIX, C.POINTER(C.c_float), C.POINTER(C.c_float)]
|
||||
lept.pixFindSkew.restype = C.c_int32
|
||||
lept.pixWriteImpliedFormat.argtypes = [C.c_char_p, PIX, C.c_int32, C.c_int32]
|
||||
lept.pixWriteImpliedFormat.restype = C.c_int32
|
||||
lept.pixDestroy.argtypes = [C.POINTER(PIX)]
|
||||
lept.pixDestroy.restype = None
|
||||
lept.getLeptonicaVersion.argtypes = []
|
||||
lept.getLeptonicaVersion.restype = C.c_char_p
|
||||
|
||||
|
||||
class LeptonicaErrorTrap(object):
|
||||
"""Context manager to trap errors reported by Leptonica.
|
||||
|
||||
@@ -100,16 +41,21 @@ class LeptonicaErrorTrap(object):
|
||||
|
||||
"""
|
||||
def __enter__(self):
|
||||
from io import UnsupportedOperation
|
||||
self.tmpfile = TemporaryFile()
|
||||
|
||||
# Save the old stderr, and redirect stderr to temporary file
|
||||
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
|
||||
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno())
|
||||
try:
|
||||
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
|
||||
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno())
|
||||
except UnsupportedOperation:
|
||||
self.old_stderr_fileno = None
|
||||
return
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
# Restore old stderr
|
||||
os.dup2(self.old_stderr_fileno, sys.stderr.fileno())
|
||||
if self.old_stderr_fileno is not None:
|
||||
os.dup2(self.old_stderr_fileno, sys.stderr.fileno())
|
||||
|
||||
# Get data from tmpfile (in with block to ensure it is closed)
|
||||
with self.tmpfile as tmpfile:
|
||||
@@ -140,103 +86,432 @@ class LeptonicaIOError(LeptonicaError):
|
||||
pass
|
||||
|
||||
|
||||
def pixRead(filename):
|
||||
"""Load an image file into a PIX object.
|
||||
class RemoveColormap(Enum):
|
||||
to_binary = 0
|
||||
to_grayscale = 1
|
||||
to_full_color = 2
|
||||
based_on_src = 3
|
||||
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If loading
|
||||
fails then the object will wrap a C null pointer.
|
||||
|
||||
class Pix:
|
||||
"""Wrapper around leptonica's PIX object.
|
||||
|
||||
Leptonica uses referencing counting on PIX objects. Also, many Leptonica
|
||||
functions return the original object with an increased reference count
|
||||
if the operation had no effect (for example, image skew was found to be 0).
|
||||
This has complications for memory management in Python. Whenever Leptonica
|
||||
returns a PIX object (new or old), we wrap it in this class, which
|
||||
registers it with the FFI garbage collector. pixDestroy() decrements the
|
||||
reference count and only destroys when the last reference is removed.
|
||||
|
||||
Leptonica's reference counting is not threadsafe. This class can be used
|
||||
in a threadsafe manner if a Python threading.Lock protects the data.
|
||||
|
||||
This class treats Pix objects as immutable. All methods return new
|
||||
modified objects. This allows convenient chaining:
|
||||
|
||||
>>> Pix.read('filename.jpg').scale((0.5, 0.5)).deskew().show()
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return lept.pixRead(filename.encode(sys.getfilesystemencoding()))
|
||||
|
||||
def __init__(self, pix):
|
||||
self._pix = ffi.gc(pix, Pix._pix_destroy)
|
||||
|
||||
def pixScale(pix, scalex, scaley):
|
||||
"""Returns the pix object rescaled according to the proportions given."""
|
||||
with LeptonicaErrorTrap():
|
||||
return lept.pixScale(pix, scalex, scaley)
|
||||
|
||||
|
||||
def pixDeskew(pix, reduction_factor=0):
|
||||
"""Returns the deskewed pix object.
|
||||
|
||||
A clone of the original is returned when the algorithm cannot find a skew
|
||||
angle with sufficient confidence.
|
||||
|
||||
reduction_factor -- amount to downsample (0 for default) when searching
|
||||
for skew angle
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return lept.pixDeskew(pix, reduction_factor)
|
||||
|
||||
|
||||
def pixFindSkew(pix):
|
||||
"""Returns a tuple (deskew angle in degrees, confidence value).
|
||||
|
||||
Returns (None, None) if no angle is available.
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
angle = C.c_float(0.0)
|
||||
confidence = C.c_float(0.0)
|
||||
result = lept.pixFindSkew(pix, C.byref(angle), C.byref(confidence))
|
||||
if result == 0:
|
||||
return (angle.value, confidence.value)
|
||||
def __repr__(self):
|
||||
if self._pix:
|
||||
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
|
||||
return s.format(self._pix.w, self._pix.h, self._pix.d,
|
||||
int(ffi.cast("intptr_t", self._pix)))
|
||||
else:
|
||||
return (None, None)
|
||||
return "<leptonica.Pix image NULL>"
|
||||
|
||||
def _repr_png_(self):
|
||||
"""iPython display hook
|
||||
|
||||
returns png version of image
|
||||
"""
|
||||
|
||||
data = ffi.new('l_uint8 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixWriteMemPng(data, size, self._pix, 0)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixWriteMemPng")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
return ffi.buffer(char_data, size[0])[:]
|
||||
|
||||
def __getstate__(self):
|
||||
data = ffi.new('l_uint32 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixSerializeToMemory(self._pix, data, size)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixSerializeToMemory")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
|
||||
# Copy from C bytes to python bytes()
|
||||
data_bytes = ffi.buffer(char_data, size[0])[:]
|
||||
|
||||
# Can now free C bytes
|
||||
lept.lept_free(char_data)
|
||||
return dict(data=data_bytes)
|
||||
|
||||
def __setstate__(self, state):
|
||||
cdata_bytes = ffi.new('char[]', state['data'])
|
||||
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
|
||||
|
||||
pix = lept.pixDeserializeFromMemory(
|
||||
cdata_uint32, len(state['data']))
|
||||
Pix.__init__(self, pix)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__getstate__() == other.__getstate__()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._pix.w
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._pix.h
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._pix.d
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return (self._pix.w, self._pix.h)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return {'dpi': (self._pix.xres, self._pix.yres)}
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"Return mode like PIL.Image"
|
||||
if self.depth == 1:
|
||||
return '1'
|
||||
elif self.depth >= 16:
|
||||
return 'RGB'
|
||||
elif not self._pix.colormap:
|
||||
return 'L'
|
||||
else:
|
||||
return 'P'
|
||||
|
||||
@classmethod
|
||||
def read(cls, filename):
|
||||
"""Load an image file into a PIX object.
|
||||
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
|
||||
loading fails then the object will wrap a C null pointer.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return cls(lept.pixRead(
|
||||
filename.encode(sys.getfilesystemencoding())))
|
||||
|
||||
def write_implied_format(
|
||||
self, filename, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
self._pix, jpeg_quality, jpeg_progressive)
|
||||
|
||||
def topil(self):
|
||||
"Returns a PIL.Image version of this Pix"
|
||||
from PIL import Image
|
||||
|
||||
# Leptonica manages data in words, so it implicitly does an endian
|
||||
# swap. Tell Pillow about this when it reads the data.
|
||||
pix = self
|
||||
if sys.byteorder == 'little':
|
||||
if self.mode == 'RGB':
|
||||
raw_mode = 'XBGR'
|
||||
elif self.mode == 'RGBA':
|
||||
raw_mode = 'ABGR'
|
||||
elif self.mode == '1':
|
||||
raw_mode = '1;I'
|
||||
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
|
||||
else:
|
||||
raw_mode = self.mode
|
||||
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
|
||||
else:
|
||||
raw_mode = self.mode # no endian swap needed
|
||||
|
||||
size = (pix._pix.w, pix._pix.h)
|
||||
bytecount = pix._pix.wpl * 4 * pix._pix.h
|
||||
buf = ffi.buffer(pix._pix.data, bytecount)
|
||||
stride = pix._pix.wpl * 4
|
||||
|
||||
im = Image.frombytes(self.mode, size, buf, 'raw', raw_mode, stride)
|
||||
|
||||
return im
|
||||
|
||||
def show(self):
|
||||
return self.topil().show()
|
||||
|
||||
def deskew(self, reduction_factor=0):
|
||||
"""Returns the deskewed pix object.
|
||||
|
||||
A clone of the original is returned when the algorithm cannot find a
|
||||
skew angle with sufficient confidence.
|
||||
|
||||
reduction_factor -- amount to downsample (0 for default) when searching
|
||||
for skew angle
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixDeskew(self._pix, reduction_factor))
|
||||
|
||||
def scale(self, scale_xy):
|
||||
"Returns the pix object rescaled according to the proportions given."
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixScale(self._pix, scale_xy[0], scale_xy[1]))
|
||||
|
||||
def rotate180(self):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
|
||||
|
||||
def rotate_orth(self, quads):
|
||||
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRotateOrth(self._pix, quads))
|
||||
|
||||
def find_skew(self):
|
||||
"""Returns a tuple (deskew angle in degrees, confidence value).
|
||||
|
||||
Returns (None, None) if no angle is available.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
angle = ffi.new('float *', 0.0)
|
||||
confidence = ffi.new('float *', 0.0)
|
||||
result = lept.pixFindSkew(self._pix, angle, confidence)
|
||||
if result == 0:
|
||||
return (angle[0], confidence[0])
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def convert_rgb_to_luminance(self):
|
||||
with LeptonicaErrorTrap():
|
||||
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
|
||||
if gray_pix:
|
||||
return Pix(gray_pix)
|
||||
return None
|
||||
|
||||
def remove_colormap(self, removal_type):
|
||||
"""Remove a palette
|
||||
|
||||
removal_type - RemovalColormap()
|
||||
"""
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
|
||||
|
||||
def otsu_adaptive_threshold(
|
||||
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
p_pix = ffi.new('PIX **')
|
||||
|
||||
result = lept.pixOtsuAdaptiveThreshold(
|
||||
self._pix,
|
||||
sx, sy,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL,
|
||||
p_pix)
|
||||
if result == 0:
|
||||
return Pix(p_pix[0])
|
||||
else:
|
||||
return None
|
||||
|
||||
def otsu_threshold_on_background_norm(
|
||||
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
|
||||
bgval=255, kernel_size=(2, 2), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
if mask is None:
|
||||
mask = ffi.NULL
|
||||
if isinstance(mask, Pix):
|
||||
mask = mask._pix
|
||||
|
||||
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
|
||||
self._pix,
|
||||
mask,
|
||||
sx, sy,
|
||||
thresh, mincount, bgval,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL
|
||||
)
|
||||
if thresh_pix == ffi.NULL:
|
||||
return None
|
||||
return Pix(thresh_pix)
|
||||
|
||||
def crop_to_foreground(
|
||||
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
|
||||
showmorph=0, display=0, pdfdir=ffi.NULL):
|
||||
with LeptonicaErrorTrap():
|
||||
cropbox = Box(lept.pixFindPageForeground(
|
||||
self._pix,
|
||||
threshold,
|
||||
mindist,
|
||||
erasedist,
|
||||
pagenum,
|
||||
showmorph,
|
||||
display,
|
||||
pdfdir))
|
||||
|
||||
print(repr(cropbox))
|
||||
|
||||
cropped_pix = lept.pixClipRectangle(
|
||||
self._pix,
|
||||
cropbox._box,
|
||||
ffi.NULL)
|
||||
|
||||
return Pix(cropped_pix)
|
||||
|
||||
def clean_background_to_white(
|
||||
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixCleanBackgroundToWhite(
|
||||
self._pix,
|
||||
mask or ffi.NULL,
|
||||
grayscale or ffi.NULL,
|
||||
gamma,
|
||||
black,
|
||||
white))
|
||||
|
||||
def gamma_trc(self, gamma=1.0, minval=0, maxval=255):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixGammaTRC(
|
||||
ffi.NULL,
|
||||
self._pix,
|
||||
gamma,
|
||||
minval,
|
||||
maxval
|
||||
))
|
||||
|
||||
def background_norm(
|
||||
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
|
||||
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixBackgroundNorm(
|
||||
self._pix,
|
||||
mask or ffi.NULL,
|
||||
grayscale or ffi.NULL,
|
||||
tile_size[0],
|
||||
tile_size[1],
|
||||
fg_threshold,
|
||||
min_count,
|
||||
bg_val,
|
||||
smooth_kernel[0],
|
||||
smooth_kernel[1]
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def make_pixel_sum_tab8():
|
||||
return lept.makePixelSumTab8()
|
||||
|
||||
@staticmethod
|
||||
def correlation_binary(pix1, pix2):
|
||||
if get_leptonica_version() < 'leptonica-1.72':
|
||||
# Older versions of Leptonica (pre-1.72) have a buggy
|
||||
# implementation of pixCorrelationBinary that overflows on larger
|
||||
# images. Ubuntu trusty has 1.70. Ubuntu PPA
|
||||
# ppa:rebuntu16/avidemux+unofficial has "leptonlib" 1.73.
|
||||
pix1_count = ffi.new('l_int32 *')
|
||||
pix2_count = ffi.new('l_int32 *')
|
||||
pixn_count = ffi.new('l_int32 *')
|
||||
tab8 = Pix.make_pixel_sum_tab8()
|
||||
|
||||
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
|
||||
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
|
||||
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
|
||||
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
|
||||
|
||||
# Python converts these int32s to larger units as needed
|
||||
# to avoid overflow. Overflow happens easily here.
|
||||
correlation = (
|
||||
(pixn_count[0] * pixn_count[0]) /
|
||||
(pix1_count[0] * pix2_count[0])
|
||||
)
|
||||
return correlation
|
||||
else:
|
||||
correlation = ffi.new('float *', 0.0)
|
||||
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
|
||||
correlation)
|
||||
if result != 0:
|
||||
raise LeptonicaError("Correlation failed")
|
||||
return correlation[0]
|
||||
|
||||
@staticmethod
|
||||
def _pix_destroy(pix):
|
||||
p_pix = ffi.new('PIX **', pix)
|
||||
lept.pixDestroy(p_pix)
|
||||
# print('pix destroy ' + repr(pix))
|
||||
|
||||
|
||||
def pixWriteImpliedFormat(filename, pix, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
class Box:
|
||||
"""Wrapper around Leptonica's BOX objects.
|
||||
|
||||
See class Pix for notes about reference counting.
|
||||
"""
|
||||
fileroot, extension = os.path.splitext(filename)
|
||||
fix_pnm = False
|
||||
if extension.lower() in ('.pbm', '.pgm', '.ppm'):
|
||||
# Leptonica does not process handle these extensions correctly, but
|
||||
# does handle .pnm correctly. Add another .pnm suffix.
|
||||
filename += '.pnm'
|
||||
fix_pnm = True
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
pix, jpeg_quality, jpeg_progressive)
|
||||
def __init__(self, box):
|
||||
self._box = ffi.gc(box, Box._box_destroy)
|
||||
|
||||
if fix_pnm:
|
||||
from shutil import move
|
||||
move(filename, filename[:-4]) # Remove .pnm suffix
|
||||
def __repr__(self):
|
||||
if self._box:
|
||||
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
|
||||
self.x, self.y, self.w, self.h)
|
||||
return '<leptonica.Box NULL>'
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self._box.x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self._box.y
|
||||
|
||||
@property
|
||||
def w(self):
|
||||
return self._box.w
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
return self._box.h
|
||||
|
||||
@staticmethod
|
||||
def _box_destroy(box):
|
||||
p_box = ffi.new('BOX **', box)
|
||||
lept.boxDestroy(p_box)
|
||||
|
||||
|
||||
def pixDestroy(pix):
|
||||
"""Destroy the pix object.
|
||||
|
||||
Function signature is pixDestroy(struct Pix **), hence C.byref() to pass
|
||||
the address of the pointer.
|
||||
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixDestroy(C.byref(pix))
|
||||
|
||||
|
||||
def getLeptonicaVersion():
|
||||
@lru_cache(maxsize=1)
|
||||
def get_leptonica_version():
|
||||
"""Get Leptonica version string.
|
||||
|
||||
Caveat: Leptonica expects the caller to free this memory. We don't,
|
||||
since that would involve binding to libc to access libc.free(),
|
||||
a pointless effort to reclaim 100 bytes of memory.
|
||||
|
||||
"""
|
||||
return lept.getLeptonicaVersion().decode()
|
||||
return ffi.string(lept.getLeptonicaVersion()).decode()
|
||||
|
||||
|
||||
def deskew(infile, outfile, dpi):
|
||||
try:
|
||||
pix_source = pixRead(infile)
|
||||
pix_source = Pix.read(infile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open file: %s" % infile)
|
||||
|
||||
@@ -244,14 +519,28 @@ def deskew(infile, outfile, dpi):
|
||||
reduction_factor = 1 # Don't downsample too much if DPI is already low
|
||||
else:
|
||||
reduction_factor = 0 # Use default
|
||||
pix_deskewed = pixDeskew(pix_source, reduction_factor)
|
||||
pix_deskewed = pix_source.deskew(reduction_factor)
|
||||
|
||||
try:
|
||||
pixWriteImpliedFormat(outfile, pix_deskewed)
|
||||
pix_deskewed.write_implied_format(outfile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
|
||||
|
||||
def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
|
||||
black_threshold=70, white_threshold=190):
|
||||
try:
|
||||
pix = Pix.read(infile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open file: %s" % infile)
|
||||
|
||||
pix = pix.background_norm(tile_size=tile_size).gamma_trc(
|
||||
gamma, black_threshold, white_threshold)
|
||||
|
||||
try:
|
||||
pix.write_implied_format(outfile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
pixDestroy(pix_source)
|
||||
pixDestroy(pix_deskewed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -270,39 +559,12 @@ if __name__ == '__main__':
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if getLeptonicaVersion() != u'leptonica-1.69':
|
||||
if get_leptonica_version() != u'leptonica-1.69':
|
||||
print("Unexpected leptonica version: %s" % getLeptonicaVersion())
|
||||
|
||||
args.func(args)
|
||||
|
||||
|
||||
def _test_output(mode, extension, im_format):
|
||||
from PIL import Image
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile(prefix='test-lept-pnm', suffix=extension, delete=True) as tmpfile:
|
||||
im = Image.new(mode=mode, size=(100, 100))
|
||||
im.save(tmpfile)
|
||||
|
||||
pix = pixRead(tmpfile.name)
|
||||
pixWriteImpliedFormat(tmpfile.name, pix)
|
||||
pixDestroy(pix)
|
||||
|
||||
im_roundtrip = Image.open(tmpfile.name)
|
||||
assert im_roundtrip.mode == im.mode, "leptonica mode differs"
|
||||
assert im_roundtrip.format == im_format, \
|
||||
"{0}: leptonica produced a {1}".format(
|
||||
extension,
|
||||
im_roundtrip.format)
|
||||
|
||||
|
||||
def test_pnm_output():
|
||||
params = [['1', '.pbm', 'PPM'], ['L', '.pgm', 'PPM'],
|
||||
['RGB', '.ppm', 'PPM']]
|
||||
for param in params:
|
||||
_test_output(*param)
|
||||
|
||||
|
||||
def test_skew_angle():
|
||||
from PIL import Image, ImageDraw
|
||||
from tempfile import NamedTemporaryFile
|
||||
@@ -325,7 +587,6 @@ def test_skew_angle():
|
||||
rotated_im.save(tmpfile)
|
||||
pix = pixRead(tmpfile.name)
|
||||
angle, confidence = pixFindSkew(pix)
|
||||
pixDestroy(pix)
|
||||
print('{0} {1} {2}'.format(rotate_angle, angle, confidence), file=sys.stderr)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
ffi.set_source("ocrmypdf.lib._leptonica", None)
|
||||
ffi.cdef("""
|
||||
typedef signed char l_int8;
|
||||
typedef unsigned char l_uint8;
|
||||
typedef short l_int16;
|
||||
typedef unsigned short l_uint16;
|
||||
typedef int l_int32;
|
||||
typedef unsigned int l_uint32;
|
||||
typedef float l_float32;
|
||||
typedef double l_float64;
|
||||
typedef long long l_int64;
|
||||
typedef unsigned long long l_uint64;
|
||||
|
||||
struct Pix
|
||||
{
|
||||
l_uint32 w; /* width in pixels */
|
||||
l_uint32 h; /* height in pixels */
|
||||
l_uint32 d; /* depth in bits (bpp) */
|
||||
l_uint32 spp; /* number of samples per pixel */
|
||||
l_uint32 wpl; /* 32-bit words/line */
|
||||
l_uint32 refcount; /* reference count (1 if no clones) */
|
||||
l_int32 xres; /* image res (ppi) in x direction */
|
||||
/* (use 0 if unknown) */
|
||||
l_int32 yres; /* image res (ppi) in y direction */
|
||||
/* (use 0 if unknown) */
|
||||
l_int32 informat; /* input file format, IFF_* */
|
||||
l_int32 special; /* special instructions for I/O, etc */
|
||||
char *text; /* text string associated with pix */
|
||||
struct PixColormap *colormap; /* colormap (may be null) */
|
||||
l_uint32 *data; /* the image data */
|
||||
};
|
||||
typedef struct Pix PIX;
|
||||
|
||||
struct PixColormap
|
||||
{
|
||||
void *array; /* colormap table (array of RGBA_QUAD) */
|
||||
l_int32 depth; /* of pix (1, 2, 4 or 8 bpp) */
|
||||
l_int32 nalloc; /* number of color entries allocated */
|
||||
l_int32 n; /* number of color entries used */
|
||||
};
|
||||
typedef struct PixColormap PIXCMAP;
|
||||
|
||||
struct Box
|
||||
{
|
||||
l_int32 x;
|
||||
l_int32 y;
|
||||
l_int32 w;
|
||||
l_int32 h;
|
||||
l_uint32 refcount; /* reference count (1 if no clones) */
|
||||
|
||||
};
|
||||
typedef struct Box BOX;
|
||||
|
||||
""")
|
||||
|
||||
ffi.cdef("""
|
||||
PIX * pixRead ( const char *filename );
|
||||
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
|
||||
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
|
||||
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
|
||||
l_int32
|
||||
pixWriteMemPng(l_uint8 **pdata,
|
||||
size_t *psize,
|
||||
PIX *pix,
|
||||
l_float32 gamma);
|
||||
|
||||
void pixDestroy ( PIX **ppix );
|
||||
|
||||
PIX *
|
||||
pixEndianByteSwapNew(PIX *pixs);
|
||||
|
||||
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
|
||||
char * getLeptonicaVersion ( );
|
||||
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
|
||||
PIX *pixRotate180(PIX *pixd, PIX *pixs);
|
||||
PIX *
|
||||
pixRotateOrth(PIX *pixs,
|
||||
l_int32 quads);
|
||||
|
||||
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
|
||||
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
|
||||
l_int32 * makePixelSumTab8 ( void );
|
||||
|
||||
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
|
||||
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
|
||||
|
||||
PIX * pixConvertRGBToLuminance(PIX *pixs);
|
||||
|
||||
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
|
||||
|
||||
l_int32
|
||||
pixOtsuAdaptiveThreshold(PIX *pixs,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
PIX **ppixth,
|
||||
PIX **ppixd);
|
||||
|
||||
PIX *
|
||||
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
|
||||
PIX *pixim,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 thresh,
|
||||
l_int32 mincount,
|
||||
l_int32 bgval,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
l_int32 *pthresh);
|
||||
|
||||
PIX *
|
||||
pixCleanBackgroundToWhite(PIX *pixs,
|
||||
PIX *pixim,
|
||||
PIX *pixg,
|
||||
l_float32 gamma,
|
||||
l_int32 blackval,
|
||||
l_int32 whiteval);
|
||||
|
||||
BOX *
|
||||
pixFindPageForeground(PIX *pixs,
|
||||
l_int32 threshold,
|
||||
l_int32 mindist,
|
||||
l_int32 erasedist,
|
||||
l_int32 pagenum,
|
||||
l_int32 showmorph,
|
||||
l_int32 display,
|
||||
const char *pdfdir);
|
||||
|
||||
PIX *
|
||||
pixClipRectangle(PIX *pixs,
|
||||
BOX *box,
|
||||
BOX **pboxc);
|
||||
|
||||
PIX *
|
||||
pixBackgroundNorm(PIX *pixs,
|
||||
PIX *pixim,
|
||||
PIX *pixg,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 thresh,
|
||||
l_int32 mincount,
|
||||
l_int32 bgval,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy);
|
||||
|
||||
PIX *
|
||||
pixGammaTRC(PIX *pixd,
|
||||
PIX *pixs,
|
||||
l_float32 gamma,
|
||||
l_int32 minval,
|
||||
l_int32 maxval);
|
||||
|
||||
|
||||
l_int32
|
||||
pixNumSignificantGrayColors(PIX *pixs,
|
||||
l_int32 darkthresh,
|
||||
l_int32 lightthresh,
|
||||
l_float32 minfract,
|
||||
l_int32 factor,
|
||||
l_int32 *pncolors);
|
||||
|
||||
l_int32
|
||||
pixColorFraction(PIX *pixs,
|
||||
l_int32 darkthresh,
|
||||
l_int32 lightthresh,
|
||||
l_int32 diffthresh,
|
||||
l_int32 factor,
|
||||
l_float32 *ppixfract,
|
||||
l_float32 *pcolorfract);
|
||||
|
||||
PIX *
|
||||
pixColorMagnitude(PIX *pixs,
|
||||
l_int32 rwhite,
|
||||
l_int32 gwhite,
|
||||
l_int32 bwhite,
|
||||
l_int32 type);
|
||||
|
||||
void
|
||||
boxDestroy(BOX **pbox);
|
||||
|
||||
void lept_free(void *ptr);
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ffi.compile()
|
||||
Executable → Regular
+4
-909
@@ -1,912 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
# © 2015-16 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from contextlib import suppress
|
||||
from tempfile import NamedTemporaryFile, mkdtemp
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import warnings
|
||||
import multiprocessing
|
||||
import atexit
|
||||
import textwrap
|
||||
# This file is now an alias for __main__
|
||||
# Consider removing in future releases
|
||||
|
||||
import PyPDF2 as pypdf
|
||||
from PIL import Image
|
||||
|
||||
from subprocess import Popen, check_call, PIPE, CalledProcessError, \
|
||||
TimeoutExpired, check_output, STDOUT
|
||||
try:
|
||||
from subprocess import DEVNULL
|
||||
except ImportError:
|
||||
DEVNULL = open(os.devnull, 'wb')
|
||||
|
||||
|
||||
from ruffus import transform, suffix, merge, active_if, regex, jobs_limit, \
|
||||
formatter, follows, split, collate, check_if_uptodate
|
||||
import ruffus.ruffus_exceptions as ruffus_exceptions
|
||||
import ruffus.cmdline as cmdline
|
||||
|
||||
from .hocrtransform import HocrTransform
|
||||
from .pageinfo import pdf_get_all_pageinfo
|
||||
from .pdfa import generate_pdfa_def
|
||||
from . import ghostscript
|
||||
from . import tesseract
|
||||
from . import ExitCode
|
||||
|
||||
warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
|
||||
|
||||
|
||||
BASEDIR = os.path.dirname(os.path.realpath(__file__))
|
||||
JHOVE_PATH = os.path.realpath(os.path.join(BASEDIR, 'jhove'))
|
||||
JHOVE_JAR = os.path.join(JHOVE_PATH, 'bin', 'JhoveApp.jar')
|
||||
JHOVE_CFG = os.path.join(JHOVE_PATH, 'conf', 'jhove.conf')
|
||||
|
||||
|
||||
# -------------
|
||||
# External dependencies
|
||||
|
||||
MINIMUM_TESS_VERSION = '3.02.02'
|
||||
|
||||
|
||||
def complain(message):
|
||||
print(*textwrap.wrap(message), file=sys.stderr)
|
||||
|
||||
|
||||
if tesseract.version() < MINIMUM_TESS_VERSION:
|
||||
complain(
|
||||
"Please install tesseract {0} or newer "
|
||||
"(currently installed version is {1})".format(
|
||||
MINIMUM_TESS_VERSION, tesseract.version()))
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
|
||||
try:
|
||||
import PIL.features
|
||||
check_codec = PIL.features.check_codec
|
||||
except (ImportError, AttributeError):
|
||||
def check_codec(codec_name):
|
||||
if codec_name == 'jpg':
|
||||
return 'jpeg_encoder' in dir(Image.core)
|
||||
elif codec_name == 'zlib':
|
||||
return 'zip_encoder' in dir(Image.core)
|
||||
raise NotImplementedError(codec_name)
|
||||
|
||||
|
||||
def check_pil_encoder(codec_name, friendly_name):
|
||||
try:
|
||||
if check_codec(codec_name):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
complain(
|
||||
"ERROR: Your version of the Python imaging library (Pillow) was "
|
||||
"compiled without support for " + friendly_name + " encoding/decoding."
|
||||
"\n"
|
||||
"You will need to uninstall Pillow and reinstall it with PNG and JPEG "
|
||||
"support (libjpeg and zlib)."
|
||||
"\n"
|
||||
"See installation instructions for your platform here:\n"
|
||||
" https://pillow.readthedocs.org/installation.html"
|
||||
)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
|
||||
check_pil_encoder('jpg', 'JPEG')
|
||||
check_pil_encoder('zlib', 'PNG')
|
||||
|
||||
|
||||
# -------------
|
||||
# Parser
|
||||
|
||||
parser = cmdline.get_argparse(
|
||||
prog="ocrmypdf",
|
||||
description="Generate searchable PDF file from an image-only PDF file.",
|
||||
version='3.0',
|
||||
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'])
|
||||
|
||||
parser.add_argument(
|
||||
'input_file',
|
||||
help="PDF file containing the images to be OCRed")
|
||||
parser.add_argument(
|
||||
'output_file',
|
||||
help="output searchable PDF file")
|
||||
parser.add_argument(
|
||||
'-l', '--language', action='append',
|
||||
help="languages of the file to be OCRed")
|
||||
|
||||
metadata = parser.add_argument_group(
|
||||
"Metadata options",
|
||||
"Set output PDF/A metadata (default: use input document's title)")
|
||||
metadata.add_argument(
|
||||
'--title', type=str,
|
||||
help="set document title (place multiple words in quotes)")
|
||||
metadata.add_argument(
|
||||
'--author', type=str,
|
||||
help="set document author")
|
||||
metadata.add_argument(
|
||||
'--subject', type=str,
|
||||
help="set document")
|
||||
metadata.add_argument(
|
||||
'--keywords', type=str,
|
||||
help="set document keywords")
|
||||
|
||||
|
||||
preprocessing = parser.add_argument_group(
|
||||
"Preprocessing options",
|
||||
"Improve OCR quality and final image")
|
||||
preprocessing.add_argument(
|
||||
'-d', '--deskew', action='store_true',
|
||||
help="deskew each page before performing OCR")
|
||||
preprocessing.add_argument(
|
||||
'-c', '--clean', action='store_true',
|
||||
help="clean pages from scanning artifacts before performing OCR")
|
||||
preprocessing.add_argument(
|
||||
'-i', '--clean-final', action='store_true',
|
||||
help="incorporate the cleaned image in the final PDF file")
|
||||
preprocessing.add_argument(
|
||||
'--oversample', metavar='DPI', type=int, default=0,
|
||||
help="oversample images to at least the specified DPI, to improve OCR "
|
||||
"results slightly")
|
||||
|
||||
parser.add_argument(
|
||||
'-f', '--force-ocr', action='store_true',
|
||||
help="rasterize any fonts or vector images on each page and apply OCR")
|
||||
parser.add_argument(
|
||||
'-s', '--skip-text', action='store_true',
|
||||
help="skip OCR on any pages that already contain text, but include the"
|
||||
" page in final output")
|
||||
parser.add_argument(
|
||||
'--skip-big', type=float, metavar='MPixels',
|
||||
help="skip OCR on pages larger than the specified amount of megapixels, "
|
||||
"but include skipped pages in final output")
|
||||
# parser.add_argument(
|
||||
# '--exact-image', action='store_true',
|
||||
# help="Use original page from PDF without re-rendering")
|
||||
|
||||
advanced = parser.add_argument_group(
|
||||
"Advanced",
|
||||
"Advanced options for power users")
|
||||
advanced.add_argument(
|
||||
'--tesseract-config', default=[], type=list, action='append',
|
||||
help="additional Tesseract configuration files")
|
||||
advanced.add_argument(
|
||||
'--pdf-renderer', choices=['tesseract', 'hocr'], default='hocr',
|
||||
help='choose OCR PDF renderer')
|
||||
advanced.add_argument(
|
||||
'--tesseract-timeout', default=180.0, type=float,
|
||||
help='give up on OCR after the timeout, but copy the preprocessed page '
|
||||
'into the final output')
|
||||
|
||||
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)")
|
||||
debugging.add_argument(
|
||||
'-g', '--debug-rendering', action='store_true',
|
||||
help="render each page twice with debug information on second page")
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
|
||||
# ----------
|
||||
# Languages
|
||||
|
||||
if not options.language:
|
||||
options.language = ['eng'] # Enforce English hegemony
|
||||
|
||||
# Support v2.x "eng+deu" language syntax
|
||||
if '+' in options.language[0]:
|
||||
options.language = options.language[0].split('+')
|
||||
|
||||
if not set(options.language).issubset(tesseract.languages()):
|
||||
complain(
|
||||
"The installed version of tesseract does not have language "
|
||||
"data for the following requested languages: ")
|
||||
for lang in (set(options.language) - tesseract.languages()):
|
||||
complain(lang)
|
||||
sys.exit(ExitCode.bad_args)
|
||||
|
||||
|
||||
# ----------
|
||||
# Arguments
|
||||
|
||||
|
||||
if any((options.deskew, options.clean, options.clean_final)):
|
||||
try:
|
||||
from . import unpaper
|
||||
except ImportError:
|
||||
complain(
|
||||
"Install the 'unpaper' program to use --deskew or --clean.")
|
||||
sys.exit(ExitCode.bad_args)
|
||||
else:
|
||||
unpaper = None
|
||||
|
||||
if options.debug_rendering and options.pdf_renderer == 'tesseract':
|
||||
complain(
|
||||
"Ignoring --debug-rendering because it is not supported with"
|
||||
"--pdf-renderer=tesseract.")
|
||||
|
||||
if options.force_ocr and options.skip_text:
|
||||
complain(
|
||||
"Error: --force-ocr and --skip-text are mutually incompatible.")
|
||||
sys.exit(ExitCode.bad_args)
|
||||
|
||||
if options.clean and not options.clean_final \
|
||||
and options.pdf_renderer == 'tesseract':
|
||||
complain(
|
||||
"Tesseract PDF renderer cannot render --clean pages without "
|
||||
"also performing --clean-final, so --clean-final is assumed.")
|
||||
|
||||
|
||||
# ----------
|
||||
# Logging
|
||||
|
||||
|
||||
_logger, _logger_mutex = cmdline.setup_logging(__name__, options.log_file,
|
||||
options.verbose)
|
||||
|
||||
|
||||
class WrappedLogger:
|
||||
|
||||
def __init__(self, my_logger, my_mutex):
|
||||
self.logger = my_logger
|
||||
self.mutex = my_mutex
|
||||
|
||||
def log(self, *args, **kwargs):
|
||||
with self.mutex:
|
||||
self.logger.log(*args, **kwargs)
|
||||
|
||||
def debug(self, *args, **kwargs):
|
||||
with self.mutex:
|
||||
self.logger.debug(*args, **kwargs)
|
||||
|
||||
def info(self, *args, **kwargs):
|
||||
with self.mutex:
|
||||
self.logger.info(*args, **kwargs)
|
||||
|
||||
def warning(self, *args, **kwargs):
|
||||
with self.mutex:
|
||||
self.logger.warning(*args, **kwargs)
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
with self.mutex:
|
||||
self.logger.error(*args, **kwargs)
|
||||
|
||||
def critical(self, *args, **kwargs):
|
||||
with self.mutex:
|
||||
self.logger.critical(*args, **kwargs)
|
||||
|
||||
_log = WrappedLogger(_logger, _logger_mutex)
|
||||
|
||||
|
||||
def re_symlink(input_file, soft_link_name, log=_log):
|
||||
"""
|
||||
Helper function: relinks soft symbolic link if necessary
|
||||
"""
|
||||
# Guard against soft linking to oneself
|
||||
if input_file == soft_link_name:
|
||||
log.debug("Warning: No symbolic link made. You are using " +
|
||||
"the original data directory as the working directory.")
|
||||
return
|
||||
|
||||
# Soft link already exists: delete for relink?
|
||||
if os.path.lexists(soft_link_name):
|
||||
# do not delete or overwrite real (non-soft link) file
|
||||
if not os.path.islink(soft_link_name):
|
||||
raise Exception("%s exists and is not a link" % soft_link_name)
|
||||
try:
|
||||
os.unlink(soft_link_name)
|
||||
except:
|
||||
log.debug("Can't unlink %s" % (soft_link_name))
|
||||
|
||||
if not os.path.exists(input_file):
|
||||
raise Exception("trying to create a broken symlink to %s" % input_file)
|
||||
|
||||
log.debug("os.symlink(%s, %s)" % (input_file, soft_link_name))
|
||||
|
||||
# Create symbolic link using absolute path
|
||||
os.symlink(
|
||||
os.path.abspath(input_file),
|
||||
soft_link_name
|
||||
)
|
||||
|
||||
|
||||
# -------------
|
||||
# The Pipeline
|
||||
|
||||
manager = multiprocessing.Manager()
|
||||
_pdfinfo = manager.list()
|
||||
_pdfinfo_lock = manager.Lock()
|
||||
|
||||
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
||||
|
||||
|
||||
@atexit.register
|
||||
def cleanup_working_files(*args):
|
||||
if options.keep_temporary_files:
|
||||
print("Temporary working files saved at:")
|
||||
print(work_folder)
|
||||
else:
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(work_folder)
|
||||
|
||||
|
||||
@transform(
|
||||
input=options.input_file,
|
||||
filter=suffix('.pdf'),
|
||||
output='.repaired.pdf',
|
||||
output_dir=work_folder,
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def repair_pdf(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
args_qpdf = [
|
||||
'qpdf', input_file, output_file
|
||||
]
|
||||
try:
|
||||
out = check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2:
|
||||
print("{0}: not a valid PDF, and could not repair it.".format(
|
||||
options.input_file))
|
||||
print("Details:")
|
||||
print(e.output)
|
||||
else:
|
||||
print(e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
|
||||
log.debug(out)
|
||||
|
||||
with pdfinfo_lock:
|
||||
pdfinfo.extend(pdf_get_all_pageinfo(output_file))
|
||||
log.info(pdfinfo)
|
||||
|
||||
|
||||
def get_pageinfo(input_file, pdfinfo, pdfinfo_lock):
|
||||
pageno = int(os.path.basename(input_file)[0:6]) - 1
|
||||
with pdfinfo_lock:
|
||||
pageinfo = pdfinfo[pageno].copy()
|
||||
return pageinfo
|
||||
|
||||
|
||||
def is_ocr_required(pageinfo, log):
|
||||
page = pageinfo['pageno'] + 1
|
||||
ocr_required = True
|
||||
if not pageinfo['images']:
|
||||
# If the page has no images, then it contains vector content or text
|
||||
# or both. It seems quite unlikely that one would find meaningful text
|
||||
# from rasterizing vector content. So skip the page.
|
||||
log.info(
|
||||
"Page {0} has no images - skipping OCR".format(page)
|
||||
)
|
||||
ocr_required = False
|
||||
elif pageinfo['has_text']:
|
||||
s = "Page {0} already has text! – {1}"
|
||||
|
||||
if not options.force_ocr and not options.skip_text:
|
||||
log.error(s.format(page,
|
||||
"aborting (use --force-ocr to force OCR)"))
|
||||
sys.exit(ExitCode.already_done_ocr)
|
||||
elif options.force_ocr:
|
||||
log.info(s.format(page,
|
||||
"rasterizing text and running OCR anyway"))
|
||||
ocr_required = True
|
||||
elif options.skip_text:
|
||||
log.info(s.format(page,
|
||||
"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 pixel_count > (options.skip_big * 1000000):
|
||||
ocr_required = False
|
||||
log.info(
|
||||
"Page {0} is very large; skipping due to -b".format(page))
|
||||
|
||||
return ocr_required
|
||||
|
||||
|
||||
@split(
|
||||
repair_pdf,
|
||||
os.path.join(work_folder, '*.page.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def split_pages(
|
||||
input_file,
|
||||
output_files,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
for oo in output_files:
|
||||
with suppress(FileNotFoundError):
|
||||
os.unlink(oo)
|
||||
|
||||
pages = check_output(['qpdf', '--show-npages', input_file],
|
||||
universal_newlines=True, close_fds=True)
|
||||
|
||||
for n in range(int(pages)):
|
||||
args_qpdf = [
|
||||
'qpdf', input_file,
|
||||
'--pages', input_file, '{0}'.format(n + 1), '--',
|
||||
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
||||
]
|
||||
check_call(args_qpdf)
|
||||
|
||||
from glob import glob
|
||||
for filename in glob(os.path.join(work_folder, '*.page.pdf')):
|
||||
pageinfo = get_pageinfo(filename, pdfinfo, pdfinfo_lock)
|
||||
|
||||
alt_suffix = '.ocr.page.pdf' if is_ocr_required(pageinfo, log) \
|
||||
else '.skip.page.pdf'
|
||||
re_symlink(
|
||||
filename,
|
||||
os.path.join(
|
||||
work_folder,
|
||||
os.path.basename(filename)[0:6] + alt_suffix))
|
||||
|
||||
|
||||
@transform(
|
||||
input=split_pages,
|
||||
filter=suffix('.ocr.page.pdf'),
|
||||
output='.page.png',
|
||||
output_dir=work_folder,
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def rasterize_with_ghostscript(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||
|
||||
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'
|
||||
|
||||
log.debug("Rendering {0} with {1}".format(
|
||||
os.path.basename(input_file), device))
|
||||
xres = max(pageinfo['xres'], options.oversample or 0)
|
||||
yres = max(pageinfo['yres'], options.oversample or 0)
|
||||
|
||||
ghostscript.rasterize_pdf(input_file, output_file, xres, yres, device, log)
|
||||
|
||||
|
||||
@transform(
|
||||
input=rasterize_with_ghostscript,
|
||||
filter=suffix(".page.png"),
|
||||
output=".pp-deskew.png",
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def preprocess_deskew(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
if not options.deskew:
|
||||
re_symlink(input_file, output_file, log)
|
||||
return
|
||||
|
||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||
dpi = int(pageinfo['xres'])
|
||||
|
||||
unpaper.deskew(input_file, output_file, dpi, log)
|
||||
|
||||
|
||||
@transform(
|
||||
input=preprocess_deskew,
|
||||
filter=suffix(".pp-deskew.png"),
|
||||
output=".pp-clean.png",
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def preprocess_clean(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
if not options.clean:
|
||||
re_symlink(input_file, output_file, log)
|
||||
return
|
||||
|
||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||
dpi = int(pageinfo['xres'])
|
||||
|
||||
unpaper.clean(input_file, output_file, dpi, log)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@transform(
|
||||
input=preprocess_clean,
|
||||
filter=suffix(".pp-clean.png"),
|
||||
output=".hocr",
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def ocr_tesseract_hocr(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
|
||||
|
||||
badxml = os.path.splitext(output_file)[0] + '.badxml'
|
||||
|
||||
args_tesseract = [
|
||||
'tesseract',
|
||||
'-l', '+'.join(options.language),
|
||||
input_file,
|
||||
badxml,
|
||||
'hocr'
|
||||
] + options.tesseract_config
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
try:
|
||||
stdout, stderr = p.communicate(timeout=options.tesseract_timeout)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
stdout, stderr = p.communicate()
|
||||
# Generate a HOCR file with no recognized text if tesseract times out
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
with open(output_file, 'w', encoding="utf-8") as f:
|
||||
f.write(tesseract.HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
else:
|
||||
if stdout:
|
||||
log.info(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise CalledProcessError(p.returncode, args_tesseract)
|
||||
|
||||
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_file, 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)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@collate(
|
||||
input=[rasterize_with_ghostscript, preprocess_deskew, preprocess_clean],
|
||||
filter=regex(r".*/(\d{6})(?:\.page|\.pp-deskew|\.pp-clean)\.png"),
|
||||
output=os.path.join(work_folder, r'\1.image'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def select_image_for_pdf(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
if options.clean_final:
|
||||
image_suffix = '.pp-clean.png'
|
||||
elif options.deskew:
|
||||
image_suffix = '.pp-deskew.png'
|
||||
else:
|
||||
image_suffix = '.page.png'
|
||||
image = next(ii for ii in infiles if ii.endswith(image_suffix))
|
||||
|
||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||
if all(image['enc'] == 'jpeg' for image in pageinfo['images']):
|
||||
# If all images were JPEGs originally, produce a JPEG as output
|
||||
Image.open(image).save(output_file, format='JPEG')
|
||||
else:
|
||||
re_symlink(image, output_file)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@collate(
|
||||
input=[select_image_for_pdf, ocr_tesseract_hocr],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.hocr)"),
|
||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def render_hocr_page(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image'))
|
||||
|
||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
|
||||
|
||||
hocrtransform = HocrTransform(hocr, dpi)
|
||||
hocrtransform.to_pdf(output_file, imageFileName=image,
|
||||
showBoundingboxes=False, invisibleText=True)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'hocr')
|
||||
@active_if(options.debug_rendering)
|
||||
@collate(
|
||||
input=[select_image_for_pdf, ocr_tesseract_hocr],
|
||||
filter=regex(r".*/(\d{6})(?:\.image|\.hocr)"),
|
||||
output=os.path.join(work_folder, r'\1.debug.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def render_hocr_debug_page(
|
||||
infiles,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
|
||||
image = next(ii for ii in infiles if ii.endswith('.image'))
|
||||
|
||||
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
|
||||
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
|
||||
|
||||
hocrtransform = HocrTransform(hocr, dpi)
|
||||
hocrtransform.to_pdf(output_file, imageFileName=None,
|
||||
showBoundingboxes=True, invisibleText=False)
|
||||
|
||||
|
||||
@active_if(options.pdf_renderer == 'tesseract')
|
||||
@collate(
|
||||
input=[preprocess_clean, split_pages],
|
||||
filter=regex(r".*/(\d{6})(?:\.pp-clean\.png|\.page\.pdf)"),
|
||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def tesseract_ocr_and_render_pdf(
|
||||
input_files,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
input_image = next((ii for ii in input_files if ii.endswith('.png')), '')
|
||||
input_pdf = next((ii for ii in input_files if ii.endswith('.pdf')))
|
||||
if not input_image:
|
||||
# Skipping this page
|
||||
re_symlink(input_pdf, output_file)
|
||||
return
|
||||
|
||||
args_tesseract = [
|
||||
'tesseract',
|
||||
'-l', '+'.join(options.language),
|
||||
input_image,
|
||||
os.path.splitext(output_file)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
] + options.tesseract_config
|
||||
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
|
||||
try:
|
||||
stdout, stderr = p.communicate(timeout=options.tesseract_timeout)
|
||||
if stdout:
|
||||
log.info(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
except TimeoutExpired:
|
||||
p.kill()
|
||||
log.info("Tesseract - page timed out")
|
||||
re_symlink(input_pdf, output_file)
|
||||
|
||||
|
||||
@transform(
|
||||
input=repair_pdf,
|
||||
filter=suffix('.repaired.pdf'),
|
||||
output='.pdfa_def.ps',
|
||||
output_dir=work_folder,
|
||||
extras=[_log])
|
||||
def generate_postscript_stub(
|
||||
input_file,
|
||||
output_file,
|
||||
log):
|
||||
|
||||
pdf = pypdf.PdfFileReader(input_file)
|
||||
|
||||
def from_document_info(key):
|
||||
# pdf.documentInfo.get() DOES NOT behave as expected for a dict-like
|
||||
# object, so call with precautions. TypeError may occur if the PDF
|
||||
# is missing the optional document info section.
|
||||
try:
|
||||
s = pdf.documentInfo[key]
|
||||
return str(s)
|
||||
except (KeyError, TypeError):
|
||||
return ''
|
||||
|
||||
pdfmark = {
|
||||
'title': from_document_info('/Title'),
|
||||
'author': from_document_info('/Author'),
|
||||
'keywords': from_document_info('/Keywords'),
|
||||
'subject': from_document_info('/Subject'),
|
||||
}
|
||||
if options.title:
|
||||
pdfmark['title'] = options.title
|
||||
if options.author:
|
||||
pdfmark['author'] = options.author
|
||||
if options.keywords:
|
||||
pdfmark['keywords'] = options.keywords
|
||||
if options.subject:
|
||||
pdfmark['subject'] = options.subject
|
||||
|
||||
generate_pdfa_def(output_file, pdfmark)
|
||||
|
||||
|
||||
@transform(
|
||||
input=split_pages,
|
||||
filter=suffix('.skip.page.pdf'),
|
||||
output='.done.pdf',
|
||||
output_dir=work_folder,
|
||||
extras=[_log])
|
||||
def skip_page(
|
||||
input_file,
|
||||
output_file,
|
||||
log):
|
||||
re_symlink(input_file, output_file, log)
|
||||
|
||||
|
||||
@merge(
|
||||
input=[render_hocr_page, render_hocr_debug_page, skip_page,
|
||||
tesseract_ocr_and_render_pdf, generate_postscript_stub],
|
||||
output=os.path.join(work_folder, 'merged.pdf'),
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def merge_pages(
|
||||
input_files,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
|
||||
def input_file_order(s):
|
||||
'''Sort order: All rendered pages followed
|
||||
by their debug page, if any, followed by Postscript stub.
|
||||
Ghostscript documentation has the Postscript stub at the
|
||||
beginning, but it works at the end and also gets document info
|
||||
right that way.'''
|
||||
if s.endswith('.ps'):
|
||||
return 99999999
|
||||
key = int(os.path.basename(s)[0:6]) * 10
|
||||
if 'debug' in os.path.basename(s):
|
||||
key += 1
|
||||
return key
|
||||
|
||||
pdf_pages = sorted(input_files, key=input_file_order)
|
||||
log.info(pdf_pages)
|
||||
ghostscript.generate_pdfa(pdf_pages, output_file, options.jobs or 1)
|
||||
|
||||
|
||||
@transform(
|
||||
input=merge_pages,
|
||||
filter=formatter(),
|
||||
output=options.output_file,
|
||||
extras=[_log, _pdfinfo, _pdfinfo_lock])
|
||||
def copy_final(
|
||||
input_file,
|
||||
output_file,
|
||||
log,
|
||||
pdfinfo,
|
||||
pdfinfo_lock):
|
||||
shutil.copy(input_file, output_file)
|
||||
|
||||
|
||||
def validate_pdfa(
|
||||
input_file,
|
||||
log):
|
||||
|
||||
args_qpdf = [
|
||||
'qpdf',
|
||||
'--check',
|
||||
input_file
|
||||
]
|
||||
|
||||
try:
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2:
|
||||
print("{0}: not a valid PDF, and could not repair it.".format(
|
||||
options.input_file))
|
||||
print("Details:")
|
||||
print(e.output)
|
||||
elif e.returncode == 3:
|
||||
log.info("qpdf --check returned warnings:")
|
||||
log.info(e.output)
|
||||
else:
|
||||
print(e.output)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# @active_if(ocr_required and options.exact_image)
|
||||
# @merge([render_hocr_blank_page, extract_single_page],
|
||||
# os.path.join(work_folder, "%04i.merged.pdf") % pageno)
|
||||
# def merge_hocr_with_original_page(infiles, output_file):
|
||||
# with open(infiles[0], 'rb') as hocr_input, \
|
||||
# open(infiles[1], 'rb') as page_input, \
|
||||
# open(output_file, 'wb') as output:
|
||||
# hocr_reader = pypdf.PdfFileReader(hocr_input)
|
||||
# page_reader = pypdf.PdfFileReader(page_input)
|
||||
# writer = pypdf.PdfFileWriter()
|
||||
|
||||
# the_page = hocr_reader.getPage(0)
|
||||
# the_page.mergePage(page_reader.getPage(0))
|
||||
# writer.addPage(the_page)
|
||||
# writer.write(output)
|
||||
|
||||
|
||||
def available_cpu_count():
|
||||
try:
|
||||
return multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import psutil
|
||||
return psutil.cpu_count()
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
complain(
|
||||
"Could not get CPU count. Assuming one (1) CPU."
|
||||
"Use -j N to set manually.")
|
||||
return 1
|
||||
|
||||
|
||||
def run_pipeline():
|
||||
if not options.jobs or options.jobs == 1:
|
||||
options.jobs = available_cpu_count()
|
||||
|
||||
try:
|
||||
cmdline.run(options)
|
||||
except ruffus_exceptions.RethrownJobError as e:
|
||||
if options.verbose:
|
||||
print(e)
|
||||
|
||||
# Yuck. Hunt through the ruffus exception to find out what the
|
||||
# return code is supposed to be.
|
||||
for exc in e.args:
|
||||
task_name, job_name, exc_name, exc_value, exc_stack = exc
|
||||
if exc_name == 'builtins.SystemExit':
|
||||
return eval(
|
||||
exc_value,
|
||||
{'ExitCode': ExitCode}, {'exc_value': exc_value})
|
||||
return ExitCode.other_error
|
||||
|
||||
if not validate_pdfa(options.output_file, _log):
|
||||
_log.warning('Output file: The generated PDF/A file is INVALID')
|
||||
return ExitCode.invalid_output_pdfa
|
||||
|
||||
return ExitCode.ok
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(run_pipeline())
|
||||
from ocrmypdf.__main__ import *
|
||||
|
||||
+315
-45
@@ -3,10 +3,21 @@
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal, getcontext
|
||||
from math import hypot
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
|
||||
try:
|
||||
from math import isclose
|
||||
except ImportError:
|
||||
def isclose(a, b, rel_tol=1e-9):
|
||||
"Python 3.4 does not have math.isclose()"
|
||||
diff = abs(b - a)
|
||||
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
FRIENDLY_COLORSPACE = {
|
||||
'/DeviceGray': 'gray',
|
||||
@@ -19,7 +30,11 @@ FRIENDLY_COLORSPACE = {
|
||||
'/Indexed': 'index',
|
||||
'/Separation': 'sep',
|
||||
'/DeviceN': 'devn',
|
||||
'/Pattern': '-'
|
||||
'/Pattern': '-',
|
||||
'/G': 'gray', # Abbreviations permitted in inline images
|
||||
'/RGB': 'rgb',
|
||||
'/CMYK': 'cmyk',
|
||||
'/I': 'index',
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING = {
|
||||
@@ -27,6 +42,13 @@ FRIENDLY_ENCODING = {
|
||||
'/DCTDecode': 'jpeg',
|
||||
'/JPXDecode': 'jpx',
|
||||
'/JBIG2Decode': 'jbig2',
|
||||
'/CCF': 'ccitt', # Abbreviations permitted in inline images
|
||||
'/DCT': 'jpeg',
|
||||
'/AHx': 'asciihex',
|
||||
'/A85': 'ascii85',
|
||||
'/LZW': 'lzw',
|
||||
'/Fl': 'flate',
|
||||
'/RL': 'runlength'
|
||||
}
|
||||
|
||||
FRIENDLY_COMP = {
|
||||
@@ -38,50 +60,225 @@ FRIENDLY_COMP = {
|
||||
}
|
||||
|
||||
|
||||
def _page_has_inline_images(page):
|
||||
# PDF always uses \r\n for separator regardless of platform
|
||||
# Really basic heuristic that might trigger the odd false positive
|
||||
# This is only finds the first image and is not quite spec compliant
|
||||
try:
|
||||
contents = page.getContents()
|
||||
data = contents.getData()
|
||||
except AttributeError:
|
||||
# If we can't access the contents or data (empty page?) then there
|
||||
# are no inline images
|
||||
return False
|
||||
|
||||
begin_image, image_data, end_image = False, False, False
|
||||
for data in re.split(b'\s+', data):
|
||||
if data == b'BI':
|
||||
begin_image = True
|
||||
elif data == b'ID':
|
||||
image_data = True
|
||||
elif data == b'EI':
|
||||
end_image = True
|
||||
if all((begin_image, image_data, end_image)):
|
||||
return True
|
||||
return False
|
||||
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo):
|
||||
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])
|
||||
|
||||
RasterSettings = namedtuple('RasterSettings',
|
||||
['name', 'shorthand', 'stack_depth'])
|
||||
|
||||
InlineSettings = namedtuple('InlineSettings',
|
||||
['settings', 'shorthand', 'stack_depth'])
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _interpret_contents(contentstream):
|
||||
"""Interpret the PDF content stream
|
||||
|
||||
The stack represents the state of the PDF graphics stack. We are only
|
||||
interested in the current transformation matrix (CTM) so we only track
|
||||
this object; a full implementation would need to track many other items.
|
||||
|
||||
The CTM is initialized to the mapping from user space to device space.
|
||||
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
|
||||
to the transformation to device space. For example if set to
|
||||
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
|
||||
|
||||
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
|
||||
image there should be a 'cm' that sets up an image coordinate system
|
||||
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
|
||||
page.
|
||||
|
||||
PDF units suit our needs so we initialize ctm to the identity matrix.
|
||||
|
||||
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
|
||||
|
||||
"""
|
||||
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand(UNIT_SQUARE)
|
||||
image_raster_settings = []
|
||||
inline_images = []
|
||||
|
||||
for op in operations:
|
||||
operands, command = op
|
||||
if command == b'q':
|
||||
stack.append(ctm)
|
||||
if len(stack) > 32:
|
||||
raise RuntimeError("PDF graphics stack overflow")
|
||||
elif command == b'Q':
|
||||
ctm = stack.pop()
|
||||
elif command == b'cm':
|
||||
ctm = matrix_mult(
|
||||
_matrix_from_shorthand(operands), ctm)
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
raster = RasterSettings(
|
||||
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
image_raster_settings.append(raster)
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline = InlineSettings(
|
||||
settings=settings, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
inline_images.append(inline)
|
||||
|
||||
return ContentsInfo(
|
||||
raster_settings=image_raster_settings,
|
||||
inline_images=inline_images)
|
||||
|
||||
|
||||
def _get_dpi(ctm_shorthand, image_size):
|
||||
"""Given the transformation matrix and image size, find the image DPI.
|
||||
|
||||
PDFs do not include image resolution information within image data.
|
||||
Instead, the PDF page content stream describes the location where the
|
||||
image will be rasterized, and the effective resolution is the ratio of the
|
||||
pixel size to raster target size.
|
||||
|
||||
Normally a scanned PDF has the paper size set appropriately but this is
|
||||
not guaranteed. The most common case is a cropped image will change the
|
||||
page size (/CropBox) without altering the page content stream. That means
|
||||
it is not sufficient to assume that the image fills the page, even though
|
||||
that is the most common case.
|
||||
|
||||
A PDF image may be scaled (always), cropped, translated, rotated in place
|
||||
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
|
||||
be expressed, that is, it is not necessary to consider distortions where
|
||||
the effective DPI varies with position.
|
||||
|
||||
To determine the image scale, transform an offset axis vector v0 (0, 0),
|
||||
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
|
||||
which gives the dimensions of the image in PDF units. From there we can
|
||||
compare to actual image dimensions. PDF uses
|
||||
row vector * matrix_tranposed unlike the traditional
|
||||
matrix * column vector.
|
||||
|
||||
The offset, width and height vectors can be combined in a matrix and
|
||||
multiplied by the transform matrix. Then we want to calculated
|
||||
magnitude(width_vector - offset_vector)
|
||||
and
|
||||
magnitude(height_vector - offset_vector)
|
||||
|
||||
When the above is worked out algebraically, the effect of translation
|
||||
cancels out, and the vector magnitudes become functions of the nonzero
|
||||
transformation matrix indices. The results of the derivation are used
|
||||
in this code.
|
||||
|
||||
pdfimages -list does calculate the DPI in some way that is not completely
|
||||
naive, but it does not get the DPI of rotated images right, so cannot be
|
||||
used anymore to validate this. Photoshop works, or using Acrobat to
|
||||
rotate the image back to normal.
|
||||
|
||||
It does not matter if the image is partially cropped, or even out of the
|
||||
/MediaBox.
|
||||
|
||||
"""
|
||||
|
||||
a, b, c, d, _, _ = ctm_shorthand
|
||||
|
||||
# Calculate the width and height of the image in PDF units
|
||||
image_drawn_width = hypot(a, b)
|
||||
image_drawn_height = hypot(c, d)
|
||||
|
||||
# The scale of the image is pixels per PDF unit (1/72")
|
||||
scale_w = image_size[0] / image_drawn_width
|
||||
scale_h = image_size[1] / image_drawn_height
|
||||
|
||||
# DPI = scale * 72
|
||||
dpi_w = scale_w * 72.0
|
||||
dpi_h = scale_h * 72.0
|
||||
|
||||
return (dpi_w, dpi_h)
|
||||
|
||||
|
||||
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||
"Find inline images on the page"
|
||||
|
||||
for n, inline in enumerate(contentsinfo.inline_images):
|
||||
image = {}
|
||||
image['name'] = str('inline-%02d' % n)
|
||||
image['width'] = inline.settings['/W']
|
||||
image['height'] = inline.settings['/H']
|
||||
if '/BPC' in inline.settings:
|
||||
image['bpc'] = inline.settings['/BPC']
|
||||
else:
|
||||
image['bpc'] = 8
|
||||
image['color'] = FRIENDLY_COLORSPACE.get(inline.settings['/CS'], '-')
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
if '/F' in inline.settings:
|
||||
filter_ = inline.settings['/F']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
image['enc'] = 'image'
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
inline.shorthand, (image['width'], image['height']))
|
||||
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
"Find images stored in XObject resources"
|
||||
|
||||
try:
|
||||
page['/Resources']['/XObject']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
# Look for XObject (out of line images)
|
||||
for xobj in page['/Resources']['/XObject']:
|
||||
# PyPDF2 returns the keys as an iterator
|
||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
||||
if pdfimage['/Subtype'] != '/Image':
|
||||
continue
|
||||
if '/ImageMask' in pdfimage:
|
||||
if pdfimage['/ImageMask']:
|
||||
continue
|
||||
image = {}
|
||||
image['name'] = str(xobj)
|
||||
image['width'] = pdfimage['/Width']
|
||||
image['height'] = pdfimage['/Height']
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
if '/BitsPerComponent' in pdfimage:
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
else:
|
||||
image['bpc'] = 8
|
||||
|
||||
# Fixme: this is incorrectly treats explicit masks as stencil masks,
|
||||
# but good enough for now. Explicit masks have /ImageMask true but are
|
||||
# never called for in content stream, instead are drawn as a /Mask on
|
||||
# other images. For our purposes finding out the details of /Mask
|
||||
# will seldom matter.
|
||||
if '/ImageMask' in pdfimage:
|
||||
image['type'] = 'stencil' if pdfimage['/ImageMask'].value \
|
||||
else 'image'
|
||||
else:
|
||||
image['type'] = 'image'
|
||||
if '/Filter' in pdfimage:
|
||||
filter_ = pdfimage['/Filter']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
@@ -98,34 +295,93 @@ def _find_page_images(page, pageinfo):
|
||||
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
|
||||
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
image['dpi_w'] = image['width'] / pageinfo['width_inches']
|
||||
image['dpi_h'] = image['height'] / pageinfo['height_inches']
|
||||
|
||||
# Bit of a hack... infer grayscale if component count is uncertain
|
||||
# but encoding must be monochrome. This happens if a monochrome image
|
||||
# has an ICC profile attached. Better solution would be to examine
|
||||
# the ICC profile.
|
||||
if image['comp'] == '?' and image['enc'] in ('ccitt', 'jbig2'):
|
||||
image['comp'] = FRIENDLY_COMP['gray']
|
||||
|
||||
image['dpi_w'] = image['dpi_h'] = 0
|
||||
|
||||
for raster in contentsinfo.raster_settings:
|
||||
# Loop in case the same image is display multiple times on a page
|
||||
if raster.name != image['name']:
|
||||
continue
|
||||
|
||||
if raster.stack_depth == 0 and _is_unit_square(raster.shorthand):
|
||||
# At least one PDF in the wild (and test suite) draws an image
|
||||
# when the graphics stack depth is 0, meaning that the image
|
||||
# gets drawn into a square of 1x1 PDF units (or 1/72",
|
||||
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
|
||||
# these from our DPI calculation for the page.
|
||||
continue
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
raster.shorthand, (image['width'], image['height']))
|
||||
|
||||
# When image is used multiple times take the highest DPI it is
|
||||
# rendered at
|
||||
image['dpi_w'] = max(dpi_w, image.get('dpi_w', 0))
|
||||
image['dpi_h'] = max(dpi_h, image.get('dpi_h', 0))
|
||||
|
||||
image['dpi_w'] = Decimal(image['dpi_w'])
|
||||
image['dpi_h'] = Decimal(image['dpi_h'])
|
||||
image['dpi'] = (image['dpi_w'] * image['dpi_h']) ** Decimal(0.5)
|
||||
yield image
|
||||
|
||||
|
||||
def _pdf_get_pageinfo(infile, page: int):
|
||||
def _find_page_images(page, pageinfo, contentsinfo):
|
||||
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
|
||||
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
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(infile, pageno: int):
|
||||
pageinfo = {}
|
||||
pageinfo['pageno'] = page
|
||||
pageinfo['pageno'] = pageno
|
||||
pageinfo['images'] = []
|
||||
|
||||
pdf = pypdf.PdfFileReader(infile)
|
||||
page = pdf.pages[page - 1]
|
||||
page = pdf.pages[pageno]
|
||||
|
||||
text = page.extractText()
|
||||
pageinfo['has_text'] = (text.strip() != '')
|
||||
pageinfo['has_text'] = _page_has_text(pdf, page)
|
||||
|
||||
width_pt = page['/MediaBox'][2] - page['/MediaBox'][0]
|
||||
height_pt = page['/MediaBox'][3] - page['/MediaBox'][1]
|
||||
width_pt = page.mediaBox.getWidth()
|
||||
height_pt = page.mediaBox.getHeight()
|
||||
pageinfo['width_inches'] = width_pt / Decimal(72.0)
|
||||
pageinfo['height_inches'] = height_pt / Decimal(72.0)
|
||||
|
||||
pageinfo['images'] = [im for im in _find_page_images(page, pageinfo)]
|
||||
try:
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
except AttributeError as e:
|
||||
return pageinfo
|
||||
|
||||
# Look for inline images
|
||||
if _page_has_inline_images(page):
|
||||
raise NotImplementedError(
|
||||
"Warning: input PDF contains inline images - not supported")
|
||||
contentsinfo = _interpret_contents(contentstream)
|
||||
pageinfo['images'] = [im for im in _find_page_images(
|
||||
page, pageinfo, contentsinfo)]
|
||||
|
||||
if pageinfo['images']:
|
||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||
@@ -143,3 +399,17 @@ def pdf_get_all_pageinfo(infile):
|
||||
pdf = pypdf.PdfFileReader(infile)
|
||||
getcontext().prec = 6
|
||||
return [_pdf_get_pageinfo(infile, n) for n in range(pdf.numPages)]
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('infile')
|
||||
args = parser.parse_args()
|
||||
info = pdf_get_all_pageinfo(args.infile)
|
||||
from pprint import pprint
|
||||
pprint(info)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+69
-41
@@ -5,9 +5,14 @@
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
from string import Template
|
||||
from subprocess import Popen, PIPE
|
||||
import os
|
||||
import codecs
|
||||
import pkg_resources
|
||||
import PyPDF2 as pypdf
|
||||
|
||||
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
|
||||
|
||||
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
|
||||
'ocrmypdf', ICC_PROFILE_RELPATH)
|
||||
|
||||
|
||||
# This is a template written in PostScript which is needed to create PDF/A
|
||||
@@ -27,6 +32,7 @@ def
|
||||
/Author <$author>
|
||||
/Subject <$subject>
|
||||
/Keywords <$keywords>
|
||||
/Creator <$creator>
|
||||
/DOCINFO pdfmark
|
||||
|
||||
% Define an ICC profile :
|
||||
@@ -64,13 +70,20 @@ def encode_text_string(s: str) -> str:
|
||||
|
||||
From PDF 32000-1:2008 a string object may be included in hexademical form
|
||||
if it is enclosed in angle brackets. For general Unicode the string should
|
||||
be UTF-16 (big endian) with byte order marks. A non-hexademical
|
||||
representation is doable but this is preferable since it allows the output
|
||||
Postscript file to be completely ASCII and no escaping of Postscript
|
||||
characters is necessary.
|
||||
be UTF-16 (big endian) with byte order marks. Many strings including all
|
||||
ASCII strings fall could be encoded as PdfDocEncoding literals provided
|
||||
that certain Postscript sequences are escaped. But it's far simpler to
|
||||
encode everything as UTF-16.
|
||||
'''
|
||||
|
||||
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
||||
# insert into PDFs
|
||||
# tests/resources/aspect.pdf is one example (created by ImageMagick)
|
||||
s = s.replace('\x00', '')
|
||||
|
||||
if s == '':
|
||||
return ''
|
||||
|
||||
utf16_bytes = s.encode('utf-16be')
|
||||
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
|
||||
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
|
||||
@@ -83,50 +96,65 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
||||
t = Template(pdfa_def_template)
|
||||
result = t.substitute(icc_profile=icc_profile,
|
||||
icc_identifier=icc_identifier,
|
||||
title=pdfmark_utf16.get('title', ''),
|
||||
author=pdfmark_utf16.get('author', ''),
|
||||
subject=pdfmark_utf16.get('subject', ''),
|
||||
keywords=pdfmark_utf16.get('keywords', ''))
|
||||
title=pdfmark_utf16.get('/Title', ''),
|
||||
author=pdfmark_utf16.get('/Author', ''),
|
||||
subject=pdfmark_utf16.get('/Subject', ''),
|
||||
creator=pdfmark_utf16.get('/Creator', ''),
|
||||
keywords=pdfmark_utf16.get('/Keywords', ''))
|
||||
return result
|
||||
|
||||
|
||||
def _get_postscript_icc_path():
|
||||
"Parse Ghostscript's help message to find where iccprofiles are stored"
|
||||
|
||||
p_gs = Popen(['gs', '--help'], close_fds=True, universal_newlines=True,
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
out, _ = p_gs.communicate()
|
||||
lines = out.splitlines()
|
||||
|
||||
def search_paths(lines):
|
||||
seeking = True
|
||||
for line in lines:
|
||||
if seeking:
|
||||
if line.startswith('Search path'):
|
||||
seeking = False
|
||||
continue
|
||||
else:
|
||||
if line.strip().startswith('/'):
|
||||
yield from (
|
||||
path.strip() for path in line.split(':')
|
||||
if path.strip() != '')
|
||||
for root in search_paths(lines):
|
||||
path = os.path.realpath(os.path.join(root, '../iccprofiles'))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
raise FileNotFoundError("Could not find Ghostscript's iccprofiles")
|
||||
|
||||
|
||||
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
|
||||
if icc == 'sRGB':
|
||||
icc_profile = os.path.join(_get_postscript_icc_path(), 'srgb.icc')
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
else:
|
||||
raise NotImplementedError("Only supporting sRGB")
|
||||
|
||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
||||
|
||||
# Since PostScript might not handle UTF-8 (it's hard to get a clear
|
||||
# answer), insist on ascii
|
||||
# We should have encoded everything to pure ASCII by this point, and
|
||||
# to be safe, only allow ASCII in PostScript
|
||||
with open(target_filename, 'w', encoding='ascii') as f:
|
||||
f.write(ps)
|
||||
|
||||
|
||||
def file_claims_pdfa(filename):
|
||||
"""Determines if the file claims to be PDF/A compliant
|
||||
|
||||
Checking if a file is a truly compliant PDF/A is a massive undertaking
|
||||
that no open source tool does properly. Some commercial tools are
|
||||
generally reliable (Acrobat).
|
||||
|
||||
This checks if the XMP metadata contains a PDF/A marker.
|
||||
"""
|
||||
|
||||
pdf = pypdf.PdfFileReader(filename)
|
||||
xmp = pdf.getXmpMetadata()
|
||||
|
||||
try:
|
||||
pdfa_nodes = xmp.getNodesInNamespace(
|
||||
aboutUri='',
|
||||
namespace='http://www.aiim.org/pdfa/ns/id/')
|
||||
except AttributeError:
|
||||
return {'pass': False, 'output': 'pdf',
|
||||
'conformance': 'No XMP metadata'}
|
||||
|
||||
pdfa_dict = {attr.localName: attr.value for attr in pdfa_nodes}
|
||||
pdfa_dict['pass'] = False
|
||||
pdfa_dict['output'] = 'pdf'
|
||||
if pdfa_dict:
|
||||
part_conformance = pdfa_dict['part'] + pdfa_dict['conformance']
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
|
||||
conformance = 'PDF/A-{}'.format(
|
||||
part_conformance)
|
||||
|
||||
if part_conformance in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
else:
|
||||
pdfa_dict['conformance'] = 'PDF'
|
||||
|
||||
return pdfa_dict
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import CalledProcessError, check_output, STDOUT, check_call
|
||||
from functools import lru_cache
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from . import ExitCode, get_program
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_qpdf = [
|
||||
get_program('qpdf'),
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_qpdf, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError:
|
||||
print("Could not find qpdf executable on system PATH.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
qpdf_version = re.match(r'qpdf version (.+)', versions).group(1)
|
||||
return qpdf_version
|
||||
|
||||
|
||||
def check(input_file, log):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'),
|
||||
'--check',
|
||||
input_file
|
||||
]
|
||||
|
||||
try:
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2:
|
||||
log.error("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
log.error("Details:")
|
||||
log.error(e.output)
|
||||
elif e.returncode == 3:
|
||||
log.info("qpdf --check returned warnings:")
|
||||
log.info(e.output)
|
||||
else:
|
||||
log.warning(e.output)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def repair(input_file, output_file, log):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_file, output_file
|
||||
]
|
||||
try:
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 3 and e.output.find("operation succeeded"):
|
||||
log.debug('qpdf found and fixed errors: ' + e.output)
|
||||
log.debug(e.output)
|
||||
return
|
||||
|
||||
if e.returncode == 2 and e.output.find("invalid password"):
|
||||
log.error("{0}: this PDF is password-protected - password must "
|
||||
"be removed for OCR".format(input_file))
|
||||
sys.exit(ExitCode.input_file)
|
||||
elif e.returncode == 2:
|
||||
log.error("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
log.error("Details: " + e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
else:
|
||||
log.error("{0}: unknown error".format(
|
||||
input_file))
|
||||
log.error(e.output)
|
||||
sys.exit(ExitCode.unknown)
|
||||
|
||||
|
||||
def get_npages(input_file, log):
|
||||
try:
|
||||
pages = check_output(
|
||||
[get_program('qpdf'), '--show-npages', input_file],
|
||||
universal_newlines=True, close_fds=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2 and e.output.find('No such file'):
|
||||
log.error(e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
return int(pages)
|
||||
|
||||
|
||||
def split_pages(input_file, work_folder, npages):
|
||||
"""Split multipage PDF into individual pages.
|
||||
|
||||
Incredibly enough, this multiple process approach is about 70 times
|
||||
faster than using Ghostscript.
|
||||
"""
|
||||
for n in range(int(npages)):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_file,
|
||||
'--pages', input_file, '{0}'.format(n + 1), '--',
|
||||
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
||||
]
|
||||
check_call(args_qpdf)
|
||||
|
||||
|
||||
def merge(input_files, output_file):
|
||||
"""Merge the list of input files (all filenames) into the output file.
|
||||
|
||||
The input files may contain one or more pages.
|
||||
"""
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_files[0], '--pages'
|
||||
] + input_files + ['--', output_file]
|
||||
check_call(args_qpdf)
|
||||
|
||||
+225
-38
@@ -1,50 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import STDOUT, CalledProcessError, check_output
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from . import ExitCode
|
||||
from . import ExitCode, get_program, page_number
|
||||
from collections import namedtuple
|
||||
|
||||
from subprocess import Popen, PIPE, CalledProcessError, \
|
||||
TimeoutExpired, check_output, STDOUT, DEVNULL
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_tess = [
|
||||
'tesseract',
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError:
|
||||
print("Could not find Tesseract executable on system PATH.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
tesseract_version = re.match(r'tesseract\s(.+)', versions).group(1)
|
||||
return tesseract_version
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def languages():
|
||||
args_tess = [
|
||||
'tesseract',
|
||||
'--list-langs'
|
||||
]
|
||||
try:
|
||||
langs = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError as e:
|
||||
print("Tesseract failed to report available languages.")
|
||||
print("Output from Tesseract:")
|
||||
print("-" * 40)
|
||||
print(e.output)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
return set(lang.strip() for lang in langs.splitlines()[1:])
|
||||
|
||||
OrientationConfidence = namedtuple(
|
||||
'OrientationConfidence',
|
||||
('angle', 'confidence'))
|
||||
|
||||
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
@@ -67,3 +38,219 @@ HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_tess = [
|
||||
get_program('tesseract'),
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError:
|
||||
print("Could not find Tesseract executable on system PATH.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
tesseract_version = re.match(r'tesseract\s(.+)', versions).group(1)
|
||||
return tesseract_version
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def languages():
|
||||
args_tess = [
|
||||
get_program('tesseract'),
|
||||
'--list-langs'
|
||||
]
|
||||
try:
|
||||
langs = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError as e:
|
||||
print("Tesseract failed to report available languages.")
|
||||
print("Output from Tesseract:")
|
||||
print("-" * 40)
|
||||
print(e.output)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
return set(lang.strip() for lang in langs.splitlines()[1:])
|
||||
|
||||
|
||||
def get_orientation(input_file, language: list, timeout: float, log):
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language),
|
||||
'-psm', '0',
|
||||
input_file,
|
||||
'stdout'
|
||||
]
|
||||
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
return OrientationConfidence(angle=0, confidence=0.0)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if ('Too few characters. Skipping this page' in e.output or
|
||||
'Image too large' in e.output):
|
||||
return OrientationConfidence(0, 0)
|
||||
raise e from e
|
||||
else:
|
||||
osd = {}
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
parts = line.split(':', maxsplit=2)
|
||||
if len(parts) == 2:
|
||||
osd[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
angle = int(osd.get('Orientation in degrees', 0))
|
||||
if 'Orientation' in osd:
|
||||
# Tesseract < 3.04.01
|
||||
# reports "Orientation in degrees" as a counterclockwise angle
|
||||
# We keep it clockwise
|
||||
assert 'Rotate' not in osd
|
||||
angle = -angle % 360
|
||||
else:
|
||||
# Tesseract == 3.04.01, hopefully also Tesseract > 3.04.01
|
||||
# reports "Orientation in degrees" as a clockwise angle
|
||||
assert 'Rotate' in osd
|
||||
|
||||
oc = OrientationConfidence(
|
||||
angle=angle,
|
||||
confidence=float(osd.get('Orientation confidence', 0)))
|
||||
return oc
|
||||
|
||||
|
||||
def tesseract_log_output(log, stdout, input_file):
|
||||
lines = stdout.splitlines()
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
for line in lines:
|
||||
if line.startswith("Tesseract Open Source"):
|
||||
continue
|
||||
elif line.startswith("Warning in pixReadMem"):
|
||||
continue
|
||||
elif 'diacritics' in line:
|
||||
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
||||
elif line.startswith('OSD: Weak margin'):
|
||||
log.warning(prefix + "unsure about page orientation")
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
else:
|
||||
log.info(prefix + line.strip())
|
||||
|
||||
|
||||
def page_timedout(log, input_file):
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, pageinfo):
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
timeout: float, pageinfo_getter, pagesegmode: int, log):
|
||||
|
||||
badxml = os.path.splitext(output_hocr)[0] + '.badxml'
|
||||
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language)
|
||||
]
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend(['-psm', str(pagesegmode)])
|
||||
|
||||
args_tesseract.extend([
|
||||
input_file,
|
||||
badxml,
|
||||
'hocr'
|
||||
] + tessconfig)
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
# Generate a HOCR file with no recognized text if tesseract times out
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if 'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
return
|
||||
|
||||
raise e from e
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
tessconfig: list, timeout: float, pagesegmode: int, log):
|
||||
'''Use Tesseract to render a PDF.
|
||||
|
||||
input_image -- image to analyze
|
||||
skip_pdf -- if we time out, use this file as output
|
||||
language -- list of languages to consider
|
||||
tessconfig -- tesseract configuration
|
||||
timeout -- timeout (seconds)
|
||||
log -- logger object
|
||||
'''
|
||||
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language)
|
||||
]
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend(['-psm', str(pagesegmode)])
|
||||
|
||||
args_tesseract.extend([
|
||||
input_image,
|
||||
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
] + tessconfig)
|
||||
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
page_timedout(log, input_image)
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_image)
|
||||
if 'Image too large' in e.output:
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
return
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_image)
|
||||
|
||||
+18
-17
@@ -3,24 +3,23 @@
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import CalledProcessError, STDOUT, check_output, check_call
|
||||
from tempfile import NamedTemporaryFile
|
||||
import sys
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from . import ExitCode
|
||||
from . import ExitCode, get_program
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_unpaper = [
|
||||
'unpaper',
|
||||
get_program('unpaper'),
|
||||
'--version'
|
||||
]
|
||||
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
version, _ = p_unpaper.communicate(timeout=5)
|
||||
|
||||
version = check_output(
|
||||
args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT, timeout=5)
|
||||
return version.strip()
|
||||
|
||||
|
||||
@@ -33,7 +32,7 @@ except ImportError:
|
||||
|
||||
def run(input_file, output_file, dpi, log, mode_args):
|
||||
args_unpaper = [
|
||||
'unpaper',
|
||||
get_program('unpaper'),
|
||||
'-v',
|
||||
'--dpi', str(dpi)
|
||||
] + mode_args
|
||||
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
|
||||
os.unlink(output_pnm.name)
|
||||
|
||||
args_unpaper.extend([input_pnm.name, output_pnm.name])
|
||||
p_unpaper = Popen(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stdout=PIPE, stderr=PIPE
|
||||
)
|
||||
out, err = p_unpaper.communicate()
|
||||
log.debug(out)
|
||||
log.debug(err)
|
||||
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stderr=STDOUT,
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
log.debug(e.output)
|
||||
raise e from e
|
||||
else:
|
||||
log.debug(stdout)
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
|
||||
|
||||
def deskew(input_file, output_file, dpi, log):
|
||||
|
||||
+268
-165
@@ -4,232 +4,335 @@
|
||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
||||
-->
|
||||
<!-- Title: Pipeline: Pages: 1 -->
|
||||
<svg width="728pt" height="651pt"
|
||||
viewBox="0.00 0.00 728.00 650.53" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 646.53)">
|
||||
<svg width="1444pt" height="973pt"
|
||||
viewBox="0.00 0.00 1444.00 973.18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 969.181)">
|
||||
<title>Pipeline:</title>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-646.53 724,-646.53 724,4 -4,4"/>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-969.181 1440,-969.181 1440,4 -4,4"/>
|
||||
<g id="clust1" class="cluster"><title>clustertasks</title>
|
||||
<polygon fill="none" stroke="black" points="8,-8 8,-634.53 712,-634.53 712,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="360" y="-606.53" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
|
||||
<polygon fill="none" stroke="black" points="8,-8 8,-957.181 1428,-957.181 1428,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="718" y="-929.181" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
|
||||
</g>
|
||||
<!-- t0 -->
|
||||
<g id="node1" class="node"><title>t0</title>
|
||||
<polygon fill="#efa03b" stroke="#006000" points="481.791,-588.53 386.209,-588.53 382.209,-584.53 382.209,-552.53 477.791,-552.53 481.791,-556.53 481.791,-588.53"/>
|
||||
<polyline fill="none" stroke="#006000" points="477.791,-584.53 382.209,-584.53 "/>
|
||||
<polyline fill="none" stroke="#006000" points="477.791,-584.53 477.791,-552.53 "/>
|
||||
<polyline fill="none" stroke="#006000" points="477.791,-584.53 481.791,-588.53 "/>
|
||||
<text text-anchor="middle" x="432" y="-564.53" font-family="Times,serif" font-size="20.00" fill="#006000">repair_pdf</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1219.77,-911.181 1034.23,-911.181 1030.23,-907.181 1030.23,-875.181 1215.77,-875.181 1219.77,-879.181 1219.77,-911.181"/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1030.23,-907.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1215.77,-875.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1219.77,-911.181 "/>
|
||||
<text text-anchor="middle" x="1125" y="-887.181" font-family="Times,serif" font-size="20.00">triage</text>
|
||||
</g>
|
||||
<!-- t1 -->
|
||||
<g id="node2" class="node"><title>t1</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="466.782,-509.564 374,-526.497 281.218,-509.564 281.304,-482.165 466.696,-482.165 466.782,-509.564"/>
|
||||
<polygon fill="none" stroke="black" points="470.799,-512.902 374,-530.569 277.201,-512.902 277.311,-478.159 470.689,-478.159 470.799,-512.902"/>
|
||||
<text text-anchor="middle" x="374" y="-495.991" font-family="Times,serif" font-size="20.00">split_pages</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1238.54,-853.181 1015.46,-853.181 1011.46,-849.181 1011.46,-817.181 1234.54,-817.181 1238.54,-821.181 1238.54,-853.181"/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1011.46,-849.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1234.54,-817.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1238.54,-853.181 "/>
|
||||
<text text-anchor="middle" x="1125" y="-829.181" font-family="Times,serif" font-size="20.00">repair_pdf</text>
|
||||
</g>
|
||||
<!-- t0->t1 -->
|
||||
<g id="edge1" class="edge"><title>t0->t1</title>
|
||||
<path fill="none" stroke="gray" d="M417.064,-552.394C412.296,-546.925 406.86,-540.689 401.493,-534.532"/>
|
||||
<polygon fill="gray" stroke="gray" points="403.996,-532.077 394.787,-526.838 398.719,-536.676 403.996,-532.077"/>
|
||||
</g>
|
||||
<!-- t10 -->
|
||||
<g id="node12" class="node"><title>t10</title>
|
||||
<polygon fill="#efa03b" stroke="#006000" points="704.338,-451.452 493.662,-451.452 489.662,-447.452 489.662,-415.452 700.338,-415.452 704.338,-419.452 704.338,-451.452"/>
|
||||
<polyline fill="none" stroke="#006000" points="700.338,-447.452 489.662,-447.452 "/>
|
||||
<polyline fill="none" stroke="#006000" points="700.338,-447.452 700.338,-415.452 "/>
|
||||
<polyline fill="none" stroke="#006000" points="700.338,-447.452 704.338,-451.452 "/>
|
||||
<text text-anchor="middle" x="597" y="-427.452" font-family="Times,serif" font-size="20.00" fill="#006000">generate_postscript_stub</text>
|
||||
</g>
|
||||
<!-- t0->t10 -->
|
||||
<g id="edge16" class="edge"><title>t0->t10</title>
|
||||
<path fill="none" stroke="gray" d="M453.048,-552.468C461.441,-545.654 471.185,-537.73 480,-530.53 510.227,-505.84 544.753,-477.467 568.421,-457.99"/>
|
||||
<polygon fill="gray" stroke="gray" points="570.818,-460.55 576.315,-451.493 566.369,-455.146 570.818,-460.55"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1125,-875.075C1125,-871.384 1125,-867.394 1125,-863.424"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-863.182 1125,-853.182 1121.5,-863.182 1128.5,-863.182"/>
|
||||
</g>
|
||||
<!-- t2 -->
|
||||
<g id="node3" class="node"><title>t2</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="451.555,-451.452 228.445,-451.452 224.445,-447.452 224.445,-415.452 447.555,-415.452 451.555,-419.452 451.555,-451.452"/>
|
||||
<polyline fill="none" stroke="black" points="447.555,-447.452 224.445,-447.452 "/>
|
||||
<polyline fill="none" stroke="black" points="447.555,-447.452 447.555,-415.452 "/>
|
||||
<polyline fill="none" stroke="black" points="447.555,-447.452 451.555,-451.452 "/>
|
||||
<text text-anchor="middle" x="338" y="-427.452" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1329.11,-774.253 1125,-791.156 920.888,-774.253 921.078,-746.905 1328.92,-746.905 1329.11,-774.253"/>
|
||||
<polygon fill="none" stroke="black" points="1333.13,-777.932 1125,-795.167 916.866,-777.932 917.11,-742.906 1332.89,-742.906 1333.13,-777.932"/>
|
||||
<text text-anchor="middle" x="1125" y="-760.694" font-family="Times,serif" font-size="20.00">split_pages</text>
|
||||
</g>
|
||||
<!-- t1->t2 -->
|
||||
<g id="edge2" class="edge"><title>t1->t2</title>
|
||||
<path fill="none" stroke="#0044a0" d="M361.611,-478.092C358.514,-472.369 355.171,-466.19 352.003,-460.333"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="355.064,-458.636 347.227,-451.506 348.907,-461.967 355.064,-458.636"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1125,-817.059C1125,-813.44 1125,-809.484 1125,-805.438"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-805.334 1125,-795.334 1121.5,-805.334 1128.5,-805.334"/>
|
||||
</g>
|
||||
<!-- t11 -->
|
||||
<g id="node10" class="node"><title>t11</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="644.594,-393.452 551.406,-393.452 547.406,-389.452 547.406,-357.452 640.594,-357.452 644.594,-361.452 644.594,-393.452"/>
|
||||
<polyline fill="none" stroke="black" points="640.594,-389.452 547.406,-389.452 "/>
|
||||
<polyline fill="none" stroke="black" points="640.594,-389.452 640.594,-357.452 "/>
|
||||
<polyline fill="none" stroke="black" points="640.594,-389.452 644.594,-393.452 "/>
|
||||
<text text-anchor="middle" x="596" y="-369.452" font-family="Times,serif" font-size="20.00">skip_page</text>
|
||||
<!-- t15 -->
|
||||
<g id="node17" class="node"><title>t15</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="632.082,-716.208 293.918,-716.208 289.918,-712.208 289.918,-680.208 628.082,-680.208 632.082,-684.208 632.082,-716.208"/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 289.918,-712.208 "/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 628.082,-680.208 "/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 632.082,-716.208 "/>
|
||||
<text text-anchor="middle" x="461" y="-692.208" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
|
||||
</g>
|
||||
<!-- t1->t11 -->
|
||||
<g id="edge13" class="edge"><title>t1->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M425.139,-478.007C437.834,-470.731 450.731,-461.83 461,-451.452 473.874,-438.442 466.946,-427.178 481,-415.452 490.212,-407.766 513.965,-399.205 537.508,-392.067"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="538.592,-395.396 547.189,-389.203 536.607,-388.683 538.592,-395.396"/>
|
||||
<!-- t1->t15 -->
|
||||
<g id="edge23" class="edge"><title>t1->t15</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1026.3,-817.165C989.157,-810.572 946.587,-802.788 908,-795.181 781.899,-770.319 636.479,-738.51 546.633,-718.49"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="547.289,-715.05 536.767,-716.289 545.765,-721.882 547.289,-715.05"/>
|
||||
</g>
|
||||
<!-- t9 -->
|
||||
<g id="node11" class="node"><title>t9</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="272.496,-277.452 19.5039,-277.452 15.5039,-273.452 15.5039,-241.452 268.496,-241.452 272.496,-245.452 272.496,-277.452"/>
|
||||
<polyline fill="none" stroke="black" points="268.496,-273.452 15.5039,-273.452 "/>
|
||||
<polyline fill="none" stroke="black" points="268.496,-273.452 268.496,-241.452 "/>
|
||||
<polyline fill="none" stroke="black" points="268.496,-273.452 272.496,-277.452 "/>
|
||||
<text text-anchor="middle" x="144" y="-253.452" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
|
||||
<!-- t18 -->
|
||||
<g id="node19" class="node"><title>t18</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1025.79,-156.452 1221,-129.134 1416.21,-156.452 1416.03,-200.654 1025.97,-200.654 1025.79,-156.452"/>
|
||||
<polygon fill="none" stroke="black" points="1021.78,-152.971 1221,-125.091 1420.22,-152.971 1420.01,-204.657 1021.99,-204.657 1021.78,-152.971"/>
|
||||
<text text-anchor="middle" x="1221" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_qpdf</text>
|
||||
</g>
|
||||
<!-- t1->t9 -->
|
||||
<g id="edge15" class="edge"><title>t1->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M277.341,-486.044C254.708,-478.702 232.21,-467.759 215,-451.452 168.207,-407.114 152.056,-329.127 146.633,-287.855"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="150.084,-287.235 145.422,-277.721 143.133,-288.065 150.084,-287.235"/>
|
||||
<!-- t1->t18 -->
|
||||
<g id="edge33" class="edge"><title>t1->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1238.67,-826.639C1321.4,-814.236 1419,-782.049 1419,-699.208 1419,-699.208 1419,-699.208 1419,-319.208 1419,-280.294 1424,-263.161 1398,-234.208 1389.64,-224.894 1379.82,-216.923 1369.17,-210.101"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1370.83,-207.019 1360.46,-204.872 1367.23,-213.02 1370.83,-207.019"/>
|
||||
</g>
|
||||
<!-- t3 -->
|
||||
<g id="node4" class="node"><title>t3</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="458.999,-393.452 291.001,-393.452 287.001,-389.452 287.001,-357.452 454.999,-357.452 458.999,-361.452 458.999,-393.452"/>
|
||||
<polyline fill="none" stroke="black" points="454.999,-389.452 287.001,-389.452 "/>
|
||||
<polyline fill="none" stroke="black" points="454.999,-389.452 454.999,-357.452 "/>
|
||||
<polyline fill="none" stroke="black" points="454.999,-389.452 458.999,-393.452 "/>
|
||||
<text text-anchor="middle" x="373" y="-369.452" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1186.88,-716.208 905.117,-716.208 901.117,-712.208 901.117,-680.208 1182.88,-680.208 1186.88,-684.208 1186.88,-716.208"/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 901.117,-712.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 1182.88,-680.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 1186.88,-716.208 "/>
|
||||
<text text-anchor="middle" x="1044" y="-692.208" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
|
||||
</g>
|
||||
<!-- t2->t3 -->
|
||||
<g id="edge3" class="edge"><title>t2->t3</title>
|
||||
<path fill="none" stroke="#0044a0" d="M348.691,-415.346C351.337,-411.112 354.23,-406.485 357.066,-401.946"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="360.042,-403.788 362.374,-393.453 354.106,-400.078 360.042,-403.788"/>
|
||||
</g>
|
||||
<!-- t6 -->
|
||||
<g id="node6" class="node"><title>t6</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="664.375,-277.452 477.625,-277.452 473.625,-273.452 473.625,-241.452 660.375,-241.452 664.375,-245.452 664.375,-277.452"/>
|
||||
<polyline fill="none" stroke="black" points="660.375,-273.452 473.625,-273.452 "/>
|
||||
<polyline fill="none" stroke="black" points="660.375,-273.452 660.375,-241.452 "/>
|
||||
<polyline fill="none" stroke="black" points="660.375,-273.452 664.375,-277.452 "/>
|
||||
<text text-anchor="middle" x="569" y="-253.452" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
|
||||
</g>
|
||||
<!-- t2->t6 -->
|
||||
<g id="edge7" class="edge"><title>t2->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M423.032,-415.374C438.812,-409.925 454.543,-402.785 468,-393.452 508.201,-365.571 539.287,-316.662 555.808,-286.558"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="559.024,-287.965 560.653,-277.496 552.851,-284.664 559.024,-287.965"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1097.12,-742.813C1089.24,-736.345 1080.66,-729.296 1072.73,-722.788"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1074.71,-719.888 1064.76,-716.249 1070.27,-725.299 1074.71,-719.888"/>
|
||||
</g>
|
||||
<!-- t4 -->
|
||||
<g id="node5" class="node"><title>t4</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="449.705,-335.452 300.295,-335.452 296.295,-331.452 296.295,-299.452 445.705,-299.452 449.705,-303.452 449.705,-335.452"/>
|
||||
<polyline fill="none" stroke="black" points="445.705,-331.452 296.295,-331.452 "/>
|
||||
<polyline fill="none" stroke="black" points="445.705,-331.452 445.705,-299.452 "/>
|
||||
<polyline fill="none" stroke="black" points="445.705,-331.452 449.705,-335.452 "/>
|
||||
<text text-anchor="middle" x="373" y="-311.452" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="1258.15,-658.208 1023.85,-658.208 1019.85,-654.208 1019.85,-622.208 1254.15,-622.208 1258.15,-626.208 1258.15,-658.208"/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1019.85,-654.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1254.15,-622.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1258.15,-658.208 "/>
|
||||
<text text-anchor="middle" x="1139" y="-634.208" font-family="Times,serif" font-size="20.00">orient_page</text>
|
||||
</g>
|
||||
<!-- t2->t4 -->
|
||||
<g id="edge5" class="edge"><title>t2->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1171.65,-742.795C1181.42,-735.692 1190.44,-726.864 1196,-716.208 1203.4,-702.023 1203.46,-694.36 1196,-680.208 1192.84,-674.222 1188.31,-668.98 1183.15,-664.445"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1185.3,-661.676 1175.25,-658.302 1181,-667.203 1185.3,-661.676"/>
|
||||
</g>
|
||||
<!-- t3->t4 -->
|
||||
<g id="edge4" class="edge"><title>t3->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M373,-357.346C373,-353.655 373,-349.665 373,-345.695"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="376.5,-345.453 373,-335.453 369.5,-345.453 376.5,-345.453"/>
|
||||
</g>
|
||||
<!-- t3->t6 -->
|
||||
<g id="edge6" class="edge"><title>t3->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M414.399,-357.412C428.785,-351.039 444.852,-343.405 459,-335.452 466.909,-331.007 506.49,-303.805 535.921,-283.431"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="538.073,-286.198 544.299,-277.625 534.086,-280.444 538.073,-286.198"/>
|
||||
</g>
|
||||
<!-- t4->t6 -->
|
||||
<g id="edge5" class="edge"><title>t4->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M432.605,-299.422C453.701,-293.395 477.617,-286.561 499.465,-280.319"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="500.687,-283.61 509.341,-277.497 498.764,-276.879 500.687,-283.61"/>
|
||||
<path fill="none" stroke="#0044a0" d="M1073.02,-680.102C1081.95,-674.836 1091.92,-668.963 1101.34,-663.407"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1103.32,-666.302 1110.16,-658.209 1099.77,-660.272 1103.32,-666.302"/>
|
||||
</g>
|
||||
<!-- t5 -->
|
||||
<g id="node7" class="node"><title>t5</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="455.922,-277.452 294.078,-277.452 290.078,-273.452 290.078,-241.452 451.922,-241.452 455.922,-245.452 455.922,-277.452"/>
|
||||
<polyline fill="none" stroke="black" points="451.922,-273.452 290.078,-273.452 "/>
|
||||
<polyline fill="none" stroke="black" points="451.922,-273.452 451.922,-241.452 "/>
|
||||
<polyline fill="none" stroke="black" points="451.922,-273.452 455.922,-277.452 "/>
|
||||
<text text-anchor="middle" x="373" y="-253.452" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
|
||||
<g id="node6" class="node"><title>t5</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1271.3,-600.208 920.701,-600.208 916.701,-596.208 916.701,-564.208 1267.3,-564.208 1271.3,-568.208 1271.3,-600.208"/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 916.701,-596.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 1267.3,-564.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 1271.3,-600.208 "/>
|
||||
<text text-anchor="middle" x="1094" y="-576.208" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
|
||||
</g>
|
||||
<!-- t4->t5 -->
|
||||
<g id="edge8" class="edge"><title>t4->t5</title>
|
||||
<path fill="none" stroke="#0044a0" d="M373,-299.346C373,-295.655 373,-291.665 373,-287.695"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="376.5,-287.453 373,-277.453 369.5,-287.453 376.5,-287.453"/>
|
||||
<g id="edge6" class="edge"><title>t4->t5</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1125.25,-622.102C1121.76,-617.759 1117.94,-613.003 1114.21,-608.353"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1116.65,-605.812 1107.66,-600.209 1111.2,-610.197 1116.65,-605.812"/>
|
||||
</g>
|
||||
<!-- t4->t9 -->
|
||||
<g id="edge14" class="edge"><title>t4->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M303.36,-299.422C278.157,-293.259 249.509,-286.253 223.521,-279.898"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="224.249,-276.473 213.704,-277.497 222.586,-283.273 224.249,-276.473"/>
|
||||
<!-- t10 -->
|
||||
<g id="node12" class="node"><title>t10</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="334,-348.208 35.7812,-320.208 334,-292.208 632.219,-320.208 334,-348.208"/>
|
||||
<text text-anchor="middle" x="334" y="-314.208" font-family="Times,serif" font-size="20.00">select_image_layer</text>
|
||||
</g>
|
||||
<!-- t4->t10 -->
|
||||
<g id="edge15" class="edge"><title>t4->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1019.87,-634.085C799.18,-622.991 349,-592.156 349,-525.208 349,-525.208 349,-525.208 349,-465.208 349,-428.606 344.147,-387.052 339.961,-358.022"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="343.393,-357.303 338.46,-347.926 336.469,-358.332 343.393,-357.303"/>
|
||||
</g>
|
||||
<!-- t16 -->
|
||||
<g id="node15" class="node"><title>t16</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1380.34,-270.208 1159.66,-270.208 1155.66,-266.208 1155.66,-234.208 1376.34,-234.208 1380.34,-238.208 1380.34,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1155.66,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1376.34,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1380.34,-270.208 "/>
|
||||
<text text-anchor="middle" x="1268" y="-246.208" font-family="Times,serif" font-size="20.00">skip_page</text>
|
||||
</g>
|
||||
<!-- t4->t16 -->
|
||||
<g id="edge20" class="edge"><title>t4->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1258.35,-631.473C1326.7,-619.809 1399,-591.745 1399,-525.208 1399,-525.208 1399,-525.208 1399,-397.208 1399,-350.33 1418.37,-328.748 1389,-292.208 1383.58,-285.469 1376.99,-279.879 1369.71,-275.244"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1371.3,-272.123 1360.88,-270.231 1367.85,-278.211 1371.3,-272.123"/>
|
||||
</g>
|
||||
<!-- t14 -->
|
||||
<g id="node16" class="node"><title>t14</title>
|
||||
<polygon fill="#66ccff" stroke="black" points="1380.24,-338.208 999.76,-338.208 995.76,-334.208 995.76,-302.208 1376.24,-302.208 1380.24,-306.208 1380.24,-338.208"/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 995.76,-334.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 1376.24,-302.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 1380.24,-338.208 "/>
|
||||
<text text-anchor="middle" x="1188" y="-314.208" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
|
||||
</g>
|
||||
<!-- t4->t14 -->
|
||||
<g id="edge22" class="edge"><title>t4->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1244.5,-622.179C1297.25,-607.003 1350,-578.503 1350,-525.208 1350,-525.208 1350,-525.208 1350,-465.208 1350,-422.752 1366.76,-403.169 1340,-370.208 1330.4,-358.377 1317.91,-349.368 1304.22,-342.513"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1305.62,-339.304 1295.07,-338.313 1302.7,-345.666 1305.62,-339.304"/>
|
||||
</g>
|
||||
<!-- t6 -->
|
||||
<g id="node7" class="node"><title>t6</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1074.74,-542.208 779.258,-542.208 775.258,-538.208 775.258,-506.208 1070.74,-506.208 1074.74,-510.208 1074.74,-542.208"/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 775.258,-538.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 1070.74,-506.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 1074.74,-542.208 "/>
|
||||
<text text-anchor="middle" x="925" y="-518.208" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
|
||||
</g>
|
||||
<!-- t5->t6 -->
|
||||
<g id="edge7" class="edge"><title>t5->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1042.38,-564.102C1024.52,-558.185 1004.35,-551.5 985.823,-545.362"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="986.901,-542.032 976.308,-542.209 984.699,-548.677 986.901,-542.032"/>
|
||||
</g>
|
||||
<!-- t9 -->
|
||||
<g id="node11" class="node"><title>t9</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1013,-426.208 694.762,-398.208 1013,-370.208 1331.24,-398.208 1013,-426.208"/>
|
||||
<text text-anchor="middle" x="1013" y="-392.208" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
|
||||
</g>
|
||||
<!-- t5->t9 -->
|
||||
<g id="edge13" class="edge"><title>t5->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1093.89,-563.996C1093.29,-548.44 1091.11,-525.146 1084,-506.208 1073.94,-479.426 1056.13,-452.609 1040.86,-432.521"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1043.62,-430.369 1034.72,-424.619 1038.09,-434.663 1043.62,-430.369"/>
|
||||
</g>
|
||||
<!-- t7 -->
|
||||
<g id="node8" class="node"><title>t7</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="426.366,-219.452 269.634,-219.452 265.634,-215.452 265.634,-183.452 422.366,-183.452 426.366,-187.452 426.366,-219.452"/>
|
||||
<polyline fill="none" stroke="black" points="422.366,-215.452 265.634,-215.452 "/>
|
||||
<polyline fill="none" stroke="black" points="422.366,-215.452 422.366,-183.452 "/>
|
||||
<polyline fill="none" stroke="black" points="422.366,-215.452 426.366,-219.452 "/>
|
||||
<text text-anchor="middle" x="346" y="-195.452" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
|
||||
<polygon fill="#efa03b" stroke="black" points="993.95,-484.208 716.05,-484.208 712.05,-480.208 712.05,-448.208 989.95,-448.208 993.95,-452.208 993.95,-484.208"/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 712.05,-480.208 "/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 989.95,-448.208 "/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 993.95,-484.208 "/>
|
||||
<text text-anchor="middle" x="853" y="-460.208" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
|
||||
</g>
|
||||
<!-- t6->t7 -->
|
||||
<g id="edge9" class="edge"><title>t6->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M501.184,-241.422C476.75,-235.286 448.99,-228.315 423.772,-221.982"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="424.429,-218.538 413.877,-219.497 422.724,-225.328 424.429,-218.538"/>
|
||||
<g id="edge8" class="edge"><title>t6->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M903.006,-506.102C896.726,-501.216 889.771,-495.807 883.095,-490.615"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="884.901,-487.586 874.859,-484.209 880.604,-493.111 884.901,-487.586"/>
|
||||
</g>
|
||||
<!-- t6->t9 -->
|
||||
<g id="edge12" class="edge"><title>t6->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M977.454,-506.003C987.329,-500.504 996.562,-493.377 1003,-484.208 1012.67,-470.441 1015.74,-452.229 1016.16,-436.195"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1019.66,-436.018 1016.09,-426.041 1012.66,-436.063 1019.66,-436.018"/>
|
||||
</g>
|
||||
<!-- t8 -->
|
||||
<g id="node9" class="node"><title>t8</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="663.742,-219.452 448.258,-219.452 444.258,-215.452 444.258,-183.452 659.742,-183.452 663.742,-187.452 663.742,-219.452"/>
|
||||
<polyline fill="none" stroke="black" points="659.742,-215.452 444.258,-215.452 "/>
|
||||
<polyline fill="none" stroke="black" points="659.742,-215.452 659.742,-183.452 "/>
|
||||
<polyline fill="none" stroke="black" points="659.742,-215.452 663.742,-219.452 "/>
|
||||
<text text-anchor="middle" x="554" y="-195.452" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
|
||||
<polygon fill="#00cc66" stroke="black" points="676.666,-416.208 387.334,-416.208 383.334,-412.208 383.334,-380.208 672.666,-380.208 676.666,-384.208 676.666,-416.208"/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 383.334,-412.208 "/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 672.666,-380.208 "/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 676.666,-416.208 "/>
|
||||
<text text-anchor="middle" x="530" y="-392.208" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
|
||||
</g>
|
||||
<!-- t6->t8 -->
|
||||
<g id="edge11" class="edge"><title>t6->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M564.418,-241.346C563.4,-237.546 562.298,-233.43 561.203,-229.345"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="564.522,-228.207 558.554,-219.453 557.761,-230.018 564.522,-228.207"/>
|
||||
<!-- t7->t8 -->
|
||||
<g id="edge9" class="edge"><title>t7->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M769.399,-448.125C724.645,-438.98 669.394,-427.691 623.252,-418.262"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="623.748,-414.791 613.25,-416.219 622.347,-421.65 623.748,-414.791"/>
|
||||
</g>
|
||||
<!-- t5->t7 -->
|
||||
<g id="edge10" class="edge"><title>t5->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M364.752,-241.346C362.816,-237.329 360.708,-232.958 358.629,-228.645"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="361.693,-226.941 354.197,-219.453 355.387,-229.981 361.693,-226.941"/>
|
||||
<!-- t7->t9 -->
|
||||
<g id="edge11" class="edge"><title>t7->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M894.62,-448.039C911.413,-441.112 931.162,-432.966 949.666,-425.333"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="951.009,-428.565 958.918,-421.516 948.339,-422.094 951.009,-428.565"/>
|
||||
</g>
|
||||
<!-- t5->t8 -->
|
||||
<g id="edge12" class="edge"><title>t5->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M428.289,-241.346C447.587,-235.375 469.416,-228.622 489.404,-222.437"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="490.53,-225.753 499.049,-219.453 488.461,-219.065 490.53,-225.753"/>
|
||||
<!-- t11 -->
|
||||
<g id="node10" class="node"><title>t11</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="938.109,-338.208 653.891,-338.208 649.891,-334.208 649.891,-302.208 934.109,-302.208 938.109,-306.208 938.109,-338.208"/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 649.891,-334.208 "/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 934.109,-302.208 "/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 938.109,-338.208 "/>
|
||||
<text text-anchor="middle" x="794" y="-314.208" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
|
||||
</g>
|
||||
<!-- t8->t11 -->
|
||||
<g id="edge10" class="edge"><title>t8->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M589.247,-380.152C629.442,-368.58 682.727,-353.241 724.863,-341.111"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="725.853,-344.468 734.495,-338.338 723.917,-337.741 725.853,-344.468"/>
|
||||
</g>
|
||||
<!-- t12 -->
|
||||
<g id="node13" class="node"><title>t12</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="473.845,-105.456 554,-78.0208 634.155,-105.456 634.08,-149.848 473.92,-149.848 473.845,-105.456"/>
|
||||
<polygon fill="none" stroke="black" points="469.836,-102.581 554,-73.7729 638.164,-102.581 638.078,-153.869 469.922,-153.869 469.836,-102.581"/>
|
||||
<text text-anchor="middle" x="554" y="-111.726" font-family="Times,serif" font-size="20.00">merge_pages</text>
|
||||
</g>
|
||||
<!-- t7->t12 -->
|
||||
<g id="edge20" class="edge"><title>t7->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M389.35,-183.419C409.937,-175.33 435.398,-165.326 460.092,-155.624"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="461.479,-158.839 469.507,-151.924 458.919,-152.324 461.479,-158.839"/>
|
||||
<g id="node14" class="node"><title>t12</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="1118.49,-270.208 775.514,-270.208 771.514,-266.208 771.514,-234.208 1114.49,-234.208 1118.49,-238.208 1118.49,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 771.514,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 1114.49,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 1118.49,-270.208 "/>
|
||||
<text text-anchor="middle" x="945" y="-246.208" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
|
||||
</g>
|
||||
<!-- t8->t12 -->
|
||||
<g id="edge19" class="edge"><title>t8->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M554,-183.12C554,-177.585 554,-171.177 554,-164.592"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="557.5,-164.201 554,-154.201 550.5,-164.201 557.5,-164.201"/>
|
||||
<path fill="none" stroke="#0044a0" d="M620.537,-380.161C641.947,-376.512 664.736,-372.939 686,-370.208 714.866,-366.5 927.304,-369.633 947,-348.208 963.502,-330.256 960.281,-301.363 954.611,-280.149"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="957.925,-279.012 951.707,-270.433 951.218,-281.017 957.925,-279.012"/>
|
||||
</g>
|
||||
<!-- t11->t12 -->
|
||||
<g id="edge17" class="edge"><title>t11->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M615.939,-357.13C634.767,-339.333 661.745,-309.733 673,-277.452 686.754,-238.003 694.347,-219.364 673,-183.452 666.345,-172.256 656.999,-162.875 646.44,-155.047"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="648.378,-152.132 638.152,-149.36 644.417,-157.904 648.378,-152.132"/>
|
||||
<!-- t13 -->
|
||||
<g id="node13" class="node"><title>t13</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="753.082,-270.208 494.918,-270.208 490.918,-266.208 490.918,-234.208 749.082,-234.208 753.082,-238.208 753.082,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 490.918,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 749.082,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 753.082,-270.208 "/>
|
||||
<text text-anchor="middle" x="622" y="-246.208" font-family="Times,serif" font-size="20.00">add_text_layer</text>
|
||||
</g>
|
||||
<!-- t11->t13 -->
|
||||
<g id="edge16" class="edge"><title>t11->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M749.258,-302.039C726.862,-293.446 699.574,-282.975 676.019,-273.936"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="677.108,-270.605 666.518,-270.29 674.6,-277.14 677.108,-270.605"/>
|
||||
</g>
|
||||
<!-- t9->t10 -->
|
||||
<g id="edge14" class="edge"><title>t9->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M877.633,-382.056C761.19,-369.023 593.788,-350.286 475.578,-337.054"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="475.697,-333.546 465.37,-335.912 474.919,-340.503 475.697,-333.546"/>
|
||||
</g>
|
||||
<!-- t9->t12 -->
|
||||
<g id="edge18" class="edge"><title>t9->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M164.711,-241.235C186.394,-224.08 222.088,-198.224 257,-183.452 321.895,-155.994 399.893,-139.571 459.67,-130.141"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="460.504,-133.554 469.855,-128.574 459.439,-126.635 460.504,-133.554"/>
|
||||
<path fill="none" stroke="#0044a0" d="M998.887,-371.447C994.931,-364.003 990.702,-355.831 987,-348.208 983.822,-341.663 967.96,-305.616 956.669,-279.865"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="959.82,-278.337 952.601,-270.582 953.409,-281.146 959.82,-278.337"/>
|
||||
</g>
|
||||
<!-- t10->t12 -->
|
||||
<g id="edge22" class="edge"><title>t10->t12</title>
|
||||
<path fill="none" stroke="gray" d="M629.379,-415.337C660.552,-396.255 703,-362.233 703,-318.452 703,-318.452 703,-318.452 703,-258.452 703,-224.066 705.581,-210.223 684,-183.452 673.976,-171.017 660.952,-160.773 647.026,-152.396"/>
|
||||
<polygon fill="gray" stroke="gray" points="648.682,-149.312 638.258,-147.419 645.227,-155.4 648.682,-149.312"/>
|
||||
<!-- t9->t14 -->
|
||||
<g id="edge21" class="edge"><title>t9->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1065.03,-374.611C1088.59,-364.381 1116.27,-352.359 1139.4,-342.314"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1140.9,-345.479 1148.68,-338.285 1138.11,-339.058 1140.9,-345.479"/>
|
||||
</g>
|
||||
<!-- t13 -->
|
||||
<g id="node14" class="node"><title>t13</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="616.338,-52 495.662,-52 491.662,-48 491.662,-16 612.338,-16 616.338,-20 616.338,-52"/>
|
||||
<polyline fill="none" stroke="black" points="612.338,-48 491.662,-48 "/>
|
||||
<polyline fill="none" stroke="black" points="612.338,-48 612.338,-16 "/>
|
||||
<polyline fill="none" stroke="black" points="612.338,-48 616.338,-52 "/>
|
||||
<text text-anchor="middle" x="554" y="-28" font-family="Times,serif" font-size="20.00">validate_pdfa</text>
|
||||
<!-- t10->t13 -->
|
||||
<g id="edge17" class="edge"><title>t10->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M417.688,-300.029C455.345,-291.399 499.718,-281.231 537.473,-272.578"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="538.461,-275.943 547.427,-270.297 536.898,-269.12 538.461,-275.943"/>
|
||||
</g>
|
||||
<!-- t12->t13 -->
|
||||
<g id="edge21" class="edge"><title>t12->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M554,-73.9482C554,-69.9654 554,-66.007 554,-62.2247"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="557.5,-62.1573 554,-52.1573 550.5,-62.1574 557.5,-62.1573"/>
|
||||
<!-- t17 -->
|
||||
<g id="node18" class="node"><title>t17</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="540.724,-156.455 770,-129.143 999.276,-156.455 999.062,-200.646 540.938,-200.646 540.724,-156.455"/>
|
||||
<polygon fill="none" stroke="black" points="536.704,-152.903 770,-125.112 1003.3,-152.903 1003.05,-204.648 536.954,-204.648 536.704,-152.903"/>
|
||||
<text text-anchor="middle" x="770" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_ghostscript</text>
|
||||
</g>
|
||||
<!-- t13->t17 -->
|
||||
<g id="edge27" class="edge"><title>t13->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M653.026,-234.114C665.992,-226.971 681.653,-218.343 697.24,-209.755"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="699.319,-212.606 706.388,-204.715 695.941,-206.475 699.319,-212.606"/>
|
||||
</g>
|
||||
<!-- t13->t18 -->
|
||||
<g id="edge31" class="edge"><title>t13->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M753.329,-235.235C756.584,-234.884 759.812,-234.541 763,-234.208 873.497,-222.668 902.059,-228.201 1012,-212.208 1023.74,-210.5 1035.81,-208.554 1047.95,-206.456"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1048.91,-209.84 1058.16,-204.659 1047.7,-202.946 1048.91,-209.84"/>
|
||||
</g>
|
||||
<!-- t12->t17 -->
|
||||
<g id="edge28" class="edge"><title>t12->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M908.314,-234.114C892.552,-226.77 873.424,-217.858 854.484,-209.033"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="855.759,-205.766 845.216,-204.715 852.803,-212.111 855.759,-205.766"/>
|
||||
</g>
|
||||
<!-- t12->t18 -->
|
||||
<g id="edge32" class="edge"><title>t12->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1002.86,-234.114C1029.07,-226.37 1061.19,-216.881 1092.64,-207.59"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1093.77,-210.905 1102.37,-204.715 1091.79,-204.192 1093.77,-210.905"/>
|
||||
</g>
|
||||
<!-- t16->t17 -->
|
||||
<g id="edge24" class="edge"><title>t16->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1155.57,-234.546C1111.13,-227.821 1059.63,-219.853 1013,-212.208 1001.53,-210.326 989.752,-208.365 977.872,-206.363"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="978.316,-202.888 967.872,-204.672 977.149,-209.79 978.316,-202.888"/>
|
||||
</g>
|
||||
<!-- t16->t18 -->
|
||||
<g id="edge29" class="edge"><title>t16->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1258.03,-233.915C1254.58,-227.933 1250.55,-220.928 1246.43,-213.785"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1249.24,-211.645 1241.21,-204.729 1243.17,-215.141 1249.24,-211.645"/>
|
||||
</g>
|
||||
<!-- t14->t17 -->
|
||||
<g id="edge26" class="edge"><title>t14->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1172.36,-302.154C1164.33,-293.013 1154.66,-281.362 1147,-270.208 1136.64,-255.121 1141.88,-244.862 1127,-234.208 1125.19,-232.915 1055.67,-220.232 978.45,-206.446"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="978.964,-202.982 968.505,-204.672 977.735,-209.874 978.964,-202.982"/>
|
||||
</g>
|
||||
<!-- t14->t18 -->
|
||||
<g id="edge30" class="edge"><title>t14->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1310.22,-302.184C1345.8,-294.579 1378.07,-284.119 1389,-270.208 1408.78,-245.028 1391.25,-224.765 1361.2,-209.211"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1362.48,-205.944 1351.96,-204.742 1359.44,-212.246 1362.48,-205.944"/>
|
||||
</g>
|
||||
<!-- t15->t17 -->
|
||||
<g id="edge25" class="edge"><title>t15->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M289.656,-684.797C164.646,-670.853 17,-641.95 17,-583.208 17,-583.208 17,-583.208 17,-319.208 17,-213.417 306.554,-181.327 526.553,-172.213"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="526.794,-175.706 536.646,-171.808 526.514,-168.711 526.794,-175.706"/>
|
||||
</g>
|
||||
<!-- t19 -->
|
||||
<g id="node20" class="node"><title>t19</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="844.361,-47.3406 995,-20.006 1145.64,-47.3406 1145.5,-91.5689 844.501,-91.5689 844.361,-47.3406"/>
|
||||
<polygon fill="none" stroke="black" points="840.355,-43.9965 995,-15.9349 1149.65,-43.9965 1149.48,-95.5749 840.519,-95.5749 840.355,-43.9965"/>
|
||||
<text text-anchor="middle" x="995" y="-53.565" font-family="Times,serif" font-size="20.00">copy_final</text>
|
||||
</g>
|
||||
<!-- t17->t19 -->
|
||||
<g id="edge35" class="edge"><title>t17->t19</title>
|
||||
<path fill="none" stroke="#0044a0" d="M841.81,-133.486C864.177,-122.839 889.024,-111.011 912.085,-100.034"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="913.64,-103.17 921.165,-95.7118 910.631,-96.8497 913.64,-103.17"/>
|
||||
</g>
|
||||
<!-- t18->t19 -->
|
||||
<g id="edge34" class="edge"><title>t18->t19</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1151.35,-134.66C1128.23,-123.705 1102.26,-111.399 1078.23,-100.009"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1079.66,-96.8131 1069.12,-95.6933 1076.66,-103.139 1079.66,-96.8131"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
norecursedirs = lib .pc .git
|
||||
+9
-4
@@ -1,4 +1,9 @@
|
||||
ruffus>=2.6.3
|
||||
Pillow>=2.4.0
|
||||
reportlab>=3.1.44
|
||||
PyPDF2>=1.25.1
|
||||
# requirements.txt can be used to replicate the developer's build environment
|
||||
# setup.py lists a separate set of requirements that are looser to simplify
|
||||
# installation
|
||||
ruffus==2.6.3
|
||||
Pillow==3.3.0
|
||||
reportlab==3.2.0
|
||||
PyPDF2==1.26
|
||||
img2pdf==0.2.1
|
||||
cffi==1.5.2
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
[bdist_wheel]
|
||||
python-tag = py34
|
||||
python-tag = py34
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[check-manifest]
|
||||
ignore =
|
||||
.github
|
||||
@@ -1,12 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from setuptools import setup
|
||||
from subprocess import Popen, STDOUT, check_output, CalledProcessError
|
||||
from string import Template
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
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)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup # nopep8
|
||||
from subprocess import STDOUT, check_output, CalledProcessError # nopep8
|
||||
from collections.abc import Mapping # nopep8
|
||||
import re # nopep8
|
||||
|
||||
|
||||
missing_program = '''
|
||||
@@ -142,44 +148,45 @@ def check_external_program(
|
||||
|
||||
|
||||
command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
|
||||
forced = ('--force' in sys.argv)
|
||||
|
||||
|
||||
if command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install', 'egg_info']:
|
||||
if not forced and command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
need_version='3.02.02',
|
||||
need_version='3.03', # limited by Travis CI / Ubuntu 12.04 backports
|
||||
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
|
||||
)
|
||||
check_external_program(
|
||||
program='gs',
|
||||
need_version='9.14',
|
||||
need_version='9.15', # limited by Travis CI / Ubuntu 12.04 backports
|
||||
package='ghostscript'
|
||||
)
|
||||
check_external_program(
|
||||
program='unpaper',
|
||||
need_version='6.1',
|
||||
need_version='6.1', # latest sane version
|
||||
package='unpaper',
|
||||
optional=True
|
||||
)
|
||||
check_external_program(
|
||||
program='qpdf',
|
||||
need_version='5.0.0',
|
||||
need_version='5.0.0', # limited by Travis CI / Ubuntu 12.04 backports
|
||||
package='qpdf',
|
||||
version_check_args=['--version']
|
||||
)
|
||||
else:
|
||||
print('Skipping external program tests because of --force')
|
||||
|
||||
|
||||
if 'upload' in sys.argv[1:]:
|
||||
print('Use twine to upload the package - setup.py upload is insecure')
|
||||
sys.exit(1)
|
||||
|
||||
install_requires = open('requirements.txt').read().splitlines()
|
||||
tests_require = open('test_requirements.txt').read().splitlines()
|
||||
|
||||
setup(
|
||||
name='ocrmypdf',
|
||||
version='3.0', # also update: release notes, main.py
|
||||
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
|
||||
url='https://github.com/jbarlow83/OCRmyPDF',
|
||||
author='James R. Barlow',
|
||||
@@ -194,7 +201,7 @@ setup(
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Intended Audience :: Science/Research",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: Public Domain",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: POSIX :: BSD",
|
||||
@@ -203,12 +210,29 @@ setup(
|
||||
"Topic :: Text Processing :: Indexing",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
],
|
||||
install_requires=install_requires,
|
||||
setup_requires=[
|
||||
'setuptools_scm',
|
||||
'cffi>=1.5.0',
|
||||
'pytest-runner'
|
||||
],
|
||||
use_scm_version={'version_scheme': 'post-release'},
|
||||
cffi_modules=[
|
||||
'ocrmypdf/lib/compile_leptonica.py:ffi'
|
||||
],
|
||||
install_requires=[
|
||||
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
|
||||
'Pillow>=3.1.0', # Pillow is pretty stable
|
||||
'reportlab>=3.2.0', # oldest released version with sane image handling
|
||||
'PyPDF2>=1.26', # pure Python, so track HEAD closely
|
||||
'img2pdf>=0.2.1', # pure Python, so track HEAD closely
|
||||
'cffi>=1.5.0' # oldest version ever tested
|
||||
],
|
||||
tests_require=tests_require,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'ocrmypdf = ocrmypdf.main:run_pipeline'
|
||||
'ocrmypdf = ocrmypdf.__main__:run_pipeline'
|
||||
],
|
||||
},
|
||||
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
||||
include_package_data=True,
|
||||
zip_safe=False)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
deb http://httpredir.debian.org/debian stretch main
|
||||
deb http://httpredir.debian.org/debian stretch-updates main
|
||||
deb http://security.debian.org stretch/updates main
|
||||
deb http://ftp.de.debian.org/debian sid main contrib non-free
|
||||
Binary file not shown.
@@ -1,2 +1 @@
|
||||
img2pdf>=0.1.5
|
||||
pytest>=2.7.2
|
||||
pytest>=2.7.2
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
'''
|
||||
For testing PDF/A generation failures, this wrapper calls ghostscript
|
||||
but filters out all PDF/A information so that a regular PDF will be
|
||||
created instead.
|
||||
|
||||
It assumes that it is called from a staged system PATH where the first
|
||||
item on the PATH contains a file named 'gs' which is a symlink to this
|
||||
file. It will strip out the first item on path to invoke the real
|
||||
'gs'. That is, it expects this when called
|
||||
|
||||
1. tests/output/[...test name...]/bin/gs is a symlink to this file
|
||||
2. tests/output/bin is the first item on PATH
|
||||
3. The real executable is on the path
|
||||
|
||||
OCRmyPDF also calls "gs --version" and "gs --help". gs answers both
|
||||
on stdout so, this wrapper prints to stderr.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def pdfa_param(arg):
|
||||
if arg.startswith('-sPDFA'):
|
||||
return True
|
||||
if arg.startswith('-dPDFA'):
|
||||
return True
|
||||
if arg.endswith('.ps'):
|
||||
return True
|
||||
if 'ColorConversionStrategy' in arg:
|
||||
return True
|
||||
if 'ProcessColorModel' in arg:
|
||||
return True
|
||||
if 'OutputICCProfile' in arg:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys_args = sys.argv[1:]
|
||||
|
||||
print("Fake Ghostscript wrapper", file=sys.stderr)
|
||||
|
||||
if any(pdfa_param(arg) for arg in sys_args):
|
||||
# We were asked to produce a PDF/A
|
||||
# Filter out PDF/A arguments
|
||||
args = [arg for arg in sys.argv[1:]
|
||||
if not pdfa_param(arg)]
|
||||
|
||||
# Tell Ghostscript to create a PDF 1.3 instead so JHOVE won't
|
||||
# think it's a PDF/A
|
||||
indexof_pdfwrite = next(n for n, item in enumerate(args)
|
||||
if 'pdfwrite' in item)
|
||||
args.insert(indexof_pdfwrite + 1, '-dCompatibilityLevel=1.3')
|
||||
print("Rewrote arguments", file=sys.stderr)
|
||||
else:
|
||||
args = sys_args
|
||||
print("Keeping arguments", file=sys.stderr)
|
||||
|
||||
exec_path = os.environ['PATH'].split(os.pathsep)
|
||||
env = os.environ.copy()
|
||||
env['PATH'] = os.pathsep.join(exec_path[1:])
|
||||
|
||||
print("Calling real Ghostscript with modified args and env",
|
||||
file=sys.stderr)
|
||||
print(args, file=sys.stderr)
|
||||
print(env['PATH'], file=sys.stderr)
|
||||
|
||||
sys.stderr.flush()
|
||||
os.execvpe('gs', ['gs'] + args, env)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -1,34 +0,0 @@
|
||||
All test resources must come from free public domain sources for
|
||||
copyright reasons.
|
||||
|
||||
Test files do not necessarily produce perfect (or even good) OCR
|
||||
results.
|
||||
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| File | Source |
|
||||
+=====================+================================================================================+
|
||||
| graph.pdf | Wikimedia |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| c02-22.pdf | Project Gutenberg: https://www.gutenberg.org/files/76/76-h/images/c02-22.jpg |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| LinnSequencer.jpg | Wikimedia_ |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| congress.jpg | http://www.baxleystamps.com/litho/meiji/courts_1871.jpg |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| blank.pdf | Blank page from Adobe Illustrator CC 2015 |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| enormous.pdf | PNG file saved to PDF using img2pdf |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| invalid.pdf | PDF file header followed by EOF marker; not valid |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| multipage.pdf | several other files concatenated |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| skew.pdf | skewed version of c02-22.PDF |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| Test_Issue_28.pdf | file with some syntax errors |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
| missing_docinfo.pdf | file missing its DocumentInfo dictionary |
|
||||
+---------------------+--------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
.. _Wikimedia: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
|
||||
@@ -0,0 +1,106 @@
|
||||
These test files are used in OCRmyPDF's test suite. They do not necessarily produce OCR results
|
||||
at all and are not meant as examples of OCR output. Some are even invalid PDFs that might
|
||||
crash certain PDF viewers.
|
||||
|
||||
|
||||
Files derived from free sources
|
||||
===============================
|
||||
|
||||
These test resources come from free sources, under either public domain or Creative Commons licenses.
|
||||
In some cases they were converted from one image format to another without other changes.
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 50 30
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- Source
|
||||
- License
|
||||
* - c02-22.pdf
|
||||
- `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22
|
||||
- Public Domain
|
||||
* - congress.jpg
|
||||
- `US Congressional Records`_
|
||||
- Public Domain
|
||||
* - graph.pdf
|
||||
- `Wikimedia: Pandas text analysis.png`_
|
||||
- Public Domain
|
||||
* - lichtenstein.pdf
|
||||
- `Wikimedia: JPEG2000 Lichtenstein`_
|
||||
- Creative Commons BY-SA 3.0
|
||||
* - LinnSequencer.jpg, linn.pdf, linn.txt
|
||||
- `Wikimedia: LinnSequencer`_
|
||||
- Creative Commons BY-SA 3.0
|
||||
|
||||
|
||||
Files generated for this project
|
||||
================================
|
||||
|
||||
The following test resources were crafted specifically for this project, and can be used
|
||||
under the terms of the license in LICENSE.rst.
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 20 60
|
||||
:header-rows: 1
|
||||
|
||||
* - File
|
||||
- Contributor
|
||||
- Purpose
|
||||
* - aspect.pdf
|
||||
- @jbarlow83
|
||||
- test image with 200 x 100 DPI resolution
|
||||
* - blank.pdf
|
||||
- @jbarlow83
|
||||
- blank PDF
|
||||
* - cmyk.pdf
|
||||
- @jbarlow83
|
||||
- a CMYK image created in Photoshop
|
||||
* - enormous.pdf
|
||||
- @jbarlow83
|
||||
- very large PDF page
|
||||
* - epson.pdf
|
||||
- @lowesjam
|
||||
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
|
||||
* - francais.pdf
|
||||
- @jbarlow83
|
||||
- a page containing French accents (diacritics)
|
||||
* - hugemono.pdf
|
||||
- @jbarlow83
|
||||
- large monochrome 35000x35000 image in JBIG2 encoding
|
||||
* - invalid.pdf
|
||||
- @jbarlow83
|
||||
- a PDF file header followed by EOF marker
|
||||
* - masks.pdf
|
||||
- @supergrobi
|
||||
- file containing explicit masks and a stencil mask drawn without a proper transformation matrix; printout of a German Wikipedia article (CC BY-SA)
|
||||
* - missing_docinfo.pdf
|
||||
- @jbarlow83
|
||||
- PDF file with no /DocumentInfo section
|
||||
|
||||
Assemblies
|
||||
==========
|
||||
|
||||
These test resources are assemblies from other previously mentioned files, released under the same license terms as their input files.
|
||||
|
||||
- cardinal.pdf (four cardinal directions, rotated copies of LinnSequencer.jpg)
|
||||
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
|
||||
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
|
||||
- graph_ocred.pdf (from graph.pdf)
|
||||
- jbig2.pdf (congress.jpg, converted to JBIG2 encoding)
|
||||
- multipage.pdf (from several other files)
|
||||
- palette.pdf (congress.jpg, converted to a 256-color palette)
|
||||
- skew.pdf (from c02-22.pdf)
|
||||
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2)
|
||||
|
||||
|
||||
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
|
||||
|
||||
.. _`Project Gutenberg`: https://www.gutenberg.org/files/76/76-h/76-h.htm#c2
|
||||
|
||||
.. _`US Congressional Records`: http://www.baxleystamps.com/litho/meiji/courts_1871.jpg
|
||||
|
||||
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
|
||||
|
||||
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
|
||||
|
||||
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
The LinnSequencer
|
||||
32 Track MIDI Sequence Recorder
|
||||
|
||||
The LinnSequencer is a state—of—the-art composition and performance tool for the professional musician. It is
|
||||
|
||||
extremely powerful, yet amazingly simple to learn and use. It’s many remarkable features include:
|
||||
|
||||
0 Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
|
||||
FORWARD, REWIND, and LOCATE controls.
|
||||
|
||||
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
|
||||
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
|
||||
|
||||
synthesizers !
|
||||
|
||||
0 Ultra-fast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
|
||||
|
||||
per disk!
|
||||
|
||||
0 One or all tracks may be TRANSPOSED at the touch of a key.
|
||||
0 Exclusive real—time ERASE function makes editing FAST.
|
||||
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
|
||||
|
||||
rhythmic value.
|
||||
|
||||
0 TIMING CORRECTION works during playback and operates without ‘chopping’ notes.
|
||||
|
||||
0 Optional SMPTE time code synchronization.
|
||||
|
||||
0 Optional remote control.
|
||||
|
||||
Recording a Sequence
|
||||
|
||||
To record a sequence, simply press RECORD and PLAY,
|
||||
then play your MIDI keyboard in time to the Sequencer’s
|
||||
click track. When the sequence loops back around to bar 1,
|
||||
you’ll hear what you played—only all timing errors will be
|
||||
|
||||
corrected! (Timing correction may be adjusted 0r defeated).
|
||||
|
||||
Any additional notes played will be added into the track
|
||||
—existing notes are not erased while recording!
|
||||
|
||||
FAST FORWARD, REWIND, and LOCATE controls
|
||||
may be used at any time to quickly access any location in
|
||||
your sequence for spot-recording. To overdub a new part,
|
||||
select a different track and start recording—while you
|
||||
record, the first‘track will play in perfect sync (unless you
|
||||
MUTE it, or SOLO another track). In this way, up to 32
|
||||
tracks may be overdubbed! All MIDI effects are recorded
|
||||
including pitch bend, modulation, velocity, aftertouch,
|
||||
sustain pedal, and program changes!
|
||||
|
||||
Editing
|
||||
|
||||
To erase a wrong note, simply hold ERASE and press
|
||||
the note to be erased just before it plays in the sequence-—
|
||||
when played back, it will be gone. Notes may also be
|
||||
|
||||
added, erased, or changed using the SINGLE STEP func-
|
||||
tion. To overdub notes at specific points within a sequence,
|
||||
|
||||
Additional Features
|
||||
|
||||
simply use LOCATE, FAST FORWARD, or REWIND to
|
||||
find the desired bar number, then start recording.
|
||||
|
||||
The INSERT/ COPY function allows you to move bars
|
||||
from one location to another—in the same sequence or a
|
||||
different one. For example, you might insert a copy of the
|
||||
first verse between the second chorus and the bridge.
|
||||
DELETE BARS operates the same way to remove
|
||||
unwanted sections.
|
||||
|
||||
Creating a Song
|
||||
|
||||
One way to create a song is to record each track all the
|
||||
way through (up to 999 bars). Another way is to record
|
||||
each basic section (verse, chorus, etc.) in individual
|
||||
sequences, then use the CREATE SONG function to “chain”
|
||||
them together. CREATE SONG will then automatically
|
||||
copy all the parts into a new sequence. If desired, you can
|
||||
even set the last few bars to repeat infinitely, for a fadeout.
|
||||
|
||||
Composition Without Compromise
|
||||
|
||||
The technology you use should never be so complex that
|
||||
it interferes with the creative process. That’s precisely why
|
||||
the LinnSequencer is designed to let you compose, record
|
||||
and edit while devoting your undivided attention to your
|
||||
music. See your Linn dealer today for a demonstration!
|
||||
|
||||
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
|
||||
|
||||
HELP button displays additional explanations.
|
||||
|
||||
0 Non-destructive recording—existing notes are not erased while recording.
|
||||
0 Two FOOTSWIT CH INPUTS may be assigned to remotely control many of the commonly used functions, including
|
||||
|
||||
ERASE, REPEAT, PLAY/ STOP, or LOCATE.
|
||||
|
||||
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
|
||||
|
||||
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
|
||||
|
||||
0 Utilizes ultra high—speed, 8 MHZ 80186 16 bit computer internally for FAST operation.
|
||||
0 TEMPO may be specified in BEATS-PER—MINUTE or FRAMES-PER—BEAT at 24, 25, or 30 frames per second,
|
||||
|
||||
(even drop frame!)
|
||||
|
||||
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
|
||||
|
||||
on the TAP TEMPO button.
|
||||
|
||||
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
|
||||
0 Any TIME SIGNATURE may be used, and may be changed within a song.
|
||||
|
||||
EDI]
|
||||
Linn Electronics, Inc.
|
||||
|
||||
18720 Oxnard Street, Tarzana, CA 91356
|
||||
(818) 708-8131 TELEX #298949 LINN UR
|
||||
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print('qpdf dummy')
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED: return error claiming image too big
|
||||
'''
|
||||
|
||||
"""Simulates an error of Tesseract failing on attempts to process large images
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
print("Image too large: (33830, 14959)\n"
|
||||
"Error during processing.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
"""Cache output of tesseract to speed up test suite
|
||||
|
||||
The cache is keyed by a hash that includes the tesseract version, some of
|
||||
the command line, and the binary dump of the input file. The output file,
|
||||
stdout, and stderr are replicated on a cache hit.
|
||||
|
||||
Page orientation checks are also cached (-psm 0 stdout)
|
||||
|
||||
Errors and crashes are not cached.
|
||||
|
||||
Things not checked:
|
||||
-changes to tesseract installation that don't affect --version
|
||||
|
||||
Will fail on Tesseract 3.02.02 in "hocr" mode because it doesn't produce
|
||||
the incorrect file extension.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
CACHE_PATH = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', 'cache'))
|
||||
|
||||
|
||||
def real_tesseract():
|
||||
tess_args = ['tesseract'] + sys.argv[1:]
|
||||
os.execvp("tesseract", tess_args)
|
||||
return # Not reachable
|
||||
|
||||
def main():
|
||||
operation = sys.argv[-1]
|
||||
# For anything unexpected operation, defer to real tesseract binary
|
||||
if operation != 'hocr' and operation != 'pdf' and operation != 'stdout':
|
||||
real_tesseract()
|
||||
return # Not reachable
|
||||
|
||||
try:
|
||||
os.makedirs(CACHE_PATH)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
m = hashlib.sha1()
|
||||
|
||||
version = subprocess.check_output(
|
||||
['tesseract', '--version'],
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
m.update(version)
|
||||
m.update(operation.encode())
|
||||
|
||||
try:
|
||||
lang = sys.argv[sys.argv.index('-l') + 1]
|
||||
m.update(lang.encode())
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
psm = sys.argv[sys.argv.index('-psm') + 1]
|
||||
m.update(psm.encode())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if operation == 'stdout' and psm != '0':
|
||||
real_tesseract()
|
||||
return
|
||||
|
||||
if operation == 'stdout':
|
||||
input_file = sys.argv[-2]
|
||||
output_file = 'stdout'
|
||||
else:
|
||||
input_file = sys.argv[-3]
|
||||
output_file = sys.argv[-2]
|
||||
|
||||
if operation == 'hocr':
|
||||
output_file += '.hocr'
|
||||
elif operation == 'pdf':
|
||||
output_file += '.pdf'
|
||||
|
||||
with open(input_file, 'rb') as f:
|
||||
m.update(f.read())
|
||||
cache_name = os.path.join(CACHE_PATH, m.hexdigest())
|
||||
print(cache_name)
|
||||
if os.path.exists(cache_name):
|
||||
# Cache hit
|
||||
print("Tesseract cache hit", file=sys.stderr)
|
||||
if operation != 'stdout':
|
||||
shutil.copy(cache_name, output_file)
|
||||
|
||||
# Replicate output
|
||||
with open(cache_name + '.stdout', 'r') as f:
|
||||
print(f.read(), end='')
|
||||
with open(cache_name + '.stderr', 'r') as f:
|
||||
print(f.read(), end='', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
# Cache miss
|
||||
print("Tesseract cache miss", file=sys.stderr)
|
||||
|
||||
# Call tesseract
|
||||
p = subprocess.Popen(
|
||||
['tesseract'] + sys.argv[1:],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
# Do not cache errors or crashes
|
||||
print("Tesseract error", file=sys.stderr)
|
||||
print(stdout, end='')
|
||||
print(stderr, end='', file=sys.stderr)
|
||||
return p.returncode
|
||||
|
||||
with open(cache_name + '.stdout', 'w') as f:
|
||||
f.write(stdout)
|
||||
with open(cache_name + '.stderr', 'w') as f:
|
||||
f.write(stderr)
|
||||
print(stdout, end='')
|
||||
print(stderr, end='', file=sys.stderr)
|
||||
|
||||
# Insert file into cache
|
||||
if output_file != 'stdout':
|
||||
if os.path.exists(output_file):
|
||||
shutil.copy(output_file, cache_name)
|
||||
else:
|
||||
print("Could not find output file", file=sys.stderr)
|
||||
else:
|
||||
open(cache_name, 'w').close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED: CRASH ON OCR or -psm 0
|
||||
'''
|
||||
|
||||
"""Simulates a Tesseract crash when asked to run OCR
|
||||
|
||||
It isn't strictly necessary to crash the process and that has unwanted
|
||||
side effects like triggering core dumps or error reporting, logging and such.
|
||||
It's enough to dump some text to stderr and return an error code.
|
||||
|
||||
Follows the POSIX(?) convention of returning 128 + signal number.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'pdf':
|
||||
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGSEGV)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
print("libc++abi.dylib: terminating with uncaught exception of type "
|
||||
"std::bad_alloc: std::bad_alloc", file=sys.stderr)
|
||||
sys.exit(128 + signal.SIGABRT)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import img2pdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
"""Tesseract no-op spoof
|
||||
|
||||
To quickly run tests where getting OCR output is not necessary.
|
||||
|
||||
In 'hocr' mode, create a .hocr file that specifies no text found.
|
||||
|
||||
In 'pdf' mode, convert the image to PDF using another program.
|
||||
|
||||
In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
|
||||
VERSION_STRING = '''tesseract 3.04.00
|
||||
leptonica-1.72
|
||||
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
|
||||
SPOOFED
|
||||
'''
|
||||
|
||||
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 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>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print(VERSION_STRING, file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--list-langs':
|
||||
print('List of available languages (1):\neng', file=sys.stderr)
|
||||
sys.exit(0)
|
||||
elif sys.argv[-1] == 'hocr':
|
||||
inputf = sys.argv[-3]
|
||||
output = sys.argv[-2]
|
||||
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)
|
||||
elif sys.argv[-1] == 'stdout':
|
||||
inputf = sys.argv[-2]
|
||||
print("""Orientation: 0
|
||||
Orientation in degrees: 0
|
||||
Orientation confidence: 100.00
|
||||
Script: 1
|
||||
Script confidence: 100.00""", file=sys.stderr)
|
||||
else:
|
||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
||||
print(sys.argv, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
def main():
|
||||
if sys.argv[1] == '--version':
|
||||
print('0.5')
|
||||
sys.exit(0)
|
||||
|
||||
print("Only supports --version")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from ocrmypdf import hocrtransform
|
||||
from ocrmypdf.tesseract import HOCR_TEMPLATE
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from PIL import Image
|
||||
from tempfile import NamedTemporaryFile
|
||||
from contextlib import suppress
|
||||
import os
|
||||
import shutil
|
||||
import pytest
|
||||
import img2pdf
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
print("Requires Python 3.4+")
|
||||
sys.exit(1)
|
||||
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
|
||||
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
||||
OCRMYPDF = os.path.join(PROJECT_ROOT, 'OCRmyPDF.sh')
|
||||
TEST_RESOURCES = os.path.join(PROJECT_ROOT, 'tests', 'resources')
|
||||
TEST_OUTPUT = os.environ.get(
|
||||
'OCRMYPDF_TEST_OUTPUT',
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'hocrtransform'))
|
||||
|
||||
|
||||
def setup_module():
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(TEST_OUTPUT)
|
||||
with suppress(FileExistsError):
|
||||
os.makedirs(TEST_OUTPUT)
|
||||
with open(_make_output('blank.hocr'), 'w') as f:
|
||||
f.write(HOCR_TEMPLATE)
|
||||
|
||||
|
||||
def _make_input(input_basename):
|
||||
return os.path.join(TEST_RESOURCES, input_basename)
|
||||
|
||||
|
||||
def _make_output(output_basename):
|
||||
return os.path.join(TEST_OUTPUT, output_basename)
|
||||
|
||||
|
||||
def test_mono_image():
|
||||
im = Image.new('1', (8, 8), 0)
|
||||
for n in range(8):
|
||||
im.putpixel((n, n), 1)
|
||||
im.save(_make_output('mono.tif'), format='TIFF')
|
||||
|
||||
hocr = hocrtransform.HocrTransform(_make_output('blank.hocr'), 300)
|
||||
hocr.to_pdf(_make_output('mono.pdf'), imageFileName=_make_output('mono.tif'))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+506
-146
@@ -2,7 +2,7 @@
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from __future__ import print_function
|
||||
from subprocess import Popen, PIPE, check_output
|
||||
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import suppress
|
||||
@@ -11,6 +11,9 @@ import pytest
|
||||
from ocrmypdf.pageinfo import pdf_get_all_pageinfo
|
||||
import PyPDF2 as pypdf
|
||||
from ocrmypdf import ExitCode
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
import platform
|
||||
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
@@ -18,58 +21,68 @@ if sys.version_info.major < 3:
|
||||
sys.exit(1)
|
||||
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
|
||||
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
||||
OCRMYPDF = os.path.join(PROJECT_ROOT, 'OCRmyPDF.sh')
|
||||
TEST_RESOURCES = os.path.join(PROJECT_ROOT, 'tests', 'resources')
|
||||
TEST_OUTPUT = os.environ.get(
|
||||
'OCRMYPDF_TEST_OUTPUT',
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output'))
|
||||
TEST_BINARY_PATH = os.path.join(TEST_OUTPUT, 'fakebin')
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'main'))
|
||||
OCRMYPDF = [sys.executable, '-m', 'ocrmypdf']
|
||||
|
||||
|
||||
def running_in_docker():
|
||||
# Docker creates a file named /.dockerinit
|
||||
return os.path.exists('/.dockerinit')
|
||||
|
||||
|
||||
def is_linux():
|
||||
return platform.system() == 'Linux'
|
||||
|
||||
|
||||
def setup_module():
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(TEST_OUTPUT)
|
||||
with suppress(FileExistsError):
|
||||
os.mkdir(TEST_OUTPUT)
|
||||
os.makedirs(TEST_OUTPUT)
|
||||
|
||||
|
||||
def run_ocrmypdf_sh(input_file, output_file, *args):
|
||||
sh_args = ['sh', OCRMYPDF] + list(args) + [input_file, output_file]
|
||||
sh = Popen(
|
||||
sh_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
out, err = sh.communicate()
|
||||
return sh, out, err
|
||||
|
||||
|
||||
def _make_input(input_basename):
|
||||
def _infile(input_basename):
|
||||
return os.path.join(TEST_RESOURCES, input_basename)
|
||||
|
||||
|
||||
def _make_output(output_basename):
|
||||
return os.path.join(TEST_OUTPUT, output_basename)
|
||||
def _outfile(output_basename):
|
||||
return os.path.join(TEST_OUTPUT, os.path.basename(output_basename))
|
||||
|
||||
|
||||
def check_ocrmypdf(input_basename, output_basename, *args):
|
||||
input_file = _make_input(input_basename)
|
||||
output_file = _make_output(output_basename)
|
||||
def check_ocrmypdf(input_basename, output_basename, *args, env=None):
|
||||
"Run ocrmypdf and confirmed that a valid file was created"
|
||||
input_file = _infile(input_basename)
|
||||
output_file = _outfile(output_basename)
|
||||
|
||||
sh, _, err = run_ocrmypdf_sh(input_file, output_file, *args)
|
||||
assert sh.returncode == 0, err
|
||||
p, out, err = run_ocrmypdf(input_basename, output_basename, *args, env=env)
|
||||
if p.returncode != 0:
|
||||
print('stdout\n======')
|
||||
print(out)
|
||||
print('stderr\n======')
|
||||
print(err)
|
||||
assert p.returncode == 0
|
||||
assert os.path.exists(output_file), "Output file not created"
|
||||
assert os.stat(output_file).st_size > 100, "PDF too small or empty"
|
||||
assert out == "", \
|
||||
"The following was written to stdout and should not have been: \n" + \
|
||||
"<stdout>\n" + out + "\n</stdout>"
|
||||
return output_file
|
||||
|
||||
|
||||
def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
input_file = _make_input(input_basename)
|
||||
output_file = _make_output(output_basename)
|
||||
def run_ocrmypdf(input_basename, output_basename, *args, env=None):
|
||||
"Run ocrmypdf and let caller deal with results"
|
||||
input_file = _infile(input_basename)
|
||||
output_file = _outfile(output_basename)
|
||||
|
||||
if env is None:
|
||||
env = os.environ
|
||||
|
||||
p_args = ['ocrmypdf'] + list(args) + [input_file, output_file]
|
||||
p_args = OCRMYPDF + list(args) + [input_file, output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True, env=env)
|
||||
@@ -77,13 +90,51 @@ def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
|
||||
return p, out, err
|
||||
|
||||
|
||||
def test_quick():
|
||||
check_ocrmypdf('c02-22.pdf', 'test_quick.pdf')
|
||||
def spoof(replace_program, with_spoof):
|
||||
"""Modify environment variables to override subprocess executables
|
||||
|
||||
Before running any executable, ocrmypdf checks the environment variable
|
||||
OCRMYPDF_PROGRAMNAME to override default program name/location, e.g.
|
||||
OCRMYPDF_GS redirects from the system path Ghostscript ("gs") to elsewhere.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
spoofer = os.path.join(SPOOF_PATH, with_spoof)
|
||||
if not os.access(spoofer, os.X_OK):
|
||||
os.chmod(spoofer, 0o755)
|
||||
env['OCRMYPDF_' + replace_program.upper()] = spoofer
|
||||
return env
|
||||
|
||||
|
||||
def test_deskew():
|
||||
@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 spoof_tesseract_crash():
|
||||
return spoof('tesseract', 'tesseract_crash.py')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_big_image_error():
|
||||
return spoof('tesseract', 'tesseract_big_image_error.py')
|
||||
|
||||
|
||||
def test_quick(spoof_tesseract_cache):
|
||||
check_ocrmypdf('ccitt.pdf', 'test_quick.pdf', env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
def test_deskew(spoof_tesseract_noop):
|
||||
# Run with deskew
|
||||
deskewed_pdf = check_ocrmypdf('skew.pdf', 'test_deskew.pdf', '-d')
|
||||
deskewed_pdf = check_ocrmypdf(
|
||||
'skew.pdf', 'test_deskew.pdf', '-d', '-v', '1', env=spoof_tesseract_noop)
|
||||
|
||||
# Now render as an image again and use Leptonica to find the skew angle
|
||||
# to confirm that it was deskewed
|
||||
@@ -91,7 +142,7 @@ def test_deskew():
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
deskewed_png = _make_output('deskewed.png')
|
||||
deskewed_png = _outfile('deskewed.png')
|
||||
|
||||
rasterize_pdf(
|
||||
deskewed_pdf,
|
||||
@@ -101,58 +152,107 @@ def test_deskew():
|
||||
raster_device='pngmono',
|
||||
log=log)
|
||||
|
||||
from ocrmypdf.leptonica import pixRead, pixDestroy, pixFindSkew
|
||||
pix = pixRead(deskewed_png)
|
||||
skew_angle, skew_confidence = pixFindSkew(pix)
|
||||
pix = pixDestroy(pix)
|
||||
from ocrmypdf.leptonica import Pix
|
||||
pix = Pix.read(deskewed_png)
|
||||
skew_angle, skew_confidence = pix.find_skew()
|
||||
|
||||
print(skew_angle)
|
||||
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
|
||||
|
||||
|
||||
def test_clean():
|
||||
check_ocrmypdf('skew.pdf', 'test_clean.pdf', '-c')
|
||||
def test_clean(spoof_tesseract_noop):
|
||||
check_ocrmypdf('skew.pdf', 'test_clean.pdf', '-c',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def check_exotic_image(pdf, renderer):
|
||||
def test_remove_background(spoof_tesseract_noop):
|
||||
from PIL import Image
|
||||
|
||||
# Ensure the input image does not contain pure white/black
|
||||
im = Image.open(_infile('congress.jpg'))
|
||||
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
output_pdf = check_ocrmypdf(
|
||||
'congress.jpg', 'test_remove_bg.pdf', '--remove-background',
|
||||
'--image-dpi', '150',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
from ocrmypdf.ghostscript import rasterize_pdf
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
output_png = _outfile('remove_bg.png')
|
||||
|
||||
rasterize_pdf(
|
||||
output_pdf,
|
||||
output_png,
|
||||
xres=100,
|
||||
yres=100,
|
||||
raster_device='png16m',
|
||||
log=log)
|
||||
|
||||
|
||||
# The output image should contain pure white and black
|
||||
im = Image.open(output_png)
|
||||
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
|
||||
|
||||
|
||||
# This will run 5 * 2 * 2 = 20 test cases
|
||||
@pytest.mark.parametrize(
|
||||
"pdf",
|
||||
['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'])
|
||||
@pytest.mark.parametrize("renderer", ['hocr', 'tesseract'])
|
||||
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
|
||||
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type):
|
||||
check_ocrmypdf(
|
||||
pdf,
|
||||
'test_{0}_{1}.pdf'.format(pdf, renderer),
|
||||
'-dc',
|
||||
'--pdf-renderer', renderer)
|
||||
'-v', '1',
|
||||
'--output-type', output_type,
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
def test_exotic_image():
|
||||
yield check_exotic_image, 'palette.pdf', 'hocr'
|
||||
yield check_exotic_image, 'palette.pdf', 'tesseract'
|
||||
yield check_exotic_image, 'cmyk.pdf', 'hocr'
|
||||
yield check_exotic_image, 'cmyk.pdf', 'tesseract'
|
||||
@pytest.mark.parametrize("output_type", [
|
||||
'pdfa', 'pdf'
|
||||
])
|
||||
def test_preserve_metadata(spoof_tesseract_noop, output_type):
|
||||
pdf_before = pypdf.PdfFileReader(_infile('graph.pdf'))
|
||||
|
||||
|
||||
def test_preserve_metadata():
|
||||
pdf_before = pypdf.PdfFileReader(_make_input('graph.pdf'))
|
||||
|
||||
output = check_ocrmypdf('graph.pdf', 'test_metadata_preserve.pdf')
|
||||
output = check_ocrmypdf('graph.pdf', 'test_metadata_preserve.pdf',
|
||||
'--output-type', output_type,
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
pdf_after = pypdf.PdfFileReader(output)
|
||||
|
||||
for key in ('/Title', '/Author'):
|
||||
assert pdf_before.documentInfo[key] == pdf_after.documentInfo[key]
|
||||
|
||||
pdfa_info = file_claims_pdfa(output)
|
||||
assert pdfa_info['output'] == output_type
|
||||
|
||||
def test_override_metadata():
|
||||
input_file = _make_input('c02-22.pdf')
|
||||
output_file = _make_output('test_override_metadata.pdf')
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_linux() and not running_in_docker(),
|
||||
reason="likely to fail if Linux locale is not configured correctly")
|
||||
@pytest.mark.parametrize("output_type", [
|
||||
'pdfa', 'pdf'
|
||||
])
|
||||
def test_override_metadata(spoof_tesseract_noop, output_type):
|
||||
input_file = _infile('c02-22.pdf')
|
||||
output_file = _outfile('test_override_metadata.pdf')
|
||||
|
||||
german = 'Du siehst den Wald vor lauter Bäumen nicht.'
|
||||
chinese = '孔子'
|
||||
high_unicode = 'U+1030C is: 𐌌'
|
||||
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
input_file, output_file,
|
||||
'--title', german,
|
||||
'--author', chinese,
|
||||
'--subject', high_unicode)
|
||||
'--subject', high_unicode,
|
||||
'--output-type', output_type,
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
@@ -170,161 +270,421 @@ def test_override_metadata():
|
||||
assert pdfinfo['Subject'] == high_unicode
|
||||
assert pdfinfo.get('Keywords', '') == ''
|
||||
|
||||
pdfa_info = file_claims_pdfa(output_file)
|
||||
assert pdfa_info['output'] == output_type
|
||||
|
||||
def check_oversample(renderer):
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_oversample(spoof_tesseract_cache, renderer):
|
||||
oversampled_pdf = check_ocrmypdf(
|
||||
'skew.pdf', 'test_oversample_%s.pdf' % renderer, '--oversample', '300',
|
||||
'--pdf-renderer', renderer)
|
||||
'skew.pdf', 'test_oversample_%s.pdf' % renderer, '--oversample', '350',
|
||||
'-f',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
pdfinfo = pdf_get_all_pageinfo(oversampled_pdf)
|
||||
|
||||
print(pdfinfo[0]['xres'])
|
||||
assert abs(pdfinfo[0]['xres'] - 300) < 1
|
||||
|
||||
|
||||
def test_oversample():
|
||||
yield check_oversample, 'hocr'
|
||||
yield check_oversample, 'tesseract'
|
||||
assert abs(pdfinfo[0]['xres'] - 350) < 1
|
||||
|
||||
|
||||
def test_repeat_ocr():
|
||||
sh, _, _ = run_ocrmypdf_sh('graph_ocred.pdf', 'wontwork.pdf')
|
||||
assert sh.returncode != 0
|
||||
p, _, _ = run_ocrmypdf('graph_ocred.pdf', 'wontwork.pdf')
|
||||
assert p.returncode != 0
|
||||
|
||||
|
||||
def test_force_ocr():
|
||||
out = check_ocrmypdf('graph_ocred.pdf', 'test_force.pdf', '-f')
|
||||
def test_force_ocr(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('graph_ocred.pdf', 'test_force.pdf', '-f',
|
||||
env=spoof_tesseract_cache)
|
||||
pdfinfo = pdf_get_all_pageinfo(out)
|
||||
assert pdfinfo[0]['has_text']
|
||||
|
||||
|
||||
def test_skip_ocr():
|
||||
check_ocrmypdf('graph_ocred.pdf', 'test_skip.pdf', '-s')
|
||||
def test_skip_ocr(spoof_tesseract_cache):
|
||||
check_ocrmypdf('graph_ocred.pdf', 'test_skip.pdf', '-s',
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
def test_argsfile():
|
||||
with open(_make_output('test_argsfile.txt'), 'w') as argsfile:
|
||||
def test_argsfile(spoof_tesseract_noop):
|
||||
with open(_outfile('test_argsfile.txt'), 'w') as argsfile:
|
||||
print('--title', 'ArgsFile Test', '--author', 'Test Cases',
|
||||
sep='\n', end='\n', file=argsfile)
|
||||
check_ocrmypdf('graph.pdf', 'test_argsfile.pdf',
|
||||
'@' + _make_output('test_argsfile.txt'))
|
||||
'@' + _outfile('test_argsfile.txt'),
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def check_ocr_timeout(renderer):
|
||||
def check_monochrome_correlation(
|
||||
reference_pdf, reference_pageno, test_pdf, test_pageno):
|
||||
|
||||
import ocrmypdf.ghostscript as ghostscript
|
||||
import logging
|
||||
|
||||
gslog = logging.getLogger()
|
||||
|
||||
reference_png = _outfile('{}.ref{:04d}.png'.format(
|
||||
reference_pdf, reference_pageno))
|
||||
test_png = _outfile('{}.test{:04d}.png'.format(
|
||||
test_pdf, test_pageno))
|
||||
|
||||
def rasterize(pdf, pageno, png):
|
||||
if os.path.exists(png):
|
||||
print(png)
|
||||
return
|
||||
ghostscript.rasterize_pdf(
|
||||
pdf,
|
||||
png,
|
||||
xres=100, yres=100,
|
||||
raster_device='pngmono', log=gslog, pageno=pageno)
|
||||
|
||||
rasterize(reference_pdf, reference_pageno, reference_png)
|
||||
rasterize(test_pdf, test_pageno, test_png)
|
||||
|
||||
pix_ref = leptonica.Pix.read(reference_png)
|
||||
pix_test = leptonica.Pix.read(test_png)
|
||||
|
||||
return leptonica.Pix.correlation_binary(pix_ref, pix_test)
|
||||
|
||||
|
||||
def test_monochrome_correlation():
|
||||
# Verify leptonica: check that an incorrect rotated image has poor
|
||||
# correlation with reference
|
||||
corr = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1, # north facing page
|
||||
test_pdf=_infile('cardinal.pdf'),
|
||||
test_pageno=3, # south facing page
|
||||
)
|
||||
assert corr < 0.10
|
||||
corr = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=2,
|
||||
test_pdf=_infile('cardinal.pdf'),
|
||||
test_pageno=2,
|
||||
)
|
||||
assert corr > 0.90
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_autorotate(spoof_tesseract_cache, renderer):
|
||||
# cardinal.pdf contains four copies of an image rotated in each cardinal
|
||||
# direction - these ones are "burned in" not tagged with /Rotate
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
for n in range(1, 4+1):
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=n)
|
||||
assert correlation > 0.80
|
||||
|
||||
|
||||
def test_autorotate_threshold_low(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_low.pdf',
|
||||
'--rotate-pages-threshold', '1',
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
|
||||
# Low threshold -> always rotate -> expect high correlation between
|
||||
# reference page and test page
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=3)
|
||||
assert correlation > 0.80
|
||||
|
||||
|
||||
def test_autorotate_threshold_high(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_high.pdf',
|
||||
'--rotate-pages-threshold', '99',
|
||||
'-r', '-v', '1', env=spoof_tesseract_cache)
|
||||
|
||||
# High threshold -> never rotate -> expect low correlation since
|
||||
# test page will not be rotated
|
||||
correlation = check_monochrome_correlation(
|
||||
reference_pdf=_infile('cardinal.pdf'),
|
||||
reference_pageno=1,
|
||||
test_pdf=out,
|
||||
test_pageno=3)
|
||||
assert correlation < 0.10
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_ocr_timeout(renderer):
|
||||
out = check_ocrmypdf('skew.pdf', 'test_timeout_%s.pdf' % renderer,
|
||||
'--tesseract-timeout', '1.0')
|
||||
pdfinfo = pdf_get_all_pageinfo(out)
|
||||
assert pdfinfo[0]['has_text'] == False
|
||||
assert not pdfinfo[0]['has_text']
|
||||
|
||||
|
||||
def test_ocr_timeout():
|
||||
yield check_ocr_timeout, 'hocr'
|
||||
yield check_ocr_timeout, 'tesseract'
|
||||
|
||||
|
||||
def test_skip_big():
|
||||
def test_skip_big(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf('enormous.pdf', 'test_enormous.pdf',
|
||||
'--skip-big', '10')
|
||||
'--skip-big', '10', env=spoof_tesseract_cache)
|
||||
pdfinfo = pdf_get_all_pageinfo(out)
|
||||
assert pdfinfo[0]['has_text'] == False
|
||||
assert not pdfinfo[0]['has_text']
|
||||
|
||||
|
||||
def check_maximum_options(renderer):
|
||||
@pytest.mark.parametrize('renderer', ['hocr', 'tesseract'])
|
||||
@pytest.mark.parametrize('output_type', ['pdf', 'pdfa'])
|
||||
def test_maximum_options(spoof_tesseract_cache, renderer, output_type):
|
||||
check_ocrmypdf(
|
||||
'multipage.pdf', 'test_multipage%s.pdf' % renderer,
|
||||
'-d', '-c', '-i', '-g', '-f', '-k', '--oversample', '300',
|
||||
'--remove-background',
|
||||
'--skip-big', '10', '--title', 'Too Many Weird Files',
|
||||
'--author', 'py.test', '--pdf-renderer', renderer)
|
||||
|
||||
|
||||
def test_maximum_options():
|
||||
yield check_maximum_options, 'hocr'
|
||||
yield check_maximum_options, 'tesseract'
|
||||
|
||||
|
||||
def override_binary(binary, replacement):
|
||||
'''Create a directory that contains a symlink named 'binary' that
|
||||
points to replacement, another program to use in place of the
|
||||
regular binary for testing.
|
||||
|
||||
override_binary('gs', 'replace_gs.py') will create an environment
|
||||
in which "gs" will invoke replace_gs.py.
|
||||
|
||||
Not thread-safe with other test at the moment.
|
||||
|
||||
Returns the os.environ["PATH"] string under which this binary will
|
||||
be invoked.'''
|
||||
|
||||
replacement_path = os.path.abspath(os.path.join(TESTS_ROOT,
|
||||
replacement))
|
||||
subdir = os.path.splitext(os.path.basename(replacement))[0]
|
||||
binary_path = os.path.abspath(os.path.join(TEST_BINARY_PATH,
|
||||
subdir,
|
||||
binary))
|
||||
with suppress(FileExistsError):
|
||||
os.makedirs(os.path.dirname(binary_path))
|
||||
assert os.path.isdir(os.path.dirname(binary_path))
|
||||
assert not os.path.lexists(binary_path)
|
||||
print("symlink %s -> %s" % (replacement_path, binary_path))
|
||||
os.symlink(replacement_path, binary_path)
|
||||
|
||||
os.chmod(replacement_path, int('755', base=8))
|
||||
|
||||
return os.path.dirname(binary_path) + os.pathsep + os.environ["PATH"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def break_ghostscript_pdfa():
|
||||
return override_binary('gs', 'replace_ghostscript_nopdfa.py')
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.environ.get('OCRMYPDF_IN_DOCKER', False),
|
||||
reason="Requires writable filesystem")
|
||||
def test_ghostscript_pdfa_fails(break_ghostscript_pdfa):
|
||||
env = os.environ.copy()
|
||||
env['PATH'] = break_ghostscript_pdfa
|
||||
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
'graph_ocred.pdf', 'not_a_pdfa.pdf', '-v', '1', '--skip-text', env=env)
|
||||
assert p.returncode == ExitCode.ok, err # no longer using JHOVE PDFA check
|
||||
'--author', 'py.test', '--pdf-renderer', renderer,
|
||||
'--output-type', output_type,
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
def test_tesseract_missing_tessdata():
|
||||
env = os.environ.copy()
|
||||
env['TESSDATA_PREFIX'] = '/tmp'
|
||||
|
||||
p, _, err = run_ocrmypdf_env(
|
||||
p, _, err = run_ocrmypdf(
|
||||
'graph_ocred.pdf', 'not_a_pdfa.pdf', '-v', '1', '--skip-text', env=env)
|
||||
assert p.returncode == ExitCode.missing_dependency, err
|
||||
|
||||
|
||||
def test_invalid_input_pdf():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'invalid.pdf', 'wont_be_created.pdf')
|
||||
assert p.returncode == ExitCode.input_file, err
|
||||
|
||||
|
||||
def test_blank_input_pdf():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'blank.pdf', 'still_blank.pdf')
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
|
||||
def test_french():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
'francais.pdf', 'francais.pdf', '-l', 'fra')
|
||||
def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash):
|
||||
# As a correctness test, make sure that --force-ocr on a PDF with no
|
||||
# content still triggers tesseract. If tesseract crashes, then it was
|
||||
# called.
|
||||
p, _, err = run_ocrmypdf(
|
||||
'blank.pdf', 'wont_be_created.pdf', '--force-ocr',
|
||||
env=spoof_tesseract_crash)
|
||||
assert p.returncode == ExitCode.child_process_error, err
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
|
||||
|
||||
def test_french(spoof_tesseract_cache):
|
||||
p, out, err = run_ocrmypdf(
|
||||
'francais.pdf', 'francais.pdf', '-l', 'fra', env=spoof_tesseract_cache)
|
||||
assert p.returncode == ExitCode.ok, \
|
||||
"This test may fail if Tesseract language packs are missing"
|
||||
|
||||
|
||||
def test_klingon():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
p, out, err = run_ocrmypdf(
|
||||
'francais.pdf', 'francais.pdf', '-l', 'klz')
|
||||
assert p.returncode == ExitCode.bad_args
|
||||
|
||||
|
||||
def test_missing_docinfo():
|
||||
p, out, err = run_ocrmypdf_env(
|
||||
'missing_docinfo.pdf', 'missing_docinfo.pdf', '-l', 'eng', '-c')
|
||||
def test_missing_docinfo(spoof_tesseract_noop):
|
||||
p, out, err = run_ocrmypdf(
|
||||
'missing_docinfo.pdf', 'missing_docinfo.pdf', '-l', 'eng', '-c',
|
||||
env=spoof_tesseract_noop)
|
||||
assert p.returncode == ExitCode.ok, err
|
||||
|
||||
|
||||
@pytest.mark.skipif(running_in_docker(),
|
||||
reason="writes to tests/resources")
|
||||
def test_uppercase_extension(spoof_tesseract_noop):
|
||||
shutil.copy(_infile("skew.pdf"), _infile("UPPERCASE.PDF"))
|
||||
try:
|
||||
check_ocrmypdf("UPPERCASE.PDF", "UPPERCASE_OUT.PDF",
|
||||
env=spoof_tesseract_noop)
|
||||
finally:
|
||||
os.unlink(_infile("UPPERCASE.PDF"))
|
||||
|
||||
|
||||
def test_input_file_not_found():
|
||||
input_file = "does not exist.pdf"
|
||||
p, out, err = run_ocrmypdf(
|
||||
_infile(input_file),
|
||||
_outfile("will not happen.pdf"))
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert (input_file in out or input_file in err)
|
||||
|
||||
|
||||
def test_input_file_not_a_pdf():
|
||||
input_file = __file__ # Try to OCR this file
|
||||
p, out, err = run_ocrmypdf(
|
||||
_infile(input_file),
|
||||
_outfile("will not happen.pdf"))
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert (input_file in out or input_file in err)
|
||||
|
||||
|
||||
def test_qpdf_repair_fails():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_QPDF'] = os.path.abspath('./spoof/qpdf_dummy_return2.py')
|
||||
p, out, err = run_ocrmypdf(
|
||||
'-v', '1',
|
||||
'c02-22.pdf', 'wont_be_created.pdf', env=env)
|
||||
print(out)
|
||||
print(err)
|
||||
assert p.returncode == ExitCode.input_file
|
||||
|
||||
|
||||
def test_encrypted():
|
||||
p, out, err = run_ocrmypdf('skew-encrypted.pdf', 'wont_be_created.pdf')
|
||||
assert p.returncode == ExitCode.input_file
|
||||
assert out.find('password')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_pagesegmode(renderer, spoof_tesseract_cache):
|
||||
check_ocrmypdf(
|
||||
'skew.pdf', 'test_psm_%s.pdf' % renderer,
|
||||
'--tesseract-pagesegmode', '7',
|
||||
'-v', '1',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_crash(renderer, spoof_tesseract_crash):
|
||||
p, out, err = run_ocrmypdf(
|
||||
'ccitt.pdf', 'wontwork.pdf', '-v', '1',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_crash)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
|
||||
|
||||
def test_tesseract_crash_autorotate(spoof_tesseract_crash):
|
||||
p, out, err = run_ocrmypdf(
|
||||
'ccitt.pdf', 'wontwork.pdf',
|
||||
'-r', env=spoof_tesseract_crash)
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
assert not os.path.exists(_outfile('wontwork.pdf'))
|
||||
assert "ERROR" in err
|
||||
print(out)
|
||||
print(err)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr',
|
||||
'tesseract',
|
||||
])
|
||||
def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
|
||||
check_ocrmypdf(
|
||||
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
|
||||
|
||||
|
||||
def test_no_unpaper():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/no_unpaper_here.py')
|
||||
p, out, err = run_ocrmypdf(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_old_unpaper():
|
||||
env = os.environ.copy()
|
||||
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/unpaper_oldversion.py')
|
||||
p, out, err = run_ocrmypdf(
|
||||
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
|
||||
assert p.returncode == ExitCode.missing_dependency
|
||||
|
||||
|
||||
def test_algo4():
|
||||
p, _, _ = run_ocrmypdf('encrypted_algo4.pdf', 'wontwork.pdf')
|
||||
assert p.returncode == ExitCode.encrypted_pdf
|
||||
|
||||
|
||||
@pytest.mark.parametrize('renderer', [
|
||||
'hocr']) # tesseract cannot pass this test - resamples to square image
|
||||
def test_non_square_resolution(renderer, spoof_tesseract_cache):
|
||||
# Confirm input image is non-square resolution
|
||||
in_pageinfo = pdf_get_all_pageinfo(_infile('aspect.pdf'))
|
||||
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
|
||||
|
||||
out = 'aspect_%s.pdf' % renderer
|
||||
check_ocrmypdf(
|
||||
'aspect.pdf', out,
|
||||
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(_outfile(out))
|
||||
|
||||
# Confirm resolution was kept the same
|
||||
assert in_pageinfo[0]['xres'] == out_pageinfo[0]['xres']
|
||||
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
|
||||
|
||||
|
||||
def test_image_to_pdf(spoof_tesseract_noop):
|
||||
check_ocrmypdf(
|
||||
'LinnSequencer.jpg', 'image_to_pdf.pdf', '--image-dpi', '200',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_jbig2_passthrough(spoof_tesseract_cache):
|
||||
out = check_ocrmypdf(
|
||||
'jbig2.pdf', 'jbig2_out.pdf',
|
||||
'--output-type', 'pdf',
|
||||
'--pdf-renderer', 'hocr',
|
||||
env=spoof_tesseract_cache)
|
||||
|
||||
out_pageinfo = pdf_get_all_pageinfo(out)
|
||||
assert out_pageinfo[0]['images'][0]['enc'] == 'jbig2'
|
||||
|
||||
|
||||
def test_stdin(spoof_tesseract_noop):
|
||||
input_file = _infile('francais.pdf')
|
||||
output_file = _outfile('test_stdin.pdf')
|
||||
|
||||
# Runs: ocrmypdf - output.pdf < testfile.pdf
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
p_args = OCRMYPDF + ['-', output_file]
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
stdin=input_stream, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
|
||||
def test_stdout(spoof_tesseract_noop):
|
||||
input_file = _infile('francais.pdf')
|
||||
output_file = _outfile('test_stdout.pdf')
|
||||
|
||||
# Runs: ocrmypdf francais.pdf - > test_stdout.pdf
|
||||
with open(output_file, 'wb') as output_stream:
|
||||
p_args = OCRMYPDF + [input_file, '-']
|
||||
p = Popen(
|
||||
p_args, close_fds=True, stdout=output_stream, stderr=PIPE,
|
||||
stdin=DEVNULL, env=spoof_tesseract_noop)
|
||||
out, err = p.communicate()
|
||||
|
||||
assert p.returncode == ExitCode.ok
|
||||
|
||||
from ocrmypdf import qpdf
|
||||
assert qpdf.check(output_file, log=None)
|
||||
|
||||
|
||||
def test_masks(spoof_tesseract_noop):
|
||||
check_ocrmypdf('masks.pdf', 'test_masks.pdf', env=spoof_tesseract_noop)
|
||||
|
||||
|
||||
def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop):
|
||||
check_ocrmypdf(
|
||||
'epson.pdf', 'test_epson.pdf',
|
||||
env=spoof_tesseract_noop)
|
||||
|
||||
@@ -10,20 +10,37 @@ import os
|
||||
import shutil
|
||||
import pytest
|
||||
import img2pdf
|
||||
from pkg_resources import Requirement, resource_filename
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
req = Requirement.parse('ocrmypdf')
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
print("Requires Python 3.4+")
|
||||
sys.exit(1)
|
||||
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
|
||||
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
||||
OCRMYPDF = os.path.join(PROJECT_ROOT, 'OCRmyPDF.sh')
|
||||
TEST_RESOURCES = os.path.join(PROJECT_ROOT, 'tests', 'resources')
|
||||
TEST_OUTPUT = os.environ.get(
|
||||
'OCRMYPDF_TEST_OUTPUT',
|
||||
default=os.path.join(os.path.dirname(__file__), 'output'))
|
||||
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'pageinfo'))
|
||||
|
||||
|
||||
def setup_module():
|
||||
with suppress(FileNotFoundError):
|
||||
shutil.rmtree(TEST_OUTPUT)
|
||||
with suppress(FileExistsError):
|
||||
os.mkdir(TEST_OUTPUT)
|
||||
os.makedirs(TEST_OUTPUT)
|
||||
|
||||
|
||||
def _make_input(input_basename):
|
||||
return os.path.join(TEST_RESOURCES, input_basename)
|
||||
|
||||
|
||||
def _make_output(output_basename):
|
||||
return os.path.join(TEST_OUTPUT, output_basename)
|
||||
|
||||
|
||||
def test_single_page_text():
|
||||
@@ -50,13 +67,21 @@ def test_single_page_text():
|
||||
def test_single_page_image():
|
||||
filename = os.path.join(TEST_OUTPUT, 'image-mono.pdf')
|
||||
|
||||
with NamedTemporaryFile() as im_tmp:
|
||||
with NamedTemporaryFile(mode='wb+', suffix='.png') as im_tmp:
|
||||
im = Image.new('1', (8, 8), 0)
|
||||
for n in range(8):
|
||||
im.putpixel((n, n), 1)
|
||||
im.save(im_tmp.name, format='PNG')
|
||||
|
||||
pdf_bytes = img2pdf.convert([im_tmp.name], dpi=8)
|
||||
imgsize = ((img2pdf.ImgSize.dpi, 8), (img2pdf.ImgSize.dpi, 8))
|
||||
layout_fun = img2pdf.get_layout_fun(None, imgsize, None, None, None)
|
||||
|
||||
im_tmp.seek(0)
|
||||
im_bytes = im_tmp.read()
|
||||
pdf_bytes = img2pdf.convert(
|
||||
im_bytes, producer="img2pdf", with_pdfrw=False,
|
||||
layout_fun=layout_fun)
|
||||
|
||||
with open(filename, 'wb') as pdf:
|
||||
pdf.write(pdf_bytes)
|
||||
|
||||
@@ -78,8 +103,8 @@ def test_single_page_image():
|
||||
assert pdfimage['bpc'] == 8
|
||||
|
||||
# DPI in a 1"x1" is the image width
|
||||
assert pdfimage['dpi_w'] == 8
|
||||
assert pdfimage['dpi_h'] == 8
|
||||
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
|
||||
|
||||
|
||||
def test_single_page_inline_image():
|
||||
@@ -95,15 +120,20 @@ def test_single_page_inline_image():
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
pageinfo.pdf_get_all_pageinfo(filename)
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(filename)
|
||||
print(pdfinfo)
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert (pdfimage['dpi_w'] - 8) < 1e-5
|
||||
assert pdfimage['color'] != '-'
|
||||
assert pdfimage['width'] == 8
|
||||
|
||||
|
||||
def test_jpeg():
|
||||
filename = resource_filename(req, 'tests/resources/c02-22.pdf')
|
||||
filename = _make_input('c02-22.pdf')
|
||||
|
||||
pdfinfo = pageinfo.pdf_get_all_pageinfo(filename)
|
||||
|
||||
pdfimage = pdfinfo[0]['images'][0]
|
||||
assert pdfimage['enc'] == 'jpeg'
|
||||
assert (pdfimage['dpi_w'] - 150) < 1e-5
|
||||
|
||||
Reference in New Issue
Block a user