Compare commits

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Broke many of the test cases
2016-02-27 00:18:36 -08:00
James R. Barlow 11a561dbce v4.0.3 release notes 2016-02-26 01:12:15 -08:00
James R. Barlow dad2198394 Log information about detected page orientations in a summary line 2016-02-26 01:07:59 -08:00
James R. Barlow e40fdc502d Always dump stack trace for unexpected errors 2016-02-26 01:06:59 -08:00
James R. Barlow d446fe5922 Fix "too few characters" reported as error by tesseract -psm 0 2016-02-21 08:53:34 -08:00
James R. Barlow 4ca90c106d Docker: fix blank JPEG2000 PDF issue 2016-02-21 04:24:21 -08:00
James R. Barlow 7c5e58a497 Fix test cases that break in Docker, improve test for running in Docker 2016-02-20 23:47:37 -08:00
James R. Barlow 323b9a5f8e Add other missing files 2016-02-20 05:34:21 -08:00
James R. Barlow cab381a339 Add JPEG 2000 test case 2016-02-20 05:13:19 -08:00
James R. Barlow fe4d4c39cd Merge commit '6f3ac46b1c176d48782347cfa14d9ef6ce773f37' into develop 2016-02-20 04:56:12 -08:00
James R. Barlow ad188d7ae1 Docker: supply openjpeg to address JPXDecode errors 2016-02-20 04:54:55 -08:00
James R. Barlow 8246cc0538 Gracefully recover from tesseract's failure to process very large images
And test cases to check this
2016-02-20 04:53:23 -08:00
James R. Barlow 6f3ac46b1c Gracefully recover from tesseract's failure to process very large images
And test cases to check this
2016-02-20 04:53:02 -08:00
James R. Barlow ac71c3be63 4.0.2rc1 - release notes, add missing file caught by Travis 2016-02-20 03:36:37 -08:00
James R. Barlow ecc0ac9b19 Fix error on --tesseract-timeout timing out 2016-02-20 03:13:23 -08:00
James R. Barlow ea4e6bf67d leptonica: serialization tweaks, memory handling 2016-02-20 02:54:53 -08:00
James R. Barlow 46c204f533 Fix leptonica pickling 2016-02-20 02:35:34 -08:00
James R. Barlow 71fbda8bf6 Adjust page orientation parsing to deal with change in Tess 3.04.01 2016-02-20 01:32:56 -08:00
James R. Barlow 9b79b4a7c8 Leptonica: documentation, helper functions 2016-02-20 01:20:06 -08:00
James R. Barlow c04cc853d7 leptonica: remove special PNM handling
We no longer use PNM as an intermediate format, so there's no need to
handle leptonica's PNM quirks.
2016-02-19 15:13:14 -08:00
James R. Barlow dd41e70ccc leptonica: nit 2016-02-19 15:11:48 -08:00
James R. Barlow 4206e74f42 tests: also check that monochrome correlation correctly detects matches 2016-02-19 14:35:31 -08:00
James R. Barlow 68c3ce56a9 Don't do chmod unless necessarily (breaks py.test on Docker) 2016-02-19 14:09:56 -08:00
James R. Barlow ab0e5fa425 Improve error checking for tesseract -psm 0 (orientation) errors 2016-02-19 03:58:39 -08:00
James R. Barlow f3b0434a87 Improve ability to capture error messages from tesseract on a crash 2016-02-19 03:48:49 -08:00
James R. Barlow aa394440db Just use the PyPI version of ocrmypdf in dockerfile
Apparently setuptools_scm_git_archive is ineffective on hub.docker.com
automatic build, it still can't find a version.
2016-02-17 15:14:23 -08:00
James R. Barlow 3b98a1a04b Fix KeyError on unexpected tess output 2016-02-17 06:05:27 -08:00
James R. Barlow fcb89b0c58 Forgot to save release notes 2016-02-17 01:48:25 -08:00
James R. Barlow ac65d6a03a v4.0: release notes 2016-02-17 01:21:17 -08:00
James R. Barlow 2103f60906 Merge branch 'release/v4.0.0' 2016-02-17 01:13:24 -08:00
James R. Barlow e3c3d848c1 Save Dockerfile comment 2016-02-17 01:11:41 -08:00
James R. Barlow d4ef3411e0 Suppress --pdf-renderer tesseract warning in Docker image
Since the corrected font is provided in the Docker image, there's no
reason to show the warning.
2016-02-17 01:03:20 -08:00
James R. Barlow 71d616e413 Restore Dockerfile on local and probably on automated build as well 2016-02-17 00:13:45 -08:00
James R. Barlow fe651d1bf5 Overwrite Tesseract 3.04 default pdf font with better pdf font 2016-02-16 21:45:44 -08:00
James R. Barlow 582ba8cfad Provide sharp2.ttf for Docker images 2016-02-16 21:45:17 -08:00
James R. Barlow d23291650a Remove duplicate line from documentation 2016-02-16 14:30:15 -08:00
James R. Barlow 812fd745b6 Remove redundant line from resources 2016-02-16 14:29:56 -08:00
James R. Barlow a87aa71d85 Remove old documentation about Pillow not linking jpeg, zlib
As of Pillow 3.0.0 this is fixed, so make Pillow 3 a requirement
2016-02-16 14:29:31 -08:00
James R. Barlow 60b2eb1455 Fix JPEG DPI: Pillow expects dpi=(x,y) 2016-02-16 07:29:20 -08:00
James R. Barlow ab3c1988c1 Work around Leptonica < 1.72 bug that breaks Travis 2016-02-16 07:03:50 -08:00
James R. Barlow ee5223eea8 Travis again: are invalid correlation measurements a use-after-free?
Try explicitly casting the value to a float.
2016-02-16 06:09:48 -08:00
James R. Barlow edd2185268 Fix pytest-runner not understanding 'norecursedirs'
As discussed here
https://github.com/pytest-dev/pytest-runner/issues/7
and sort of
https://github.com/pytest-dev/pytest/issues/567
2016-02-16 05:44:55 -08:00
James R. Barlow 35b1ca2be2 Travis: try replacing non-standard invocation of py.test
It seems the normal thing to wire up python setup.py test to invoke
the test suite rather than py.test. This may be the reason for the
past chain of cffi-related commits.
2016-02-16 05:36:14 -08:00
James R. Barlow 71e493a810 Fix case of JPEG missing DPI field 2016-02-16 05:29:32 -08:00
James R. Barlow 6178e22e7f Travis: force compile leptonica? 2016-02-16 05:24:46 -08:00
James R. Barlow ef0aab060a Make debug output more verbose on failure 2016-02-16 05:17:18 -08:00
James R. Barlow d70ce61cfd Travis: maybe it's just the missing __init__.py? 2016-02-16 05:09:12 -08:00
James R. Barlow 8cd84afac8 Revert "Try moving leptonica build script, playing with wheels a bit"
This reverts commit ec2c6c312b.
2016-02-16 05:04:20 -08:00
James R. Barlow ec2c6c312b Try moving leptonica build script, playing with wheels a bit 2016-02-16 04:05:58 -08:00
James R. Barlow 3946bba318 Too soon, try again 2016-02-16 03:51:08 -08:00
James R. Barlow 2ed0b78a7b Travis: are you creating _leptonica.py? 2016-02-16 03:47:35 -08:00
James R. Barlow ed346d032c Does Travis need explicit install libffi-dev? 2016-02-16 03:41:11 -08:00
James R. Barlow acd645f192 Fix travis syntax error 2016-02-16 02:42:47 -08:00
James R. Barlow 88433e4c34 Fiddle with travis, try to get better debug output
Essentially cffi failed somehow, not clear how
2016-02-16 02:12:14 -08:00
James R. Barlow 1224af1780 Update test resources to address files with unknown source
-Remove Test_Issue_28.pdf (inherited from fritz-hh, source unknown)
-Replace missing_docinfo.pdf (received from user, but it's a printout of
a website; unclear status, so created a new PDF with the same effect)
-Others are okay
2016-02-16 00:28:28 -08:00
James R. Barlow ab13342931 Revise rotation tests in prep for adding a few more 2016-02-15 17:17:43 -08:00
James R. Barlow d7913da484 Test case: remove filename conflict 2016-02-15 16:49:28 -08:00
James R. Barlow c50e3f1329 Complain about older tesseracts that don't have sharp2.ttf installed 2016-02-15 16:43:41 -08:00
James R. Barlow a62f86dbd7 Update release notes 2016-02-15 16:43:14 -08:00
James R. Barlow 33b88b18db Update the notes 2016-02-15 14:03:59 -08:00
James R. Barlow 7c691c21ab Fix image layer rotation for pages with nonzero crop boxes 2016-02-10 17:48:33 -08:00
James R. Barlow 4ec51729d8 Partial fix for images not anchored to (0, 0) 2016-02-10 17:14:48 -08:00
James R. Barlow 07b41e479a Cleaner access to mediabox 2016-02-09 02:19:05 -08:00
James R. Barlow 6510bcad19 DPI information not transferred automatically from PNG to JPEG 2016-02-09 02:18:54 -08:00
James R. Barlow 265d2ce39b Better skewed image 2016-02-08 23:44:46 -08:00
James R. Barlow 1928a64cae Better logging output for autorotation 2016-02-08 23:42:25 -08:00
James R. Barlow 11e575a5a3 leptonica: suppress debug output 2016-02-08 23:41:45 -08:00
James R. Barlow 7fbc0d6460 tesseract: unify logging function 2016-02-08 23:40:36 -08:00
James R. Barlow 1ba8b1aa4b unpaper is lousy at deskewing, so let leptonica do it 2016-02-08 15:26:33 -08:00
James R. Barlow 3569c76c0f Also include cardinal.pdf 2016-02-08 15:23:04 -08:00
James R. Barlow 16c7ac2582 Fix test_deskew for new Leptonica API 2016-02-08 15:20:01 -08:00
James R. Barlow 4ceb59215f Leptonica: classes are better 2016-02-08 15:14:44 -08:00
James R. Barlow 2e6879ee51 Introduce Leptonica class for Pix 2016-02-08 14:52:01 -08:00
James R. Barlow 66fc2e9d7d Add rotate 180 correlation sanity check 2016-02-08 13:10:11 -08:00
James R. Barlow 2c7a6e574f Shorten names of _make_input/output 2016-02-08 12:57:26 -08:00
James R. Barlow 78c3bf5dba Check autorotate using leptonica correlation 2016-02-08 12:55:50 -08:00
James R. Barlow 98c115e3bb Cache wasn't enabled properly for test_autorotate 2016-02-08 12:55:28 -08:00
James R. Barlow 2752bda80b Merge branch 'feature/leptdeskew' into feature/logging
Need leptonica for testing now, I think
# Conflicts:
#	ocrmypdf/tesseract.py
#	requirements.txt
#	setup.py
2016-02-08 12:34:48 -08:00
James R. Barlow 7c0940609a Take a stab at writing test case for autorotate 2016-02-08 12:32:39 -08:00
James R. Barlow d30a879e2d Fix test suite by running select_image_for_pdf unconditionally
The purpose of this change that caused the problem was a minor
optimization for the tesseract renderer path that had it pull an image
from select_image_for_pdf so that it could use a JPEG instead of PNG,
instead of taking it from preprocess_clean where it would only get a PNG
and make large files.
2016-02-08 02:33:03 -08:00
James R. Barlow b907234d5c Update tesseract spoofing to cache orientation and script detection checks
No cache: 269 s
With cache: 144 s

