Compare commits

...
175 Commits
Author SHA1 Message Date
James R. Barlow c48acf165a v4.3.5: Python 3.6 compatibility 2017-01-03 00:45:33 -08:00
James R. Barlow 9e004c3ec0 Another attempt at py 3.4/3.5
Revert to exactly what the previous passing build specified.
2017-01-03 00:34:26 -08:00
James R. Barlow 7be4e9c919 fix setuptools-scm for py 3.4, 3.5 2017-01-03 00:25:57 -08:00
James R. Barlow 5ec38a4bed Update requirements files and documentation for Python 3.6 - no code changes 2017-01-03 00:11:34 -08:00
James R. Barlow cc9ceaeb74 v4.3.4: release notes 2016-12-08 16:34:09 -08:00
James R. Barlow ad2fa8d1d7 Fix MANIFEST for .png 2016-12-08 16:25:04 -08:00
James R. Barlow adc1580742 Help py.test collect output in more cases 2016-12-08 16:21:07 -08:00
James R. Barlow 4d3b44d6df ghostscript: cleanup harmless error message printed for overprint
Redirect stderr->stdout to hopefully make GS output easier to work with
overall, since the previous code didn’t seem to account for mixed used
properly.
2016-12-08 16:19:15 -08:00
James R. Barlow e57aa0eee2 pageinfo: fix “decimal.InvalidOperation: quantize result has too many digits”
And add new test case for this.
2016-12-08 16:06:53 -08:00
James R. Barlow 1ae1d116c7 Make setup.py license internally consistent 2016-12-08 16:06:31 -08:00
James R. Barlow 9f51ed9d01 Finalize v4.3.3 release notes 2016-12-03 00:39:24 -08:00
James R. Barlow 731e6792c7 Add test cases for Ghostscript PDF/A warnings 2016-12-03 00:32:09 -08:00
James R. Barlow c35ec0b4aa ghostscript: more effort at error logging 2016-12-03 00:22:03 -08:00
James R. Barlow 03aaf575dc v4.3.3 release notes, fix more gs 9.20 issues 2016-12-02 16:26:34 -08:00
James R. Barlow de939951d4 Record version in debug log 2016-11-29 15:30:50 -08:00
James R. Barlow 7725d16a26 Fix exception on inline stencil masks with no /CS attribute 2016-11-24 22:37:00 -08:00
James R. Barlow 8a74408d83 Add security suggestions 2016-11-21 20:58:31 -08:00
James R. Barlow 3d0dc95a06 Moved venvs 2016-11-21 20:40:22 -08:00
James R. Barlow 04a57a3cc2 OS X -> macOS 2016-11-21 20:40:06 -08:00
James R. Barlow d0c22ce01d v4.3.2 release notes 2016-11-10 23:16:08 -08:00
James R. Barlow 23c95e9660 ghostscript: elide overprinting to fix PDF/A errors in GS 9.20
It looks like GS 9.19 can incorrectly set overprinting for the text layer
even though this makes no sense in PDF/A, or at least someone produced
PDFs that have this after a Tesseract PDF -> GS PDF/A conversion. GS 9.20
complains about this. Instead of aborting, elide the feature.

See
http://git.ghostscript.com/?p=ghostpdl.git;a=commitdiff;h=094d5a1880f1cb9ed320ca9353eb69436e09b594
and
issue #107.

