Compare commits

..
163 Commits
Author SHA1 Message Date
James R. Barlow aff597cef4 v4.2.5: update release notes, fix silly typo in pageinfo.py 2016-10-13 13:26:39 -07:00
James R. Barlow 61b05b3dee Fix issue: BitsPerComponent is an optional field, sometimes omitted 2016-10-13 13:15:27 -07:00
Julian Kahnertandjbarlow83 453c4ef602 Update README.rst (#98)
`brew install tesseract` just installed the english language pack not French, German or Spanish
2016-10-12 11:20:58 -07:00
James R. Barlow cf4b04f92d The main 'quick' test should be a file that OCRs to recognizable text 2016-10-07 16:25:34 -07:00
James R. Barlow 06c6999987 Merge commit '07891d994aab92e7a14aebe1ac509aab2d4f170c' 2016-10-07 12:45:56 -07:00
James R. Barlow 013c5a369f Replace redacted file with an OCR-able file 2016-10-07 12:45:22 -07:00
James R. Barlow 07891d994a Replace redacted file with an OCR-able file 2016-10-07 12:44:49 -07:00
James R. Barlow 6baf8668a6 Replace with non-free file milk.pdf with free equivalent 2016-10-06 13:10:28 -07:00
James R. Barlow 4ba2962c56 Comment on non-free files 2016-10-05 16:48:16 -07:00
James R. Barlow 7ad92f5db4 Merge branch 'master' of https://github.com/jbarlow83/OCRmyPDF 2016-10-05 16:39:00 -07:00
James R. Barlow 4dad09cc91 resources/README: replace the other large table with a list table 2016-10-05 16:38:51 -07:00
Sean Whittonandjbarlow83 7b2e0c7a7a also exclude .git in pytest.ini (#94) 2016-09-15 08:56:14 -07:00
Sean Whittonandjbarlow83 7f08f15fc9 pytest skipif for milk.pdf test (#95)
Skip the test if the fair use restricted milk.pdf is not present.
2016-09-15 08:55:31 -07:00
James R. Barlow 825c0f8b2a Note that milk.pdf is non-free, start using list-tables 2016-09-10 14:44:00 -07:00
James R. Barlow dbe880bc41 Update tesseract supported languages 2016-09-09 12:55:07 -07:00
James R. Barlow 220f1ce161 tasks.py: stop tracking this file for now
This helper script is still in development and needs to be changed each
release, which breaks the release.

It shouldn't be in MANIFEST.in at all because it's not part of a
distribution.
2016-09-04 10:55:57 -07:00
James R. Barlow c62a8a97c9 v4.2.4 release notes 2016-09-01 21:33:38 -07:00
James R. Barlow f8a1136979 tasks: show logging info 2016-09-01 21:24:13 -07:00
James R. Barlow 9ca29c787b Update description of masks.pdf to reflect what it actually tests 2016-09-01 21:21:14 -07:00
James R. Barlow 6af748a251 pageinfo: regression - didn't add inline images to list 2016-09-01 15:27:51 -07:00
James R. Barlow 9041867f86 pageinfo: exclude images from DPI calculation if drawn at stack depth 0
More thorough testing showed that Acrobat do not presume that images
fill the page if the CTM is unspecified, as tests/resources/masks.pdf
seems to want.  Instead they treat it literally and draw the image
as 1x1 PDF units or 1/72" square in the bottom left corner of the page.

Seems like the best thing to do is ignore any such images for the purpose
of DPI calculation.  masks.pdf still works out okay because it has
other images.

For more robustness we could consider invalidating any DPI above some
limit, or warning the user about these microdot thumbnails.
2016-09-01 14:23:31 -07:00
James R. Barlow 04099b087c pageinfo: handle stencil masks when stack depth > 0 2016-09-01 14:03:30 -07:00
James R. Barlow 6d6234714c tasks: fix logic error and make magic numbers disappear 2016-09-01 14:03:08 -07:00
James R. Barlow 520be23481 Add release helper script 2016-08-31 20:33:04 -07:00
James R. Barlow 346c3c8dd3 Start tracking development requirements 2016-08-31 20:31:31 -07:00
James R. Barlow bd534c3313 main.py -> __main__.py
Executing a package with python -m packagename will check for
__main__.py inside the package.  In other words main.py should have
always been named __main__.py.

In the unlikely event that someone depends on "import ocrmypdf.main"
being meaningful, main.py continues to exist and replicates the
behavior of __main__.  (It's unlikely because import ocrmypdf.main does
unpythonic ruffus-related things at things import time, essentially
configuring itself to work with sys.argv.  To fix another day.)

This should solve the problem of Debian needing to run test suites
before installation and afterwards for continuous integration without
having to patch either file, as python -m ocrmypdf will follow import
order.  That is, if the current directory contains "ocrmypdf/" (e.g.
staging a new version) then that will be tested, else sys.path will
be checked.
2016-08-31 17:01:42 -07:00
James R. Barlow 2625368aed link: more MANIFEST.in tweaks 2016-08-31 16:28:39 -07:00
James R. Barlow 8ac94879f1 lint: no need to check for DEVNULL; all supported versions have it 2016-08-31 16:28:18 -07:00
James R. Barlow dd8c0f3756 Merge branch 'master' of https://github.com/jbarlow83/OCRmyPDF 2016-08-31 13:19:46 -07:00
James R. Barlow 010f353a5e v4.2.3 release notes 2016-08-31 13:19:27 -07:00
James R. Barlow e0a18edb92 Fix MANIFEST.in, as Python packages require 2016-08-31 13:19:17 -07:00
James R. Barlow c6f2eea058 Reinstate OCRmyPDF.sh with a deprecation warning 2016-08-31 11:57:02 -07:00
James R. Barlow bf89e38c69 Add milk.pdf test case 2016-08-31 11:42:21 -07:00
jbarlow83andGitHub e1f0640d42 Create issue template 2016-08-31 11:26:29 -07:00
James R. Barlow 71b54035ba Bug fix issue #89: trying to perform arithmetic on IndirectObject
TypeError: bad operand type for unary -: 'IndirectObject'
2016-08-31 10:25:58 -07:00
James R. Barlow 325cc0beca Allow test cases to run without installing first
As @spwhitton found:

The test suite needs to call "python3 -m ocrmypdf.main" instead of
just "ocrmypdf" because this /usr/bin/ocrmypdf script has not yet been
generated when dh runs the test suite.

---

Seems reasonable to perform in-place testing independent of installation.

Source:
https://sources.debian.net/src/ocrmypdf/4.2.1%2Bgit.20160824.1.5d67cc7-1/debian/patches/0001-patch-test-suite-executable.patch/
2016-08-26 15:23:26 -07:00
James R. Barlow 1a9f09c4d5 Remove OCRmyPDF.sh and its usage in all test cases 2016-08-26 15:18:38 -07:00
James R. Barlow 4fed4e2af3 tests: don't try to pass Unicode arguments on command line on Linux
Depends on locale being configured properly, and it's not necessary
to be able to do this.
2016-08-26 15:08:56 -07:00
James R. Barlow 74cc2346a5 pytest.ini: apply patch from Debian to exclude .pc dir
https://sources.debian.net/src/ocrmypdf/4.2.1%2Bgit.20160824.1.5d67cc7-1/debian/patches/0003-pytest-exclude-.pc-dir.patch/

From: Sean Whitton <spwhitton@spwhitton.name>
Date: Thu, 12 May 2016 08:21:39 -0700
Subject: pytest exclude .pc dir
Forwarded: not-needed

Patches to the test suite present in .pc confuse pytest.
2016-08-26 15:07:11 -07:00
James R. Barlow cc7e328358 Improve some documentation for tests 2016-08-26 15:04:08 -07:00
James R. Barlow d25397e2b0 Add test case for PDFs with masks and stencil masks 2016-08-26 15:03:27 -07:00
James R. Barlow bc11454e1c Help text: example of shell pipeline with img2pdf 2016-08-25 14:58:25 -07:00
James R. Barlow 2025a096c3 Test case for stdin streaming 2016-08-25 14:46:54 -07:00
James R. Barlow 38fe14b108 Make final PDF/A output message less obtuse 2016-08-25 14:46:40 -07:00
James R. Barlow 1b7b2f3695 v4.2.2 release notes, documentation improvements 2016-08-25 14:46:09 -07:00
James R. Barlow 5d67cc76cc Update 4.2.1 release notes 2016-08-24 14:16:22 -07:00
James R. Barlow 27a3813207 Recover input filename from symlink on error message
The recent commit to accept files from stdin broken the feature of
returning the input filename on an error, returning the temp filename
instead, which is confusing.
2016-08-23 17:38:28 -07:00
James R. Barlow b06e0bfdcd Merge branch 'develop' 2016-08-23 16:03:07 -07:00
James R. Barlow d616f25324 Implement DPI checking for stencil masks 2016-08-23 15:59:34 -07:00
James R. Barlow b03028e31f setup.py -> license is MIT 2016-08-19 10:14:33 -07:00
James R. Barlow e08c42fd3d Tweak pipeline again 2016-08-09 22:40:29 -07:00
James R. Barlow 16901f7134 Accept input from stdin if input filename is '-' 2016-08-09 15:46:24 -07:00
James R. Barlow dffceedd85 Update the pipeline image 2016-08-09 15:45:19 -07:00
James R. Barlow e5541e435c New test to confirm we can emit JBIG2 with appropriate settings 2016-08-03 11:35:48 -07:00
James R. Barlow b969aad67b Tweak release notes 2016-08-03 03:36:45 -07:00
James R. Barlow e70387b1af Add a simple test for image to PDF 2016-08-03 03:35:30 -07:00
James R. Barlow 44f47fba21 PDF/A: handle case of no XMP metadata gracefully 2016-08-03 02:57:25 -07:00
James R. Barlow 02584094a1 Suppress NUL bytes in metadata from input files 2016-08-03 02:47:44 -07:00
James R. Barlow 91d715ac93 Add test cases for --output-type 2016-08-03 02:47:18 -07:00
James R. Barlow 35addb8a33 Complain if Chinese is requested with settings known to not work
Should extend test for other Asian languages
2016-08-03 01:29:12 -07:00
James R. Barlow d32ea8d0dd Remove dead code from qpdf merge + PyPDF2 metadata patching
I tried "qpdf merge + PyPDF2 metadata patching" first. The problem is
that PyPDF2 produces a 1.3 by default and generally I have less
confidence it.

New approach is to stuff the Document Info metadata in the first page
with PyPdf2, cross fingers and use qpdf to merge. It's not quite as
clean and might harm the first page, but it's better than shipping
files produced by PyPDF2.
2016-08-03 01:28:27 -07:00
James R. Barlow 12575d594a Improve PDF/A validity checking at end 2016-08-03 01:26:16 -07:00
James R. Barlow 0746083301 Fix failing test case - unbound local variable in finally block 2016-08-03 01:00:38 -07:00
James R. Barlow 5c99acf6d1 Experimental change to use qpdf to merge files (disables Ghostscript)
All but one tests pass, test_input_file_not_a_pdf

Not sure if PyPDF2 metadata generation will mangle the first page.
2016-08-03 00:56:44 -07:00
James R. Barlow 2b10df7b74 leptonica: note about when it may be safe to drop <1.72 workaround 2016-08-03 00:54:37 -07:00
James R. Barlow ebe68de4ff Functional qpdfmerge with PyPDF2 for DocumentInfo block
Tests mostly passing. For the moment this is the new default.

Although PyPDF2 produces a PDF-1.3 which will be wrong for some contents
and possible should be repaired with qpdf. Again.

Looks like it could work better to merge PyPDF2 and fix everything
with qpdf.
2016-08-02 16:48:13 -07:00
James R. Barlow b17c6a146d Experimental qpdf merging
Does not copy /Catalog metadata, but otherwise functional
2016-08-02 02:19:02 -07:00
James R. Barlow 46d837c866 Clarify trusty/precise stuff 2016-08-02 01:29:33 -07:00
James R. Barlow 24856b61e4 Fix typo in readme 2016-08-02 01:29:22 -07:00
James R. Barlow 8d0c6ff616 pyvenv -> python3 -m venv
Sadly the Python developers are removing this script
2016-08-02 01:27:50 -07:00
James R. Barlow 0b24f971cd ocrmyimage: complain about ICC profiles being presumed 2016-08-02 01:22:36 -07:00
James R. Barlow bc5d3824bd Don't overload --oversample, use --image-dpi instead for images 2016-07-31 02:09:30 -07:00
James R. Barlow 4356983707 Suppress overly long stack traces on traverse_ruffus_exception 2016-07-31 02:06:44 -07:00
James R. Barlow 2414b79ee6 More cleanup of exception related errors 2016-07-31 01:48:13 -07:00
James R. Barlow 968e1546f0 Refactor image file triage 2016-07-31 01:47:57 -07:00
James R. Barlow 48213c9c3f Update release notes and readme 2016-07-29 15:25:16 -07:00
James R. Barlow f385772d21 Refactor "is this an iterable that's not a string?" test 2016-07-29 15:25:02 -07:00
James R. Barlow d257c83520 Most tests were failing at split_pages()
It seems that ruffus sometimes decides to send a ['inputfile.pdf']
instead of a bare string.
2016-07-29 14:59:17 -07:00
James R. Barlow 7b72ffec4f ocrmyimage: better handling of missing/invalid DPI 2016-07-29 14:38:07 -07:00
James R. Barlow 757f6826dc ocrmyimage - Attempt conversion to PDF if input file is not a PDF
First cut.

May have broken ruffus errors again too.
2016-07-29 14:03:19 -07:00
James R. Barlow 5df83a0d30 Travis: use Python 3.5 too 2016-07-29 13:31:40 -07:00
James R. Barlow d70e3d3753 ruffus exceptions: for clarity only, don't iterate strings
It's a good habit to ensure any iterator test is explicit about
allowing or disallowing strings.
2016-07-29 13:31:24 -07:00
James R. Barlow 0dfceedcfb Remove old OCRmyPDF 2.x from release notes; update 4.2 notes 2016-07-29 03:08:59 -07:00
James R. Barlow 2c30f4bfc5 Travis: build partly working on trusty; tweak requirements again
The build is #122
https://travis-ci.org/jbarlow83/OCRmyPDF/builds/148255615

Errors seem to be related to either Ghostscript or leptonica? Maybe
-dSAFER?
2016-07-29 03:08:01 -07:00
James R. Barlow 9e7fb52b47 Travis: add PPA to support unpaper 2016-07-29 01:57:12 -07:00
James R. Barlow bb5fd38e38 Remove additional PPA's and try again 2016-07-29 01:47:56 -07:00
James R. Barlow 7c8cf5cfa2 Try travis-trusty
This removes some backports for packages that Ubuntu trusty offers but
for which Ubuntu precise needed help.
2016-07-29 01:44:57 -07:00
James R. Barlow fef35e4eb2 Fix handling of DPI for rare case of JPEG recompression after deskew/clean
This test is exercised by page 4 of multipage.pdf. If all images are
JPEGs, and one of deskew/clean removes DPI information, make sure that
we can get the right information back and that the DPI stays square.
2016-07-29 01:34:52 -07:00
James R. Barlow 8f77576dc4 Fix non-square image resolution for "hocr" case; use img2pdf 0.2.1
Tesseract renderer not immediately fixable.
2016-07-28 16:43:51 -07:00
James R. Barlow b3fcf24a26 Refactor DPI: fix regressions in test suite
Some called functions are particular about the data format of DPI and
don't like to deal with the Decimal() returned by PyPDF2. Convert to
float and int where needed.
2016-07-28 00:19:32 -07:00
James R. Barlow 16e4d342d2 Bug fix: --force-ocr should still run on pages with no images
Useful for people who want to reprocess text.

This also requires --oversample because DPI is undefined. To be fixed
in next commit.
2016-07-27 15:06:49 -07:00
James R. Barlow 8458a51860 Tighten requirements and dependencies 2016-07-27 14:47:59 -07:00
James R. Barlow 636d1903b3 Ghostscript: do raster output with -dSAFER
-dSAFER does not work when rendering PDF/A, because that needs to load
the ICC file, and -dSAFER prevents access to external files.
2016-07-27 00:54:40 -07:00
jbarlow83andGitHub 514efa36fc Readme: Add table of contents, brew install tesseract --with-language packs 2016-07-24 11:21:46 -07:00
James R. Barlow bd48f40d3d v4.1.4 release notes 2016-07-17 00:35:06 -07:00
James R. Barlow c02dbc809a Merge commit '68cf9cbd87c188823027f9d1bfe9029017e7281f' into develop 2016-07-17 00:29:48 -07:00
James R. Barlow 410111d6fb Bug fix: Monochrome images with ICC treated as full color images
Issue #79.
User submitted PDF with ICC profile attached to the monochrome image
in the input file, which is not common but useful for PDFs that want to
define how light the paper is or how dark the black is. The code was
written to assume unusual images are full color unless it can prove
otherwise. Handle this simple case. Other ICC cases should be tested.
2016-07-17 00:29:32 -07:00
jbarlow83andGitHub 68cf9cbd87 .rst: add code-block markup 2016-07-05 14:03:55 -07:00
jbarlow83andGitHub c9b2540d9d Fix some .rst formatting errors 2016-07-05 13:48:19 -07:00
jbarlow83andGitHub 1bacf35a2c Update license information for encrypted_algo4.pdf 2016-06-24 14:25:15 -07:00
jbarlow83andGitHub 8aef0d9277 Merge pull request #76 from Jmuccigr/patch-2
Adding explicit reference to help
2016-06-24 14:21:23 -07:00
John MuccigrossoandGitHub b2fa8645ba Adding explicit reference to help 2016-06-24 13:44:12 -05:00
James R. Barlow c96823a648 v4.1.3 release notes 2016-06-23 13:47:56 -07:00
James R. Barlow 3807b7d655 Merge branch 'feature/leptfun' into develop 2016-06-23 13:45:35 -07:00
James R. Barlow a45505cf1d Fix order of operations in matrix multiplication
Issue #73. The order of operations happens to not matter for scaling
but does matter for translation. We only need scaling to find the DPI,
so the error was not noticed. Mainly useful to other uses of this
library.
2016-06-23 13:36:23 -07:00
James R. Barlow b4a734fc0d Test case for "algorithm 4" test
Algorithm 4 -> PDF version 1.6
2016-06-23 13:21:26 -07:00
James R. Barlow bbd02926e1 Add helpful error message for PDFs that use algorithm 4 2016-06-23 13:13:17 -07:00
jbarlow83andGitHub 5022ded276 Update Windows directions 2016-06-16 15:15:46 -07:00
James R. Barlow c7612152ef leptonica: pillow interop 2016-06-06 23:55:23 -07:00
James R. Barlow af91642cd1 lept: fix __getstate/__setstate 2016-06-06 23:55:23 -07:00
James R. Barlow 9c66334c38 Leptonica - ortho rotate, background norm 2016-06-06 23:55:23 -07:00
James R. Barlow 8d79b94b84 cpix -> _pix 2016-06-06 23:55:23 -07:00
James R. Barlow d7f60b96c1 More leptonica functions for page manipulation 2016-06-06 23:55:23 -07:00
James R. Barlow b964999427 Update filename references from sRGB_IEC to sRGB 2016-05-10 21:58:04 -07:00
James R. Barlow 3473345ea6 Replace sRGB_IEC with MIT license compatible sRGB
New file is from Debian package icc-profiles-free
2016-05-10 21:48:32 -07:00
James R. Barlow 349ec5c81f Provide more helpful error message if pypdf can't merge pages 2016-04-28 14:02:12 -07:00
James R. Barlow ff78d7c56c v4.1 release notes 2016-04-28 00:46:16 -07:00
James R. Barlow ff092c8629 Fix race condition between these tests when run in parallel 2016-04-28 00:39:15 -07:00
James R. Barlow fe14cb57c0 Fix ruffus exception output
I found this issue in ruffus 2.6.3
https://github.com/bunbun/ruffus/issues/65
also discussed here
https://github.com/bunbun/ruffus/pull/67

ruffus 2.6.3 RethrownJobError don't follow the normal conventions and
so its exception causes problems when they cross process boundaries.
This change carefully examines the various forms of ruffus exception
objects that can appear in 2.6.3 and parses them more carefully. It
also removes any direct posting of the exception to the logger because
this triggers another serializing of the exception object, mutating it
further.
2016-04-28 00:38:50 -07:00
James R. Barlow 507fbc01d5 Refactor _find_page_images 2016-04-27 13:13:43 -07:00
James R. Barlow 325479e5be Fix test failure: inline images with multiple image filters specified 2016-04-27 13:09:41 -07:00
James R. Barlow e926ecb8b2 Fuzzing: check for graphics stack overflow
Very unlikely to occur
2016-04-18 13:08:35 -07:00
James R. Barlow d0cb6c0e92 Replace private hypotenuse formula with hypot() 2016-04-18 13:07:53 -07:00
James R. Barlow 5b7c8cf5d3 Remove check for /ImageMask
/ImageMask means the the image is a stencil mask for a grayscale or
color image. From issue #63 a user has a PDF apparently with only a
stencil mask and no other images. According to PDF spec you're supposed
to use masks to help draw other images rather than draw masks
explicitly, although drawing masks is not forbidden.

In the event that the image mask is higher resolution than the input
 image, the composite image should be drawn at the highest DPI to a
ccurately capture the effect of the mask. It looks like the right to
 do is take masks into account rather than ignore them (a holdover
from earlier, less robust versions), or don't treat it different.
2016-04-14 14:24:33 -07:00
James R. Barlow 40baab32ac Remove dead code "import stuff in testcase" 2016-04-14 14:22:34 -07:00
James R. Barlow e877d37ac8 --rotate-pages: Only apply rotation if we're reasonable confident
Take the threshold from tesseract's default value for -psm 1.
2016-04-14 13:49:44 -07:00
James R. Barlow 5a9f77e438 Merge commit '1605408c23fa1b9252c5d3f10f279b43733b0728' into develop 2016-04-14 13:07:10 -07:00
James R. Barlow 8ddd67d1e2 Check encoding of inline images 2016-03-24 15:11:10 -07:00
jbarlow83 1605408c23 README: add libffi-dev 2016-03-22 23:39:02 -07:00
James R. Barlow 2d3b1ebf6e Simplify DPI calculation with algebraic derivation
Needs testing
2016-03-20 12:35:41 -07:00
James R. Barlow c74eaab7f5 Update license: sRGB ICC 2016-03-20 12:34:34 -07:00
James R. Barlow c21d231388 Merge commit 'a73afc4e769202b916d35dee481d741cf6bb7224' 2016-03-20 12:33:15 -07:00
jbarlow83 a73afc4e76 Merge pull request #59 from spwhitton/apt-get
README: Debian and Ubuntu installation option
2016-03-14 00:19:59 -07:00
Sean Whitton 76c364150d README: Debian and Ubuntu installation option 2016-03-13 23:15:00 -07:00
James R. Barlow 94a3e447cc Add otsu threshold to leptonica 2016-03-12 00:09:20 -08:00
James R. Barlow 12868b461a Travis: install unpaper.deb instead of compiling from source 2016-03-11 21:16:01 -08:00
James R. Barlow 322085933b unpaper: fix check for missing and old versions, add test case 2016-03-10 15:37:09 -08:00
James R. Barlow 3fed94bb79 v4.0.7 2016-03-02 06:27:01 -08:00
James R. Barlow 8c877482bd Fix leptonica initializers 2016-03-02 06:26:25 -08:00
James R. Barlow b17d589e84 Don't set -sOutputICCProfile
Ghostscript dev advised against. It appears that this is for
creating target for a device that colors in a particular format.
2016-03-02 06:25:34 -08:00
James R. Barlow 368252a243 setuptools_scm_git_archive seems suddenly broken 2016-03-01 02:09:45 -08:00
James R. Barlow ccefda1bee v4.0.6 notes 2016-03-01 01:58:32 -08:00
James R. Barlow 3d0e8c9629 Provide our own sRGB profile instead of Ghostscript's 2016-03-01 01:27:40 -08:00
James R. Barlow 313bbbb94c setup_scm_git_archive: add additional files 2016-02-29 12:46:27 -08:00
James R. Barlow 0360f078de get_postscript_icc_path: don't check the same path multiple times 2016-02-29 12:45:58 -08:00
James R. Barlow c8901666c4 Merge branch 'master' of https://github.com/jbarlow83/OCRmyPDF 2016-02-29 00:06:07 -08:00
James R. Barlow 7430006596 Improve install instructions for OS X (unpaper) 2016-02-29 00:05:31 -08:00
James R. Barlow f3e06b2dbd Add bookmarks to file for more testing 2016-02-29 00:05:07 -08:00
jbarlow83 e97df307ff Merge pull request #54 from stweil/master
Replace broken link to c't article by permalink
2016-02-28 07:18:40 -08:00
Stefan Weil 1443354aa2 Replace broken link to c't article by permalink
Update also the 2nd article link to use a permalink, too.

Signed-off-by: Stefan Weil <sw@weilnetz.de>
2016-02-28 13:57:42 +01:00
James R. Barlow 250e68c1cd v4.0.5 release notes 2016-02-27 01:01:38 -08:00
James R. Barlow 6a380ee99c Fix temporary file placed in wrong folder 2016-02-27 00:51:47 -08:00
James R. Barlow 3c90bd96a9 Remove extraneous debug print() messages 2016-02-27 00:50:58 -08:00
James R. Barlow 06a7ceb25a v4.0.4 Updates release notes 2016-02-27 00:22:37 -08:00
James R. Barlow 733a8e7d58 Merge branch 'feature/parsecontent' 2016-02-27 00:19:19 -08:00
James R. Barlow 570bbe9a05 Add comments and remove debugging, improve inline handling
Squashed commits:
[bfff3c9] pageinfo, have a main()
2016-02-27 00:18:36 -08:00
James R. Barlow 5cc3adb39a Add support for inline images 2016-02-27 00:18:36 -08:00
James R. Barlow 3957a0606c Compute image pixel density without performing rectangle intersection (+5 squashed commits)
Squashed commits:
[0e27904] Partially implement DPI calculation with rotation of the image

Fixes test suite
[a64f662] pageinfo: all tests pass
[c5b811a] Fix typos
[cdd2286] Can now find inline images for efficiently
[60dde8d] First cut at implementing intelligent DPI detection based on content stream

Broke many of the test cases
2016-02-27 00:18:36 -08:00
James R. Barlow 11a561dbce v4.0.3 release notes 2016-02-26 01:12:15 -08:00
James R. Barlow dad2198394 Log information about detected page orientations in a summary line 2016-02-26 01:07:59 -08:00
James R. Barlow e40fdc502d Always dump stack trace for unexpected errors 2016-02-26 01:06:59 -08:00
James R. Barlow d446fe5922 Fix "too few characters" reported as error by tesseract -psm 0 2016-02-21 08:53:34 -08:00
James R. Barlow 4ca90c106d Docker: fix blank JPEG2000 PDF issue 2016-02-21 04:24:21 -08:00
43 changed files with 3165 additions and 1958 deletions
+1
View File
@@ -0,0 +1 @@
ref-names: $Format:%D$
+3 -1
View File
@@ -5,4 +5,6 @@
# (binary is a macro for -text -diff)
*.jar binary
*.pdf binary
*.PDF binary
*.PDF binary
.git_archival.txt export-subst
+3
View File
@@ -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).
+2 -1
View File
@@ -3,6 +3,7 @@
*.sublime-*
venv-*/
pyvenv.cfg
tasks.py
# Package building
*.egg-info/
@@ -26,5 +27,5 @@ log/
.ipynb_checkpoints/
tests/cache/
tests/output/
tests/resources/private
tests/resources/private/
tmp/
+10 -11
View File
@@ -1,3 +1,5 @@
sudo: required
dist: trusty
language: python
cache: pip
@@ -5,33 +7,30 @@ cache: pip
cache:
directories:
- $HOME/.ccache
- tarballs
- 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:evl.ms/precise -y # for Ghostscript 9.15
- sudo add-apt-repository ppa:lyrasis/precise-backports -y # for Tesseract 3.03
- sudo add-apt-repository ppa:b-eltzner/qpdfview-exp -y # for QPDF 5
- sudo add-apt-repository ppa:itachi-san/ffmpeg -y # for libav 11.2 (for unpaper)
- sudo 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 gcc libavformat-dev libavcodec-dev libavutil-dev automake make pkg-config xsltproc libffi-dev
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
# pip
- pip install --upgrade pip
# Download, make and install unpaper (using ccache)
- mkdir -p tarballs
- "[ -f tarballs/unpaper-6.1.tar.xz ] || wget -q https://www.flameeyes.eu/files/unpaper-6.1.tar.xz -O tarballs/unpaper-6.1.tar.xz"
- tar -xvf tarballs/unpaper-6.1.tar.xz
- export PATH="/usr/lib/ccache:$PATH"
- pushd unpaper-6.1 && ./configure --prefix=/usr && make -j && sudo make install && popd
- mkdir -p packages
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
- sudo dpkg -i packages/unpaper_6.1-1.deb
install:
- pip install -r requirements.txt
+9 -5
View File
@@ -23,12 +23,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get install -y --no-install-recommends \
unpaper \
libopenjpeg-dev \
ghostscript \
qpdf \
poppler-utils
RUN apt-get install -y --no-install-recommends \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
@@ -37,6 +33,14 @@ RUN apt-get install -qy --no-install-recommends \
libpython3-dev \
gcc
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghostscript/sid
# Enforce UTF-8
# Borrowed from https://index.docker.io/u/crosbymichael/python/
RUN dpkg-reconfigure locales && \
+1 -1
View File
@@ -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
+41 -2
View File
@@ -1,4 +1,43 @@
recursive-exclude tests/output *
# requirements
include requirements.txt
include test_requirements.txt
include share/*
include dev_requirements.txt
# git
include .git_archival.txt
# docker
include Dockerfile
include Dockerfile.polyglot
include .dockerignore
# tests
include pytest.ini
recursive-include tests *.jpg
recursive-include tests *.pdf
recursive-include tests *.py
recursive-include tests *.rst
recursive-include tests *.txt
recursive-exclude tests/output *
recursive-exclude tests/output_pageinfo *
recursive-exclude tests/resources/private *
recursive-exclude tests/cache *
# documentation
include *.rst
include pipeline.svg
recursive-exclude .github *
# 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
+2
View File
@@ -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 "$@"
+157 -48
View File
@@ -8,7 +8,7 @@ Main features
-------------
- Generates a searchable
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`__ file from a regular PDF
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`_ file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without rendering vector information
@@ -18,11 +18,11 @@ Main features
- Provides debug mode to enable easy verification of the OCR results
- Processes pages in parallel when more than one CPU core is
available
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`__ engine
- Supports the `39 languages <https://code.google.com/p/tesseract-ocr/downloads/list>`__ recognized by Tesseract
- 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
----------
@@ -31,9 +31,9 @@ I searched the web for a free command line tool to OCR PDF files on
Linux/UNIX: I found many, but none of them were really satisfying.
- 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)
@@ -46,22 +46,45 @@ Installation
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
You can install it to a Python virtual environment or system-wide.
These steps describe how to install OCRmyPDF on your system.
- `Installing on Debian and Ubuntu`_ (Debian stretch and Ubuntu 16.10 or later)
- `Installing the Docker image`_
- `Installing on Mac OS X`_
- `Installing on Ubuntu 14.04 LTS`_
- Installing and running on `Windows`_ using the Docker image
If you prefer to install from source or install OCRmyPDF to a Python virtual environment, see steps for `Installing HEAD revision from sources`_.
.. _Windows: `Installing on Windows`_
Installing on Debian and Ubuntu
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
``apt-get install ocrmypdf``.
.. _Docker:
Installing the Docker image
~~~~~~~~~~~~~~~~~~~~~~~~~~~
For many users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
If you have `Docker <https://docs.docker.com/>`__ installed on your system, you can install
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install
a Docker image of the latest release.
Follow the Docker installation instructions for your platform. If you can run this command
successfully, your system is ready to download and execute the image::
successfully, your system is ready to download and execute the image:
.. code-block:: bash
docker run hello-world
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs::
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
.. code-block:: bash
# Optional step for Mac OS X users
docker-machine stop "yourVM"
@@ -70,29 +93,41 @@ OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine i
eval $(docker-machine env "yourVM")
Assuming you have a Docker engine running somewhere, you can run these commands to download
the image::
the image:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
Then tag it to give a more convenient name, just ocrmypdf::
Then tag it to give a more convenient name, just ocrmypdf:
.. code-block:: bash
docker tag jbarlow83/ocrmypdf ocrmypdf
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__::
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
.. code-block:: bash
# Alternative step: If you need all language packs
docker pull jbarlow83/ocrmypdf-polyglot
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
You can then run ocrmypdf using the command::
You can then run ocrmypdf using the command:
.. code-block:: bash
docker run ocrmypdf --help
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`__, such as this in this template::
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, such as this in this template:
.. code-block:: bash
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``::
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
.. code-block:: bash
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
@@ -104,84 +139,135 @@ Installing on Mac OS X
These instructions probably work on all Mac OS X versions later than 10.7 (Lion). OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
If it's not already present, `install Homebrew <http://brew.sh/>`__.
If it's not already present, `install Homebrew <http://brew.sh/>`_.
Update Homebrew::
Update Homebrew:
.. code-block:: bash
brew update
Install or upgrade the required Homebrew packages, if any are missing::
Install or upgrade the required Homebrew packages, if any are missing:
brew install libpng openjpeg jbig2dec # image libraries
.. code-block:: bash
brew install libpng openjpeg jbig2dec libtiff # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2
brew install leptonica
brew install tesseract
brew install libxml2 libffi leptonica
brew install unpaper # optional
Update the homebrew pip and install Pillow::
Install the required Tesseract OCR engine with the language packs you plan to use:
.. code-block:: bash
brew install tesseract # Option 1: for English
.. 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::
You can then install OCRmyPDF from PyPI:
.. code-block:: bash
pip3 install ocrmypdf
The command line program should now be available::
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 other options, because of certain bugs in Python package installation.
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
Update apt-get::
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
.. code-block:: bash
sudo add-apt-repository ppa:vshn/ghostscript -y
sudo add-apt-repository ppa:heyarje/libav-11 -y
Update apt-get:
.. code-block:: bash
sudo apt-get update
sudo apt-get upgrade
Install system dependencies::
Install system dependencies:
.. code-block:: bash
sudo apt-get install \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
libavformat56 libavcodec56 libavutil54 \
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)::
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 system Python from modified, you can
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`__::
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
.. code-block:: bash
sudo apt-get install python3-venv
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
python3 -m venv --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.
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper`` and is not motivated to figure how to set up a Ubuntu PPA to distribute it. You can create a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
sudo dpkg -i unpaper_6.1-1.deb
Installing on Windows
~~~~~~~~~~~~~~~~~~~~~
Direct installation on Windows is not possible. Install the Docker container as described above.
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -189,24 +275,32 @@ Installing HEAD revision from sources
If you have ``git`` and ``python3.4`` or ``python3.5`` installed, you can install from source. When the ``pip`` installer runs,
it will alert you if dependencies are missing.
To install the HEAD revision from sources in the current Python 3 environment::
To install the HEAD revision from sources in the current Python 3 environment:
.. code-block:: bash
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__, allowing customization of OCRmyPDF, use the ``-e`` flag::
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
On certain Linux distributions such as Ubuntu, you may need to use
run the install command as superuser::
run the install command as superuser:
.. code-block:: bash
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
Note that this will alter your system's Python distribution. If you prefer
to not install as superuser, you can install the package in a Python virtual environment::
to not install as superuser, you can install the package in a Python virtual environment:
.. code-block:: bash
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
pyvenv venv
python3 -m venv
source venv/bin/activate
cd OCRmyPDF
pip3 install .
@@ -214,7 +308,9 @@ to not install as superuser, you can install the package in a Python virtual env
However, ``ocrmypdf`` will only be accessible on the system PATH after
you activate the virtual environment.
To run the program::
To run the program:
.. code-block:: bash
ocrmypdf --help
@@ -227,10 +323,15 @@ Languages
---------
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
you can often find packages that provide language packs::
you can often find packages that provide language packs:
.. code-block:: bash
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
sudo apt-get install tesseract-ocr-chi-sim
sudo apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
languages can be requested.
@@ -238,9 +339,17 @@ languages can be requested.
Support
-------
In case you detect an issue, please:
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
- Check if your issue is already known
.. code-block:: bash
ocrmypdf --help
The `Wiki <https://github.com/jbarlow83/OCRmyPDF/wiki>`_ page also contains some tips and suggests.
If you detect an issue, please:
- Check whether your issue is already known
- If no problem report exists on github, please create one here:
https://github.com/jbarlow83/OCRmyPDF/issues
- Describe your problem thoroughly
@@ -252,11 +361,11 @@ In case you detect an issue, please:
Press & Media
-------------
- `c't 1-2014, page 59 <http://www.heise.de/ct/inhalt/2014/1/58/>`__:
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
magazine c't
- `heise Open Source, 09/2014: Texterkennung mit
OCRmyPDF <http://www.heise.de/-2356670>`__
OCRmyPDF <http://heise.de/-2356670>`_
Disclaimer
----------
+150 -361
View File
@@ -1,9 +1,155 @@
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.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:
@@ -273,363 +419,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>`_.
+4
View File
@@ -0,0 +1,4 @@
check-manifest>=0.33
setuptools-scm>=1.11.1
twine>=1.8.1
coverage>=4.2
+8 -1
View File
@@ -1,5 +1,6 @@
from enum import IntEnum
import os
from collections.abc import Iterable
class ExitCode(IntEnum):
@@ -7,11 +8,13 @@ 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):
@@ -21,3 +24,7 @@ def get_program(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)
+1472
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+2 -1
View File
@@ -5,6 +5,7 @@ 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,
@@ -13,6 +14,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
args_gs = [
get_program('gs'),
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=%s' % raster_device,
@@ -52,7 +54,6 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
"-dJPEGQ=95",
"-dPDFA=2",
"-sPDFACompatibilityPolicy=2",
"-sOutputICCProfile=srgb.icc",
"-sOutputFile=" + gs_pdf.name,
]
args_gs.extend(pdf_pages)
+228 -28
View File
@@ -17,6 +17,7 @@ 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'))
@@ -83,6 +84,13 @@ class LeptonicaIOError(LeptonicaError):
pass
class RemoveColormap(Enum):
to_binary = 0
to_grayscale = 1
to_full_color = 2
based_on_src = 3
class Pix:
"""Wrapper around leptonica's PIX object.
@@ -98,27 +106,31 @@ class Pix:
in a threadsafe manner if a Python threading.Lock protects the data.
"""
def __init__(self, cpix):
self.cpix = ffi.gc(cpix, Pix._pix_destroy)
def __init__(self, pix):
self._pix = ffi.gc(pix, Pix._pix_destroy)
def __repr__(self):
if self.cpix:
if self._pix:
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
return s.format(self.cpix.w, self.cpix.h, self.cpix.d,
int(ffi.cast("intptr_t", self.cpix)))
return s.format(self._pix.w, self._pix.h, self._pix.d,
int(ffi.cast("intptr_t", self._pix)))
else:
return "<leptonica.Pix image NULL>"
def __getstate__(self):
data = ffi.new('l_uint32 *[]', 1)
size = ffi.new('size_t *', 0)
data = ffi.new('l_uint32 **')
size = ffi.new('size_t *')
err = lept.pixSerializeToMemory(self.cpix, data, size)
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)
@@ -126,16 +138,41 @@ class Pix:
cdata_bytes = ffi.new('char[]', state['data'])
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
self.cpix = lept.pixDeserializeFromMemory(
pix = lept.pixDeserializeFromMemory(
cdata_uint32, len(state['data']))
Pix.__init__(self, pix)
@property
def width(self):
return self.cpix.w
return self._pix.w
@property
def height(self):
return self.cpix.h
return self._pix.h
@property
def depth(self):
return self._pix.d
@property
def size(self):
return (self._pix.w, self._pix.h)
@property
def info(self):
return {'dpi': (self._pix.xres, self._pix.yres)}
@property
def mode(self):
"Return mode like PIL.Image"
if self.depth == 1:
return '1'
elif self.depth >= 16:
return 'RGB'
elif not self._pix.colormap:
return 'L'
else:
return 'P'
@classmethod
def read(cls, filename):
@@ -158,7 +195,29 @@ class Pix:
with LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
filename.encode(sys.getfilesystemencoding()),
self.cpix, jpeg_quality, jpeg_progressive)
self._pix, jpeg_quality, jpeg_progressive)
def topil(self):
"Returns a PIL.Image version of this Pix"
from PIL import Image
with LeptonicaErrorTrap():
pix_swapped = Pix(lept.pixEndianByteSwapNew(self._pix))
size = (pix_swapped._pix.wpl * 4, pix_swapped._pix.h)
buf = ffi.buffer(pix_swapped._pix.data, size[0] * size[1])
im_raw = Image.frombytes(self.mode, size, buf, 'raw')
# Leptonica stores images in 32-bit words
# Need to crop the any trailing amount
box = (0, 0, self.width, self.height)
im = im_raw.crop(box)
return im
def show(self):
return self.topil().show()
def deskew(self, reduction_factor=0):
"""Returns the deskewed pix object.
@@ -170,16 +229,21 @@ class Pix:
for skew angle
"""
with LeptonicaErrorTrap():
return Pix(lept.pixDeskew(self.cpix, reduction_factor))
return Pix(lept.pixDeskew(self._pix, reduction_factor))
def scale(self, scalex, scaley):
"Returns the pix object rescaled according to the proportions given."
with LeptonicaErrorTrap():
return Pix(lept.pixScale(self.cpix, scalex, scaley))
return Pix(lept.pixScale(self._pix, scalex, scaley))
def rotate180(self):
with LeptonicaErrorTrap():
return Pix(lept.pixRotate180(ffi.NULL, self.cpix))
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
def rotate_orth(self, quads):
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
with LeptonicaErrorTrap():
return Pix(lept.pixRotateOrth(self._pix, quads))
def find_skew(self):
"""Returns a tuple (deskew angle in degrees, confidence value).
@@ -189,27 +253,126 @@ class Pix:
with LeptonicaErrorTrap():
angle = ffi.new('float *', 0.0)
confidence = ffi.new('float *', 0.0)
result = lept.pixFindSkew(self.cpix, angle, confidence)
result = lept.pixFindSkew(self._pix, angle, confidence)
if result == 0:
return (angle[0], confidence[0])
else:
return (None, None)
def convert_rgb_to_luminance(self):
with LeptonicaErrorTrap():
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
if gray_pix:
return Pix(gray_pix)
return None
def remove_colormap(self, removal_type):
"""Remove a palette
removal_type - RemovalColormap()
"""
with LeptonicaErrorTrap():
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
def otsu_adaptive_threshold(
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
p_pix = ffi.new('PIX **')
result = lept.pixOtsuAdaptiveThreshold(
self._pix,
sx, sy,
smoothx, smoothy,
scorefract,
ffi.NULL,
p_pix)
if result == 0:
return Pix(p_pix[0])
else:
return None
def otsu_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
bgval=255, kernel_size=(2, 2), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
if mask is None:
mask = ffi.NULL
if isinstance(mask, Pix):
mask = mask._pix
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
self._pix,
mask,
sx, sy,
thresh, mincount, bgval,
smoothx, smoothy,
scorefract,
ffi.NULL
)
if thresh_pix == ffi.NULL:
return None
return Pix(thresh_pix)
def crop_to_foreground(
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
showmorph=0, display=0, pdfdir=ffi.NULL):
with LeptonicaErrorTrap():
cropbox = Box(lept.pixFindPageForeground(
self._pix,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir))
print(repr(cropbox))
cropped_pix = lept.pixClipRectangle(
self._pix,
cropbox._box,
ffi.NULL)
return Pix(cropped_pix)
def clean_background_to_white(
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
with LeptonicaErrorTrap():
return Pix(lept.pixCleanBackgroundToWhite(
self._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white))
@staticmethod
@lru_cache(maxsize=1)
def make_pixel_sum_tab8():
return lept.makePixelSumTab8()
@staticmethod
def correlation_binary(pix1, pix2):
if get_leptonica_version() < 'leptonica-1.72':
# Older versions of Leptonica (pre-1.72) have a buggy
# implementation of pixCorrelationBinary that overflows on larger
# images.
pix1_count = ffi.new('l_int32 *', 0)
pix2_count = ffi.new('l_int32 *', 0)
pixn_count = ffi.new('l_int32 *', 0)
tab8 = lept.makePixelSumTab8() # Small memory leak on each call
# 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.cpix, pix1_count, tab8)
lept.pixCountPixels(pix2.cpix, pix2_count, tab8)
pixn = Pix(lept.pixAnd(ffi.NULL, pix1.cpix, pix2.cpix))
lept.pixCountPixels(pixn.cpix, pixn_count, 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.
@@ -220,7 +383,7 @@ class Pix:
return correlation
else:
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1.cpix, pix2.cpix,
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
@@ -228,11 +391,48 @@ class Pix:
@staticmethod
def _pix_destroy(pix):
ptr_to_pix = ffi.new('PIX **', pix)
lept.pixDestroy(ptr_to_pix)
p_pix = ffi.new('PIX **', pix)
lept.pixDestroy(p_pix)
# print('pix destroy ' + repr(pix))
class Box:
"""Wrapper around Leptonica's BOX objects.
See class Pix for notes about reference counting.
"""
def __init__(self, box):
self._box = ffi.gc(box, Box._box_destroy)
def __repr__(self):
if self._box:
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
self.x, self.y, self.w, self.h)
return '<leptonica.Box NULL>'
@property
def x(self):
return self._box.x
@property
def y(self):
return self._box.y
@property
def w(self):
return self._box.w
@property
def h(self):
return self._box.h
@staticmethod
def _box_destroy(box):
p_box = ffi.new('BOX **', box)
lept.boxDestroy(p_box)
@lru_cache(maxsize=1)
def get_leptonica_version():
"""Get Leptonica version string.
+72
View File
@@ -42,6 +42,18 @@ struct PixColormap
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("""
@@ -50,10 +62,17 @@ PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
void pixDestroy ( PIX **ppix );
PIX *
pixEndianByteSwapNew(PIX *pixs);
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
char * getLeptonicaVersion ( );
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
PIX *pixRotate180(PIX *pixd, PIX *pixs);
PIX *
pixRotateOrth(PIX *pixs,
l_int32 quads);
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
@@ -62,6 +81,59 @@ l_int32 * makePixelSumTab8 ( void );
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
PIX * pixConvertRGBToLuminance(PIX *pixs);
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
l_int32
pixOtsuAdaptiveThreshold(PIX *pixs,
l_int32 sx,
l_int32 sy,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
PIX **ppixth,
PIX **ppixd);
PIX *
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
PIX *pixim,
l_int32 sx,
l_int32 sy,
l_int32 thresh,
l_int32 mincount,
l_int32 bgval,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
l_int32 *pthresh);
PIX *
pixCleanBackgroundToWhite(PIX *pixs,
PIX *pixim,
PIX *pixg,
l_float32 gamma,
l_int32 blackval,
l_int32 whiteval);
BOX *
pixFindPageForeground(PIX *pixs,
l_int32 threshold,
l_int32 mindist,
l_int32 erasedist,
l_int32 pagenum,
l_int32 showmorph,
l_int32 display,
const char *pdfdir);
PIX *
pixClipRectangle(PIX *pixs,
BOX *box,
BOX **pboxc);
void
boxDestroy(BOX **pbox);
void lept_free(void *ptr);
""")
Executable → Regular
+4 -1059
View File
File diff suppressed because it is too large Load Diff
+269 -37
View File
@@ -3,10 +3,13 @@
from subprocess import Popen, PIPE
from decimal import Decimal, getcontext
from math import hypot
import re
import sys
import PyPDF2 as pypdf
from collections import namedtuple
matrix_mult = pypdf.pdf.utils.matrixMultiply
FRIENDLY_COLORSPACE = {
'/DeviceGray': 'gray',
@@ -19,7 +22,11 @@ FRIENDLY_COLORSPACE = {
'/Indexed': 'index',
'/Separation': 'sep',
'/DeviceN': 'devn',
'/Pattern': '-'
'/Pattern': '-',
'/G': 'gray', # Abbreviations permitted in inline images
'/RGB': 'rgb',
'/CMYK': 'cmyk',
'/I': 'index',
}
FRIENDLY_ENCODING = {
@@ -27,6 +34,13 @@ FRIENDLY_ENCODING = {
'/DCTDecode': 'jpeg',
'/JPXDecode': 'jpx',
'/JBIG2Decode': 'jbig2',
'/CCF': 'ccitt', # Abbreviations permitted in inline images
'/DCT': 'jpeg',
'/AHx': 'asciihex',
'/A85': 'ascii85',
'/LZW': 'lzw',
'/Fl': 'flate',
'/RL': 'runlength'
}
FRIENDLY_COMP = {
@@ -38,50 +52,216 @@ FRIENDLY_COMP = {
}
def _page_has_inline_images(page):
# PDF always uses \r\n for separator regardless of platform
# Really basic heuristic that might trigger the odd false positive
# This is only finds the first image and is not quite spec compliant
try:
contents = page.getContents()
data = contents.getData()
except AttributeError:
# If we can't access the contents or data (empty page?) then there
# are no inline images
return False
def _matrix_from_shorthand(shorthand):
"""Convert from PDF matrix shorthand to full matrix
begin_image, image_data, end_image = False, False, False
for data in re.split(b'\s+', data):
if data == b'BI':
begin_image = True
elif data == b'ID':
image_data = True
elif data == b'EI':
end_image = True
if all((begin_image, image_data, end_image)):
return True
return False
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
since the last column is always (0, 0, 1).
"""
a, b, c, d, e, f = map(float, shorthand)
return ((a, b, 0),
(c, d, 0),
(e, f, 1))
def _find_page_images(page, pageinfo):
def _shorthand_from_matrix(matrix):
"""Convert from transformation matrix to PDF shorthand."""
a, b = matrix[0][0], matrix[0][1]
c, d = matrix[1][0], matrix[1][1]
e, f = matrix[2][0], matrix[2][1]
return tuple(map(float, (a, b, c, d, e, f)))
RasterSettings = namedtuple('RasterSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['settings', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
def _interpret_contents(contentstream):
"""Interpret the PDF content stream
The stack represents the state of the PDF graphics stack. We are only
interested in the current transformation matrix (CTM) so we only track
this object; a full implementation would need to track many other items.
The CTM is initialized to the mapping from user space to device space.
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
to the transformation to device space. For example if set to
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
image there should be a 'cm' that sets up an image coordinate system
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
page.
PDF units suit our needs so we initialize ctm to the identity matrix.
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
"""
operations = contentstream.operations
stack = []
ctm = _matrix_from_shorthand((1, 0, 0, 1, 0, 0))
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,12 +278,48 @@ 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:
# 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 _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()
@@ -141,12 +357,14 @@ def _pdf_get_pageinfo(infile, pageno: int):
pageinfo['width_inches'] = width_pt / Decimal(72.0)
pageinfo['height_inches'] = height_pt / Decimal(72.0)
pageinfo['images'] = [im for im in _find_page_images(page, pageinfo)]
try:
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
except AttributeError as e:
return pageinfo
# Look for inline images
if _page_has_inline_images(page):
raise NotImplementedError(
"Warning: input PDF contains inline images - not supported")
contentsinfo = _interpret_contents(contentstream)
pageinfo['images'] = [im for im in _find_page_images(
page, pageinfo, contentsinfo)]
if pageinfo['images']:
xres = max(image['dpi_w'] for image in pageinfo['images'])
@@ -164,3 +382,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()
+63 -40
View File
@@ -5,10 +5,14 @@
from __future__ import print_function, absolute_import, division
from string import Template
from subprocess import Popen, PIPE
import os
import codecs
from . import get_program
import pkg_resources
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
@@ -71,8 +75,14 @@ def encode_text_string(s: str) -> str:
Postscript file to be completely ASCII and no escaping of Postscript
characters is necessary.
'''
# Sometimes lazy C programmer leave their NULs at the end of strings
# tests/resources/aspect.pdf is one example (created by ImageMagick)
s = s.replace('\x00', '')
if s == '':
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()
@@ -85,52 +95,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', ''),
creator=pdfmark_utf16.get('creator', ''),
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([get_program('gs'), '--help'], close_fds=True,
universal_newlines=True,
stdout=PIPE, stderr=PIPE)
out, _ = p_gs.communicate()
lines = out.splitlines()
def search_paths(lines):
seeking = True
for line in lines:
if seeking:
if line.startswith('Search path'):
seeking = False
continue
else:
if line.strip().startswith('/'):
yield from (
path.strip() for path in line.split(':')
if path.strip() != '')
for root in search_paths(lines):
path = os.path.realpath(os.path.join(root, '../iccprofiles'))
if os.path.exists(path):
return path
raise FileNotFoundError("Could not find Ghostscript's iccprofiles")
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
if icc == 'sRGB':
icc_profile = os.path.join(_get_postscript_icc_path(), 'srgb.icc')
icc_profile = SRGB_ICC_PROFILE
else:
raise NotImplementedError("Only supporting sRGB")
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
+33 -1
View File
@@ -2,12 +2,32 @@
# © 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'),
@@ -20,7 +40,7 @@ def check(input_file, log):
except CalledProcessError as e:
if e.returncode == 2:
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
input_file))
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
@@ -85,3 +105,15 @@ def split_pages(input_file, work_folder, npages):
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
]
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)
+3 -6
View File
@@ -10,11 +10,7 @@ from . import ExitCode, get_program, page_number
from collections import namedtuple
from subprocess import Popen, PIPE, CalledProcessError, \
TimeoutExpired, check_output, STDOUT
try:
from subprocess import DEVNULL
except ImportError:
DEVNULL = open(os.devnull, 'wb')
TimeoutExpired, check_output, STDOUT, DEVNULL
OrientationConfidence = namedtuple(
@@ -98,7 +94,8 @@ def get_orientation(input_file, language: list, timeout: float, log):
return OrientationConfidence(angle=0, confidence=0.0)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if 'Image too large' in e.output:
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:
+15 -14
View File
@@ -3,7 +3,7 @@
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
from subprocess import Popen, PIPE
from subprocess import CalledProcessError, STDOUT, check_output, check_call
from tempfile import NamedTemporaryFile
import sys
import os
@@ -17,10 +17,9 @@ def version():
get_program('unpaper'),
'--version'
]
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=PIPE)
version, _ = p_unpaper.communicate(timeout=5)
version = check_output(
args_unpaper, close_fds=True, universal_newlines=True,
stderr=STDOUT, timeout=5)
return version.strip()
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
os.unlink(output_pnm.name)
args_unpaper.extend([input_pnm.name, output_pnm.name])
p_unpaper = Popen(
args_unpaper, close_fds=True,
universal_newlines=True, stdout=PIPE, stderr=PIPE
)
out, err = p_unpaper.communicate()
log.debug(out)
log.debug(err)
Image.open(output_pnm.name).save(output_file)
try:
stdout = check_output(
args_unpaper, close_fds=True,
universal_newlines=True, stderr=STDOUT,
)
except CalledProcessError as e:
log.debug(e.output)
raise e from e
else:
log.debug(stdout)
Image.open(output_pnm.name).save(output_file)
def deskew(input_file, output_file, dpi, log):
+263 -222
View File
@@ -4,294 +4,335 @@
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: Pipeline: Pages: 1 -->
<svg width="1452pt" height="824pt"
viewBox="0.00 0.00 1452.00 824.08" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 820.083)">
<svg width="1444pt" height="973pt"
viewBox="0.00 0.00 1444.00 973.18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 969.181)">
<title>Pipeline:</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-820.083 1448,-820.083 1448,4 -4,4"/>
<polygon fill="white" stroke="none" points="-4,4 -4,-969.181 1440,-969.181 1440,4 -4,4"/>
<g id="clust1" class="cluster"><title>clustertasks</title>
<polygon fill="none" stroke="black" points="8,-8 8,-808.083 1436,-808.083 1436,-8 8,-8"/>
<text text-anchor="middle" x="722" y="-780.083" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
<polygon fill="none" stroke="black" points="8,-8 8,-957.181 1428,-957.181 1428,-8 8,-8"/>
<text text-anchor="middle" x="718" y="-929.181" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
</g>
<!-- t0 -->
<g id="node1" class="node"><title>t0</title>
<polygon fill="#efa03b" stroke="black" points="1256.54,-762.083 1033.46,-762.083 1029.46,-758.083 1029.46,-726.083 1252.54,-726.083 1256.54,-730.083 1256.54,-762.083"/>
<polyline fill="none" stroke="black" points="1252.54,-758.083 1029.46,-758.083 "/>
<polyline fill="none" stroke="black" points="1252.54,-758.083 1252.54,-726.083 "/>
<polyline fill="none" stroke="black" points="1252.54,-758.083 1256.54,-762.083 "/>
<text text-anchor="middle" x="1143" y="-738.083" font-family="Times,serif" font-size="20.00">repair_pdf</text>
<polygon fill="#efa03b" stroke="black" points="1219.77,-911.181 1034.23,-911.181 1030.23,-907.181 1030.23,-875.181 1215.77,-875.181 1219.77,-879.181 1219.77,-911.181"/>
<polyline fill="none" stroke="black" points="1215.77,-907.181 1030.23,-907.181 "/>
<polyline fill="none" stroke="black" points="1215.77,-907.181 1215.77,-875.181 "/>
<polyline fill="none" stroke="black" points="1215.77,-907.181 1219.77,-911.181 "/>
<text text-anchor="middle" x="1125" y="-887.181" font-family="Times,serif" font-size="20.00">triage</text>
</g>
<!-- t1 -->
<g id="node2" class="node"><title>t1</title>
<polygon fill="#efa03b" stroke="black" points="1234.11,-683.155 1030,-700.057 825.888,-683.155 826.078,-655.806 1233.92,-655.806 1234.11,-683.155"/>
<polygon fill="none" stroke="black" points="1238.13,-686.834 1030,-704.069 821.866,-686.834 822.11,-651.808 1237.89,-651.808 1238.13,-686.834"/>
<text text-anchor="middle" x="1030" y="-669.596" font-family="Times,serif" font-size="20.00">split_pages</text>
<polygon fill="#efa03b" stroke="black" points="1238.54,-853.181 1015.46,-853.181 1011.46,-849.181 1011.46,-817.181 1234.54,-817.181 1238.54,-821.181 1238.54,-853.181"/>
<polyline fill="none" stroke="black" points="1234.54,-849.181 1011.46,-849.181 "/>
<polyline fill="none" stroke="black" points="1234.54,-849.181 1234.54,-817.181 "/>
<polyline fill="none" stroke="black" points="1234.54,-849.181 1238.54,-853.181 "/>
<text text-anchor="middle" x="1125" y="-829.181" font-family="Times,serif" font-size="20.00">repair_pdf</text>
</g>
<!-- t0&#45;&gt;t1 -->
<g id="edge1" class="edge"><title>t0&#45;&gt;t1</title>
<path fill="none" stroke="#0044a0" d="M1113.9,-725.961C1103.58,-719.89 1091.66,-712.873 1080.09,-706.069"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1081.75,-702.982 1071.35,-700.927 1078.2,-709.015 1081.75,-702.982"/>
</g>
<!-- t14 -->
<g id="node16" class="node"><title>t14</title>
<polygon fill="#efa03b" stroke="black" points="1428.08,-625.109 1089.92,-625.109 1085.92,-621.109 1085.92,-589.109 1424.08,-589.109 1428.08,-593.109 1428.08,-625.109"/>
<polyline fill="none" stroke="black" points="1424.08,-621.109 1085.92,-621.109 "/>
<polyline fill="none" stroke="black" points="1424.08,-621.109 1424.08,-589.109 "/>
<polyline fill="none" stroke="black" points="1424.08,-621.109 1428.08,-625.109 "/>
<text text-anchor="middle" x="1257" y="-601.109" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
</g>
<!-- t0&#45;&gt;t14 -->
<g id="edge22" class="edge"><title>t0&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1219.32,-726.004C1229.98,-720.592 1239.75,-713.466 1247,-704.083 1261.92,-684.78 1263.1,-656.385 1261.35,-635.486"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1264.8,-634.881 1260.22,-625.328 1257.84,-635.653 1264.8,-634.881"/>
<path fill="none" stroke="#0044a0" d="M1125,-875.075C1125,-871.384 1125,-867.394 1125,-863.424"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-863.182 1125,-853.182 1121.5,-863.182 1128.5,-863.182"/>
</g>
<!-- t2 -->
<g id="node3" class="node"><title>t2</title>
<polygon fill="#efa03b" stroke="black" points="1029.88,-625.109 748.117,-625.109 744.117,-621.109 744.117,-589.109 1025.88,-589.109 1029.88,-593.109 1029.88,-625.109"/>
<polyline fill="none" stroke="black" points="1025.88,-621.109 744.117,-621.109 "/>
<polyline fill="none" stroke="black" points="1025.88,-621.109 1025.88,-589.109 "/>
<polyline fill="none" stroke="black" points="1025.88,-621.109 1029.88,-625.109 "/>
<text text-anchor="middle" x="887" y="-601.109" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
<polygon fill="#efa03b" stroke="black" points="1329.11,-774.253 1125,-791.156 920.888,-774.253 921.078,-746.905 1328.92,-746.905 1329.11,-774.253"/>
<polygon fill="none" stroke="black" points="1333.13,-777.932 1125,-795.167 916.866,-777.932 917.11,-742.906 1332.89,-742.906 1333.13,-777.932"/>
<text text-anchor="middle" x="1125" y="-760.694" font-family="Times,serif" font-size="20.00">split_pages</text>
</g>
<!-- t1&#45;&gt;t2 -->
<g id="edge2" class="edge"><title>t1&#45;&gt;t2</title>
<path fill="none" stroke="#0044a0" d="M980.788,-651.715C965.266,-644.498 948.187,-636.558 932.905,-629.452"/>
<polygon fill="#0044a0" stroke="#0044a0" points="934.195,-626.193 923.652,-625.15 931.244,-632.54 934.195,-626.193"/>
<path fill="none" stroke="#0044a0" d="M1125,-817.059C1125,-813.44 1125,-809.484 1125,-805.438"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-805.334 1125,-795.334 1121.5,-805.334 1128.5,-805.334"/>
</g>
<!-- t15 -->
<g id="node17" class="node"><title>t15</title>
<polygon fill="#efa03b" stroke="black" points="632.082,-716.208 293.918,-716.208 289.918,-712.208 289.918,-680.208 628.082,-680.208 632.082,-684.208 632.082,-716.208"/>
<polyline fill="none" stroke="black" points="628.082,-712.208 289.918,-712.208 "/>
<polyline fill="none" stroke="black" points="628.082,-712.208 628.082,-680.208 "/>
<polyline fill="none" stroke="black" points="628.082,-712.208 632.082,-716.208 "/>
<text text-anchor="middle" x="461" y="-692.208" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
</g>
<!-- t1&#45;&gt;t15 -->
<g id="edge23" class="edge"><title>t1&#45;&gt;t15</title>
<path fill="none" stroke="#0044a0" d="M1026.3,-817.165C989.157,-810.572 946.587,-802.788 908,-795.181 781.899,-770.319 636.479,-738.51 546.633,-718.49"/>
<polygon fill="#0044a0" stroke="#0044a0" points="547.289,-715.05 536.767,-716.289 545.765,-721.882 547.289,-715.05"/>
</g>
<!-- t18 -->
<g id="node19" class="node"><title>t18</title>
<polygon fill="#efa03b" stroke="black" points="1025.79,-156.452 1221,-129.134 1416.21,-156.452 1416.03,-200.654 1025.97,-200.654 1025.79,-156.452"/>
<polygon fill="none" stroke="black" points="1021.78,-152.971 1221,-125.091 1420.22,-152.971 1420.01,-204.657 1021.99,-204.657 1021.78,-152.971"/>
<text text-anchor="middle" x="1221" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_qpdf</text>
</g>
<!-- t1&#45;&gt;t18 -->
<g id="edge33" class="edge"><title>t1&#45;&gt;t18</title>
<path fill="none" stroke="#0044a0" d="M1238.67,-826.639C1321.4,-814.236 1419,-782.049 1419,-699.208 1419,-699.208 1419,-699.208 1419,-319.208 1419,-280.294 1424,-263.161 1398,-234.208 1389.64,-224.894 1379.82,-216.923 1369.17,-210.101"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1370.83,-207.019 1360.46,-204.872 1367.23,-213.02 1370.83,-207.019"/>
</g>
<!-- t3 -->
<g id="node4" class="node"><title>t3</title>
<polygon fill="#efa03b" stroke="black" points="1128.15,-567.109 893.852,-567.109 889.852,-563.109 889.852,-531.109 1124.15,-531.109 1128.15,-535.109 1128.15,-567.109"/>
<polyline fill="none" stroke="black" points="1124.15,-563.109 889.852,-563.109 "/>
<polyline fill="none" stroke="black" points="1124.15,-563.109 1124.15,-531.109 "/>
<polyline fill="none" stroke="black" points="1124.15,-563.109 1128.15,-567.109 "/>
<text text-anchor="middle" x="1009" y="-543.109" font-family="Times,serif" font-size="20.00">orient_page</text>
</g>
<!-- t1&#45;&gt;t3 -->
<g id="edge4" class="edge"><title>t1&#45;&gt;t3</title>
<path fill="none" stroke="#0044a0" d="M1037.63,-651.508C1042.21,-633.96 1045.92,-609.455 1039,-589.109 1037.4,-584.404 1034.98,-579.822 1032.19,-575.552"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1034.85,-573.258 1026.1,-567.282 1029.21,-577.409 1034.85,-573.258"/>
<polygon fill="#efa03b" stroke="black" points="1186.88,-716.208 905.117,-716.208 901.117,-712.208 901.117,-680.208 1182.88,-680.208 1186.88,-684.208 1186.88,-716.208"/>
<polyline fill="none" stroke="black" points="1182.88,-712.208 901.117,-712.208 "/>
<polyline fill="none" stroke="black" points="1182.88,-712.208 1182.88,-680.208 "/>
<polyline fill="none" stroke="black" points="1182.88,-712.208 1186.88,-716.208 "/>
<text text-anchor="middle" x="1044" y="-692.208" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
</g>
<!-- t2&#45;&gt;t3 -->
<g id="edge3" class="edge"><title>t2&#45;&gt;t3</title>
<path fill="none" stroke="#0044a0" d="M924.267,-589.003C936.328,-583.467 949.857,-577.257 962.502,-571.453"/>
<polygon fill="#0044a0" stroke="#0044a0" points="964.333,-574.463 971.961,-567.111 961.413,-568.102 964.333,-574.463"/>
<path fill="none" stroke="#0044a0" d="M1097.12,-742.813C1089.24,-736.345 1080.66,-729.296 1072.73,-722.788"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1074.71,-719.888 1064.76,-716.249 1070.27,-725.299 1074.71,-719.888"/>
</g>
<!-- t4 -->
<g id="node5" class="node"><title>t4</title>
<polygon fill="#efa03b" stroke="black" points="1027.3,-509.109 676.701,-509.109 672.701,-505.109 672.701,-473.109 1023.3,-473.109 1027.3,-477.109 1027.3,-509.109"/>
<polyline fill="none" stroke="black" points="1023.3,-505.109 672.701,-505.109 "/>
<polyline fill="none" stroke="black" points="1023.3,-505.109 1023.3,-473.109 "/>
<polyline fill="none" stroke="black" points="1023.3,-505.109 1027.3,-509.109 "/>
<text text-anchor="middle" x="850" y="-485.109" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
<polygon fill="#efa03b" stroke="black" points="1258.15,-658.208 1023.85,-658.208 1019.85,-654.208 1019.85,-622.208 1254.15,-622.208 1258.15,-626.208 1258.15,-658.208"/>
<polyline fill="none" stroke="black" points="1254.15,-654.208 1019.85,-654.208 "/>
<polyline fill="none" stroke="black" points="1254.15,-654.208 1254.15,-622.208 "/>
<polyline fill="none" stroke="black" points="1254.15,-654.208 1258.15,-658.208 "/>
<text text-anchor="middle" x="1139" y="-634.208" font-family="Times,serif" font-size="20.00">orient_page</text>
</g>
<!-- t2&#45;&gt;t4 -->
<g id="edge5" class="edge"><title>t2&#45;&gt;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&#45;&gt;t4 -->
<g id="edge5" class="edge"><title>t3&#45;&gt;t4</title>
<path fill="none" stroke="#0044a0" d="M960.431,-531.003C943.787,-525.141 924.999,-518.524 907.704,-512.433"/>
<polygon fill="#0044a0" stroke="#0044a0" points="908.867,-509.132 898.272,-509.111 906.541,-515.734 908.867,-509.132"/>
</g>
<!-- t9 -->
<g id="node11" class="node"><title>t9</title>
<polygon fill="#efa03b" stroke="black" points="314.109,-277.109 19.8906,-277.109 15.8906,-273.109 15.8906,-241.109 310.109,-241.109 314.109,-245.109 314.109,-277.109"/>
<polyline fill="none" stroke="black" points="310.109,-273.109 15.8906,-273.109 "/>
<polyline fill="none" stroke="black" points="310.109,-273.109 310.109,-241.109 "/>
<polyline fill="none" stroke="black" points="310.109,-273.109 314.109,-277.109 "/>
<text text-anchor="middle" x="165" y="-253.109" font-family="Times,serif" font-size="20.00">select_image_layer</text>
</g>
<!-- t3&#45;&gt;t9 -->
<g id="edge14" class="edge"><title>t3&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M889.675,-542.297C675.505,-530.255 248,-498.192 248,-434.109 248,-434.109 248,-434.109 248,-374.109 248,-337.597 219.83,-304.936 196.312,-284.031"/>
<polygon fill="#0044a0" stroke="#0044a0" points="198.437,-281.243 188.562,-277.404 193.888,-286.563 198.437,-281.243"/>
</g>
<!-- t15 -->
<g id="node14" class="node"><title>t15</title>
<polygon fill="#efa03b" stroke="black" points="1391.34,-451.109 1170.66,-451.109 1166.66,-447.109 1166.66,-415.109 1387.34,-415.109 1391.34,-419.109 1391.34,-451.109"/>
<polyline fill="none" stroke="black" points="1387.34,-447.109 1166.66,-447.109 "/>
<polyline fill="none" stroke="black" points="1387.34,-447.109 1387.34,-415.109 "/>
<polyline fill="none" stroke="black" points="1387.34,-447.109 1391.34,-451.109 "/>
<text text-anchor="middle" x="1279" y="-427.109" font-family="Times,serif" font-size="20.00">skip_page</text>
</g>
<!-- t3&#45;&gt;t15 -->
<g id="edge19" class="edge"><title>t3&#45;&gt;t15</title>
<path fill="none" stroke="#0044a0" d="M1060.32,-531.078C1078.95,-524.591 1100.07,-516.881 1119,-509.109 1159.3,-492.568 1204.11,-471.304 1236.04,-455.645"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1237.67,-458.74 1245.1,-451.181 1234.58,-452.461 1237.67,-458.74"/>
</g>
<!-- t13 -->
<g id="node15" class="node"><title>t13</title>
<polygon fill="#efa03b" stroke="black" points="1388.24,-277.109 1007.76,-277.109 1003.76,-273.109 1003.76,-241.109 1384.24,-241.109 1388.24,-245.109 1388.24,-277.109"/>
<polyline fill="none" stroke="black" points="1384.24,-273.109 1003.76,-273.109 "/>
<polyline fill="none" stroke="black" points="1384.24,-273.109 1384.24,-241.109 "/>
<polyline fill="none" stroke="black" points="1384.24,-273.109 1388.24,-277.109 "/>
<text text-anchor="middle" x="1196" y="-253.109" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
</g>
<!-- t3&#45;&gt;t13 -->
<g id="edge21" class="edge"><title>t3&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M1040.79,-531.094C1071.69,-511.983 1114,-477.834 1114,-434.109 1114,-434.109 1114,-434.109 1114,-374.109 1114,-337.749 1141.83,-305.063 1165.07,-284.11"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1167.46,-286.664 1172.72,-277.466 1162.87,-281.377 1167.46,-286.664"/>
<g id="edge4" class="edge"><title>t3&#45;&gt;t4</title>
<path fill="none" stroke="#0044a0" d="M1073.02,-680.102C1081.95,-674.836 1091.92,-668.963 1101.34,-663.407"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1103.32,-666.302 1110.16,-658.209 1099.77,-660.272 1103.32,-666.302"/>
</g>
<!-- t5 -->
<g id="node6" class="node"><title>t5</title>
<polygon fill="#efa03b" stroke="black" points="830.742,-451.109 535.258,-451.109 531.258,-447.109 531.258,-415.109 826.742,-415.109 830.742,-419.109 830.742,-451.109"/>
<polyline fill="none" stroke="black" points="826.742,-447.109 531.258,-447.109 "/>
<polyline fill="none" stroke="black" points="826.742,-447.109 826.742,-415.109 "/>
<polyline fill="none" stroke="black" points="826.742,-447.109 830.742,-451.109 "/>
<text text-anchor="middle" x="681" y="-427.109" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
<polygon fill="#efa03b" stroke="black" points="1271.3,-600.208 920.701,-600.208 916.701,-596.208 916.701,-564.208 1267.3,-564.208 1271.3,-568.208 1271.3,-600.208"/>
<polyline fill="none" stroke="black" points="1267.3,-596.208 916.701,-596.208 "/>
<polyline fill="none" stroke="black" points="1267.3,-596.208 1267.3,-564.208 "/>
<polyline fill="none" stroke="black" points="1267.3,-596.208 1271.3,-600.208 "/>
<text text-anchor="middle" x="1094" y="-576.208" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
</g>
<!-- t4&#45;&gt;t5 -->
<g id="edge6" class="edge"><title>t4&#45;&gt;t5</title>
<path fill="none" stroke="#0044a0" d="M798.376,-473.003C780.521,-467.087 760.346,-460.402 741.823,-454.264"/>
<polygon fill="#0044a0" stroke="#0044a0" points="742.901,-450.934 732.308,-451.111 740.699,-457.579 742.901,-450.934"/>
<path fill="none" stroke="#0044a0" d="M1125.25,-622.102C1121.76,-617.759 1117.94,-613.003 1114.21,-608.353"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1116.65,-605.812 1107.66,-600.209 1111.2,-610.197 1116.65,-605.812"/>
</g>
<!-- t8 -->
<g id="node10" class="node"><title>t8</title>
<polygon fill="#efa03b" stroke="black" points="980.119,-335.109 665.881,-335.109 661.881,-331.109 661.881,-299.109 976.119,-299.109 980.119,-303.109 980.119,-335.109"/>
<polyline fill="none" stroke="black" points="976.119,-331.109 661.881,-331.109 "/>
<polyline fill="none" stroke="black" points="976.119,-331.109 976.119,-299.109 "/>
<polyline fill="none" stroke="black" points="976.119,-331.109 980.119,-335.109 "/>
<text text-anchor="middle" x="821" y="-311.109" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
<!-- t10 -->
<g id="node12" class="node"><title>t10</title>
<polygon fill="#00cc66" stroke="black" points="334,-348.208 35.7812,-320.208 334,-292.208 632.219,-320.208 334,-348.208"/>
<text text-anchor="middle" x="334" y="-314.208" font-family="Times,serif" font-size="20.00">select_image_layer</text>
</g>
<!-- t4&#45;&gt;t8 -->
<g id="edge12" class="edge"><title>t4&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M848.03,-473.087C846.196,-457.857 843.261,-434.941 840,-415.109 836.1,-391.393 830.78,-364.592 826.761,-345.181"/>
<polygon fill="#0044a0" stroke="#0044a0" points="830.152,-344.296 824.68,-335.224 823.3,-345.728 830.152,-344.296"/>
<!-- t4&#45;&gt;t10 -->
<g id="edge15" class="edge"><title>t4&#45;&gt;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&#45;&gt;t16 -->
<g id="edge20" class="edge"><title>t4&#45;&gt;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&#45;&gt;t14 -->
<g id="edge22" class="edge"><title>t4&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1244.5,-622.179C1297.25,-607.003 1350,-578.503 1350,-525.208 1350,-525.208 1350,-525.208 1350,-465.208 1350,-422.752 1366.76,-403.169 1340,-370.208 1330.4,-358.377 1317.91,-349.368 1304.22,-342.513"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1305.62,-339.304 1295.07,-338.313 1302.7,-345.666 1305.62,-339.304"/>
</g>
<!-- t6 -->
<g id="node7" class="node"><title>t6</title>
<polygon fill="#efa03b" stroke="black" points="801.95,-393.109 524.05,-393.109 520.05,-389.109 520.05,-357.109 797.95,-357.109 801.95,-361.109 801.95,-393.109"/>
<polyline fill="none" stroke="black" points="797.95,-389.109 520.05,-389.109 "/>
<polyline fill="none" stroke="black" points="797.95,-389.109 797.95,-357.109 "/>
<polyline fill="none" stroke="black" points="797.95,-389.109 801.95,-393.109 "/>
<text text-anchor="middle" x="661" y="-369.109" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
<polygon fill="#efa03b" stroke="black" points="1074.74,-542.208 779.258,-542.208 775.258,-538.208 775.258,-506.208 1070.74,-506.208 1074.74,-510.208 1074.74,-542.208"/>
<polyline fill="none" stroke="black" points="1070.74,-538.208 775.258,-538.208 "/>
<polyline fill="none" stroke="black" points="1070.74,-538.208 1070.74,-506.208 "/>
<polyline fill="none" stroke="black" points="1070.74,-538.208 1074.74,-542.208 "/>
<text text-anchor="middle" x="925" y="-518.208" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
</g>
<!-- t5&#45;&gt;t6 -->
<g id="edge7" class="edge"><title>t5&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M674.891,-415.003C673.495,-411.095 671.979,-406.852 670.48,-402.652"/>
<polygon fill="#0044a0" stroke="#0044a0" points="673.731,-401.351 667.072,-393.111 667.139,-403.705 673.731,-401.351"/>
<path fill="none" stroke="#0044a0" d="M1042.38,-564.102C1024.52,-558.185 1004.35,-551.5 985.823,-545.362"/>
<polygon fill="#0044a0" stroke="#0044a0" points="986.901,-542.032 976.308,-542.209 984.699,-548.677 986.901,-542.032"/>
</g>
<!-- t5&#45;&gt;t8 -->
<g id="edge11" class="edge"><title>t5&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M781.944,-415.046C792.967,-409.579 803.051,-402.434 811,-393.109 822.009,-380.195 824.454,-361.215 824.174,-345.581"/>
<polygon fill="#0044a0" stroke="#0044a0" points="827.659,-345.2 823.617,-335.407 820.669,-345.583 827.659,-345.2"/>
<!-- t9 -->
<g id="node11" class="node"><title>t9</title>
<polygon fill="#efa03b" stroke="black" points="1013,-426.208 694.762,-398.208 1013,-370.208 1331.24,-398.208 1013,-426.208"/>
<text text-anchor="middle" x="1013" y="-392.208" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
</g>
<!-- t5&#45;&gt;t9 -->
<g id="edge13" class="edge"><title>t5&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M1093.89,-563.996C1093.29,-548.44 1091.11,-525.146 1084,-506.208 1073.94,-479.426 1056.13,-452.609 1040.86,-432.521"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1043.62,-430.369 1034.72,-424.619 1038.09,-434.663 1043.62,-430.369"/>
</g>
<!-- t7 -->
<g id="node8" class="node"><title>t7</title>
<polygon fill="#efa03b" stroke="black" points="643.666,-335.109 354.334,-335.109 350.334,-331.109 350.334,-299.109 639.666,-299.109 643.666,-303.109 643.666,-335.109"/>
<polyline fill="none" stroke="black" points="639.666,-331.109 350.334,-331.109 "/>
<polyline fill="none" stroke="black" points="639.666,-331.109 639.666,-299.109 "/>
<polyline fill="none" stroke="black" points="639.666,-331.109 643.666,-335.109 "/>
<text text-anchor="middle" x="497" y="-311.109" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
<polygon fill="#efa03b" stroke="black" points="993.95,-484.208 716.05,-484.208 712.05,-480.208 712.05,-448.208 989.95,-448.208 993.95,-452.208 993.95,-484.208"/>
<polyline fill="none" stroke="black" points="989.95,-480.208 712.05,-480.208 "/>
<polyline fill="none" stroke="black" points="989.95,-480.208 989.95,-448.208 "/>
<polyline fill="none" stroke="black" points="989.95,-480.208 993.95,-484.208 "/>
<text text-anchor="middle" x="853" y="-460.208" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
</g>
<!-- t6&#45;&gt;t7 -->
<g id="edge8" class="edge"><title>t6&#45;&gt;t7</title>
<path fill="none" stroke="#0044a0" d="M610.903,-357.003C593.736,-351.141 574.358,-344.524 556.518,-338.433"/>
<polygon fill="#0044a0" stroke="#0044a0" points="557.384,-335.03 546.79,-335.111 555.122,-341.654 557.384,-335.03"/>
<path fill="none" stroke="#0044a0" d="M903.006,-506.102C896.726,-501.216 889.771,-495.807 883.095,-490.615"/>
<polygon fill="#0044a0" stroke="#0044a0" points="884.901,-487.586 874.859,-484.209 880.604,-493.111 884.901,-487.586"/>
</g>
<!-- t6&#45;&gt;t8 -->
<g id="edge10" class="edge"><title>t6&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M709.875,-357.003C726.624,-351.141 745.529,-344.524 762.933,-338.433"/>
<polygon fill="#0044a0" stroke="#0044a0" points="764.142,-341.718 772.425,-335.111 761.83,-335.111 764.142,-341.718"/>
<!-- t6&#45;&gt;t9 -->
<g id="edge12" class="edge"><title>t6&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M977.454,-506.003C987.329,-500.504 996.562,-493.377 1003,-484.208 1012.67,-470.441 1015.74,-452.229 1016.16,-436.195"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1019.66,-436.018 1016.09,-426.041 1012.66,-436.063 1019.66,-436.018"/>
</g>
<!-- t10 -->
<g id="node9" class="node"><title>t10</title>
<polygon fill="#efa03b" stroke="black" points="620.109,-277.109 335.891,-277.109 331.891,-273.109 331.891,-241.109 616.109,-241.109 620.109,-245.109 620.109,-277.109"/>
<polyline fill="none" stroke="black" points="616.109,-273.109 331.891,-273.109 "/>
<polyline fill="none" stroke="black" points="616.109,-273.109 616.109,-241.109 "/>
<polyline fill="none" stroke="black" points="616.109,-273.109 620.109,-277.109 "/>
<text text-anchor="middle" x="476" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
<!-- t8 -->
<g id="node9" class="node"><title>t8</title>
<polygon fill="#00cc66" stroke="black" points="676.666,-416.208 387.334,-416.208 383.334,-412.208 383.334,-380.208 672.666,-380.208 676.666,-384.208 676.666,-416.208"/>
<polyline fill="none" stroke="black" points="672.666,-412.208 383.334,-412.208 "/>
<polyline fill="none" stroke="black" points="672.666,-412.208 672.666,-380.208 "/>
<polyline fill="none" stroke="black" points="672.666,-412.208 676.666,-416.208 "/>
<text text-anchor="middle" x="530" y="-392.208" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
</g>
<!-- t7&#45;&gt;t10 -->
<g id="edge9" class="edge"><title>t7&#45;&gt;t10</title>
<path fill="none" stroke="#0044a0" d="M490.585,-299.003C489.12,-295.095 487.528,-290.852 485.953,-286.652"/>
<polygon fill="#0044a0" stroke="#0044a0" points="489.164,-285.245 482.376,-277.111 482.61,-287.703 489.164,-285.245"/>
<!-- t7&#45;&gt;t8 -->
<g id="edge9" class="edge"><title>t7&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M769.399,-448.125C724.645,-438.98 669.394,-427.691 623.252,-418.262"/>
<polygon fill="#0044a0" stroke="#0044a0" points="623.748,-414.791 613.25,-416.219 622.347,-421.65 623.748,-414.791"/>
</g>
<!-- t7&#45;&gt;t9 -->
<g id="edge11" class="edge"><title>t7&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M894.62,-448.039C911.413,-441.112 931.162,-432.966 949.666,-425.333"/>
<polygon fill="#0044a0" stroke="#0044a0" points="951.009,-428.565 958.918,-421.516 948.339,-422.094 951.009,-428.565"/>
</g>
<!-- t11 -->
<g id="node13" class="node"><title>t11</title>
<polygon fill="#efa03b" stroke="black" points="985.486,-277.109 642.514,-277.109 638.514,-273.109 638.514,-241.109 981.486,-241.109 985.486,-245.109 985.486,-277.109"/>
<polyline fill="none" stroke="black" points="981.486,-273.109 638.514,-273.109 "/>
<polyline fill="none" stroke="black" points="981.486,-273.109 981.486,-241.109 "/>
<polyline fill="none" stroke="black" points="981.486,-273.109 985.486,-277.109 "/>
<text text-anchor="middle" x="812" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
</g>
<!-- t7&#45;&gt;t11 -->
<g id="edge18" class="edge"><title>t7&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M592.793,-299.079C628.531,-292.726 669.303,-285.478 705.912,-278.969"/>
<polygon fill="#0044a0" stroke="#0044a0" points="706.887,-282.351 716.12,-277.155 705.661,-275.459 706.887,-282.351"/>
</g>
<!-- t12 -->
<g id="node12" class="node"><title>t12</title>
<polygon fill="#efa03b" stroke="black" points="607.082,-219.109 348.918,-219.109 344.918,-215.109 344.918,-183.109 603.082,-183.109 607.082,-187.109 607.082,-219.109"/>
<polyline fill="none" stroke="black" points="603.082,-215.109 344.918,-215.109 "/>
<polyline fill="none" stroke="black" points="603.082,-215.109 603.082,-183.109 "/>
<polyline fill="none" stroke="black" points="603.082,-215.109 607.082,-219.109 "/>
<text text-anchor="middle" x="476" y="-195.109" font-family="Times,serif" font-size="20.00">add_text_layer</text>
</g>
<!-- t10&#45;&gt;t12 -->
<g id="edge15" class="edge"><title>t10&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M476,-241.003C476,-237.312 476,-233.322 476,-229.352"/>
<polygon fill="#0044a0" stroke="#0044a0" points="479.5,-229.111 476,-219.111 472.5,-229.111 479.5,-229.111"/>
</g>
<!-- t8&#45;&gt;t9 -->
<g id="edge13" class="edge"><title>t8&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M661.842,-299.864C658.871,-299.606 655.921,-299.354 653,-299.109 509.812,-287.095 471.159,-289.734 324.542,-277.219"/>
<polygon fill="#0044a0" stroke="#0044a0" points="324.526,-273.705 314.262,-276.332 323.924,-280.679 324.526,-273.705"/>
<g id="node10" class="node"><title>t11</title>
<polygon fill="#00cc66" stroke="black" points="938.109,-338.208 653.891,-338.208 649.891,-334.208 649.891,-302.208 934.109,-302.208 938.109,-306.208 938.109,-338.208"/>
<polyline fill="none" stroke="black" points="934.109,-334.208 649.891,-334.208 "/>
<polyline fill="none" stroke="black" points="934.109,-334.208 934.109,-302.208 "/>
<polyline fill="none" stroke="black" points="934.109,-334.208 938.109,-338.208 "/>
<text text-anchor="middle" x="794" y="-314.208" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
</g>
<!-- t8&#45;&gt;t11 -->
<g id="edge17" class="edge"><title>t8&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M818.251,-299.003C817.64,-295.204 816.979,-291.087 816.322,-287.002"/>
<polygon fill="#0044a0" stroke="#0044a0" points="819.775,-286.429 814.732,-277.111 812.864,-287.539 819.775,-286.429"/>
<g id="edge10" class="edge"><title>t8&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M589.247,-380.152C629.442,-368.58 682.727,-353.241 724.863,-341.111"/>
<polygon fill="#0044a0" stroke="#0044a0" points="725.853,-344.468 734.495,-338.338 723.917,-337.741 725.853,-344.468"/>
</g>
<!-- t8&#45;&gt;t13 -->
<g id="edge20" class="edge"><title>t8&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M935.04,-299.079C978.311,-292.618 1027.78,-285.23 1071.94,-278.636"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1072.48,-282.093 1081.86,-277.155 1071.45,-275.17 1072.48,-282.093"/>
<!-- t12 -->
<g id="node14" class="node"><title>t12</title>
<polygon fill="#00cc66" stroke="black" points="1118.49,-270.208 775.514,-270.208 771.514,-266.208 771.514,-234.208 1114.49,-234.208 1118.49,-238.208 1118.49,-270.208"/>
<polyline fill="none" stroke="black" points="1114.49,-266.208 771.514,-266.208 "/>
<polyline fill="none" stroke="black" points="1114.49,-266.208 1114.49,-234.208 "/>
<polyline fill="none" stroke="black" points="1114.49,-266.208 1118.49,-270.208 "/>
<text text-anchor="middle" x="945" y="-246.208" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
</g>
<!-- t8&#45;&gt;t12 -->
<g id="edge19" class="edge"><title>t8&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M620.537,-380.161C641.947,-376.512 664.736,-372.939 686,-370.208 714.866,-366.5 927.304,-369.633 947,-348.208 963.502,-330.256 960.281,-301.363 954.611,-280.149"/>
<polygon fill="#0044a0" stroke="#0044a0" points="957.925,-279.012 951.707,-270.433 951.218,-281.017 957.925,-279.012"/>
</g>
<!-- t13 -->
<g id="node13" class="node"><title>t13</title>
<polygon fill="#00cc66" stroke="black" points="753.082,-270.208 494.918,-270.208 490.918,-266.208 490.918,-234.208 749.082,-234.208 753.082,-238.208 753.082,-270.208"/>
<polyline fill="none" stroke="black" points="749.082,-266.208 490.918,-266.208 "/>
<polyline fill="none" stroke="black" points="749.082,-266.208 749.082,-234.208 "/>
<polyline fill="none" stroke="black" points="749.082,-266.208 753.082,-270.208 "/>
<text text-anchor="middle" x="622" y="-246.208" font-family="Times,serif" font-size="20.00">add_text_layer</text>
</g>
<!-- t11&#45;&gt;t13 -->
<g id="edge16" class="edge"><title>t11&#45;&gt;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&#45;&gt;t10 -->
<g id="edge14" class="edge"><title>t9&#45;&gt;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&#45;&gt;t12 -->
<g id="edge16" class="edge"><title>t9&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M259.577,-241.079C294.86,-234.726 335.115,-227.478 371.259,-220.969"/>
<polygon fill="#0044a0" stroke="#0044a0" points="372.116,-224.372 381.337,-219.155 370.875,-217.482 372.116,-224.372"/>
<g id="edge18" class="edge"><title>t9&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M998.887,-371.447C994.931,-364.003 990.702,-355.831 987,-348.208 983.822,-341.663 967.96,-305.616 956.669,-279.865"/>
<polygon fill="#0044a0" stroke="#0044a0" points="959.82,-278.337 952.601,-270.582 953.409,-281.146 959.82,-278.337"/>
</g>
<!-- t16 -->
<g id="node17" class="node"><title>t16</title>
<polygon fill="#efa03b" stroke="black" points="1031.47,-105.333 1196,-78.005 1360.53,-105.333 1360.37,-149.551 1031.63,-149.551 1031.47,-105.333"/>
<polygon fill="none" stroke="black" points="1027.46,-101.94 1196,-73.9453 1364.54,-101.94 1364.36,-153.556 1027.64,-153.556 1027.46,-101.94"/>
<text text-anchor="middle" x="1196" y="-111.555" font-family="Times,serif" font-size="20.00">merge_pages</text>
<!-- t9&#45;&gt;t14 -->
<g id="edge21" class="edge"><title>t9&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1065.03,-374.611C1088.59,-364.381 1116.27,-352.359 1139.4,-342.314"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1140.9,-345.479 1148.68,-338.285 1138.11,-339.058 1140.9,-345.479"/>
</g>
<!-- t12&#45;&gt;t16 -->
<g id="edge26" class="edge"><title>t12&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M607.222,-185.246C722.142,-172.229 890.172,-153.196 1017.34,-138.791"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1018.04,-142.235 1027.58,-137.632 1017.25,-135.279 1018.04,-142.235"/>
</g>
<!-- t11&#45;&gt;t16 -->
<g id="edge27" class="edge"><title>t11&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M859.086,-240.997C916.223,-220.232 1014.29,-184.591 1089.94,-157.099"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1091.32,-160.323 1099.52,-153.618 1088.92,-153.744 1091.32,-160.323"/>
</g>
<!-- t15&#45;&gt;t16 -->
<g id="edge23" class="edge"><title>t15&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M1321.98,-414.938C1359.16,-396.916 1407,-364.681 1407,-318.109 1407,-318.109 1407,-318.109 1407,-258.109 1407,-211.469 1370.69,-179.509 1328.2,-158.076"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1329.57,-154.854 1319.04,-153.66 1326.53,-161.158 1329.57,-154.854"/>
</g>
<!-- t13&#45;&gt;t16 -->
<g id="edge25" class="edge"><title>t13&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M1196,-240.997C1196,-221.914 1196,-190.265 1196,-163.926"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1199.5,-163.759 1196,-153.759 1192.5,-163.759 1199.5,-163.759"/>
</g>
<!-- t14&#45;&gt;t16 -->
<g id="edge24" class="edge"><title>t14&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M1326.64,-588.956C1373.58,-572.748 1427,-543.103 1427,-492.109 1427,-492.109 1427,-492.109 1427,-258.109 1427,-220.539 1416.55,-207.537 1388,-183.109 1376.82,-173.542 1364.16,-165.377 1350.82,-158.413"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1351.99,-155.087 1341.47,-153.781 1348.88,-161.358 1351.99,-155.087"/>
<!-- t10&#45;&gt;t13 -->
<g id="edge17" class="edge"><title>t10&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M417.688,-300.029C455.345,-291.399 499.718,-281.231 537.473,-272.578"/>
<polygon fill="#0044a0" stroke="#0044a0" points="538.461,-275.943 547.427,-270.297 536.898,-269.12 538.461,-275.943"/>
</g>
<!-- t17 -->
<g id="node18" class="node"><title>t17</title>
<polygon fill="#efa03b" stroke="black" points="1310.18,-52 1085.82,-52 1081.82,-48 1081.82,-16 1306.18,-16 1310.18,-20 1310.18,-52"/>
<polyline fill="none" stroke="black" points="1306.18,-48 1081.82,-48 "/>
<polyline fill="none" stroke="black" points="1306.18,-48 1306.18,-16 "/>
<polyline fill="none" stroke="black" points="1306.18,-48 1310.18,-52 "/>
<text text-anchor="middle" x="1196" y="-28" font-family="Times,serif" font-size="20.00">copy_final</text>
<polygon fill="#efa03b" stroke="black" points="540.724,-156.455 770,-129.143 999.276,-156.455 999.062,-200.646 540.938,-200.646 540.724,-156.455"/>
<polygon fill="none" stroke="black" points="536.704,-152.903 770,-125.112 1003.3,-152.903 1003.05,-204.648 536.954,-204.648 536.704,-152.903"/>
<text text-anchor="middle" x="770" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_ghostscript</text>
</g>
<!-- t13&#45;&gt;t17 -->
<g id="edge27" class="edge"><title>t13&#45;&gt;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&#45;&gt;t18 -->
<g id="edge31" class="edge"><title>t13&#45;&gt;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&#45;&gt;t17 -->
<g id="edge28" class="edge"><title>t12&#45;&gt;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&#45;&gt;t18 -->
<g id="edge32" class="edge"><title>t12&#45;&gt;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&#45;&gt;t17 -->
<g id="edge28" class="edge"><title>t16&#45;&gt;t17</title>
<path fill="none" stroke="#0044a0" d="M1196,-73.8665C1196,-69.8921 1196,-65.942 1196,-62.1676"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1199.5,-62.1213 1196,-52.1214 1192.5,-62.1214 1199.5,-62.1213"/>
<g id="edge24" class="edge"><title>t16&#45;&gt;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&#45;&gt;t18 -->
<g id="edge29" class="edge"><title>t16&#45;&gt;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&#45;&gt;t17 -->
<g id="edge26" class="edge"><title>t14&#45;&gt;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&#45;&gt;t18 -->
<g id="edge30" class="edge"><title>t14&#45;&gt;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&#45;&gt;t17 -->
<g id="edge25" class="edge"><title>t15&#45;&gt;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&#45;&gt;t19 -->
<g id="edge35" class="edge"><title>t17&#45;&gt;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&#45;&gt;t19 -->
<g id="edge34" class="edge"><title>t18&#45;&gt;t19</title>
<path fill="none" stroke="#0044a0" d="M1151.35,-134.66C1128.23,-123.705 1102.26,-111.399 1078.23,-100.009"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1079.66,-96.8131 1069.12,-95.6933 1076.66,-103.139 1079.66,-96.8131"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
@@ -1,2 +1,2 @@
[pytest]
norecursedirs = lib
norecursedirs = lib .pc .git
+6 -3
View File
@@ -1,6 +1,9 @@
# requirements.txt can be used to replicate the developer's build environment
# setup.py lists a separate set of requirements that are looser to simplify
# installation
ruffus==2.6.3
Pillow==3.1.1
Pillow==3.3.0
reportlab==3.2.0
PyPDF2==1.25.1
img2pdf==0.2
PyPDF2==1.26
img2pdf==0.2.1
cffi==1.5.2
+6 -1
View File
@@ -1,4 +1,9 @@
[bdist_wheel]
python-tag = py34
[aliases]
test=pytest
test=pytest
[check-manifest]
ignore =
.github
+14 -14
View File
@@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals
import sys
if sys.version_info < (3, 4):
print("Python 3.4 or newer is required")
print("Python 3.4 or newer is required", file=sys.stderr)
sys.exit(1)
from setuptools import setup # nopep8
@@ -154,23 +154,23 @@ if 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']
)
@@ -198,7 +198,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",
@@ -209,7 +209,6 @@ setup(
],
setup_requires=[
'setuptools_scm',
'setuptools_scm_git_archive',
'cffi>=1.5.0',
'pytest-runner'
],
@@ -218,18 +217,19 @@ setup(
'ocrmypdf/lib/compile_leptonica.py:ffi'
],
install_requires=[
'ruffus>=2.6.3',
'Pillow>=3.0.0',
'reportlab>=3.1.44',
'PyPDF2>=1.25.1',
'img2pdf>=0.2.0',
'cffi>=1.5.0'
'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)
+4
View File
@@ -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
+64 -24
View File
@@ -9,21 +9,29 @@ Files derived from free sources
These test resources come from free sources, under either public domain or Creative Commons licenses.
In some cases they were converted from one image format to another without other changes.
+---------------------+--------------------------------------------------------------------------------+
| File | Source |
+=====================+================================================================================+
| c02-22.pdf | `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22 |
+---------------------+--------------------------------------------------------------------------------+
| congress.jpg | `US Congressional Records`_ |
+---------------------+--------------------------------------------------------------------------------+
| graph.pdf | `Wikimedia: Pandas text analysis.png`_ |
+---------------------+--------------------------------------------------------------------------------+
| lichtenstein.pdf | `Wikimedia: JPEG2000 Lichtenstein`_ (Creative Commons BY-SA 3.0) |
+---------------------+--------------------------------------------------------------------------------+
| LinnSequencer.jpg, | `Wikimedia: LinnSequencer`_ (Creative Commons Attribution-ShareAlike 3.0) |
| linn.pdf, linn.txt | |
+---------------------+--------------------------------------------------------------------------------+
.. list-table::
:widths: 20 50 30
:header-rows: 1
* - File
- Source
- License
* - c02-22.pdf
- `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22
- Public Domain
* - congress.jpg
- `US Congressional Records`_
- Public Domain
* - graph.pdf
- `Wikimedia: Pandas text analysis.png`_
- Public Domain
* - lichtenstein.pdf
- `Wikimedia: JPEG2000 Lichtenstein`_
- Creative Commons BY-SA 3.0
* - LinnSequencer.jpg, linn.pdf, linn.txt
- `Wikimedia: LinnSequencer`_
- Creative Commons BY-SA 3.0
Files generated for this project
================================
@@ -31,14 +39,43 @@ Files generated for this project
The following test resources were crafted specifically for this project, and can be used
under the terms of the license in LICENSE.rst.
- blank.pdf (a blank PDF page)
- cmyk.pdf (a CMYK image created in Photoshop)
- enormous.pdf (a very lage page)
- francais.pdf (a page containing French accented characters)
- hugemono.pdf (large monochrome JBIG2 page with pixel dimensions of 35000x35000)
- invalid.pdf (a PDF file header followed by EOF marker)
- missing_docinfo.pdf (PDF file with no /DocumentInfo section)
.. 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
==========
@@ -47,12 +84,13 @@ These test resources are assemblies from other previously mentioned files, relea
- 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 encrypted applied)
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2)
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
@@ -63,4 +101,6 @@ These test resources are assemblies from other previously mentioned files, relea
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+1 -7
View File
@@ -8,13 +8,7 @@ VERSION_STRING = '''tesseract 3.04.00
SPOOFED: return error claiming image too big
'''
"""Simulates a Tesseract crash
It isn't strictly necessary to crash the process and that has unwanted
side effects like triggering core dumps or error reporting, logging and such.
It's enough to dump some text to stderr and return an error code.
Follows the POSIX? convention of returning 128 + signal number.
"""Simulates an error of Tesseract failing on attempts to process large images
"""
+19
View File
@@ -6,6 +6,25 @@ import shutil
import subprocess
"""Cache output of tesseract to speed up test suite
The cache is keyed by a hash that includes the tesseract version, some of
the command line, and the binary dump of the input file. The output file,
stdout, and stderr are replicated on a cache hit.
Page orientation checks are also cached (-psm 0 stdout)
Errors and crashes are not cached.
Things not checked:
-changes to tesseract installation that don't affect --version
Will fail on Tesseract 3.02.02 in "hocr" mode because it doesn't produce
the incorrect file extension.
"""
CACHE_PATH = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'cache'))
+2 -2
View File
@@ -10,13 +10,13 @@ VERSION_STRING = '''tesseract 3.04.00
SPOOFED: CRASH ON OCR or -psm 0
'''
"""Simulates a Tesseract crash
"""Simulates a Tesseract crash when asked to run OCR
It isn't strictly necessary to crash the process and that has unwanted
side effects like triggering core dumps or error reporting, logging and such.
It's enough to dump some text to stderr and return an error code.
Follows the POSIX? convention of returning 128 + signal number.
Follows the POSIX(?) convention of returning 128 + signal number.
"""
+12
View File
@@ -4,6 +4,18 @@ import img2pdf
from PIL import Image
"""Tesseract no-op spoof
To quickly run tests where getting OCR output is not necessary.
In 'hocr' mode, create a .hocr file that specifies no text found.
In 'pdf' mode, convert the image to PDF using another program.
In orientation check mode, report the orientation is upright.
"""
VERSION_STRING = '''tesseract 3.04.00
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
+14
View File
@@ -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()
+198 -63
View File
@@ -2,7 +2,7 @@
# © 2015 James R. Barlow: github.com/jbarlow83
from __future__ import print_function
from subprocess import Popen, PIPE, check_output, check_call
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
import os
import shutil
from contextlib import suppress
@@ -12,6 +12,8 @@ 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:
@@ -21,11 +23,11 @@ if sys.version_info.major < 3:
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
OCRMYPDF = os.path.join(PROJECT_ROOT, 'OCRmyPDF.sh')
TEST_RESOURCES = os.path.join(PROJECT_ROOT, 'tests', 'resources')
TEST_OUTPUT = os.environ.get(
'OCRMYPDF_TEST_OUTPUT',
default=os.path.join(PROJECT_ROOT, 'tests', 'output', 'main'))
OCRMYPDF = [sys.executable, '-m', 'ocrmypdf']
def running_in_docker():
@@ -33,6 +35,10 @@ def running_in_docker():
return os.path.exists('/.dockerinit')
def is_linux():
return platform.system() == 'Linux'
def setup_module():
with suppress(FileNotFoundError):
shutil.rmtree(TEST_OUTPUT)
@@ -40,15 +46,6 @@ def setup_module():
os.makedirs(TEST_OUTPUT)
def run_ocrmypdf_sh(input_file, output_file, *args, env=None):
sh_args = ['sh', OCRMYPDF] + list(args) + [input_file, output_file]
sh = Popen(
sh_args, close_fds=True, stdout=PIPE, stderr=PIPE,
universal_newlines=True, env=env)
out, err = sh.communicate()
return sh, out, err
def _infile(input_basename):
return os.path.join(TEST_RESOURCES, input_basename)
@@ -58,29 +55,31 @@ def _outfile(output_basename):
def check_ocrmypdf(input_basename, output_basename, *args, env=None):
"Run ocrmypdf and confirmed that a valid file was created"
input_file = _infile(input_basename)
output_file = _outfile(output_basename)
sh, out, err = run_ocrmypdf_sh(input_file, output_file, *args, env=env)
if sh.returncode != 0:
p, out, err = run_ocrmypdf(input_basename, output_basename, *args, env=env)
if p.returncode != 0:
print('stdout\n======')
print(out)
print('stderr\n======')
print(err)
assert sh.returncode == 0
assert p.returncode == 0
assert os.path.exists(output_file), "Output file not created"
assert os.stat(output_file).st_size > 100, "PDF too small or empty"
return output_file
def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
def run_ocrmypdf(input_basename, output_basename, *args, env=None):
"Run ocrmypdf and let caller deal with results"
input_file = _infile(input_basename)
output_file = _outfile(output_basename)
if env is None:
env = os.environ
p_args = ['ocrmypdf'] + list(args) + [input_file, output_file]
p_args = OCRMYPDF + list(args) + [input_file, output_file]
p = Popen(
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
universal_newlines=True, env=env)
@@ -125,8 +124,8 @@ def spoof_tesseract_big_image_error():
return spoof('tesseract', 'tesseract_big_image_error.py')
def test_quick(spoof_tesseract_noop):
check_ocrmypdf('c02-22.pdf', 'test_quick.pdf', env=spoof_tesseract_noop)
def test_quick(spoof_tesseract_cache):
check_ocrmypdf('ccitt.pdf', 'test_quick.pdf', env=spoof_tesseract_cache)
def test_deskew(spoof_tesseract_noop):
@@ -159,34 +158,34 @@ def test_deskew(spoof_tesseract_noop):
def test_clean(spoof_tesseract_noop):
check_ocrmypdf('skew.pdf', 'test_clean.pdf', '-c', env=spoof_tesseract_noop)
check_ocrmypdf('skew.pdf', 'test_clean.pdf', '-c',
env=spoof_tesseract_noop)
@pytest.mark.parametrize("pdf,renderer", [
('palette.pdf', 'hocr'),
('palette.pdf', 'tesseract'),
('cmyk.pdf', 'hocr'),
('cmyk.pdf', 'tesseract'),
('ccitt.pdf', 'hocr'),
('ccitt.pdf', 'tesseract'),
('jbig2.pdf', 'hocr'),
('jbig2.pdf', 'tesseract'),
('lichtenstein.pdf', 'hocr'),
('lichtenstein.pdf', 'tesseract')
])
def test_exotic_image(spoof_tesseract_cache, pdf, renderer):
# 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',
'-v', '1',
'--output-type', output_type,
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
def test_preserve_metadata(spoof_tesseract_noop):
@pytest.mark.parametrize("output_type", [
'pdfa', 'pdf'
])
def test_preserve_metadata(spoof_tesseract_noop, output_type):
pdf_before = pypdf.PdfFileReader(_infile('graph.pdf'))
output = check_ocrmypdf('graph.pdf', 'test_metadata_preserve.pdf',
'--output-type', output_type,
env=spoof_tesseract_noop)
pdf_after = pypdf.PdfFileReader(output)
@@ -194,8 +193,17 @@ def test_preserve_metadata(spoof_tesseract_noop):
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(spoof_tesseract_noop):
@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')
@@ -203,11 +211,12 @@ def test_override_metadata(spoof_tesseract_noop):
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,
'--output-type', output_type,
env=spoof_tesseract_noop)
assert p.returncode == ExitCode.ok
@@ -226,6 +235,9 @@ def test_override_metadata(spoof_tesseract_noop):
assert pdfinfo['Subject'] == high_unicode
assert pdfinfo.get('Keywords', '') == ''
pdfa_info = file_claims_pdfa(output_file)
assert pdfa_info['output'] == output_type
@pytest.mark.parametrize('renderer', [
'hocr',
@@ -244,8 +256,8 @@ def test_oversample(spoof_tesseract_cache, renderer):
def test_repeat_ocr():
sh, _, _ = run_ocrmypdf_sh('graph_ocred.pdf', 'wontwork.pdf')
assert sh.returncode != 0
p, _, _ = run_ocrmypdf('graph_ocred.pdf', 'wontwork.pdf')
assert p.returncode != 0
def test_force_ocr(spoof_tesseract_cache):
@@ -325,11 +337,6 @@ def test_monochrome_correlation():
'tesseract',
])
def test_autorotate(spoof_tesseract_cache, renderer):
import ocrmypdf.ghostscript as ghostscript
import logging
gslog = logging.getLogger()
# cardinal.pdf contains four copies of an image rotated in each cardinal
# direction - these ones are "burned in" not tagged with /Rotate
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
@@ -343,6 +350,36 @@ def test_autorotate(spoof_tesseract_cache, renderer):
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',
@@ -361,16 +398,15 @@ def test_skip_big(spoof_tesseract_cache):
assert not pdfinfo[0]['has_text']
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
def test_maximum_options(spoof_tesseract_cache, 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',
'--skip-big', '10', '--title', 'Too Many Weird Files',
'--author', 'py.test', '--pdf-renderer', renderer,
'--output-type', output_type,
env=spoof_tesseract_cache)
@@ -378,38 +414,49 @@ 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_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_env(
p, out, err = run_ocrmypdf(
'francais.pdf', 'francais.pdf', '-l', 'fra', env=spoof_tesseract_cache)
assert p.returncode == ExitCode.ok, \
"This test may fail if Tesseract language packs are missing"
def test_klingon():
p, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'francais.pdf', 'francais.pdf', '-l', 'klz')
assert p.returncode == ExitCode.bad_args
def test_missing_docinfo(spoof_tesseract_noop):
p, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'missing_docinfo.pdf', 'missing_docinfo.pdf', '-l', 'eng', '-c',
env=spoof_tesseract_noop)
assert p.returncode == ExitCode.ok, err
@@ -428,26 +475,26 @@ def test_uppercase_extension(spoof_tesseract_noop):
def test_input_file_not_found():
input_file = "does not exist.pdf"
sh, out, err = run_ocrmypdf_sh(
p, out, err = run_ocrmypdf(
_infile(input_file),
_outfile("will not happen.pdf"))
assert sh.returncode == ExitCode.input_file
assert p.returncode == ExitCode.input_file
assert (input_file in out or input_file in err)
def test_input_file_not_a_pdf():
input_file = __file__ # Try to OCR this file
sh, out, err = run_ocrmypdf_sh(
p, out, err = run_ocrmypdf(
_infile(input_file),
_outfile("will not happen.pdf"))
assert sh.returncode == ExitCode.input_file
assert p.returncode == ExitCode.input_file
assert (input_file in out or input_file in err)
def test_qpdf_repair_fails():
env = os.environ.copy()
env['OCRMYPDF_QPDF'] = os.path.abspath('./spoof/qpdf_dummy_return2.py')
p, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'-v', '1',
'c02-22.pdf', 'wont_be_created.pdf', env=env)
print(out)
@@ -456,7 +503,7 @@ def test_qpdf_repair_fails():
def test_encrypted():
p, out, err = run_ocrmypdf_env('skew-encrypted.pdf', 'wont_be_created.pdf')
p, out, err = run_ocrmypdf('skew-encrypted.pdf', 'wont_be_created.pdf')
assert p.returncode == ExitCode.input_file
assert out.find('password')
@@ -478,21 +525,23 @@ def test_pagesegmode(renderer, spoof_tesseract_cache):
'tesseract',
])
def test_tesseract_crash(renderer, spoof_tesseract_crash):
sh, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'ccitt.pdf', 'wontwork.pdf', '-v', '1',
'--pdf-renderer', renderer, env=spoof_tesseract_crash)
assert sh.returncode == ExitCode.child_process_error
assert p.returncode == ExitCode.child_process_error
assert not os.path.exists(_outfile('wontwork.pdf'))
assert "ERROR" in err
def test_tesseract_crash_autorotate(spoof_tesseract_crash):
sh, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'ccitt.pdf', 'wontwork.pdf',
'-r', env=spoof_tesseract_crash)
assert sh.returncode == ExitCode.child_process_error
assert p.returncode == ExitCode.child_process_error
assert not os.path.exists(_outfile('wontwork.pdf'))
assert "ERROR" in err
print(out)
print(err)
@pytest.mark.parametrize('renderer', [
@@ -503,3 +552,89 @@ 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: cat testfile.pdf | ocrmypdf - output.pdf
p1_args = ['cat', input_file]
p1 = Popen(p1_args, close_fds=True, stdin=DEVNULL, stdout=PIPE)
p2_args = OCRMYPDF + ['-', output_file]
p2 = Popen(
p2_args, close_fds=True, stdout=PIPE, stderr=PIPE,
stdin=p1.stdout, env=spoof_tesseract_noop)
p1.stdout.close()
out, err = p2.communicate()
assert p2.returncode == ExitCode.ok
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)
+9 -4
View File
@@ -103,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():
@@ -120,8 +120,12 @@ def test_single_page_inline_image():
pdf.showPage()
pdf.save()
with pytest.raises(NotImplementedError):
pageinfo.pdf_get_all_pageinfo(filename)
pdfinfo = pageinfo.pdf_get_all_pageinfo(filename)
print(pdfinfo)
pdfimage = pdfinfo[0]['images'][0]
assert (pdfimage['dpi_w'] - 8) < 1e-5
assert pdfimage['color'] != '-'
assert pdfimage['width'] == 8
def test_jpeg():
@@ -131,4 +135,5 @@ def test_jpeg():
pdfimage = pdfinfo[0]['images'][0]
assert pdfimage['enc'] == 'jpeg'
assert (pdfimage['dpi_w'] - 150) < 1e-5