test_oversample[tesseract] now fails, all others good
2016-02-08 02:21:56 -08:00
James R. Barlow b0114c9174 More logging improvements 2016-02-08 01:31:15 -08:00
James R. Barlow d2ba8c501f Restore invisibletext for normal output 2016-02-08 01:14:39 -08:00
James R. Barlow 6a7ed7d359 Make logging output a lot more useful 2016-02-08 00:58:14 -08:00
James R. Barlow 6289afa1a6 Better: custom logging factory to avoid whatever ruffus is doing 2016-02-08 00:18:52 -08:00
James R. Barlow 9bb6fa04cb Return logging to a semblance of normalcy 2016-02-08 00:09:31 -08:00
James R. Barlow afb6f6f5c9 Render preview as .jpg instead of .png
Smaller file size of JPEG seems to help performance, although the
difference is only about 1%.
2016-02-07 15:49:10 -08:00
James R. Barlow 8a69671dbd Suppress debug message 2016-02-07 15:43:57 -08:00
James R. Barlow 178aee4687 Make rotation optional (for now it's off, possibly should be on) 2016-02-07 15:43:45 -08:00
James R. Barlow 8484caddfb Tweak pipeline, allowing --pdf-renderer to use JPEGs instead of PNGs 2016-02-07 15:36:51 -08:00
James R. Barlow 08313316de Cleanup auto-rotation 2016-02-07 15:06:54 -08:00
James R. Barlow 1d0eca5c63 All four rotation directions working 2016-02-07 06:09:01 -08:00
James R. Barlow fe89232a30 Fix autorotate for some lossless cases 2016-02-07 05:59:46 -08:00
James R. Barlow 4b51b521e2 Implement autorotate (provided lossless reconstruction is disabled)
Works for a single page file, probably

Although arguably rotation is not quite lossless, and the two could be
mutually exclusive anyway, so maybe this is it. Did not check in some
debugging changes (lossless=False, text debugging=True)

PyPDF seems to get merging wrong when one of the pages is rotated.
2016-02-07 03:27:33 -08:00
James R. Barlow e9ec458304 tesseract: add command to access OSD values 2016-02-07 03:21:32 -08:00
James R. Barlow 54b0ddd787 ghostscript: don't try to "help" autorotation
It uses text direction alone -- unreliable guide.
2016-02-07 03:20:42 -08:00
jbarlow83 93bec22f9c README: mention polyglot, fix container vs image 2016-02-07 00:32:20 -08:00
James R. Barlow 0dc96442d8 Fix img2pdf usage in test case (to make Travis CI happy again) 2016-02-06 23:41:32 -08:00
James R. Barlow 58f4582517 More Dockerfile repair
I'm not fully happy with this arrangement, as it effectively downloads
OCRmyPDF twice, not to mention the lengthy setup time overall.

Will need to try separate build/run images in the future, but now just
get it working again.
2016-02-06 23:13:16 -08:00
James R. Barlow 2d15c09cca Merge branch 'develop' 2016-02-06 18:18:49 -08:00
James R. Barlow 04cb8865b0 Fetch application from PyPI instead of local
setuptools_scm barfs because it can't find the version, because Docker hub
retrieves the application from Github in a way that omits the necessary
details.

I suppose there is a certain logic to Docker only using the tagged
released versions from PyPI, so go with it.  The other attractive option
is to nix setuptools_scm.
2016-02-06 18:18:30 -08:00
James R. Barlow 6fe32bbaf7 v3.2.1 2016-02-05 16:10:18 -08:00
James R. Barlow 4abb20390d Bump Dockerfile versions 2016-02-05 16:08:26 -08:00
James R. Barlow daa3916430 Fix img2pdf 0.2 usage
All tests pass when forced to rely on img2pdf, so seems okay
2016-02-05 15:13:26 -08:00
James R. Barlow e9b87cefcc Try img2pdf 0.2 2016-02-05 14:38:37 -08:00
James R. Barlow 60593b5ad3 Tighten up package requirements to deal with incompatible img2pdf 0.2 release 2016-02-05 14:37:05 -08:00
James R. Barlow f708b11ea4 Fix Python 2.7 warning 2016-02-05 02:34:49 -08:00
James R. Barlow cb3ba8e973 Merge branch 'release/v3.2' into develop 2016-02-05 00:10:41 -08:00
James R. Barlow ec3d92ad8e Reorg gitignore 2016-01-30 15:28:24 -08:00
James R. Barlow 66a095d7de Improve organization of CFFI setup 2016-01-30 15:19:40 -08:00
James R. Barlow 411981efbc Experiment with CFFI instead of ctypes 2016-01-30 15:06:25 -08:00
James R. Barlow 350ad5210e Leptonica: convert to CFFI 2016-01-20 15:03:07 -08:00
James R. Barlow f3b588764e Suppress tesseract argument printout 2016-01-20 15:02:48 -08:00
James R. Barlow b49f5a7d77 Support optionally using leptonica to deskew
unpaper doesn't seem to be good at deskewing. It fails on test case
with a lot of italics. I think it also struggles on pages with a lot
of whitespace. Leptonica continues to shine here.

However, this is only a first crack at Leptonica. The leptonica module
should be redone to use cffi (more extensible).

Also considering the possibility of making all Lept calls in a forked
process to insulate the calling process from C code crashes and the
messy redirect of stdout/stderr to read Leptonica's errors.

I don't think the redirect is a huge problem as long as multiprocesses
rather than multithreads are used. The ruffus child process that is
handling a page is single threaded and will not be affected by the
redirection. It just feels dirty. The main reason to consider a child
process is crash isolation.
2016-01-19 17:43:40 -08:00
57 changed files with 4202 additions and 38060 deletions
+1
View File
@@ -0,0 +1 @@
ref-names: $Format:%D$
+3 -1
View File
@@ -5,4 +5,6 @@
# (binary is a macro for -text -diff)
*.jar binary
*.pdf binary
*.PDF binary
*.PDF binary
.git_archival.txt export-subst
+3
View File
@@ -0,0 +1,3 @@
Please include the command line and (if needed) a test file with your issue report.
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
+3 -4
View File
@@ -1,9 +1,7 @@
# Development environment
*.pyc
*.sublime-*
venv-3.4/
venv-3.5/
venv/
venv-*/
pyvenv.cfg
# Package building
@@ -12,6 +10,7 @@ pyvenv.cfg
.eggs/
build/
dist/
wheelhouse/
# Automatically generated files
ocrmypdf/lib/_*.py
@@ -27,5 +26,5 @@ log/
.ipynb_checkpoints/
tests/cache/
tests/output/
tests/resources/private
tests/resources/private/
tmp/
+16 -13
View File
@@ -1,33 +1,36 @@
sudo: required
dist: trusty
language: python
cache: pip
cache:
directories:
- $HOME/.cache/pip
- $HOME/.ccache
- tarballs
- packages
- tests/cache
python:
- 3.4
- 3.5
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
before_install:
# Ubuntu packages
- sudo add-apt-repository ppa:evl.ms/precise -y # for Ghostscript 9.15
- sudo add-apt-repository ppa:lyrasis/precise-backports -y # for Tesseract 3.03
- sudo add-apt-repository ppa:b-eltzner/qpdfview-exp -y # for QPDF 5
- sudo add-apt-repository ppa:itachi-san/ffmpeg -y # for libav 11.2 (for unpaper)
- sudo add-apt-repository ppa:vshn/ghostscript -y # for ghostscript 9.16 (trusty has 9.10)
- sudo add-apt-repository ppa:heyarje/libav-11 -y # for libav11, which is for unpaper
- sudo apt-get update -qq # must go after all add-apt-repo
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils gcc libavformat-dev libavcodec-dev libavutil-dev automake make pkg-config xsltproc
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
# pip
- pip install --upgrade pip
# Download, make and install unpaper (using ccache)
- mkdir -p tarballs
- "[ -f tarballs/unpaper-6.1.tar.xz ] || wget -q https://www.flameeyes.eu/files/unpaper-6.1.tar.xz -O tarballs/unpaper-6.1.tar.xz"
- tar -xvf tarballs/unpaper-6.1.tar.xz
- export PATH="/usr/lib/ccache:$PATH"
- pushd unpaper-6.1 && ./configure --prefix=/usr && make -j && sudo make install && popd
- mkdir -p packages
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
- sudo dpkg -i packages/unpaper_6.1-1.deb
install:
- pip install -r requirements.txt
@@ -36,7 +39,7 @@ install:
script:
- python setup.py clean
- python setup.py install
- py.test
- python setup.py test
os:
- linux
+34 -10
View File
@@ -1,6 +1,6 @@
# OCRmyPDF
#
# VERSION 3.0.2
# VERSION 3.2
FROM debian:stretch
MAINTAINER James R. Barlow <jim@purplerock.ca>
@@ -23,14 +23,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get install -y --no-install-recommends \
unpaper \
ghostscript \
qpdf \
poppler-utils
RUN apt-get install -y --no-install-recommends \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
RUN apt-get install -qy --no-install-recommends \
libffi-dev \
libpython3-dev \
gcc
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y ghostscript/sid
# Enforce UTF-8
# Borrowed from https://index.docker.io/u/crosbymichael/python/
RUN dpkg-reconfigure locales && \
@@ -38,9 +48,6 @@ RUN dpkg-reconfigure locales && \
/usr/sbin/update-locale LANG=C.UTF-8
ENV LC_ALL C.UTF-8
# Remove the junk
RUN apt-get autoremove -y && apt-get clean -y
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
# Set up a Python virtualenv and take all of the system packages, so we can
# rely on the platform packages rather than importing GCC and compiling them
@@ -49,18 +56,35 @@ RUN pyvenv /appenv \
COPY . /application/
# Replace stock Tesseract 3.04.00 font with improved sharp2.ttf that resolves
# issues in many PDF viewers.
# Discussion is in https://github.com/tesseract-ocr/tesseract/issues/182
COPY ./share/sharp2.ttf /usr/share/tesseract-ocr/tessdata/pdf.ttf
RUN chmod 644 /usr/share/tesseract-ocr/tessdata/pdf.ttf
# Set this here to force a docker version, allowing non-tagged versions to
# be built
# ENV SETUPTOOLS_SCM_PRETEND_VERSION=v3.3.0
# Install application and dependencies
# In this arrangement Pillow and reportlab will be provided by the system
# Even though ocrmypdf is locally present, pull from PyPI because
# Dockerhub and setuptools_scm clash
RUN . /appenv/bin/activate; \
pip install --upgrade pip \
&& pip install --no-cache-dir /application \
&& pip install ocrmypdf \
&& pip install --no-cache-dir -r /application/test_requirements.txt
# Remove the junk
RUN apt-get remove -qy gcc
RUN apt-get autoremove -y && apt-get clean -y
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
USER docker
WORKDIR /home/docker
ENV OCRMYPDF_TEST_OUTPUT=/tmp/test-output
ENV OCRMYPDF_IN_DOCKER=1
ENV OCRMYPDF_SHARP_TTF=1
# Must use array form of ENTRYPOINT
# Non-array form does not append other arguments, because that is "intuitive"
+1 -1
View File
@@ -1,6 +1,6 @@
# OCRmyPDF polyglot
#
# VERSION 3.0.2
# VERSION 3.2
FROM jbarlow83/ocrmypdf:latest
MAINTAINER James R. Barlow <jim@purplerock.ca>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2013-2015, The OCRmyPDF Authors
Copyright (c) 2013-2016, The OCRmyPDF Authors
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
+41 -2
View File
@@ -1,3 +1,42 @@
recursive-exclude tests/output *
# requirements
include requirements.txt
include test_requirements.txt
include test_requirements.txt
include dev_requirements.txt
# git
include .git_archival.txt
# docker
include Dockerfile
include Dockerfile.polyglot
include .dockerignore
# tests
include pytest.ini
recursive-include tests *.jpg
recursive-include tests *.pdf
recursive-include tests *.py
recursive-include tests *.rst
recursive-include tests *.txt
recursive-exclude tests/output *
recursive-exclude tests/output_pageinfo *
recursive-exclude tests/resources/private *
recursive-exclude tests/cache *
# documentation
include *.rst
include pipeline.svg
recursive-exclude .github *
# support files
recursive-include ocrmypdf/data *
recursive-include share *
include *.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 "$@"
+179 -63
View File
@@ -8,7 +8,7 @@ Main features
-------------
- Generates a searchable
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`__ file from a regular PDF
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`_ file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without rendering vector information
@@ -18,11 +18,11 @@ Main features
- Provides debug mode to enable easy verification of the OCR results
- Processes pages in parallel when more than one CPU core is
available
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`__ engine
- Supports the `39 languages <https://code.google.com/p/tesseract-ocr/downloads/list>`__ recognized by Tesseract
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`_ engine
- Supports the `39 languages <https://code.google.com/p/tesseract-ocr/downloads/list>`_ recognized by Tesseract
- Battle-tested on thousands of PDFs, a test suite and continuous integration
For details: please consult the `release notes <RELEASE_NOTES.rst>`__.
For details: please consult the `release notes <RELEASE_NOTES.rst>`_.
Motivation
----------
@@ -31,9 +31,9 @@ I searched the web for a free command line tool to OCR PDF files on
Linux/UNIX: I found many, but none of them were really satisfying.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not display correctly some escaped HTML characters located in the hOCR file produced by the OCR engine
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated PDF files having a ridiculous big size
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR some of my PDF files
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
@@ -46,141 +46,228 @@ Installation
Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
You can install it to a Python virtual environment or system-wide.
These steps describe how to install OCRmyPDF on your system.
Installing the Docker container
- `Installing on Debian and Ubuntu`_ (Debian stretch and Ubuntu 16.10 or later)
- `Installing the Docker image`_
- `Installing on Mac OS X`_
- `Installing on Ubuntu 14.04 LTS`_
- Installing and running on `Windows`_ using the Docker image
If you prefer to install from source or install OCRmyPDF to a Python virtual environment, see steps for `Installing HEAD revision from sources`_.
.. _Windows: `Installing on Windows`_
Installing on Debian and Ubuntu
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For many users, installing the Docker container will be easier than installing all of OCRmyPDF's dependencies. For Windows, it is the only option.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
``apt-get install ocrmypdf``.
If you have `Docker <https://docs.docker.com/>`__ installed on your system, you can install
a Docker container of the latest release.
.. _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::
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 container host, and then follow these optional steps to enable multiple CPUs::
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and OS X has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
# Optional
.. 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::
the image:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
Then tag it to give a more convenient name, just ocrmypdf::
Then tag it to give a more convenient name, just ocrmypdf:
.. code-block:: bash
docker tag jbarlow83/ocrmypdf ocrmypdf
You can then run using the command::
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 ocrmypdf --help
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`__, such as this in this template::
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, such as this in this template:
.. code-block:: bash
docker run -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``::
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
.. code-block:: bash
docker run -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` container name and all arguments to ``ocrmypdf`` should be listed after.
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/>`__.
If it's not already present, `install Homebrew <http://brew.sh/>`_.
Update Homebrew::
Update Homebrew:
.. code-block:: bash
brew update
Install or upgrade the required Homebrew packages, if any are missing::
Install or upgrade the required Homebrew packages, if any are missing:
brew install libpng openjpeg jbig2dec # image libraries
.. code-block:: bash
brew install libpng openjpeg jbig2dec libtiff # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2
brew install leptonica
brew install tesseract
brew install libxml2 libffi leptonica
brew install unpaper # optional
It is also recommended that install Pillow and confirm it can read and write JPEG and PNG files::
Install the required Tesseract OCR engine with the language packs you plan to use:
.. code-block:: bash
brew install tesseract # Option 1: for English, French, German, Spanish
.. code-block:: bash
brew install tesseract --with-all-languages # Option 2: for all language packs
Update the homebrew pip and install Pillow:
.. code-block:: bash
pip3 install --upgrade pip
pip3 install --upgrade pillow
Sometimes, the Python imaging library (Pillow) can end up being compiled and installed without support for JPEG and PNG files. (Arguably, this is an unfixed bug in Pillow's installer.) To confirm that Pillow is compiled correctly and can access JPEG and PNG files, try this command::
You can then install OCRmyPDF from PyPI:
python3 -c "from PIL import Image; im = Image.new('1', (1, 1)); im.save('test.png'); im.save('test.jpg')"
If you have trouble getting Pillow to access JPEG and PNG files, `review the installation instructions <https://pillow.readthedocs.org/installation.html>`__.
You can then install OCRmyPDF from PyPI::
.. code-block:: bash
pip3 install ocrmypdf
The command line program should now be available::
The command line program should now be available:
.. code-block:: bash
ocrmypdf --help
Installing on Ubuntu 14.04 LTS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than other options, because of certain bugs in Python package installation.
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
Update apt-get::
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
.. code-block:: bash
sudo add-apt-repository ppa:vshn/ghostscript -y
sudo add-apt-repository ppa:heyarje/libav-11 -y
Update apt-get:
.. code-block:: bash
sudo apt-get update
sudo apt-get upgrade
Install system dependencies::
Install system dependencies:
.. code-block:: bash
sudo apt-get install \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
libavformat56 libavcodec56 libavutil54 \
ghostscript \
tesseract-ocr \
qpdf \
unpaper \
python3-pip \
python3-pil \
python3-pytest \
python3-reportlab
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
into your system Python, which could interfere with other programs)::
into your system Python, which could interfere with other programs):
.. code-block:: bash
sudo pip3 install ocrmypdf
If you wish to install OCRmyPDF to a virtual environment to isolate system Python from modified, you can
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`__::
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
.. code-block:: bash
sudo apt-get install python3-venv
python3 -m venv venv-ocrmypdf --without-pip
source venv-ocrmypdf/bin/activate
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
deactivate
pyvenv --system-site-packages venv-ocrmypdf
python3 -m venv --system-site-packages venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip install ocrmypdf
Ubuntu 14.04 only installs ``unpaper`` version 0.4.2, which is not supported by OCRmyPDF because it is produces invalid output. This program is an optional dependency, and provides page deskewing and cleaning. See `Dockerfile <Dockerfile>`__ for an example of how to building unpaper 6.1 from source. If you choose to install unpaper later, OCRmyPDF will use the foremost version on the system PATH.
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper`` and is not motivated to figure how to set up a Ubuntu PPA to distribute it. You can create a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
sudo dpkg -i unpaper_6.1-1.deb
Installing on Windows
~~~~~~~~~~~~~~~~~~~~~
Direct installation on Windows is not possible. Install the Docker container as described above.
Direct installation on Windows is not possible. Install the _`Docker` container as described above. Ensure that your command prompt can run the docker "hello world" container.
Running on Windows
~~~~~~~~~~~~~~~~~~
The command line syntax to run ocrmypdf from a command prompt will resemble:
.. code-block:: bat
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
Installing HEAD revision from sources
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -188,30 +275,32 @@ Installing HEAD revision from sources
If you have ``git`` and ``python3.4`` or ``python3.5`` installed, you can install from source. When the ``pip`` installer runs,
it will alert you if dependencies are missing.
First, clone the HEAD revision::
To install the HEAD revision from sources in the current Python 3 environment:
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
cd OCRmyPDF
.. code-block:: bash
To install the HEAD revision from sources::
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
pip3 install .
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
allowing customization of OCRmyPDF, use the ``-e`` flag::
.. code-block:: bash
pip3 install -e .
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
On certain Linux distributions such as Ubuntu, you may need to use
run the install command as superuser::
run the install command as superuser:
sudo pip3 install [-e] .
.. code-block:: bash
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
Note that this will alter your system's Python distribution. If you prefer
to not install as superuser, you can install the package in a Python virtual environment::
to not install as superuser, you can install the package in a Python virtual environment:
.. code-block:: bash
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
pyvenv venv
python3 -m venv
source venv/bin/activate
cd OCRmyPDF
pip3 install .
@@ -219,7 +308,9 @@ to not install as superuser, you can install the package in a Python virtual env
However, ``ocrmypdf`` will only be accessible on the system PATH after
you activate the virtual environment.
To run the program::
To run the program:
.. code-block:: bash
ocrmypdf --help
@@ -228,12 +319,37 @@ 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.
Languages
---------
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
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 # 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
-------
In case you detect an issue, please:
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
- Check if your issue is already known
.. code-block:: bash
ocrmypdf --help
The `Wiki <https://github.com/jbarlow83/OCRmyPDF/wiki>`_ page also contains some tips and suggests.
If you detect an issue, please:
- Check whether your issue is already known
- If no problem report exists on github, please create one here:
https://github.com/jbarlow83/OCRmyPDF/issues
- Describe your problem thoroughly
@@ -245,11 +361,11 @@ In case you detect an issue, please:
Press & Media
-------------
- `c't 1-2014, page 59 <http://www.heise.de/ct/inhalt/2014/1/58/>`__:
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
magazine c't
- `heise Open Source, 09/2014: Texterkennung mit
OCRmyPDF <http://www.heise.de/-2356670>`__
OCRmyPDF <http://heise.de/-2356670>`_
Disclaimer
----------
+210 -362
View File
@@ -1,13 +1,218 @@
RELEASE NOTES
=============
Please always read this file before installing the package
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
v4.2.4:
=======
- Fixed an error (#90) caused by PDFs that use stencil masks properly
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
v4.2.3:
=======
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
- Integrated a few fixes to simplify downstream packaging (Debian)
+ The test suite no longer assumes it is installed
+ If running Linux, skip a test that passes Unicode on the command line
- Added a test case to check explicit masks and stencil masks
- Added a test case for indirect objects and linearized PDFs
- Deprecated the OCRmyPDF.sh shell script
v4.2.2:
=======
- Improvements to documentation
v4.2.1:
=======
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
- Implemented stdin streaming
v4.2:
=====
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
+ ``pdfa`` is the default, consistent with past behavior
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
- ``--force-ocr`` rasterizes pages even if they contain no images
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
+ fixes issue #82
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
+ Some Ubuntu "PPAs" (backports) are needed to make it work
- Support for some older dependencies dropped
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
- Ghostscript now runs in "safer" mode where possible
v4.1.4:
=======
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
v4.1.3:
=======
- More helpful error message for PDFs with version 4 security handler
- Update usage instructions for Windows/Docker users
- Fix order of operations for matrix multiplication (no effect on most users)
- Add a few leptonica wrapper functions (no effect on most users)
v4.1.2:
=======
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
- More helpful error message for an error related to certain types of malformed PDFs
v4.1:
=====
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
v4.0.7:
=======
- Minor correction to Ghostscript output settings
v4.0.6:
=======
- Update install instructions
- Provide a sRGB profile instead of using Ghostscript's
v4.0.5:
=======
- Remove some verbose debug messages from v4.0.4
- Fixed temporary that wasn't being deleted
- DPI is now calculated correctly for cropped images, along with other image transformations
- Inline images are now checked during DPI calculation instead of rejecting the image
v4.0.4:
=======
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
v4.0.3:
=======
New features
------------
- Page orientations detected are now reported in a summary comment
Fixes
-----
- Show stack trace if unexpected errors occur
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
abort the file
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
v4.0.2:
=======
Fixes
-----
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
orientation information
- Improved handling of Tesseract errors and crashes
- Fixed use of chmod on Docker that broke most test cases
v4.0.1:
=======
Fixes
-----
- Fixed a KeyError if tesseract fails to find page orientation information
v4.0:
=====
New features
------------
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
at image deskewing than unpaper.
Fixes
-----
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
Changes
-------
- Logging output is now much easier to read
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
- libffi is now required
- Some changes were made to the Docker and Travis build environments to support libffi
- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01,
the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00.
You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder
to correct the problem.
v3.2.1:
=======
Changes
-------
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
- Tweaked the Dockerfiles
v3.2:
=========
=====
New features
------------
@@ -207,363 +412,6 @@ Notes and known issues
v2.2-stable (2014-09-29):
=========================
New features
------------
OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions.
- None
Changes
-------
- Update to jhove v1.11
- Request the python library reportlab v3.0 or newer (So that we could remove a patch to the previous version of reportlab leading to issues for some users)
Fixes
-----
- Fix bug on Mac OS X (resolution of simlink to OCRmyPDF.sh script) (thanks to jbarlow83)
- Check if the input pdf file exists before to continue
Tested with
-----------
- Operating system: FreeBSD 9.2
- Dependencies:
- parallel 20140822
- poppler-utils 0.24.5
- ImageMagick 6.8.9-4 2014-09-17
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.8
- ghostcript (gs): 9.06
- java: openjdk version "1.7.0_65"
v2.1-stable (2014-09-20):
=========================
New features
------------
- None
Changes
-------
- None
Fixes
-----
- Allow execution via simlink
- Add support for tesseract 3.03
- Add support for newer version of reportlab
- Lowered minimum version of gnu parallel
- Various typo
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- parallel 20130222
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v2.0-stable (2014-01-25):
=========================
New features
------------
- Check if the language(s) passed using the -l option is supported by
tesseract (fixes #60)
Changes
-------
- Allow OCRmyPDF to be used with tesseract 3.02.01, even though OCR
might fail for few PDF file (see issue #28). Rationale: For some
linux distribution, no newer version than tesseract 3.02.01 is
available
Fixes
-----
- More robust algorithm for checking the version of the installed
tesseract package
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- parallel 20130222
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v2.0-rc2 (2014-01-16):
======================
New features
------------
- None
Changes
-------
- Size reduction of final PDF file: (fixes #50)
- Support for monochrome (Black&White) images (massive size reduction
in final PDF: >80%)
- Reduced size of grayscale images (by 13% on test PDF file)
- Preventing fi, fl ligatures does not require anymore to pass an
additional config file to tesseract using the -C option (fixes #58)
- Location of temporary folder according to content of environment
variable TMPDIR.
- Dependency to pdftk removed
- Check for compatible versions of dependencies: (fixes #51)
- parallel and tesseract
- python libraries reportlab and lxml
Fixes
-----
- Improved portability with various shells (dash, bash, tcsh) and OS
(FreeBSD, MAC OSX, Linux) (fixes #59)
- Corrected bug in case the input PDF file contains a space character
(fixes #48)
- Prevent spurious error message in case there is no image in a PDF
page
- Prevent collision of temporary folder names (fixes #57)
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- parallel 20130222
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v2.0-rc1 (2014-01-07):
======================
New features
------------
- Huge performance improvement on machines having multiple CPU/cores
(processing of several pages concurrently) (fixes #18)
- By default prevent from processing a PDF file already containing
fonts (i.e. text)(it can be overridden with the -f flag) (fixes #16)
- Warn if the resolution is too low to get reasonable OCR results
(fixes #37)
- New option (-o) to perform automatic oversampling if the image
resolution is too low. This can improve OCR results.
- Warn if using a tesseract version older than v3.02.02 (as older
versions are known to produce invalid output) (fixes #41)
- Echo version of the installed dependencies (e.g. tesseract) in debug
mode in order to ease support (fixes #35)
- Echo the arguments passed to the script in debug mode to ease support
Changes
-------
- In debug mode: The debug page is now placed after the respective
"normal" page
- Reduced disk space usage in temporary folder if -d (deskew) or -c
(cleanup) options are not selected
- New file src/config.sh containing various configuration parameters
- Documentation of the tesseract config file "tess-cfg/no\_ligature"
improved
- Improved consistency of the temporary file names
Fixes
-----
- Improved robustness:
- in case vertical resolution differs from horizontal resolution (fixes
#38)
- in case a PDF page contains more than one image (fixes #36)
- Fix a problem occurring if python 3 is the standard interpreter
(fixes #33)
- Fix a problem occurring if the input PDF file contains special
characters like "#" (fixes #34)
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- parallel 20130222
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- pdftk 1.45
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v1.1-stable (2014-01-06):
=========================
New features
------------
- N/A
Changes
-------
- N/A
Fixes
-----
- Fixed syntax error (bashism) leading to an error message on certain
systems (fixes #42)
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- pdftk 1.45
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v1.0-stable (2013-05-06):
=========================
New features
------------
- In debug mode: compute and echo time required for processing (fixes
#26)
Changes
-------
- Removed feature to add metadata in final pdf file (because it lead to
to final PDF file that does not comply to the PDF/A-1 format)
- Removed feature to set same owner & permissions in final PDF file
than in input file
- Removed many unused jhove files (e.g. documentation, \*.java and
\*.class files)
Fixes
-----
- Correction to handle correctly path and input PDF files having spaces
(fixes #31)
- Resolutions (x/y) that are nearly equal are now supported (fixes #25)
- Fix compatibility issue with Ubuntu server 12.04 / Ubuntu server
10.04 / Linux Mint 13 Maya and probably other Linux distributions
(fixes #27)
- Commit missing jhove files (\*.jar mainly) due to wrong .gitignore
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- pdftk 1.45
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v1.0-rc2 (2013-04-29):
======================
New features
------------
- Keep temporary files if debug mode is set (fixes #22)
- Set same owner & permissions in final PDF file than in input file
(fixes #9)
- Added metadata in final pdf file (fixes #4)
Changes
-------
- N/A
Fixes
-----
- Fixed wrong image cropping when deskew option is activated
- Exit with error message if page size is not found in hocr file (fixes
#21)
- Various minor fixes in log messages
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- pdftk 1.45
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
v1.0-rc1 (2013-04-26):
======================
New features
------------
- First release candidate
Changes
-------
- N/A
Fixes
-----
- N/A
Tested with
-----------
- Operating system: FreeBSD 9.1
- Dependencies:
- poppler-utils 0.22.2
- ImageMagick 6.8.0-7 2013-03-30
- Unpaper 0.3
- tesseract 3.02.02
- Python 2.7.3
- pdftk 1.45
- ghoscript (gs): 9.06
- java: openjdk version "1.7.0\_17"
For details on older versions, see the `final version of its release notes <https://github.com/fritz-hh/OCRmyPDF/blob/7fd3dbdf42ca53a619412ce8add7532c5e81a9d1/RELEASE_NOTES.md>`_.
+4
View File
@@ -0,0 +1,4 @@
check-manifest>=0.33
setuptools-scm>=1.11.1
twine>=1.8.1
coverage>=4.2
+13 -1
View File
@@ -1,5 +1,6 @@
from enum import IntEnum
import os
from collections.abc import Iterable
class ExitCode(IntEnum):
@@ -7,12 +8,23 @@ class ExitCode(IntEnum):
bad_args = 1
input_file = 2
missing_dependency = 3
invalid_output_pdfa = 4
invalid_output_pdf = 4
file_access_error = 5
already_done_ocr = 6
child_process_error = 7
encrypted_pdf = 8
other_error = 15
ctrl_c = 130
def get_program(name):
envvar = 'OCRMYPDF_' + name.upper()
return os.environ.get(envvar, name)
def page_number(input_file):
return int(os.path.basename(input_file)[0:6])
def is_iterable_notstr(thing):
return isinstance(thing, Iterable) and not isinstance(thing, str)
+1472
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+7 -2
View File
@@ -5,16 +5,21 @@ from tempfile import NamedTemporaryFile
from subprocess import Popen, PIPE, check_call
from shutil import copy
from . import get_program
from .pdfa import SRGB_ICC_PROFILE
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log):
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
pageno=1):
with NamedTemporaryFile(delete=True) as tmp:
args_gs = [
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)),
input_file
@@ -43,12 +48,12 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
"-dNOPAUSE",
'-dNumRenderingThreads=' + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=/RGB",
"-sProcessColorModel=DeviceRGB",
"-dJPEGQ=95",
"-dPDFA=2",
"-sPDFACompatibilityPolicy=2",
"-sOutputICCProfile=srgb.icc",
"-sOutputFile=" + gs_pdf.name,
]
args_gs.extend(pdf_pages)
+348 -168
View File
@@ -10,11 +10,16 @@
from __future__ import print_function, absolute_import, division
import argparse
import ctypes as C
import sys
import os
import logging
from tempfile import TemporaryFile
from ctypes.util import find_library
from .lib._leptonica import ffi
from functools import lru_cache
from enum import Enum
lept = ffi.dlopen(find_library('lept'))
logger = logging.getLogger(__name__)
@@ -25,67 +30,6 @@ def stderr(*objs):
print("leptonica.py:", *objs, file=sys.stderr)
from ctypes.util import find_library
lept_lib = find_library('lept')
if not lept_lib:
stderr("Could not find the Leptonica library")
sys.exit(3)
try:
lept = C.cdll.LoadLibrary(lept_lib)
except Exception:
stderr("Could not load the Leptonica library from %s", lept_lib)
sys.exit(3)
class _PIXCOLORMAP(C.Structure):
"""struct PixColormap from Leptonica src/pix.h
"""
_fields_ = [
("array", C.c_void_p),
("depth", C.c_int32),
("nalloc", C.c_int32),
("n", C.c_int32)
]
class _PIX(C.Structure):
"""struct Pix from Leptonica src/pix.h
"""
_fields_ = [
("w", C.c_uint32),
("h", C.c_uint32),
("d", C.c_uint32),
("wpl", C.c_uint32),
("refcount", C.c_uint32),
("xres", C.c_int32),
("yres", C.c_int32),
("informat", C.c_int32),
("text", C.POINTER(C.c_char)),
("colormap", C.POINTER(_PIXCOLORMAP)),
("data", C.POINTER(C.c_uint32))
]
PIX = C.POINTER(_PIX)
lept.pixRead.argtypes = [C.c_char_p]
lept.pixRead.restype = PIX
lept.pixScale.argtypes = [PIX, C.c_float, C.c_float]
lept.pixScale.restype = PIX
lept.pixDeskew.argtypes = [PIX, C.c_int32]
lept.pixDeskew.restype = PIX
lept.pixFindSkew.argtypes = [PIX, C.POINTER(C.c_float), C.POINTER(C.c_float)]
lept.pixFindSkew.restype = C.c_int32
lept.pixWriteImpliedFormat.argtypes = [C.c_char_p, PIX, C.c_int32, C.c_int32]
lept.pixWriteImpliedFormat.restype = C.c_int32
lept.pixDestroy.argtypes = [C.POINTER(PIX)]
lept.pixDestroy.restype = None
lept.getLeptonicaVersion.argtypes = []
lept.getLeptonicaVersion.restype = C.c_char_p
class LeptonicaErrorTrap(object):
"""Context manager to trap errors reported by Leptonica.
@@ -140,103 +84,369 @@ class LeptonicaIOError(LeptonicaError):
pass
def pixRead(filename):
"""Load an image file into a PIX object.
class RemoveColormap(Enum):
to_binary = 0
to_grayscale = 1
to_full_color = 2
based_on_src = 3
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If loading
fails then the object will wrap a C null pointer.
class Pix:
"""Wrapper around leptonica's PIX object.
Leptonica uses referencing counting on PIX objects. Also, many Leptonica
functions return the original object with an increased reference count
if the operation had no effect (for example, image skew was found to be 0).
This has complications for memory management in Python. Whenever Leptonica
returns a PIX object (new or old), we wrap it in this class, which
registers it with the FFI garbage collector. pixDestroy() decrements the
reference count and only destroys when the last reference is removed.
Leptonica's reference counting is not threadsafe. This class can be used
in a threadsafe manner if a Python threading.Lock protects the data.
"""
with LeptonicaErrorTrap():
return lept.pixRead(filename.encode(sys.getfilesystemencoding()))
def __init__(self, pix):
self._pix = ffi.gc(pix, Pix._pix_destroy)
def pixScale(pix, scalex, scaley):
"""Returns the pix object rescaled according to the proportions given."""
with LeptonicaErrorTrap():
return lept.pixScale(pix, scalex, scaley)
def pixDeskew(pix, reduction_factor=0):
"""Returns the deskewed pix object.
A clone of the original is returned when the algorithm cannot find a skew
angle with sufficient confidence.
reduction_factor -- amount to downsample (0 for default) when searching
for skew angle
"""
with LeptonicaErrorTrap():
return lept.pixDeskew(pix, reduction_factor)
def pixFindSkew(pix):
"""Returns a tuple (deskew angle in degrees, confidence value).
Returns (None, None) if no angle is available.
"""
with LeptonicaErrorTrap():
angle = C.c_float(0.0)
confidence = C.c_float(0.0)
result = lept.pixFindSkew(pix, C.byref(angle), C.byref(confidence))
if result == 0:
return (angle.value, confidence.value)
def __repr__(self):
if self._pix:
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
return s.format(self._pix.w, self._pix.h, self._pix.d,
int(ffi.cast("intptr_t", self._pix)))
else:
return (None, None)
return "<leptonica.Pix image NULL>"
def __getstate__(self):
data = ffi.new('l_uint32 **')
size = ffi.new('size_t *')
err = lept.pixSerializeToMemory(self._pix, data, size)
if err != 0:
raise LeptonicaIOError("pixSerializeToMemory")
char_data = ffi.cast('char *', data[0])
# Copy from C bytes to python bytes()
data_bytes = ffi.buffer(char_data, size[0])[:]
# Can now free C bytes
lept.lept_free(char_data)
return dict(data=data_bytes)
def __setstate__(self, state):
cdata_bytes = ffi.new('char[]', state['data'])
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
pix = lept.pixDeserializeFromMemory(
cdata_uint32, len(state['data']))
Pix.__init__(self, pix)
@property
def width(self):
return self._pix.w
@property
def height(self):
return self._pix.h
@property
def depth(self):
return self._pix.d
@property
def size(self):
return (self._pix.w, self._pix.h)
@property
def info(self):
return {'dpi': (self._pix.xres, self._pix.yres)}
@property
def mode(self):
"Return mode like PIL.Image"
if self.depth == 1:
return '1'
elif self.depth >= 16:
return 'RGB'
elif not self._pix.colormap:
return 'L'
else:
return 'P'
@classmethod
def read(cls, filename):
"""Load an image file into a PIX object.
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
loading fails then the object will wrap a C null pointer.
"""
with LeptonicaErrorTrap():
return cls(lept.pixRead(
filename.encode(sys.getfilesystemencoding())))
def write_implied_format(
self, filename, jpeg_quality=0, jpeg_progressive=0):
"""Write pix to the filename, with the extension indicating format.
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
"""
with LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
filename.encode(sys.getfilesystemencoding()),
self._pix, jpeg_quality, jpeg_progressive)
def topil(self):
"Returns a PIL.Image version of this Pix"
from PIL import Image
with LeptonicaErrorTrap():
pix_swapped = Pix(lept.pixEndianByteSwapNew(self._pix))
size = (pix_swapped._pix.wpl * 4, pix_swapped._pix.h)
buf = ffi.buffer(pix_swapped._pix.data, size[0] * size[1])
im_raw = Image.frombytes(self.mode, size, buf, 'raw')
# Leptonica stores images in 32-bit words
# Need to crop the any trailing amount
box = (0, 0, self.width, self.height)
im = im_raw.crop(box)
return im
def show(self):
return self.topil().show()
def deskew(self, reduction_factor=0):
"""Returns the deskewed pix object.
A clone of the original is returned when the algorithm cannot find a
skew angle with sufficient confidence.
reduction_factor -- amount to downsample (0 for default) when searching
for skew angle
"""
with LeptonicaErrorTrap():
return Pix(lept.pixDeskew(self._pix, reduction_factor))
def scale(self, scalex, scaley):
"Returns the pix object rescaled according to the proportions given."
with LeptonicaErrorTrap():
return Pix(lept.pixScale(self._pix, scalex, scaley))
def rotate180(self):
with LeptonicaErrorTrap():
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
def rotate_orth(self, quads):
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
with LeptonicaErrorTrap():
return Pix(lept.pixRotateOrth(self._pix, quads))
def find_skew(self):
"""Returns a tuple (deskew angle in degrees, confidence value).
Returns (None, None) if no angle is available.
"""
with LeptonicaErrorTrap():
angle = ffi.new('float *', 0.0)
confidence = ffi.new('float *', 0.0)
result = lept.pixFindSkew(self._pix, angle, confidence)
if result == 0:
return (angle[0], confidence[0])
else:
return (None, None)
def convert_rgb_to_luminance(self):
with LeptonicaErrorTrap():
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
if gray_pix:
return Pix(gray_pix)
return None
def remove_colormap(self, removal_type):
"""Remove a palette
removal_type - RemovalColormap()
"""
with LeptonicaErrorTrap():
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
def otsu_adaptive_threshold(
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
p_pix = ffi.new('PIX **')
result = lept.pixOtsuAdaptiveThreshold(
self._pix,
sx, sy,
smoothx, smoothy,
scorefract,
ffi.NULL,
p_pix)
if result == 0:
return Pix(p_pix[0])
else:
return None
def otsu_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
bgval=255, kernel_size=(2, 2), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
if mask is None:
mask = ffi.NULL
if isinstance(mask, Pix):
mask = mask._pix
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
self._pix,
mask,
sx, sy,
thresh, mincount, bgval,
smoothx, smoothy,
scorefract,
ffi.NULL
)
if thresh_pix == ffi.NULL:
return None
return Pix(thresh_pix)
def crop_to_foreground(
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
showmorph=0, display=0, pdfdir=ffi.NULL):
with LeptonicaErrorTrap():
cropbox = Box(lept.pixFindPageForeground(
self._pix,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir))
print(repr(cropbox))
cropped_pix = lept.pixClipRectangle(
self._pix,
cropbox._box,
ffi.NULL)
return Pix(cropped_pix)
def clean_background_to_white(
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
with LeptonicaErrorTrap():
return Pix(lept.pixCleanBackgroundToWhite(
self._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white))
@staticmethod
@lru_cache(maxsize=1)
def make_pixel_sum_tab8():
return lept.makePixelSumTab8()
@staticmethod
def correlation_binary(pix1, pix2):
if get_leptonica_version() < 'leptonica-1.72':
# Older versions of Leptonica (pre-1.72) have a buggy
# implementation of pixCorrelationBinary that overflows on larger
# images. Ubuntu trusty has 1.70. Ubuntu PPA
# ppa:rebuntu16/avidemux+unofficial has "leptonlib" 1.73.
pix1_count = ffi.new('l_int32 *')
pix2_count = ffi.new('l_int32 *')
pixn_count = ffi.new('l_int32 *')
tab8 = Pix.make_pixel_sum_tab8()
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
# Python converts these int32s to larger units as needed
# to avoid overflow. Overflow happens easily here.
correlation = (
(pixn_count[0] * pixn_count[0]) /
(pix1_count[0] * pix2_count[0])
)
return correlation
else:
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
return correlation[0]
@staticmethod
def _pix_destroy(pix):
p_pix = ffi.new('PIX **', pix)
lept.pixDestroy(p_pix)
# print('pix destroy ' + repr(pix))
def pixWriteImpliedFormat(filename, pix, jpeg_quality=0, jpeg_progressive=0):
"""Write pix to the filename, with the extension indicating format.
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
class Box:
"""Wrapper around Leptonica's BOX objects.
See class Pix for notes about reference counting.
"""
fileroot, extension = os.path.splitext(filename)
fix_pnm = False
if extension.lower() in ('.pbm', '.pgm', '.ppm'):
# Leptonica does not process handle these extensions correctly, but
# does handle .pnm correctly. Add another .pnm suffix.
filename += '.pnm'
fix_pnm = True
with LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
filename.encode(sys.getfilesystemencoding()),
pix, jpeg_quality, jpeg_progressive)
def __init__(self, box):
self._box = ffi.gc(box, Box._box_destroy)
if fix_pnm:
from shutil import move
move(filename, filename[:-4]) # Remove .pnm suffix
def __repr__(self):
if self._box:
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
self.x, self.y, self.w, self.h)
return '<leptonica.Box NULL>'
@property
def x(self):
return self._box.x
@property
def y(self):
return self._box.y
@property
def w(self):
return self._box.w
@property
def h(self):
return self._box.h
@staticmethod
def _box_destroy(box):
p_box = ffi.new('BOX **', box)
lept.boxDestroy(p_box)
def pixDestroy(pix):
"""Destroy the pix object.
Function signature is pixDestroy(struct Pix **), hence C.byref() to pass
the address of the pointer.
"""
with LeptonicaErrorTrap():
lept.pixDestroy(C.byref(pix))
def getLeptonicaVersion():
@lru_cache(maxsize=1)
def get_leptonica_version():
"""Get Leptonica version string.
Caveat: Leptonica expects the caller to free this memory. We don't,
since that would involve binding to libc to access libc.free(),
a pointless effort to reclaim 100 bytes of memory.
"""
return lept.getLeptonicaVersion().decode()
return ffi.string(lept.getLeptonicaVersion()).decode()
def deskew(infile, outfile, dpi):
try:
pix_source = pixRead(infile)
pix_source = Pix.read(infile)
except LeptonicaIOError:
raise LeptonicaIOError("Failed to open file: %s" % infile)
@@ -244,14 +454,12 @@ def deskew(infile, outfile, dpi):
reduction_factor = 1 # Don't downsample too much if DPI is already low
else:
reduction_factor = 0 # Use default
pix_deskewed = pixDeskew(pix_source, reduction_factor)
pix_deskewed = pix_source.deskew(reduction_factor)
try:
pixWriteImpliedFormat(outfile, pix_deskewed)
pix_deskewed.write_implied_format(outfile)
except LeptonicaIOError:
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
pixDestroy(pix_source)
pixDestroy(pix_deskewed)
if __name__ == '__main__':
@@ -270,39 +478,12 @@ if __name__ == '__main__':
args = parser.parse_args()
if getLeptonicaVersion() != u'leptonica-1.69':
if get_leptonica_version() != u'leptonica-1.69':
print("Unexpected leptonica version: %s" % getLeptonicaVersion())
args.func(args)
def _test_output(mode, extension, im_format):
from PIL import Image
from tempfile import NamedTemporaryFile
with NamedTemporaryFile(prefix='test-lept-pnm', suffix=extension, delete=True) as tmpfile:
im = Image.new(mode=mode, size=(100, 100))
im.save(tmpfile)
pix = pixRead(tmpfile.name)
pixWriteImpliedFormat(tmpfile.name, pix)
pixDestroy(pix)
im_roundtrip = Image.open(tmpfile.name)
assert im_roundtrip.mode == im.mode, "leptonica mode differs"
assert im_roundtrip.format == im_format, \
"{0}: leptonica produced a {1}".format(
extension,
im_roundtrip.format)
def test_pnm_output():
params = [['1', '.pbm', 'PPM'], ['L', '.pgm', 'PPM'],
['RGB', '.ppm', 'PPM']]
for param in params:
_test_output(*param)
def test_skew_angle():
from PIL import Image, ImageDraw
from tempfile import NamedTemporaryFile
@@ -325,7 +506,6 @@ def test_skew_angle():
rotated_im.save(tmpfile)
pix = pixRead(tmpfile.name)
angle, confidence = pixFindSkew(pix)
pixDestroy(pix)
print('{0} {1} {2}'.format(rotate_angle, angle, confidence), file=sys.stderr)
View File
+142
View File
@@ -0,0 +1,142 @@
from cffi import FFI
ffi = FFI()
ffi.set_source("ocrmypdf.lib._leptonica", None)
ffi.cdef("""
typedef signed char l_int8;
typedef unsigned char l_uint8;
typedef short l_int16;
typedef unsigned short l_uint16;
typedef int l_int32;
typedef unsigned int l_uint32;
typedef float l_float32;
typedef double l_float64;
typedef long long l_int64;
typedef unsigned long long l_uint64;
struct Pix
{
l_uint32 w; /* width in pixels */
l_uint32 h; /* height in pixels */
l_uint32 d; /* depth in bits (bpp) */
l_uint32 spp; /* number of samples per pixel */
l_uint32 wpl; /* 32-bit words/line */
l_uint32 refcount; /* reference count (1 if no clones) */
l_int32 xres; /* image res (ppi) in x direction */
/* (use 0 if unknown) */
l_int32 yres; /* image res (ppi) in y direction */
/* (use 0 if unknown) */
l_int32 informat; /* input file format, IFF_* */
l_int32 special; /* special instructions for I/O, etc */
char *text; /* text string associated with pix */
struct PixColormap *colormap; /* colormap (may be null) */
l_uint32 *data; /* the image data */
};
typedef struct Pix PIX;
struct PixColormap
{
void *array; /* colormap table (array of RGBA_QUAD) */
l_int32 depth; /* of pix (1, 2, 4 or 8 bpp) */
l_int32 nalloc; /* number of color entries allocated */
l_int32 n; /* number of color entries used */
};
typedef struct PixColormap PIXCMAP;
struct Box
{
l_int32 x;
l_int32 y;
l_int32 w;
l_int32 h;
l_uint32 refcount; /* reference count (1 if no clones) */
};
typedef struct Box BOX;
""")
ffi.cdef("""
PIX * pixRead ( const char *filename );
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
void pixDestroy ( PIX **ppix );
PIX *
pixEndianByteSwapNew(PIX *pixs);
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
char * getLeptonicaVersion ( );
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
PIX *pixRotate180(PIX *pixd, PIX *pixs);
PIX *
pixRotateOrth(PIX *pixs,
l_int32 quads);
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
l_int32 * makePixelSumTab8 ( void );
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
PIX * pixConvertRGBToLuminance(PIX *pixs);
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
l_int32
pixOtsuAdaptiveThreshold(PIX *pixs,
l_int32 sx,
l_int32 sy,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
PIX **ppixth,
PIX **ppixd);
PIX *
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
PIX *pixim,
l_int32 sx,
l_int32 sy,
l_int32 thresh,
l_int32 mincount,
l_int32 bgval,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
l_int32 *pthresh);
PIX *
pixCleanBackgroundToWhite(PIX *pixs,
PIX *pixim,
PIX *pixg,
l_float32 gamma,
l_int32 blackval,
l_int32 whiteval);
BOX *
pixFindPageForeground(PIX *pixs,
l_int32 threshold,
l_int32 mindist,
l_int32 erasedist,
l_int32 pagenum,
l_int32 showmorph,
l_int32 display,
const char *pdfdir);
PIX *
pixClipRectangle(PIX *pixs,
BOX *box,
BOX **pboxc);
void
boxDestroy(BOX **pbox);
void lept_free(void *ptr);
""")
if __name__ == '__main__':
ffi.compile()
Executable → Regular
+4 -875
View File
@@ -1,878 +1,7 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2015-16 James R. Barlow: github.com/jbarlow83
from contextlib import suppress
from tempfile import mkdtemp
import sys
import os
import re
import shutil
import warnings
import multiprocessing
import atexit
import textwrap
import img2pdf
# This file is now an alias for __main__
# Consider removing in future releases
import PyPDF2 as pypdf
from PIL import Image
from functools import partial
from ruffus import transform, suffix, merge, active_if, regex, jobs_limit, \
formatter, follows, split, collate, check_if_uptodate
import ruffus.ruffus_exceptions as ruffus_exceptions
import ruffus.cmdline as cmdline
from .hocrtransform import HocrTransform
from .pageinfo import pdf_get_all_pageinfo
from .pdfa import generate_pdfa_def
from . import ghostscript
from . import tesseract
from . import qpdf
from . import ExitCode
import pkg_resources
VERSION = pkg_resources.get_distribution('ocrmypdf').version
warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
BASEDIR = os.path.dirname(os.path.realpath(__file__))
# -------------
# External dependencies
MINIMUM_TESS_VERSION = '3.02.02'
def complain(message):
print(*textwrap.wrap(message), file=sys.stderr)
if tesseract.version() < MINIMUM_TESS_VERSION:
complain(
"Please install tesseract {0} or newer "
"(currently installed version is {1})".format(
MINIMUM_TESS_VERSION, tesseract.version()))
sys.exit(ExitCode.missing_dependency)
try:
import PIL.features
check_codec = PIL.features.check_codec
except (ImportError, AttributeError):
def check_codec(codec_name):
if codec_name == 'jpg':
return 'jpeg_encoder' in dir(Image.core)
elif codec_name == 'zlib':
return 'zip_encoder' in dir(Image.core)
raise NotImplementedError(codec_name)
def check_pil_encoder(codec_name, friendly_name):
try:
if check_codec(codec_name):
return
except Exception:
pass
complain(
"ERROR: Your version of the Python imaging library (Pillow) was "
"compiled without support for " + friendly_name + " encoding/decoding."
"\n"
"You will need to uninstall Pillow and reinstall it with PNG and JPEG "
"support (libjpeg and zlib)."
"\n"
"See installation instructions for your platform here:\n"
" https://pillow.readthedocs.org/installation.html"
)
sys.exit(ExitCode.missing_dependency)
check_pil_encoder('jpg', 'JPEG')
check_pil_encoder('zlib', 'PNG')
# -------------
# Parser
parser = cmdline.get_argparse(
prog="ocrmypdf",
description="Generate searchable PDF file from an image-only PDF file.",
version=VERSION,
fromfile_prefix_chars='@',
ignored_args=[
'touch_files_only', 'recreate_database', 'checksum_file_name',
'key_legend_in_graph', 'draw_graph_horizontally', 'flowchart_format',
'forced_tasks', 'target_tasks', 'use_threads', 'jobs'])
parser.add_argument(
'input_file',
help="PDF file containing the images to be OCRed")
parser.add_argument(
'output_file',
help="output searchable PDF file")
parser.add_argument(
'-l', '--language', action='append',
help="languages of the file to be OCRed")
parser.add_argument(
'-j', '--jobs', metavar='N', type=int,
help="Use up to N CPU cores simultaneously (default: use all)")
metadata = parser.add_argument_group(
"Metadata options",
"Set output PDF/A metadata (default: use input document's title)")
metadata.add_argument(
'--title', type=str,
help="set document title (place multiple words in quotes)")
metadata.add_argument(
'--author', type=str,
help="set document author")
metadata.add_argument(
'--subject', type=str,
help="set document")
metadata.add_argument(
'--keywords', type=str,
help="set document keywords")
preprocessing = parser.add_argument_group(
"Preprocessing options",
"Improve OCR quality and final image")
preprocessing.add_argument(
'-d', '--deskew', action='store_true',
help="deskew each page before performing OCR")
preprocessing.add_argument(
'-c', '--clean', action='store_true',
help="clean pages from scanning artifacts before performing OCR")
preprocessing.add_argument(
'-i', '--clean-final', action='store_true',
help="incorporate the cleaned image in the final PDF file")
preprocessing.add_argument(
'--oversample', metavar='DPI', type=int, default=0,
help="oversample images to at least the specified DPI, to improve OCR "
"results slightly")
parser.add_argument(
'-f', '--force-ocr', action='store_true',
help="rasterize any fonts or vector images on each page and apply OCR")
parser.add_argument(
'-s', '--skip-text', action='store_true',
help="skip OCR on any pages that already contain text, but include the"
" page in final output")
parser.add_argument(
'--skip-big', type=float, metavar='MPixels',
help="skip OCR on pages larger than the specified amount of megapixels, "
"but include skipped pages in final output")
# parser.add_argument(
# '--exact-image', action='store_true',
# help="Use original page from PDF without re-rendering")
advanced = parser.add_argument_group(
"Advanced",
"Advanced options for power users")
advanced.add_argument(
'--tesseract-config', action='append', metavar='CFG', default=[],
help="additional Tesseract configuration files")
advanced.add_argument(
'--tesseract-pagesegmode', action='store', type=int, metavar='PSM',
help="set Tesseract page segmentation mode (see tesseract --help)")
advanced.add_argument(
'--pdf-renderer', choices=['auto', 'tesseract', 'hocr'], default='auto',
help='choose OCR PDF renderer')
advanced.add_argument(
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
help='give up on OCR after the timeout, but copy the preprocessed page '
'into the final output')
debugging = parser.add_argument_group(
"Debugging",
"Arguments to help with troubleshooting and debugging")
debugging.add_argument(
'-k', '--keep-temporary-files', action='store_true',
help="keep temporary files (helpful for debugging)")
debugging.add_argument(
'-g', '--debug-rendering', action='store_true',
help="render each page twice with debug information on second page")
options = parser.parse_args()
# ----------
# Languages
if not options.language:
options.language = ['eng'] # Enforce English hegemony
# Support v2.x "eng+deu" language syntax
if '+' in options.language[0]:
options.language = options.language[0].split('+')
if not set(options.language).issubset(tesseract.languages()):
complain(
"The installed version of tesseract does not have language "
"data for the following requested languages: ")
for lang in (set(options.language) - tesseract.languages()):
complain(lang)
sys.exit(ExitCode.bad_args)
# ----------
# Arguments
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'hocr'
if any((options.deskew, options.clean, options.clean_final)):
try:
from . import unpaper
except ImportError:
complain(
"Install the 'unpaper' program to use --deskew or --clean.")
sys.exit(ExitCode.bad_args)
else:
unpaper = None
if options.debug_rendering and options.pdf_renderer == 'tesseract':
complain(
"Ignoring --debug-rendering because it is not supported with"
"--pdf-renderer=tesseract.")
if options.force_ocr and options.skip_text:
complain(
"Error: --force-ocr and --skip-text are mutually incompatible.")
sys.exit(ExitCode.bad_args)
if options.clean and not options.clean_final \
and options.pdf_renderer == 'tesseract':
complain(
"Tesseract PDF renderer cannot render --clean pages without "
"also performing --clean-final, so --clean-final is assumed.")
lossless_reconstruction = False
if options.pdf_renderer == 'hocr':
if not options.deskew and not options.clean_final and not options.force_ocr:
lossless_reconstruction = True
# ----------
# Logging
_logger, _logger_mutex = cmdline.setup_logging(__name__, options.log_file,
options.verbose)
class WrappedLogger:
def __init__(self, my_logger, my_mutex):
self.logger = my_logger
self.mutex = my_mutex
def log(self, *args, **kwargs):
with self.mutex:
self.logger.log(*args, **kwargs)
def debug(self, *args, **kwargs):
with self.mutex:
self.logger.debug(*args, **kwargs)
def info(self, *args, **kwargs):
with self.mutex:
self.logger.info(*args, **kwargs)
def warning(self, *args, **kwargs):
with self.mutex:
self.logger.warning(*args, **kwargs)
def error(self, *args, **kwargs):
with self.mutex:
self.logger.error(*args, **kwargs)
def critical(self, *args, **kwargs):
with self.mutex:
self.logger.critical(*args, **kwargs)
_log = WrappedLogger(_logger, _logger_mutex)
def re_symlink(input_file, soft_link_name, log=_log):
"""
Helper function: relinks soft symbolic link if necessary
"""
# Guard against soft linking to oneself
if input_file == soft_link_name:
log.debug("Warning: No symbolic link made. You are using " +
"the original data directory as the working directory.")
return
# Soft link already exists: delete for relink?
if os.path.lexists(soft_link_name):
# do not delete or overwrite real (non-soft link) file
if not os.path.islink(soft_link_name):
raise Exception("%s exists and is not a link" % soft_link_name)
try:
os.unlink(soft_link_name)
except:
log.debug("Can't unlink %s" % (soft_link_name))
if not os.path.exists(input_file):
raise Exception("trying to create a broken symlink to %s" % input_file)
log.debug("os.symlink(%s, %s)" % (input_file, soft_link_name))
# Create symbolic link using absolute path
os.symlink(
os.path.abspath(input_file),
soft_link_name
)
# -------------
# The Pipeline
manager = multiprocessing.Manager()
_pdfinfo = manager.list()
_pdfinfo_lock = manager.Lock()
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
@atexit.register
def cleanup_working_files(*args):
if options.keep_temporary_files:
print("Temporary working files saved at:")
print(work_folder)
else:
with suppress(FileNotFoundError):
shutil.rmtree(work_folder)
@transform(
input=options.input_file,
filter=formatter('(?i)\.pdf'),
output=work_folder + '{basename[0]}.repaired.pdf',
extras=[_log, _pdfinfo, _pdfinfo_lock])
def repair_pdf(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
qpdf.repair(input_file, output_file, log)
with pdfinfo_lock:
pdfinfo.extend(pdf_get_all_pageinfo(output_file))
log.info(pdfinfo)
def get_pageinfo(input_file, pdfinfo, pdfinfo_lock):
pageno = int(os.path.basename(input_file)[0:6]) - 1
with pdfinfo_lock:
pageinfo = pdfinfo[pageno].copy()
return pageinfo
def is_ocr_required(pageinfo, log):
page = pageinfo['pageno'] + 1
ocr_required = True
if not pageinfo['images']:
# If the page has no images, then it contains vector content or text
# or both. It seems quite unlikely that one would find meaningful text
# from rasterizing vector content. So skip the page.
log.info(
"Page {0} has no images - skipping OCR".format(page)
)
ocr_required = False
elif pageinfo['has_text']:
s = "Page {0} already has text! {1}"
if not options.force_ocr and not options.skip_text:
log.error(s.format(page,
"aborting (use --force-ocr to force OCR)"))
sys.exit(ExitCode.already_done_ocr)
elif options.force_ocr:
log.info(s.format(page,
"rasterizing text and running OCR anyway"))
ocr_required = True
elif options.skip_text:
log.info(s.format(page,
"skipping all processing on this page"))
ocr_required = False
if ocr_required and options.skip_big:
pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels']
if pixel_count > (options.skip_big * 1000000):
ocr_required = False
log.info(
"Page {0} is very large; skipping due to -b".format(page))
return ocr_required
@split(
repair_pdf,
os.path.join(work_folder, '*.page.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def split_pages(
input_file,
output_files,
log,
pdfinfo,
pdfinfo_lock):
for oo in output_files:
with suppress(FileNotFoundError):
os.unlink(oo)
npages = qpdf.get_npages(input_file)
qpdf.split_pages(input_file, work_folder, npages)
from glob import glob
for filename in glob(os.path.join(work_folder, '*.page.pdf')):
pageinfo = get_pageinfo(filename, pdfinfo, pdfinfo_lock)
alt_suffix = '.ocr.page.pdf' if is_ocr_required(pageinfo, log) \
else '.skip.page.pdf'
re_symlink(
filename,
os.path.join(
work_folder,
os.path.basename(filename)[0:6] + alt_suffix))
@transform(
input=split_pages,
filter=suffix('.ocr.page.pdf'),
output='.page.png',
output_dir=work_folder,
extras=[_log, _pdfinfo, _pdfinfo_lock])
def rasterize_with_ghostscript(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
device = 'png16m' # 24-bit
if all(image['comp'] == 1 for image in pageinfo['images']):
if all(image['bpc'] == 1 for image in pageinfo['images']):
device = 'pngmono'
elif all(image['bpc'] > 1 and image['color'] == 'index'
for image in pageinfo['images']):
device = 'png256'
elif all(image['bpc'] > 1 and image['color'] == 'gray'
for image in pageinfo['images']):
device = 'pnggray'
log.debug("Rendering {0} with {1}".format(
os.path.basename(input_file), device))
xres = max(pageinfo['xres'], options.oversample or 0)
yres = max(pageinfo['yres'], options.oversample or 0)
ghostscript.rasterize_pdf(input_file, output_file, xres, yres, device, log)
@transform(
input=rasterize_with_ghostscript,
filter=suffix(".page.png"),
output=".pp-deskew.png",
extras=[_log, _pdfinfo, _pdfinfo_lock])
def preprocess_deskew(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
if not options.deskew:
re_symlink(input_file, output_file, log)
return
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
dpi = int(pageinfo['xres'])
unpaper.deskew(input_file, output_file, dpi, log)
@transform(
input=preprocess_deskew,
filter=suffix(".pp-deskew.png"),
output=".pp-clean.png",
extras=[_log, _pdfinfo, _pdfinfo_lock])
def preprocess_clean(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
if not options.clean:
re_symlink(input_file, output_file, log)
return
pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock)
dpi = int(pageinfo['xres'])
unpaper.clean(input_file, output_file, dpi, log)
@active_if(options.pdf_renderer == 'hocr')
@transform(
input=preprocess_clean,
filter=suffix(".pp-clean.png"),
output=".hocr",
extras=[_log, _pdfinfo, _pdfinfo_lock])
def ocr_tesseract_hocr(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
tesseract.generate_hocr(
input_file=input_file,
output_hocr=output_file,
language=options.language,
tessconfig=options.tesseract_config,
timeout=options.tesseract_timeout,
pageinfo_getter=partial(get_pageinfo, input_file, pdfinfo,
pdfinfo_lock),
pagesegmode=options.tesseract_pagesegmode,
log=log
)
@active_if(options.pdf_renderer == 'hocr')
@collate(
input=[rasterize_with_ghostscript, preprocess_deskew, preprocess_clean],
filter=regex(r".*/(\d{6})(?:\.page|\.pp-deskew|\.pp-clean)\.png"),
output=os.path.join(work_folder, r'\1.image'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def select_image_for_pdf(
infiles,
output_file,
log,
pdfinfo,
pdfinfo_lock):
if options.clean_final:
image_suffix = '.pp-clean.png'
elif options.deskew:
image_suffix = '.pp-deskew.png'
else:
image_suffix = '.page.png'
image = next(ii for ii in infiles if ii.endswith(image_suffix))
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
if all(image['enc'] == 'jpeg' for image in pageinfo['images']):
# If all images were JPEGs originally, produce a JPEG as output
Image.open(image).save(output_file, format='JPEG')
else:
re_symlink(image, output_file)
@active_if(options.pdf_renderer == 'hocr')
@collate(
input=[select_image_for_pdf, split_pages],
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.page\.pdf)"),
output=os.path.join(work_folder, r'\1.image-layer.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def select_image_layer(
infiles,
output_file,
log,
pdfinfo,
pdfinfo_lock):
page_pdf = next(ii for ii in infiles if ii.endswith('.page.pdf'))
image = next(ii for ii in infiles if ii.endswith('.image'))
if lossless_reconstruction:
re_symlink(page_pdf, output_file)
else:
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
with open(output_file, 'wb') as pdf:
img2pdf.convert([image], dpi=dpi, outputstream=pdf)
@active_if(options.pdf_renderer == 'hocr')
@transform(
input=ocr_tesseract_hocr,
filter=suffix('.hocr'),
output='.hocr.pdf',
extras=[_log, _pdfinfo, _pdfinfo_lock])
def render_hocr_page(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
hocr = input_file
pageinfo = get_pageinfo(hocr, pdfinfo, pdfinfo_lock)
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
hocrtransform = HocrTransform(hocr, dpi)
hocrtransform.to_pdf(output_file, imageFileName=None,
showBoundingboxes=False, invisibleText=True)
@active_if(options.pdf_renderer == 'hocr')
@active_if(options.debug_rendering)
@collate(
input=[select_image_for_pdf, ocr_tesseract_hocr],
filter=regex(r".*/(\d{6})(?:\.image|\.hocr)"),
output=os.path.join(work_folder, r'\1.debug.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def render_hocr_debug_page(
infiles,
output_file,
log,
pdfinfo,
pdfinfo_lock):
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
image = next(ii for ii in infiles if ii.endswith('.image'))
pageinfo = get_pageinfo(image, pdfinfo, pdfinfo_lock)
dpi = round(max(pageinfo['xres'], pageinfo['yres'], options.oversample))
hocrtransform = HocrTransform(hocr, dpi)
hocrtransform.to_pdf(output_file, imageFileName=None,
showBoundingboxes=True, invisibleText=False)
@active_if(options.pdf_renderer == 'hocr')
@collate(
input=[render_hocr_page, select_image_layer],
filter=regex(r".*/(\d{6})(?:\.hocr\.pdf|\.image-layer\.pdf)"),
output=os.path.join(work_folder, r'\1.rendered.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def add_text_layer(
infiles,
output_file,
log,
pdfinfo,
pdfinfo_lock):
text = next(ii for ii in infiles if ii.endswith('.hocr.pdf'))
image = next(ii for ii in infiles if ii.endswith('.image-layer.pdf'))
pdf_output = pypdf.PdfFileWriter()
pdf_text = pypdf.PdfFileReader(open(text, "rb"))
pdf_image = pypdf.PdfFileReader(open(image, "rb"))
page = pdf_text.getPage(0)
page.mergePage(pdf_image.getPage(0))
pdf_output.addPage(page)
with open(output_file, "wb") as out:
pdf_output.write(out)
@active_if(options.pdf_renderer == 'tesseract')
@collate(
input=[preprocess_clean, split_pages],
filter=regex(r".*/(\d{6})(?:\.pp-clean\.png|\.page\.pdf)"),
output=os.path.join(work_folder, r'\1.rendered.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def tesseract_ocr_and_render_pdf(
input_files,
output_file,
log,
pdfinfo,
pdfinfo_lock):
input_image = next((ii for ii in input_files if ii.endswith('.png')), '')
input_pdf = next((ii for ii in input_files if ii.endswith('.pdf')))
if not input_image:
# Skipping this page
re_symlink(input_pdf, output_file)
return
tesseract.generate_pdf(
input_image=input_image,
skip_pdf=input_pdf,
output_pdf=output_file,
language=options.language,
tessconfig=options.tesseract_config,
timeout=options.tesseract_timeout,
pagesegmode=options.tesseract_pagesegmode,
log=log)
@transform(
input=repair_pdf,
filter=formatter(r'\.repaired\.pdf'),
output=os.path.join(work_folder, 'pdfa_def.ps'),
extras=[_log])
def generate_postscript_stub(
input_file,
output_file,
log):
pdf = pypdf.PdfFileReader(input_file)
def from_document_info(key):
# pdf.documentInfo.get() DOES NOT behave as expected for a dict-like
# object, so call with precautions. TypeError may occur if the PDF
# is missing the optional document info section.
try:
s = pdf.documentInfo[key]
return str(s)
except (KeyError, TypeError):
return ''
pdfmark = {
'title': from_document_info('/Title'),
'author': from_document_info('/Author'),
'keywords': from_document_info('/Keywords'),
'subject': from_document_info('/Subject'),
}
if options.title:
pdfmark['title'] = options.title
if options.author:
pdfmark['author'] = options.author
if options.keywords:
pdfmark['keywords'] = options.keywords
if options.subject:
pdfmark['subject'] = options.subject
pdfmark['creator'] = '{0} {1} / Tesseract OCR{2} {3}'.format(
parser.prog, VERSION,
'+PDF' if options.pdf_renderer == 'tesseract' else '',
tesseract.version())
generate_pdfa_def(output_file, pdfmark)
@transform(
input=split_pages,
filter=suffix('.skip.page.pdf'),
output='.done.pdf',
output_dir=work_folder,
extras=[_log])
def skip_page(
input_file,
output_file,
log):
re_symlink(input_file, output_file, log)
@merge(
input=[add_text_layer, render_hocr_debug_page, skip_page,
tesseract_ocr_and_render_pdf, generate_postscript_stub],
output=os.path.join(work_folder, 'merged.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def merge_pages(
input_files,
output_file,
log,
pdfinfo,
pdfinfo_lock):
def input_file_order(s):
'''Sort order: All rendered pages followed
by their debug page, if any, followed by Postscript stub.
Ghostscript documentation has the Postscript stub at the
beginning, but it works at the end and also gets document info
right that way.'''
if s.endswith('.ps'):
return 99999999
key = int(os.path.basename(s)[0:6]) * 10
if 'debug' in os.path.basename(s):
key += 1
return key
pdf_pages = sorted(input_files, key=input_file_order)
log.info(pdf_pages)
ghostscript.generate_pdfa(pdf_pages, output_file, options.jobs or 1)
@transform(
input=merge_pages,
filter=formatter(),
output=options.output_file,
extras=[_log, _pdfinfo, _pdfinfo_lock])
def copy_final(
input_file,
output_file,
log,
pdfinfo,
pdfinfo_lock):
shutil.copy(input_file, output_file)
def validate_pdfa(
input_file,
log):
return qpdf.check(input_file, log)
def available_cpu_count():
try:
return multiprocessing.cpu_count()
except NotImplementedError:
pass
try:
import psutil
return psutil.cpu_count()
except (ImportError, AttributeError):
pass
complain(
"Could not get CPU count. Assuming one (1) CPU."
"Use -j N to set manually.")
return 1
def cleanup_ruffus_error_message(msg):
msg = re.sub(r'\s+', r' ', msg, re.MULTILINE)
msg = re.sub(r"\((.+?)\)", r'\1', msg)
msg = msg.strip()
return msg
def run_pipeline():
if not options.jobs:
options.jobs = available_cpu_count()
try:
options.history_file = os.path.join(work_folder, 'ruffus_history.sqlite')
cmdline.run(options)
except ruffus_exceptions.RethrownJobError as e:
if options.verbose:
print(e)
# Yuck. Hunt through the ruffus exception to find out what the
# return code is supposed to be.
for exc in e.args:
task_name, job_name, exc_name, exc_value, exc_stack = exc
if exc_name == 'builtins.SystemExit':
match = re.search(r"\.(.+?)\)", exc_value)
exit_code_name = match.groups()[0]
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
return exit_code
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
print(cleanup_ruffus_error_message(exc_value))
return ExitCode.input_file
elif exc_name == 'builtins.TypeError':
# Even though repair_pdf will fail, ruffus will still try
# to call split_pages with no input files, likely due to a bug
if task_name == 'split_pages':
print("Input file '{0}' is not a valid PDF".format(
options.input_file))
return ExitCode.input_file
return ExitCode.other_error
if not validate_pdfa(options.output_file, _log):
_log.warning('Output file: The generated PDF/A file is INVALID')
return ExitCode.invalid_output_pdfa
return ExitCode.ok
if __name__ == '__main__':
sys.exit(run_pipeline())
from ocrmypdf.__main__ import *
+264 -38
View File
@@ -3,10 +3,13 @@
from subprocess import Popen, PIPE
from decimal import Decimal, getcontext
from math import hypot
import re
import sys
import PyPDF2 as pypdf
from collections import namedtuple
matrix_mult = pypdf.pdf.utils.matrixMultiply
FRIENDLY_COLORSPACE = {
'/DeviceGray': 'gray',
@@ -19,7 +22,11 @@ FRIENDLY_COLORSPACE = {
'/Indexed': 'index',
'/Separation': 'sep',
'/DeviceN': 'devn',
'/Pattern': '-'
'/Pattern': '-',
'/G': 'gray', # Abbreviations permitted in inline images
'/RGB': 'rgb',
'/CMYK': 'cmyk',
'/I': 'index',
}
FRIENDLY_ENCODING = {
@@ -27,6 +34,13 @@ FRIENDLY_ENCODING = {
'/DCTDecode': 'jpeg',
'/JPXDecode': 'jpx',
'/JBIG2Decode': 'jbig2',
'/CCF': 'ccitt', # Abbreviations permitted in inline images
'/DCT': 'jpeg',
'/AHx': 'asciihex',
'/A85': 'ascii85',
'/LZW': 'lzw',
'/Fl': 'flate',
'/RL': 'runlength'
}
FRIENDLY_COMP = {
@@ -38,50 +52,210 @@ FRIENDLY_COMP = {
}
def _page_has_inline_images(page):
# PDF always uses \r\n for separator regardless of platform
# Really basic heuristic that might trigger the odd false positive
# This is only finds the first image and is not quite spec compliant
try:
contents = page.getContents()
data = contents.getData()
except AttributeError:
# If we can't access the contents or data (empty page?) then there
# are no inline images
return False
def _matrix_from_shorthand(shorthand):
"""Convert from PDF matrix shorthand to full matrix
begin_image, image_data, end_image = False, False, False
for data in re.split(b'\s+', data):
if data == b'BI':
begin_image = True
elif data == b'ID':
image_data = True
elif data == b'EI':
end_image = True
if all((begin_image, image_data, end_image)):
return True
return False
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
since the last column is always (0, 0, 1).
"""
a, b, c, d, e, f = map(float, shorthand)
return ((a, b, 0),
(c, d, 0),
(e, f, 1))
def _find_page_images(page, pageinfo):
def _shorthand_from_matrix(matrix):
"""Convert from transformation matrix to PDF shorthand."""
a, b = matrix[0][0], matrix[0][1]
c, d = matrix[1][0], matrix[1][1]
e, f = matrix[2][0], matrix[2][1]
return tuple(map(float, (a, b, c, d, e, f)))
RasterSettings = namedtuple('RasterSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['settings', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
def _interpret_contents(contentstream):
"""Interpret the PDF content stream
The stack represents the state of the PDF graphics stack. We are only
interested in the current transformation matrix (CTM) so we only track
this object; a full implementation would need to track many other items.
The CTM is initialized to the mapping from user space to device space.
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
to the transformation to device space. For example if set to
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
image there should be a 'cm' that sets up an image coordinate system
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
page.
PDF units suit our needs so we initialize ctm to the identity matrix.
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
"""
operations = contentstream.operations
stack = []
ctm = _matrix_from_shorthand((1, 0, 0, 1, 0, 0))
image_raster_settings = []
inline_images = []
for op in operations:
operands, command = op
if command == b'q':
stack.append(ctm)
if len(stack) > 32:
raise RuntimeError("PDF graphics stack overflow")
elif command == b'Q':
ctm = stack.pop()
elif command == b'cm':
ctm = matrix_mult(
_matrix_from_shorthand(operands), ctm)
elif command == b'Do':
image_name = operands[0]
raster = RasterSettings(
name=image_name, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
image_raster_settings.append(raster)
elif command == b'INLINE IMAGE':
settings = operands['settings']
inline = InlineSettings(
settings=settings, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
inline_images.append(inline)
return ContentsInfo(
raster_settings=image_raster_settings,
inline_images=inline_images)
def _get_dpi(ctm_shorthand, image_size):
"""Given the transformation matrix and image size, find the image DPI.
PDFs do not include image resolution information within image data.
Instead, the PDF page content stream describes the location where the
image will be rasterized, and the effective resolution is the ratio of the
pixel size to raster target size.
Normally a scanned PDF has the paper size set appropriately but this is
not guaranteed. The most common case is a cropped image will change the
page size (/CropBox) without altering the page content stream. That means
it is not sufficient to assume that the image fills the page, even though
that is the most common case.
A PDF image may be scaled (always), cropped, translated, rotated in place
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
be expressed, that is, it is not necessary to consider distortions where
the effective DPI varies with position.
To determine the image scale, transform an offset axis vector v0 (0, 0),
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
which gives the dimensions of the image in PDF units. From there we can
compare to actual image dimensions. PDF uses
row vector * matrix_tranposed unlike the traditional
matrix * column vector.
The offset, width and height vectors can be combined in a matrix and
multiplied by the transform matrix. Then we want to calculated
magnitude(width_vector - offset_vector)
and
magnitude(height_vector - offset_vector)
When the above is worked out algebraically, the effect of translation
cancels out, and the vector magnitudes become functions of the nonzero
transformation matrix indices. The results of the derivation are used
in this code.
pdfimages -list does calculate the DPI in some way that is not completely
naive, but it does not get the DPI of rotated images right, so cannot be
used anymore to validate this. Photoshop works, or using Acrobat to
rotate the image back to normal.
It does not matter if the image is partially cropped, or even out of the
/MediaBox.
"""
a, b, c, d, _, _ = ctm_shorthand
# Calculate the width and height of the image in PDF units
image_drawn_width = hypot(a, b)
image_drawn_height = hypot(c, d)
# The scale of the image is pixels per PDF unit (1/72")
scale_w = image_size[0] / image_drawn_width
scale_h = image_size[1] / image_drawn_height
# DPI = scale * 72
dpi_w = scale_w * 72.0
dpi_h = scale_h * 72.0
return (dpi_w, dpi_h)
def _find_page_inline_images(page, pageinfo, contentsinfo):
"Find inline images on the page"
for n, inline in enumerate(contentsinfo.inline_images):
image = {}
image['name'] = str('inline-%02d' % n)
image['width'] = inline.settings['/W']
image['height'] = inline.settings['/H']
image['bpc'] = inline.settings['/BPC']
image['color'] = FRIENDLY_COLORSPACE.get(inline.settings['/CS'], '-')
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
if '/F' in inline.settings:
filter_ = inline.settings['/F']
if isinstance(filter_, pypdf.generic.ArrayObject):
filter_ = filter_[0]
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
else:
image['enc'] = 'image'
dpi_w, dpi_h = _get_dpi(
inline.shorthand, (image['width'], image['height']))
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
yield image
def _find_page_regular_images(page, pageinfo, contentsinfo):
"Find images stored in XObject resources"
try:
page['/Resources']['/XObject']
except KeyError:
return
# Look for XObject (out of line images)
for xobj in page['/Resources']['/XObject']:
# PyPDF2 returns the keys as an iterator
pdfimage = page['/Resources']['/XObject'][xobj]
if pdfimage['/Subtype'] != '/Image':
continue
if '/ImageMask' in pdfimage:
if pdfimage['/ImageMask']:
continue
image = {}
image['name'] = str(xobj)
image['width'] = pdfimage['/Width']
image['height'] = pdfimage['/Height']
image['bpc'] = pdfimage['/BitsPerComponent']
# Fixme: this is incorrectly treats explicit masks as stencil masks,
# but good enough for now. Explicit masks have /ImageMask true but are
# never called for in content stream, instead are drawn as a /Mask on
# other images. For our purposes finding out the details of /Mask
# will seldom matter.
if '/ImageMask' in pdfimage:
image['type'] = 'stencil' if pdfimage['/ImageMask'].value \
else 'image'
else:
image['type'] = 'image'
if '/Filter' in pdfimage:
filter_ = pdfimage['/Filter']
if isinstance(filter_, pypdf.generic.ArrayObject):
@@ -98,12 +272,48 @@ def _find_page_images(page, pageinfo):
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
image['dpi_w'] = image['width'] / pageinfo['width_inches']
image['dpi_h'] = image['height'] / pageinfo['height_inches']
# Bit of a hack... infer grayscale if component count is uncertain
# but encoding must be monochrome. This happens if a monochrome image
# has an ICC profile attached. Better solution would be to examine
# the ICC profile.
if image['comp'] == '?' and image['enc'] in ('ccitt', 'jbig2'):
image['comp'] = FRIENDLY_COMP['gray']
image['dpi_w'] = image['dpi_h'] = 0
for raster in contentsinfo.raster_settings:
# Loop in case the same image is display multiple times on a page
if raster.name != image['name']:
continue
if raster.stack_depth == 0:
# At least one PDF in the wild (and test suite) draws an image
# when the graphics stack depth is 0, meaning that the image
# gets drawn into a square of 1x1 PDF units (or 1/72",
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
# these from our DPI calculation for the page.
continue
dpi_w, dpi_h = _get_dpi(
raster.shorthand, (image['width'], image['height']))
# When image is used multiple times take the highest DPI it is
# rendered at
image['dpi_w'] = max(dpi_w, image.get('dpi_w', 0))
image['dpi_h'] = max(dpi_h, image.get('dpi_h', 0))
image['dpi_w'] = Decimal(image['dpi_w'])
image['dpi_h'] = Decimal(image['dpi_h'])
image['dpi'] = (image['dpi_w'] * image['dpi_h']) ** Decimal(0.5)
yield image
def _find_page_images(page, pageinfo, contentsinfo):
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
def _page_has_text(pdf, page):
# Simple test
text = page.extractText()
@@ -136,17 +346,19 @@ def _pdf_get_pageinfo(infile, pageno: int):
pageinfo['has_text'] = _page_has_text(pdf, page)
width_pt = page['/MediaBox'][2] - page['/MediaBox'][0]
height_pt = page['/MediaBox'][3] - page['/MediaBox'][1]
width_pt = page.mediaBox.getWidth()
height_pt = page.mediaBox.getHeight()
pageinfo['width_inches'] = width_pt / Decimal(72.0)
pageinfo['height_inches'] = height_pt / Decimal(72.0)
pageinfo['images'] = [im for im in _find_page_images(page, pageinfo)]
try:
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
except AttributeError as e:
return pageinfo
# Look for inline images
if _page_has_inline_images(page):
raise NotImplementedError(
"Warning: input PDF contains inline images - not supported")
contentsinfo = _interpret_contents(contentstream)
pageinfo['images'] = [im for im in _find_page_images(
page, pageinfo, contentsinfo)]
if pageinfo['images']:
xres = max(image['dpi_w'] for image in pageinfo['images'])
@@ -164,3 +376,17 @@ def pdf_get_all_pageinfo(infile):
pdf = pypdf.PdfFileReader(infile)
getcontext().prec = 6
return [_pdf_get_pageinfo(infile, n) for n in range(pdf.numPages)]
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile')
args = parser.parse_args()
info = pdf_get_all_pageinfo(args.infile)
from pprint import pprint
pprint(info)
if __name__ == '__main__':
main()
+63 -40
View File
@@ -5,10 +5,14 @@
from __future__ import print_function, absolute_import, division
from string import Template
from subprocess import Popen, PIPE
import os
import codecs
from . import get_program
import pkg_resources
import PyPDF2 as pypdf
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
'ocrmypdf', ICC_PROFILE_RELPATH)
# This is a template written in PostScript which is needed to create PDF/A
@@ -71,8 +75,14 @@ def encode_text_string(s: str) -> str:
Postscript file to be completely ASCII and no escaping of Postscript
characters is necessary.
'''
# Sometimes lazy C programmer leave their NULs at the end of strings
# tests/resources/aspect.pdf is one example (created by ImageMagick)
s = s.replace('\x00', '')
if s == '':
return ''
utf16_bytes = s.encode('utf-16be')
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
@@ -85,52 +95,65 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
t = Template(pdfa_def_template)
result = t.substitute(icc_profile=icc_profile,
icc_identifier=icc_identifier,
title=pdfmark_utf16.get('title', ''),
author=pdfmark_utf16.get('author', ''),
subject=pdfmark_utf16.get('subject', ''),
creator=pdfmark_utf16.get('creator', ''),
keywords=pdfmark_utf16.get('keywords', ''))
title=pdfmark_utf16.get('/Title', ''),
author=pdfmark_utf16.get('/Author', ''),
subject=pdfmark_utf16.get('/Subject', ''),
creator=pdfmark_utf16.get('/Creator', ''),
keywords=pdfmark_utf16.get('/Keywords', ''))
return result
def _get_postscript_icc_path():
"Parse Ghostscript's help message to find where iccprofiles are stored"
p_gs = Popen([get_program('gs'), '--help'], close_fds=True,
universal_newlines=True,
stdout=PIPE, stderr=PIPE)
out, _ = p_gs.communicate()
lines = out.splitlines()
def search_paths(lines):
seeking = True
for line in lines:
if seeking:
if line.startswith('Search path'):
seeking = False
continue
else:
if line.strip().startswith('/'):
yield from (
path.strip() for path in line.split(':')
if path.strip() != '')
for root in search_paths(lines):
path = os.path.realpath(os.path.join(root, '../iccprofiles'))
if os.path.exists(path):
return path
raise FileNotFoundError("Could not find Ghostscript's iccprofiles")
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
if icc == 'sRGB':
icc_profile = os.path.join(_get_postscript_icc_path(), 'srgb.icc')
icc_profile = SRGB_ICC_PROFILE
else:
raise NotImplementedError("Only supporting sRGB")
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
# Since PostScript might not handle UTF-8 (it's hard to get a clear
# answer), insist on ascii
# We should have encoded everything to pure ASCII by this point, and
# to be safe, only allow ASCII in PostScript
with open(target_filename, 'w', encoding='ascii') as f:
f.write(ps)
def file_claims_pdfa(filename):
"""Determines if the file claims to be PDF/A compliant
Checking if a file is a truly compliant PDF/A is a massive undertaking
that no open source tool does properly. Some commercial tools are
generally reliable (Acrobat).
This checks if the XMP metadata contains a PDF/A marker.
"""
pdf = pypdf.PdfFileReader(filename)
xmp = pdf.getXmpMetadata()
try:
pdfa_nodes = xmp.getNodesInNamespace(
aboutUri='',
namespace='http://www.aiim.org/pdfa/ns/id/')
except AttributeError:
return {'pass': False, 'output': 'pdf',
'conformance': 'No XMP metadata'}
pdfa_dict = {attr.localName: attr.value for attr in pdfa_nodes}
pdfa_dict['pass'] = False
pdfa_dict['output'] = 'pdf'
if pdfa_dict:
part_conformance = pdfa_dict['part'] + pdfa_dict['conformance']
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
conformance = 'PDF/A-{}'.format(
part_conformance)
if part_conformance in valid_part_conforms:
pdfa_dict['pass'] = True
pdfa_dict['output'] = 'pdfa'
pdfa_dict['conformance'] = conformance
else:
pdfa_dict['conformance'] = 'PDF'
return pdfa_dict
+55 -20
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'),
@@ -19,15 +39,15 @@ def check(input_file, log):
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
except CalledProcessError as e:
if e.returncode == 2:
print("{0}: not a valid PDF, and could not repair it.".format(
input_file))
print("Details:")
print(e.output)
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
log.info("qpdf --check returned warnings:")
log.info(e.output)
else:
print(e.output)
log.warning(e.output)
return False
return True
@@ -40,32 +60,35 @@ def repair(input_file, output_file, log):
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
except CalledProcessError as e:
if e.returncode == 3 and e.output.find("operation succeeded"):
log.debug('qpdf found and fixed errors:')
log.debug('qpdf found and fixed errors: ' + e.output)
log.debug(e.output)
print(e.output)
return
if e.returncode == 2 and e.output.find("invalid password"):
print("{0}: this PDF is password-protected - password must "
"be removed for OCR".format(input_file))
log.error("{0}: this PDF is password-protected - password must "
"be removed for OCR".format(input_file))
sys.exit(ExitCode.input_file)
elif e.returncode == 2:
print("{0}: not a valid PDF, and could not repair it.".format(
input_file))
print("Details:")
print(e.output)
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
log.error("Details: " + e.output)
sys.exit(ExitCode.input_file)
else:
print("{0}: unknown error".format(
input_file))
print(e.output)
log.error("{0}: unknown error".format(
input_file))
log.error(e.output)
sys.exit(ExitCode.unknown)
def get_npages(input_file):
pages = check_output(
[get_program('qpdf'), '--show-npages', input_file],
universal_newlines=True, close_fds=True)
def get_npages(input_file, log):
try:
pages = check_output(
[get_program('qpdf'), '--show-npages', input_file],
universal_newlines=True, close_fds=True)
except CalledProcessError as e:
if e.returncode == 2 and e.output.find('No such file'):
log.error(e.output)
sys.exit(ExitCode.input_file)
return int(pages)
@@ -82,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)
+109 -34
View File
@@ -6,16 +6,17 @@ import os
import re
import shutil
from functools import lru_cache
from . import ExitCode, get_program
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(
'OrientationConfidence',
('angle', 'confidence'))
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
@@ -76,6 +77,83 @@ def languages():
return set(lang.strip() for lang in langs.splitlines()[1:])
def get_orientation(input_file, language: list, timeout: float, log):
args_tesseract = [
get_program('tesseract'),
'-l', '+'.join(language),
'-psm', '0',
input_file,
'stdout'
]
try:
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
except TimeoutExpired:
return OrientationConfidence(angle=0, confidence=0.0)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if ('Too few characters. Skipping this page' in e.output or
'Image too large' in e.output):
return OrientationConfidence(0, 0)
raise e from e
else:
osd = {}
for line in stdout.splitlines():
line = line.strip()
parts = line.split(':', maxsplit=2)
if len(parts) == 2:
osd[parts[0].strip()] = parts[1].strip()
angle = int(osd.get('Orientation in degrees', 0))
if 'Orientation' in osd:
# Tesseract < 3.04.01
# reports "Orientation in degrees" as a counterclockwise angle
# We keep it clockwise
assert 'Rotate' not in osd
angle = -angle % 360
else:
# Tesseract == 3.04.01, hopefully also Tesseract > 3.04.01
# reports "Orientation in degrees" as a clockwise angle
assert 'Rotate' in osd
oc = OrientationConfidence(
angle=angle,
confidence=float(osd.get('Orientation confidence', 0)))
return oc
def tesseract_log_output(log, stdout, input_file):
lines = stdout.splitlines()
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
for line in lines:
if line.startswith("Tesseract Open Source"):
continue
elif line.startswith("Warning in pixReadMem"):
continue
elif 'diacritics' in line:
log.warning(prefix + "lots of diacritics - possibly poor OCR")
elif line.startswith('OSD: Weak margin'):
log.warning(prefix + "unsure about page orientation")
elif 'error' in line.lower() or 'exception' in line.lower():
log.error(prefix + line.strip())
else:
log.info(prefix + line.strip())
def page_timedout(log, input_file):
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
log.warning(prefix + " took too long to OCR - skipping")
def _generate_null_hocr(output_hocr, pageinfo):
with open(output_hocr, 'w', encoding="utf-8") as f:
f.write(HOCR_TEMPLATE.format(
pageinfo['width_pixels'],
pageinfo['height_pixels']))
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
timeout: float, pageinfo_getter, pagesegmode: int, log):
@@ -94,30 +172,25 @@ def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
badxml,
'hocr'
] + tessconfig)
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=PIPE,
universal_newlines=True)
try:
stdout, stderr = p.communicate(timeout=timeout)
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
except TimeoutExpired:
p.kill()
stdout, stderr = p.communicate()
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
# it does not have a valid hOCR file.
with open(output_hocr, 'w', encoding="utf-8") as f:
pageinfo = pageinfo_getter()
f.write(HOCR_TEMPLATE.format(
pageinfo['width_pixels'],
pageinfo['height_pixels']))
page_timedout(log, input_file)
_generate_null_hocr(output_hocr, pageinfo_getter())
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if 'Image too large' in e.output:
_generate_null_hocr(output_hocr, pageinfo_getter())
return
raise e from e
else:
if stdout:
log.info(stdout)
if stderr:
log.error(stderr)
if p.returncode != 0:
raise CalledProcessError(p.returncode, args_tesseract)
tesseract_log_output(log, stdout, input_file)
if os.path.exists(badxml + '.html'):
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
shutil.move(badxml + '.html', badxml)
@@ -165,17 +238,19 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
'pdf'
] + tessconfig)
p = Popen(args_tesseract, close_fds=True, stdout=PIPE, stderr=PIPE,
universal_newlines=True)
try:
stdout, stderr = p.communicate(timeout=timeout)
if stdout:
log.info(stdout)
if stderr:
log.error(stderr)
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
except TimeoutExpired:
p.kill()
log.info("Tesseract - page timed out")
page_timedout(log, input_image)
shutil.copy(skip_pdf, output_pdf)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image)
if 'Image too large' in e.output:
shutil.copy(skip_pdf, output_pdf)
return
raise e from e
else:
tesseract_log_output(log, stdout, input_image)
+15 -14
View File
@@ -3,7 +3,7 @@
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
from subprocess import Popen, PIPE
from subprocess import CalledProcessError, STDOUT, check_output, check_call
from tempfile import NamedTemporaryFile
import sys
import os
@@ -17,10 +17,9 @@ def version():
get_program('unpaper'),
'--version'
]
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=PIPE)
version, _ = p_unpaper.communicate(timeout=5)
version = check_output(
args_unpaper, close_fds=True, universal_newlines=True,
stderr=STDOUT, timeout=5)
return version.strip()
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
os.unlink(output_pnm.name)
args_unpaper.extend([input_pnm.name, output_pnm.name])
p_unpaper = Popen(
args_unpaper, close_fds=True,
universal_newlines=True, stdout=PIPE, stderr=PIPE
)
out, err = p_unpaper.communicate()
log.debug(out)
log.debug(err)
Image.open(output_pnm.name).save(output_file)
try:
stdout = check_output(
args_unpaper, close_fds=True,
universal_newlines=True, stderr=STDOUT,
)
except CalledProcessError as e:
log.debug(e.output)
raise e from e
else:
log.debug(stdout)
Image.open(output_pnm.name).save(output_file)
def deskew(input_file, output_file, dpi, log):
+267 -195
View File
@@ -4,263 +4,335 @@
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: Pipeline: Pages: 1 -->
<svg width="1132pt" height="708pt"
viewBox="0.00 0.00 1132.00 708.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 704.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,-704.083 1128,-704.083 1128,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,-692.083 1116,-692.083 1116,-8 8,-8"/>
<text text-anchor="middle" x="562" y="-664.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="936.535,-646.083 713.465,-646.083 709.465,-642.083 709.465,-610.083 932.535,-610.083 936.535,-614.083 936.535,-646.083"/>
<polyline fill="none" stroke="black" points="932.535,-642.083 709.465,-642.083 "/>
<polyline fill="none" stroke="black" points="932.535,-642.083 932.535,-610.083 "/>
<polyline fill="none" stroke="black" points="932.535,-642.083 936.535,-646.083 "/>
<text text-anchor="middle" x="823" y="-622.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="914.112,-567.155 710,-584.057 505.888,-567.155 506.078,-539.806 913.922,-539.806 914.112,-567.155"/>
<polygon fill="none" stroke="black" points="918.134,-570.834 710,-588.069 501.866,-570.834 502.11,-535.808 917.89,-535.808 918.134,-570.834"/>
<text text-anchor="middle" x="710" y="-553.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="M793.9,-609.961C783.582,-603.89 771.656,-596.873 760.092,-590.069"/>
<polygon fill="#0044a0" stroke="#0044a0" points="761.747,-586.982 751.353,-584.927 758.197,-593.015 761.747,-586.982"/>
</g>
<!-- t12 -->
<g id="node14" class="node"><title>t12</title>
<polygon fill="#efa03b" stroke="black" points="1108.08,-509.109 769.918,-509.109 765.918,-505.109 765.918,-473.109 1104.08,-473.109 1108.08,-477.109 1108.08,-509.109"/>
<polyline fill="none" stroke="black" points="1104.08,-505.109 765.918,-505.109 "/>
<polyline fill="none" stroke="black" points="1104.08,-505.109 1104.08,-473.109 "/>
<polyline fill="none" stroke="black" points="1104.08,-505.109 1108.08,-509.109 "/>
<text text-anchor="middle" x="937" y="-485.109" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
</g>
<!-- t0&#45;&gt;t12 -->
<g id="edge19" class="edge"><title>t0&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M899.32,-610.004C909.979,-604.592 919.748,-597.466 927,-588.083 941.916,-568.78 943.099,-540.385 941.345,-519.486"/>
<polygon fill="#0044a0" stroke="#0044a0" points="944.8,-518.881 940.218,-509.328 937.843,-519.653 944.8,-518.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="592.299,-509.109 241.701,-509.109 237.701,-505.109 237.701,-473.109 588.299,-473.109 592.299,-477.109 592.299,-509.109"/>
<polyline fill="none" stroke="black" points="588.299,-505.109 237.701,-505.109 "/>
<polyline fill="none" stroke="black" points="588.299,-505.109 588.299,-473.109 "/>
<polyline fill="none" stroke="black" points="588.299,-505.109 592.299,-509.109 "/>
<text text-anchor="middle" x="415" y="-485.109" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
<polygon fill="#efa03b" stroke="black" points="1329.11,-774.253 1125,-791.156 920.888,-774.253 921.078,-746.905 1328.92,-746.905 1329.11,-774.253"/>
<polygon fill="none" stroke="black" points="1333.13,-777.932 1125,-795.167 916.866,-777.932 917.11,-742.906 1332.89,-742.906 1333.13,-777.932"/>
<text text-anchor="middle" x="1125" y="-760.694" font-family="Times,serif" font-size="20.00">split_pages</text>
</g>
<!-- t1&#45;&gt;t2 -->
<g id="edge2" class="edge"><title>t1&#45;&gt;t2</title>
<path fill="none" stroke="#0044a0" d="M608.89,-535.808C573.672,-527.87 534.524,-519.048 500.679,-511.42"/>
<polygon fill="#0044a0" stroke="#0044a0" points="501.345,-507.982 490.82,-509.198 499.806,-514.811 501.345,-507.982"/>
<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>
<!-- t7 -->
<g id="node9" class="node"><title>t7</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>
<!-- 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;t7 -->
<g id="edge11" class="edge"><title>t1&#45;&gt;t7</title>
<path fill="none" stroke="#0044a0" d="M501.985,-548.028C416.651,-540.897 317.351,-528.985 229,-509.109 131.492,-487.174 17,-534.054 17,-434.109 17,-434.109 17,-434.109 17,-374.109 17,-340.481 4.97908,-324.525 27,-299.109 32.8004,-292.415 39.6539,-286.828 47.1559,-282.17"/>
<polygon fill="#0044a0" stroke="#0044a0" points="49.1993,-285.038 56.2361,-277.118 45.7959,-278.921 49.1993,-285.038"/>
<!-- 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>
<!-- t13 -->
<g id="node12" class="node"><title>t13</title>
<polygon fill="#efa03b" stroke="black" points="1029.34,-451.109 808.662,-451.109 804.662,-447.109 804.662,-415.109 1025.34,-415.109 1029.34,-419.109 1029.34,-451.109"/>
<polyline fill="none" stroke="black" points="1025.34,-447.109 804.662,-447.109 "/>
<polyline fill="none" stroke="black" points="1025.34,-447.109 1025.34,-415.109 "/>
<polyline fill="none" stroke="black" points="1025.34,-447.109 1029.34,-451.109 "/>
<text text-anchor="middle" x="917" y="-427.109" font-family="Times,serif" font-size="20.00">skip_page</text>
<!-- t18 -->
<g id="node19" class="node"><title>t18</title>
<polygon fill="#efa03b" stroke="black" points="1025.79,-156.452 1221,-129.134 1416.21,-156.452 1416.03,-200.654 1025.97,-200.654 1025.79,-156.452"/>
<polygon fill="none" stroke="black" points="1021.78,-152.971 1221,-125.091 1420.22,-152.971 1420.01,-204.657 1021.99,-204.657 1021.78,-152.971"/>
<text text-anchor="middle" x="1221" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_qpdf</text>
</g>
<!-- t1&#45;&gt;t13 -->
<g id="edge16" class="edge"><title>t1&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M716.849,-535.484C723.825,-515.965 736.562,-488.763 757,-473.109 768.342,-464.422 781.352,-457.641 794.947,-452.352"/>
<polygon fill="#0044a0" stroke="#0044a0" points="796.223,-455.612 804.44,-448.923 793.845,-449.029 796.223,-455.612"/>
</g>
<!-- t11 -->
<g id="node13" class="node"><title>t11</title>
<polygon fill="#efa03b" stroke="black" points="1068.24,-335.109 687.76,-335.109 683.76,-331.109 683.76,-299.109 1064.24,-299.109 1068.24,-303.109 1068.24,-335.109"/>
<polyline fill="none" stroke="black" points="1064.24,-331.109 683.76,-331.109 "/>
<polyline fill="none" stroke="black" points="1064.24,-331.109 1064.24,-299.109 "/>
<polyline fill="none" stroke="black" points="1064.24,-331.109 1068.24,-335.109 "/>
<text text-anchor="middle" x="876" y="-311.109" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
</g>
<!-- t1&#45;&gt;t11 -->
<g id="edge18" class="edge"><title>t1&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M710.717,-535.657C712.098,-517.744 715.879,-492.693 726,-473.109 754.651,-417.673 809.56,-368.731 844.403,-341.318"/>
<polygon fill="#0044a0" stroke="#0044a0" points="846.607,-344.038 852.372,-335.148 842.322,-338.503 846.607,-344.038"/>
<!-- 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="564.742,-451.109 269.258,-451.109 265.258,-447.109 265.258,-415.109 560.742,-415.109 564.742,-419.109 564.742,-451.109"/>
<polyline fill="none" stroke="black" points="560.742,-447.109 265.258,-447.109 "/>
<polyline fill="none" stroke="black" points="560.742,-447.109 560.742,-415.109 "/>
<polyline fill="none" stroke="black" points="560.742,-447.109 564.742,-451.109 "/>
<text text-anchor="middle" x="415" y="-427.109" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
<polygon fill="#efa03b" stroke="black" points="1186.88,-716.208 905.117,-716.208 901.117,-712.208 901.117,-680.208 1182.88,-680.208 1186.88,-684.208 1186.88,-716.208"/>
<polyline fill="none" stroke="black" points="1182.88,-712.208 901.117,-712.208 "/>
<polyline fill="none" stroke="black" points="1182.88,-712.208 1182.88,-680.208 "/>
<polyline fill="none" stroke="black" points="1182.88,-712.208 1186.88,-716.208 "/>
<text text-anchor="middle" x="1044" y="-692.208" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
</g>
<!-- t2&#45;&gt;t3 -->
<g id="edge3" class="edge"><title>t2&#45;&gt;t3</title>
<path fill="none" stroke="#0044a0" d="M415,-473.003C415,-469.312 415,-465.322 415,-461.352"/>
<polygon fill="#0044a0" stroke="#0044a0" points="418.5,-461.111 415,-451.111 411.5,-461.111 418.5,-461.111"/>
</g>
<!-- t6 -->
<g id="node8" class="node"><title>t6</title>
<polygon fill="#efa03b" stroke="black" points="354.119,-335.109 39.8808,-335.109 35.8808,-331.109 35.8808,-299.109 350.119,-299.109 354.119,-303.109 354.119,-335.109"/>
<polyline fill="none" stroke="black" points="350.119,-331.109 35.8808,-331.109 "/>
<polyline fill="none" stroke="black" points="350.119,-331.109 350.119,-299.109 "/>
<polyline fill="none" stroke="black" points="350.119,-331.109 354.119,-335.109 "/>
<text text-anchor="middle" x="195" y="-311.109" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
</g>
<!-- t2&#45;&gt;t6 -->
<g id="edge9" class="edge"><title>t2&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M294.315,-473.06C280.461,-467.585 267.283,-460.434 256,-451.109 223.245,-424.039 207.251,-375.559 200.099,-345.207"/>
<polygon fill="#0044a0" stroke="#0044a0" points="203.486,-344.31 197.925,-335.292 196.648,-345.809 203.486,-344.31"/>
<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="587.95,-393.109 310.05,-393.109 306.05,-389.109 306.05,-357.109 583.95,-357.109 587.95,-361.109 587.95,-393.109"/>
<polyline fill="none" stroke="black" points="583.95,-389.109 306.05,-389.109 "/>
<polyline fill="none" stroke="black" points="583.95,-389.109 583.95,-357.109 "/>
<polyline fill="none" stroke="black" points="583.95,-389.109 587.95,-393.109 "/>
<text text-anchor="middle" x="447" y="-369.109" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
<polygon fill="#efa03b" stroke="black" points="1258.15,-658.208 1023.85,-658.208 1019.85,-654.208 1019.85,-622.208 1254.15,-622.208 1258.15,-626.208 1258.15,-658.208"/>
<polyline fill="none" stroke="black" points="1254.15,-654.208 1019.85,-654.208 "/>
<polyline fill="none" stroke="black" points="1254.15,-654.208 1254.15,-622.208 "/>
<polyline fill="none" stroke="black" points="1254.15,-654.208 1258.15,-658.208 "/>
<text text-anchor="middle" x="1139" y="-634.208" font-family="Times,serif" font-size="20.00">orient_page</text>
</g>
<!-- t2&#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="edge4" class="edge"><title>t3&#45;&gt;t4</title>
<path fill="none" stroke="#0044a0" d="M424.775,-415.003C427.132,-410.878 429.703,-406.379 432.232,-401.952"/>
<polygon fill="#0044a0" stroke="#0044a0" points="435.362,-403.53 437.285,-393.111 429.285,-400.057 435.362,-403.53"/>
</g>
<!-- t3&#45;&gt;t6 -->
<g id="edge8" class="edge"><title>t3&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M351.041,-415.02C333.086,-409.151 313.871,-401.82 297,-393.109 269.828,-379.08 242.065,-358.149 222.377,-341.96"/>
<polygon fill="#0044a0" stroke="#0044a0" points="224.378,-339.071 214.46,-335.347 219.891,-344.444 224.378,-339.071"/>
<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="665.666,-335.109 376.334,-335.109 372.334,-331.109 372.334,-299.109 661.666,-299.109 665.666,-303.109 665.666,-335.109"/>
<polyline fill="none" stroke="black" points="661.666,-331.109 372.334,-331.109 "/>
<polyline fill="none" stroke="black" points="661.666,-331.109 661.666,-299.109 "/>
<polyline fill="none" stroke="black" points="661.666,-331.109 665.666,-335.109 "/>
<text text-anchor="middle" x="519" y="-311.109" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</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="edge5" class="edge"><title>t4&#45;&gt;t5</title>
<path fill="none" stroke="#0044a0" d="M468.994,-357.003C475.274,-352.118 482.229,-346.709 488.905,-341.516"/>
<polygon fill="#0044a0" stroke="#0044a0" points="491.396,-344.013 497.141,-335.111 487.099,-338.487 491.396,-344.013"/>
<g id="edge6" class="edge"><title>t4&#45;&gt;t5</title>
<path fill="none" stroke="#0044a0" d="M1125.25,-622.102C1121.76,-617.759 1117.94,-613.003 1114.21,-608.353"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1116.65,-605.812 1107.66,-600.209 1111.2,-610.197 1116.65,-605.812"/>
</g>
<!-- t4&#45;&gt;t6 -->
<g id="edge7" class="edge"><title>t4&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M370.365,-357.079C342.386,-350.862 310.55,-343.787 281.752,-337.388"/>
<polygon fill="#0044a0" stroke="#0044a0" points="282.225,-333.907 271.704,-335.155 280.707,-340.741 282.225,-333.907"/>
<!-- 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;t11 -->
<g id="edge17" class="edge"><title>t4&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M577.461,-357.079C627.38,-350.563 684.512,-343.106 735.342,-336.47"/>
<polygon fill="#0044a0" stroke="#0044a0" points="735.957,-339.92 745.42,-335.155 735.051,-332.979 735.957,-339.92"/>
<!-- 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>
<!-- t8 -->
<g id="node7" class="node"><title>t8</title>
<polygon fill="#efa03b" stroke="black" points="986.109,-277.109 701.891,-277.109 697.891,-273.109 697.891,-241.109 982.109,-241.109 986.109,-245.109 986.109,-277.109"/>
<polyline fill="none" stroke="black" points="982.109,-273.109 697.891,-273.109 "/>
<polyline fill="none" stroke="black" points="982.109,-273.109 982.109,-241.109 "/>
<polyline fill="none" stroke="black" points="982.109,-273.109 986.109,-277.109 "/>
<text text-anchor="middle" x="842" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
<!-- 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>
<!-- t5&#45;&gt;t8 -->
<g id="edge6" class="edge"><title>t5&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M617.226,-299.079C654.028,-292.699 696.036,-285.416 733.699,-278.886"/>
<polygon fill="#0044a0" stroke="#0044a0" points="734.429,-282.312 743.685,-277.155 733.234,-275.415 734.429,-282.312"/>
<!-- 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="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="M1042.38,-564.102C1024.52,-558.185 1004.35,-551.5 985.823,-545.362"/>
<polygon fill="#0044a0" stroke="#0044a0" points="986.901,-542.032 976.308,-542.209 984.699,-548.677 986.901,-542.032"/>
</g>
<!-- t9 -->
<g id="node11" class="node"><title>t9</title>
<polygon fill="#efa03b" stroke="black" points="679.486,-277.109 336.514,-277.109 332.514,-273.109 332.514,-241.109 675.486,-241.109 679.486,-245.109 679.486,-277.109"/>
<polyline fill="none" stroke="black" points="675.486,-273.109 332.514,-273.109 "/>
<polyline fill="none" stroke="black" points="675.486,-273.109 675.486,-241.109 "/>
<polyline fill="none" stroke="black" points="675.486,-273.109 679.486,-277.109 "/>
<text text-anchor="middle" x="506" y="-253.109" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
<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="edge15" class="edge"><title>t5&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M515.029,-299.003C514.147,-295.204 513.191,-291.087 512.243,-287.002"/>
<polygon fill="#0044a0" stroke="#0044a0" points="515.617,-286.06 509.947,-277.111 508.799,-287.643 515.617,-286.06"/>
<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>
<!-- t10 -->
<g id="node10" class="node"><title>t10</title>
<polygon fill="#efa03b" stroke="black" points="973.082,-219.109 714.918,-219.109 710.918,-215.109 710.918,-183.109 969.082,-183.109 973.082,-187.109 973.082,-219.109"/>
<polyline fill="none" stroke="black" points="969.082,-215.109 710.918,-215.109 "/>
<polyline fill="none" stroke="black" points="969.082,-215.109 969.082,-183.109 "/>
<polyline fill="none" stroke="black" points="969.082,-215.109 973.082,-219.109 "/>
<text text-anchor="middle" x="842" y="-195.109" font-family="Times,serif" font-size="20.00">add_text_layer</text>
</g>
<!-- t8&#45;&gt;t10 -->
<g id="edge12" class="edge"><title>t8&#45;&gt;t10</title>
<path fill="none" stroke="#0044a0" d="M842,-241.003C842,-237.312 842,-233.322 842,-229.352"/>
<polygon fill="#0044a0" stroke="#0044a0" points="845.5,-229.111 842,-219.111 838.5,-229.111 845.5,-229.111"/>
<!-- t7 -->
<g id="node8" class="node"><title>t7</title>
<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="edge10" class="edge"><title>t6&#45;&gt;t7</title>
<path fill="none" stroke="#0044a0" d="M185.836,-299.003C183.626,-294.878 181.216,-290.379 178.845,-285.952"/>
<polygon fill="#0044a0" stroke="#0044a0" points="181.915,-284.273 174.108,-277.111 175.745,-287.578 181.915,-284.273"/>
<g id="edge8" class="edge"><title>t6&#45;&gt;t7</title>
<path fill="none" stroke="#0044a0" d="M903.006,-506.102C896.726,-501.216 889.771,-495.807 883.095,-490.615"/>
<polygon fill="#0044a0" stroke="#0044a0" points="884.901,-487.586 874.859,-484.209 880.604,-493.111 884.901,-487.586"/>
</g>
<!-- t6&#45;&gt;t9 -->
<g id="edge14" class="edge"><title>t6&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M289.577,-299.079C324.86,-292.726 365.115,-285.478 401.259,-278.969"/>
<polygon fill="#0044a0" stroke="#0044a0" points="402.116,-282.372 411.337,-277.155 400.875,-275.482 402.116,-282.372"/>
<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>
<!-- t7&#45;&gt;t10 -->
<g id="edge13" class="edge"><title>t7&#45;&gt;t10</title>
<path fill="none" stroke="#0044a0" d="M314.366,-242.002C317.606,-241.697 320.821,-241.399 324,-241.109 451.228,-229.521 596.262,-218.816 700.44,-211.565"/>
<polygon fill="#0044a0" stroke="#0044a0" points="700.976,-215.036 710.71,-210.852 700.491,-208.053 700.976,-215.036"/>
<!-- 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>
<!-- t14 -->
<g id="node15" class="node"><title>t14</title>
<polygon fill="#efa03b" stroke="black" points="774.472,-105.333 939,-78.005 1103.53,-105.333 1103.37,-149.551 774.625,-149.551 774.472,-105.333"/>
<polygon fill="none" stroke="black" points="770.46,-101.94 939,-73.9453 1107.54,-101.94 1107.36,-153.556 770.639,-153.556 770.46,-101.94"/>
<text text-anchor="middle" x="939" y="-111.555" font-family="Times,serif" font-size="20.00">merge_pages</text>
<!-- 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>
<!-- t10&#45;&gt;t14 -->
<g id="edge23" class="edge"><title>t10&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M862.571,-182.814C870.479,-176.165 879.888,-168.254 889.356,-160.293"/>
<polygon fill="#0044a0" stroke="#0044a0" points="891.89,-162.736 897.292,-153.622 887.385,-157.378 891.89,-162.736"/>
<!-- 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="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="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>
<!-- 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="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>
<!-- t9&#45;&gt;t14 -->
<g id="edge24" class="edge"><title>t9&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M546.87,-241.103C586.226,-225.054 647.626,-200.869 702,-183.109 730.59,-173.771 761.415,-164.679 791.074,-156.405"/>
<polygon fill="#0044a0" stroke="#0044a0" points="792.38,-159.675 801.082,-153.632 790.511,-152.93 792.38,-159.675"/>
<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>
<!-- t13&#45;&gt;t14 -->
<g id="edge20" class="edge"><title>t13&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M986.636,-414.956C1033.58,-398.748 1087,-369.103 1087,-318.109 1087,-318.109 1087,-318.109 1087,-258.109 1087,-215.948 1054.81,-182.761 1020.46,-159.346"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1021.98,-156.158 1011.7,-153.604 1018.15,-162.013 1021.98,-156.158"/>
<!-- 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>
<!-- t11&#45;&gt;t14 -->
<g id="edge22" class="edge"><title>t11&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M969.056,-299.033C979.177,-293.579 988.239,-286.44 995,-277.109 1020.03,-242.568 998.243,-195.803 974.929,-162.021"/>
<polygon fill="#0044a0" stroke="#0044a0" points="977.661,-159.825 968.996,-153.728 971.968,-163.897 977.661,-159.825"/>
<!-- t17 -->
<g id="node18" class="node"><title>t17</title>
<polygon fill="#efa03b" stroke="black" points="540.724,-156.455 770,-129.143 999.276,-156.455 999.062,-200.646 540.938,-200.646 540.724,-156.455"/>
<polygon fill="none" stroke="black" points="536.704,-152.903 770,-125.112 1003.3,-152.903 1003.05,-204.648 536.954,-204.648 536.704,-152.903"/>
<text text-anchor="middle" x="770" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_ghostscript</text>
</g>
<!-- t12&#45;&gt;t14 -->
<g id="edge21" class="edge"><title>t12&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1006.64,-472.956C1053.58,-456.748 1107,-427.103 1107,-376.109 1107,-376.109 1107,-376.109 1107,-258.109 1107,-220.742 1095.53,-209.426 1069,-183.109 1059.8,-173.988 1049.06,-165.927 1037.76,-158.869"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1039.41,-155.777 1029.03,-153.665 1035.83,-161.791 1039.41,-155.777"/>
<!-- 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>
<!-- t15 -->
<g id="node16" class="node"><title>t15</title>
<polygon fill="#efa03b" stroke="black" points="1053.18,-52 828.822,-52 824.822,-48 824.822,-16 1049.18,-16 1053.18,-20 1053.18,-52"/>
<polyline fill="none" stroke="black" points="1049.18,-48 824.822,-48 "/>
<polyline fill="none" stroke="black" points="1049.18,-48 1049.18,-16 "/>
<polyline fill="none" stroke="black" points="1049.18,-48 1053.18,-52 "/>
<text text-anchor="middle" x="939" y="-28" font-family="Times,serif" font-size="20.00">copy_final</text>
<!-- 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>
<!-- t14&#45;&gt;t15 -->
<g id="edge25" class="edge"><title>t14&#45;&gt;t15</title>
<path fill="none" stroke="#0044a0" d="M939,-73.8665C939,-69.8921 939,-65.942 939,-62.1676"/>
<polygon fill="#0044a0" stroke="#0044a0" points="942.5,-62.1213 939,-52.1214 935.5,-62.1214 942.5,-62.1213"/>
<!-- 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="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: 18 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+2
View File
@@ -0,0 +1,2 @@
[pytest]
norecursedirs = lib .pc
+9 -5
View File
@@ -1,5 +1,9 @@
ruffus>=2.6.3
Pillow>=2.4.0
reportlab>=3.1.44
PyPDF2>=1.25.1
git+https://github.com/jbarlow83/img2pdf.git@e9bcce0afc3720752ca53a991db93f911a1df709#egg=img2pdf-0.1.5.dev
# requirements.txt can be used to replicate the developer's build environment
# setup.py lists a separate set of requirements that are looser to simplify
# installation
ruffus==2.6.3
Pillow==3.3.0
reportlab==3.2.0
PyPDF2==1.26
img2pdf==0.2.1
cffi==1.5.2
+7 -2
View File
@@ -1,4 +1,9 @@
[bdist_wheel]
python-tag = py34
[pytest]
norecursedirs = lib
[aliases]
test=pytest
[check-manifest]
ignore =
.github
+29 -20
View File
@@ -1,18 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# © 2015 James R. Barlow: github.com/jbarlow83
from __future__ import print_function, unicode_literals
from setuptools import setup
from subprocess import STDOUT, check_output, CalledProcessError
from collections.abc import Mapping
import re
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
from subprocess import STDOUT, check_output, CalledProcessError # nopep8
from collections.abc import Mapping # nopep8
import re # nopep8
missing_program = '''
The program '{program}' could not be executed or was not found on your
system PATH.
@@ -149,26 +151,26 @@ command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
if command.startswith('install') or \
command in ['check', 'test', 'nosetests', 'easy_install', 'egg_info']:
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']
)
@@ -196,7 +198,7 @@ setup(
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"License :: Public Domain",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX",
"Operating System :: POSIX :: BSD",
@@ -206,21 +208,28 @@ setup(
"Topic :: Text Processing :: Linguistic",
],
setup_requires=[
'setuptools_scm'
'setuptools_scm',
'cffi>=1.5.0',
'pytest-runner'
],
use_scm_version={'version_scheme': 'post-release'},
cffi_modules=[
'ocrmypdf/lib/compile_leptonica.py:ffi'
],
install_requires=[
'ruffus',
'Pillow',
'reportlab',
'PyPDF2',
'img2pdf'
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
'Pillow>=3.1.0', # Pillow is pretty stable
'reportlab>=3.2.0', # oldest released version with sane image handling
'PyPDF2>=1.26', # pure Python, so track HEAD closely
'img2pdf>=0.2.1', # pure Python, so track HEAD closely
'cffi>=1.5.0' # oldest version ever tested
],
tests_require=tests_require,
entry_points={
'console_scripts': [
'ocrmypdf = ocrmypdf.main:run_pipeline'
'ocrmypdf = ocrmypdf.__main__:run_pipeline'
],
},
package_data={'ocrmypdf': ['data/sRGB.icc']},
include_package_data=True,
zip_safe=False)
+4
View File
@@ -0,0 +1,4 @@
deb http://httpredir.debian.org/debian stretch main
deb http://httpredir.debian.org/debian stretch-updates main
deb http://security.debian.org stretch/updates main
deb http://ftp.de.debian.org/debian sid main contrib non-free
BIN
View File
Binary file not shown.
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# © 2016 James R. Barlow: github.com/jbarlow83
# Release sanity checking
import argparse
from subprocess import run, PIPE, DEVNULL, STDOUT, CalledProcessError
from git import Repo, Remote, PushInfo
import logging
import re
import sys
import os
logging.basicConfig(level=logging.INFO)
REMOTE_ERROR_FLAGS = \
PushInfo.REJECTED | PushInfo.NO_MATCH | PushInfo.REMOTE_REJECTED | \
PushInfo.REMOTE_FAILURE | PushInfo.DELETED | PushInfo.ERROR
def test_repo(repo):
assert not repo.is_dirty(), "Repository is dirty"
if repo.untracked_files:
logging.warning('Some files are untracked:')
logging.warning('\n' + '\n'.join(repo.untracked_files))
assert repo.active_branch.name == 'master', 'Not on branch master'
def travis(args):
repo = Repo('.')
test_repo(repo)
git_describe = repo.git.describe()
try:
env = os.environ.copy()
env['SETUPTOOLS_SCM_PRETEND_VERSION'] = git_describe
proc = run(['check-manifest'], check=True, universal_newlines=True, stdout=PIPE, stderr=STDOUT, env=env)
logging.info(proc.stdout)
except CalledProcessError as e:
logging.error('MANIFEST.in error')
logging.error(e.stdout)
sys.exit(1)
run(['python3', 'setup.py', 'build'], check=True)
origin = Remote(repo, 'jbarlow')
result = origin.push(refspec='master:master')[0]
if result.flags & REMOTE_ERROR_FLAGS:
logging.error(result.summary)
sys.exit(1)
else:
logging.info(result.summary)
logging.info("Pushed to Travis CI")
logging.info("If this passes, git tag and release")
def release(args):
repo = Repo('.')
test_repo(repo)
git_describe = repo.git.describe()
assert git_describe.startswith('v') and not '-' in git_describe and not '+ng' in git_describe, \
"Not tagged properly for release: " + git_describe
plain_version = git_describe[1:] # without 'v' prefix
with open('RELEASE_NOTES.rst') as f:
notes = f.read()
assert plain_version in notes, "Version not mentioned in release notes"
proc = run(['python3', 'setup.py', 'sdist', 'bdist_wheel'], universal_newlines=True, check=True, stdout=PIPE, stderr=STDOUT)
logging.info(proc.stdout)
origin = Remote(repo, 'jbarlow')
result = origin.push(refspec='master:master', tags=True)[0]
if result.flags & REMOTE_ERROR_FLAGS:
logging.error(result.summary)
sys.exit(1)
else:
logging.info(result.summary)
run(['twine', 'upload', '-r', 'pypitest',
'dist/ocrmypdf-{}.tar.gz'.format(plain_version),
'dist/ocrmypdf-{}-py34-none-any.whl'.format(plain_version)], check=True, universal_newlines=True, stdout=PIPE)
parser = argparse.ArgumentParser(description="ocrmypdf release tasks")
subparsers = parser.add_subparsers()
push_travis = subparsers.add_parser(
'push-travis', description="Push master to travis for testing")
push_travis.set_defaults(func=travis)
release_parser = subparsers.add_parser(
'release', description="Release to PyPI etc")
release_parser.set_defaults(func=release)
def main():
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()
+1 -1
View File
@@ -1 +1 @@
pytest>=2.7.2
pytest>=2.7.2
-34
View File
@@ -1,34 +0,0 @@
All test resources must come from free public domain sources for
copyright reasons.
Test files do not necessarily produce perfect (or even good) OCR
results.
+---------------------+--------------------------------------------------------------------------------+
| File | Source |
+=====================+================================================================================+
| graph.pdf | Wikimedia |
+---------------------+--------------------------------------------------------------------------------+
| c02-22.pdf | Project Gutenberg: https://www.gutenberg.org/files/76/76-h/images/c02-22.jpg |
+---------------------+--------------------------------------------------------------------------------+
| LinnSequencer.jpg | Wikimedia_ |
+---------------------+--------------------------------------------------------------------------------+
| congress.jpg | http://www.baxleystamps.com/litho/meiji/courts_1871.jpg |
+---------------------+--------------------------------------------------------------------------------+
| blank.pdf | Blank page from Adobe Illustrator CC 2015 |
+---------------------+--------------------------------------------------------------------------------+
| enormous.pdf | PNG file saved to PDF using img2pdf |
+---------------------+--------------------------------------------------------------------------------+
| invalid.pdf | PDF file header followed by EOF marker; not valid |
+---------------------+--------------------------------------------------------------------------------+
| multipage.pdf | several other files concatenated |
+---------------------+--------------------------------------------------------------------------------+
| skew.pdf | skewed version of c02-22.PDF |
+---------------------+--------------------------------------------------------------------------------+
| Test_Issue_28.pdf | file with some syntax errors |
+---------------------+--------------------------------------------------------------------------------+
| missing_docinfo.pdf | file missing its DocumentInfo dictionary |
+---------------------+--------------------------------------------------------------------------------+
.. _Wikimedia: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
+86
View File
@@ -0,0 +1,86 @@
These test files are used in OCRmyPDF's test suite. They do not necessarily produce OCR results
at all and are not meant as examples of OCR output. Some are even invalid PDFs that might
crash certain PDF viewers.
Files derived from free sources
===============================
These test resources come from free sources, under either public domain or Creative Commons licenses.
In some cases they were converted from one image format to another without other changes.
+---------------------+--------------------------------------------------------------------------------+
| File | Source |
+=====================+================================================================================+
| c02-22.pdf | `Project Gutenberg`_, Adventures of Huckleberry Finn, page 22 |
+---------------------+--------------------------------------------------------------------------------+
| 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, | `Wikimedia: LinnSequencer`_ (Creative Commons BY-SA 3.0) |
| linn.pdf, linn.txt | |
+---------------------+--------------------------------------------------------------------------------+
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.
+---------------------+-----------------------+---------------------------------------------------------+
| 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 |
+---------------------+-----------------------+---------------------------------------------------------+
| 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 (Creative Commons BY-SA) |
+---------------------+-----------------------+---------------------------------------------------------+
| milk.pdf | @lowesjam | linearized PDF containing some indirect objects |
+---------------------+-----------------------+---------------------------------------------------------+
| missing_docinfo.pdf | @jbarlow83 | PDF file with no /DocumentInfo section |
+---------------------+-----------------------+---------------------------------------------------------+
Assemblies
==========
These test resources are assemblies from other previously mentioned files, released under the same license terms as their input files.
- cardinal.pdf (four cardinal directions, rotated copies of LinnSequencer.jpg)
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
- encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2)
- graph_ocred.pdf (from graph.pdf)
- jbig2.pdf (congress.jpg, converted to JBIG2 encoding)
- multipage.pdf (from several other files)
- palette.pdf (congress.jpg, converted to a 256-color palette)
- skew.pdf (from c02-22.pdf)
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2)
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
.. _`Project Gutenberg`: https://www.gutenberg.org/files/76/76-h/76-h.htm#c2
.. _`US Congressional Records`: http://www.baxleystamps.com/litho/meiji/courts_1871.jpg
.. _`Wikimedia: Pandas text analysis.png`: https://en.wikipedia.org/wiki/File:Pandas_text_analysis.png
.. _`Wikimedia: JPEG2000 Lichtenstein`: https://en.wikipedia.org/wiki/JPEG_2000#/media/File:Jpeg2000_2-level_wavelet_transform-lichtenstein.png
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+123
View File
@@ -0,0 +1,123 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a state—of—the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
0 Ultra-fast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive real—time ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
0 Optional remote control.
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted 0r defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the firsttrack will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
Editing
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence-—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
Additional Features
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
The INSERT/ COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
Creating a Song
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
Composition Without Compromise
The technology you use should never be so complex that
it interferes with the creative process. Thats precisely why
the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWIT CH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/ STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
0 Utilizes ultra high—speed, 8 MHZ 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PER—MINUTE or FRAMES-PER—BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
0 Any TIME SIGNATURE may be used, and may be changed within a song.
EDI]
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import sys
VERSION_STRING = '''tesseract 3.04.00
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
SPOOFED: return error claiming image too big
'''
"""Simulates an error of Tesseract failing on attempts to process large images
"""
def main():
if sys.argv[1] == '--version':
print(VERSION_STRING, file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
sys.exit(0)
elif sys.argv[-1] == 'hocr':
print("Image too large: (33830, 14959)\n"
"Error during processing.", file=sys.stderr)
sys.exit(1)
elif sys.argv[-1] == 'pdf':
print("Image too large: (33830, 14959)\n"
"Error during processing.", file=sys.stderr)
sys.exit(1)
elif sys.argv[-1] == 'stdout':
print("Image too large: (33830, 14959)\n"
"Error during processing.", file=sys.stderr)
sys.exit(1)
else:
print("Spoof doesn't understand arguments", file=sys.stderr)
print(sys.argv, file=sys.stderr)
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main()
+71 -12
View File
@@ -6,16 +6,39 @@ import shutil
import subprocess
"""Cache output of tesseract to speed up test suite
The cache is keyed by a hash that includes the tesseract version, some of
the command line, and the binary dump of the input file. The output file,
stdout, and stderr are replicated on a cache hit.
Page orientation checks are also cached (-psm 0 stdout)
Errors and crashes are not cached.
Things not checked:
-changes to tesseract installation that don't affect --version
Will fail on Tesseract 3.02.02 in "hocr" mode because it doesn't produce
the incorrect file extension.
"""
CACHE_PATH = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'cache'))
def real_tesseract():
tess_args = ['tesseract'] + sys.argv[1:]
os.execvp("tesseract", tess_args)
return # Not reachable
def main():
operation = sys.argv[-1]
# For anything except a hocr or pdf, defer to real tesseract
if operation != 'hocr' and operation != 'pdf':
tess_args = ['tesseract'] + sys.argv[1:]
os.execvp("tesseract", tess_args)
# For anything unexpected operation, defer to real tesseract binary
if operation != 'hocr' and operation != 'pdf' and operation != 'stdout':
real_tesseract()
return # Not reachable
try:
@@ -43,8 +66,16 @@ def main():
except ValueError:
pass
input_file = sys.argv[-3]
output_file = sys.argv[-2]
if operation == 'stdout' and psm != '0':
real_tesseract()
return
if operation == 'stdout':
input_file = sys.argv[-2]
output_file = 'stdout'
else:
input_file = sys.argv[-3]
output_file = sys.argv[-2]
if operation == 'hocr':
output_file += '.hocr'
@@ -53,25 +84,53 @@ def main():
with open(input_file, 'rb') as f:
m.update(f.read())
cache_name = os.path.join(CACHE_PATH, m.hexdigest())
print(cache_name)
if os.path.exists(cache_name):
# Cache hit
print("Tesseract cache hit", file=sys.stderr)
shutil.copy(cache_name, output_file)
if operation != 'stdout':
shutil.copy(cache_name, output_file)
# Replicate output
with open(cache_name + '.stdout', 'r') as f:
print(f.read(), end='')
with open(cache_name + '.stderr', 'r') as f:
print(f.read(), end='', file=sys.stderr)
sys.exit(0)
# Cache miss
print("Tesseract cache miss", file=sys.stderr)
# Call tesseract
subprocess.check_call(['tesseract'] + sys.argv[1:])
p = subprocess.Popen(
['tesseract'] + sys.argv[1:],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True)
stdout, stderr = p.communicate()
if p.returncode != 0:
# Do not cache errors or crashes
print("Tesseract error", file=sys.stderr)
print(stdout, end='')
print(stderr, end='', file=sys.stderr)
return p.returncode
with open(cache_name + '.stdout', 'w') as f:
f.write(stdout)
with open(cache_name + '.stderr', 'w') as f:
f.write(stderr)
print(stdout, end='')
print(stderr, end='', file=sys.stderr)
# Insert file into cache
if os.path.exists(output_file):
shutil.copy(output_file, cache_name)
if output_file != 'stdout':
if os.path.exists(output_file):
shutil.copy(output_file, cache_name)
else:
print("Could not find output file", file=sys.stderr)
else:
print("Could not find output file", file=sys.stderr)
open(cache_name, 'w').close()
if __name__ == '__main__':
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
import sys
import os
import signal
VERSION_STRING = '''tesseract 3.04.00
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
SPOOFED: CRASH ON OCR or -psm 0
'''
"""Simulates a Tesseract crash when asked to run OCR
It isn't strictly necessary to crash the process and that has unwanted
side effects like triggering core dumps or error reporting, logging and such.
It's enough to dump some text to stderr and return an error code.
Follows the POSIX(?) convention of returning 128 + signal number.
"""
def main():
if sys.argv[1] == '--version':
print(VERSION_STRING, file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
sys.exit(0)
elif sys.argv[-1] == 'hocr':
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
sys.exit(128 + signal.SIGSEGV)
elif sys.argv[-1] == 'pdf':
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
sys.exit(128 + signal.SIGSEGV)
elif sys.argv[-1] == 'stdout':
print("libc++abi.dylib: terminating with uncaught exception of type "
"std::bad_alloc: std::bad_alloc", file=sys.stderr)
sys.exit(128 + signal.SIGABRT)
else:
print("Spoof doesn't understand arguments", file=sys.stderr)
print(sys.argv, file=sys.stderr)
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main()
+19
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
@@ -53,6 +65,13 @@ def main():
pdf_bytes = img2pdf.convert([inputf], dpi=300)
with open(output + '.pdf', 'wb') as f:
f.write(pdf_bytes)
elif sys.argv[-1] == 'stdout':
inputf = sys.argv[-2]
print("""Orientation: 0
Orientation in degrees: 0
Orientation confidence: 100.00
Script: 1
Script confidence: 100.00""", file=sys.stderr)
else:
print("Spoof doesn't understand arguments", file=sys.stderr)
print(sys.argv, file=sys.stderr)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
import sys
def main():
if sys.argv[1] == '--version':
print('0.5')
sys.exit(0)
print("Only supports --version")
sys.exit(1)
if __name__ == '__main__':
main()
+356 -87
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
@@ -11,6 +11,9 @@ import pytest
from ocrmypdf.pageinfo import pdf_get_all_pageinfo
import PyPDF2 as pypdf
from ocrmypdf import ExitCode
from ocrmypdf import leptonica
from ocrmypdf.pdfa import file_claims_pdfa
import platform
if sys.version_info.major < 3:
@@ -20,11 +23,20 @@ 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():
# Docker creates a file named /.dockerinit
return os.path.exists('/.dockerinit')
def is_linux():
return platform.system() == 'Linux'
def setup_module():
@@ -34,42 +46,40 @@ 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 _make_input(input_basename):
def _infile(input_basename):
return os.path.join(TEST_RESOURCES, input_basename)
def _make_output(output_basename):
return os.path.join(TEST_OUTPUT, output_basename)
def _outfile(output_basename):
return os.path.join(TEST_OUTPUT, os.path.basename(output_basename))
def check_ocrmypdf(input_basename, output_basename, *args, env=None):
input_file = _make_input(input_basename)
output_file = _make_output(output_basename)
"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)
assert sh.returncode == 0, dict(stdout=out, stderr=err)
p, out, err = run_ocrmypdf(input_basename, output_basename, *args, env=env)
if p.returncode != 0:
print('stdout\n======')
print(out)
print('stderr\n======')
print(err)
assert p.returncode == 0
assert os.path.exists(output_file), "Output file not created"
assert os.stat(output_file).st_size > 100, "PDF too small or empty"
return output_file
def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
input_file = _make_input(input_basename)
output_file = _make_output(output_basename)
def run_ocrmypdf(input_basename, output_basename, *args, env=None):
"Run ocrmypdf and let caller deal with results"
input_file = _infile(input_basename)
output_file = _outfile(output_basename)
if env is None:
env = os.environ
p_args = ['ocrmypdf'] + list(args) + [input_file, output_file]
p_args = OCRMYPDF + list(args) + [input_file, output_file]
p = Popen(
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
universal_newlines=True, env=env)
@@ -77,22 +87,41 @@ def run_ocrmypdf_env(input_basename, output_basename, *args, env=None):
return p, out, err
def spoof(replace_program, with_spoof):
"""Modify environment variables to override subprocess executables
Before running any executable, ocrmypdf checks the environment variable
OCRMYPDF_PROGRAMNAME to override default program name/location, e.g.
OCRMYPDF_GS redirects from the system path Ghostscript ("gs") to elsewhere.
"""
env = os.environ.copy()
spoofer = os.path.join(SPOOF_PATH, with_spoof)
if not os.access(spoofer, os.X_OK):
os.chmod(spoofer, 0o755)
env['OCRMYPDF_' + replace_program.upper()] = spoofer
return env
@pytest.fixture
def spoof_tesseract_noop():
env = os.environ.copy()
program = os.path.join(SPOOF_PATH, 'tesseract_noop.py')
check_call(['chmod', "+x", program])
env['OCRMYPDF_TESSERACT'] = program
return env
return spoof('tesseract', 'tesseract_noop.py')
@pytest.fixture
def spoof_tesseract_cache():
env = os.environ.copy()
program = os.path.join(SPOOF_PATH, "tesseract_cache.py")
check_call(['chmod', '+x', program])
env['OCRMYPDF_TESSERACT'] = program
return env
if running_in_docker():
return os.environ.copy()
return spoof('tesseract', "tesseract_cache.py")
@pytest.fixture
def spoof_tesseract_crash():
return spoof('tesseract', 'tesseract_crash.py')
@pytest.fixture
def spoof_tesseract_big_image_error():
return spoof('tesseract', 'tesseract_big_image_error.py')
def test_quick(spoof_tesseract_noop):
@@ -102,7 +131,7 @@ def test_quick(spoof_tesseract_noop):
def test_deskew(spoof_tesseract_noop):
# Run with deskew
deskewed_pdf = check_ocrmypdf(
'skew.pdf', 'test_deskew.pdf', '-d', env=spoof_tesseract_noop)
'skew.pdf', 'test_deskew.pdf', '-d', '-v', '1', env=spoof_tesseract_noop)
# Now render as an image again and use Leptonica to find the skew angle
# to confirm that it was deskewed
@@ -110,7 +139,7 @@ def test_deskew(spoof_tesseract_noop):
import logging
log = logging.getLogger()
deskewed_png = _make_output('deskewed.png')
deskewed_png = _outfile('deskewed.png')
rasterize_pdf(
deskewed_pdf,
@@ -120,42 +149,43 @@ def test_deskew(spoof_tesseract_noop):
raster_device='pngmono',
log=log)
from ocrmypdf.leptonica import pixRead, pixDestroy, pixFindSkew
pix = pixRead(deskewed_png)
skew_angle, skew_confidence = pixFindSkew(pix)
pix = pixDestroy(pix)
from ocrmypdf.leptonica import Pix
pix = Pix.read(deskewed_png)
skew_angle, skew_confidence = pix.find_skew()
print(skew_angle)
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
def test_clean(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')
])
def test_exotic_image(spoof_tesseract_cache, pdf, renderer):
# This will run 5 * 2 * 2 = 20 test cases
@pytest.mark.parametrize(
"pdf",
['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'])
@pytest.mark.parametrize("renderer", ['hocr', 'tesseract'])
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type):
check_ocrmypdf(
pdf,
'test_{0}_{1}.pdf'.format(pdf, renderer),
'-dc',
'-v', '1',
'--output-type', output_type,
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
def test_preserve_metadata(spoof_tesseract_noop):
pdf_before = pypdf.PdfFileReader(_make_input('graph.pdf'))
@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)
@@ -163,20 +193,30 @@ 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):
input_file = _make_input('c02-22.pdf')
output_file = _make_output('test_override_metadata.pdf')
@pytest.mark.skipif(
is_linux() and not running_in_docker(),
reason="likely to fail if Linux locale is not configured correctly")
@pytest.mark.parametrize("output_type", [
'pdfa', 'pdf'
])
def test_override_metadata(spoof_tesseract_noop, output_type):
input_file = _infile('c02-22.pdf')
output_file = _outfile('test_override_metadata.pdf')
german = 'Du siehst den Wald vor lauter Bäumen nicht.'
chinese = '孔子'
high_unicode = 'U+1030C is: 𐌌'
p, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
input_file, output_file,
'--title', german,
'--author', chinese,
'--subject', high_unicode,
'--output-type', output_type,
env=spoof_tesseract_noop)
assert p.returncode == ExitCode.ok
@@ -195,6 +235,9 @@ def test_override_metadata(spoof_tesseract_noop):
assert pdfinfo['Subject'] == high_unicode
assert pdfinfo.get('Keywords', '') == ''
pdfa_info = file_claims_pdfa(output_file)
assert pdfa_info['output'] == output_type
@pytest.mark.parametrize('renderer', [
'hocr',
@@ -202,19 +245,19 @@ def test_override_metadata(spoof_tesseract_noop):
])
def test_oversample(spoof_tesseract_cache, renderer):
oversampled_pdf = check_ocrmypdf(
'skew.pdf', 'test_oversample_%s.pdf' % renderer, '--oversample', '300',
'skew.pdf', 'test_oversample_%s.pdf' % renderer, '--oversample', '350',
'-f',
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
pdfinfo = pdf_get_all_pageinfo(oversampled_pdf)
print(pdfinfo[0]['xres'])
assert abs(pdfinfo[0]['xres'] - 300) < 1
assert abs(pdfinfo[0]['xres'] - 350) < 1
def test_repeat_ocr():
sh, _, _ = run_ocrmypdf_sh('graph_ocred.pdf', 'wontwork.pdf')
assert sh.returncode != 0
p, _, _ = run_ocrmypdf('graph_ocred.pdf', 'wontwork.pdf')
assert p.returncode != 0
def test_force_ocr(spoof_tesseract_cache):
@@ -230,14 +273,113 @@ def test_skip_ocr(spoof_tesseract_cache):
def test_argsfile(spoof_tesseract_noop):
with open(_make_output('test_argsfile.txt'), 'w') as argsfile:
with open(_outfile('test_argsfile.txt'), 'w') as argsfile:
print('--title', 'ArgsFile Test', '--author', 'Test Cases',
sep='\n', end='\n', file=argsfile)
check_ocrmypdf('graph.pdf', 'test_argsfile.pdf',
'@' + _make_output('test_argsfile.txt'),
'@' + _outfile('test_argsfile.txt'),
env=spoof_tesseract_noop)
def check_monochrome_correlation(
reference_pdf, reference_pageno, test_pdf, test_pageno):
import ocrmypdf.ghostscript as ghostscript
import logging
gslog = logging.getLogger()
reference_png = _outfile('{}.ref{:04d}.png'.format(
reference_pdf, reference_pageno))
test_png = _outfile('{}.test{:04d}.png'.format(
test_pdf, test_pageno))
def rasterize(pdf, pageno, png):
if os.path.exists(png):
print(png)
return
ghostscript.rasterize_pdf(
pdf,
png,
xres=100, yres=100,
raster_device='pngmono', log=gslog, pageno=pageno)
rasterize(reference_pdf, reference_pageno, reference_png)
rasterize(test_pdf, test_pageno, test_png)
pix_ref = leptonica.Pix.read(reference_png)
pix_test = leptonica.Pix.read(test_png)
return leptonica.Pix.correlation_binary(pix_ref, pix_test)
def test_monochrome_correlation():
# Verify leptonica: check that an incorrect rotated image has poor
# correlation with reference
corr = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=1, # north facing page
test_pdf=_infile('cardinal.pdf'),
test_pageno=3, # south facing page
)
assert corr < 0.10
corr = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=2,
test_pdf=_infile('cardinal.pdf'),
test_pageno=2,
)
assert corr > 0.90
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
def test_autorotate(spoof_tesseract_cache, renderer):
# cardinal.pdf contains four copies of an image rotated in each cardinal
# direction - these ones are "burned in" not tagged with /Rotate
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
'-r', '-v', '1', env=spoof_tesseract_cache)
for n in range(1, 4+1):
correlation = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=1,
test_pdf=out,
test_pageno=n)
assert correlation > 0.80
def test_autorotate_threshold_low(spoof_tesseract_cache):
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_low.pdf',
'--rotate-pages-threshold', '1',
'-r', '-v', '1', env=spoof_tesseract_cache)
# Low threshold -> always rotate -> expect high correlation between
# reference page and test page
correlation = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=1,
test_pdf=out,
test_pageno=3)
assert correlation > 0.80
def test_autorotate_threshold_high(spoof_tesseract_cache):
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_high.pdf',
'--rotate-pages-threshold', '99',
'-r', '-v', '1', env=spoof_tesseract_cache)
# High threshold -> never rotate -> expect low correlation since
# test page will not be rotated
correlation = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=1,
test_pdf=out,
test_pageno=3)
assert correlation < 0.10
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
@@ -246,26 +388,25 @@ def test_ocr_timeout(renderer):
out = check_ocrmypdf('skew.pdf', 'test_timeout_%s.pdf' % renderer,
'--tesseract-timeout', '1.0')
pdfinfo = pdf_get_all_pageinfo(out)
assert pdfinfo[0]['has_text'] == False
assert not pdfinfo[0]['has_text']
def test_skip_big(spoof_tesseract_cache):
out = check_ocrmypdf('enormous.pdf', 'test_enormous.pdf',
'--skip-big', '10', env=spoof_tesseract_cache)
pdfinfo = pdf_get_all_pageinfo(out)
assert pdfinfo[0]['has_text'] == False
assert not pdfinfo[0]['has_text']
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
def test_maximum_options(spoof_tesseract_cache, renderer):
@pytest.mark.parametrize('renderer', ['hocr', 'tesseract'])
@pytest.mark.parametrize('output_type', ['pdf', 'pdfa'])
def test_maximum_options(spoof_tesseract_cache, renderer, output_type):
check_ocrmypdf(
'multipage.pdf', 'test_multipage%s.pdf' % renderer,
'-d', '-c', '-i', '-g', '-f', '-k', '--oversample', '300',
'--skip-big', '10', '--title', 'Too Many Weird Files',
'--author', 'py.test', '--pdf-renderer', renderer,
'--output-type', output_type,
env=spoof_tesseract_cache)
@@ -273,74 +414,87 @@ 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
@pytest.mark.skipif(running_in_docker(),
reason="writes to tests/resources")
def test_uppercase_extension(spoof_tesseract_noop):
shutil.copy(_make_input("skew.pdf"), _make_input("UPPERCASE.PDF"))
shutil.copy(_infile("skew.pdf"), _infile("UPPERCASE.PDF"))
try:
check_ocrmypdf("UPPERCASE.PDF", "UPPERCASE_OUT.PDF",
env=spoof_tesseract_noop)
finally:
os.unlink(_make_input("UPPERCASE.PDF"))
os.unlink(_infile("UPPERCASE.PDF"))
def test_input_file_not_found():
input_file = "does not exist.pdf"
sh, out, err = run_ocrmypdf_sh(
_make_input(input_file),
_make_output("will not happen.pdf"))
assert sh.returncode == ExitCode.input_file
p, out, err = run_ocrmypdf(
_infile(input_file),
_outfile("will not happen.pdf"))
assert p.returncode == ExitCode.input_file
assert (input_file in out or input_file in err)
def test_input_file_not_a_pdf():
input_file = __file__ # Try to OCR this file
sh, out, err = run_ocrmypdf_sh(
_make_input(input_file),
_make_output("will not happen.pdf"))
assert sh.returncode == ExitCode.input_file
p, out, err = run_ocrmypdf(
_infile(input_file),
_outfile("will not happen.pdf"))
assert p.returncode == ExitCode.input_file
assert (input_file in out or input_file in err)
def test_qpdf_repair_fails():
env = os.environ.copy()
env['OCRMYPDF_QPDF'] = os.path.abspath('./spoof/qpdf_dummy_return2.py')
p, out, err = run_ocrmypdf_env(
p, out, err = run_ocrmypdf(
'-v', '1',
'c02-22.pdf', 'wont_be_created.pdf', env=env)
print(out)
@@ -349,7 +503,7 @@ def test_qpdf_repair_fails():
def test_encrypted():
p, out, err = run_ocrmypdf_env('skew-encrypted.pdf', 'wont_be_created.pdf')
p, out, err = run_ocrmypdf('skew-encrypted.pdf', 'wont_be_created.pdf')
assert p.returncode == ExitCode.input_file
assert out.find('password')
@@ -366,4 +520,119 @@ def test_pagesegmode(renderer, spoof_tesseract_cache):
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
def test_tesseract_crash(renderer, spoof_tesseract_crash):
p, out, err = run_ocrmypdf(
'ccitt.pdf', 'wontwork.pdf', '-v', '1',
'--pdf-renderer', renderer, env=spoof_tesseract_crash)
assert p.returncode == ExitCode.child_process_error
assert not os.path.exists(_outfile('wontwork.pdf'))
assert "ERROR" in err
def test_tesseract_crash_autorotate(spoof_tesseract_crash):
p, out, err = run_ocrmypdf(
'ccitt.pdf', 'wontwork.pdf',
'-r', env=spoof_tesseract_crash)
assert p.returncode == ExitCode.child_process_error
assert not os.path.exists(_outfile('wontwork.pdf'))
assert "ERROR" in err
print(out)
print(err)
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
check_ocrmypdf(
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
def test_no_unpaper():
env = os.environ.copy()
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/no_unpaper_here.py')
p, out, err = run_ocrmypdf(
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
assert p.returncode == ExitCode.missing_dependency
def test_old_unpaper():
env = os.environ.copy()
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/unpaper_oldversion.py')
p, out, err = run_ocrmypdf(
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
assert p.returncode == ExitCode.missing_dependency
def test_algo4():
p, _, _ = run_ocrmypdf('encrypted_algo4.pdf', 'wontwork.pdf')
assert p.returncode == ExitCode.encrypted_pdf
@pytest.mark.parametrize('renderer', [
'hocr']) # tesseract cannot pass this test - resamples to square image
def test_non_square_resolution(renderer, spoof_tesseract_cache):
# Confirm input image is non-square resolution
in_pageinfo = pdf_get_all_pageinfo(_infile('aspect.pdf'))
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
out = 'aspect_%s.pdf' % renderer
check_ocrmypdf(
'aspect.pdf', out,
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(_outfile(out))
# Confirm resolution was kept the same
assert in_pageinfo[0]['xres'] == out_pageinfo[0]['xres']
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
def test_image_to_pdf(spoof_tesseract_noop):
check_ocrmypdf(
'LinnSequencer.jpg', 'image_to_pdf.pdf', '--image-dpi', '200',
env=spoof_tesseract_noop)
def test_jbig2_passthrough(spoof_tesseract_cache):
out = check_ocrmypdf(
'jbig2.pdf', 'jbig2_out.pdf',
'--output-type', 'pdf',
'--pdf-renderer', 'hocr',
env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(out)
assert out_pageinfo[0]['images'][0]['enc'] == 'jbig2'
def test_stdin(spoof_tesseract_noop):
input_file = _infile('francais.pdf')
output_file = _outfile('test_stdin.pdf')
# Runs: cat testfile.pdf | ocrmypdf - output.pdf
p1_args = ['cat', input_file]
p1 = Popen(p1_args, close_fds=True, stdin=DEVNULL, stdout=PIPE)
p2_args = OCRMYPDF + ['-', output_file]
p2 = Popen(
p2_args, close_fds=True, stdout=PIPE, stderr=PIPE,
stdin=p1.stdout, env=spoof_tesseract_noop)
p1.stdout.close()
out, err = p2.communicate()
assert p2.returncode == ExitCode.ok
def test_masks(spoof_tesseract_noop):
check_ocrmypdf('masks.pdf', 'test_masks.pdf', env=spoof_tesseract_noop)
def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop):
check_ocrmypdf('milk.pdf', 'test_milk.pdf', env=spoof_tesseract_noop)
+19 -6
View File
@@ -67,13 +67,21 @@ def test_single_page_text():
def test_single_page_image():
filename = os.path.join(TEST_OUTPUT, 'image-mono.pdf')
with NamedTemporaryFile() as im_tmp:
with NamedTemporaryFile(mode='wb+', suffix='.png') as im_tmp:
im = Image.new('1', (8, 8), 0)
for n in range(8):
im.putpixel((n, n), 1)
im.save(im_tmp.name, format='PNG')
pdf_bytes = img2pdf.convert([im_tmp.name], dpi=8)
imgsize = ((img2pdf.ImgSize.dpi, 8), (img2pdf.ImgSize.dpi, 8))
layout_fun = img2pdf.get_layout_fun(None, imgsize, None, None, None)
im_tmp.seek(0)
im_bytes = im_tmp.read()
pdf_bytes = img2pdf.convert(
im_bytes, producer="img2pdf", with_pdfrw=False,
layout_fun=layout_fun)
with open(filename, 'wb') as pdf:
pdf.write(pdf_bytes)
@@ -95,8 +103,8 @@ def test_single_page_image():
assert pdfimage['bpc'] == 8
# DPI in a 1"x1" is the image width
assert pdfimage['dpi_w'] == 8
assert pdfimage['dpi_h'] == 8
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
def test_single_page_inline_image():
@@ -112,8 +120,12 @@ def test_single_page_inline_image():
pdf.showPage()
pdf.save()
with pytest.raises(NotImplementedError):
pageinfo.pdf_get_all_pageinfo(filename)
pdfinfo = pageinfo.pdf_get_all_pageinfo(filename)
print(pdfinfo)
pdfimage = pdfinfo[0]['images'][0]
assert (pdfimage['dpi_w'] - 8) < 1e-5
assert pdfimage['color'] != '-'
assert pdfimage['width'] == 8
def test_jpeg():
@@ -123,4 +135,5 @@ def test_jpeg():
pdfimage = pdfinfo[0]['images'][0]
assert pdfimage['enc'] == 'jpeg'
assert (pdfimage['dpi_w'] - 150) < 1e-5