It looks like it is better to elide features and warn about elision rather
than abort with an error.
2016-11-10 14:48:02 -08:00
James R. Barlow eecab9b95d pdfa: fix KeyError on pdfa_dict if document has some xmp metadata but
not exactly what we’re looking for
2016-11-09 05:41:12 -08:00
James R. Barlow 8abc2f113c Merge branch 'develop' 2016-11-07 14:36:50 -08:00
James R. Barlow 949d2ff1c2 v4.3.1 release notes 2016-11-07 14:36:08 -08:00
James R. Barlow 1c8b763d53 test_pageinfo: Remove bits per component test
The behavior of this test will ultimately depend on what version of
img2pdf is installed, since after my patch it will be able to produce
1bpp images.
2016-11-07 14:35:54 -08:00
James R. Barlow bb91393b85 Fix “deskew-rotate” bug.
Turns out this occurred in any case where pdf-renderer hocr was used
and a tesseract timeout or error occurred. We created a replacement
page based on the unrotated page dimensions instead of the input image’s
dimensions.
2016-11-07 14:17:31 -08:00
James R. Barlow cc9c0d819e Add test case for documents that get rotated incorrectly after deskew 2016-11-07 14:15:03 -08:00
James R. Barlow a72b8caf47 Update documentation on other languages, multilingual documents 2016-11-07 14:14:06 -08:00
James R. Barlow fdd9b8b8ce Optimize some of the test resources to reduce file sizes
Mostly by reducing RGB -> monochrome and applying JBIG2 compression
2016-11-07 14:01:23 -08:00
James R. Barlow c096b4ca8c Make debug dump of pageinfo at the end of processing readable 2016-11-04 02:23:02 -07:00
James R. Barlow 427add3008 Add @posttask debug hooks 2016-11-03 18:15:21 -07:00
James R. Barlow c45871700d Fix bug: LeptonicaErrorTrap() leaks file handles 2016-11-03 15:51:27 -07:00
Sean Whittonandjbarlow83 6821e8eeb2 disable mathjax sphinx extension (#103)
Mathjax isn't actually needed for OCRmyPDF's docs, but enabling this
extension causes the browser to download a copy of mathjax.js from
cdn.mathjax.org anyway.

I have to disable this for the offline docs bundled with Debian, but
since you're not using mathjax, it would be nice to have the diff merged
upstream.
2016-11-01 21:56:57 -07:00
James R. Barlow a4f07756a5 tesseract caching: don't transcode tesseract's output, hash source file
For sanity's sake, deal with tesseract streams in binary without
transcoding (via universal_newlines, etc.). The only differences are
printing messages regarding spoofing.

Also hash the source file so that changes to the cache mechanism
invalidate old cache automatically. That is probably too aggressive,
but simple and safer than the previous approach.
2016-10-28 16:44:12 -07:00
James R. Barlow f24fb0e0c5 Obligatory MANIFEST.in repair 2016-10-28 01:28:46 -07:00
James R. Barlow 73b88a0a6f More work on documentation 2016-10-28 01:22:40 -07:00
James R. Barlow c42f39e2d4 Update README to point to ReadTheDocs 2016-10-28 00:33:17 -07:00
James R. Barlow 5e5fe3175f docs: OS X -> macOS branding change 2016-10-28 00:32:57 -07:00
James R. Barlow cab65d1f11 pageinfo: add a python3.4 implementation of isclose() 2016-10-28 00:31:04 -07:00
James R. Barlow 245f05d5f4 docs: allow python setup.py install --force to bypass checks
ReadTheDocs needs this.
2016-10-28 00:07:26 -07:00
James R. Barlow dda751f9e3 Merge branch 'feature/docs' into develop
# Conflicts:
#	ocrmypdf/__main__.py
2016-10-27 23:50:08 -07:00
James R. Barlow 3d37ae988a Update release notes for 4.3 2016-10-27 23:48:12 -07:00
James R. Barlow 717acd9855 Prevent dumping binary PDFs to stdout 2016-10-27 16:20:53 -07:00
James R. Barlow 2e4431cc63 Allow piping output to stdout 2016-10-27 16:14:42 -07:00
James R. Barlow f7387b0859 test_stdin: simplify this test
No need to involve 'cat', just hook the file up to stdin.
2016-10-27 16:01:07 -07:00
James R. Barlow a09f6b8977 Test cases: check that stdout is clear of output
To ensure piping to stdout is possible.
2016-10-27 15:58:24 -07:00
James R. Barlow d63449c214 main: don't print output file location to stdout, use stderr 2016-10-27 15:57:33 -07:00
James R. Barlow a86805f0d9 Remove possibly non-free page from "multipage.pdf" 2016-10-27 15:56:43 -07:00
James R. Barlow 7d2009ccef ghostscript: log errors from stdout 2016-10-27 15:36:20 -07:00
James R. Barlow 18ae5db06d ghostscript: ensure raster resolution is specified in integer units 2016-10-27 15:35:33 -07:00
James R. Barlow 9a1838f102 pageinfo: accept "cm/Do" image drawing without the usual "q/Q" wrapper
Some PDFs omit the traditional q/Q wrapper and alter ctm with a stack
depth of zero, so make our test for stack depth specifically test for
the case where the PDF calls for rendering to an uninitialized ctm.

Probably related to #97.
2016-10-27 15:35:00 -07:00
James R. Barlow e20346032d leptonica: add color testing functions for future experiments 2016-10-27 14:49:49 -07:00
James R. Barlow 693a27d76c leptonica: add iPython display hook and equality test 2016-10-26 14:44:41 -07:00
James R. Barlow 203966d86b leptonica: fix Pillow conversion for 1-bit and 8-bit gray images 2016-10-26 13:10:13 -07:00
James R. Barlow 7eca8508fd Implement new preprocessing feature, background removal 2016-10-14 17:23:34 -07:00
James R. Barlow b85270df1c Merge branch 'master' into develop 2016-10-14 15:56:58 -07:00
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 2ec516b6ff leptonica: learn a few new tricks
Found some interesting options for background norm.
2016-09-09 12:54:36 -07:00
James R. Barlow 7942a01e50 leptonica: This is not a Py2 module anymore 2016-09-08 20:59:38 -07:00
James R. Barlow df684f9344 Update tesseract supported languages 2016-09-08 15:52:25 -07:00
James R. Barlow ae16e95e42 leptonica: scale should be a tuple for consistency 2016-09-08 11:38:10 -07:00
James R. Barlow 2ac8e8a0cc More doc tweaks, mainly introduction 2016-09-08 11:29:53 -07:00
James R. Barlow 0a0ceda71f Start the documentation 2016-09-06 13:52:40 -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
55 changed files with 4322 additions and 2195 deletions
+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).
+6 -2
View File
@@ -1,8 +1,9 @@
# Development environment
*.pyc
*.sublime-*
venv-*/
venv*/
pyvenv.cfg
tasks.py
# Package building
*.egg-info/
@@ -15,6 +16,9 @@ wheelhouse/
# Automatically generated files
ocrmypdf/lib/_*.py
ocrmypdf/version.py
docs/_build/
docs/_static/
docs/_templates/
# Code coverage
.coverage
@@ -26,5 +30,5 @@ log/
.ipynb_checkpoints/
tests/cache/
tests/output/
tests/resources/private
tests/resources/private/
tmp/
+7 -5
View File
@@ -1,3 +1,5 @@
sudo: required
dist: trusty
language: python
cache: pip
@@ -9,17 +11,17 @@ cache:
- tests/cache
python:
- 3.4
- "3.4"
- "3.5"
- "3.6-dev" # 3.6 not available yet
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 libavformat56 libavcodec56 libavutil54 libffi-dev
+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
+47 -2
View File
@@ -1,4 +1,49 @@
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 *.png
recursive-include tests *.pdf
recursive-include tests *.py
recursive-include tests *.rst
recursive-include tests *.txt
recursive-exclude tests/output *
recursive-exclude tests/output_pageinfo *
recursive-exclude tests/resources/private *
recursive-exclude tests/cache *
# documentation
include *.rst
include pipeline.svg
recursive-exclude .github *
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs *.svg
recursive-exclude docs/_build *
# support files
recursive-include ocrmypdf/data *
recursive-include share *
include *.py
exclude tasks.py
# code
recursive-include ocrmypdf *.py
exclude ocrmypdf/lib/_leptonica.py
include OCRmyPDF.sh
include docker-wrapper.sh
+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 "$@"
+45 -198
View File
@@ -2,13 +2,26 @@ OCRmyPDF
========
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched.
be searched or copy-pasted.
.. code-block:: bash
ocrmypdf # it's a scriptable command line program
-l eng+fra # it supports multiple languages
--rotate-pages # it can fix pages that are misrotated
--deskew # it can deskew crooked PDFs!
--title "My PDF" # it can change output metadata
--jobs 4 # it uses multiple cores by default
--output-type pdfa # it produces PDF/A by default
input_scanned.pdf # takes PDF input (or images)
output_searchable.pdf # produces validated PDF output
Main features
-------------
- Generates a searchable
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`__ file from a regular PDF
`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 +31,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,223 +44,57 @@ I searched the web for a free command line tool to OCR PDF files on
Linux/UNIX: I found many, but none of them were really satisfying.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not display correctly some escaped HTML characters located in the hOCR file produced by the OCR engine
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated PDF files having a ridiculous big size
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR some of my PDF files
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
... so I decided to develop my own tool (using various existing scripts
as an inspiration)
...so I decided to develop my own tool (using various existing scripts
as an inspiration).
Installation
------------
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
You can install it to a Python virtual environment or system-wide.
Debian and Ubuntu
~~~~~~~~~~~~~~~~~
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
``apt-get install ocrmypdf``.
Installing the Docker image
~~~~~~~~~~~~~~~~~~~~~~~~~~~
For many users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
If you have `Docker <https://docs.docker.com/>`__ installed on your system, you can install
a Docker image of the latest release.
Follow the Docker installation instructions for your platform. If you can run this command
successfully, your system is ready to download and execute the image::
docker run hello-world
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs::
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
Assuming you have a Docker engine running somewhere, you can run these commands to download
the image::
docker pull jbarlow83/ocrmypdf
Then tag it to give a more convenient name, just ocrmypdf::
docker tag jbarlow83/ocrmypdf ocrmypdf
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__::
# Alternative step: If you need all language packs
docker pull jbarlow83/ocrmypdf-polyglot
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
You can then run ocrmypdf using the command::
docker run ocrmypdf --help
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`__, such as this in this template::
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``::
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
Installing on Mac OS X
~~~~~~~~~~~~~~~~~~~~~~
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/>`__.
Update Homebrew::
brew update
Install or upgrade the required Homebrew packages, if any are missing::
brew install libpng openjpeg jbig2dec # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2 libffi leptonica
brew install unpaper # optional
brew install tesseract
Update the homebrew pip and install Pillow::
pip3 install --upgrade pip
pip3 install --upgrade pillow
You can then install OCRmyPDF from PyPI::
pip3 install ocrmypdf
The command line program should now be available::
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.
Update apt-get::
sudo apt-get update
sudo apt-get upgrade
Install system dependencies::
sudo apt-get install \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
ghostscript \
tesseract-ocr \
qpdf \
unpaper \
python3-pip \
python3-pil \
python3-pytest \
python3-reportlab
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
into your system Python, which could interfere with other programs)::
sudo pip3 install ocrmypdf
If you wish to install OCRmyPDF to a virtual environment to isolate system Python from modified, you can
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`__::
sudo apt-get install python3-venv
python3 -m venv venv-ocrmypdf --without-pip
source venv-ocrmypdf/bin/activate
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
deactivate
pyvenv --system-site-packages venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip install ocrmypdf
Ubuntu 14.04 only installs ``unpaper`` version 0.4.2, which is not supported by OCRmyPDF because it is produces invalid output. This program is an optional dependency, and provides page deskewing and cleaning. See `Dockerfile <Dockerfile>`__ for an example of how to building unpaper 6.1 from source. If you choose to install unpaper later, OCRmyPDF will use the foremost version on the system PATH.
Installing on Windows
~~~~~~~~~~~~~~~~~~~~~
Direct installation on Windows is not possible. Install the Docker container as described above.
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::
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::
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
On certain Linux distributions such as Ubuntu, you may need to use
run the install command as superuser::
sudo pip3 install [-e] 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::
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
pyvenv venv
source venv/bin/activate
cd OCRmyPDF
pip3 install .
However, ``ocrmypdf`` will only be accessible on the system PATH after
you activate the virtual environment.
To run the program::
ocrmypdf --help
If not yet installed, the script will notify you about dependencies that
need to be installed. The script requires specific versions of the
dependencies. Older version than the ones mentioned in the release notes
are likely not to be compatible to OCRmyPDF.
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
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
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.
Support
-------
Documentation and 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
Our `documentation is served on Read the Docs <https://ocrmypdf.readthedocs.io/en/latest/index.html>`_.
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
@@ -259,11 +106,11 @@ In case you detect an issue, please:
Press & Media
-------------
- `c't 1-2014, page 59 <http://heise.de/-2279695>`__:
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
magazine c't
- `heise Open Source, 09/2014: Texterkennung mit
OCRmyPDF <http://heise.de/-2356670>`__
OCRmyPDF <http://heise.de/-2356670>`_
Disclaimer
----------
+139 -366
View File
@@ -1,9 +1,142 @@
RELEASE NOTES
=============
Please always read this file before installing the package
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
v4.3.5:
=======
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
v4.3.4:
=======
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
v4.3.3:
=======
- Fixed PDF/A creation with Ghostscript 9.20 properly
- Fixed an exception on inline stencil masks with a missing optional parameter
v4.3.2:
=======
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
v4.3.1:
=======
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
- Ability to specify multiple languages for multilingual documents is now advertised in documentation
- Reduced the file sizes of some test resources
- Cleaned up debug output
- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed
v4.3:
=====
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
- Better documentation
- Fixed an issue with PDFs that draw images when the raster stack depth is zero
- ocrmypdf can now redirect its output to stdout for use in a shell pipeline
+ This does not improve performance since temporary files are still used for buffering
+ Some output validation is disabled in this mode
v4.2.5:
=======
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
- Removed non-free file milk.pdf
v4.2.4:
=======
- Fixed an error (#90) caused by PDFs that use stencil masks properly
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
v4.2.3:
=======
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
- Integrated a few fixes to simplify downstream packaging (Debian)
+ The test suite no longer assumes it is installed
+ If running Linux, skip a test that passes Unicode on the command line
- Added a test case to check explicit masks and stencil masks
- Added a test case for indirect objects and linearized PDFs
- Deprecated the OCRmyPDF.sh shell script
v4.2.2:
=======
- Improvements to documentation
v4.2.1:
=======
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
- Implemented stdin streaming
v4.2:
=====
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
+ ``pdfa`` is the default, consistent with past behavior
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
- ``--force-ocr`` rasterizes pages even if they contain no images
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
+ fixes issue #82
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
+ Some Ubuntu "PPAs" (backports) are needed to make it work
- Support for some older dependencies dropped
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
- Ghostscript now runs in "safer" mode where possible
v4.1.4:
=======
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
v4.1.3:
=======
- More helpful error message for PDFs with version 4 security handler
- Update usage instructions for Windows/Docker users
- Fix order of operations for matrix multiplication (no effect on most users)
- Add a few leptonica wrapper functions (no effect on most users)
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
v4.1.2:
=======
@@ -36,9 +169,6 @@ v4.0.6:
v4.0.5:
=======
Fixes
-----
- Remove some verbose debug messages from v4.0.4
- Fixed temporary that wasn't being deleted
- DPI is now calculated correctly for cropped images, along with other image transformations
@@ -62,7 +192,7 @@ New features
Fixes
-----
- Show stack trace if unexpect errors occur
- Show stack trace if unexpected errors occur
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
abort the file
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
@@ -171,7 +301,7 @@ Changes
-------
- Default output format is now PDF/A-2b instead of PDF/A-1b
- Python 3.5 and OS X El Capitan are now supported platforms - no changes were
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
needed to implement support
- Improved some error messages related to missing input files
- Fixed issue #20 - uppercase .PDF extension not accepted
@@ -335,363 +465,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>`_.
+9
View File
@@ -0,0 +1,9 @@
check-manifest >= 0.34
twine >= 1.8.1
coverage >= 4.3.1
pytest-xdist >= 1.15.0
# Known good versions: 1.11.1
# Known broken versions: 1.15.0
setuptools-scm == 1.11.1
setuptools-scm-git-archive == 1.0
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 915 585" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="b" y2="445" gradientUnits="userSpaceOnUse" y1="179" gradientTransform="translate(0 -2.06)" x2="-29.7" x1="322">
<stop stop-color="#333" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="a" y2="414" gradientUnits="userSpaceOnUse" y1="159" x2="490" x1="815">
<stop stop-color="#33f" offset="0"/>
<stop stop-color="#3f3fff" stop-opacity="0" offset="1"/>
</linearGradient>
</defs>
<path fill="url(#b)" d="m403 247c-12 115-135 122-368 123-4.3-1.07-7.32-7.33-6-41 76-37 151-124 167-236 93 123 201 40.9 207 154z"/>
<g stroke-width="3.45" fill="none">
<path stroke="#000" d="m11.8 11.8h411v411l-411 0.01v-411z"/>
<path stroke="#448" d="m489 11.7h415v411h-415v-411z"/>
</g>
<path d="m876 244c-12 115-133 120-366 121-6-14-10-40-3-43 76-37.3 136-106 152-218 38 48 209 101 217 140z" fill="url(#a)"/>
<g id="RasterLarge" transform="matrix(1.36 0 0 1.28 -161 -636)">
<path fill="#999" d="m287 730h-30v-60h20v20h10v10h10v10h20v10h40v-10h20v-20h10v-20h-10v-10h-10v-10h-40v-10h-30v-10h-20v-10h-10v-10h-10v-60h10v-10h10v-10h10v-10h90v10h30v60h-20v-10h-10v-20h-10v-10h-20v-10h-30v10h-20v10h-10v20h10v20h30v10h30v10h30v10h10v10h10v10h10v60h-10v10h-10v10h-10v10h-100v-10z"/>
<path fill="#555" d="m297 730h-30v-10h-10v-50h20v20h10v20h10 10v10h60v-10h10v-10h10v-40h-20v-10h-20v-10h-40v-10h-20v-10h-20v-20h-10v-50h10v-10h10v-10h20v-10h70v10h30v10h10v50h-20v-20h-10v-20h-20v-10h-50v10h-10v10h-10v30h10v10h20v10h30v10h30v10h20v10h10v10h10v50h-10v20h-10v10h-20v10h-80z"/>
<path d="m307 730h-30v-10h-20v-50h20v30h10v10h10v10h70v-10h20v-50h-10v-10h-20v-10h-40v-10h-30v-10h-10v-10h-10v-20h-10v-30h10v-20h20v-10h20v-10h50v10h30v10h20v50h-20v-30h-10v-10h-10v-10h-70v10h-10v40h10v10h10v10h30v10h40v10h20v10h10v20h10v40h-10v20h-20v10h-30v10h-50v-10z"/>
</g>
<g font-size="40" font-family="sans-serif" text-anchor="middle">
<g font-size="100">
<text y="518" x="210">Raster</text>
<text y="518" x="695" fill="#338">Vector</text>
</g>
<text y="563" x="210">.jpeg .gif .png</text>
<text y="563" x="696" fill="#338">.svg</text>
</g>
<path id="VectorLarge" fill="#005" d="m661 294v-62.5l23.4 0.184c0.678 20.8 7.32 36.3 19.9 46.4 12.7 9.93 32.1 14.9 57.9 14.9 24.1 0 42.5-4.29 55.1-12.9 12.7-8.71 19.1-21.3 19.1-37.9 0-13.2-3.86-23.4-11.6-30.5-7.59-7.11-23.7-14-48.4-20.8l-40.1-10.9c-29-7.97-49.5-17.9-61.4-29.8-11.8-11.9-17.7-28.2-17.7-48.9 0-23.3 9.15-41.4 27.4-54.3 18.3-12.9 44-19.3 77.1-19.3 14.1 0 29.6 1.41 46.4 4.23 16.8 2.7 34.7 6.68 53.7 12v58.5h-23c-2.3-19.4-9.49-33.4-21.6-41.9-11.9-8.71-30.2-13.1-54.7-13.1-21.4 0-37.7 3.99-49 12-11.1 7.85-16.7 19.3-16.7 34.4 0 13.1 4.2 23.4 12.6 30.9 8.4 7.48 26.2 14.9 53.5 22.3l37.6 10.1c27.5 7.48 47.1 17 58.8 28.7 11.8 11.5 17.7 27 17.7 46.5 0 26.6-9.42 46.7-28.3 60.2s-46.9 20.2-84.2 20.2c-16.7 0-33.7-1.53-51-4.6-18-3-35-7-53-13z"/>
<use xlink:href="#VectorLarge" transform="matrix(.17 0 0 .17 392 313)"/>
<use xlink:href="#RasterLarge" transform="matrix(.173 0 0 .173 -8.25 314)" height="100%" width="100%" y="0" x="0"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Executable
+350
View File
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# ocrmypdf documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 4 14:29:43 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
# 'sphinx.ext.mathjax',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'ocrmypdf'
copyright = '2016, James R. Barlow'
author = 'James R. Barlow'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
from ocrmypdf import VERSION as OCRMYPDF_VERSION
_version_parts = OCRMYPDF_VERSION.split('.')
version = '.'.join(_version_parts[0:2])
# The full version, including alpha/beta/rc tags.
release = OCRMYPDF_VERSION
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
today_fmt = '%Y-%m-%d'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
import sphinx_rtd_theme
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'display_version': False
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = 'ocrmypdf v4.2'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'ocrmypdfdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation',
'James R. Barlow', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
author, 'ocrmypdf', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False
+153
View File
@@ -0,0 +1,153 @@
Cookbook
========
Basic examples
--------------
Help!
"""""
ocrmypdf has built-in help.
.. code-block:: bash
ocrmypdf --help
Add an OCR layer and convert to PDF/A
"""""""""""""""""""""""""""""""""""""
.. code-block:: bash
ocrmypdf input.pdf output.pdf
Add an OCR layer and output a standard PDF
""""""""""""""""""""""""""""""""""""""""""
.. code-block:: bash
ocrmypdf --output-type pdf input.pdf output.pdf
Modify a file in place
""""""""""""""""""""""
The file will only be overwritten if OCRmyPDF is successful.
.. code-block:: bash
ocrmypdf myfile.pdf myfile.pdf
Correct page rotation
"""""""""""""""""""""
OCR will attempt to automatic correct the rotation of each page. This can help fix a scanning job that contains a mix of landscape and portrait pages.
.. code-block:: bash
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive.
OCR languages other than English
""""""""""""""""""""""""""""""""
By default OCRmyPDF assumes the document is English.
.. code-block:: bash
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
OCR images, not PDFs
--------------------
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the results to run ocrmypdf:
.. code-block:: bash
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
If given a single image as input, OCRmyPDF will try converting it to a PDF on its own. This feature may be removed at some point, because OCRmyPDF does not specialize in converting images to PDFs.
You can also use Tesseract 3.04+ directly to convert single page images or multi-page TIFFs to PDF:
.. code-block:: bash
tesseract my-image.jpg output-prefix pdf
Image processing
----------------
OCRmyPDF perform some image processing on each page of a PDF, if desired. The same processing is applied to each page. It is suggested that the user review files after image processing as these commands might remove desirable content, especially from poor quality scans.
* ``--rotate-pages`` attempts to determine the correct orientation for each page and rotates the page if necessary.
* ``--remove-background`` attempts to detect and remove a noisy background from grayscale or color images. Monochrome images are ignored. This should not be used on documents that contain color photos as it may remove them.
* ``--deskew`` will correct pages were scanned at a skewed angle by rotating them back into place. Skew determination and correction is performed using `Postl's variance of line sums <http://www.leptonica.com/skew-measurement.html>`_ algorithm as implemented in `Leptonica <http://www.leptonica.com/index.html>`_.
* ``--clean`` uses `unpaper <https://www.flameeyes.eu/projects/unpaper>`_ to clean up pages before OCR, but does not alter the final output. This makes it less likely that OCR will try to find text in background noise.
* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important.
OCR and correct document skew (crooked scan)
""""""""""""""""""""""""""""""""""""""""""""
.. code-block:: bash
ocrmypdf --deskew input.pdf output.pdf
Hot (watched) folders
---------------------
To set up a "hot folder" that will trigger an OCR operation for every file inserted, use a program like Python `watchdog <https://pypi.python.org/pypi/watchdog>`_ (supports all major OS).
.. code-block:: bash
pip install watchdog
watchdog installs the command line program ``watchmedo``, which can be told to run ``ocrmypdf`` on any .pdf added to the current directory (``.``) and place the result in the previously created ``out/`` folder.
.. code-block:: bash
cd hot-folder
mkdir out
watchmedo shell-command \
--patterns="*.pdf" \
--ignore-directories \
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
. # don't forget the final dot
For more complex behavior you can write a Python script around to use the watchdog API.
On file servers, you could configure watchmedo as a system service so it will run all the time.
Caveats
"""""""
* ``watchmedo`` may not work properly on a networked file system, depending on the capabilities of the file system client and server.
* This simple recipe does not filter for the type of file system event, so file copies, deletes and moves, and directory operations, will all be sent to ocrmypdf, producing errors in several cases. Disable your watched folder if you are doing anything other than copying files to it.
* If the source and destination directory are the same, watchmedo may create an infinite loop.
Batch jobs
----------
Consider using the excellent `GNU Parallel <https://www.gnu.org/software/parallel/>`_ to apply OCRmyPDF to multiple files at once.
Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once.
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder.
.. code-block:: bash
parallel -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
If you have thousands of files to work with, contact the author.
+33
View File
@@ -0,0 +1,33 @@
Common error messages
=====================
Page already has text
---------------------
.. code::
ERROR - 1: page already has text! aborting (use --force-ocr to force OCR)
You ran ocrmypdf on a file that already contains printable text or a hidden OCR text layer (it can't quite tell the difference). You probably don't want to do this, because the file is already searchable.
As the error message suggests, your options are:
- ``ocrmypdf --force-ocr`` to :ref:`rasterize <raster-vector>` all vector content and run OCR on the images. This is useful if a previous OCR program failed, or if the document contains a text watermark.
- ``ocrmypdf --skip-text`` to skip OCR and other processing on any pages that contain text. Text pages will be copied into the output PDF without modification.
Input file 'filename' is not a valid PDF
----------------------------------------
OCRmyPDF passes files through qpdf, a program that fixes errors in PDFs, before it tries to work on them. In most cases this happens because the PDF is corrupt and
truncated (incomplete file copying) and not much can be done.
You can try rewriting the file with Ghostscript or pdftk:
- ``gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf``
- ``pdftk input.pdf cat output output.pdf``
Sometimes Acrobat can repair PDFs with its `Preflight tool <https://helpx.adobe.com/acrobat/using/correcting-problem-areas-preflight-tool.html>`_.
+34
View File
@@ -0,0 +1,34 @@
.. ocrmypdf documentation master file, created by
sphinx-quickstart on Sun Sep 4 14:29:43 2016.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
OCRmyPDF documentation
======================
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched.
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
Contents:
.. toctree::
:maxdepth: 2
introduction
installation
languages
cookbook
security
errors
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+273
View File
@@ -0,0 +1,273 @@
Installation
============
Installing on Debian and Ubuntu
-------------------------------
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
.. code-block:: bash
apt-get install ocrmypdf
.. _Docker:
Installing the Docker image
---------------------------
For many users, installing the Docker image will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install
a Docker image of the latest release.
Follow the Docker installation instructions for your platform. If you can run this command
successfully, your system is ready to download and execute the image:
.. code-block:: bash
docker run hello-world
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
.. code-block:: bash
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
Assuming you have a Docker engine running somewhere, you can run these commands to download
the image:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
Then tag it to give a more convenient name, just ocrmypdf:
.. code-block:: bash
docker tag jbarlow83/ocrmypdf ocrmypdf
.. _docker-polyglot:
This image contains language packs for English, French, Spanish and German. The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_:
.. code-block:: bash
# Alternative step: If you need all language packs
docker pull jbarlow83/ocrmypdf-polyglot
docker tag jbarlow83/ocrmypdf-polyglot ocrmypdf
You can then run ocrmypdf using the command:
.. code-block:: bash
docker run --rm ocrmypdf --help
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
.. note:: The working directory should be a writable local volume or Docker may not have permission to access it.
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
Installing on macOS (formerly Mac OS X)
---------------------------------------
These instructions probably work on all macOS supported by Homebrew. OCRmyPDF is known to work on Yosemite and El Capitan, and regularly tested on El Capitan.
If it's not already present, `install Homebrew <http://brew.sh/>`_.
Update Homebrew:
.. code-block:: bash
brew update
Install or upgrade the required Homebrew packages, if any are missing:
.. code-block:: bash
brew install libpng openjpeg jbig2dec libtiff # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2 libffi leptonica
brew install unpaper # optional
Python 3.4, 3.5 and 3.6 are supported.
Install the required Tesseract OCR engine with the language packs you plan to use:
.. code-block:: bash
brew install tesseract # Option 1: for English, French, German, Spanish
.. _macos-all-languages:
.. code-block:: bash
brew install tesseract --with-all-languages # Option 2: for all language packs
Update the homebrew pip and install Pillow:
.. code-block:: bash
pip3 install --upgrade pip
pip3 install --upgrade pillow
You can then install OCRmyPDF from PyPI:
.. code-block:: bash
pip3 install ocrmypdf
The command line program should now be available:
.. code-block:: bash
ocrmypdf --help
Installing on Ubuntu 14.04 LTS
------------------------------
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
.. code-block:: bash
sudo add-apt-repository ppa:vshn/ghostscript -y
sudo add-apt-repository ppa:heyarje/libav-11 -y
Update apt-get:
.. code-block:: bash
sudo apt-get update
sudo apt-get upgrade
Install system dependencies:
.. code-block:: bash
sudo apt-get install \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
libavformat56 libavcodec56 libavutil54 \
ghostscript \
tesseract-ocr \
qpdf \
python3-pip \
python3-pil \
python3-pytest \
python3-reportlab
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
into your system Python, which could interfere with other programs):
.. code-block:: bash
sudo pip3 install ocrmypdf
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
.. code-block:: bash
sudo apt-get install python3-venv
python3 -m venv venv-ocrmypdf --without-pip
source venv-ocrmypdf/bin/activate
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
deactivate
python3 -m venv --system-site-packages venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip install ocrmypdf
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
sudo dpkg -i unpaper_6.1-1.deb
Installing on Windows
---------------------
Direct installation on Windows is not possible. Install the _`Docker` container as described above. Ensure that your command prompt can run the docker "hello world" container.
Running on Windows
~~~~~~~~~~~~~~~~~~
The command line syntax to run ocrmypdf from a command prompt will resemble:
.. code-block:: bat
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
Installing HEAD revision from sources
-------------------------------------
If you have ``git`` and ``python3.4`` or ``python3.5`` installed, you can install from source. When the ``pip`` installer runs,
it will alert you if dependencies are missing.
To install the HEAD revision from sources in the current Python 3 environment:
.. code-block:: bash
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
On certain Linux distributions such as Ubuntu, you may need to use
run the install command as superuser:
.. code-block:: bash
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
Note that this will alter your system's Python distribution. If you prefer
to not install as superuser, you can install the package in a Python virtual environment:
.. code-block:: bash
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
python3 -m venv
source venv/bin/activate
cd OCRmyPDF
pip3 install .
However, ``ocrmypdf`` will only be accessible on the system PATH after
you activate the virtual environment.
To run the program:
.. code-block:: bash
ocrmypdf --help
If not yet installed, the script will notify you about dependencies that
need to be installed. The script requires specific versions of the
dependencies. Older version than the ones mentioned in the release notes
are likely not to be compatible to OCRmyPDF.
+76
View File
@@ -0,0 +1,76 @@
Introduction
============
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
About OCR
---------
`Optical character recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`_ is technology that converts images of typed or handwritten text, such as in a scanned document, to computer text that can be searched and copied.
OCRmyPDF uses `Tesseract <https://github.com/tesseract-ocr/tesseract>`_, the best available open source OCR engine, to perform OCR.
.. _raster-vector:
About PDFs
----------
PDFs are page description files that attempts to preserve a layout exactly. They can contain `vector graphic files <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
.. image:: bitmap_vs_svg.svg
A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page.
Rasterizing a PDF is the process of generating an image suitable for display or analyzing with an OCR engine. OCR engines like Tesseract work with images, not vector objects.
About PDF/A
-----------
`PDF/A <https://en.wikipedia.org/wiki/PDF/A>`_ is an ISO-standardized subset of the full PDF specification that is designed for archiving (the 'A' stands for Archive). PDF/A differs from PDF primarily by omitting features that would make it difficult to read the file in the future, such as embedded Javascript, video, audio and references to external fonts. All fonts and resources needed to interpret the PDF must be contained within it. Because PDF/A disables Javascript and other types of embedded content, it is probably more secure.
There are various conformance levels and versions, such as "PDF/A-2b".
Generally speaking, the best format for scanned documents is PDF/A. Some governments and jurisdictions, US Courts in particular, `mandate the use of PDF/A <https://pdfblog.com/2012/02/13/what-is-pdfa/>`_ for scanned documents.
Since most people who scan documents are interested in reading them indefinitely into the future, OCRmyPDF generates PDF/A-2b by default.
PDF/A has a few drawbacks. Some PDF viewers include an alert that the file is a PDF/A, which may confuse some users. It also tends to produce larger files than PDF, because it embeds certain resources even if they are commonly available. PDF/A files can be digitally signed, but may not be encrypted, to ensure they can be read in the future. Fortunately, converting from PDF/A to a regular PDF is trivial, and any PDF viewer can view PDF/A.
What OCRmyPDF does
------------------
OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript <http://ghostscript.com/>`_ to rasterize the page, and then performs on OCR on the rasterized image. It is not enough to simply extract the images from each page and run OCR on them individually. Of course one could use Ghostscript or another PDF rasterizer and then pass the image to Tesseract. OCRmyPDF automates this process and produces a minimally changed output file that contains the same information, colorspace and resolution.
The Tesseract OCR engine can output 'hOCR' files, which are XML files that contain a description of the text it found on the page. OCRmyPDF will render a new PDF that contains only the hidden text layer, and merge this with the original page.
Alternately, OCRmyPDF can use the Tesseract OCR engine to directly output PDFs for each page, then merge them.
By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be disabled with the ``--output-type pdf`` argument.
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
Limitations
-----------
OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these limitations, as do any other programs that rely on Tesseract:
* The OCR is not as accurate as commercial solutions such as Abbyy.
* It is not capable of recognizing handwriting.
* It may find gibberish and report this as OCR output.
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
OCRmyPDF is also limited by the PDF specification:
* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others.
Ghostscript also imposes some limitations:
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
+48
View File
@@ -0,0 +1,48 @@
.. _lang-packs:
Installing additional language packs
====================================
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English.
Tesseract supports `most languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
For Linux users, you can often find packages that provide language packs:
Debian and Ubuntu users
-----------------------
.. code-block:: bash
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
languages can be requested using either ``-l eng+fre`` (English and French) or ``-l eng -l fre``.
Mac OS X (macOS) users
----------------------
You can install additional language packs by :ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
Docker users
------------
Users of the Docker image may use the alternative :ref:`"polyglot" container <docker-polyglot>` which includes all languages.
Known limitations
-----------------
As of v4.2, users of ocrmypdf working languages outside the Latin alphabet should use the following syntax:
.. code-block:: bash
ocrmypdf -l eng+gre --output-type pdf --pdf-renderer tesseract
The reasons for this are:
* The latest version of Ghostscript (9.19 as of this writing) has unfixed bugs in Unicode handling that generate invalid character maps, so Ghostscript cannot be used for PDF/A conversion
* The default "hocr" PDF renderer does not handle Asian fonts properly
+44
View File
@@ -0,0 +1,44 @@
PDF Security Issues
===================
OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware.
Recognizing that many users have an interest in handling PDFs and applying OCR to PDFs they did not generate themselves, this article discusses the security implications of PDFs and how users can protect themselves.
The disclaimer applies: this software has no warranties of any kind.
PDFs may contain malware
------------------------
PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of packages long and references several annexes each of which are similar in length. PDFs can contain video, audio, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors.
In short, PDFs `may contain viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`_.
This `article <https://theinvisiblethings.blogspot.ca/2013/02/converting-untrusted-pdfs-into-trusted.html>`_ describes a method which allows potentially hostile PDFs to be viewed and rasterized safely in a disposable virtual machine. A trusted PDF created in this manner is converted to images and loses all information making it searchable. OCRmyPDF could be used restore searchability.
How OCRmyPDF processes PDFs
---------------------------
OCRmyPDF must open and interpret your PDF in order to insert an OCR layer. First, it runs all PDFs through `qpdf <https://github.com/qpdf/qpdf>`_, a program that repairs PDFs with syntax errors. This is done because, in the author's experience, a significant number of PDFs in the wild especially those created by scanners are not well-formed files. qpdf makes it more likely that OCRmyPDF will succeed, but offers no security guarantees. qpdf is also used to split the PDF into single page PDFs.
After qpdf, OCRmyPDF examines each page using `PyPDF2 <https://github.com/mstamy2/PyPDF2>`_. This library also has no warranties or guarantees.
Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://ghostscript.com/>`_ in ``-dSAFER`` mode.
Depending on the options specified, OCRmyPDF may graft the OCR layer into the existing PDF or it may essentially reconstruct ("re-fry") a visually identical PDF that may be quite different at the binary level. That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
Using OCRmyPDF online
---------------------
OCRmyPDF is not designed to be deployed "as a service", in a setting where a user/attacker could upload a file for OCR processing online. It is not designed to be secure in this case.
Abbyy Cloud OCR is a viable commercial alternative with a web services API. The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation.
Password protection, digital signatures and certification
---------------------------------------------------------
OCRmyPDF cannot remove password protection from a PDF. ``qpdf``, one of its dependencies, has this capability. After OCR is applied, password protection is not permitted on PDF/A documents but the file can be converted to regular PDF.
Many programs exist which are capable of inserting an image of someone's signature. On its own, this offers no security guarantees. It is trivial to remove the signature image and apply it to other files. This practice offers no real security.
Important documents can be digitally signed and certified to attest to their authorship. OCRmyPDF cannot do this. Open source tools such as pdfbox (Java) have this capability as does Adobe Acrobat.
+12 -1
View File
@@ -1,5 +1,9 @@
from enum import IntEnum
import os
from collections.abc import Iterable
import pkg_resources
VERSION = pkg_resources.get_distribution('ocrmypdf').version
class ExitCode(IntEnum):
@@ -7,11 +11,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 +27,8 @@ 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)
+1557
View File
File diff suppressed because it is too large Load Diff
+33 -11
View File
@@ -2,7 +2,7 @@
# © 2015 James R. Barlow: github.com/jbarlow83
from tempfile import NamedTemporaryFile
from subprocess import Popen, PIPE, check_call
from subprocess import Popen, PIPE, STDOUT, check_call
from shutil import copy
from . import get_program
from .pdfa import SRGB_ICC_PROFILE
@@ -14,23 +14,24 @@ 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,
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
'-o', tmp.name,
'-r{0}x{1}'.format(str(xres), str(yres)),
'-r{0}x{1}'.format(str(round(xres)), str(round(yres))),
input_file
]
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=PIPE,
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
stdout, stderr = p.communicate()
if stdout:
stdout, _ = p.communicate()
if 'error' in stdout:
log.error(stdout) # Ghostscript puts errors in stdout
else:
log.debug(stdout)
if stderr:
log.error(stderr)
if p.returncode == 0:
copy(tmp.name, output_file)
@@ -38,7 +39,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
log.error('Ghostscript rendering failed')
def generate_pdfa(pdf_pages, output_file, threads=1):
def generate_pdfa(pdf_pages, output_file, log, threads=1):
with NamedTemporaryFile(delete=True) as gs_pdf:
args_gs = [
get_program("gs"),
@@ -52,9 +53,30 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
"-sProcessColorModel=DeviceRGB",
"-dJPEGQ=95",
"-dPDFA=2",
"-sPDFACompatibilityPolicy=2",
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
args_gs.extend(pdf_pages)
check_call(args_gs)
copy(gs_pdf.name, output_file)
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
stdout, _ = p.communicate()
if 'error' in stdout:
log.error(stdout)
elif 'overprint mode not set' in stdout:
# Unless someone is going to print PDF/A documents on a
# magical sRGB printer I can't see the removal of overprinting
# being a problem....
log.debug(
"Ghostscript had to remove PDF 'overprinting' from the "
"input file to complete PDF/A conversion. "
)
else:
log.debug(stdout)
if p.returncode == 0:
# Ghostscript does not change return code when it fails to create
# PDF/A - check PDF/A status elsewhere
copy(gs_pdf.name, output_file)
else:
log.error('Ghostscript PDF/A failed')
+300 -37
View File
@@ -1,14 +1,10 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# © 2013-15: jbarlow83 from Github (https://github.com/jbarlow83)
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
#
#
# Use Leptonica to detect find and remove page skew. Leptonica uses the method
# of differential square sums, which its author claim is faster and more robust
# than the Hough transform used by ImageMagick.
# Python FFI wrapper for Leptonica library
from __future__ import print_function, absolute_import, division
import argparse
import sys
import os
@@ -17,6 +13,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'))
@@ -43,22 +40,34 @@ class LeptonicaErrorTrap(object):
"""
def __enter__(self):
from io import UnsupportedOperation
self.tmpfile = TemporaryFile()
# Save the old stderr, and redirect stderr to temporary file
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno())
sys.stderr.flush()
try:
self.copy_of_stderr = os.dup(sys.stderr.fileno())
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(),
inheritable=False)
except UnsupportedOperation:
self.copy_of_stderr = None
return
def __exit__(self, exc_type, exc_value, traceback):
# Restore old stderr
os.dup2(self.old_stderr_fileno, sys.stderr.fileno())
sys.stderr.flush()
if self.copy_of_stderr is not None:
os.dup2(self.copy_of_stderr, sys.stderr.fileno())
os.close(self.copy_of_stderr)
# Get data from tmpfile (in with block to ensure it is closed)
with self.tmpfile as tmpfile:
tmpfile.seek(0) # Cursor will be at end, so move back to beginning
leptonica_output = tmpfile.read().decode(errors='replace')
assert self.tmpfile.closed
assert not sys.stderr.closed
# If there are Python errors, let them bubble up
if exc_type:
logger.warning(leptonica_output)
@@ -83,6 +92,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.
@@ -96,29 +112,55 @@ class Pix:
Leptonica's reference counting is not threadsafe. This class can be used
in a threadsafe manner if a Python threading.Lock protects the data.
This class treats Pix objects as immutable. All methods return new
modified objects. This allows convenient chaining:
>>> Pix.read('filename.jpg').scale((0.5, 0.5)).deskew().show()
"""
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 _repr_png_(self):
"""iPython display hook
returns png version of image
"""
data = ffi.new('l_uint8 **')
size = ffi.new('size_t *')
err = lept.pixWriteMemPng(data, size, self._pix, 0)
if err != 0:
raise LeptonicaIOError("pixWriteMemPng")
char_data = ffi.cast('char *', data[0])
return ffi.buffer(char_data, size[0])[:]
def __getstate__(self):
data = ffi.new('l_uint32 **')
size = ffi.new('size_t *')
err = lept.pixSerializeToMemory(self.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 +168,44 @@ 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)
def __eq__(self, other):
return self.__getstate__() == other.__getstate__()
@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 +228,40 @@ 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
# Leptonica manages data in words, so it implicitly does an endian
# swap. Tell Pillow about this when it reads the data.
pix = self
if sys.byteorder == 'little':
if self.mode == 'RGB':
raw_mode = 'XBGR'
elif self.mode == 'RGBA':
raw_mode = 'ABGR'
elif self.mode == '1':
raw_mode = '1;I'
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
else:
raw_mode = self.mode
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
else:
raw_mode = self.mode # no endian swap needed
size = (pix._pix.w, pix._pix.h)
bytecount = pix._pix.wpl * 4 * pix._pix.h
buf = ffi.buffer(pix._pix.data, bytecount)
stride = pix._pix.wpl * 4
im = Image.frombytes(self.mode, size, buf, 'raw', raw_mode, stride)
return im
def show(self):
return self.topil().show()
def deskew(self, reduction_factor=0):
"""Returns the deskewed pix object.
@@ -170,16 +273,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):
def scale(self, scale_xy):
"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, scale_xy[0], scale_xy[1]))
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,31 +297,132 @@ 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_cpix = ffi.new('PIX **')
p_pix = ffi.new('PIX **')
result = lept.pixOtsuAdaptiveThreshold(
self.cpix,
self._pix,
sx, sy,
smoothx, smoothy,
scorefract,
ffi.NULL,
p_cpix)
p_pix)
if result == 0:
return Pix(p_cpix[0])
return Pix(p_pix[0])
else:
return None
def otsu_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
bgval=255, kernel_size=(2, 2), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
if mask is None:
mask = ffi.NULL
if isinstance(mask, Pix):
mask = mask._pix
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
self._pix,
mask,
sx, sy,
thresh, mincount, bgval,
smoothx, smoothy,
scorefract,
ffi.NULL
)
if thresh_pix == ffi.NULL:
return None
return Pix(thresh_pix)
def crop_to_foreground(
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
showmorph=0, display=0, pdfdir=ffi.NULL):
with LeptonicaErrorTrap():
cropbox = Box(lept.pixFindPageForeground(
self._pix,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir))
print(repr(cropbox))
cropped_pix = lept.pixClipRectangle(
self._pix,
cropbox._box,
ffi.NULL)
return Pix(cropped_pix)
def clean_background_to_white(
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
with LeptonicaErrorTrap():
return Pix(lept.pixCleanBackgroundToWhite(
self._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white))
def gamma_trc(self, gamma=1.0, minval=0, maxval=255):
with LeptonicaErrorTrap():
return Pix(lept.pixGammaTRC(
ffi.NULL,
self._pix,
gamma,
minval,
maxval
))
def background_norm(
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
with LeptonicaErrorTrap():
return Pix(lept.pixBackgroundNorm(
self._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
tile_size[0],
tile_size[1],
fg_threshold,
min_count,
bg_val,
smooth_kernel[0],
smooth_kernel[1]
))
@staticmethod
@lru_cache(maxsize=1)
def make_pixel_sum_tab8():
@@ -224,16 +433,17 @@ class Pix:
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.
# 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.
@@ -244,7 +454,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")
@@ -252,11 +462,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.
@@ -286,6 +533,22 @@ def deskew(infile, outfile, dpi):
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
black_threshold=70, white_threshold=190):
try:
pix = Pix.read(infile)
except LeptonicaIOError:
raise LeptonicaIOError("Failed to open file: %s" % infile)
pix = pix.background_norm(tile_size=tile_size).gamma_trc(
gamma, black_threshold, white_threshold)
try:
pix.write_implied_format(outfile)
except LeptonicaIOError:
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Python wrapper to access Leptonica")
+112
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("""
@@ -49,11 +61,24 @@ PIX * pixRead ( const char *filename );
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
l_int32
pixWriteMemPng(l_uint8 **pdata,
size_t *psize,
PIX *pix,
l_float32 gamma);
void pixDestroy ( PIX **ppix );
PIX *
pixEndianByteSwapNew(PIX *pixs);
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
char * getLeptonicaVersion ( );
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
PIX *pixRotate180(PIX *pixd, PIX *pixs);
PIX *
pixRotateOrth(PIX *pixs,
l_int32 quads);
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
@@ -62,6 +87,10 @@ 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,
@@ -72,6 +101,89 @@ pixOtsuAdaptiveThreshold(PIX *pixs,
PIX **ppixth,
PIX **ppixd);
PIX *
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
PIX *pixim,
l_int32 sx,
l_int32 sy,
l_int32 thresh,
l_int32 mincount,
l_int32 bgval,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
l_int32 *pthresh);
PIX *
pixCleanBackgroundToWhite(PIX *pixs,
PIX *pixim,
PIX *pixg,
l_float32 gamma,
l_int32 blackval,
l_int32 whiteval);
BOX *
pixFindPageForeground(PIX *pixs,
l_int32 threshold,
l_int32 mindist,
l_int32 erasedist,
l_int32 pagenum,
l_int32 showmorph,
l_int32 display,
const char *pdfdir);
PIX *
pixClipRectangle(PIX *pixs,
BOX *box,
BOX **pboxc);
PIX *
pixBackgroundNorm(PIX *pixs,
PIX *pixim,
PIX *pixg,
l_int32 sx,
l_int32 sy,
l_int32 thresh,
l_int32 mincount,
l_int32 bgval,
l_int32 smoothx,
l_int32 smoothy);
PIX *
pixGammaTRC(PIX *pixd,
PIX *pixs,
l_float32 gamma,
l_int32 minval,
l_int32 maxval);
l_int32
pixNumSignificantGrayColors(PIX *pixs,
l_int32 darkthresh,
l_int32 lightthresh,
l_float32 minfract,
l_int32 factor,
l_int32 *pncolors);
l_int32
pixColorFraction(PIX *pixs,
l_int32 darkthresh,
l_int32 lightthresh,
l_int32 diffthresh,
l_int32 factor,
l_float32 *ppixfract,
l_float32 *pcolorfract);
PIX *
pixColorMagnitude(PIX *pixs,
l_int32 rwhite,
l_int32 gwhite,
l_int32 bwhite,
l_int32 type);
void
boxDestroy(BOX **pbox);
void lept_free(void *ptr);
""")
Executable → Regular
+4 -1155
View File
File diff suppressed because it is too large Load Diff
+113 -24
View File
@@ -2,13 +2,21 @@
# © 2015 James R. Barlow: github.com/jbarlow83
from subprocess import Popen, PIPE
from decimal import Decimal, getcontext
from decimal import Decimal
from math import hypot
import re
import sys
import PyPDF2 as pypdf
from collections import namedtuple
try:
from math import isclose
except ImportError:
def isclose(a, b, rel_tol=1e-9):
"Python 3.4 does not have math.isclose()"
diff = abs(b - a)
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
matrix_mult = pypdf.pdf.utils.matrixMultiply
FRIENDLY_COLORSPACE = {
@@ -52,6 +60,9 @@ FRIENDLY_COMP = {
}
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
def _matrix_from_shorthand(shorthand):
"""Convert from PDF matrix shorthand to full matrix
@@ -73,13 +84,46 @@ def _shorthand_from_matrix(matrix):
return tuple(map(float, (a, b, c, d, e, f)))
def _is_unit_square(shorthand):
values = map(float, shorthand)
pairwise = zip(values, UNIT_SQUARE)
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
RasterSettings = namedtuple('RasterSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['settings', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
def _interpret_contents(contentstream):
"""Interpret the PDF content stream
The stack represents the state of the PDF graphics stack. We are only
interested in the current transformation matrix (CTM) so we only track
this object; a full implementation would need to track many other items.
The CTM is initialized to the mapping from user space to device space.
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
to the transformation to device space. For example if set to
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
image there should be a 'cm' that sets up an image coordinate system
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
page.
PDF units suit our needs so we initialize ctm to the identity matrix.
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
"""
operations = contentstream.operations
stack = []
ctm = _matrix_from_shorthand((1, 0, 0, 1, 0, 0))
ctm = _matrix_from_shorthand(UNIT_SQUARE)
image_raster_settings = []
inline_images = []
@@ -93,15 +137,19 @@ def _interpret_contents(contentstream):
ctm = stack.pop()
elif command == b'cm':
ctm = matrix_mult(
ctm, _matrix_from_shorthand(operands))
_matrix_from_shorthand(operands), ctm)
elif command == b'Do':
image_name = operands[0]
image_raster_settings.append(
(image_name, _shorthand_from_matrix(ctm)))
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_images.append(
(settings, _shorthand_from_matrix(ctm)))
inline = InlineSettings(
settings=settings, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
inline_images.append(inline)
return ContentsInfo(
raster_settings=image_raster_settings,
@@ -175,24 +223,30 @@ def _get_dpi(ctm_shorthand, image_size):
def _find_page_inline_images(page, pageinfo, contentsinfo):
"Find inline images on the page"
for n, im in enumerate(contentsinfo.inline_images):
settings, shorthand = im
for n, inline in enumerate(contentsinfo.inline_images):
image = {}
image['name'] = str('inline-%02d' % n)
image['width'] = settings['/W']
image['height'] = settings['/H']
image['bpc'] = settings['/BPC']
image['color'] = FRIENDLY_COLORSPACE.get(settings['/CS'], '-')
image['width'] = inline.settings['/W']
image['height'] = inline.settings['/H']
if '/BPC' in inline.settings:
image['bpc'] = inline.settings['/BPC']
else:
image['bpc'] = 8
if '/CS' in inline.settings:
image['color'] = FRIENDLY_COLORSPACE.get(inline.settings['/CS'], '-')
else:
image['color'] = '-'
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
if '/F' in settings:
filter_ = settings['/F']
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(shorthand, (image['width'], image['height']))
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
@@ -213,7 +267,21 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
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):
@@ -230,25 +298,42 @@ def _find_page_regular_images(page, pageinfo, contentsinfo):
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
# Bit of a hack... infer grayscale if component count is uncertain
# but encoding must be monochrome. This happens if a monochrome image
# has an ICC profile attached. Better solution would be to examine
# the ICC profile.
if image['comp'] == '?' and image['enc'] in ('ccitt', 'jbig2'):
image['comp'] = FRIENDLY_COMP['gray']
image['dpi_w'] = image['dpi_h'] = 0
for raster in contentsinfo.raster_settings:
# Loop in case the same image is display multiple times on a page
if raster[0] != image['name']:
if raster.name != image['name']:
continue
if raster.stack_depth == 0 and _is_unit_square(raster.shorthand):
# At least one PDF in the wild (and test suite) draws an image
# when the graphics stack depth is 0, meaning that the image
# gets drawn into a square of 1x1 PDF units (or 1/72",
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
# these from our DPI calculation for the page.
continue
shorthand = raster[1]
dpi_w, dpi_h = _get_dpi(
shorthand, (image['width'], image['height']))
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)
DPI_PREC = Decimal('1.000')
dpi = Decimal(image['dpi_w'] * image['dpi_h']).sqrt()
image['dpi_w'] = Decimal(image['dpi_w']).quantize(DPI_PREC)
image['dpi_h'] = Decimal(image['dpi_h']).quantize(DPI_PREC)
image['dpi'] = dpi.quantize(DPI_PREC)
yield image
@@ -294,6 +379,11 @@ def _pdf_get_pageinfo(infile, pageno: int):
pageinfo['width_inches'] = width_pt / Decimal(72.0)
pageinfo['height_inches'] = height_pt / Decimal(72.0)
try:
pageinfo['rotate'] = int(page['/Rotate'])
except KeyError:
pageinfo['rotate'] = 0
try:
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
except AttributeError as e:
@@ -317,7 +407,6 @@ def _pdf_get_pageinfo(infile, pageno: int):
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)]
+60 -11
View File
@@ -7,6 +7,7 @@ from __future__ import print_function, absolute_import, division
from string import Template
import codecs
import pkg_resources
import PyPDF2 as pypdf
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
@@ -69,13 +70,20 @@ def encode_text_string(s: str) -> str:
From PDF 32000-1:2008 a string object may be included in hexademical form
if it is enclosed in angle brackets. For general Unicode the string should
be UTF-16 (big endian) with byte order marks. A non-hexademical
representation is doable but this is preferable since it allows the output
Postscript file to be completely ASCII and no escaping of Postscript
characters is necessary.
be UTF-16 (big endian) with byte order marks. Many strings including all
ASCII strings fall could be encoded as PdfDocEncoding literals provided
that certain Postscript sequences are escaped. But it's far simpler to
encode everything as UTF-16.
'''
# Sometimes lazy C programmers leave their NULs at the end of strings they
# insert into PDFs
# tests/resources/aspect.pdf is one example (created by ImageMagick)
s = s.replace('\x00', '')
if s == '':
return ''
utf16_bytes = s.encode('utf-16be')
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
@@ -88,11 +96,11 @@ 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
@@ -104,7 +112,48 @@ def generate_pdfa_def(target_filename, pdfmark, icc='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}
if not pdfa_dict:
return {'pass': False, 'output': 'pdf',
'conformance': 'No XMP metadata'}
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
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)
+12 -11
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(
@@ -151,11 +147,16 @@ def page_timedout(log, input_file):
log.warning(prefix + " took too long to OCR - skipping")
def _generate_null_hocr(output_hocr, pageinfo):
def _generate_null_hocr(output_hocr, image):
"""Produce a .hocr file that reports no text detected on a page that is
the same size as the input image."""
from PIL import Image
im = Image.open(image)
w, h = im.size
with open(output_hocr, 'w', encoding="utf-8") as f:
f.write(HOCR_TEMPLATE.format(
pageinfo['width_pixels'],
pageinfo['height_pixels']))
f.write(HOCR_TEMPLATE.format(w, h))
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
@@ -185,11 +186,11 @@ def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
# Temporary workaround to hocrTransform not being able to function if
# it does not have a valid hOCR file.
page_timedout(log, input_file)
_generate_null_hocr(output_hocr, pageinfo_getter())
_generate_null_hocr(output_hocr, input_file)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if 'Image too large' in e.output:
_generate_null_hocr(output_hocr, pageinfo_getter())
_generate_null_hocr(output_hocr, input_file)
return
raise e from e
+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
+8 -5
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
reportlab==3.2.0
PyPDF2==1.25.1
img2pdf==0.2
cffi==1.5.2
Pillow==4.0.0
reportlab==3.3.0
PyPDF2==1.26
img2pdf==0.2.1
cffi==1.9.1
+6 -1
View File
@@ -1,4 +1,9 @@
[bdist_wheel]
python-tag = py34
[aliases]
test=pytest
test=pytest
[check-manifest]
ignore =
.github
+21 -15
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
@@ -148,32 +148,35 @@ def check_external_program(
command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
forced = ('--force' in sys.argv)
if command.startswith('install') or \
if not forced and command.startswith('install') or \
command in ['check', 'test', 'nosetests', 'easy_install']:
check_external_program(
program='tesseract',
need_version='3.02.02',
need_version='3.03', # limited by Travis CI / Ubuntu 12.04 backports
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
)
check_external_program(
program='gs',
need_version='9.14',
need_version='9.15', # limited by Travis CI / Ubuntu 12.04 backports
package='ghostscript'
)
check_external_program(
program='unpaper',
need_version='6.1',
need_version='6.1', # latest sane version
package='unpaper',
optional=True
)
check_external_program(
program='qpdf',
need_version='5.0.0',
need_version='5.0.0', # limited by Travis CI / Ubuntu 12.04 backports
package='qpdf',
version_check_args=['--version']
)
else:
print('Skipping external program tests because of --force')
if 'upload' in sys.argv[1:]:
@@ -188,17 +191,20 @@ setup(
url='https://github.com/jbarlow83/OCRmyPDF',
author='James R. Barlow',
author_email='jim@purplerock.ca',
license='Public Domain',
license='MIT',
packages=['ocrmypdf'],
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"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",
@@ -217,17 +223,17 @@ 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']},
+1 -1
View File
@@ -1 +1 @@
pytest>=2.7.2
pytest >= 2.7.2
Binary file not shown.
+72 -26
View File
@@ -9,21 +9,32 @@ 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
* - typewriter.png, 2400dpi.pdf
- `Wikimedia: Triumph typewrtier text Linzensoep`_
* Creative Commons BY-SA 2.5
Files generated for this project
================================
@@ -31,28 +42,59 @@ 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
==========
These test resources are assemblies from other previously mentioned files, released under the same license terms as their input files.
- cardinal.pdf (four cardinal directions, rotated copies of LinnSequencer.jpg)
- cardinal.pdf (four cardinal directions, baked-in 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)
- rotated_skew.pdf (a /Rotate'd and skewed document from LinnSequencer.jpg)
- skew.pdf (from LinnSequencer.jpg, skew simulated by adjusting the transformation matrix)
- 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 +105,8 @@ 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
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import sys
import os
from subprocess import check_call
"""Replicate one type of Ghostscript feature elision warning during
PDF/A creation."""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
not permitted in PDF/A-2, overprint mode not set"""
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
sys.exit(0)
gs_args = ['gs'] + sys.argv[1:]
check_call(gs_args)
if '-sDEVICE=pdfwrite' in sys.argv[1:]:
print(elision_warning)
sys.exit(0)
if __name__ == '__main__':
main()
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import sys
import os
"""Replicate Ghostscript PDF/A conversion failure by suppressing some
arguments"""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
sys.exit(0)
# Unless some argument is calling for PDFA generation, forward to
# real ghostscript
if not any(arg.startswith('-dPDFA') for arg in sys.argv):
real_ghostscript(sys.argv)
return
# Remove the two arguments that tell ghostscript to create a PDF/A
# Does not remove the Postscript definition file - not necessary
# to cause PDF/A creation failure
argv = []
for arg in sys.argv:
if arg.startswith('-dPDFA'):
continue
elif arg.startswith('-dPDFACompatibilityPolicy'):
continue
argv.append(arg)
real_ghostscript(argv)
if __name__ == '__main__':
main()
+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
"""
+38 -14
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'))
@@ -29,11 +48,17 @@ def main():
m = hashlib.sha1()
version = subprocess.check_output(
tess_version = subprocess.check_output(
['tesseract', '--version'],
stderr=subprocess.STDOUT)
m.update(version)
m.update(tess_version)
# Insert this source file into the hash function, to ensure that any
# changes to this file invalidate previous hashes
with open(__file__, 'rb') as f:
m.update(f.read())
m.update(operation.encode())
try:
@@ -74,10 +99,10 @@ def main():
shutil.copy(cache_name, output_file)
# Replicate output
with open(cache_name + '.stdout', 'r') as f:
print(f.read(), end='')
with open(cache_name + '.stderr', 'r') as f:
print(f.read(), end='', file=sys.stderr)
with open(cache_name + '.stdout', 'rb') as f:
sys.stdout.buffer.write(f.read())
with open(cache_name + '.stderr', 'rb') as f:
sys.stderr.buffer.write(f.read())
sys.exit(0)
# Cache miss
@@ -86,23 +111,22 @@ def main():
# Call tesseract
p = subprocess.Popen(
['tesseract'] + sys.argv[1:],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True)
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
# Do not cache errors or crashes
print("Tesseract error", file=sys.stderr)
print(stdout, end='')
print(stderr, end='', file=sys.stderr)
sys.stdout.buffer.write(stdout)
sys.stderr.buffer.write(stderr)
return p.returncode
with open(cache_name + '.stdout', 'w') as f:
with open(cache_name + '.stdout', 'wb') as f:
f.write(stdout)
with open(cache_name + '.stderr', 'w') as f:
with open(cache_name + '.stderr', 'wb') as f:
f.write(stderr)
print(stdout, end='')
print(stderr, end='', file=sys.stderr)
sys.stdout.buffer.write(stdout)
sys.stderr.buffer.write(stderr)
# Insert file into cache
if output_file != 'stdout':
+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
+279 -71
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,35 @@ 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)
print(err) # ensure py.test collects the output, use -s to view
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"
assert out == "", \
"The following was written to stdout and should not have been: \n" + \
"<stdout>\n" + out + "\n</stdout>"
return output_file
def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
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)
@@ -88,45 +91,60 @@ def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
return p, out, err
def spoof(replace_program, with_spoof):
def spoof(**kwargs):
"""Modify environment variables to override subprocess executables
spoof(program1='replacement', ...)
Before running any executable, ocrmypdf checks the environment variable
OCRMYPDF_PROGRAMNAME to override default program name/location, e.g.
OCRMYPDF_GS redirects from the system path Ghostscript ("gs") to elsewhere.
"""
env = os.environ.copy()
spoofer = os.path.join(SPOOF_PATH, with_spoof)
if not os.access(spoofer, os.X_OK):
os.chmod(spoofer, 0o755)
env['OCRMYPDF_' + replace_program.upper()] = spoofer
for replace_program, with_spoof in kwargs.items():
spoofer = os.path.join(SPOOF_PATH, with_spoof)
if not os.access(spoofer, os.X_OK):
os.chmod(spoofer, 0o755)
env['OCRMYPDF_' + replace_program.upper()] = spoofer
return env
@pytest.fixture
def spoof_tesseract_noop():
return spoof('tesseract', 'tesseract_noop.py')
return spoof(tesseract='tesseract_noop.py')
@pytest.fixture
def spoof_tesseract_cache():
if running_in_docker():
return os.environ.copy()
return spoof('tesseract', "tesseract_cache.py")
return spoof(tesseract="tesseract_cache.py")
@pytest.fixture
def spoof_tesseract_crash():
return spoof('tesseract', 'tesseract_crash.py')
return spoof(tesseract='tesseract_crash.py')
@pytest.fixture
def spoof_tesseract_big_image_error():
return spoof('tesseract', 'tesseract_big_image_error.py')
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)
@pytest.fixture
def spoof_no_tess_no_pdfa():
return spoof(tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py')
@pytest.fixture
def spoof_no_tess_pdfa_warning():
return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py')
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 +177,66 @@ 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):
def test_remove_background(spoof_tesseract_noop):
from PIL import Image
# Ensure the input image does not contain pure white/black
im = Image.open(_infile('congress.jpg'))
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
output_pdf = check_ocrmypdf(
'congress.jpg', 'test_remove_bg.pdf', '--remove-background',
'--image-dpi', '150',
env=spoof_tesseract_noop)
from ocrmypdf.ghostscript import rasterize_pdf
import logging
log = logging.getLogger()
output_png = _outfile('remove_bg.png')
rasterize_pdf(
output_pdf,
output_png,
xres=100,
yres=100,
raster_device='png16m',
log=log)
# The output image should contain pure white and black
im = Image.open(output_png)
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
# This will run 5 * 2 * 2 = 20 test cases
@pytest.mark.parametrize(
"pdf",
['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'])
@pytest.mark.parametrize("renderer", ['hocr', 'tesseract'])
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type):
check_ocrmypdf(
pdf,
'test_{0}_{1}.pdf'.format(pdf, renderer),
'-dc',
'-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 +244,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 +262,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 +286,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 +307,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):
@@ -386,16 +449,16 @@ 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',
'--remove-background',
'--skip-big', '10', '--title', 'Too Many Weird Files',
'--author', 'py.test', '--pdf-renderer', renderer,
'--output-type', output_type,
env=spoof_tesseract_cache)
@@ -403,38 +466,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
@@ -453,26 +527,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)
@@ -481,7 +555,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')
@@ -503,19 +577,19 @@ 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)
@@ -535,15 +609,149 @@ def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
def test_no_unpaper():
env = os.environ.copy()
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/no_unpaper_here.py')
sh, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
assert sh.returncode == ExitCode.missing_dependency
assert p.returncode == ExitCode.missing_dependency
def test_old_unpaper():
env = os.environ.copy()
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/unpaper_oldversion.py')
sh, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
assert sh.returncode == ExitCode.missing_dependency
assert p.returncode == ExitCode.missing_dependency
def test_algo4():
p, _, _ = run_ocrmypdf('encrypted_algo4.pdf', 'wontwork.pdf')
assert p.returncode == ExitCode.encrypted_pdf
@pytest.mark.parametrize('renderer', [
'hocr']) # tesseract cannot pass this test - resamples to square image
def test_non_square_resolution(renderer, spoof_tesseract_cache):
# Confirm input image is non-square resolution
in_pageinfo = pdf_get_all_pageinfo(_infile('aspect.pdf'))
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
out = 'aspect_%s.pdf' % renderer
check_ocrmypdf(
'aspect.pdf', out,
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(_outfile(out))
# Confirm resolution was kept the same
assert in_pageinfo[0]['xres'] == out_pageinfo[0]['xres']
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
def test_image_to_pdf(spoof_tesseract_noop):
check_ocrmypdf(
'LinnSequencer.jpg', 'image_to_pdf.pdf', '--image-dpi', '200',
env=spoof_tesseract_noop)
def test_jbig2_passthrough(spoof_tesseract_cache):
out = check_ocrmypdf(
'jbig2.pdf', 'jbig2_out.pdf',
'--output-type', 'pdf',
'--pdf-renderer', 'hocr',
env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(out)
assert out_pageinfo[0]['images'][0]['enc'] == 'jbig2'
def test_stdin(spoof_tesseract_noop):
input_file = _infile('francais.pdf')
output_file = _outfile('test_stdin.pdf')
# Runs: ocrmypdf - output.pdf < testfile.pdf
with open(input_file, 'rb') as input_stream:
p_args = OCRMYPDF + ['-', output_file]
p = Popen(
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
stdin=input_stream, env=spoof_tesseract_noop)
out, err = p.communicate()
assert p.returncode == ExitCode.ok
def test_stdout(spoof_tesseract_noop):
input_file = _infile('francais.pdf')
output_file = _outfile('test_stdout.pdf')
# Runs: ocrmypdf francais.pdf - > test_stdout.pdf
with open(output_file, 'wb') as output_stream:
p_args = OCRMYPDF + [input_file, '-']
p = Popen(
p_args, close_fds=True, stdout=output_stream, stderr=PIPE,
stdin=DEVNULL, env=spoof_tesseract_noop)
out, err = p.communicate()
assert p.returncode == ExitCode.ok
from ocrmypdf import qpdf
assert qpdf.check(output_file, log=None)
def test_masks(spoof_tesseract_noop):
check_ocrmypdf('masks.pdf', 'test_masks.pdf', env=spoof_tesseract_noop)
def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop):
check_ocrmypdf(
'epson.pdf', 'test_epson.pdf',
env=spoof_tesseract_noop)
def test_rotated_skew_timeout():
"""This document contains an image that is rotated 90 into place with a
/Rotate tag and intentionally skewed by altering the transformation matrix.
This tests for a bug where the combinatino of preprocessing and a tesseract
timeout produced a page whose dimensions did not match the original's.
"""
input_file = _infile('rotated_skew.pdf')
in_pageinfo = pdf_get_all_pageinfo(input_file)[0]
assert in_pageinfo['height_pixels'] < in_pageinfo['width_pixels'], \
"Expected the input page to be landscape"
assert in_pageinfo['rotate'] == 90, "Expected a rotated page"
out = check_ocrmypdf(
'rotated_skew.pdf', 'test_rotated_skew.pdf',
'--pdf-renderer', 'hocr',
'--deskew', '--tesseract-timeout', '0')
out_pageinfo = pdf_get_all_pageinfo(out)[0]
assert out_pageinfo['height_pixels'] > out_pageinfo['width_pixels'], \
"Expected the output page to be portrait"
assert out_pageinfo['rotate'] == 0, \
"Expected no page rotation for output"
assert in_pageinfo['width_pixels'] == out_pageinfo['height_pixels'] and \
in_pageinfo['height_pixels'] == out_pageinfo['width_pixels'], \
"Expected page rotation to be baked in"
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa):
p, out, err = run_ocrmypdf(
'ccitt.pdf', 'test_pdfa_failure.pdf',
env=spoof_no_tess_no_pdfa)
assert p.returncode == 4, "Expected return code 4 when PDF/A fails"
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning):
check_ocrmypdf('ccitt.pdf', 'test_feature_elision.pdf',
env=spoof_no_tess_pdfa_warning)
def test_very_high_dpi(spoof_tesseract_cache):
"Checks for a Decimal quantize error with high DPI, etc"
check_ocrmypdf('2400dpi.pdf', 'test_2400dpi.pdf',
env=spoof_tesseract_cache)
-5
View File
@@ -97,11 +97,6 @@ def test_single_page_image():
assert pdfimage['width'] == 8
assert pdfimage['color'] == 'gray'
# While unexpected, this is correct
# PDF spec says /FlateDecode image must have /BitsPerComponent 8
# So mono images get upgraded to 8-bit
assert pdfimage['bpc'] == 8
# DPI in a 1"x1" is the image width
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
assert abs(pdfimage['dpi_h'] - 8) < 1e-5