Compare commits

..
278 Commits
Author SHA1 Message Date
James R. Barlow 0b10db91be Fix regression: Disable Ghostscript JPEG passthrough entirely 2018-04-17 17:00:24 -07:00
James R. Barlow 1a516b2af9 Fix regression: time stamp test suite failures 2018-04-17 16:59:21 -07:00
James R. Barlow 076363d78e Disable JPEG passthrough for Ghostscript 9.23
Seems to corrupt JPEGs involved in image masks?
2018-04-17 16:31:03 -07:00
James R. Barlow 5fde214290 Update notes for v6.1.5 2018-04-17 15:23:35 -07:00
James R. Barlow a620724d6a Fix PDF/A validation failure due to timezone being omitted from /ModDate 2018-04-17 15:16:48 -07:00
James R. Barlow 7368399f8b Clarify license of two test files - https://github.com/jbarlow83/OCRmyPDF/issues/254 2018-04-17 11:56:36 -07:00
James R. Barlow 34c78a892a Fix list table for tests/resources
[ci skip]
2018-04-15 23:52:19 -07:00
James R. Barlow 9d28879505 Update Ubuntu 14.04 instructions
Closes #252
2018-04-14 17:30:33 -07:00
James R. Barlow 2482296e2b hocr: avoid division by zero
Issue #253 - PDF that produces the error is not available, but if font_width
is zero, chances are the text is nonprinting characters, so suppress it.
2018-04-14 17:24:21 -07:00
James R. Barlow 7fc897e6dc Fix NameError 'ghostscript' 2018-04-12 21:24:05 -07:00
James R. Barlow 9b731d63b8 Set Ghostscript -sColorConversionStrategy the way old/new versions expect 2018-04-12 16:28:48 -07:00
James R. Barlow 10aa59f674 v6.1.4 fix test suite regression with Ghostscript 9.23 2018-04-12 15:16:54 -07:00
James R. Barlow 1f7837e7b1 v6.1.4 release notes update 2018-04-12 00:55:45 -07:00
James R. Barlow ba0535e3fb Update test cache to account for unpaper --layout none change 2018-04-12 00:48:21 -07:00
James R. Barlow 49fa7f6b5c tesseract_cache: don't reveal host system file paths in manifest file 2018-04-12 00:47:28 -07:00
James R. Barlow c95db246d4 v6.1.4 merge 2018-04-11 15:58:00 -07:00
James R. Barlow 1ba93371ce docs: Update installation to reflect qpdf 7.0.0 requirement 2018-04-11 15:40:50 -07:00
James R. Barlow fedbbdb575 Travis: compile qpdf from source
The older version in Travis's Ubuntu 14.04 can't pass the test suite anymore.
2018-04-11 15:40:45 -07:00
James R. Barlow 85ebba72bc Fix setup.py syntax 2018-04-10 18:30:48 -07:00
James R. Barlow b6cd436d5d setup: Blacklist Pillow 5.1.0 on macos
https://github.com/python-pillow/Pillow/issues/3068
2018-04-10 18:15:37 -07:00
James R. Barlow ec170c7e1e Travis: use setup.py for requirements, don't override with .txt 2018-04-10 17:52:19 -07:00
James R. Barlow 3d69b46fca Release notes 2018-04-10 15:53:02 -07:00
James R. Barlow 4b6153ad18 Use defusedxml for XML parsing when reading XMP 2018-04-10 14:25:13 -07:00
James R. Barlow 75d37eb103 docs: expand ocr of image usage 2018-04-09 13:06:09 -07:00
James R. Barlow 11b6f77df0 unpaper: close images on error paths 2018-04-09 13:05:12 -07:00
James R. Barlow db8b0319dd get_version: repeat system error messages if the process exists with a signal 2018-04-09 13:04:51 -07:00
James R. Barlow d1d4f1e198 Add issue links to release notes 2018-04-06 14:52:40 -07:00
James R. Barlow be41ff6d54 Update flowchart
[ci skip]
2018-04-05 21:26:37 -07:00
James R. Barlow 1dbb6f1746 Notes on relevant envvars, repology 2018-04-05 02:15:01 -07:00
James R. Barlow 753e6274ab Tell unpaper to use --layout none so it won't blank out multi column text 2018-04-05 02:14:33 -07:00
James R. Barlow 7f462c618b v6.1.3 notes 2018-04-03 00:11:20 -07:00
James R. Barlow 1b01d45dd2 Warn about Python 3.5 page count issue 2018-04-02 19:29:17 -07:00
James R. Barlow 7a1cd39b21 Fix creation date metadata lost from input
Closes #247
2018-04-02 17:53:39 -07:00
James R. Barlow 1c1fd9616a Don't depend on pytest-xdist in setup.cfg 2018-04-02 11:45:03 -07:00
Sean Whittonandjbarlow83 11e19e4085 remove addopts key from tool:pytest section of setup.cfg (#246)
The '-n' command line argument is not supported by recent pytest.
2018-04-02 14:43:38 -04:00
James R. Barlow 2a43f73228 Update installation.rst, further info on fitz 2018-04-02 11:32:57 -07:00
James R. Barlow b1d1310a75 Dockerfile: use fitz 2018-04-02 11:08:03 -07:00
James R. Barlow 0e7fa78e65 Remove inaccurate statement from setup.py 2018-04-01 13:20:17 -07:00
James R. Barlow 4032570d97 Change docs for fitz/PyMuPDF 2018-04-01 13:19:57 -07:00
James R. Barlow 90644a3017 pipeline: refactoring, use with block for images 2018-03-31 13:26:40 -07:00
James R. Barlow 4f6bffb477 Update copyrights 2018-03-31 11:54:38 -07:00
James R. Barlow 158f902c3b Fixed setup.py syntax error 2018-03-30 14:00:36 -07:00
James R. Barlow 6dc25ddc6e v6.1.2: add license to wheels, depend on defusedxml 2018-03-30 13:22:35 -07:00
James R. Barlow 7f6aaeaecf v6.1.2 2018-03-30 12:39:33 -07:00
James R. Barlow ace439910e Remove PyMuPDF 1.12.4 shim 2018-03-30 12:33:27 -07:00
James R. Barlow 7f038568de Add envvar to ease testing without PyMuPDF 2018-03-30 12:32:48 -07:00
James R. Barlow af777c0b6a Test macos without fitz too 2018-03-30 00:13:09 -07:00
James R. Barlow fc299032a4 v6.1.1 release notes
Better get the last one out
2018-03-30 00:11:52 -07:00
James R. Barlow e0f3f07907 Fix text reported as found on all pages when PyMuPDF is not available 2018-03-30 00:10:53 -07:00
James R. Barlow b36df9cf9e pdfa: codecs.encode -> hexlify (simpler) 2018-03-29 22:17:23 -07:00
James R. Barlow 81c3f780d4 Travis: Should test 3.6 Linux without fitz too 2018-03-28 23:54:43 -07:00
James R. Barlow b51efdd3e3 Travis: don't upload to legacy PyPI anymore, it will stop working soon 2018-03-28 23:40:29 -07:00
James R. Barlow 610b769df9 Update release notes 2018-03-28 23:33:34 -07:00
James R. Barlow 527f4d0101 Workaround fitz not escaping parentheses
Closes #239
2018-03-28 23:23:34 -07:00
James R. Barlow 8d9be43c60 test_bookmarks_preserved won't raise ImportError any more
Due to trapping this in ocrmypdf.lib
2018-03-28 23:22:55 -07:00
James R. Barlow 40ef4f0bbe Add new argument --skip-repair to skip the repair step 2018-03-28 00:54:58 -07:00
James R. Barlow d0271d5049 More debug messages on repair; update notes 2018-03-28 00:39:38 -07:00
James R. Barlow 5becfcf8ea Refactor fitz ImportError trap 2018-03-27 21:38:02 -07:00
James R. Barlow 112e8d6c18 Fix regression: PDF/A broken without fitz 2018-03-27 21:33:10 -07:00
James R. Barlow 1d8d49a01d Add PyMuPDF to preamble 2018-03-27 21:32:38 -07:00
James R. Barlow 5050155685 Add warning for large file size increases 2018-03-27 15:49:16 -07:00
James R. Barlow a9bd494cc0 Merge branch 'optional-fitz' 2018-03-27 13:36:33 -07:00
James R. Barlow 6a4df78bc0 Add _naive_find_text to search for text when fitz is not available 2018-03-27 13:36:17 -07:00
James R. Barlow 530eae3898 Fix test_main missing file_claims_pdfa 2018-03-26 15:33:53 -07:00
James R. Barlow 3e444f6a90 Make fitz optional 2018-03-26 13:22:09 -07:00
James R. Barlow 45dbff6401 Fix table of contents not preserved in PDF/A 2018-03-26 02:23:19 -07:00
James R. Barlow bc56b8e058 Move metadata tests to new test_metadata 2018-03-26 01:49:25 -07:00
James R. Barlow d86e315c48 v6.0.1 start release notes 2018-03-26 01:44:01 -07:00
James R. Barlow 746969207a Remove deprecated --pdf-renderer tess4, which was renamed to sandwich
Should have been cut in v6.0.0
2018-03-26 01:17:22 -07:00
James R. Barlow 1caebaefb5 tesseract: Fix FileExistsError on if output file was created at timeout 2018-03-25 21:38:20 -07:00
James R. Barlow 2d10fdcf0f Fix typo in release notes 2018-03-25 21:37:06 -07:00
James R. Barlow 355ec70a80 Note other web frontends 2018-03-25 21:36:39 -07:00
James R. Barlow a2f499de01 Remove pageinfo.py which release notes said was gone for v6 2018-03-25 12:16:56 -07:00
James R. Barlow f4bca89722 Remove Tesseract 4 message 2018-03-25 12:16:31 -07:00
James R. Barlow 9fbc69df3f v6.0.0 release 2018-03-25 01:34:26 -07:00
James R. Barlow 230d301268 conftest: py3.5 path issue 2018-03-25 00:52:45 -07:00
James R. Barlow 1ce7b02d94 Travis: don't cache tests/cache anymore, you get it with git 2018-03-25 00:52:19 -07:00
James R. Barlow a2d00f5f1d tess cache: fix tess3 error for -psm instead of --psm 2018-03-25 00:43:02 -07:00
James R. Barlow f68eaa3b46 Fix PyMuPDF version for Travis 2018-03-25 00:36:26 -07:00
James R. Barlow 0199ab220e Tweak Manifest and .travis once more
Travis "do_not_include" moving around no longer needed, thankfully.
Manifest needed LICENSE.
2018-03-25 00:19:45 -07:00
James R. Barlow 656045610a Update release notes 2018-03-25 00:17:23 -07:00
James R. Barlow 8c1c61f207 test cache: fix Path + str error 2018-03-25 00:02:03 -07:00
James R. Barlow af085b79dd Move ocrmypdf to src/ocrmypdf 2018-03-24 23:59:08 -07:00
James R. Barlow 77476965ae test cache: use .bin extension, fix .gitignore .gitattributes 2018-03-24 23:54:16 -07:00
James R. Barlow 961c1365f9 Update manifest.in 2018-03-24 23:50:58 -07:00
James R. Barlow ca51514046 Add test cache 2018-03-24 23:50:41 -07:00
James R. Barlow 8975b72a01 Fix test_testonly_pdf generating an output file in pwd 2018-03-24 22:34:35 -07:00
James R. Barlow 874ec6a87f Add missing fixture to test_unpaper 2018-03-24 22:24:14 -07:00
James R. Barlow 909eaeeead spoof: Allow tesseract cache to share cache
Previous incarnation was only suitable for generating a local cache
where the suite was executed repeatedly. Now the cache ignores
differences, so it can be checked into Github and shared.
2018-03-24 22:17:36 -07:00
James R. Barlow c138161fae Tests: more cleanup 2018-03-24 15:35:57 -07:00
James R. Barlow e48590d66c Refactor out unpaper-specific tests 2018-03-24 15:21:44 -07:00
James R. Barlow 5b1c8541fc Review some skipped tests to make sure reasons still valid 2018-03-24 15:13:23 -07:00
James R. Barlow e5e011021b Remove the OCRMYPDF_program environment variables
Really, this was just replicating the functionality of the PATH
environment variable, and users probably do that anyway.
2018-03-24 15:09:08 -07:00
James R. Barlow 11d74dea09 Remove the OCRMYPDF_program environment variables
Really, this was just replicating the functionality of the PATH
environment variable, and users probably do that anyway.
2018-03-24 15:07:02 -07:00
James R. Barlow cbdf9c88c5 Update requirements 2018-03-24 14:03:34 -07:00
James R. Barlow 46601b1350 setup: skip 1.12.4.1 since it does not provide wheels 2018-03-24 02:59:58 -07:00
James R. Barlow 6f1a40b2ca v6.0.0 notes, build machinery changes 2018-03-24 02:52:56 -07:00
James R. Barlow a2b1f54eb2 Update documentation license info 2018-03-24 02:33:24 -07:00
James R. Barlow 6756016572 Add license notice to all files
Source files to GPL3

Exceptions:
-tests/spoof/* to MIT
-hocrtransform.py
-_unicodefun.py

Test resources to CC BY-SA 4.0 except when otherwise noted.

Add GPL license.
2018-03-24 02:33:24 -07:00
James R. Barlow f42123afc3 pipeline: make removal of merge_qpdf more explicit 2018-03-24 02:30:05 -07:00
James R. Barlow 1425ffd274 pipeline: Merge branch 'feature/mumerge' into test
Replaces qpdf page merging
2018-03-24 02:26:01 -07:00
James R. Barlow d700154e0e Fix regressions after --skip-text improvements 2018-03-24 02:24:45 -07:00
James R. Barlow efecf42566 Add PyMuPDF and use to detect text on pages 2018-03-24 02:16:53 -07:00
James R. Barlow 74bdfc07fb mumerge: fix regressions 2018-03-24 01:18:22 -07:00
James R. Barlow 376dfdba1c Fix text/image files not closed in combine_layers 2018-03-23 13:48:37 -07:00
James R. Barlow 3795d6720f Try out pymupdf merging
With garbage collection it reduces waste on the worst case file.
That's nice. 1 MB -> 105 MB -> 1.5 MB.

Indicates really problem is using PyPDF2 to watermark.

Currently hacked into --output-type pdf.
2018-03-23 13:39:16 -07:00
James R. Barlow 537aaf56d7 Remove duplication between page merge functions 2018-03-23 13:38:53 -07:00
James R. Barlow 34d51b5d3d Merge branch 'feature/faster-split' 2018-03-23 13:10:53 -07:00
James R. Barlow dea8fcfb5b Optimize page splitting by multiprocessing
Previously page splitting occurred in a single process because it was
not believed to affect performance much. It turned out to be an expensive
operation.

It now scales better with large page sizes although this has a negative
effect on small files.

Overall time changes as follows:

7 page file, 9.02s -> 9.56s
731 page file, 213s -> 97s

WITH --tesseract-timeout 0 --output-type pdf --skip-text

i.e. you don't get a 2.2x speed gain when OCR is available.

Squashed a commit to ix test suite failure on --rotate-pages
Squashed a commit to remove debug code
2018-03-23 13:07:51 -07:00
James R. Barlow 4f1f3b9b51 Move available_cpu_count to helpers 2018-03-23 13:07:51 -07:00
James R. Barlow dfeb8812ad Document some instances of 0 vs 1-based page numbering, import cleanup 2018-03-23 13:07:31 -07:00
James R. Barlow 63e2b4273a Travis: avoid using set -e since it interferes with Travis
https://github.com/travis-ci/docs-travis-ci-com/issues/1672
2018-03-23 12:49:12 -07:00
James R. Barlow 5790dbc085 Merge commit '9e2105e08d5fc765dbf636d108809bb66ab562a5' 2018-03-20 18:15:57 -07:00
jbarlow83andGitHub 9e2105e08d Update readme shields
Drop Docker Hub for now, add homebrew
2018-03-20 17:16:28 -07:00
James R. Barlow 22582bbd1c Travis: don't trigger Docker Hub anymore
Docker Cloud is set up to build on pushes to master and tagged releases.
Hopefully that will work out.
2018-03-19 21:09:31 -07:00
James R. Barlow e5f27b7a12 Solve text detection issue with PyMuPDF 2018-03-15 22:29:56 -07:00
James R. Barlow e88ec9822b Tweak release notes 2018-03-15 17:09:43 -07:00
James R. Barlow 5ffd2f5c96 Not ending Py3.5 support just yet 2018-03-15 17:06:11 -07:00
James R. Barlow 11fdb4c5d8 Update release notes for v5.7.0 2018-03-15 17:06:04 -07:00
James R. Barlow 319aff6d09 Merge better-hocr 2018-03-15 16:59:59 -07:00
James R. Barlow a614fa3400 hocr: simplify some math expressions and add comments 2018-03-14 17:05:40 -07:00
endolithandjbarlow83 8d691391ac Fix typos in advanced.rst (#228) 2018-03-14 15:54:55 -04:00
James R. Barlow 0089a84c94 hocr: Make interword spaces default and non-optional for hocr
Update documentation to match.
2018-03-13 14:51:47 -07:00
James R. Barlow 90676e1c6a hocr: Remove baseline dashes 2018-03-13 14:45:31 -07:00
James R. Barlow 062901be43 Some cleanup and variable renaming 2018-03-13 14:29:22 -07:00
James R. Barlow b195d79b50 Refactoring 2018-03-13 11:04:34 -07:00
James R. Barlow 6d7ee98721 Force Tesseract 4 to be single threaded
Gives better performance (throughput basis) than the existing solution
and scales better on powerful boxes.
2018-03-13 08:54:52 -07:00
James R. Barlow fc0800ed5d v5.6.3 notes 2018-03-12 03:41:12 -07:00
James R. Barlow f4e3a0e5b2 v5.6.2 notes 2018-03-09 15:37:08 -08:00
James R. Barlow d631c80024 Suppress debug message when merging large files 2018-03-09 11:10:45 -08:00
James R. Barlow f1f0033875 Suppress spurious debug message in --output-type pdf 2018-03-09 11:07:28 -08:00
James R. Barlow 84d120e850 v5.6.1 notes 2018-03-09 08:00:42 -08:00
James R. Barlow 8159cc6b88 Skip one test that fails for qpdf 8.0.[0,1], due to qpdf regression 2018-03-09 07:57:22 -08:00
James R. Barlow 995f8c106b hocr: account for baseline offset to position text more accurately 2018-03-09 07:45:41 -08:00
Jim Barlow 7cc104b138 hocr: account for skewed baseline 2018-03-05 11:16:40 -05:00
James R. Barlow b3a7299a62 hocr: refactor/improve PEP8 a bit 2018-03-05 10:47:36 -05:00
James R. Barlow 0e7a4deaec hocr: add baseline function, hocr doc link 2018-03-05 10:47:36 -05:00
James R. Barlow b4d66650bd hocr: adjust text cursor with relative moves 2018-03-05 10:47:36 -05:00
James R. Barlow 4986afca28 hocr: Refactor use of text object
We don't need to declare the font on each word.

No improvement for removing trailing space or adding \n
2018-03-05 10:47:36 -05:00
James R. Barlow 2b6004a82b hocr: Make words on line use the line height
Seems to improve the behavior and appearance
of selected text a fair bit.
2018-03-05 10:47:36 -05:00
James R. Barlow 04c54a7c31 Suppress spurious debug message in --output-type pdf 2018-03-05 10:47:36 -05:00
James R. Barlow 7ae6c5ae87 Trial merge interword-spaces 2018-03-02 23:47:06 -08:00
James R. Barlow 4046766ca5 Fix Python 3.5 test suite failure on symlinks
Did not account for API difference in pathlib
2018-03-02 16:57:46 -08:00
James R. Barlow 810390df0b pipeline: Refactor duplicate with clause 2018-03-02 16:57:15 -08:00
James R. Barlow de749bc7ae Fix regression - output to stdout broken 2018-03-02 15:35:51 -08:00
James R. Barlow 9965b8800c Move Dockerfiles out of the way 2018-03-02 15:26:41 -08:00
James R. Barlow ab870fddd6 Dockerignore: glob supported now 2018-03-02 15:24:42 -08:00
James R. Barlow a79d6807cf Dockerfiles: remove deprecated MAINTAINER tag 2018-03-02 15:24:28 -08:00
James R. Barlow 8185fb7e43 Migrate Travis CI setup to Brewfile 2018-03-02 15:16:05 -08:00
Tucker Barbourandjbarlow83 4b10929b25 Fix Homebrew python package (#227)
Homebrew removed python3 and python now defaults to version 3. Here we
use `brew upgrade python` to upgrade the pre-installed version of
python to python3.
2018-03-02 14:59:23 -08:00
Tucker Barbour f6c70312c9 Fix Homebrew python package
Homebrew removed python3 and python now defaults to version 3. Here we
use `brew upgrade python` to upgrade the pre-installed version of
python to python3.
2018-03-02 14:26:13 +00:00
Tucker Barbour 9fd9c7a51f Scale BoundingBox and Text elements to account for additional space.
Here we are manually scaling the pt width used for the BoundingBox and
the Text element when manually adding whitespace to account for
limitations of the PDF.js viewer. This fixes an initial regression
noticed when selecting text elements in Chrome and PDFium. The width
of the Text element and BoundBox had not been adjusted for the
additional whitespace so the highlighting was offset slightly.
2018-03-02 11:18:47 +00:00
Charles Forcey 422e619978 Add a note to the documentation about interword-spaces 2018-03-01 13:15:03 -05:00
Tucker BarbourandCharles Forcey e6e34251c6 Add option to explicitly add interword spaces to HOCR pdf-renderer
This commit includes an optional work around for limitations of the
PDF.js viewer described in
https://github.com/jbarlow83/OCRmyPDF/issues/133. Here is explicitly
add an addition space to text elements before drawing them on the PDF
canvas when using the HOCR renderer. This option does not apply to
other pdf renderers in OCRmyPDF and is turned off by default.
2018-03-01 13:15:03 -05:00
James R. Barlow 2d8aad1086 Improve docs 2018-03-01 00:24:38 -08:00
James R. Barlow 74ca736333 Issue #223: improve text of encrypted PDF error message 2018-02-27 15:08:22 -08:00
James R. Barlow 5e4fd8b0b9 compile_leptonica should have shebang 2018-02-24 12:49:36 -08:00
James R. Barlow 8ab8132411 lint: unused variables, wildcard imports 2018-02-24 12:48:52 -08:00
James R. Barlow 6899dd46e4 lint: remove extraneous backslash 2018-02-24 12:42:37 -08:00
James R. Barlow 8ad0697a20 lint: remove duplicate property definition 2018-02-24 12:42:03 -08:00
James R. Barlow b47e5672e6 Remove old test case that no longer works 2018-02-24 12:40:14 -08:00
James R. Barlow 45c7bd9a60 lint: Remove shebangs from non-executable files 2018-02-24 12:38:58 -08:00
James R. Barlow e7bcb95635 Fix pylint errors 2018-02-24 11:59:01 -08:00
James R. Barlow 3de83627a9 Handle output to /dev/null or directory (#219)
Previously we threw an exception if the output name was a directory (only after doing OCR) and would trigger a PermissionError on trying to flip permission bits of /dev/null due to shutil.copyfile implementation. Instead of copying file use shutil.copyfileobj which should also respect umask etc.
2018-02-19 22:15:07 -08:00
James R. Barlow f248576994 Change instructions to point away from private tap 2018-02-19 17:33:58 -08:00
James R. Barlow aac5b6de3b Update autobrew script to match final changes 2018-02-17 00:12:03 -08:00
James R. Barlow 24435f11e0 We are now in homebrew 2018-02-15 17:42:16 -08:00
James R. Barlow a9da839c39 Add vector-only PDF test case 2018-02-08 00:17:35 -08:00
James R. Barlow fa2c0296d6 v5.6.0 release notes, docs 2018-02-07 16:48:04 -08:00
James R. Barlow 1dfc32d7e6 Preserve "text as curves" vector content
Never updated the checking logic to deal with a pure vector file with no text that needs an OCR layer. This is doable, so allow it.
2018-02-07 16:05:48 -08:00
James R. Barlow 4a61beae41 autobrew: use homebrew's built-in test fixture 2018-02-05 11:10:33 -08:00
James R. Barlow bd30587bf1 Update depends_on order 2018-01-29 13:16:07 -08:00
James R. Barlow e0070e3e18 Update Dockerfile to use Ubuntu 17.10 (issue #214) 2018-01-28 15:30:41 -08:00
James R. Barlow 019513696b Ghostscript spoof scripts did not report their --version correctly 2018-01-10 17:08:14 -08:00
James R. Barlow ad7a4476db hugemono.pdf needs --max-image-mpixels to pass with Pillow 5.0 2018-01-10 16:55:18 -08:00
James R. Barlow 4812b20fb2 Fix tesseract_noop.py generating wrong size of output PDF in tests
This caused trouble before with test_deskew
2018-01-10 16:35:31 -08:00
James R. Barlow f5e07c9427 Fix Ghostscript parameter order 2018-01-10 16:33:26 -08:00
James R. Barlow 75dcb90621 Niceties: when environment variable overload is used clarify we're not checking the PATH 2018-01-10 16:33:03 -08:00
James R. Barlow dfc0434cc2 Update requirements to set Pillow to 5.0 2018-01-10 15:45:23 -08:00
James R. Barlow 882fc2257c Add --max-image-mpixels argument to support Pillow 5.0 2018-01-10 15:43:59 -08:00
James R. Barlow 41e83b52fc Document process for redoing OCR 2018-01-10 15:39:58 -08:00
James R. Barlow 47758b4d8f Reactivate autobrew 2018-01-10 15:39:36 -08:00
James R. Barlow 6bf1f970a0 Fix some parameter validation for --output-type pdfa-1 and pdfa-2 2018-01-10 11:50:08 -08:00
James R. Barlow 7d451f101f Detect old versions of Ghostscript and warn about them (#208) 2018-01-10 11:47:39 -08:00
James R. Barlow 7edbfe0e40 Update autobrew template 2018-01-09 12:36:14 -08:00
James R. Barlow 91b42cbfa8 Fix issue in sandwich renderer when skipping OCR on a rotated and deskewed page
If OCR is skipped due to --tesseract-timeout or similar, and the skip page is rotated with /Rotate, and the skip page was deskewed or had other image processing, then the skip page was created with the wrong dimensions causing the output page to be cropped.
2018-01-09 00:17:53 -08:00
James R. Barlow 6907df41b4 Disable autobrew until homebrew accepts the official release 2018-01-08 23:26:58 -08:00
James R. Barlow 2cebd90cbd Fix brew audit --strict warnings 2017-12-09 12:14:28 -08:00
James R. Barlow 376a121aaa Re-enable macos 2017-11-29 15:06:19 -08:00
James R. Barlow da11fd17ee qpdf dummy: needs to return version now 2017-11-29 14:35:37 -08:00
James R. Barlow a40689a0ff tesseract: handle return of bytes properly in error cases 2017-11-29 14:35:26 -08:00
James R. Barlow 44a45fc3fb Add "bad UTF8 output from Tesseract" test 2017-11-29 14:08:07 -08:00
James R. Barlow ec4bb5359a Read tesseract's output as binary to avoid UnicodeDecodeErrors if it messes up 2017-11-29 13:44:40 -08:00
James R. Barlow d2217632df Rename _verify_python3_env 2017-11-29 13:43:18 -08:00
James R. Barlow 2cc044feed Move qpdf complaint to after options checking so that it won't break ocrmypdf --version 2017-11-29 13:42:55 -08:00
James R. Barlow 64fd0cb54f Remove test_qpdf.py only from travis 2017-11-29 12:54:48 -08:00
James R. Barlow c5a1d22e81 That fixed it. Complain about old versions of qpdf now 2017-11-29 12:53:34 -08:00
James R. Barlow a7b307af04 Looks like issue was negzero.pdf with qpdf 5.1.1 on travis, which is why osx passes
Reorganize and see if this is better now
2017-11-29 12:47:09 -08:00
James R. Barlow 3269eba16c Is it negzero.pdf? 2017-11-29 12:02:37 -08:00
James R. Barlow d472860e3b Try to diagnose travis-only failure of qpdf test 2017-11-29 11:41:06 -08:00
James R. Barlow 731c9ea55e Set timeouts on the tests that seem to be stalling on travis (but not elsewhere) 2017-11-27 14:46:10 -08:00
James R. Barlow 0b6af8d965 Clarify hocrtransform license/copyright 2017-11-27 13:41:46 -08:00
James R. Barlow 92ca9e954c Fix test warning/failures, hopefully 2017-11-27 13:41:32 -08:00
James R. Barlow 67aa00e2c5 Remove py3.5 deprecation
It'll be in Debian stable for a long time, so maybe not such a good idea
2017-11-27 01:10:34 -08:00
James R. Barlow a1bf161eb7 Update v5.4.4 release notes 2017-11-27 01:09:35 -08:00
James R. Barlow 56614fcaa4 Add support and tests for handling page count > ulimit - fixes issue #181 2017-11-27 00:32:35 -08:00
James R. Barlow 4d9169e15f Add merge ulimit test case 2017-11-26 23:34:36 -08:00
James R. Barlow 3a167af2c4 Nearly smallest possible PDF-1.3 with all required fields 2017-11-26 23:32:21 -08:00
James R. Barlow 64b9005030 v5.4.4 release notes; mention deprecation of 3.5 2017-11-26 23:08:55 -08:00
James R. Barlow 965de3a235 Test case for issue #200 2017-11-26 22:52:53 -08:00
James R. Barlow 2040ae4856 Fix issue #200, uncommon but valid decimal syntax treated as error
Also replace check_output() calls with run() in qpdf.py
2017-11-26 22:52:43 -08:00
James R. Barlow 31a1f9f5ea pytest-runner should be a setup requirement 2017-11-26 22:51:51 -08:00
James R. Barlow 7eadd82048 Update docker script based on uid issue
issue #180, fix reported by @Atredis76
2017-11-26 15:11:17 -08:00
James R. Barlow 31d0eaac8e Ensure intermediate metadata holder PDF has same version as its input file
While not known to cause problems, the absence of this could confuse parsers
2017-11-24 00:09:13 -08:00
James R. Barlow f45d012635 Update docs: security, pip install --user 2017-11-21 16:50:14 -08:00
James R. Barlow 5a777ee9bc v5.4.3 release notes 2017-11-17 02:28:02 -08:00
James R. Barlow 7bbf6bc7f4 Travis didn't like LANG, use LC_ALL 2017-11-16 20:37:30 -08:00
James R. Barlow 9bfc45702d Update copyright in license 2017-11-16 17:18:29 -08:00
James R. Barlow 40aa82ab41 Check that the locale is sane before allowing OCR to proceed 2017-11-16 17:18:02 -08:00
James R. Barlow 5d2c67c62b Declare in yet another place that this shouldn't run on py2 2017-11-16 14:54:28 -08:00
James R. Barlow a00ef4836d autobrew: declare conflict with mactex
As reported in https://github.com/jbarlow83/homebrew-ocrmypdf/issues/4#issuecomment-345029899
2017-11-16 14:53:42 -08:00
James R. Barlow 3ef766bb93 Remove bare 'except:' 2017-11-01 01:52:13 -07:00
James R. Barlow 44b5a18462 Declare our __version__ properly 2017-11-01 01:49:51 -07:00
James R. Barlow 4df716f0e8 Update requirements.txt 2017-11-01 01:49:35 -07:00
James R. Barlow fcbf34a4d3 Refactor obtaining version from subprocesses
Issue #196 raised the need to deal with linker warnings on --version.
2017-11-01 01:44:36 -07:00
James R. Barlow c7b8b6e18b Fix issue #194 - --sidecar creates blank txt file 2017-10-26 18:15:31 -07:00
James R. Barlow 9fd165bf4b Add 'zlib' to travis recipe 2017-10-26 18:06:12 -07:00
James R. Barlow 2061092ec5 Describe how to stream with Docker
[ci skip]
2017-10-18 12:37:27 -07:00
James R. Barlow 57466a31ba v5.4.1 release notes 2017-10-12 14:04:45 -07:00
James R. Barlow 4213c69bef Try to make travis actually deploy macOS 2017-10-12 12:45:40 -07:00
James R. Barlow cc5578488a Remove workaround for OSD crash and use explicit -l osd
Suggested by amitdo in https://github.com/tesseract-ocr/tesseract/issues/1167
2017-10-12 12:45:18 -07:00
James R. Barlow 4b7135f0e5 Add option to produce PDF/A-1B 2017-10-11 14:32:58 -07:00
James R. Barlow 984df023b1 Fixup manifest 2017-10-11 13:12:14 -07:00
James R. Barlow 6f8682f558 Track .coveragerc for future reference 2017-10-11 13:05:19 -07:00
James R. Barlow 51defa6d66 Import cleanup and some pylint fixes 2017-10-11 12:58:35 -07:00
James R. Barlow 7d73098d6e ghostscript.py: fix missing imports 2017-10-11 12:39:16 -07:00
James R. Barlow c580aa4683 Revert to pre-build stages travis.yml
It's still in beta for a reason apparently
2017-10-11 11:32:47 -07:00
James R. Barlow 47d1199360 Fix travis.yml typo 2017-10-11 11:26:08 -07:00
James R. Barlow 61ab435b31 Try disabling the matrix and osx for build stages 2017-10-11 11:18:32 -07:00
James R. Barlow aa8f534b45 pipeline: fix variable not defined in __init__ 2017-10-11 11:17:33 -07:00
James R. Barlow dc9589aca8 travis: Explicitly set python versions 2017-10-11 00:37:13 -07:00
James R. Barlow dd6eaacc6b More travis build tweaks 2017-10-11 00:28:33 -07:00
James R. Barlow 37dc03eec6 Add workaround for tess4 form feed behavior change 2017-10-10 23:33:54 -07:00
James R. Barlow 6b478172f6 Try using plain tessdata instead of tessdata_best 2017-10-10 20:52:03 -07:00
James R. Barlow c6e73bcfd6 Still failing, did the cp work at all? 2017-10-10 15:15:26 -07:00
James R. Barlow a2d62938ce cp overwrite needs sudo 2017-10-10 14:54:52 -07:00
James R. Barlow d7ae1f3cca travis: Replacement problematic traineddate file 2017-10-10 14:39:05 -07:00
James R. Barlow 70219581c4 Disable tesseract 4 so tests can succeed
tess4 -psm 0 is broken right now
2017-10-10 14:18:02 -07:00
James R. Barlow f70ac9fb89 Workaround travis issues in build stages... maybe 2017-10-10 12:36:50 -07:00
James R. Barlow 235b9fbaf0 Resolve merge conflicts 2017-10-10 12:24:10 -07:00
James R. Barlow ebda7f42db travis: need script for each stage 2017-10-10 12:22:23 -07:00
James R. Barlow 0b04e4b977 Try out travis build matrix 2017-10-10 12:14:50 -07:00
James R. Barlow 9498601a37 Add docs on adding to docker iamge 2017-10-10 12:13:20 -07:00
James R. Barlow ef5d320e06 Ignore .vscode too 2017-10-09 16:19:41 -07:00
James R. Barlow b00c9a562d Remove meaningless version from Dockerfile.polyglot 2017-10-09 16:18:02 -07:00
James R. Barlow 5372656893 Don't say tess4 support is experimental - it's pretty good now 2017-10-09 16:17:42 -07:00
James R. Barlow 571de0e368 Update release notes 2017-10-08 12:41:03 -07:00
James R. Barlow 82cea2fd85 Update batch processing docs to include Synology script 2017-10-08 12:34:36 -07:00
James R. Barlow aed9814345 Use Ubuntu 17.04 instead of 16.10 for Docker image (issue #191)
Due to 16.10 PPAs no longer being generated by alex-p
2017-10-08 12:13:20 -07:00
James R. Barlow 34fc1f5fd7 Add reminder that blank.pdf is not trivial 2017-09-13 01:19:18 -07:00
James R. Barlow 87c2ed8b27 Improve clarity of --pdf-renderer=tesseract deprecation warning 2017-09-12 14:34:53 -07:00
James R. Barlow 1467d118ab Add more leptonica functions 2017-09-06 00:27:02 -07:00
James R. Barlow 922dbe83c3 Update MANIFEST rules 2017-09-02 20:05:57 -07:00
James R. Barlow 6af7d61ee5 Fix CI failure due to spoofers not being updated to Tesseract 3.05 strings 2017-09-01 16:17:26 -07:00
James R. Barlow bafd08391d Update release notes 2017-09-01 12:50:45 -07:00
James R. Barlow 82ebd8ef1a Fix missing error message about trying to use sandwich on old tesseract 2017-09-01 12:50:36 -07:00
James R. Barlow 4ed1aa4d23 Release notes: fix indentation 2017-09-01 12:47:22 -07:00
James R. Barlow d04e43d46d Update copyright info for test files
[ci skip]
2017-09-01 01:00:32 -07:00
James R. Barlow 952f0cca15 Dockerfiles: set LANG=C.UTF-8
Issue #184 to avoid issue with printing UTF-8 text to sidecar
2017-08-30 13:25:54 -07:00
James R. Barlow f6a4d8f1f8 Fix Ubuntu 14.04 install instructions to account for dropping Py3.4 support
[ci skip]
2017-08-27 13:53:36 -07:00
James R. Barlow b3097a2384 Fix broken test case related to language packs 2017-08-24 13:01:02 -07:00
James R. Barlow 6d9ddbe98b v5.3.1 notes 2017-08-24 01:09:19 -07:00
James R. Barlow 9bb42c0229 Wrong error type used for missing language 2017-08-24 01:07:23 -07:00
James R. Barlow bd7226b27a Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2017-08-23 23:30:19 -07:00
James R. Barlow 5b413e3873 Cookbook: add "don't OCR" examples 2017-08-23 23:29:41 -07:00
James R. Barlow be5831a629 Offer the readme as a long description for new PyPI 2017-08-23 23:29:21 -07:00
jbarlow83andGitHub 084d2bf8e2 More badges 2017-08-23 23:19:29 -07:00
James R. Barlow da79e6bac7 macos: Skip brew audit because it seems to crash ruby on travis 2017-07-27 16:00:41 -07:00
261 changed files with 9424 additions and 2637 deletions
+16
View File
@@ -0,0 +1,16 @@
# Coverage isn't really compatible with subprocesses so results are unreliable
[run]
branch = True
#concurrency = multiprocessing
source = ocrmypdf/
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if 0:
if False:
if __name__ == .__main__.:
+5 -5
View File
@@ -1,8 +1,6 @@
# OCRmyPDF
#
# VERSION 4.4.2
FROM ubuntu:16.10
MAINTAINER James R. Barlow <jim@purplerock.ca>
FROM ubuntu:17.10
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common python-software-properties \
@@ -20,6 +18,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-spa \
tesseract-ocr-deu
ENV LANG=C.UTF-8
RUN python3 -m venv --system-site-packages /appenv
# This installs the latest binary wheel instead of the code in the current
@@ -28,7 +28,7 @@ RUN python3 -m venv --system-site-packages /appenv
# (i.e. "pip install ."). It's unclear to me why this is the case.
RUN . /appenv/bin/activate; \
pip install --upgrade pip \
&& pip install ocrmypdf
&& pip install ocrmypdf[fitz]
# Now copy the application in, mainly to get the test suite.
# Do this now to make the best use of Docker cache.
@@ -51,4 +51,4 @@ WORKDIR /home/docker
# Must use array form of ENTRYPOINT
# Non-array form does not append other arguments, because that is "intuitive"
ENTRYPOINT ["/application/docker-wrapper.sh"]
ENTRYPOINT ["/application/.docker/docker-wrapper.sh"]
@@ -1,8 +1,6 @@
# OCRmyPDF polyglot
#
# VERSION 4.4.2
FROM jbarlow83/ocrmypdf:latest
MAINTAINER James R. Barlow <jim@purplerock.ca>
USER root
@@ -16,4 +14,4 @@ USER docker
# Must use array form of ENTRYPOINT
# Non-array form does not append other arguments, because that is "intuitive"
ENTRYPOINT ["/application/docker-wrapper.sh"]
ENTRYPOINT ["/application/.docker/docker-wrapper.sh"]
@@ -1,8 +1,6 @@
# OCRmyPDF
#
# VERSION 4.4.2
FROM ubuntu:16.10
MAINTAINER James R. Barlow <jim@purplerock.ca>
FROM ubuntu:17.10
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common python-software-properties \
@@ -30,6 +28,8 @@ RUN apt-get update \
tesseract-ocr-rus \
tesseract-ocr-chi-sim
ENV LANG=C.UTF-8
RUN python3 -m venv --system-site-packages /appenv
# This installs the latest binary wheel instead of the code in the current
@@ -38,7 +38,7 @@ RUN python3 -m venv --system-site-packages /appenv
# (i.e. "pip install ."). It's unclear to me why this is the case.
RUN . /appenv/bin/activate; \
pip install --upgrade pip \
&& pip install ocrmypdf
&& pip install ocrmypdf[fitz]
# Now copy the application in, mainly to get the test suite.
# Do this now to make the best use of Docker cache.
@@ -61,4 +61,4 @@ WORKDIR /home/docker
# Must use array form of ENTRYPOINT
# Non-array form does not append other arguments, because that is "intuitive"
ENTRYPOINT ["/application/docker-wrapper.sh"]
ENTRYPOINT ["/application/.docker/docker-wrapper.sh"]
+3 -12
View File
@@ -3,18 +3,9 @@
*.pyc
*.rst
*.sublime*
*/*.pyc
*/*/*.pyc
*/*/*/*.pyc
*/*/*/*/*.pyc
*/*/*/*/*/*.pyc
*/*/*/*/*/*/*.pyc
*/*/*/*/*/*/*/*.pyc
.cache/
.eggs/
.github/
.git/
.ipynb_checkpoints/
**/*.pyc
.*/
!.docker/
.ruffus_history.sqlite
bin/
build/
+1 -1
View File
@@ -8,6 +8,6 @@
*.PDF binary
*.png binary
*.jpg binary
*.bin binary
.git_archival.txt export-subst
+5 -2
View File
@@ -7,6 +7,7 @@ tasks.py
.bash_history
.ruffus_history.sqlite
.idea/
.pytest_cache/
# Package building
*.egg-info/
@@ -34,8 +35,10 @@ log/
/*.qdf
*.ipynb
.ipynb_checkpoints/
tests/cache/
tests/output/
tests/resources/private/
tmp/
pdfbox-app*.jar
pdfbox-app*.jar
.vscode/
IDEAS
_Dockerfile.local
+24 -16
View File
@@ -1,16 +1,13 @@
dist: trusty
language: python
cache:
ccache: true
pip: true
directories:
- packages
- tests/cache
- $HOME/Library/Caches/Homebrew
env:
global:
- secure: "oyX5xesoHD7qcDXKIxMyGZPi+H/WxcvfFkaviEmq84K1DDyHk48+9e92IKgrw8/lcTADnEo/AgVKfnhCPflFimk1xTkgaK4sUg1WLI2YjmaHcwl5SlBHa2rN3uGBwy1hyP92qyv/mMc9R59NtRJ8u76lbn6eN9wi7lkFWdE6BTw=" # DOCKERHUB_OCRMYPDF_TOKEN
- secure: "WlyII8YLsiUUyLtEA563GvEZmbneDb/T8q/P1uNbyQ2ps1U82tH0nSUV2CspSMxOFtZzPHCrRvnAmuTYKshBj+GNnBb1J9FKQmFwF+4NPeqsFdUkQ1NeeCmfIRShuNC3Otg2GGwj4Zssdg+QnVy43t2L11qizzfY+lY+MVzAYcM=" # DOCKERHUB_OCRMYPDF_TESS4_TOKEN
- secure: "hsf6MT+n2x3OiDM2fQyJZdV0/PWYmv81LdVqC6cfnHBE/8N3DloJRqQ7WfO14TxhiK9PEC7MpyCj0lSabUHEO7gSH6Vks6I1asoSkt8S9/bSMlhT4hei+pwVpeGEiU5xHVATNjY+D919VC3IFvc3XmjT74h/2SLhaZ+jhEmDggM=" # HOMEBREW_OCRMYPDF_TOKEN
matrix:
@@ -19,13 +16,25 @@ matrix:
sudo: required
language: python
python: 3.5
env: EXTRAS=
- os: linux
sudo: required
language: python
python: 3.6
env: EXTRAS=
- os: linux
sudo: required
language: python
python: 3.6
env: EXTRAS=[fitz]
- os: osx
osx_image: xcode8
language: generic
env: EXTRAS=
- os: osx
osx_image: xcode8
language: generic
env: EXTRAS=[fitz]
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
@@ -34,26 +43,27 @@ before_install: |
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
bash .travis/linux_before_install.sh
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
bash .travis/osx_before_install.sh
brew update && brew bundle --file=.travis/Brewfile
pip3 install --upgrade pip
pip3 install wheel
fi
install:
- pip3 install .
- pip3 install -r requirements.txt -r test_requirements.txt
- pip3 install ".$EXTRAS"
- pip3 install -r test_requirements.txt
script:
- mv ocrmypdf dont_import_this_ocrmypdf
- tesseract --version
- qpdf --version
- pytest -n auto
- mv dont_import_this_ocrmypdf ocrmypdf
deploy:
# release for main pypi
# 3.6 is considered the build leader and does the deploy, otherwise there is
# 3.6 w/o fitz is considered the build leader and does the deploy, otherwise there is
# a race and all versions will try to deploy
# OTOH if we ever need separate binary wheels then each version needs its
# own deploy
- provider: pypi
server: https://upload.pypi.org/legacy/
user: ocrmypdf-travis
password:
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
@@ -61,7 +71,7 @@ deploy:
on:
branch: master
tags: true
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux"
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux" && $EXTRAS == ""
skip_upload_docs: true
# test pypi
@@ -84,12 +94,10 @@ deploy:
script: /usr/bin/true
on:
branch: master
tags: true
condition: $TRAVIS_OS_NAME == "osx"
after_deploy: |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
bash .travis/osx_brew.sh
elif [[ "$TRAVIS_PYTHON_VERSION" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf/trigger/$DOCKERHUB_OCRMYPDF_TOKEN/
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf-tess4/trigger/$DOCKERHUB_OCRMYPDF_TESS4_TOKEN/
fi
fi
+13
View File
@@ -0,0 +1,13 @@
brew 'python'
brew 'ghostscript'
brew 'jbig2dec'
brew 'leptonica'
brew 'libffi'
brew 'libtiff'
brew 'libxml2'
brew 'mupdf-tools'
brew 'openjpeg'
brew 'qpdf'
brew 'tesseract'
brew 'unpaper'
+36 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
# © 2017-18 James R. Barlow: github.com/jbarlow83
from string import Template
from subprocess import run, PIPE
@@ -14,38 +14,51 @@ class Ocrmypdf < Formula
${ocrmypdf_url}
${ocrmypdf_sha256}
depends_on :python3
depends_on :x11 # Pillow needs XQuartz
depends_on "pkg-config" => :build
depends_on "libffi"
depends_on "tesseract"
depends_on "ghostscript"
depends_on "unpaper"
depends_on "qpdf"
# For Pillow source install
depends_on "openjpeg"
depends_on "mupdf-tools" => :build # statically links libmupdf.a
depends_on "freetype"
depends_on "ghostscript"
depends_on "jpeg"
depends_on "libpng"
depends_on "libjpeg"
depends_on "webp"
depends_on "little-cms2"
depends_on "python"
depends_on "qpdf"
depends_on "tesseract"
depends_on "unpaper"
${resources}
def install
ENV.append ["SETUPTOOLS_SCM_PRETEND_VERSION"], "v${ocrmypdf_version}"
ENV.each do |key, value|
puts "#{key}:#{value}"
venv = virtualenv_create(libexec, "python3")
resource("Pillow").stage do
inreplace "setup.py" do |s|
sdkprefix = MacOS::CLT.installed? ? "" : MacOS.sdk_path
s.gsub! "openjpeg.h", "probably_not_a_header_called_this_eh.h"
s.gsub! "ZLIB_ROOT = None", "ZLIB_ROOT = ('#{sdkprefix}/usr/lib', '#{sdkprefix}/usr/include')"
s.gsub! "JPEG_ROOT = None", "JPEG_ROOT = ('#{Formula["jpeg"].opt_prefix}/lib', '#{Formula["jpeg"].opt_prefix}/include')"
s.gsub! "FREETYPE_ROOT = None", "FREETYPE_ROOT = ('#{Formula["freetype"].opt_prefix}/lib', '#{Formula["freetype"].opt_prefix}/include')"
end
# avoid triggering "helpful" distutils code that doesn't recognize Xcode 7 .tbd stubs
ENV.append "CFLAGS", "-I#{MacOS.sdk_path}/System/Library/Frameworks/Tk.framework/Versions/8.5/Headers" unless MacOS::CLT.installed?
venv.pip_install Pathname.pwd
end
virtualenv_install_with_resources
res = resources.map(&:name).to_set - ["Pillow"]
res.each do |r|
venv.pip_install resource(r)
end
venv.pip_install_and_link buildpath
end
test do
# `test do` will create, run in and delete a temporary directory.
#
# The installed folder is not in the path, so use the entire path to any
# executables being tested: `system "#{bin}/program", "do", "something"`.
system "#{bin}/ocrmypdf", "--version"
# Since we use Python 3, we require a UTF-8 locale
ENV["LC_ALL"] = "en_US.UTF-8"
system "#{bin}/ocrmypdf", "-f", "-q", "--deskew",
test_fixtures("test.pdf"), "ocr.pdf"
assert_predicate testpath/"ocr.pdf", :exist?
end
end
""")
+13 -2
View File
@@ -8,7 +8,6 @@ sudo add-apt-repository ppa:heyarje/libav-11 -y
sudo apt-get update -qq
sudo apt-get install -y \
ghostscript \
qpdf \
poppler-utils \
libavformat56 \
libavcodec56 \
@@ -27,5 +26,17 @@ sudo apt-get install -y --no-install-recommends \
pip install --upgrade pip
mkdir -p packages
[ -f packages/unpaper_6.1-1.deb ] || wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O packages/unpaper_6.1-1.deb
sudo dpkg -i packages/unpaper_6.1-1.deb
if [ ! -f /usr/local/bin/qpdf ]; then
export QPDF_RELEASE='https://github.com/qpdf/qpdf/releases/download/release-qpdf-8.0.2/qpdf-8.0.2.tar.gz'
mkdir qpdf
wget -q $QPDF_RELEASE -O - | tar xz -C qpdf --strip-components=1
cd qpdf/
export PATH="/usr/local/opt/ccache/libexec:$PATH"
./configure --prefix=/usr
make -j 2
sudo make install
cd ..
fi
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# © 2017 James R. Barlow: github.com/jbarlow83
set -euo pipefail
set -x
brew update
brew install openjpeg jbig2dec libtiff # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2 libffi leptonica
brew install unpaper # optional
brew install tesseract
pip3 install --upgrade pip
pip3 install wheel
+4 -2
View File
@@ -1,12 +1,14 @@
#!/bin/bash
# © 2017 James R. Barlow: github.com/jbarlow83
set -euo pipefail
set -uo pipefail
set -x
pip3 install homebrew-pypi-poet
python3 .travis/autobrew.py
cat ocrmypdf.rb
brew audit ocrmypdf.rb
# brew audit crashes Travis
#brew audit ocrmypdf.rb
# Important: disable debug output so token is hidden
set +x
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
-20
View File
@@ -1,20 +0,0 @@
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
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+10 -9
View File
@@ -7,26 +7,25 @@ include dev_requirements.txt
include .git_archival.txt
# docker
include Dockerfile
include Dockerfile.polyglot
include .dockerignore
recursive-include .docker *
# tests
include pytest.ini
include .coveragerc
recursive-include tests *.jpg
recursive-include tests *.png
recursive-include tests *.pdf
recursive-include tests *.py
recursive-include tests *.rst
recursive-include tests *.txt
recursive-include tests/cache *
recursive-exclude tests/output *
recursive-exclude tests/output_pageinfo *
recursive-exclude tests/resources/private *
recursive-exclude tests/cache *
# documentation
include LICENSE
include *.rst
include pipeline.svg
recursive-exclude .github *
recursive-include docs *.py
recursive-include docs *.rst
@@ -35,14 +34,16 @@ recursive-exclude docs/_build *
# support files
recursive-include ocrmypdf/data *
recursive-include src/ocrmypdf/data *
include *.py
exclude tasks.py
recursive-exclude .travis *
exclude .travis*
# code
recursive-include ocrmypdf *.py
recursive-include src/ocrmypdf *.py
exclude ocrmypdf/lib/_leptonica.py
include OCRmyPDF.sh
include docker-wrapper.sh
+25 -2
View File
@@ -4,6 +4,14 @@ OCRmyPDF
.. image:: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master
:target: https://travis-ci.org/jbarlow83/OCRmyPDF
.. image:: https://img.shields.io/pypi/v/ocrmypdf.svg
:target: https://pypi.org/project/ocrmypdf/
.. image:: https://img.shields.io/homebrew/v/ocrmypdf.svg
:alt: homebrew
:target: http://brewformulas.org/Ocrmypdf
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched or copy-pasted.
@@ -68,11 +76,10 @@ Users of Debian 9 or later or Ubuntu 16.10 or later may simply
apt-get install ocrmypdf
and macOS users may simply
and macOS users with Homebrew may simply
.. code-block:: bash
brew tap jbarlow83/ocrmypdf
brew install ocrmypdf
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
@@ -116,6 +123,13 @@ If you detect an issue, please:
- If possible provide your input PDF file as well as the content of the
temporary folder (using a file sharing service like Dropbox)
Requirements
------------
Runs on CPython 3.6, and requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
Python 3.5 is also supported.
Press & Media
-------------
@@ -125,6 +139,15 @@ Press & Media
- `heise Open Source, 09/2014: Texterkennung mit
OCRmyPDF <http://heise.de/-2356670>`_
License
-------
The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered by other licenses, as noted in their source files.
The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0).
OCRmyPDF versions prior to 6.0 were licensed under the MIT License.
Disclaimer
----------
-6
View File
@@ -1,10 +1,4 @@
check-manifest >= 0.35
twine >= 1.8.1
coverage >= 4.4
pytest-xdist >= 1.16.0
GitPython == 2.1.3
# Known good versions: 1.11.1
# Known broken versions: 1.15.0
setuptools-scm == 1.11.1
setuptools-scm-git-archive == 1.0
+21 -25
View File
@@ -19,7 +19,7 @@ If ``--force-ocr`` is issued, then all pages will be rasterized to images, disca
Time and image size limits
""""""""""""""""""""""""""
By default, OCRmyPDF permits tesseract to run for only three minutes (180 seconds) per page. This is usually more than enough time to find all text on a reasonably sized page with modern hardware.
By default, OCRmyPDF permits tesseract to run for three minutes (180 seconds) per page. This is usually more than enough time to find all text on a reasonably sized page with modern hardware.
If a page is skipped, it will be inserted without OCR. If preprocessing was requested, the preprocessed image layer will be inserted.
@@ -33,26 +33,28 @@ If you want to adjust the amount of time spent on OCR, change ``--tesseract-time
Overriding default tesseract
""""""""""""""""""""""""""""
OCRmyPDF checks the environment variable ``OCRMYPDF_TESSERACT`` for the full path *to the tesseract binary* first.
OCRmyPDF checks the system ``PATH`` for the ``tesseract`` binary.
.. envvar:: OCRMYPDF_TESSERACT
Specify the location of the Tesseract binary.
Some relevant environment variables that influence Tesseract's behavior include:
.. envvar:: TESSDATA_PREFIX
A Tesseract environment variable that overrides the path to Tesseract's data files.
Overrides the path to Tesseract's data files. This can allow simultaneous installation of the "best" and "fast" training data sets. OCRmyPDF does not manage this environment variable.
For example, if you are testing tesseract 4.00 and don't wish to disturb use an existing tesseract 3.04 installation, you can launch OCRmyPDF as follows:
.. envvar:: OMP_THREAD_LIMIT
Controls the number of threads Tesseract will use. OCRmyPDF will manage this environment if it is not already set. (Currently, it will set it to 1 because this gives the best results in testing.)
For example, if you are testing tesseract 4.00 and don't wish to use an existing tesseract 3.04 installation, you can launch OCRmyPDF as follows:
.. code-block:: bash
env \
OCRMYPDF_TESSERACT=/home/user/src/tesseract4/api/tesseract \
PATH=/home/user/src/tesseract4/api:$PATH \
TESSDATA_PREFIX=/home/user/src/tesseract4 \
ocrmypdf --tesseract-oem 2 input.pdf output.pdf
In this example ``TESSDATA_PREFIX`` directs tesseract 4.0 to use LSTM training data. ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
In this example ``TESSDATA_PREFIX`` directs Tesseract 4.0 to use LSTM training data. ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
Overriding other support programs
@@ -64,19 +66,7 @@ In addition to tesseract, OCRmyPDF uses the following external binaries:
* ``unpaper``
* ``qpdf``
In each case OCRmyPDF will check the environment variable ``OCRMYPDF_{program}`` before asking the system to find ``{program}`` on the PATH. For example, you could redirect OCRmyPDF to ``OCRMYPDF_GS`` to override Ghostscript. The full list is below:
.. envvar:: OCRMYPDF_GS
Specify the location of the Ghostscript binary.
.. envvar:: OCRMYPDF_UNPAPER
Specify the location of the unpaper binary.
.. envvar:: OCRMYPDF_QPDF
Specify the location of the qpdf binary.
In each case OCRmyPDF will search the ``PATH`` environment variable to locate the binaries.
Changing tesseract configuration variables
@@ -115,13 +105,15 @@ rendering
Creating a new PDF from other data (such as an existing PDF).
OCRmyPDF has three PDF renderers: ``sandwich``, ``hocr``, ``tesseract``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` selects ``sandwich`` for Tesseract 3.05.01, and newer, ``hocr`` for older versions of Tesseract.
OCRmyPDF has three PDF renderers: ``sandwich``, ``hocr``, ``tesseract``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` selects ``sandwich`` for Tesseract 3.05.01 or newer, or ``hocr`` for older versions of Tesseract.
The ``sandwich`` renderer
"""""""""""""""""""""""""
The ``sandwich`` renderer uses Tesseract's new text-only PDF feature, which produces a PDF page that lays out the OCR in invisible text. This page is then "sandwiched" onto the original PDF page, allowing lossless application of OCR even to PDF pages that contain other vector objects.
Currently this is the best renderer for most uses, however it is implemented in Tesseract so OCRmyPDF cannot influence it. Currently some problematic PDF viewers like Mozilla PDF.js and macOS Preview have problems with segmenting its text output, and mightrunseveralwordstogether.
When image preprocessing features like ``--deskew`` are used, the original PDF will be rendered as a full page and the OCR layer will be placed on top.
This renderer requires Tesseract 3.05.01 or newer.
@@ -131,13 +123,17 @@ The ``hocr`` renderer
The ``hocr`` renderer works with older versions of Tesseract. The image layer is copied from the original PDF page if possible, avoiding potentially lossy transcoding or loss of other PDF information. If preprocessing is specified, then the image layer is a new PDF.
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone looking to customize how OCR is presented should look here. A major disadvantage of this renderer is it not capable of correctly handling text outside the Latin alphabet. Pull requests to improve the situation are welcome.
Currently, this renderer has the best compatibility with Mozilla's PDF.js viewer.
This works in all versions of Tesseract.
The ``tesseract`` renderer
""""""""""""""""""""""""""
The ``tesseract`` renderer creates a PDF with the image and text layers precomposed, meaning that it always transcodes, loses image quality and rasterizes and vector objects. It does a better job on non-Latin text and document structure than ``hocr``.
The ``tesseract`` renderer creates a PDF with the image and text layers precomposed, meaning that it always transcodes, loses image quality and rasterizes any vector objects. It does a better job on non-Latin text and document structure than ``hocr``.
If a PDF created with this renderer using Tesseract versions older than 3.05.00 is then passed through Ghostscript's pdfwrite feature, the OCR text *may* be corrupted. The ``--output-type=pdfa`` argument will produce a warning in this situation.
*This renderer is deprecated and will be removed whenever support for older versions of Tesseract is dropped.*
*This renderer is deprecated and will be removed whenever support for older versions of Tesseract is dropped.*
+74 -2
View File
@@ -18,6 +18,8 @@ The ``--tag`` argument tells parallel to print the filename as a prefix whenever
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
OCRmyPDF automatically repairs PDFs before parsing and gathering information from them. If you are already repairing PDFs with ``qpdf`` prior to attempting OCR, or you can use ``--skip-repair`` to skip this step. It may improve performance for large files, since repairing PDFs is single-threaded.
Directory trees
---------------
@@ -90,13 +92,83 @@ This user contributed script also provides an example of batch processing.
API
"""
OCRmyPDF is currently supported as a command line interface. Due to limitations in one of the libraries OCRmyPDF depends on, it is not yet usable as an API.
OCRmyPDF is currently supported as a command line interface. This means that even if you are using OCRmyPDF in a Python script, you should run it in a subprocess rather importing the ocrmypdf package.
The reason for this limitation is that the `ruffus <https://github.com/bunbun/ruffus/>`_ library that OCRmyPDF depends on is unfortunately not reentrant. OCRmyPDF works by defining each operation it does as a ruffus task that takes one or more files as input and generates one or more files as output. As such ruffus is fairly fundamental.
(If you find individual functions implemented in OCRmyPDF useful (such as ``ocrmypdf.pdfinfo``), you can use these if you wish to.)
Synology DiskStations
"""""""""""""""""""""
Synology DiskStations (Network Attached Storage devices) can run the Docker image of OCRmyPDF if the Synology `Docker package <https://www.synology.com/en-global/dsm/packages/Docker>`_ is installed. Attached is a script to address particular quirks of using OCRmyPDF on one of these devices.
This is only possible for x86-based Synology products. Some Synology products use ARM or Power processors and do not support Docker. Further adjustments might be needed to deal with the Synology's relatively limited CPU and RAM.
.. code-block:: python
#!/bin/env python3
# Contributed by github.com/Enantiomerie
# script needs 2 arguments
# 1. source dir with *.pdf - default is location of script
# 2. move dir where *.pdf and *_OCR.pdf are moved to
import logging
import os
import subprocess
import sys
import time
import shutil
script_dir = os.path.dirname(os.path.realpath(__file__))
timestamp = time.strftime("%Y-%m-%d-%H%M_")
log_file = script_dir + '/' + timestamp + 'ocrmypdf.log'
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', filename=log_file, filemode='w')
if len(sys.argv) > 1:
start_dir = sys.argv[1]
else:
start_dir = '.'
for dir_name, subdirs, file_list in os.walk(start_dir):
logging.info('\n')
logging.info(dir_name + '\n')
os.chdir(dir_name)
for filename in file_list:
file_ext = os.path.splitext(filename)[1]
if file_ext == '.pdf':
full_path = dir_name + '/' + filename
file_noext = os.path.splitext(filename)[0]
timestamp_OCR = time.strftime("%Y-%m-%d-%H%M_OCR_")
filename_OCR = timestamp_OCR + file_noext + '.pdf'
docker_mount = dir_name + ':/home/docker'
# create string for pdf processing
# diskstation needs a user:group docker:docker. find uid:gid of your diskstation docker:docker with id docker.
# use this uid:gid in -u flag
# rw rights for docker:docker at source dir are also necessary
# the script is processed as root user via chron
cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u=1030:65538', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR]
logging.info(cmd)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout.read()
logging.info(result)
full_path_OCR = dir_name + '/' + filename_OCR
os.chmod(full_path_OCR, 0o666)
os.chmod(full_path, 0o666)
full_path_OCR_archive = sys.argv[2]
full_path_archive = sys.argv[2] + '/no_ocr'
shutil.move(full_path_OCR,full_path_OCR_archive)
shutil.move(full_path, full_path_archive)
logging.info('Finished.\n')
Huge batch jobs
"""""""""""""""
If you have thousands of files to work with, contact the author.
If you have thousands of files to work with, contact the author. Consulting work related to OCRmyPDF helps fund this open source project and all inquiries are appreciated.
Hot (watched) folders
+2 -2
View File
@@ -52,7 +52,7 @@ master_doc = 'index'
# General information about the project.
project = 'ocrmypdf'
copyright = '2017, James R. Barlow'
copyright = '2018, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
author = 'James R. Barlow'
# The version info for the project you're documenting, acts as replacement for
@@ -60,7 +60,7 @@ author = 'James R. Barlow'
# built documents.
#
# The short X.Y version.
from ocrmypdf import VERSION as OCRMYPDF_VERSION
from ocrmypdf import __version__ as OCRMYPDF_VERSION
_version_parts = OCRMYPDF_VERSION.split('.')
+68 -25
View File
@@ -11,7 +11,7 @@ ocrmypdf has built-in help.
.. code-block:: bash
ocrmypdf --help
ocrmypdf --help
Add an OCR layer and convert to PDF/A
@@ -19,21 +19,21 @@ Add an OCR layer and convert to PDF/A
.. code-block:: bash
ocrmypdf input.pdf output.pdf
ocrmypdf input.pdf output.pdf
Add an OCR layer and output a standard PDF
""""""""""""""""""""""""""""""""""""""""""
.. code-block:: bash
ocrmypdf --output-type pdf input.pdf output.pdf
ocrmypdf --output-type pdf input.pdf output.pdf
Create a PDF/A with all color and grayscale images converted to JPEG
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
.. code-block:: bash
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
Modify a file in place
""""""""""""""""""""""
@@ -42,7 +42,7 @@ The file will only be overwritten if OCRmyPDF is successful.
.. code-block:: bash
ocrmypdf myfile.pdf myfile.pdf
ocrmypdf myfile.pdf myfile.pdf
Correct page rotation
"""""""""""""""""""""
@@ -51,10 +51,12 @@ OCR will attempt to automatic correct the rotation of each page. This can help f
.. code-block:: bash
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive.
If the page is "just a little off horizontal", like a crooked picture, then you want ``--deskew``. ``--rotate-pages`` is for when the cardinal angle is wrong.
OCR languages other than English
""""""""""""""""""""""""""""""""
@@ -63,8 +65,8 @@ By default OCRmyPDF assumes the document is English.
.. code-block:: bash
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
ocrmypdf -l fre LeParisien.pdf LeParisien.pdf
ocrmypdf -l eng+fre Bilingual-English-French.pdf Bilingual-English-French.pdf
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
@@ -76,24 +78,37 @@ This produces a file named "output.pdf" and a companion text file named "output.
.. code-block:: bash
ocrmypdf --sidecar output.txt input.pdf output.pdf
ocrmypdf --sidecar output.txt input.pdf output.pdf
OCR images, not PDFs
--------------------
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the results to run ocrmypdf:
If you are starting with images, you can just use Tesseract 3.04 or later directly to convert images to PDFs:
.. code-block:: bash
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
``img2pdf`` also has features to control the position of images on a page, if desired.
For convenience, OCRmyPDF can convert single images to PDFs on its own. If the resolution (dots per inch, DPI) of an image is not set or is incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54 cm, 1 dpi = 0.39 dpcm).
tesseract my-image.jpg output-prefix pdf
.. code-block:: bash
ocrmypdf --image-dpi 300 image.png myfile.pdf
# When there are multiple images
tesseract text-file-containing-list-of-image-filenames.txt output-prefix pdf
Tesseract's PDF output is quite good  OCRmyPDF uses it by internally by default. However, OCRmyPDF has many features not available in Tesseract like like image processing, metadata control, and PDF/A generation.
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the results to run ocrmypdf. The `-` tells ocrmypdf to read standard input.
.. code-block:: bash
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
``img2pdf`` is recommended because it does an excellent job at generating PDFs without transcoding images.
For convenience, OCRmyPDF can also convert single images to PDFs on its own. If the resolution (dots per inch, DPI) of an image is not set or is incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54 cm, 1 dpi = 0.39 dpcm).
.. code-block:: bash
ocrmypdf --image-dpi 300 image.png myfile.pdf
If you have multiple images, you must use ``img2pdf`` to convert the images to PDF.
@@ -101,11 +116,6 @@ If you have multiple images, you must use ``img2pdf`` to convert the images to P
ImageMagick ``convert`` can also convert a group of images to PDF, but in the author's experience it takes a long time, transcodes unnecessarily and gives poor results.
You can also use Tesseract 3.04+ directly to convert single page images or multi-page TIFFs to PDF:
.. code-block:: bash
tesseract my-image.jpg output-prefix pdf
Image processing
----------------
@@ -124,11 +134,11 @@ OCRmyPDF perform some image processing on each page of a PDF, if desired. The s
.. note::
In many cases image processing will rasterize PDF pages as images, potentially losing quality.
In many cases image processing will rasterize PDF pages as images, potentially losing quality.
.. warning::
``--clean-final`` and ``-remove-background`` may leave undesirable visual artifacts in some images where their algorithms have shortcomings. Files should be visually reviewed after using these options.
``--clean-final`` and ``-remove-background`` may leave undesirable visual artifacts in some images where their algorithms have shortcomings. Files should be visually reviewed after using these options.
OCR and correct document skew (crooked scan)
@@ -138,13 +148,46 @@ Deskew:
.. code-block:: bash
ocrmypdf --deskew input.pdf output.pdf
ocrmypdf --deskew input.pdf output.pdf
Image processing commands can be combined. The order in which options are given does not matter. OCRmyPDF always applies the steps of the image processing pipeline in the same order (rotate, remove background, deskew, clean).
.. code-block:: bash
ocrmypdf --deskew --clean --rotate-pages input.pdf output.pdf
ocrmypdf --deskew --clean --rotate-pages input.pdf output.pdf
Don't actually OCR my PDF
"""""""""""""""""""""""""
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image processing without performing OCR, if all you want to is to apply image processing or PDF/A conversion.
.. code-block:: bash
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
Redo OCR
""""""""
To redo OCR on a file OCRed with other OCR software or a previous version of OCRmyPDF and/or Tesseract, you may use the ``--force-ocr`` argument. Normally, OCRmyPDF does not modify files that already appear to contain OCR text.
.. code-block:: bash
ocrmypdf --force-ocr input.pdf output.pdf
Note that the method above will force rasterization of all pages, potentially reducing quality or losing vector content.
To ensure quality is preserved, one could extract all of the images and rebuild the PDF for a lossless transformation. This recipe does not work when PDFs contain multiple images per page, as many do in practice. It will also lose any page rotation information.
.. code-block:: bash
pdfimages -all old-ocr.pdf prefix # extract all images
img2pdf -o temp.pdf prefix* # construct new PDF from the images
# review the new PDF to ensure it visually matches the old one
ocrmypdf --output-type pdf temp.pdf new-ocr.pdf
``--output-type pdf`` is used here to avoid using Ghostscript which will also rasterize images.
Improving OCR quality
+1 -1
View File
@@ -9,7 +9,7 @@ OCRmyPDF documentation
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched.
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
PDF is the best format for storing and exchanging scanned documents. Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
.. toctree::
:maxdepth: 1
+153 -77
View File
@@ -1,7 +1,13 @@
Installation
============
OCRmyPDF requires Python 3.5 (or newer) and Tesseract 3.04 (or newer).
The easiest way to install OCRmyPDF to follow the steps for your operating system/platform.
If you want to use the latest version of OCRmyPDF, your best bet is to install the most recent version your platform provides, and then upgrade that version by installing the Python binary wheels.
.. contents:: Platform-specific steps
:depth: 1
:local:
Installing on Debian and Ubuntu 16.10 or newer
----------------------------------------------
@@ -12,21 +18,29 @@ Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
apt-get install ocrmypdf
Installing on macOS
-------------------
To see what versions are available, check the `Debian Package Tracker <https://tracker.debian.org/pkg/ocrmypdf>`_ or `Ubuntu launchpad.net <https://launchpad.net/ocrmypdf>`_.
A `Homebrew <https://brew.sh>`_ tap is available for macOS:
Installing on macOS with Homebrew
---------------------------------
.. image:: https://img.shields.io/homebrew/v/ocrmypdf.svg
:alt: homebrew
:target: http://brewformulas.org/Ocrmypdf
OCRmyPDF is now a standard `Homebrew <https://brew.sh>`_ formula. To install on macOS:
.. code-block:: bash
brew tap jbarlow83/ocrmypdf
brew install ocrmypdf
.. warning::
.. note::
Users who previously installed OCRmyPDF on macOS using ``pip install ocrmypdf`` should remove the pip version (``pip3 uninstall ocrmypdf``) before switching to the Homebrew version.
.. note::
Users who previously installed OCRmyPDF from the private tap should switch to the mainline version (``brew untap jbarlow83/ocrmypdf``) and install from there.
.. _Docker-install:
Installing the Docker image
@@ -34,11 +48,9 @@ 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.
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:
Follow the Docker installation instructions for your platform. If you can run this command successfully, your system is ready to download and execute the image:
.. code-block:: bash
@@ -56,16 +68,22 @@ OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine i
Assuming you have a Docker engine running, you can download one of the three available images:
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
| Image name | Download command | Notes |
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
| ocrmypdf | ``docker pull jbarlow83/ocrmypdf`` | Latest ocrmypdf with Tesseract 3.04. Includes English, French, German, Spanish. |
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
| ocrmypdf-polyglot | ``docker pull jbarlow83/ocrmypdf-polyglot`` | As above, with all available language packs. |
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
| ocrmypdf-tess4 | ``docker pull jbarlow83/ocrmypdf-tess4`` | Latest ocrmypdf with Tesseract 4.00.00alpha and English, French, German, |
| | | Spanish, Portuguese, Chinese Simplified, Arabic and Russian (the top 8). |
+-----------------------------+---------------------------------------------+---------------------------------------------------------------------------------+
.. list-table::
:widths: auto
:header-rows: 1
* - Image name
- Download command
- Notes
* - ocrmypdf
- ``docker pull jbarlow83/ocrmypdf``
- Latest ocrmypdf with Tesseract 3.x. Includes English, French, German, Spanish.
* - ocrmypdf-polyglot
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- As above, with all available language packs.
* - ocrmypdf-tess4
- ``docker pull jbarlow83/ocrmypdf-tess4``
- Latest ocrmypdf with Tesseract 4.x and English, French, German, Spanish, Portuguese, Chinese Simplified, Arabic and Russian (the top 8).
For example:
@@ -105,6 +123,12 @@ In this worked example, the current working directory contains an input file cal
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.
In some environments the permissions associated with Docker can be complex to configure. The process that executes Docker may end up not having the permissions to write the specified file system. In that case one can stream the file into and out of the Docker process and avoid all permission hassles, using ``-`` as the input and output filename:
.. code-block:: bash
docker run --rm -i ocrmypdf <other arguments to ocrmypdf> - - <input.pdf >output.pdf
For convenience, a shell alias can hide the docker command:
.. code-block:: bash
@@ -119,6 +143,9 @@ Or in the wonderful `fish shell <https://fishshell.com/>`_:
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
funcsave ocrmypdf
.. note::
The ocrmypdf Docker containers are designed to be used for a single OCR job. The ``docker run --rm`` argument tells Docker to delete temporary storage associated with container when it is done executing.
Manual installation on macOS
----------------------------
@@ -158,18 +185,25 @@ Install the required Tesseract OCR engine with the language packs you plan to us
brew install tesseract --with-all-languages # Option 2: for all language packs
Update the homebrew pip and install Pillow:
Update the homebrew pip:
.. code-block:: bash
pip3 install --upgrade pip
pip3 install --upgrade pillow
You can then install OCRmyPDF from PyPI:
You can then install OCRmyPDF from PyPI, for the current user:
.. code-block:: bash
pip3 install ocrmypdf
pip3 install --user ocrmypdf[fitz]
or system-wide:
.. code-block:: bash
pip3 install ocrmypdf[fitz]
``[fitz]`` includes the optional dependency on PyMuPDF, which improves OCRmyPDF's output in many cases.
The command line program should now be available:
@@ -194,35 +228,33 @@ No package is currently available for Ubuntu 16.04, but you can install the depe
python3-pip \
python3-cffi
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
into your system Python, which could interfere with other programs):
If you wish install OCRmyPDF for the current user:
.. code-block:: bash
sudo pip3 install ocrmypdf
pip3 install --user ocrmypdf[fitz]
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps.
Alternately, system-wide. Note that this may modify the system Python environment:
.. code-block:: bash
sudo pip3 install ocrmypdf[fitz]
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can follow these steps.
.. code-block:: bash
python3 -m venv venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip3 install ocrmypdf
pip3 install ocrmypdf[fitz]
``[fitz]`` includes the optional dependency on PyMuPDF, which improves OCRmyPDF's output in many cases.
Installing on Ubuntu 14.04 LTS
------------------------------
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation and because OCRmyPDF depends on some packages newer than are available in the main distribution.
Add new "apt" repositories needed for backports of Ghostscript 9.16, libav-11 (for unpaper 6.1) and Tesseract 4.00 (alpha). This will replace Ghostscript and Tesseract 3.x on your system. If you prefer to not modify your system in this matter, consider using a Docker container.
.. code-block:: bash
sudo add-apt-repository ppa:vshn/ghostscript -y
sudo add-apt-repository ppa:heyarje/libav-11 -y
sudo add-apt-repository ppa:alex-p/tesseract-ocr
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because it is older and does not provide ``pip``.
Update apt-get:
@@ -239,39 +271,37 @@ Install system dependencies:
zlib1g-dev \
libjpeg-dev \
libffi-dev \
libavformat56 libavcodec56 libavutil54 \
qpdf
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1), Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and Tesseract 3.x on your system. Python 3.6 will be installed alongside the system Python 3.
If you prefer to not modify your system in this matter, consider using a Docker container.
.. code-block:: bash
sudo add-apt-repository ppa:vshn/ghostscript -y
sudo add-apt-repository ppa:heyarje/libav-11 -y
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
sudo add-apt-repository ppa:jonathonf/python-3.6 -y
sudo apt-get update
sudo apt-get install \
python3.6-dev \
ghostscript \
qpdf \
python3-pip \
python3-pil \
python3-pytest \
python3-reportlab \
python3-wheel \
python3-venv \
tesseract-ocr \
tesseract-ocr-eng
tesseract-ocr-eng \
libavformat56 libavcodec56 libavutil54 \
wget
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):
Now we need to install ``pip`` and let it install ocrmypdf:
.. code-block:: bash
sudo pip3 install ocrmypdf
curl https://bootstrap.pypa.io/ez_setup.py -o - | python3.6 && python3.6 -m easy_install pip
pip3.6 install ocrmypdf[fitz]
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
.. code-block:: bash
sudo apt-get install python3-venv
python3 -m venv venv-ocrmypdf --without-pip
source venv-ocrmypdf/bin/activate
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python
deactivate
python3 -m venv --system-site-packages venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip install ocrmypdf
The ``wget`` command will download a program and run it.
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
@@ -296,6 +326,9 @@ Installing on Windows
Direct installation on Windows is not possible. Install the _`Docker` container as described above. Ensure that your command prompt can run the docker "hello world" container.
It would probably not be too difficult to run on Windows. The main reason this has been avoided is the difficulty of packaging and installing the various non-Python dependencies: Tesseract, QPDF, Ghostscript, Leptonica. Pull requests to add or improve Windows support would be quite welcome.
Running on Windows
~~~~~~~~~~~~~~~~~~
@@ -309,11 +342,56 @@ where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users
`Bash on Ubuntu on Windows <https://github.com/Microsoft/BashOnWindows>`_ should also be a viable route for running the OCRmyPDF Docker container.
Installing with Python pip
--------------------------
First, install `your platform's version <https://repology.org/metapackage/ocrmypdf/versions>`_ of ``ocrmypdf``, if available, as a way of ensuring that external dependencies are (mostly) satisified, even though the platform version may be out of date. Use ``ocrmypdf --version`` to confirm what version was installed.
Then you can install the latest OCRmyPDF from the Python wheels. First try:
.. code-block:: bash
pip3 install --user ocrmypdf
You should then be able to run ``ocrmypdf --version`` and see that the latest version was located.
Since ``pip3 install --user`` does not work correctly on some platforms, notably Ubuntu 16.04 and older, and the Homebrew version of Python, instead use this for a system wide installation:
.. code-block:: bash
pip3 install ocrmypdf
Requirements for pip and HEAD install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OCRmyPDF currently requires these external programs to be installed:
- Python 3.5 or newer
- Tesseract 3.04 or newer
- Ghostscript 9.15 or newer
- qpdf 7.0.0 or newer
The following dependencies are recommended:
- Python 3.6
- Tesseract 4.00 or newer
- Ghostscript 9.22 or newer
- qpdf 8.0.2 or newer
- unpaper 6.1
- PyMuPDF 1.12.5 or newer
These are in addition to the Python packaging dependencies, meaning that unfortunately, the ``pip install`` command cannot satisfy all of them.
Python 3.6 and Tesseract 4.x are recommended for best OCR results and best performance.
The library PyMuPDF is not widely available in platform distributions, and it improves OCRmyPDF in certain conditions. Consider installing OCRmyPDF from the Python binary wheels, which include a precompiled version of this library.
Installing HEAD revision from sources
-------------------------------------
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs,
it will alert you if dependencies are missing.
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing.
To install the HEAD revision from sources in the current Python 3 environment:
@@ -327,15 +405,7 @@ Or, to install in `development mode <https://pythonhosted.org/setuptools/setupto
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
On certain Linux distributions such as Ubuntu, you may need to use
run the install command as superuser:
.. code-block:: bash
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
Note that this will alter your system's Python distribution. If you prefer
to not install as superuser, you can install the package in a Python virtual environment:
You may find it easiest to install in a virtual environment, rather than system-wide:
.. code-block:: bash
@@ -358,3 +428,9 @@ If not yet installed, the script will notify you about dependencies that
need to be installed. The script requires specific versions of the
dependencies. Older version than the ones mentioned in the release notes
are likely not to be compatible to OCRmyPDF.
Other Linux packages
--------------------
See the `Repology <https://repology.org/metapackage/ocrmypdf/versions>`_ page.
+15 -9
View File
@@ -16,7 +16,7 @@ OCRmyPDF uses `Tesseract <https://github.com/tesseract-ocr/tesseract>`_, the bes
About PDFs
----------
PDFs are page description files that attempts to preserve a layout exactly. They can contain `vector graphic files <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
PDFs are page description files that attempts to preserve a layout exactly. They contain `vector graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
.. image:: bitmap_vs_svg.svg
@@ -42,15 +42,13 @@ PDF/A has a few drawbacks. Some PDF viewers include an alert that the file is a
What OCRmyPDF does
------------------
OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript <http://ghostscript.com/>`_ to rasterize the page, and then performs on OCR on the rasterized image. It is not enough to simply extract the images from each page and run OCR on them individually. Of course one could use Ghostscript or another PDF rasterizer and then pass the image to Tesseract. OCRmyPDF automates this process and produces a minimally changed output file that contains the same information, colorspace and resolution.
OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript <http://ghostscript.com/>`_ to rasterize the page, and then performs on OCR on the rasterized image to create an OCR "layer". The layer is then grafted back onto the original PDF.
The Tesseract OCR engine can output 'hOCR' files, which are XML files that contain a description of the text it found on the page. OCRmyPDF will render a new PDF that contains only the hidden text layer, and merge this with the original page.
While one can use a program like Ghostscript or ImageMagick to get an image and put the image through Tesseract, that actually creates a new PDF and many details may be lost. OCRmyPDF can produce a minimally changed PDF as output.
Alternately, OCRmyPDF can use the Tesseract OCR engine to directly output PDFs for each page, then merge them.
OCRmyPDF also some image processing options like deskew which improve the appearance of files and quality of OCR. When these are used, the OCR layer is grafted onto the processed image instead.
By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be disabled with the ``--output-type pdf`` argument.
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
By default, OCRmyPDF produces archival PDFs PDF/A, which are a stricter subset of PDF features designed for long term archives. If regular PDFs are desired, this can be disabled with ``--output-type pdf``.
Why you shouldn't do this manually
@@ -81,10 +79,12 @@ OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these l
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
* PDFs that use transparent layers are not currently checked in the test suite, so they may not work correctly.
* It does not expose information about what font family text belongs to.
OCRmyPDF is also limited by the PDF specification:
* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others.
* Because some popular open source PDF viewers have a particularly hard time with spaces betweem words, OCRmyPDF appends a space to each text element as a workaround. While this mixes document structure with graphical information that ideally should be left to the PDF viewer to interpret, it improves compatibility with some viewers and does not cause problems for better ones.
Ghostscript also imposes some limitations:
@@ -105,5 +105,11 @@ To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly test
* pypdfocr
* pdfbeads
If you are looking for a micro web-frontend for OCRmyPDF, consider the third-party `OCRmyPDF-web <https://github.com/sseemayer/OCRmyPDF-web>`_. Bear in mind that OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online`_).
Web front-ends
--------------
* `Nextcloud OCR <https://github.com/janis91/ocr>`_ is a free software plugin for the Nextcloud private cloud software
* `OCRmyPDF-web <https://github.com/sseemayer/OCRmyPDF-web>`_, a micro web-frontend for OCRmyPDF (third-party, not actively maintained)
Bear in mind that OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online`_).
+28
View File
@@ -33,6 +33,34 @@ Docker users
Users of the Docker image may use the alternative :ref:`"polyglot" container <docker-polyglot>` which includes all languages.
Adding individual language packs to a Docker image
""""""""""""""""""""""""""""""""""""""""""""""""""
If you wish to add a single language pack, you could do the following:
* Download the desired ``.trainedata`` file from the `tessdata <https://github.com/tesseract-ocr/tessdata>`_ repository. Let's use Hebrew in this example (``heb.traineddata``)
* Copy the file to ``/home/user/downloads/heb.traineddata``.
* Create a new container based on the ocrmypdf-tess4 image and jump into it with a terminal:
.. code-block:: bash
host$ docker run -v /home/user/downloads:/home/docker -it --entrypoint /bin/bash ocrmypdf-tess4
* Put the file where Tesseract expects it:
.. code-block:: bash
docker$ cp /home/docker/heb.traineddata /usr/share/tesseract-ocr/tessdata
* Note the container id, and save it as a new image (in this example, ``ocrmypdf-tess4-heb``)
.. code-block:: bash
host$ docker commit <container_id> ocrmypdf-tess4-heb
Known limitations
-----------------
+518 -357
View File
@@ -1,392 +1,553 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: Pipeline: Pages: 1 -->
<svg width="1544pt" height="1089pt"
viewBox="0.00 0.00 1544.00 1089.14" 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 1085.14)">
<svg width="1952pt" height="1277pt"
viewBox="0.00 0.00 1952.00 1277.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 1273.1825)">
<title>Pipeline:</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1085.14 1540,-1085.14 1540,4 -4,4"/>
<g id="clust1" class="cluster"><title>clustertasks</title>
<polygon fill="none" stroke="black" points="8,-8 8,-1073.14 1528,-1073.14 1528,-8 8,-8"/>
<text text-anchor="middle" x="768" y="-1045.14" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1273.1825 1948,-1273.1825 1948,4 -4,4"/>
<g id="clust1" class="cluster">
<title>clustertasks</title>
<polygon fill="none" stroke="#000000" points="8,-8 8,-1261.1825 1936,-1261.1825 1936,-8 8,-8"/>
<text text-anchor="middle" x="972" y="-1233.1825" 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="750.705,-1027.14 541.295,-1027.14 537.295,-1023.14 537.295,-991.141 746.705,-991.141 750.705,-995.141 750.705,-1027.14"/>
<polyline fill="none" stroke="black" points="746.705,-1023.14 537.295,-1023.14 "/>
<polyline fill="none" stroke="black" points="746.705,-1023.14 746.705,-991.141 "/>
<polyline fill="none" stroke="black" points="746.705,-1023.14 750.705,-1027.14 "/>
<text text-anchor="middle" x="644" y="-1003.14" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.triage</text>
<g id="node1" class="node">
<title>t0</title>
<polygon fill="#efa03b" stroke="#000000" points="876.7053,-1215.1825 667.2947,-1215.1825 663.2947,-1211.1825 663.2947,-1179.1825 872.7053,-1179.1825 876.7053,-1183.1825 876.7053,-1215.1825"/>
<polyline fill="none" stroke="#000000" points="872.7053,-1211.1825 663.2947,-1211.1825 "/>
<polyline fill="none" stroke="#000000" points="872.7053,-1211.1825 872.7053,-1179.1825 "/>
<polyline fill="none" stroke="#000000" points="872.7053,-1211.1825 876.7053,-1215.1825 "/>
<text text-anchor="middle" x="770" y="-1191.1825" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.triage</text>
</g>
<!-- t1 -->
<g id="node2" class="node"><title>t1</title>
<polygon fill="#efa03b" stroke="black" points="769.969,-969.141 522.031,-969.141 518.031,-965.141 518.031,-933.141 765.969,-933.141 769.969,-937.141 769.969,-969.141"/>
<polyline fill="none" stroke="black" points="765.969,-965.141 518.031,-965.141 "/>
<polyline fill="none" stroke="black" points="765.969,-965.141 765.969,-933.141 "/>
<polyline fill="none" stroke="black" points="765.969,-965.141 769.969,-969.141 "/>
<text text-anchor="middle" x="644" y="-945.141" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.repair_pdf</text>
<g id="node2" class="node">
<title>t1</title>
<polygon fill="#efa03b" stroke="#000000" points="941.0433,-1157.1825 602.9567,-1157.1825 598.9567,-1153.1825 598.9567,-1121.1825 937.0433,-1121.1825 941.0433,-1125.1825 941.0433,-1157.1825"/>
<polyline fill="none" stroke="#000000" points="937.0433,-1153.1825 598.9567,-1153.1825 "/>
<polyline fill="none" stroke="#000000" points="937.0433,-1153.1825 937.0433,-1121.1825 "/>
<polyline fill="none" stroke="#000000" points="937.0433,-1153.1825 941.0433,-1157.1825 "/>
<text text-anchor="middle" x="770" y="-1133.1825" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.repair_and_parse_pdf</text>
</g>
<!-- t0&#45;&gt;t1 -->
<g id="edge1" class="edge"><title>t0&#45;&gt;t1</title>
<path fill="none" stroke="#0044a0" d="M644,-991.035C644,-987.344 644,-983.354 644,-979.384"/>
<polygon fill="#0044a0" stroke="#0044a0" points="647.5,-979.142 644,-969.142 640.5,-979.142 647.5,-979.142"/>
<g id="edge1" class="edge">
<title>t0&#45;&gt;t1</title>
<path fill="none" stroke="#0044a0" d="M770,-1179.1488C770,-1175.4743 770,-1171.5242 770,-1167.5888"/>
<polygon fill="#0044a0" stroke="#0044a0" points="773.5001,-1167.4047 770,-1157.4047 766.5001,-1167.4047 773.5001,-1167.4047"/>
</g>
<!-- t2 -->
<g id="node3" class="node"><title>t2</title>
<polygon fill="#efa03b" stroke="black" points="869.82,-890.215 644,-907.116 418.18,-890.215 418.391,-862.869 869.609,-862.869 869.82,-890.215"/>
<polygon fill="none" stroke="black" points="873.852,-893.922 644,-911.125 414.148,-893.922 414.419,-858.871 873.581,-858.871 873.852,-893.922"/>
<text text-anchor="middle" x="644" y="-876.657" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.split_pages</text>
<g id="node3" class="node">
<title>t2</title>
<polygon fill="#efa03b" stroke="#000000" points="1043.9447,-1078.2585 787,-1095.158 530.0553,-1078.2585 530.2951,-1050.9147 1043.7049,-1050.9147 1043.9447,-1078.2585"/>
<polygon fill="none" stroke="#000000" points="1047.9814,-1081.9991 787,-1099.164 526.0186,-1081.9991 526.3263,-1046.9173 1047.6737,-1046.9173 1047.9814,-1081.9991"/>
<text text-anchor="middle" x="787" y="-1064.7009" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.pre_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="M644,-933.019C644,-929.4 644,-925.445 644,-921.399"/>
<polygon fill="#0044a0" stroke="#0044a0" points="647.5,-921.295 644,-911.295 640.5,-921.295 647.5,-921.295"/>
</g>
<!-- t18 -->
<g id="node20" class="node"><title>t18</title>
<polygon fill="#efa03b" stroke="black" points="383.516,-832.173 20.4844,-832.173 16.4844,-828.173 16.4844,-796.173 379.516,-796.173 383.516,-800.173 383.516,-832.173"/>
<polyline fill="none" stroke="black" points="379.516,-828.173 16.4844,-828.173 "/>
<polyline fill="none" stroke="black" points="379.516,-828.173 379.516,-796.173 "/>
<polyline fill="none" stroke="black" points="379.516,-828.173 383.516,-832.173 "/>
<text text-anchor="middle" x="200" y="-808.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.generate_postscript_stub</text>
</g>
<!-- t1&#45;&gt;t18 -->
<g id="edge29" class="edge"><title>t1&#45;&gt;t18</title>
<path fill="none" stroke="#0044a0" d="M518.14,-936.259C481.198,-930.408 441.05,-922.284 405,-911.141 344.394,-892.407 278.709,-859.14 238.448,-837.118"/>
<polygon fill="#0044a0" stroke="#0044a0" points="239.963,-833.956 229.517,-832.188 236.581,-840.085 239.963,-833.956"/>
</g>
<!-- t21 -->
<g id="node22" class="node"><title>t21</title>
<polygon fill="#efa03b" stroke="black" points="751.49,-156.424 963,-129.109 1174.51,-156.424 1174.31,-200.62 751.688,-200.62 751.49,-156.424"/>
<polygon fill="none" stroke="black" points="747.474,-152.906 963,-125.073 1178.53,-152.906 1178.29,-204.623 747.705,-204.623 747.474,-152.906"/>
<text text-anchor="middle" x="963" y="-162.639" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.merge_pages_qpdf</text>
</g>
<!-- t1&#45;&gt;t21 -->
<g id="edge39" class="edge"><title>t1&#45;&gt;t21</title>
<path fill="none" stroke="#0044a0" d="M769.832,-943.539C1011.96,-929.143 1519,-890.056 1519,-815.173 1519,-815.173 1519,-815.173 1519,-309.173 1519,-237.474 1342.87,-202.283 1188.87,-185.216"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1189.08,-181.719 1178.76,-184.118 1188.32,-188.678 1189.08,-181.719"/>
</g>
<!-- t3 -->
<g id="node4" class="node"><title>t3</title>
<polygon fill="#efa03b" stroke="black" points="712.317,-832.173 405.683,-832.173 401.683,-828.173 401.683,-796.173 708.317,-796.173 712.317,-800.173 712.317,-832.173"/>
<polyline fill="none" stroke="black" points="708.317,-828.173 401.683,-828.173 "/>
<polyline fill="none" stroke="black" points="708.317,-828.173 708.317,-796.173 "/>
<polyline fill="none" stroke="black" points="708.317,-828.173 712.317,-832.173 "/>
<text text-anchor="middle" x="557" y="-808.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.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="M614.06,-858.777C605.418,-852.173 595.983,-844.963 587.319,-838.342"/>
<polygon fill="#0044a0" stroke="#0044a0" points="589.37,-835.504 579.299,-832.213 585.119,-841.066 589.37,-835.504"/>
</g>
<!-- t4 -->
<g id="node5" class="node"><title>t4</title>
<polygon fill="#efa03b" stroke="black" points="688.082,-774.173 429.918,-774.173 425.918,-770.173 425.918,-738.173 684.082,-738.173 688.082,-742.173 688.082,-774.173"/>
<polyline fill="none" stroke="black" points="684.082,-770.173 425.918,-770.173 "/>
<polyline fill="none" stroke="black" points="684.082,-770.173 684.082,-738.173 "/>
<polyline fill="none" stroke="black" points="684.082,-770.173 688.082,-774.173 "/>
<text text-anchor="middle" x="557" y="-750.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.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="M695.691,-858.661C705.833,-851.667 715.136,-842.912 721,-832.173 728.668,-818.13 730.919,-808.727 721,-796.173 714.915,-788.471 700.759,-782.127 683.18,-776.959"/>
<polygon fill="#0044a0" stroke="#0044a0" points="683.659,-773.462 673.091,-774.208 681.817,-780.216 683.659,-773.462"/>
</g>
<!-- t3&#45;&gt;t4 -->
<g id="edge4" class="edge"><title>t3&#45;&gt;t4</title>
<path fill="none" stroke="#0044a0" d="M557,-796.066C557,-792.375 557,-788.385 557,-784.415"/>
<polygon fill="#0044a0" stroke="#0044a0" points="560.5,-784.174 557,-774.174 553.5,-784.174 560.5,-784.174"/>
</g>
<!-- t5 -->
<g id="node6" class="node"><title>t5</title>
<polygon fill="#efa03b" stroke="black" points="1064.73,-716.173 689.268,-716.173 685.268,-712.173 685.268,-680.173 1060.73,-680.173 1064.73,-684.173 1064.73,-716.173"/>
<polyline fill="none" stroke="black" points="1060.73,-712.173 685.268,-712.173 "/>
<polyline fill="none" stroke="black" points="1060.73,-712.173 1060.73,-680.173 "/>
<polyline fill="none" stroke="black" points="1060.73,-712.173 1064.73,-716.173 "/>
<text text-anchor="middle" x="875" y="-692.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.rasterize_with_ghostscript</text>
</g>
<!-- t4&#45;&gt;t5 -->
<g id="edge6" class="edge"><title>t4&#45;&gt;t5</title>
<path fill="none" stroke="#0044a0" d="M653.706,-738.143C689.783,-731.789 730.943,-724.541 767.902,-718.033"/>
<polygon fill="#0044a0" stroke="#0044a0" points="768.965,-721.399 778.206,-716.218 767.751,-714.505 768.965,-721.399"/>
</g>
<!-- t15 -->
<g id="node13" class="node"><title>t15</title>
<polygon fill="#ff69b4" stroke="black" points="440.27,-396.173 59.7304,-396.173 55.7304,-392.173 55.7304,-360.173 436.27,-360.173 440.27,-364.173 440.27,-396.173"/>
<polyline fill="none" stroke="black" points="436.27,-392.173 55.7304,-392.173 "/>
<polyline fill="none" stroke="black" points="436.27,-392.173 436.27,-360.173 "/>
<polyline fill="none" stroke="black" points="436.27,-392.173 440.27,-396.173 "/>
<text text-anchor="middle" x="248" y="-372.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.ocr_tesseract_textonly_pdf</text>
</g>
<!-- t4&#45;&gt;t15 -->
<g id="edge14" class="edge"><title>t4&#45;&gt;t15</title>
<path fill="none" stroke="#0044a0" d="M425.857,-745.263C361.241,-732.425 297,-703.891 297,-641.173 297,-641.173 297,-641.173 297,-523.173 297,-480.18 277.182,-433.63 262.754,-405.436"/>
<polygon fill="#0044a0" stroke="#0044a0" points="265.677,-403.477 257.916,-396.265 259.486,-406.743 265.677,-403.477"/>
</g>
<!-- t12 -->
<g id="node15" class="node"><title>t12</title>
<polygon fill="#00cc66" stroke="black" points="781,-406.173 458.414,-378.173 781,-350.173 1103.59,-378.173 781,-406.173"/>
<text text-anchor="middle" x="781" y="-372.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.select_image_layer</text>
</g>
<!-- t4&#45;&gt;t12 -->
<g id="edge20" class="edge"><title>t4&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M471.449,-738.167C421.018,-722.605 367,-693.681 367,-641.173 367,-641.173 367,-641.173 367,-523.173 367,-480.717 347.218,-458.43 377,-428.173 392.917,-412.001 494.592,-399.721 591.948,-391.499"/>
<polygon fill="#0044a0" stroke="#0044a0" points="592.284,-394.983 601.96,-390.666 591.704,-388.007 592.284,-394.983"/>
</g>
<!-- t19 -->
<g id="node18" class="node"><title>t19</title>
<polygon fill="#efa03b" stroke="black" points="323.772,-270.173 78.2284,-270.173 74.2284,-266.173 74.2284,-234.173 319.772,-234.173 323.772,-238.173 323.772,-270.173"/>
<polyline fill="none" stroke="black" points="319.772,-266.173 74.2284,-266.173 "/>
<polyline fill="none" stroke="black" points="319.772,-266.173 319.772,-234.173 "/>
<polyline fill="none" stroke="black" points="319.772,-266.173 323.772,-270.173 "/>
<text text-anchor="middle" x="199" y="-246.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.skip_page</text>
</g>
<!-- t4&#45;&gt;t19 -->
<g id="edge26" class="edge"><title>t4&#45;&gt;t19</title>
<path fill="none" stroke="#0044a0" d="M425.782,-752.078C271.736,-744.795 37,-720.655 37,-641.173 37,-641.173 37,-641.173 37,-377.173 37,-325.92 88.7624,-292.989 133.769,-274.043"/>
<polygon fill="#0044a0" stroke="#0044a0" points="135.289,-277.203 143.249,-270.211 132.666,-270.713 135.289,-277.203"/>
</g>
<!-- t17 -->
<g id="node19" class="node"><title>t17</title>
<polygon fill="#66ccff" stroke="black" points="1499.67,-328.173 1094.33,-328.173 1090.33,-324.173 1090.33,-292.173 1495.67,-292.173 1499.67,-296.173 1499.67,-328.173"/>
<polyline fill="none" stroke="black" points="1495.67,-324.173 1090.33,-324.173 "/>
<polyline fill="none" stroke="black" points="1495.67,-324.173 1495.67,-292.173 "/>
<polyline fill="none" stroke="black" points="1495.67,-324.173 1499.67,-328.173 "/>
<text text-anchor="middle" x="1295" y="-304.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.ocr_tesseract_and_render_pdf</text>
</g>
<!-- t4&#45;&gt;t17 -->
<g id="edge28" class="edge"><title>t4&#45;&gt;t17</title>
<path fill="none" stroke="#0044a0" d="M688.477,-752.611C947.521,-745.804 1499,-721.841 1499,-641.173 1499,-641.173 1499,-641.173 1499,-455.173 1499,-407.201 1508.25,-383.766 1474,-350.173 1465.98,-342.302 1448.74,-335.804 1427.99,-330.522"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1428.77,-327.11 1418.23,-328.18 1427.14,-333.917 1428.77,-327.11"/>
</g>
<!-- t6 -->
<g id="node7" class="node"><title>t6</title>
<polygon fill="#efa03b" stroke="black" points="1089.08,-658.173 664.92,-658.173 660.92,-654.173 660.92,-622.173 1085.08,-622.173 1089.08,-626.173 1089.08,-658.173"/>
<polyline fill="none" stroke="black" points="1085.08,-654.173 660.92,-654.173 "/>
<polyline fill="none" stroke="black" points="1085.08,-654.173 1085.08,-622.173 "/>
<polyline fill="none" stroke="black" points="1085.08,-654.173 1089.08,-658.173 "/>
<text text-anchor="middle" x="875" y="-634.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.preprocess_remove_background</text>
</g>
<!-- t5&#45;&gt;t6 -->
<g id="edge7" class="edge"><title>t5&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M875,-680.066C875,-676.375 875,-672.385 875,-668.415"/>
<polygon fill="#0044a0" stroke="#0044a0" points="878.5,-668.174 875,-658.174 871.5,-668.174 878.5,-668.174"/>
</g>
<!-- t11 -->
<g id="node14" class="node"><title>t11</title>
<polygon fill="#efa03b" stroke="black" points="1096,-484.173 712.187,-456.173 1096,-428.173 1479.81,-456.173 1096,-484.173"/>
<text text-anchor="middle" x="1096" y="-450.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.select_visible_page_image</text>
</g>
<!-- t5&#45;&gt;t11 -->
<g id="edge18" class="edge"><title>t5&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M1051.07,-680.0px8C1072.76,-674.712 1090.1,-667.576 1098,-658.173 1136.53,-612.317 1122.5,-537.602 1109.06,-493.335"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1112.33,-492.043 1105.96,-483.573 1105.65,-494.164 1112.33,-492.043"/>
</g>
<!-- t7 -->
<g id="node8" class="node"><title>t7</title>
<polygon fill="#efa03b" stroke="black" points="1039.68,-600.173 720.324,-600.173 716.324,-596.173 716.324,-564.173 1035.68,-564.173 1039.68,-568.173 1039.68,-600.173"/>
<polyline fill="none" stroke="black" points="1035.68,-596.173 716.324,-596.173 "/>
<polyline fill="none" stroke="black" points="1035.68,-596.173 1035.68,-564.173 "/>
<polyline fill="none" stroke="black" points="1035.68,-596.173 1039.68,-600.173 "/>
<text text-anchor="middle" x="878" y="-576.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.preprocess_deskew</text>
</g>
<!-- t6&#45;&gt;t7 -->
<g id="edge8" class="edge"><title>t6&#45;&gt;t7</title>
<path fill="none" stroke="#0044a0" d="M875.916,-622.066C876.114,-618.375 876.328,-614.385 876.541,-610.415"/>
<polygon fill="#0044a0" stroke="#0044a0" points="880.049,-610.347 877.089,-600.174 873.059,-609.972 880.049,-610.347"/>
</g>
<!-- t6&#45;&gt;t11 -->
<g id="edge17" class="edge"><title>t6&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M990.695,-622.152C1016.54,-616.369 1039.2,-609.066 1049,-600.173 1078.78,-573.156 1089.74,-526.964 1093.74,-494.403"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1097.24,-494.666 1094.82,-484.351 1090.28,-493.924 1097.24,-494.666"/>
</g>
<!-- t8 -->
<g id="node9" class="node"><title>t8</title>
<polygon fill="#efa03b" stroke="black" points="1030.88,-542.173 729.117,-542.173 725.117,-538.173 725.117,-506.173 1026.88,-506.173 1030.88,-510.173 1030.88,-542.173"/>
<polyline fill="none" stroke="black" points="1026.88,-538.173 725.117,-538.173 "/>
<polyline fill="none" stroke="black" points="1026.88,-538.173 1026.88,-506.173 "/>
<polyline fill="none" stroke="black" points="1026.88,-538.173 1030.88,-542.173 "/>
<text text-anchor="middle" x="878" y="-518.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.preprocess_clean</text>
</g>
<!-- t7&#45;&gt;t8 -->
<g id="edge9" class="edge"><title>t7&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M878,-564.066C878,-560.375 878,-556.385 878,-552.415"/>
<polygon fill="#0044a0" stroke="#0044a0" points="881.5,-552.174 878,-542.174 874.5,-552.174 881.5,-552.174"/>
</g>
<!-- t7&#45;&gt;t11 -->
<g id="edge16" class="edge"><title>t7&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M995.441,-564.147C1011.26,-558.713 1026.62,-551.566 1040,-542.173 1057.45,-529.919 1070.89,-510.334 1080.22,-493.051"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1083.55,-494.243 1084.99,-483.747 1077.31,-491.053 1083.55,-494.243"/>
</g>
<!-- t9 -->
<g id="node10" class="node"><title>t9</title>
<polygon fill="#efa03b" stroke="black" points="694.109,-474.173 389.891,-474.173 385.891,-470.173 385.891,-438.173 690.109,-438.173 694.109,-442.173 694.109,-474.173"/>
<polyline fill="none" stroke="black" points="690.109,-470.173 385.891,-470.173 "/>
<polyline fill="none" stroke="black" points="690.109,-470.173 690.109,-438.173 "/>
<polyline fill="none" stroke="black" points="690.109,-470.173 694.109,-474.173 "/>
<text text-anchor="middle" x="540" y="-450.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.select_ocr_image</text>
</g>
<!-- t8&#45;&gt;t9 -->
<g id="edge10" class="edge"><title>t8&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M790.517,-506.09C743.487,-496.907 685.383,-485.561 636.976,-476.109"/>
<polygon fill="#0044a0" stroke="#0044a0" points="637.602,-472.665 627.116,-474.183 636.26,-479.535 637.602,-472.665"/>
</g>
<!-- t8&#45;&gt;t11 -->
<g id="edge15" class="edge"><title>t8&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M934.424,-506.09C958.693,-498.742 987.536,-490.01 1014.09,-481.971"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1015.2,-485.293 1023.75,-479.045 1013.17,-478.593 1015.2,-485.293"/>
</g>
<!-- t10 -->
<g id="node11" class="node"><title>t10</title>
<polygon fill="#00cc66" stroke="black" points="1440.1,-396.173 1125.9,-396.173 1121.9,-392.173 1121.9,-360.173 1436.1,-360.173 1440.1,-364.173 1440.1,-396.173"/>
<polyline fill="none" stroke="black" points="1436.1,-392.173 1121.9,-392.173 "/>
<polyline fill="none" stroke="black" points="1436.1,-392.173 1436.1,-360.173 "/>
<polyline fill="none" stroke="black" points="1436.1,-392.173 1440.1,-396.173 "/>
<text text-anchor="middle" x="1281" y="-372.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.ocr_tesseract_hocr</text>
</g>
<!-- t9&#45;&gt;t10 -->
<g id="edge11" class="edge"><title>t9&#45;&gt;t10</title>
<path fill="none" stroke="#0044a0" d="M631.992,-438.111C655.104,-434.327 679.903,-430.697 703,-428.173 884.404,-408.343 931.549,-425.567 1113,-406.173 1133.45,-403.987 1155.2,-400.994 1176.03,-397.794"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1176.83,-401.211 1186.17,-396.209 1175.75,-394.295 1176.83,-401.211"/>
</g>
<!-- t9&#45;&gt;t15 -->
<g id="edge13" class="edge"><title>t9&#45;&gt;t15</title>
<path fill="none" stroke="#0044a0" d="M474.47,-438.117C429.651,-426.452 370.0px1,-410.957 323.342,-398.782"/>
<polygon fill="#0044a0" stroke="#0044a0" points="324.123,-395.369 313.564,-396.237 322.36,-402.143 324.123,-395.369"/>
</g>
<!-- t13 -->
<g id="node12" class="node"><title>t13</title>
<polygon fill="#00cc66" stroke="black" points="683.043,-328.173 374.957,-328.173 370.957,-324.173 370.957,-292.173 679.043,-292.173 683.043,-296.173 683.043,-328.173"/>
<polyline fill="none" stroke="black" points="679.043,-324.173 370.957,-324.173 "/>
<polyline fill="none" stroke="black" points="679.043,-324.173 679.043,-292.173 "/>
<polyline fill="none" stroke="black" points="679.043,-324.173 683.043,-328.173 "/>
<text text-anchor="middle" x="527" y="-304.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.render_hocr_page</text>
</g>
<!-- t10&#45;&gt;t13 -->
<g id="edge12" class="edge"><title>t10&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M1186.17,-360.113C1162.35,-356.33 1136.79,-352.699 1113,-350.173 929.956,-330.738 880.531,-342.246 693.485,-328.255"/>
<polygon fill="#0044a0" stroke="#0044a0" points="693.548,-324.75 683.311,-327.48 693.016,-331.73 693.548,-324.75"/>
</g>
<!-- t14 -->
<g id="node17" class="node"><title>t14</title>
<polygon fill="#00cc66" stroke="black" points="1072.92,-328.173 705.08,-328.173 701.08,-324.173 701.08,-292.173 1068.92,-292.173 1072.92,-296.173 1072.92,-328.173"/>
<polyline fill="none" stroke="black" points="1068.92,-324.173 701.08,-324.173 "/>
<polyline fill="none" stroke="black" points="1068.92,-324.173 1068.92,-292.173 "/>
<polyline fill="none" stroke="black" points="1068.92,-324.173 1072.92,-328.173 "/>
<text text-anchor="middle" x="887" y="-304.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.render_hocr_debug_page</text>
</g>
<!-- t10&#45;&gt;t14 -->
<g id="edge25" class="edge"><title>t10&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1179.28,-360.133C1124,-350.873 1055.54,-339.405 998.77,-329.896"/>
<polygon fill="#0044a0" stroke="#0044a0" points="999.118,-326.405 988.677,-328.205 997.962,-333.309 999.118,-326.405"/>
</g>
<!-- t16 -->
<g id="node16" class="node"><title>t16</title>
<polygon fill="#00cc66" stroke="black" points="636.299,-270.173 345.701,-270.173 341.701,-266.173 341.701,-234.173 632.299,-234.173 636.299,-238.173 636.299,-270.173"/>
<polyline fill="none" stroke="black" points="632.299,-266.173 341.701,-266.173 "/>
<polyline fill="none" stroke="black" points="632.299,-266.173 632.299,-234.173 "/>
<polyline fill="none" stroke="black" points="632.299,-266.173 636.299,-270.173 "/>
<text text-anchor="middle" x="489" y="-246.173" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.combine_layers</text>
</g>
<!-- t13&#45;&gt;t16 -->
<g id="edge22" class="edge"><title>t13&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M515.392,-292.066C512.519,-287.833 509.379,-283.205 506.299,-278.666"/>
<polygon fill="#0044a0" stroke="#0044a0" points="509.048,-276.483 500.537,-270.174 503.256,-280.414 509.048,-276.483"/>
</g>
<!-- t15&#45;&gt;t16 -->
<g id="edge21" class="edge"><title>t15&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M263.466,-359.936C281.645,-340.695 313.598,-309.935 347,-292.173 361.3,-284.568 377.219,-278.339 393.079,-273.27"/>
<polygon fill="#0044a0" stroke="#0044a0" points="394.389,-276.53 402.932,-270.262 392.345,-269.835 394.389,-276.53"/>
</g>
<!-- t11&#45;&gt;t12 -->
<g id="edge19" class="edge"><title>t11&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M1009.92,-434.405C967.591,-424.191 916.524,-411.87 873.373,-401.459"/>
<polygon fill="#0044a0" stroke="#0044a0" points="873.983,-398.006 863.441,-399.063 872.341,-404.811 873.983,-398.006"/>
</g>
<!-- t11&#45;&gt;t14 -->
<g id="edge24" class="edge"><title>t11&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1334.06,-445.464C1386.49,-438.209 1431.63,-426.12 1449,-406.173 1465.34,-387.403 1466.07,-368.289 1449,-350.173 1422.45,-321.987 1153.75,-330.0px 1083.05,-328.037"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1083,-324.531 1072.85,-327.564 1082.68,-331.524 1083,-324.531"/>
</g>
<!-- t11&#45;&gt;t17 -->
<g id="edge27" class="edge"><title>t11&#45;&gt;t17</title>
<path fill="none" stroke="#0044a0" d="M1278.64,-441.463C1364.44,-433.122 1450.86,-421.174 1464,-406.173 1480.4,-387.448 1479.49,-369.65 1464,-350.173 1457.83,-342.418 1443.67,-336.074 1426.02,-330.926"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1426.46,-327.418 1415.89,-328.188 1424.63,-334.176 1426.46,-327.418"/>
</g>
<!-- t12&#45;&gt;t16 -->
<g id="edge23" class="edge"><title>t12&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M624.426,-363.718C509.174,-353.04 370.992,-338.277 362,-328.173 351.364,-316.22 352.491,-305.04 362,-292.173 366.819,-285.651 372.71,-280.186 379.287,-275.609"/>
<polygon fill="#0044a0" stroke="#0044a0" points="381.274,-278.496 387.956,-270.274 377.605,-272.534 381.274,-278.496"/>
<g id="edge2" class="edge">
<title>t1&#45;&gt;t2</title>
<path fill="none" stroke="#0044a0" d="M774.5557,-1120.8306C775.4962,-1117.0421 776.5194,-1112.92 777.5618,-1108.7212"/>
<polygon fill="#0044a0" stroke="#0044a0" points="780.986,-1109.4542 779.9984,-1098.9055 774.1922,-1107.7677 780.986,-1109.4542"/>
</g>
<!-- t20 -->
<g id="node21" class="node"><title>t20</title>
<polygon fill="#efa03b" stroke="black" points="234.918,-156.426 480,-129.117 725.082,-156.426 724.853,-200.614 235.147,-200.614 234.918,-156.426"/>
<polygon fill="none" stroke="black" points="230.903,-152.847 480,-125.09 729.097,-152.847 728.829,-204.616 231.171,-204.616 230.903,-152.847"/>
<text text-anchor="middle" x="480" y="-162.639" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.merge_pages_ghostscript</text>
<g id="node22" class="node">
<title>t20</title>
<polygon fill="#efa03b" stroke="#000000" points="649.5156,-1020.2193 286.4844,-1020.2193 282.4844,-1016.2193 282.4844,-984.2193 645.5156,-984.2193 649.5156,-988.2193 649.5156,-1020.2193"/>
<polyline fill="none" stroke="#000000" points="645.5156,-1016.2193 282.4844,-1016.2193 "/>
<polyline fill="none" stroke="#000000" points="645.5156,-1016.2193 645.5156,-984.2193 "/>
<polyline fill="none" stroke="#000000" points="645.5156,-1016.2193 649.5156,-1020.2193 "/>
<text text-anchor="middle" x="466" y="-996.2193" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.generate_postscript_stub</text>
</g>
<!-- t16&#45;&gt;t20 -->
<g id="edge33" class="edge"><title>t16&#45;&gt;t20</title>
<path fill="none" stroke="#0044a0" d="M487.091,-233.881C486.46,-228.165 485.726,-221.515 484.974,-214.703"/>
<polygon fill="#0044a0" stroke="#0044a0" points="488.446,-214.253 483.87,-204.697 481.488,-215.021 488.446,-214.253"/>
<!-- t1&#45;&gt;t20 -->
<g id="edge31" class="edge">
<title>t1&#45;&gt;t20</title>
<path fill="none" stroke="#0044a0" d="M598.785,-1123.5822C563.105,-1117.7214 531.8209,-1109.8012 517,-1099.1825 493.9382,-1082.6593 480.4369,-1052.4741 473.1842,-1030.1561"/>
<polygon fill="#0044a0" stroke="#0044a0" points="476.5199,-1029.095 470.2797,-1020.5329 469.8185,-1031.1177 476.5199,-1029.095"/>
</g>
<!-- t16&#45;&gt;t21 -->
<g id="edge37" class="edge"><title>t16&#45;&gt;t21</title>
<path fill="none" stroke="#0044a0" d="M602.543,-234.159C645.082,-227.582 693.805,-219.809 738,-212.173 748.572,-210.346 759.416,-208.431 770.357,-206.468"/>
<polygon fill="#0044a0" stroke="#0044a0" points="771.206,-209.872 780.427,-204.653 769.964,-202.983 771.206,-209.872"/>
<!-- t24 -->
<g id="node24" class="node">
<title>t24</title>
<polygon fill="#efa03b" stroke="#000000" points="20.4213,-156.4247 242,-129.112 463.5787,-156.4247 463.3719,-200.6177 20.6281,-200.6177 20.4213,-156.4247"/>
<polygon fill="none" stroke="#000000" points="16.4083,-152.8864 242,-125.079 467.5917,-152.8864 467.3496,-204.6205 16.6504,-204.6205 16.4083,-152.8864"/>
<text text-anchor="middle" x="242" y="-162.6394" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.merge_pages_mupdf</text>
</g>
<!-- t14&#45;&gt;t20 -->
<g id="edge34" class="edge"><title>t14&#45;&gt;t20</title>
<path fill="none" stroke="#0044a0" d="M837.094,-292.063C776.411,-271.259 672.165,-235.52 591.922,-208.01"/>
<polygon fill="#0044a0" stroke="#0044a0" points="592.855,-204.63 582.26,-204.697 590.584,-211.251 592.855,-204.63"/>
<!-- t1&#45;&gt;t24 -->
<g id="edge41" class="edge">
<title>t1&#45;&gt;t24</title>
<path fill="none" stroke="#0044a0" d="M598.6617,-1124.8567C376.4578,-1103.9215 17,-1060.9746 17,-1002.2193 17,-1002.2193 17,-1002.2193 17,-335.7123 17,-276.266 68.2524,-235.5065 121.8346,-209.1377"/>
<polygon fill="#0044a0" stroke="#0044a0" points="123.6227,-212.1629 131.1479,-204.7049 120.6143,-205.8423 123.6227,-212.1629"/>
</g>
<!-- t14&#45;&gt;t21 -->
<g id="edge38" class="edge"><title>t14&#45;&gt;t21</title>
<path fill="none" stroke="#0044a0" d="M896.319,-292.063C906.85,-272.729 924.406,-240.497 938.861,-213.958"/>
<polygon fill="#0044a0" stroke="#0044a0" points="942.118,-215.294 943.828,-204.838 935.971,-211.946 942.118,-215.294"/>
<!-- t23 -->
<g id="node25" class="node">
<title>t23</title>
<polygon fill="#efa03b" stroke="#000000" points="1006.4902,-156.4239 1218,-129.1092 1429.5098,-156.4239 1429.3124,-200.6199 1006.6876,-200.6199 1006.4902,-156.4239"/>
<polygon fill="none" stroke="#000000" points="1002.4742,-152.9063 1218,-125.073 1433.5258,-152.9063 1433.2948,-204.623 1002.7052,-204.623 1002.4742,-152.9063"/>
<text text-anchor="middle" x="1218" y="-162.6394" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.merge_pages_qpdf</text>
</g>
<!-- t19&#45;&gt;t20 -->
<g id="edge30" class="edge"><title>t19&#45;&gt;t20</title>
<path fill="none" stroke="#0044a0" d="M257.907,-234.081C284.597,-226.336 317.298,-216.848 349.316,-207.558"/>
<polygon fill="#0044a0" stroke="#0044a0" points="350.595,-210.831 359.224,-204.683 348.645,-204.108 350.595,-210.831"/>
<!-- t1&#45;&gt;t23 -->
<g id="edge46" class="edge">
<title>t1&#45;&gt;t23</title>
<path fill="none" stroke="#0044a0" d="M941.3419,-1136.2202C1263.4143,-1128.3561 1927,-1100.2573 1927,-1002.2193 1927,-1002.2193 1927,-1002.2193 1927,-335.7123 1927,-234.7734 1651.3069,-194.7548 1443.7522,-178.9295"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1443.8443,-175.4267 1433.6111,-178.1712 1443.3223,-182.4072 1443.8443,-175.4267"/>
</g>
<!-- t19&#45;&gt;t21 -->
<g id="edge35" class="edge"><title>t19&#45;&gt;t21</title>
<path fill="none" stroke="#0044a0" d="M323.792,-235.042C326.891,-234.738 329.964,-234.447 333,-234.173 512.532,-217.929 559.043,-233.851 738,-212.173 751.869,-210.493 766.169,-208.472 780.522,-206.237"/>
<polygon fill="#0044a0" stroke="#0044a0" points="781.136,-209.683 790.462,-204.657 780.037,-202.77 781.136,-209.683"/>
<!-- t3 -->
<g id="node4" class="node">
<title>t3</title>
<polygon fill="#efa03b" stroke="#000000" points="1055.3851,-1020.2193 808.6149,-1020.2193 804.6149,-1016.2193 804.6149,-984.2193 1051.3851,-984.2193 1055.3851,-988.2193 1055.3851,-1020.2193"/>
<polyline fill="none" stroke="#000000" points="1051.3851,-1016.2193 804.6149,-1016.2193 "/>
<polyline fill="none" stroke="#000000" points="1051.3851,-1016.2193 1051.3851,-984.2193 "/>
<polyline fill="none" stroke="#000000" points="1051.3851,-1016.2193 1055.3851,-1020.2193 "/>
<text text-anchor="middle" x="930" y="-996.2193" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.split_page</text>
</g>
<!-- t17&#45;&gt;t20 -->
<g id="edge32" class="edge"><title>t17&#45;&gt;t20</title>
<path fill="none" stroke="#0044a0" d="M1195.47,-292.133C1071.63,-270.93 856.728,-234.138 694.688,-206.395"/>
<polygon fill="#0044a0" stroke="#0044a0" points="695.028,-202.903 684.581,-204.665 693.846,-209.802 695.028,-202.903"/>
<!-- t2&#45;&gt;t3 -->
<g id="edge3" class="edge">
<title>t2&#45;&gt;t3</title>
<path fill="none" stroke="#0044a0" d="M837.0145,-1046.7494C851.946,-1039.5987 868.2558,-1031.7881 882.9879,-1024.733"/>
<polygon fill="#0044a0" stroke="#0044a0" points="884.9035,-1027.6964 892.4109,-1020.2204 881.88,-1021.383 884.9035,-1027.6964"/>
</g>
<!-- t17&#45;&gt;t21 -->
<g id="edge36" class="edge"><title>t17&#45;&gt;t21</title>
<path fill="none" stroke="#0044a0" d="M1254.29,-292.063C1205.19,-271.428 1121.13,-236.1 1055.9,-208.683"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1056.99,-205.345 1046.42,-204.697 1054.28,-211.799 1056.99,-205.345"/>
<!-- t4 -->
<g id="node5" class="node">
<title>t4</title>
<polygon fill="#efa03b" stroke="#000000" points="1194.5194,-941.2939 964,-958.1945 733.4806,-941.2939 733.6958,-913.948 1194.3042,-913.948 1194.5194,-941.2939"/>
<polygon fill="none" stroke="#000000" points="1198.5491,-945.0069 964,-962.203 729.4509,-945.0069 729.7267,-909.9503 1198.2733,-909.9503 1198.5491,-945.0069"/>
<text text-anchor="middle" x="964" y="-927.7357" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.ocr_or_skip</text>
</g>
<!-- t18&#45;&gt;t20 -->
<g id="edge31" class="edge"><title>t18&#45;&gt;t20</title>
<path fill="none" stroke="#0044a0" d="M120.266,-796.105C70.9512,-780.34 17,-751.202 17,-699.173 17,-699.173 17,-699.173 17,-309.173 17,-269.597 31.8505,-255.79 65,-234.173 92.9782,-215.927 154.085,-202.475 221.024,-192.724"/>
<polygon fill="#0044a0" stroke="#0044a0" points="221.533,-196.187 230.939,-191.311 220.546,-189.256 221.533,-196.187"/>
<!-- t3&#45;&gt;t4 -->
<g id="edge4" class="edge">
<title>t3&#45;&gt;t4</title>
<path fill="none" stroke="#0044a0" d="M939.1114,-983.867C941.197,-979.6659 943.4863,-975.0549 945.8053,-970.3839"/>
<polygon fill="#0044a0" stroke="#0044a0" points="948.9439,-971.9327 950.2559,-961.4194 942.6741,-968.8199 948.9439,-971.9327"/>
</g>
<!-- t5 -->
<g id="node6" class="node">
<title>t5</title>
<polygon fill="#efa03b" stroke="#000000" points="1119.3166,-883.252 812.6834,-883.252 808.6834,-879.252 808.6834,-847.252 1115.3166,-847.252 1119.3166,-851.252 1119.3166,-883.252"/>
<polyline fill="none" stroke="#000000" points="1115.3166,-879.252 808.6834,-879.252 "/>
<polyline fill="none" stroke="#000000" points="1115.3166,-879.252 1115.3166,-847.252 "/>
<polyline fill="none" stroke="#000000" points="1115.3166,-879.252 1119.3166,-883.252 "/>
<text text-anchor="middle" x="964" y="-859.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.rasterize_preview</text>
</g>
<!-- t4&#45;&gt;t5 -->
<g id="edge5" class="edge">
<title>t4&#45;&gt;t5</title>
<path fill="none" stroke="#0044a0" d="M964,-909.7834C964,-904.5184 964,-898.8955 964,-893.4799"/>
<polygon fill="#0044a0" stroke="#0044a0" points="967.5001,-893.4695 964,-883.4695 960.5001,-893.4695 967.5001,-893.4695"/>
</g>
<!-- t6 -->
<g id="node7" class="node">
<title>t6</title>
<polygon fill="#efa03b" stroke="#000000" points="1269.0824,-825.252 1010.9176,-825.252 1006.9176,-821.252 1006.9176,-789.252 1265.0824,-789.252 1269.0824,-793.252 1269.0824,-825.252"/>
<polyline fill="none" stroke="#000000" points="1265.0824,-821.252 1006.9176,-821.252 "/>
<polyline fill="none" stroke="#000000" points="1265.0824,-821.252 1265.0824,-789.252 "/>
<polyline fill="none" stroke="#000000" points="1265.0824,-821.252 1269.0824,-825.252 "/>
<text text-anchor="middle" x="1138" y="-801.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.orient_page</text>
</g>
<!-- t4&#45;&gt;t6 -->
<g id="edge7" class="edge">
<title>t4&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M1072.6871,-909.8532C1097.768,-902.2558 1119.5887,-893.2079 1128,-883.252 1138.9559,-870.2843 1141.4763,-851.3696 1141.2475,-835.6545"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1144.7291,-835.2052 1140.7199,-825.3983 1137.7384,-835.5649 1144.7291,-835.2052"/>
</g>
<!-- t5&#45;&gt;t6 -->
<g id="edge6" class="edge">
<title>t5&#45;&gt;t6</title>
<path fill="none" stroke="#0044a0" d="M1018.1009,-847.2184C1035.9411,-841.2716 1055.9459,-834.6034 1074.4267,-828.4431"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1075.5526,-831.7572 1083.9326,-825.2745 1073.3389,-825.1165 1075.5526,-831.7572"/>
</g>
<!-- t7 -->
<g id="node8" class="node">
<title>t7</title>
<polygon fill="#efa03b" stroke="#000000" points="917.7325,-767.252 542.2675,-767.252 538.2675,-763.252 538.2675,-731.252 913.7325,-731.252 917.7325,-735.252 917.7325,-767.252"/>
<polyline fill="none" stroke="#000000" points="913.7325,-763.252 538.2675,-763.252 "/>
<polyline fill="none" stroke="#000000" points="913.7325,-763.252 913.7325,-731.252 "/>
<polyline fill="none" stroke="#000000" points="913.7325,-763.252 917.7325,-767.252 "/>
<text text-anchor="middle" x="728" y="-743.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.rasterize_with_ghostscript</text>
</g>
<!-- t6&#45;&gt;t7 -->
<g id="edge8" class="edge">
<title>t6&#45;&gt;t7</title>
<path fill="none" stroke="#0044a0" d="M1010.5209,-789.2184C964.4341,-782.6988 912.2171,-775.312 865.3222,-768.6781"/>
<polygon fill="#0044a0" stroke="#0044a0" points="865.792,-765.2098 855.4003,-767.2745 864.8114,-772.1408 865.792,-765.2098"/>
</g>
<!-- t17 -->
<g id="node15" class="node">
<title>t17</title>
<polygon fill="#ff69b4" stroke="#000000" points="1224.2696,-447.252 843.7304,-447.252 839.7304,-443.252 839.7304,-411.252 1220.2696,-411.252 1224.2696,-415.252 1224.2696,-447.252"/>
<polyline fill="none" stroke="#000000" points="1220.2696,-443.252 839.7304,-443.252 "/>
<polyline fill="none" stroke="#000000" points="1220.2696,-443.252 1220.2696,-411.252 "/>
<polyline fill="none" stroke="#000000" points="1220.2696,-443.252 1224.2696,-447.252 "/>
<text text-anchor="middle" x="1032" y="-423.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.ocr_tesseract_textonly_pdf</text>
</g>
<!-- t6&#45;&gt;t17 -->
<g id="edge16" class="edge">
<title>t6&#45;&gt;t17</title>
<path fill="none" stroke="#0044a0" d="M1158.4397,-788.9107C1179.7261,-767.6923 1210,-730.5987 1210,-691.252 1210,-691.252 1210,-691.252 1210,-575.252 1210,-532.3545 1227.3937,-512.2638 1200,-479.252 1190.5284,-467.8379 1163.9108,-457.8666 1134.5783,-449.8944"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1135.1079,-446.4151 1124.5475,-447.2681 1133.3349,-453.1869 1135.1079,-446.4151"/>
</g>
<!-- t14 -->
<g id="node17" class="node">
<title>t14</title>
<polygon fill="#00cc66" stroke="#000000" points="1565,-457.252 1242.4141,-429.252 1565,-401.252 1887.5859,-429.252 1565,-457.252"/>
<text text-anchor="middle" x="1565" y="-423.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.select_image_layer</text>
</g>
<!-- t6&#45;&gt;t14 -->
<g id="edge22" class="edge">
<title>t6&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M1211.6459,-789.1984C1287.5889,-768.3738 1395,-731.7602 1395,-691.252 1395,-691.252 1395,-691.252 1395,-575.252 1395,-519.4485 1448.3697,-480.3436 1495.3026,-456.7437"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1497.1229,-459.7505 1504.5857,-452.2301 1494.0619,-453.4552 1497.1229,-459.7505"/>
</g>
<!-- t21 -->
<g id="node20" class="node">
<title>t21</title>
<polygon fill="#efa03b" stroke="#000000" points="1414.7716,-270.1725 1169.2284,-270.1725 1165.2284,-266.1725 1165.2284,-234.1725 1410.7716,-234.1725 1414.7716,-238.1725 1414.7716,-270.1725"/>
<polyline fill="none" stroke="#000000" points="1410.7716,-266.1725 1165.2284,-266.1725 "/>
<polyline fill="none" stroke="#000000" points="1410.7716,-266.1725 1410.7716,-234.1725 "/>
<polyline fill="none" stroke="#000000" points="1410.7716,-266.1725 1414.7716,-270.1725 "/>
<text text-anchor="middle" x="1290" y="-246.1725" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.skip_page</text>
</g>
<!-- t6&#45;&gt;t21 -->
<g id="edge28" class="edge">
<title>t6&#45;&gt;t21</title>
<path fill="none" stroke="#0044a0" d="M1269.322,-800.6326C1488.9576,-787.7851 1907,-754.9633 1907,-691.252 1907,-691.252 1907,-691.252 1907,-429.252 1907,-329.7211 1606.6136,-282.8737 1425.0278,-263.647"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1425.1684,-260.1428 1414.8592,-262.586 1424.4419,-267.105 1425.1684,-260.1428"/>
</g>
<!-- t19 -->
<g id="node21" class="node">
<title>t19</title>
<polygon fill="#66ccff" stroke="#000000" points="465.6739,-447.252 60.3261,-447.252 56.3261,-443.252 56.3261,-411.252 461.6739,-411.252 465.6739,-415.252 465.6739,-447.252"/>
<polyline fill="none" stroke="#000000" points="461.6739,-443.252 56.3261,-443.252 "/>
<polyline fill="none" stroke="#000000" points="461.6739,-443.252 461.6739,-411.252 "/>
<polyline fill="none" stroke="#000000" points="461.6739,-443.252 465.6739,-447.252 "/>
<text text-anchor="middle" x="261" y="-423.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.ocr_tesseract_and_render_pdf</text>
</g>
<!-- t6&#45;&gt;t19 -->
<g id="edge30" class="edge">
<title>t6&#45;&gt;t19</title>
<path fill="none" stroke="#0044a0" d="M1006.6775,-802.4744C883.1032,-797.0687 693.0454,-786.4114 529,-767.252 327.1018,-743.6717 78,-894.5226 78,-691.252 78,-691.252 78,-691.252 78,-575.252 78,-532.3545 60.4757,-512.1551 88,-479.252 97.684,-467.6756 124.6714,-457.7191 154.5327,-449.8074"/>
<polygon fill="#0044a0" stroke="#0044a0" points="155.4872,-453.176 164.3106,-447.311 153.7556,-446.3936 155.4872,-453.176"/>
</g>
<!-- t8 -->
<g id="node9" class="node">
<title>t8</title>
<polygon fill="#efa03b" stroke="#000000" points="942.0801,-709.252 517.9199,-709.252 513.9199,-705.252 513.9199,-673.252 938.0801,-673.252 942.0801,-677.252 942.0801,-709.252"/>
<polyline fill="none" stroke="#000000" points="938.0801,-705.252 513.9199,-705.252 "/>
<polyline fill="none" stroke="#000000" points="938.0801,-705.252 938.0801,-673.252 "/>
<polyline fill="none" stroke="#000000" points="938.0801,-705.252 942.0801,-709.252 "/>
<text text-anchor="middle" x="728" y="-685.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.preprocess_remove_background</text>
</g>
<!-- t7&#45;&gt;t8 -->
<g id="edge9" class="edge">
<title>t7&#45;&gt;t8</title>
<path fill="none" stroke="#0044a0" d="M728,-731.2184C728,-727.5438 728,-723.5938 728,-719.6583"/>
<polygon fill="#0044a0" stroke="#0044a0" points="731.5001,-719.4742 728,-709.4743 724.5001,-719.4743 731.5001,-719.4742"/>
</g>
<!-- t13 -->
<g id="node16" class="node">
<title>t13</title>
<polygon fill="#efa03b" stroke="#000000" points="481,-535.252 97.1874,-507.252 481,-479.252 864.8126,-507.252 481,-535.252"/>
<text text-anchor="middle" x="481" y="-501.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.select_visible_page_image</text>
</g>
<!-- t7&#45;&gt;t13 -->
<g id="edge20" class="edge">
<title>t7&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M592.7552,-731.2177C555.8805,-724.9263 521.5464,-717.3855 505,-709.252 470.5499,-692.3178 456.557,-686.3456 441,-651.252 425.2764,-615.7827 441.5685,-572.6191 457.8398,-542.8513"/>
<polygon fill="#0044a0" stroke="#0044a0" points="460.9365,-544.4864 462.8533,-534.0664 454.8568,-541.0167 460.9365,-544.4864"/>
</g>
<!-- t9 -->
<g id="node10" class="node">
<title>t9</title>
<polygon fill="#efa03b" stroke="#000000" points="773.6759,-651.252 454.3241,-651.252 450.3241,-647.252 450.3241,-615.252 769.6759,-615.252 773.6759,-619.252 773.6759,-651.252"/>
<polyline fill="none" stroke="#000000" points="769.6759,-647.252 450.3241,-647.252 "/>
<polyline fill="none" stroke="#000000" points="769.6759,-647.252 769.6759,-615.252 "/>
<polyline fill="none" stroke="#000000" points="769.6759,-647.252 773.6759,-651.252 "/>
<text text-anchor="middle" x="612" y="-627.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.preprocess_deskew</text>
</g>
<!-- t8&#45;&gt;t9 -->
<g id="edge10" class="edge">
<title>t8&#45;&gt;t9</title>
<path fill="none" stroke="#0044a0" d="M691.9327,-673.2184C680.9122,-667.7081 668.6526,-661.5783 657.1104,-655.8072"/>
<polygon fill="#0044a0" stroke="#0044a0" points="658.5545,-652.6162 648.045,-651.2745 655.424,-658.8772 658.5545,-652.6162"/>
</g>
<!-- t8&#45;&gt;t13 -->
<g id="edge19" class="edge">
<title>t8&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M763.3121,-673.1037C771.2083,-667.2594 778.5531,-659.9847 783,-651.252 790.2604,-636.9942 792.214,-628.3326 783,-615.252 759.3602,-581.6921 662.7714,-550.9641 585.1057,-530.9385"/>
<polygon fill="#0044a0" stroke="#0044a0" points="585.806,-527.5052 575.2515,-528.4283 584.078,-534.2886 585.806,-527.5052"/>
</g>
<!-- t10 -->
<g id="node11" class="node">
<title>t10</title>
<polygon fill="#efa03b" stroke="#000000" points="1117.8828,-593.252 816.1172,-593.252 812.1172,-589.252 812.1172,-557.252 1113.8828,-557.252 1117.8828,-561.252 1117.8828,-593.252"/>
<polyline fill="none" stroke="#000000" points="1113.8828,-589.252 812.1172,-589.252 "/>
<polyline fill="none" stroke="#000000" points="1113.8828,-589.252 1113.8828,-557.252 "/>
<polyline fill="none" stroke="#000000" points="1113.8828,-589.252 1117.8828,-593.252 "/>
<text text-anchor="middle" x="965" y="-569.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.preprocess_clean</text>
</g>
<!-- t9&#45;&gt;t10 -->
<g id="edge11" class="edge">
<title>t9&#45;&gt;t10</title>
<path fill="none" stroke="#0044a0" d="M721.7564,-615.2184C760.9379,-608.7806 805.2658,-601.4973 845.2464,-594.9283"/>
<polygon fill="#0044a0" stroke="#0044a0" points="846.0113,-598.3496 855.3115,-593.2745 844.8763,-591.4422 846.0113,-598.3496"/>
</g>
<!-- t9&#45;&gt;t13 -->
<g id="edge18" class="edge">
<title>t9&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M593.2309,-615.1993C573.0988,-595.8356 540.6771,-564.6514 515.8453,-540.7673"/>
<polygon fill="#0044a0" stroke="#0044a0" points="518.0381,-538.0203 508.4046,-533.6106 513.1856,-543.0654 518.0381,-538.0203"/>
</g>
<!-- t11 -->
<g id="node12" class="node">
<title>t11</title>
<polygon fill="#efa03b" stroke="#000000" points="1191.1094,-525.252 886.8906,-525.252 882.8906,-521.252 882.8906,-489.252 1187.1094,-489.252 1191.1094,-493.252 1191.1094,-525.252"/>
<polyline fill="none" stroke="#000000" points="1187.1094,-521.252 882.8906,-521.252 "/>
<polyline fill="none" stroke="#000000" points="1187.1094,-521.252 1187.1094,-489.252 "/>
<polyline fill="none" stroke="#000000" points="1187.1094,-521.252 1191.1094,-525.252 "/>
<text text-anchor="middle" x="1037" y="-501.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.select_ocr_image</text>
</g>
<!-- t10&#45;&gt;t11 -->
<g id="edge12" class="edge">
<title>t10&#45;&gt;t11</title>
<path fill="none" stroke="#0044a0" d="M984.2947,-557.0293C992.2846,-549.4832 1001.6851,-540.6049 1010.318,-532.4517"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1012.7851,-534.9359 1017.6521,-525.5251 1007.9787,-529.8468 1012.7851,-534.9359"/>
</g>
<!-- t10&#45;&gt;t13 -->
<g id="edge17" class="edge">
<title>t10&#45;&gt;t13</title>
<path fill="none" stroke="#0044a0" d="M836.5676,-557.2078C770.772,-547.9638 690.4915,-536.6847 622.8731,-527.1846"/>
<polygon fill="#0044a0" stroke="#0044a0" points="622.9531,-523.6616 612.5634,-525.7361 621.9792,-530.5935 622.9531,-523.6616"/>
</g>
<!-- t12 -->
<g id="node13" class="node">
<title>t12</title>
<polygon fill="#00cc66" stroke="#000000" points="822.0996,-447.252 507.9004,-447.252 503.9004,-443.252 503.9004,-411.252 818.0996,-411.252 822.0996,-415.252 822.0996,-447.252"/>
<polyline fill="none" stroke="#000000" points="818.0996,-443.252 503.9004,-443.252 "/>
<polyline fill="none" stroke="#000000" points="818.0996,-443.252 818.0996,-411.252 "/>
<polyline fill="none" stroke="#000000" points="818.0996,-443.252 822.0996,-447.252 "/>
<text text-anchor="middle" x="663" y="-423.252" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.ocr_tesseract_hocr</text>
</g>
<!-- t11&#45;&gt;t12 -->
<g id="edge13" class="edge">
<title>t11&#45;&gt;t12</title>
<path fill="none" stroke="#0044a0" d="M950.2568,-489.1612C893.2425,-477.2705 818.5749,-461.6981 759.6569,-449.4104"/>
<polygon fill="#0044a0" stroke="#0044a0" points="760.1,-445.9276 749.5961,-447.3122 758.6708,-452.7802 760.1,-445.9276"/>
</g>
<!-- t11&#45;&gt;t17 -->
<g id="edge15" class="edge">
<title>t11&#45;&gt;t17</title>
<path fill="none" stroke="#0044a0" d="M1035.8403,-489.1612C1035.2388,-479.7779 1034.4904,-468.1019 1033.8144,-457.5566"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1037.3033,-457.2705 1033.1707,-447.5149 1030.3177,-457.7184 1037.3033,-457.2705"/>
</g>
<!-- t15 -->
<g id="node14" class="node">
<title>t15</title>
<polygon fill="#00cc66" stroke="#000000" points="1229.0433,-353.7123 920.9567,-353.7123 916.9567,-349.7123 916.9567,-317.7123 1225.0433,-317.7123 1229.0433,-321.7123 1229.0433,-353.7123"/>
<polyline fill="none" stroke="#000000" points="1225.0433,-349.7123 916.9567,-349.7123 "/>
<polyline fill="none" stroke="#000000" points="1225.0433,-349.7123 1225.0433,-317.7123 "/>
<polyline fill="none" stroke="#000000" points="1225.0433,-349.7123 1229.0433,-353.7123 "/>
<text text-anchor="middle" x="1073" y="-329.7123" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.render_hocr_page</text>
</g>
<!-- t12&#45;&gt;t15 -->
<g id="edge14" class="edge">
<title>t12&#45;&gt;t15</title>
<path fill="none" stroke="#0044a0" d="M754.3855,-411.2269C799.2649,-402.117 854.0781,-390.6067 903,-379.252 933.2642,-372.2277 966.3927,-363.8908 995.3012,-356.4015"/>
<polygon fill="#0044a0" stroke="#0044a0" points="996.5416,-359.6955 1005.3389,-353.7914 994.7799,-352.9208 996.5416,-359.6955"/>
</g>
<!-- t16 -->
<g id="node19" class="node">
<title>t16</title>
<polygon fill="#00cc66" stroke="#000000" points="651.9202,-270.1725 284.0798,-270.1725 280.0798,-266.1725 280.0798,-234.1725 647.9202,-234.1725 651.9202,-238.1725 651.9202,-270.1725"/>
<polyline fill="none" stroke="#000000" points="647.9202,-266.1725 280.0798,-266.1725 "/>
<polyline fill="none" stroke="#000000" points="647.9202,-266.1725 647.9202,-234.1725 "/>
<polyline fill="none" stroke="#000000" points="647.9202,-266.1725 651.9202,-270.1725 "/>
<text text-anchor="middle" x="466" y="-246.1725" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.render_hocr_debug_page</text>
</g>
<!-- t12&#45;&gt;t16 -->
<g id="edge27" class="edge">
<title>t12&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M772.353,-411.2135C825.7002,-401.3017 880.0px85,-389.0648 888,-379.252 912.2169,-349.0628 914.0433,-320.8011 888,-292.1725 871.9898,-274.5731 764.4698,-264.5473 661.9779,-258.9344"/>
<polygon fill="#0044a0" stroke="#0044a0" points="661.9319,-255.4271 651.7593,-258.3883 661.5583,-262.4171 661.9319,-255.4271"/>
</g>
<!-- t25 -->
<g id="node27" class="node">
<title>t25</title>
<polygon fill="#efa03b" stroke="#000000" points="488.7205,-323.495 682,-296.1762 875.2795,-323.495 875.0991,-367.6976 488.9009,-367.6976 488.7205,-323.495"/>
<polygon fill="none" stroke="#000000" points="484.7033,-320.0193 682,-292.1327 879.2967,-320.0193 879.0858,-371.7013 484.9142,-371.7013 484.7033,-320.0193"/>
<text text-anchor="middle" x="682" y="-329.7123" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.merge_sidecars</text>
</g>
<!-- t12&#45;&gt;t25 -->
<g id="edge52" class="edge">
<title>t12&#45;&gt;t25</title>
<path fill="none" stroke="#0044a0" d="M666.6634,-411.2166C668.3795,-402.768 670.5105,-392.2769 672.6326,-381.8293"/>
<polygon fill="#0044a0" stroke="#0044a0" points="676.09,-382.3905 674.6507,-371.8939 669.2301,-380.9971 676.09,-382.3905"/>
</g>
<!-- t18 -->
<g id="node18" class="node">
<title>t18</title>
<polygon fill="#00cc66" stroke="#000000" points="1147.2989,-270.1725 856.7011,-270.1725 852.7011,-266.1725 852.7011,-234.1725 1143.2989,-234.1725 1147.2989,-238.1725 1147.2989,-270.1725"/>
<polyline fill="none" stroke="#000000" points="1143.2989,-266.1725 852.7011,-266.1725 "/>
<polyline fill="none" stroke="#000000" points="1143.2989,-266.1725 1143.2989,-234.1725 "/>
<polyline fill="none" stroke="#000000" points="1143.2989,-266.1725 1147.2989,-270.1725 "/>
<text text-anchor="middle" x="1000" y="-246.1725" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.combine_layers</text>
</g>
<!-- t15&#45;&gt;t18 -->
<g id="edge24" class="edge">
<title>t15&#45;&gt;t18</title>
<path fill="none" stroke="#0044a0" d="M1057.1601,-317.5854C1047.0201,-305.9814 1033.7081,-290.7474 1022.4822,-277.9007"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1025.0247,-275.4911 1015.809,-270.264 1019.7536,-280.0972 1025.0247,-275.4911"/>
</g>
<!-- t17&#45;&gt;t18 -->
<g id="edge23" class="edge">
<title>t17&#45;&gt;t18</title>
<path fill="none" stroke="#0044a0" d="M1160.5767,-411.2248C1195.7121,-403.5317 1227.1378,-393.066 1238,-379.252 1261.9222,-348.8288 1262.9959,-321.72 1238,-292.1725 1231.6408,-284.6553 1197.8632,-277.2656 1157.6598,-270.9231"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1158.0117,-267.4362 1147.596,-269.3761 1156.9481,-274.3549 1158.0117,-267.4362"/>
</g>
<!-- t17&#45;&gt;t25 -->
<g id="edge51" class="edge">
<title>t17&#45;&gt;t25</title>
<path fill="none" stroke="#0044a0" d="M964.5165,-411.2166C925.304,-400.7368 874.3329,-387.1145 826.4989,-374.3305"/>
<polygon fill="#0044a0" stroke="#0044a0" points="827.2267,-370.9023 816.6621,-371.7016 825.4193,-377.6649 827.2267,-370.9023"/>
</g>
<!-- t13&#45;&gt;t14 -->
<g id="edge21" class="edge">
<title>t13&#45;&gt;t14</title>
<path fill="none" stroke="#0044a0" d="M672.8683,-493.1993C736.8314,-488.6166 808.4777,-483.5937 874,-479.252 1033.5051,-468.6828 1073.5671,-468.8587 1233,-457.252 1285.8429,-453.4051 1343.3743,-448.7264 1395.4741,-444.3037"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1395.81,-447.7879 1405.477,-443.4521 1395.2161,-440.8131 1395.81,-447.7879"/>
</g>
<!-- t13&#45;&gt;t16 -->
<g id="edge26" class="edge">
<title>t13&#45;&gt;t16</title>
<path fill="none" stroke="#0044a0" d="M479.4495,-479.3529C478.2545,-457.9761 476.5497,-427.7492 475,-401.252 472.546,-359.2945 469.5914,-310.7019 467.754,-280.6981"/>
<polygon fill="#0044a0" stroke="#0044a0" points="471.2362,-280.2988 467.1306,-270.5318 464.2493,-280.7273 471.2362,-280.2988"/>
</g>
<!-- t13&#45;&gt;t19 -->
<g id="edge29" class="edge">
<title>t13&#45;&gt;t19</title>
<path fill="none" stroke="#0044a0" d="M415.2907,-483.9551C385.6007,-473.4286 350.7427,-461.0699 321.6782,-450.7652"/>
<polygon fill="#0044a0" stroke="#0044a0" points="322.6542,-447.3978 312.0595,-447.3549 320.315,-453.9954 322.6542,-447.3978"/>
</g>
<!-- t14&#45;&gt;t18 -->
<g id="edge25" class="edge">
<title>t14&#45;&gt;t18</title>
<path fill="none" stroke="#0044a0" d="M1519.9776,-405.138C1460.3688,-374.2505 1351.1978,-321.2463 1252,-292.1725 1221.8888,-283.3473 1189.1477,-276.3434 1157.5373,-270.8287"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1157.7641,-267.3171 1147.3187,-269.0901 1156.59,-274.2179 1157.7641,-267.3171"/>
</g>
<!-- t22 -->
<g id="node23" class="node"><title>t22</title>
<polygon fill="#efa03b" stroke="black" points="554.065,-47.332 721,-20.0049 887.935,-47.332 887.779,-91.5483 554.221,-91.5483 554.065,-47.332"/>
<polygon fill="none" stroke="black" points="550.054,-43.9306 721,-15.9468 891.946,-43.9306 891.764,-95.5532 550.236,-95.5532 550.054,-43.9306"/>
<text text-anchor="middle" x="721" y="-53.5531" font-family="Times,serif" font-size="20.00">ocrmypdf.pipeline.copy_final</text>
<g id="node23" class="node">
<title>t22</title>
<polygon fill="#efa03b" stroke="#000000" points="489.918,-156.4263 735,-129.1169 980.082,-156.4263 979.8532,-200.6137 490.1468,-200.6137 489.918,-156.4263"/>
<polygon fill="none" stroke="#000000" points="485.9027,-152.8467 735,-125.09 984.0973,-152.8467 983.8293,-204.6159 486.1707,-204.6159 485.9027,-152.8467"/>
<text text-anchor="middle" x="735" y="-162.6394" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.merge_pages_ghostscript</text>
</g>
<!-- t20&#45;&gt;t22 -->
<g id="edge41" class="edge"><title>t20&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M556.585,-133.609C580.784,-122.857 607.72,-110.888 632.67,-99.8018"/>
<polygon fill="#0044a0" stroke="#0044a0" points="634.408,-102.859 642.125,-95.6004 631.566,-96.4625 634.408,-102.859"/>
<!-- t18&#45;&gt;t22 -->
<g id="edge35" class="edge">
<title>t18&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M942.8252,-234.1499C918.2191,-226.3936 888.4232,-217.0013 859.1708,-207.7804"/>
<polygon fill="#0044a0" stroke="#0044a0" points="859.8692,-204.3309 849.2796,-204.6625 857.7647,-211.007 859.8692,-204.3309"/>
</g>
<!-- t18&#45;&gt;t24 -->
<g id="edge39" class="edge">
<title>t18&#45;&gt;t24</title>
<path fill="none" stroke="#0044a0" d="M852.4716,-245.1051C747.4645,-239.0474 603.1919,-228.6387 477,-212.1725 463.5912,-210.4229 449.788,-208.4132 435.908,-206.238"/>
<polygon fill="#0044a0" stroke="#0044a0" points="436.2183,-202.7434 425.7921,-204.6263 435.1168,-209.6562 436.2183,-202.7434"/>
</g>
<!-- t18&#45;&gt;t23 -->
<g id="edge44" class="edge">
<title>t18&#45;&gt;t23</title>
<path fill="none" stroke="#0044a0" d="M1047.0344,-234.1499C1066.9243,-226.5285 1090.9364,-217.3275 1114.596,-208.2617"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1115.9033,-211.509 1123.9889,-204.6625 1113.3986,-204.9724 1115.9033,-211.509"/>
</g>
<!-- t16&#45;&gt;t22 -->
<g id="edge36" class="edge">
<title>t16&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M524.0378,-234.1499C549.1239,-226.3599 579.5241,-216.9196 609.3422,-207.6602"/>
<polygon fill="#0044a0" stroke="#0044a0" points="610.4833,-210.9707 618.9955,-204.6625 608.4073,-204.2857 610.4833,-210.9707"/>
</g>
<!-- t16&#45;&gt;t24 -->
<g id="edge40" class="edge">
<title>t16&#45;&gt;t24</title>
<path fill="none" stroke="#0044a0" d="M417.6711,-234.1499C397.2337,-226.5285 372.5607,-217.3275 348.2499,-208.2617"/>
<polygon fill="#0044a0" stroke="#0044a0" points="349.1912,-204.8773 338.5986,-204.6625 346.7453,-211.4361 349.1912,-204.8773"/>
</g>
<!-- t16&#45;&gt;t23 -->
<g id="edge45" class="edge">
<title>t16&#45;&gt;t23</title>
<path fill="none" stroke="#0044a0" d="M651.7596,-235.3105C656.5547,-234.9215 661.308,-234.5415 666,-234.1725 811.2137,-222.7545 848.5512,-230.9322 993,-212.1725 1006.176,-210.4614 1019.7432,-208.4558 1033.3753,-206.2619"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1034.0068,-209.7051 1043.3083,-204.6325 1032.8736,-202.7974 1034.0068,-209.7051"/>
</g>
<!-- t21&#45;&gt;t22 -->
<g id="edge40" class="edge"><title>t21&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M888.417,-134.636C863.556,-123.635 835.613,-111.27 809.783,-99.8401"/>
<polygon fill="#0044a0" stroke="#0044a0" points="810.933,-96.5216 800.372,-95.6756 808.1,-102.923 810.933,-96.5216"/>
<g id="edge32" class="edge">
<title>t21&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M1165.0941,-235.3973C1085.9035,-224.7541 997.501,-212.8552 993,-212.1725 980.474,-210.2728 967.615,-208.2703 954.6442,-206.2099"/>
<polygon fill="#0044a0" stroke="#0044a0" points="955.1339,-202.7439 944.7072,-204.6238 954.0305,-209.6564 955.1339,-202.7439"/>
</g>
<!-- t21&#45;&gt;t24 -->
<g id="edge37" class="edge">
<title>t21&#45;&gt;t24</title>
<path fill="none" stroke="#0044a0" d="M1165.2215,-234.9906C1162.1187,-234.699 1159.041,-234.4254 1156,-234.1725 855.1023,-209.1529 777.1841,-244.6523 477,-212.1725 462.026,-210.5524 446.5737,-208.5164 431.0798,-206.2151"/>
<polygon fill="#0044a0" stroke="#0044a0" points="431.3094,-202.7099 420.8968,-204.6663 430.2567,-209.6303 431.3094,-202.7099"/>
</g>
<!-- t21&#45;&gt;t23 -->
<g id="edge42" class="edge">
<title>t21&#45;&gt;t23</title>
<path fill="none" stroke="#0044a0" d="M1274.3771,-234.0471C1268.8557,-227.6412 1262.3807,-220.0px91 1255.8248,-212.5231"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1258.4543,-210.2128 1249.2743,-204.9233 1253.152,-214.783 1258.4543,-210.2128"/>
</g>
<!-- t19&#45;&gt;t22 -->
<g id="edge34" class="edge">
<title>t19&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M260.6159,-410.9505C259.8316,-364.871 259.0249,-246.8273 271,-234.1725 302.6435,-200.7331 431.3919,-218.4498 477,-212.1725 490.4888,-210.316 504.3558,-208.2974 518.3278,-206.1801"/>
<polygon fill="#0044a0" stroke="#0044a0" points="519.1602,-209.5936 528.5161,-204.6219 518.1019,-202.6741 519.1602,-209.5936"/>
</g>
<!-- t19&#45;&gt;t24 -->
<g id="edge38" class="edge">
<title>t19&#45;&gt;t24</title>
<path fill="none" stroke="#0044a0" d="M259.0498,-411.2136C258.0595,-401.7112 256.8826,-389.8585 256,-379.252 251.2976,-322.7418 247.1648,-257.5963 244.6312,-214.9019"/>
<polygon fill="#0044a0" stroke="#0044a0" points="248.1192,-214.5935 244.0377,-204.8165 241.1313,-215.0048 248.1192,-214.5935"/>
</g>
<!-- t19&#45;&gt;t23 -->
<g id="edge43" class="edge">
<title>t19&#45;&gt;t23</title>
<path fill="none" stroke="#0044a0" d="M282.1528,-411.0561C318.8331,-380.6455 397.8323,-320.0855 476,-292.1725 553.979,-264.3269 579.2078,-283.062 661,-270.1725 742.8817,-257.269 762.3086,-248.2304 844,-234.1725 909.9705,-222.82 927.0119,-223.4221 993,-212.1725 1003.5851,-210.368 1014.439,-208.4601 1025.3874,-206.4907"/>
<polygon fill="#0044a0" stroke="#0044a0" points="1026.2474,-209.892 1035.4637,-204.666 1025,-203.004 1026.2474,-209.892"/>
</g>
<!-- t19&#45;&gt;t25 -->
<g id="edge50" class="edge">
<title>t19&#45;&gt;t25</title>
<path fill="none" stroke="#0044a0" d="M342.173,-411.2166C389.8324,-400.6274 451.9326,-386.8297 509.989,-373.9305"/>
<polygon fill="#0044a0" stroke="#0044a0" points="511.0179,-377.2873 520.0207,-371.7016 509.4996,-370.4539 511.0179,-377.2873"/>
</g>
<!-- t20&#45;&gt;t22 -->
<g id="edge33" class="edge">
<title>t20&#45;&gt;t22</title>
<path fill="none" stroke="#0044a0" d="M323.4472,-984.1943C198.6099,-964.2959 37,-926.6746 37,-865.252 37,-865.252 37,-865.252 37,-429.252 37,-211.1917 260.8906,-241.2767 477,-212.1725 490.808,-210.313 505.0108,-208.2741 519.3157,-206.1255"/>
<polygon fill="#0044a0" stroke="#0044a0" points="519.8697,-209.5816 529.2317,-204.6214 518.8199,-202.6608 519.8697,-209.5816"/>
</g>
<!-- t26 -->
<g id="node26" class="node">
<title>t26</title>
<polygon fill="#efa03b" stroke="#000000" points="568.0652,-47.332 735,-20.0049 901.9348,-47.332 901.779,-91.5483 568.221,-91.5483 568.0652,-47.332"/>
<polygon fill="none" stroke="#000000" points="564.0538,-43.9306 735,-15.9468 905.9462,-43.9306 905.7643,-95.5532 564.2357,-95.5532 564.0538,-43.9306"/>
<text text-anchor="middle" x="735" y="-53.5531" font-family="Times,serif" font-size="20.00" fill="#000000">ocrmypdf.pipeline.copy_final</text>
</g>
<!-- t22&#45;&gt;t26 -->
<g id="edge49" class="edge">
<title>t22&#45;&gt;t26</title>
<path fill="none" stroke="#0044a0" d="M735,-124.8984C735,-118.6708 735,-112.2475 735,-105.9711"/>
<polygon fill="#0044a0" stroke="#0044a0" points="738.5001,-105.6074 735,-95.6074 731.5001,-105.6075 738.5001,-105.6074"/>
</g>
<!-- t24&#45;&gt;t26 -->
<g id="edge47" class="edge">
<title>t24&#45;&gt;t26</title>
<path fill="none" stroke="#0044a0" d="M368.6395,-140.6178C427.4047,-127.6148 498.1821,-111.9539 561.7955,-97.8781"/>
<polygon fill="#0044a0" stroke="#0044a0" points="562.9329,-101.2112 571.9406,-95.6333 561.4205,-94.3765 562.9329,-101.2112"/>
</g>
<!-- t23&#45;&gt;t26 -->
<g id="edge48" class="edge">
<title>t23&#45;&gt;t26</title>
<path fill="none" stroke="#0044a0" d="M1095.1818,-140.9007C1037.3475,-127.8387 967.4279,-112.0473 904.6388,-97.8663"/>
<polygon fill="#0044a0" stroke="#0044a0" points="905.1511,-94.3939 894.6256,-95.6048 903.6089,-101.2219 905.1511,-94.3939"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+463 -253
View File
File diff suppressed because it is too large Load Diff
+43 -8
View File
@@ -10,7 +10,7 @@ The disclaimer applies: this software has no warranties of any kind.
PDFs may contain malware
------------------------
PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of packages long and references several annexes each of which are similar in length. PDFs can contain video, audio, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors.
PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of pages long and references several annexes each of which are similar in length. PDFs can contain video, audio, XML, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors.
In short, PDFs `may contain viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`_.
@@ -21,23 +21,58 @@ How OCRmyPDF processes PDFs
OCRmyPDF must open and interpret your PDF in order to insert an OCR layer. First, it runs all PDFs through `qpdf <https://github.com/qpdf/qpdf>`_, a program that repairs PDFs with syntax errors. This is done because, in the author's experience, a significant number of PDFs in the wild especially those created by scanners are not well-formed files. qpdf makes it more likely that OCRmyPDF will succeed, but offers no security guarantees. qpdf is also used to split the PDF into single page PDFs.
After qpdf, OCRmyPDF examines each page using `PyPDF2 <https://github.com/mstamy2/PyPDF2>`_. This library also has no warranties or guarantees.
After qpdf, OCRmyPDF examines each page using `PyPDF2 <https://github.com/mstamy2/PyPDF2>`_. This library also has no warranties or guarantees. OCRmyPDF works with qpdf 5.0 and up, but version 7.0 is recommended because of known security vulnerabilities in early versions.
Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://ghostscript.com/>`_ in ``-dSAFER`` mode.
Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://ghostscript.com/>`_ in ``-dSAFER`` mode.
Depending on the options specified, OCRmyPDF may graft the OCR layer into the existing PDF or it may essentially reconstruct ("re-fry") a visually identical PDF that may be quite different at the binary level. That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
Using OCRmyPDF online
---------------------
Using OCRmyPDF online or as a service
-------------------------------------
OCRmyPDF is not designed to be deployed "as a service", in a setting where a user/attacker could upload a file for OCR processing online. It is not designed to be secure in this case.
OCRmyPDF should not be deployed as a public-facing service, such as a website where a potential attacker could upload a PDF of their choice for OCR. OCRmyPDF is not designed to be secure against PDF malware. Another concern is PDFs specifically designed to be a denial of service attack: PDFs can contain recursive data structures that sometimes send parsers into infinite loops, and issue complex graphics drawing commands.
Setting aside these concerns, a side effect of OCRmyPDF is it may incidentally sanitize PDFs that contain malware. It runs ``qpdf`` to repair the PDF, which could correct malformed PDF structures that are part of an attack. When PDF/A output is selected (the default), the input PDF is partially reconstructed by Ghostscript. When ``--force-ocr`` is used, all pages are rasterized and reconverted to PDF, which could remove malware in embedded images. No guarantees.
OCRmyPDF should be relatively safe to use in a trusted intranet, with some considerations:
Limiting CPU usage
^^^^^^^^^^^^^^^^^^
OCRmyPDF will attempt to use all available CPUs and storage, so executing ``nice ocrmypdf`` or limiting the number of jobs with the ``-j`` argument may ensure the server remains available. Another option would be run OCRmyPDF jobs inside a Docker container, a virtual machine, or a cloud instance, which can impose its own limits on CPU usage and be terminated "from orbit" if it fails to complete.
Temporary storage requirements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OCRmyPDF will use a large amount of temporary storage for its work, proportional to the total number of pixels needed to rasterize the PDF. The raster image of a 8.5×11" color page at 300 DPI takes 25 MB uncompressed; OCRmyPDF saves its intermediates as PNG, but that still means it requires about 9 MB per intermediate based on average compression ratios. Multiple intermediates per page are also required, depending on the command line given. A rule of thumb would be to allow 100 MB of temporary storage per page in a file meaning that a small cloud servers or small VM partitions should be provisioned with plenty of extra space, if say, a 500 page file might be sent.
To check temporary storage usage on actual files, run ``ocrmypdf -k ...`` which will preserve and print the path to temporary storage when the job is done.
To change where temporary files are stored, change the ``TMPDIR`` environment variable for ocrmypdf's environment. (Python's ``tempfile.gettempdir()`` returns the root directory in which temporary files will be stored.) For example, one could redirect ``TMPDIR`` to a large RAM disk to avoid wear on HDD/SSD and potentially improve performance. On Amazon Web Services, ``TMPDIR`` can be set to `empheral storage <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html>`_.
Timeouts
^^^^^^^^
To prevent excessively long OCR jobs consider setting ``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big`` is particularly helpful if your PDFs include documents such as reports on standard page sizes with large images attached - often large images are not worth OCR'ing anyway.
Commercial alternatives
^^^^^^^^^^^^^^^^^^^^^^^
The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation.
Abbyy Cloud OCR is a viable commercial alternative with a web services API.
Abbyy Cloud OCR is a viable commercial alternative with a web services API. The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation.
Password protection, digital signatures and certification
---------------------------------------------------------
OCRmyPDF cannot remove password protection from a PDF. ``qpdf``, one of its dependencies, has this capability. After OCR is applied, password protection is not permitted on PDF/A documents but the file can be converted to regular PDF.
Password protected PDFs usually have two passwords, and owner and user password. When the user password is set to empty, PDF readers will open the file automatically and marked it as "(SECURED)". While not as reliable as a digital signature, this indicates that whoever set the password approved of the file at that time. When the user password is set, the document cannot be viewed without the password.
Either way, OCRmyPDF does not remove passwords from PDFs and exits with an error on encountering them.
``qpdf``, one of OCRmyPDF's dependencies, can remove passwords. If the owner and user password are set, a password is required for ``qpdf``. If only the owner password is set, then the password can be stripped, even if one does not have the owner password.
After OCR is applied, password protection is not permitted on PDF/A documents but the file can be converted to regular PDF.
Many programs exist which are capable of inserting an image of someone's signature. On its own, this offers no security guarantees. It is trivial to remove the signature image and apply it to other files. This practice offers no real security.
-9
View File
@@ -1,9 +0,0 @@
# © 2017 James R. Barlow: github.com/jbarlow83
import pkg_resources
PROGRAM_NAME = 'ocrmypdf'
VERSION = pkg_resources.get_distribution('ocrmypdf').version
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
"""Wrappers to manage subprocess calls"""
import os
def get_program(name):
envvar = 'OCRMYPDF_' + name.upper()
return os.environ.get(envvar, name)
-137
View File
@@ -1,137 +0,0 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
from subprocess import CalledProcessError, STDOUT, PIPE, run, check_output
from functools import lru_cache
import sys
import os
import re
from ..exceptions import InputFileError, SubprocessOutputError, \
MissingDependencyError, EncryptedPdfError
from . import get_program
@lru_cache(maxsize=1)
def version():
args_qpdf = [
get_program('qpdf'),
'--version'
]
try:
p = run(args_qpdf, universal_newlines=True, stderr=STDOUT,
stdout=PIPE)
except CalledProcessError as e:
print("Could not find qpdf executable on system PATH.",
file=sys.stderr)
raise MissingDependencyError() from e
qpdf_version = re.match(r'qpdf version (.+)', p.stdout).group(1)
return qpdf_version
def check(input_file, log=None):
args_qpdf = [
get_program('qpdf'),
'--check',
input_file
]
if log is None:
import logging as log
try:
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
except CalledProcessError as e:
if e.returncode == 2:
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
log.info("qpdf --check returned warnings:")
log.info(e.output)
else:
log.warning(e.output)
return False
return True
def _probably_encrypted(e):
"""qpdf can report a false positive "file is encrypted" message for damaged
files - suppress this"""
return e.returncode == 2 and \
'invalid password' in e.output and \
'file is damaged' not in e.output
def repair(input_file, output_file, log):
args_qpdf = [
get_program('qpdf'), input_file, output_file
]
try:
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
except CalledProcessError as e:
if e.returncode == 3 and e.output.find("operation succeeded"):
log.debug('qpdf found and fixed errors: ' + e.output)
log.debug(e.output)
return
if _probably_encrypted(e):
log.error("{0}: this PDF is password-protected - password must "
"be removed for OCR".format(input_file))
raise EncryptedPdfError() from e
elif e.returncode == 2:
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
log.error("Details: " + e.output)
raise InputFileError() from e
else:
log.error("{0}: unknown error".format(
input_file))
log.error(e.output)
raise SubprocessOutputError() from e
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)
raise InputFileError() from e
return int(pages)
def split_pages(input_file, work_folder, npages):
"""Split multipage PDF into individual pages.
Incredibly enough, this multiple process approach is about 70 times
faster than using Ghostscript.
"""
for n in range(int(npages)):
args_qpdf = [
get_program('qpdf'), input_file,
'--pages', input_file, '{0}'.format(n + 1), '--',
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
]
run(args_qpdf, check=True)
def merge(input_files, output_file, min_version=None):
"""Merge the list of input files (all filenames) into the output file.
The input files may contain one or more pages.
"""
version_arg = ['--min-version={}'.format(min_version)] \
if min_version else []
args_qpdf = [
get_program('qpdf')
] + version_arg + [
input_files[0], '--pages'
] + input_files + ['--', output_file]
run(args_qpdf, check=True)
-230
View File
@@ -1,230 +0,0 @@
#!/usr/bin/env python3
##############################################################################
# Copyright (c) 2013-14: fritz-hh from Github
# (https://github.com/fritz-hh)
#
# Copyright (c) 2010: Jonathan Brinley from Github
# (https://github.com/jbrinley/HocrConverter)
# Initial version by Jonathan Brinley, jonathanbrinley@gmail.com
##############################################################################
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch
from xml.etree import ElementTree
from PIL import Image
from collections import namedtuple
import re
import argparse
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
class HocrTransformError(Exception):
pass
class HocrTransform():
"""
A class for converting documents from the hOCR format.
For details of the hOCR format, see:
http://docs.google.com/View?docid=dfxcv4vc_67g844kf
"""
def __init__(self, hocrFileName, dpi):
self.dpi = dpi
self.boxPattern = re.compile(r'bbox((\s+\d+){4})')
self.hocr = ElementTree.parse(hocrFileName)
# if the hOCR file has a namespace, ElementTree requires its use to
# find elements
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
self.xmlns = ''
if matches:
self.xmlns = matches.group(1)
# get dimension in pt (not pixel!!!!) of the OCRed image
self.width, self.height = None, None
for div in self.hocr.findall(
".//%sdiv[@class='ocr_page']" % (self.xmlns)):
coords = self.element_coordinates(div)
pt_coords = self.pt_from_pixel(coords)
self.width = pt_coords.x2 - pt_coords.x1
self.height = pt_coords.y2 - pt_coords.y1
# there shouldn't be more than one, and if there is, we don't want
# it
break
if self.width is None or self.height is None:
raise HocrTransformError("hocr file is missing page dimensions")
def __str__(self):
"""
Return the textual content of the HTML body
"""
if self.hocr is None:
return ''
body = self.hocr.find(".//%sbody" % (self.xmlns))
if body:
return self._get_element_text(body)
else:
return ''
def _get_element_text(self, element):
"""
Return the textual content of the element and its children
"""
text = ''
if element.text is not None:
text += element.text
for child in element.getchildren():
text += self._get_element_text(child)
if element.tail is not None:
text += element.tail
return text
def element_coordinates(self, element):
"""
Returns a tuple containing the coordinates of the bounding box around
an element
"""
out = (0, 0, 0, 0)
if 'title' in element.attrib:
matches = self.boxPattern.search(element.attrib['title'])
if matches:
coords = matches.group(1).split()
out = Rect._make(int(coords[n]) for n in range(4))
return out
def pt_from_pixel(self, pxl):
"""
Returns the quantity in PDF units (pt) given quantity in pixels
"""
return Rect._make(
(c / self.dpi * inch) for c in pxl)
def replace_unsupported_chars(self, s):
"""
Given an input string, returns the corresponding string that:
- is available in the helvetica facetype
- does not contain any ligature (to allow easy search in the PDF file)
"""
# The 'u' before the character to replace indicates that it is a
# unicode character
s = s.replace(u"", "fl")
s = s.replace(u"", "fi")
return s
def to_pdf(self, outFileName, imageFileName=None, showBoundingboxes=False,
fontname="Helvetica", invisibleText=False):
"""
Creates a PDF file with an image superimposed on top of the text.
Text is positioned according to the bounding box of the lines in
the hOCR file.
The image need not be identical to the image used to create the hOCR
file.
It can have a lower resolution, different color mode, etc.
"""
# create the PDF file
# page size in points (1/72 in.)
pdf = Canvas(
outFileName, pagesize=(self.width, self.height), pageCompression=1)
# draw bounding box for each paragraph
# light blue for bounding box of paragraph
pdf.setStrokeColorRGB(0, 1, 1)
# light blue for bounding box of paragraph
pdf.setFillColorRGB(0, 1, 1)
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.findall(
".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
elemtxt = self._get_element_text(elem).rstrip()
if len(elemtxt) == 0:
continue
pxl_coords = self.element_coordinates(elem)
pt = self.pt_from_pixel(pxl_coords)
# draw the bbox border
if showBoundingboxes:
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
fill=1)
# check if element with class 'ocrx_word' are available
# otherwise use 'ocr_line' as fallback
elemclass = "ocr_line"
if self.hocr.find(
".//%sspan[@class='ocrx_word']" % (self.xmlns)) is not None:
elemclass = "ocrx_word"
# itterate all text elements
# light green for bounding box of word/line
pdf.setStrokeColorRGB(1, 0, 0)
pdf.setLineWidth(0.5) # bounding box line width
pdf.setDash(6, 3) # bounding box is dashed
pdf.setFillColorRGB(0, 0, 0) # text in black
for elem in self.hocr.findall(
".//%sspan[@class='%s']" % (self.xmlns, elemclass)):
elemtxt = self._get_element_text(elem).rstrip()
elemtxt = self.replace_unsupported_chars(elemtxt)
if len(elemtxt) == 0:
continue
pxl_coords = self.element_coordinates(elem)
pt = self.pt_from_pixel(pxl_coords)
# draw the bbox border
if showBoundingboxes:
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
fill=0)
text = pdf.beginText()
fontsize = pt.y2 - pt.y1
text.setFont(fontname, fontsize)
if invisibleText:
text.setTextRenderMode(3) # Invisible (indicates OCR text)
# set cursor to bottom left corner of bbox (adjust for dpi)
text.setTextOrigin(pt.x1, self.height - pt.y2)
# scale the width of the text to fill the width of the bbox
text.setHorizScale(
100 * (pt.x2 - pt.x1) / pdf.stringWidth(
elemtxt, fontname, fontsize))
# write the text to the page
text.textLine(elemtxt)
pdf.drawText(text)
# put the image on the page, scaled to fill the page
if imageFileName is not None:
pdf.drawImage(imageFileName, 0, 0,
width=self.width, height=self.height)
# finish up the page and save it
pdf.showPage()
pdf.save()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
parser.add_argument('-b', '--boundingboxes', action="store_true",
default=False, help='Show bounding boxes borders')
parser.add_argument('-r', '--resolution', type=int,
default=300,
help='Resolution of the image that was OCRed')
parser.add_argument('-i', '--image', default=None,
help='Path to the image to be placed above the text')
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument(
'outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(args.hocrfile, args.resolution)
hocr.to_pdf(args.outputfile, args.image, args.boundingboxes)
-2
View File
@@ -1,2 +0,0 @@
"""Bindings to external libraries"""
# © 2017 James R. Barlow: github.com/jbarlow83
-538
View File
@@ -1,538 +0,0 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
from subprocess import Popen, PIPE
from decimal import Decimal
from math import hypot, isclose
import re
import sys
import PyPDF2 as pypdf
from collections import namedtuple
import warnings
warnings.warn("ocrmypdf.pageinfo is deprecated'; use ocrmypdf.pdfinfo",
DeprecationWarning)
matrix_mult = pypdf.pdf.utils.matrixMultiply
FRIENDLY_COLORSPACE = {
'/DeviceGray': 'gray',
'/CalGray': 'gray',
'/DeviceRGB': 'rgb',
'/CalRGB': 'rgb',
'/DeviceCMYK': 'cmyk',
'/Lab': 'lab',
'/ICCBased': 'icc',
'/Indexed': 'index',
'/Separation': 'sep',
'/DeviceN': 'devn',
'/Pattern': '-',
'/G': 'gray', # Abbreviations permitted in inline images
'/RGB': 'rgb',
'/CMYK': 'cmyk',
'/I': 'index',
}
FRIENDLY_ENCODING = {
'/CCITTFaxDecode': 'ccitt',
'/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 = {
'gray': 1,
'rgb': 3,
'cmyk': 4,
'lab': 3,
'index': 1
}
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
def _matrix_from_shorthand(shorthand):
"""Convert from PDF matrix shorthand to full matrix
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
since the last column is always (0, 0, 1).
"""
a, b, c, d, e, f = map(float, shorthand)
return ((a, b, 0),
(c, d, 0),
(e, f, 1))
def _shorthand_from_matrix(matrix):
"""Convert from transformation matrix to PDF shorthand."""
a, b = matrix[0][0], matrix[0][1]
c, d = matrix[1][0], matrix[1][1]
e, f = matrix[2][0], matrix[2][1]
return tuple(map(float, (a, b, c, d, e, f)))
def _is_unit_square(shorthand):
values = map(float, shorthand)
pairwise = zip(values, UNIT_SQUARE)
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
XobjectSettings = namedtuple('XobjectSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['settings', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo', ['xobject_settings', 'inline_images'])
def _normalize_stack(operations):
"""Fix runs of qQ's in the stack
For some reason PyPDF2 converts runs of qqq, QQ, QQQq, etc. into single
operations. Break this silliness up and issue each stack operation
individually so we don't lose count.
"""
for operands, command in operations:
if re.match(br'Q*q+$', command): # Zero or more Q, one or more q
for char in command: # Split into individual bytes
yield ([], bytes([char])) # Yield individual bytes
else:
yield (operands, command)
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
"""Interpret the PDF content stream
The stack represents the state of the PDF graphics stack. We are only
interested in the current transformation matrix (CTM) so we only track
this object; a full implementation would need to track many other items.
The CTM is initialized to the mapping from user space to device space.
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
to the transformation to device space. For example if set to
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
image there should be a 'cm' that sets up an image coordinate system
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
page.
PDF units suit our needs so we initialize ctm to the identity matrix.
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
"""
operations = contentstream.operations
stack = []
ctm = _matrix_from_shorthand(initial_shorthand)
xobject_settings = []
inline_images = []
for n, op in enumerate(_normalize_stack(operations)):
operands, command = op
if command == b'q':
stack.append(ctm)
if len(stack) > 32:
raise RuntimeError(
"PDF graphics stack overflow, command %i" % n)
elif command == b'Q':
try:
ctm = stack.pop()
except IndexError:
raise RuntimeError(
"PDF graphics stack underflow, command %i" % n)
elif command == b'cm':
ctm = matrix_mult(
_matrix_from_shorthand(operands), ctm)
elif command == b'Do':
image_name = operands[0]
settings = XobjectSettings(
name=image_name, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
xobject_settings.append(settings)
elif command == b'INLINE IMAGE':
settings = operands['settings']
inline = InlineSettings(
settings=settings, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
inline_images.append(inline)
return ContentsInfo(
xobject_settings=xobject_settings,
inline_images=inline_images)
def _get_dpi(ctm_shorthand, image_size):
"""Given the transformation matrix and image size, find the image DPI.
PDFs do not include image resolution information within image data.
Instead, the PDF page content stream describes the location where the
image will be rasterized, and the effective resolution is the ratio of the
pixel size to raster target size.
Normally a scanned PDF has the paper size set appropriately but this is
not guaranteed. The most common case is a cropped image will change the
page size (/CropBox) without altering the page content stream. That means
it is not sufficient to assume that the image fills the page, even though
that is the most common case.
A PDF image may be scaled (always), cropped, translated, rotated in place
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
be expressed, that is, it is not necessary to consider distortions where
the effective DPI varies with position.
To determine the image scale, transform an offset axis vector v0 (0, 0),
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
which gives the dimensions of the image in PDF units. From there we can
compare to actual image dimensions. PDF uses
row vector * matrix_tranposed unlike the traditional
matrix * column vector.
The offset, width and height vectors can be combined in a matrix and
multiplied by the transform matrix. Then we want to calculated
magnitude(width_vector - offset_vector)
and
magnitude(height_vector - offset_vector)
When the above is worked out algebraically, the effect of translation
cancels out, and the vector magnitudes become functions of the nonzero
transformation matrix indices. The results of the derivation are used
in this code.
pdfimages -list does calculate the DPI in some way that is not completely
naive, but it does not get the DPI of rotated images right, so cannot be
used anymore to validate this. Photoshop works, or using Acrobat to
rotate the image back to normal.
It does not matter if the image is partially cropped, or even out of the
/MediaBox.
"""
a, b, c, d, _, _ = ctm_shorthand
# Calculate the width and height of the image in PDF units
image_drawn_width = hypot(a, b)
image_drawn_height = hypot(c, d)
# The scale of the image is pixels per 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_inline_images(contentsinfo):
"Find inline images in the contentstream"
for n, inline in enumerate(contentsinfo.inline_images):
image = {}
image['name'] = str('inline-%02d' % n)
image['width'] = inline.settings['/W']
image['height'] = inline.settings['/H']
if '/BPC' in inline.settings:
image['bpc'] = inline.settings['/BPC']
else:
image['bpc'] = 8
if '/CS' in inline.settings:
image['color'] = FRIENDLY_COLORSPACE.get(inline.settings['/CS'], '-')
else:
image['color'] = '-'
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
if '/F' in 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 _image_xobjects(container):
"""Search for all XObject-based images in the container
Usually the container is a page, but it could also be a Form XObject
that contains images. Filter out the Form XObjects which are dealt with
elsewhere.
Generate a sequence of tuples (image, xobj container), where container,
where xobj is the name of the object and image is the object itself,
since the object does not know its own name.
"""
if '/Resources' not in container:
return
resources = container['/Resources']
if '/XObject' not in resources:
return
for xobj in resources['/XObject']:
candidate = resources['/XObject'][xobj]
if candidate['/Subtype'] == '/Image':
image = candidate
yield (image, xobj)
def _find_regular_images(container, contentsinfo):
"""Find images stored in the container's /Resources /XObject
Usually the container is a page, but it could also be a Form XObject
that contains images.
Generates images with their DPI at time of drawing.
"""
for pdfimage, xobj in _image_xobjects(container):
image = {}
image['name'] = xobj
image['width'] = pdfimage['/Width']
image['height'] = pdfimage['/Height']
if '/BitsPerComponent' in pdfimage:
image['bpc'] = pdfimage['/BitsPerComponent']
else:
image['bpc'] = 8
# Fixme: this is incorrectly treats explicit masks as stencil masks,
# but good enough for now. Explicit masks have /ImageMask true but are
# never called for in content stream, instead are drawn as a /Mask on
# other images. For our purposes finding out the details of /Mask
# will seldom matter.
if '/ImageMask' in pdfimage:
image['type'] = 'stencil' if pdfimage['/ImageMask'].value \
else 'image'
else:
image['type'] = 'image'
if '/Filter' in pdfimage:
filter_ = pdfimage['/Filter']
if isinstance(filter_, pypdf.generic.ArrayObject):
filter_ = filter_[0]
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
else:
image['enc'] = 'image'
if '/ColorSpace' in pdfimage:
cs = pdfimage['/ColorSpace']
if isinstance(cs, pypdf.generic.ArrayObject):
cs = cs[0]
image['color'] = FRIENDLY_COLORSPACE.get(cs, '-')
else:
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
# Bit of a hack... infer grayscale if component count is uncertain
# but encoding must be monochrome. This happens if a monochrome image
# has an ICC profile attached. Better solution would be to examine
# the ICC profile.
if image['comp'] == '?' and image['enc'] in ('ccitt', 'jbig2'):
image['comp'] = FRIENDLY_COMP['gray']
image['dpi_w'] = image['dpi_h'] = 0
for xobj in contentsinfo.xobject_settings:
# Loop in case the same image is display multiple times on a page
if xobj.name != image['name']:
continue
if xobj.stack_depth == 0 and _is_unit_square(xobj.shorthand):
# At least one PDF in the wild (and test suite) draws an image
# when the graphics stack depth is 0, meaning that the image
# gets drawn into a square of 1x1 PDF units (or 1/72",
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
# these from our DPI calculation for the page.
continue
dpi_w, dpi_h = _get_dpi(
xobj.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))
DPI_PREC = Decimal('1.000')
dpi = Decimal(image['dpi_w'] * image['dpi_h']).sqrt()
image['dpi_w'] = Decimal(image['dpi_w']).quantize(DPI_PREC)
image['dpi_h'] = Decimal(image['dpi_h']).quantize(DPI_PREC)
image['dpi'] = dpi.quantize(DPI_PREC)
yield image
def _find_form_xobject_images(pdf, container, contentsinfo):
"""Find any images that are in Form XObjects in the container
The container may be a page, or a parent Form XObject.
"""
if '/Resources' not in container:
return
resources = container['/Resources']
if '/XObject' not in resources:
return
for xobj in resources['/XObject']:
candidate = resources['/XObject'][xobj]
if candidate['/Subtype'] != '/Form':
continue
form_xobject = candidate
for settings in contentsinfo.xobject_settings:
if settings.name != xobj:
continue
# Find images once for each time this Form XObject is drawn.
# This could be optimized to cache the multiple drawing events
# but in practice both Form XObjects and multiple drawing of the
# same object are both very rare.
ctm_shorthand = settings.shorthand
yield from _find_images(pdf, form_xobject, ctm_shorthand)
def _find_images(pdf, container, shorthand=None):
"""Find all individual instances of images drawn in the container
Usually the container is a page, but it may also be a Form XObject.
On a typical page images are stored inline or as regular images
in an XObject.
Form XObjects may include inline images, XObject images,
and recursively, other Form XObjects; and also vector drawing commands.
Every instance of an image being drawn somewhere is flattened and
treated as a unique image, since if the same image is drawn multiple times
on one page it may be drawn at differing resolutions, and our objective
is to find the resolution at which the page can be rastered without
downsampling.
"""
if container.get('/Type') == '/Page' and '/Contents' in container:
# For a /Page the content stream is attached to the page's /Contents
page = container
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
initial_shorthand = shorthand or UNIT_SQUARE
elif container.get('/Type') == '/XObject' and \
container['/Subtype'] == '/Form':
# For a Form XObject that content stream is attached to the XObject
contentstream = pypdf.pdf.ContentStream(container, pdf)
# Set the CTM to the state it was when the "Do" operator was
# encountered that is drawing this instance of the Form XObject
ctm = _matrix_from_shorthand(shorthand or UNIT_SQUARE)
# A Form XObject may provide its own matrix to map form space into
# user space. Get this if one exists
form_matrix = _matrix_from_shorthand(
container.get('/Matrix', UNIT_SQUARE))
# Concatenate form matrix with CTM to ensure CTM is correct for
# drawing this instance of the XObject
ctm = matrix_mult(form_matrix, ctm)
initial_shorthand = _shorthand_from_matrix(ctm)
else:
return
contentsinfo = _interpret_contents(contentstream, initial_shorthand)
yield from _find_inline_images(contentsinfo)
yield from _find_regular_images(container, contentsinfo)
yield from _find_form_xobject_images(pdf, container, contentsinfo)
def _page_has_text(pdf, page):
if not '/Contents' in page:
return False
# Simple test
text = page.extractText()
if text.strip() != '':
return True
# More nuanced test to deal with quirks of Tesseract PDF generation
# Check if there's a Glyphless font
try:
font = page['/Resources']['/Font']
except KeyError:
pass
else:
font_objects = list(font.keys())
for font_object in font_objects:
basefont = font[font_object]['/BaseFont']
if basefont.endswith('GlyphLessFont'):
return True
return False
def _pdf_get_pageinfo(infile, pageno: int):
pageinfo = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
pdf = pypdf.PdfFileReader(infile)
page = pdf.pages[pageno]
pageinfo['has_text'] = _page_has_text(pdf, page)
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)
try:
pageinfo['rotate'] = int(page['/Rotate'])
except KeyError:
pageinfo['rotate'] = 0
pageinfo['images'] = [im for im in
_find_images(pdf, page)]
if pageinfo['images']:
xres = max(image['dpi_w'] for image in pageinfo['images'])
yres = max(image['dpi_h'] for image in pageinfo['images'])
pageinfo['xres'], pageinfo['yres'] = xres, yres
pageinfo['width_pixels'] = \
int(round(xres * pageinfo['width_inches']))
pageinfo['height_pixels'] = \
int(round(yres * pageinfo['height_inches']))
return pageinfo
def pdf_get_all_pageinfo(infile):
pdf = pypdf.PdfFileReader(infile)
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()
+4 -3
View File
@@ -2,8 +2,9 @@
# setup.py lists a separate set of requirements that are looser to simplify
# installation
ruffus == 2.6.3
Pillow == 4.1.1
Pillow == 5.1.0
reportlab == 3.4.0
PyPDF2 == 1.26.0
img2pdf == 0.2.3
cffi == 1.10.0
img2pdf == 0.2.4
cffi == 1.11.5
PyMuPDF == 1.12.5
+3 -1
View File
@@ -11,4 +11,6 @@ ignore =
[tool:pytest]
norecursedirs = lib .pc .git output cache resources
testpaths = tests
addopts = -n auto
[metadata]
license_file = LICENSE
+41 -15
View File
@@ -1,6 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# © 2015 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function, unicode_literals
@@ -171,13 +186,10 @@ if not forced and command.startswith('install') or \
)
check_external_program(
program='qpdf',
need_version='5.1.1', # limited by Travis CI / Ubuntu 14.04 backports
need_version='7.0.0', # test suite known to fail on 5.1.1
package='qpdf',
version_check_args=['--version']
)
else:
print('Skipping external program tests because of --force')
if 'upload' in sys.argv[1:]:
print('Use twine to upload the package - setup.py upload is insecure')
@@ -185,14 +197,20 @@ if 'upload' in sys.argv[1:]:
tests_require = open('test_requirements.txt').read().splitlines()
def readme():
with open('README.rst') as f:
return f.read()
setup(
name='ocrmypdf',
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
long_description=readme(),
url='https://github.com/jbarlow83/OCRmyPDF',
author='James R. Barlow',
author_email='jim@purplerock.ca',
license='MIT',
packages=find_packages(exclude=["tests", "tests.*"]),
packages=find_packages('src', exclude=["tests", "tests.*"]),
package_dir={'': 'src'},
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
classifiers=[
"Programming Language :: Python :: 3.5",
@@ -202,7 +220,7 @@ setup(
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX",
"Operating System :: POSIX :: BSD",
@@ -211,22 +229,30 @@ setup(
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
],
python_requires=' >= 3.5',
setup_requires=[
'cffi >= 1.9.1', # to build the leptonica module
'pytest-runner', # to enable python setup.py test
'setuptools_scm', # so that version will work
'cffi>=1.9.1' # to build the leptonica module
],
use_scm_version={'version_scheme': 'post-release'},
cffi_modules=[
'ocrmypdf/lib/compile_leptonica.py:ffibuilder'
'src/ocrmypdf/lib/compile_leptonica.py:ffibuilder'
],
install_requires=[
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
'Pillow>=4.0.0', # Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
'reportlab>=3.3.0', # oldest released version with sane image handling
'PyPDF2>=1.26', # pure Python, so track HEAD closely
'img2pdf>=0.2.3', # pure Python, so track HEAD closely
'cffi>=1.9.1' # must be a setup and install requirement
'cffi >= 1.9.1', # must be a setup and install requirement
'defusedxml >= 0.5.0', # pure Python, so track HEAD closely
'img2pdf >= 0.2.4', # pure Python, so track HEAD closely
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
'PyPDF2 >= 1.26', # pure Python, so track HEAD closely
'reportlab >= 3.3.0', # oldest released version with sane image handling
'ruffus == 2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
],
extras_require={
'fitz': ['PyMuPDF >= 1.12.5'] # for table of contents bug
},
tests_require=tests_require,
entry_points={
'console_scripts': [
+25
View File
@@ -0,0 +1,25 @@
# © 2017 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import pkg_resources
PROGRAM_NAME = 'ocrmypdf'
# Official PEP 396
__version__ = pkg_resources.get_distribution('ocrmypdf').version
VERSION = __version__
+274 -135
View File
@@ -1,37 +1,54 @@
#!/usr/bin/env python3
# © 2015-17 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from contextlib import suppress
from tempfile import mkdtemp
from collections.abc import Sequence
from pathlib import Path
import sys
import os
import re
import shutil
import warnings
import multiprocessing
import atexit
import textwrap
import img2pdf
import logging
import argparse
import PyPDF2 as pypdf
from PIL import Image
import PIL
import ruffus.ruffus_exceptions as ruffus_exceptions
import ruffus.cmdline as cmdline
import ruffus.proxy_logger as proxy_logger
from .pipeline import JobContext, JobContextManager, re_symlink, \
from .pipeline import JobContext, JobContextManager, \
cleanup_working_files, build_pipeline
from .pdfa import file_claims_pdfa
from .helpers import is_iterable_notstr, re_symlink, is_file_writable
from .exec import tesseract, qpdf
from .helpers import is_iterable_notstr, re_symlink, is_file_writable, \
available_cpu_count
from .exec import tesseract, qpdf, ghostscript
from .lib import fitz
from . import PROGRAM_NAME, VERSION
from .exceptions import *
from .exceptions import ExitCode, ExitCodeException, MissingDependencyError, \
InputFileError, BadArgsError, OutputFileAccessError
from . import exceptions as ocrmypdf_exceptions
from ._unicodefun import verify_python3_env
warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
@@ -53,6 +70,11 @@ def complain(message):
if 'IDE_PROJECT_ROOTS' in os.environ:
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
# --------
# Critical environment tests
verify_python3_env()
if tesseract.version() < MINIMUM_TESS_VERSION:
complain(
"Please install tesseract {0} or newer "
@@ -134,12 +156,15 @@ parser.add_argument(
'--image-dpi', metavar='DPI', type=int,
help="For input image instead of PDF, use this DPI instead of file's.")
parser.add_argument(
'--output-type', choices=['pdfa', 'pdf'], default='pdfa',
'--output-type', choices=['pdfa', 'pdf', 'pdfa-1', 'pdfa-2'],
default='pdfa',
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
"long term archiving (default, recommended) but may not suitable "
"for users who want their file altered as little as possible. 'pdfa' "
"also has problems with full Unicode text. 'pdf' attempts to "
"preserve file contents as much as possible.")
"preserve file contents as much as possible. 'pdf-a1' creates a "
"PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'."
)
# Use null string '\0' as sentinel to indicate the user supplied no argument,
# since that is the only invalid character for filepaths on all platforms
@@ -236,6 +261,11 @@ ocrsettings.add_argument(
advanced = parser.add_argument_group(
"Advanced",
"Advanced options to control Tesseract's OCR behavior")
advanced.add_argument(
'--max-image-mpixels', action='store', type=float, metavar='MPixels',
help="Set maximum number of pixels to unpack before treating an image as a "
"decompression bomb",
default=128.0)
advanced.add_argument(
'--tesseract-config', action='append', metavar='CFG', default=[],
help="Additional Tesseract configuration files -- see documentation")
@@ -254,7 +284,7 @@ advanced.add_argument(
)
advanced.add_argument(
'--pdf-renderer',
choices=['auto', 'tesseract', 'hocr', 'tess4', 'sandwich'], default='auto',
choices=['auto', 'tesseract', 'hocr', 'sandwich'], default='auto',
help="Choose OCR PDF renderer - the default option is to let OCRmyPDF "
"choose."
"auto - let OCRmyPDF choose; "
@@ -263,7 +293,6 @@ advanced.add_argument(
"tesseract - gives better results for non-Latin languages and "
"Tesseract older than 3.05.01 but has problems with some versions "
" of Ghostscript; deprecated"
"tess4 - deprecated alias for 'sandwich'"
)
advanced.add_argument(
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
@@ -293,6 +322,14 @@ advanced.add_argument(
advanced.add_argument(
'--user-patterns', metavar='FILE',
help="Specify the location of the Tesseract user patterns file.")
advanced.add_argument(
'--skip-repair', action='store_true',
help="Normally OCRmyPDF automatically repairs PDFs using qpdf before "
"processing. If you have already run qpdf or a similar program "
"that repairs PDF errors, you can tell OCRmyPDF to skip repair with "
"this option. This may be helpful in batch processing where all "
"files are repaired prior to OCR occurs, since repair is single "
"threaded and time consuming for large files.")
debugging = parser.add_argument_group(
"Debugging",
@@ -316,13 +353,14 @@ def check_options_languages(options, _log):
if '+' in options.language[0]:
options.language = options.language[0].split('+')
if not set(options.language).issubset(tesseract.languages()):
languages = set(options.language)
if not languages.issubset(tesseract.languages()):
msg = (
"The installed version of tesseract does not have language "
"data for the following requested languages: \n")
for lang in (set(options.language) - tesseract.languages()):
for lang in (languages - tesseract.languages()):
msg += lang + '\n'
raise argparse.ArgumentError(None, msg)
raise MissingDependencyError(msg)
def check_options_output(options, log):
@@ -332,19 +370,25 @@ def check_options_output(options, log):
else:
options.pdf_renderer = 'hocr'
if options.pdf_renderer == 'tess4':
log.warning("The 'tess4' PDF renderer has been renamed to 'sandwich'. "
"Please use --pdf-renderer=sandwich.")
options.pdf_renderer = 'sandwich'
if options.pdf_renderer == 'sandwich' and not tesseract.has_textonly_pdf():
raise MissingDependencyError(
"The 'sandwich' renderer requires Tesseract 3.05.01 or newer; "
"or Tesseract 4.00 alpha newer than February 2017.")
if options.pdf_renderer == 'tesseract':
log.warning("The 'tesseract' PDF renderer is deprecated.")
if tesseract.version() < '3.05' and options.output_type == 'pdfa':
if tesseract.version() < '3.05' and \
options.output_type.startswith('pdfa'):
log.warning(
"For best results use --pdf-renderer=tesseract "
"--output-type=pdf to disable PDF/A generation via "
"Ghostscript, which is known to corrupt the OCR text of "
"some PDFs produced your version of Tesseract.")
elif tesseract.has_textonly_pdf():
log.warning(
"The argument --pdf-renderer=tesseract provides support for "
"versions of tesseract older than your version. For best "
"results omit this argument and let OCRmyPDF choose the "
"best available renderer.")
if options.debug_rendering and options.pdf_renderer != 'hocr':
log.info(
@@ -397,9 +441,9 @@ def check_options_ocr_behavior(options, log):
# if options.redo_ocr and (options.skip_text or options.force_ocr):
# raise argparse.ArgumentError(
# "Error: --redo-ocr and other OCR options are incompatible.")
languages = set(options.language)
if options.pdf_renderer == 'hocr' and \
not set(options.language).issubset(HOCR_OK_LANGS):
not languages.issubset(HOCR_OK_LANGS):
msg = (
"The 'hocr' PDF renderer is known to cause problems with one "
"or more of the languages in your document. ")
@@ -412,32 +456,38 @@ def check_options_ocr_behavior(options, log):
"Use --pdf-renderer tesseract --output-type pdf to avoid "
"this issue")
log.warning(msg)
elif ghostscript.version() < '9.20' and \
not languages.issubset(HOCR_OK_LANGS) \
and options.output_type != 'pdf':
msg = (
"The installed version of Ghostscript does not work correctly "
"with the OCR languages you specified. Use --output-type pdf or "
"upgrade to Ghostscript 9.20 or later to avoid this issue.")
msg += "Found Ghostscript {}".format(ghostscript.version())
log.warning(msg)
def check_options_advanced(options, log):
if tesseract.v4():
log.info(
"Tesseract v4.x.alpha found. OCRmyPDF support is experimental.")
if options.tesseract_oem and not tesseract.v4():
log.warning(
"--tesseract-oem requires Tesseract 4.x -- argument ignored")
if options.pdf_renderer == 'tess4' and not tesseract.has_textonly_pdf():
if options.pdf_renderer == 'sandwich' and not tesseract.has_textonly_pdf():
raise MissingDependencyError(
"--pdf-renderer tess4 requires Tesseract 4.x "
"--pdf-renderer sandwich requires Tesseract 4.x "
"commit 3d9fb3b or later")
if options.pdfa_image_compression != 'auto' and \
options.output_type != 'pdfa':
options.output_type.startswith('pdfa'):
log.warning(
"--pdfa-image-compression argument has no effect when "
"--output-type is not 'pdfa'"
"--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'"
)
def check_options_metadata(options, log):
import unicodedata
metadata = [options.title, options.author, options.keywords,
options.subject]
for s in (m for m in metadata if m):
docinfo = [options.title, options.author, options.keywords,
options.subject]
for s in (m for m in docinfo if m):
for c in s:
if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000:
raise ValueError(
@@ -447,6 +497,12 @@ def check_options_metadata(options, log):
))
def check_options_pillow(options, log):
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1000000)
if PIL.Image.MAX_IMAGE_PIXELS == 0:
PIL.Image.MAX_IMAGE_PIXELS = None
def check_options(options, log):
try:
check_options_languages(options, log)
@@ -456,6 +512,7 @@ def check_options(options, log):
check_options_preprocessing(options, log)
check_options_ocr_behavior(options, log)
check_options_advanced(options, log)
check_options_pillow(options, log)
except ValueError as e:
log.error(e)
sys.exit(ExitCode.bad_args)
@@ -491,24 +548,6 @@ def logging_factory(logger_name, logger_args):
return root_logger
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)
msg = re.sub(r"\((.+?)\)", r'\1', msg)
@@ -519,31 +558,47 @@ def cleanup_ruffus_error_message(msg):
def do_ruffus_exception(ruffus_five_tuple, options, log):
"""Replace the elaborate ruffus stack trace with a user friendly
description of the error message that occurred."""
exit_code = None
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
job_name = job_name # unused
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
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
log.error(cleanup_ruffus_error_message(exc_value))
return ExitCode.input_file
exit_code = 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':
log.error("Input file '{0}' is not a valid PDF".format(
options.input_file))
return ExitCode.input_file
exit_code = ExitCode.input_file
elif exc_name == 'builtins.KeyboardInterrupt':
log.error("Interrupted by user")
return ExitCode.ctrl_c
exit_code = ExitCode.ctrl_c
elif exc_name == 'subprocess.CalledProcessError':
# It's up to the subprocess handler to report something useful
msg = "Error occurred while running this command:"
log.error(msg + '\n' + exc_value)
return ExitCode.child_process_error
exit_code = ExitCode.child_process_error
elif (exc_name == 'PyPDF2.utils.PdfReadError' and \
'not been decrypted' in exc_value) or \
(exc_name == 'ocrmypdf.exceptions.EncryptedPdfError'):
log.error(textwrap.dedent("""\
Input PDF is encrypted. The encryption must be removed to
perform OCR.
For information about this PDF's security use
qpdf --show-encryption infilename
You can remove the encryption using
qpdf --decrypt [--password=[password]] infilename
"""))
exit_code = ExitCode.encrypted_pdf
elif exc_name == 'ocrmypdf.exceptions.PdfMergeFailedError':
log.error(textwrap.dedent("""\
Failed to merge PDF image layer with OCR layer
@@ -554,24 +609,20 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
Try using
ocrmypdf --pdf-renderer tesseract [..other args..]
"""))
return ExitCode.input_file
exit_code = ExitCode.input_file
elif exc_name.startswith('ocrmypdf.exceptions.'):
base_exc_name = exc_name.replace('ocrmypdf.exceptions.', '')
exc_class = getattr(ocrmypdf_exceptions, base_exc_name)
return exc_class.exit_code
elif exc_name == 'PyPDF2.utils.PdfReadError' and \
'not been decrypted' in exc_value:
log.error(textwrap.dedent("""\
Input PDF uses either an encryption algorithm or a PDF security
handler that is not supported by ocrmypdf.
exit_code = exc_class.exit_code
elif exc_name == 'PIL.Image.DecompressionBombError':
msg = cleanup_ruffus_error_message(exc_value)
msg += ("\nUse the --max-image-mpixels argument to set increase the "
"maximum number of megapixels to accept.")
log.error(msg)
exit_code = ExitCode.input_file
For information about this PDF's security use
qpdf --show-encryption [...input PDF...]
(Only algorithms "R = 1" and "R = 2" are supported.)
"""))
return ExitCode.encrypted_pdf
if exit_code is not None:
return exit_code
if not options.verbose:
log.error(exc_stack)
@@ -581,6 +632,21 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
def traverse_ruffus_exception(e_args, options, log):
"""Walk through a RethrownJobError and find the first exception.
Ruffus flattens exception to 5 element tuples. Because of a bug
in <= 2.6.3 it may present either the single:
(task, job, exc, value, stack)
or something like:
[[(task, job, exc, value, stack)]]
Generally cross-process exception marshalling doesn't work well
and ruffus doesn't support because BaseException has its own
implementation of __reduce__ that attempts to reconstruct the
exception based on e.__init__(e.args).
Attempting to log the exception directly marshalls it to the logger
which is probably in another process, so it's better to log only
data from the exception at this point.
The exit code will be based on this, even if multiple exceptions occurred
at the same time."""
@@ -612,6 +678,9 @@ def check_closed_streams(options):
"""
if sys.version_info[0:3] >= (3, 6, 4):
return True # Issued fixed in Python 3.6.4+
if sys.stderr is None:
sys.stderr = open(os.devnull, 'w')
@@ -637,6 +706,103 @@ def check_closed_streams(options):
return True
def log_page_orientations(pdfinfo, _log):
direction = {0: 'n', 90: 'e',
180: 's', 270: 'w'}
orientations = []
for n, page in enumerate(pdfinfo):
angle = page.rotation or 0
if angle != 0:
orientations.append('{0}{1}'.format(
n + 1,
direction.get(angle, '')))
if orientations:
_log.info('Page orientations detected: ' + ' '.join(orientations))
def preamble(_log):
_log.debug('ocrmypdf ' + VERSION)
_log.debug('tesseract ' + tesseract.version())
_log.debug('qpdf ' + qpdf.version())
if fitz:
_log.debug('PyMuPDF ' + fitz.version[0])
_log.debug('libmupdf ' + fitz.version[1])
else:
_log.debug('PyMuPDF not installed')
def check_environ(options, _log):
old_envvars = (
'OCRMYPDF_TESSERACT',
'OCRMYPDF_QPDF',
'OCRMYPDF_GS',
'OCRMYPDF_UNPAPER')
for k in old_envvars:
if k in os.environ:
_log.warning(textwrap.dedent("""\
OCRmyPDF no longer uses the environment variable {}.
Change PATH to select alternate programs.""".format(k)))
def check_input_file(options, _log, start_input_file):
if options.input_file == '-':
# stdin
_log.info('reading file from standard input')
with open(start_input_file, 'wb') as stream_buffer:
from shutil import copyfileobj
copyfileobj(sys.stdin.buffer, stream_buffer)
else:
try:
re_symlink(options.input_file, start_input_file, _log)
except FileNotFoundError:
_log.error("File not found - " + options.input_file)
raise InputFileError()
def check_requested_output_file(options, _log):
if options.output_file == '-':
if sys.stdout.isatty():
_log.error(textwrap.dedent("""\
Output was set to stdout '-' but it looks like stdout
is connected to a terminal. Please redirect stdout to a
file."""))
raise BadArgsError()
elif not is_file_writable(options.output_file):
_log.error(
"Output file location (" + options.output_file + ") " +
"is not a writable file.")
raise OutputFileAccessError()
def report_output_file_size(options, _log, input_file, output_file):
try:
output_size = Path(output_file).stat().st_size
input_size = Path(input_file).stat().st_size
except FileNotFoundError:
return # Outputting to stream or something
ratio = output_size / input_size
if ratio < 1.35 or input_size < 25000:
return # Seems fine
reasons = []
if not fitz:
reasons.append("The optional dependency PyMuPDF is not installed.")
if options.force_ocr:
reasons.append("The argument --force-ocr was issued.")
if reasons:
explanation = (
"Possible reasons for this include:\n" + '\n'.join(reasons) + "\n")
else:
explanation = (
"No reason for this increase is known. Please report this issue.")
_log.warning(textwrap.dedent("""\
The output file size is {:.2f}× larger than the input file.
{}
""".format(ratio, explanation)))
def run_pipeline():
options = parser.parse_args()
options.verbose_abbreviated_path = 1
@@ -648,16 +814,33 @@ def run_pipeline():
_log, _log_mutex = proxy_logger.make_shared_logger_and_proxy(
logging_factory, __name__, logger_args)
_log.debug('ocrmypdf ' + VERSION)
_log.debug('tesseract ' + tesseract.version())
preamble(_log)
check_options(options, _log)
# Complain about qpdf version < 7.0.0
# Suppress the warning if in the test suite, since there are no PPAs
# for qpdf 7.0.0 for Ubuntu trusty (i.e. Travis)
if qpdf.version() < '7.0.0' and not os.environ.get('PYTEST_CURRENT_TEST'):
complain(
"You are using qpdf version {0} which has known issues including "
"security vulnerabilities with certain malformed PDFs. Consider "
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
# options.input_file, options.pdf_renderer are already bound.)
if not options.jobs:
options.jobs = available_cpu_count()
# Performance is improved by setting Tesseract to single threaded. In tests
# this gives better throughput than letting a smaller number of Tesseract
# jobs run multithreaded.
if tesseract.v4():
os.environ.setdefault('OMP_THREAD_LIMIT', '1')
check_environ(options, _log)
if os.environ.get('PYTEST_CURRENT_TEST'):
os.environ['_OCRMYPDF_TEST_INFILE'] = options.input_file
try:
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
options.history_file = os.path.join(
@@ -665,37 +848,14 @@ def run_pipeline():
start_input_file = os.path.join(
work_folder, 'origin')
if options.input_file == '-':
# stdin
_log.info('reading file from standard input')
with open(start_input_file, 'wb') as stream_buffer:
from shutil import copyfileobj
copyfileobj(sys.stdin.buffer, stream_buffer)
else:
try:
re_symlink(options.input_file, start_input_file, _log)
except FileNotFoundError:
_log.error("File not found - " + options.input_file)
return ExitCode.input_file
if options.output_file == '-':
if sys.stdout.isatty():
_log.error(textwrap.dedent("""\
Output was set to stdout '-' but it looks like stdout
is connected to a terminal. Please redirect stdout to a
file."""))
return ExitCode.bad_args
elif not is_file_writable(options.output_file):
_log.error(
"Output file location (" + options.output_file + ") " +
"is not writable.")
return ExitCode.file_access_error
check_input_file(options, _log, start_input_file)
check_requested_output_file(options, _log)
manager = JobContextManager()
manager.register('JobContext', JobContext)
manager.register('JobContext', JobContext) # pylint: disable=no-member
manager.start()
context = manager.JobContext()
context = manager.JobContext() # pylint: disable=no-member
context.set_options(options)
context.set_work_folder(work_folder)
@@ -705,29 +865,12 @@ def run_pipeline():
except ruffus_exceptions.RethrownJobError as e:
if options.verbose:
_log.debug(str(e)) # stringify exception so logger doesn't have to
# Ruffus flattens exception to 5 element tuples. Because of a bug
# in <= 2.6.3 it may present either the single:
# (task, job, exc, value, stack)
# or something like:
# [[(task, job, exc, value, stack)]]
#
# Generally cross-process exception marshalling doesn't work well
# and ruffus doesn't support because BaseException has its own
# implementation of __reduce__ that attempts to reconstruct the
# exception based on e.__init__(e.args).
#
# Attempting to log the exception directly marshalls it to the logger
# which is probably in another process, so it's better to log only
# data from the exception at this point.
exitcode = traverse_ruffus_exception(e.args, options, _log)
if exitcode is None:
_log.error("Unexpected ruffus exception: " + str(e))
_log.error(repr(e))
return ExitCode.other_error
else:
return exitcode
return exitcode
except ExitCodeException as e:
return e.exit_code
except Exception as e:
@@ -736,8 +879,13 @@ def run_pipeline():
if options.flowchart:
_log.info("Flowchart saved to {}".format(options.flowchart))
elif options.output_file != '-':
if options.output_type == 'pdfa':
return ExitCode.ok
elif options.output_file == '-':
_log.info("Output sent to stdout")
elif os.path.samefile(options.output_file, os.devnull):
pass # Say nothing when sending to dev null
else:
if options.output_type.startswith('pdfa'):
pdfa_info = file_claims_pdfa(options.output_file)
if pdfa_info['pass']:
msg = 'Output file is a {} (as expected)'
@@ -745,29 +893,20 @@ def run_pipeline():
else:
msg = 'Output file is okay but is not PDF/A (seems to be {})'
_log.warning(msg.format(pdfa_info['conformance']))
return ExitCode.invalid_output_pdf
if not qpdf.check(options.output_file, _log):
_log.warning('Output file: The generated PDF is INVALID')
return ExitCode.invalid_output_pdf
else:
_log.info("Output sent to stdout")
report_output_file_size(options, _log, start_input_file,
options.output_file)
pdfinfo = context.get_pdfinfo()
if options.verbose:
from pprint import pformat
_log.debug(pformat(pdfinfo))
direction = {0: 'n', 90: 'e',
180: 's', 270: 'w'}
orientations = []
for n, page in enumerate(pdfinfo):
angle = pdfinfo[n].rotation or 0
if angle != 0:
orientations.append('{0}{1}'.format(
n + 1,
direction.get(angle, '')))
if orientations:
_log.info('Page orientations detected: ' + ' '.join(orientations))
log_page_orientations(pdfinfo, _log)
return ExitCode.ok
+108
View File
@@ -0,0 +1,108 @@
# Copyright (c) 2014, Armin Ronacher
#
# Copyright (c) 2017, James R Barlow
#
# Some rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * The names of the contributors may not be used to endorse or
# promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import codecs
def verify_python3_env():
"""Ensures that the environment is good for unicode on Python 3."""
try:
import locale
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
except Exception:
fs_enc = 'ascii'
if fs_enc != 'ascii':
return
extra = ''
if os.name == 'posix':
import subprocess
rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()[0]
good_locales = set()
has_c_utf8 = False
# Make sure we're operating on text here.
if isinstance(rv, bytes):
rv = rv.decode('ascii', 'replace')
for line in rv.splitlines():
locale = line.strip()
if locale.lower().endswith(('.utf-8', '.utf8')):
good_locales.add(locale)
if locale.lower() in ('c.utf8', 'c.utf-8'):
has_c_utf8 = True
extra += '\n\n'
if not good_locales:
extra += (
'Additional information: on this system no suitable UTF-8\n'
'locales were discovered. This most likely requires resolving\n'
'by reconfiguring the locale system.'
)
elif has_c_utf8:
extra += (
'This system supports the C.UTF-8 locale which is recommended.\n'
'You might be able to resolve your issue by exporting the\n'
'following environment variables:\n\n'
' export LC_ALL=C.UTF-8\n'
' export LANG=C.UTF-8'
)
else:
extra += (
'This system lists a couple of UTF-8 supporting locales that\n'
'you can pick from. The following suitable locales were\n'
'discovered: %s'
) % ', '.join(sorted(good_locales))
bad_locale = None
for locale in os.environ.get('LC_ALL'), os.environ.get('LANG'):
if locale and locale.lower().endswith(('.utf-8', '.utf8')):
bad_locale = locale
if locale is not None:
break
if bad_locale is not None:
extra += (
'\nocrmypdf discovered that you exported a UTF-8 locale\n'
'but the locale system could not pick up from it because\n'
'it does not exist. The exported locale is "%s" but it\n'
'is not supported'
) % bad_locale
raise RuntimeError('ocrmypdf will abort further execution because Python 3 '
'was configured to use ASCII as encoding for the '
'environment.' + extra)
@@ -1,5 +1,19 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from enum import IntEnum
@@ -20,7 +34,11 @@ class ExitCode(IntEnum):
class ExitCodeException(Exception):
pass
exit_code = ExitCode.other_error
class BadArgsError(ExitCodeException):
exit_code = ExitCode.bad_args
class PdfMergeFailedError(ExitCodeException):
@@ -39,6 +57,10 @@ class DpiError(ExitCodeException):
exit_code = ExitCode.input_file
class OutputFileAccessError(ExitCodeException):
exit_code = ExitCode.file_access_error
class PriorOcrFoundError(ExitCodeException):
exit_code = ExitCode.already_done_ocr
+56
View File
@@ -0,0 +1,56 @@
# © 2016 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
"""Wrappers to manage subprocess calls"""
import os
import re
import sys
from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
def get_version(program, *,
version_arg='--version', regex=r'(\d+(\.\d+)*)'):
"Get the version of the specified program"
args_prog = [
program,
version_arg
]
try:
proc = run(
args_prog, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=STDOUT, check=True)
output = proc.stdout
except CalledProcessError as e:
if e.returncode < 0:
raise MissingDependencyError(
"Ran program '{}' but it exited with an error:\n{}".format(
program, e.output)) from e
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
try:
version = re.match(regex, output.strip()).group(1)
except AttributeError as e:
raise MissingDependencyError(
("The program '{}' did not report its version. "
"Message was:\n{}").format(program, output)
)
return version
@@ -1,5 +1,19 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2017 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from tempfile import NamedTemporaryFile
from subprocess import run, PIPE, STDOUT, CalledProcessError
@@ -7,28 +21,27 @@ from shutil import copy
from functools import lru_cache
import re
import sys
from . import get_program
from ..exceptions import SubprocessOutputError
from PIL import Image
from . import get_version
from ..exceptions import SubprocessOutputError, MissingDependencyError
from ..helpers import fspath
@lru_cache(maxsize=1)
def version():
args_gs = [
get_program('gs'),
'--version'
]
try:
version = check_output(
args_gs, close_fds=True, universal_newlines=True,
stderr=STDOUT)
except CalledProcessError as e:
print("Could not find Ghostscript executable on system PATH.",
file=sys.stderr)
raise MissingDependencyError from e
return get_version('gs')
return version.strip()
def jpeg_passthrough_available():
"""
Ghostscript 9.23 introduced JPEG passthrough but it seems to corrupt the
last two bytes of certain images, for now we disable it for 9.23 and
do not mention it for < 9.23.
https://bugs.ghostscript.com/show_bug.cgi?id=699216
"""
return False
def _gs_error_reported(stream):
@@ -50,7 +63,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
:param yres:
:param raster_device:
:param log:
:param pageno: page number to rasterize
:param pageno: page number to rasterize (beginning at page 1)
:param page_dpi: resolution tuple (x, y) overriding output image DPI
:return:
"""
@@ -60,7 +73,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
page_dpi = res
with NamedTemporaryFile(delete=True) as tmp:
args_gs = [
get_program('gs'),
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
@@ -68,11 +81,11 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
'-sDEVICE=%s' % raster_device,
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
'-o', tmp.name,
'-r{0}x{1}'.format(str(int_res[0]), str(int_res[1])),
'-o', tmp.name,
fspath(input_file)
]
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
if _gs_error_reported(p.stdout):
@@ -88,6 +101,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
# if the resolution happens to be fractional, then the discrepancy
# would change the size of the output page, especially if the DPI
# is quite low. Resize the image to the expected size
tmp.seek(0)
with Image.open(tmp) as im:
expected_size = round(im.size[0] / int_res[0] * res[0]), \
@@ -103,7 +117,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
def generate_pdfa(pdf_pages, output_file, compression, log,
threads=1, pdf_version='1.5'):
threads=1, pdf_version='1.5', pdfa_part='2'):
compression_args = []
if compression == 'jpeg':
compression_args = [
@@ -125,9 +139,20 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
"-dAutoFilterGrayImages=true",
]
# Older versions of Ghostscript expect a leading slash in
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
# git commit fe1c025d.
strategy = 'RGB' if version() >= '9.19' else '/RGB'
if version() == '9.23':
# 9.23: new feature JPEG passthrough is broken in some cases, best to
# disable it always
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
compression_args.append('-dPassThroughJPEGImages=false')
with NamedTemporaryFile(delete=True) as gs_pdf:
args_gs = [
get_program("gs"),
"gs",
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
@@ -135,11 +160,11 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=/RGB",
"-sColorConversionStrategy=" + strategy,
"-sProcessColorModel=DeviceRGB"
] + compression_args + [
"-dJPEGQ=95",
"-dPDFA=2",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
@@ -166,4 +191,4 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
copy(gs_pdf.name, output_file)
else:
log.error('Ghostscript PDF/A rendering failed')
raise SubprocessOutputError()
raise SubprocessOutputError()
+227
View File
@@ -0,0 +1,227 @@
# © 2017 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import CalledProcessError, STDOUT, PIPE, run
from functools import lru_cache
import sys
import os
import re
import resource
from ..exceptions import InputFileError, SubprocessOutputError, \
MissingDependencyError, EncryptedPdfError
from . import get_version
from ..helpers import re_symlink
@lru_cache(maxsize=1)
def version():
return get_version('qpdf', regex=r'qpdf version (.+)')
def check(input_file, log=None):
args_qpdf = [
'qpdf',
'--check',
input_file
]
if log is None:
import logging as log
try:
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True,
check=True)
except CalledProcessError as e:
if e.returncode == 2:
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
log.info("qpdf --check returned warnings:")
log.info(e.output)
else:
log.warning(e.output)
return False
return True
def _probably_encrypted(e):
"""qpdf can report a false positive "file is encrypted" message for damaged
files - suppress this"""
return e.returncode == 2 and \
'invalid password' in e.output and \
'file is damaged' not in e.output
def repair(input_file, output_file, log):
args_qpdf = [
'qpdf', input_file, output_file
]
try:
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True,
check=True)
except CalledProcessError as e:
if e.returncode == 3 and e.output.find("operation succeeded"):
log.debug('qpdf found and fixed errors: ' + e.output)
return
if _probably_encrypted(e):
raise EncryptedPdfError() from e
elif e.returncode == 2:
log.error("{0}: not a valid PDF, and could not repair it.".format(
input_file))
log.error("Details: " + e.output)
raise InputFileError() from e
else:
log.error("{0}: unknown error".format(
input_file))
log.error(e.output)
raise SubprocessOutputError() from e
def get_npages(input_file, log):
try:
pages = run(
['qpdf', '--show-npages', input_file],
universal_newlines=True, check=True, stdout=PIPE, stderr=STDOUT)
except CalledProcessError as e:
if e.returncode == 2 and e.output.find('No such file'):
log.error(e.output)
raise InputFileError() from e
return int(pages)
def split_pages(input_file, work_folder, npages):
"""Split multipage PDF into individual pages.
Incredibly enough, this multiple process approach is about 70 times
faster than using Ghostscript.
"""
for n in range(int(npages)):
args_qpdf = [
'qpdf', input_file,
'--pages', input_file, '{0}'.format(n + 1), '--',
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
]
run(args_qpdf, check=True)
def extract_page(input_file, output_file, pageno):
args_qpdf = [
'qpdf', input_file,
'--pages', input_file, '{0}'.format(pageno + 1), '--',
output_file
]
run(args_qpdf, check=True)
def _merge_inner(input_files, output_file, min_version=None, log=None):
"""Merge the list of input files (all filenames) into the output file.
The input files may contain one or more pages.
"""
# Single page 'merges' should still be attempted to that the same error
# checking is applied to single page case
version_arg = ['--min-version={}'.format(min_version)] \
if min_version else []
if log is None:
import logging as log
args_qpdf = [
'qpdf'
] + version_arg + [
input_files[0], '--pages'
] + input_files + ['--', output_file]
try:
run(args_qpdf, check=True, stderr=PIPE, universal_newlines=True)
except CalledProcessError as e:
if e.returncode == 3 and \
e.stderr.find("unknown token while reading object") and \
e.stderr.find("operation succeeded"):
# Only whitelist the 'unknown token' problem (decimal/string issue)
# qpdf issue #165
log.warning('qpdf found and fixed errors: ' + e.stderr)
return
raise e from e
def merge(input_files, output_file, min_version=None, log=None, max_files=None):
"""Merge the list of input files (all filenames) into the output file.
The input files may contain one or more pages.
"""
# qpdf requires that every file that contributes to the output has a file
# descriptor that remains open. That means, given our approach of one
# intermediate PDF per, we can practically hit the number of file
# descriptors.
if max_files is None or max_files < 2:
# Find out how many open file descriptors we can get away with
ulimits = resource.getrlimit(resource.RLIMIT_NOFILE)
max_open_files = ulimits[0]
max_files = max_open_files // 2 # Conservative guess
# We'll write things alongside the output file
output_dir = os.path.dirname(output_file)
import random
import string
def randstr():
return ''.join(random.sample(string.ascii_lowercase, 6))
# How many files to grab at once, merging all their contents
step_size = max_files
workqueue = input_files.copy()
counter = 1
next_workqueue = []
while len(workqueue) > 1 or len(next_workqueue) > 0:
# Take n files out of the queue
n = min(step_size, len(workqueue))
job = workqueue[0:n]
del workqueue[0:n]
log.debug('merging ' + repr(job))
# Merge them into 1 file, which will contain n^depth pages
merge_file = os.path.join(
output_dir, "merge-{:06d}-{}.pdf".format(counter, randstr()))
counter += 1
_merge_inner(job, merge_file, min_version=min_version, log=log)
# On the next
next_workqueue.append(merge_file)
log.debug('next_workqueue ' + repr(next_workqueue))
# If we're out of things to do in this queue, move on to the next
# queue. On the counter-th pass of the workqueue we can chew through
# (step_size)**N pages, so on most systems the second pass finishes
# the job.
if len(workqueue) == 0:
workqueue = next_workqueue
next_workqueue = []
re_symlink(workqueue.pop(), output_file, log)
@@ -1,21 +1,36 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2017 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import re
import shutil
from functools import lru_cache
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number
from . import get_program
from collections import namedtuple
from textwrap import dedent
from subprocess import PIPE, CalledProcessError, \
TimeoutExpired, check_output, STDOUT
from contextlib import suppress
import PyPDF2 as pypdf
from subprocess import Popen, PIPE, CalledProcessError, \
TimeoutExpired, check_output, STDOUT, DEVNULL
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number
from . import get_version
OrientationConfidence = namedtuple(
'OrientationConfidence',
@@ -41,26 +56,12 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
@lru_cache(maxsize=1)
def version():
args_tess = [
get_program('tesseract'),
'--version'
]
try:
versions = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
except CalledProcessError as e:
print("Could not find Tesseract executable on system PATH.",
file=sys.stderr)
raise MissingDependencyError from e
tesseract_version = re.match(r'tesseract\s(.+)', versions).group(1)
return tesseract_version
return get_version('tesseract', regex=r'tesseract\s(.+)')
def v4():
"Is this Tesseract v4.0?"
return (version() >= '4')
return version() >= '4'
@lru_cache(maxsize=1)
@@ -71,13 +72,13 @@ def has_textonly_pdf():
parse the parameter list
"""
args_tess = [
get_program('tesseract'),
'tesseract',
'--print-parameters'
]
params = ''
try:
params = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
print("Could not --print-parameters from tesseract",
file=sys.stderr)
@@ -95,13 +96,12 @@ def psm():
@lru_cache(maxsize=1)
def languages():
args_tess = [
get_program('tesseract'),
'tesseract',
'--list-langs'
]
try:
langs = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
msg = dedent("""Tesseract failed to report available languages.
Output from Tesseract:
@@ -113,12 +113,12 @@ def languages():
return set(lang.strip() for lang in langs.splitlines()[1:])
def tess_base_args(languages, engine_mode):
def tess_base_args(langs, engine_mode):
args = [
get_program('tesseract'),
'tesseract',
]
if languages:
args.extend(['-l', '+'.join(languages)])
if langs:
args.extend(['-l', '+'.join(langs)])
if engine_mode is not None and v4():
args.extend(['--oem', str(engine_mode)])
return args
@@ -126,7 +126,7 @@ def tess_base_args(languages, engine_mode):
def get_orientation(input_file, language: list, engine_mode, timeout: float,
log):
args_tesseract = tess_base_args(language, engine_mode) + [
args_tesseract = tess_base_args(['osd'], engine_mode) + [
psm(), '0',
input_file,
'stdout'
@@ -134,19 +134,18 @@ def get_orientation(input_file, language: list, engine_mode, timeout: float,
try:
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
args_tesseract, stderr=STDOUT, 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):
if (b'Too few characters. Skipping this page' in e.output or
b'Image too large' in e.output):
return OrientationConfidence(0, 0)
raise e from e
else:
osd = {}
for line in stdout.splitlines():
for line in stdout.decode().splitlines():
line = line.strip()
parts = line.split(':', maxsplit=2)
if len(parts) == 2:
@@ -171,8 +170,17 @@ def get_orientation(input_file, language: list, engine_mode, timeout: float,
def tesseract_log_output(log, stdout, input_file):
lines = stdout.splitlines()
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
try:
text = stdout.decode()
except UnicodeDecodeError:
log.error(prefix + "command line output was not utf-8. " +
"This usually means Tesseract's language packs do not match "
"the installed version of Tesseract.")
text = stdout.decode('utf-8', 'backslashreplace')
lines = text.splitlines()
for line in lines:
if line.startswith("Tesseract Open Source"):
continue
@@ -245,8 +253,8 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
args_tesseract, stderr=STDOUT,
timeout=timeout)
except TimeoutExpired:
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
@@ -255,9 +263,9 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
_generate_null_hocr(output_hocr, output_sidecar, input_file)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if 'read_params_file: parameter not found' in e.output:
if b'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e
if 'Image too large' in e.output:
if b'Image too large' in e.output:
_generate_null_hocr(output_hocr, output_sidecar, input_file)
return
@@ -275,6 +283,8 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
f.write('[skipped page]')
if not text_only:
with suppress(FileNotFoundError):
os.remove(output_pdf) # In case it was partially created
os.symlink(skip_pdf, output_pdf)
return
@@ -287,6 +297,10 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
with open(output_pdf, 'wb') as out:
pdf_out = pypdf.PdfFileWriter()
w, h = page0.mediaBox.getWidth(), page0.mediaBox.getHeight()
# If skip page has a /Rotate key, replicate the rotation
rotation = int(page0.get('/Rotate', 0))
if rotation % 180 == 90:
w, h = h, w
pdf_out.addBlankPage(w, h)
pdf_out.write(out)
@@ -338,8 +352,8 @@ def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
args_tesseract, stderr=STDOUT,
timeout=timeout)
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_text)
except TimeoutExpired:
@@ -347,10 +361,10 @@ def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image)
if 'read_params_file: parameter not found' in e.output:
if b'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e
if 'Image too large' in e.output:
if b'Image too large' in e.output:
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
return
raise e from e
@@ -1,27 +1,30 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
from subprocess import CalledProcessError, STDOUT, check_output, check_call
from subprocess import CalledProcessError, STDOUT, check_output
from tempfile import NamedTemporaryFile
import sys
import os
from functools import lru_cache
from ..exceptions import MissingDependencyError
from . import get_program
@lru_cache(maxsize=1)
def version():
args_unpaper = [
get_program('unpaper'),
'--version'
]
version = check_output(
args_unpaper, close_fds=True, universal_newlines=True,
stderr=STDOUT, timeout=5)
return version.strip()
from . import get_version
try:
@@ -31,9 +34,14 @@ except ImportError:
raise
@lru_cache(maxsize=1)
def version():
return get_version('unpaper')
def run(input_file, output_file, dpi, log, mode_args):
args_unpaper = [
get_program('unpaper'),
'unpaper',
'-v',
'--dpi', str(dpi)
] + mode_args
@@ -49,15 +57,15 @@ def run(input_file, output_file, dpi, log, mode_args):
else:
im = im.convert(mode='RGB')
except IOError as e:
log.error(
"Could not convert image with type " + im.mode)
log.error("Could not convert image with type " + im.mode)
im.close()
raise MissingDependencyError() from e
try:
suffix = SUFFIXES[im.mode]
except KeyError:
log.error(
"Failed to convert image to a supported format.")
log.error("Failed to convert image to a supported format.")
im.close()
raise MissingDependencyError() from e
with NamedTemporaryFile(suffix=suffix) as input_pnm, \
@@ -82,20 +90,9 @@ def run(input_file, output_file, dpi, log, mode_args):
Image.open(output_pnm.name).save(output_file, dpi=(dpi, dpi))
def deskew(input_file, output_file, dpi, log):
run(input_file, output_file, dpi, log, [
'--mask-scan-size', '100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-noisefilter', # don't remove salt and pepper noise
'--no-blurfilter' # don't remove blurry objects/debris
])
def clean(input_file, output_file, dpi, log):
run(input_file, output_file, dpi, log, [
'--layout', 'none',
'--mask-scan-size', '100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
@@ -1,11 +1,27 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from functools import partial
from collections.abc import Iterable
from contextlib import suppress, contextmanager
from pathlib import Path
import sys
import os
import multiprocessing
def re_symlink(input_file, soft_link_name, log=None):
@@ -32,7 +48,7 @@ def re_symlink(input_file, soft_link_name, log=None):
"%s exists and is not a link" % soft_link_name)
try:
os.unlink(soft_link_name)
except:
except OSError:
prdebug("Can't unlink %s" % (soft_link_name))
if not os.path.exists(input_file):
@@ -53,9 +69,28 @@ def is_iterable_notstr(thing):
def page_number(input_file):
"Get one-based page number implied by filename (000002.pdf -> 2)"
return int(os.path.basename(input_file)[0:6])
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 is_file_writable(test_file):
"""Intentionally racy test if target is writable.
@@ -63,19 +98,32 @@ def is_file_writable(test_file):
can replace it atomically. Before doing the OCR work, make sure
the location is writable.
"""
if os.path.exists(test_file):
p = Path(test_file)
if p.is_symlink():
# Python 3.5 does not accept parameters for Path.resolve() and behaves
# as if strict=True (throws an exception on failure). Python 3.6
# defaults to strict=False. This implements strict=False like behavior
# for Python 3.5.
if sys.version_info[0:2] <= (3, 5):
p = Path(os.path.realpath(str(p)))
else:
p = p.resolve(strict=False)
# p.is_file() throws an exception in some cases
if p.exists() and p.is_file():
return os.access(
test_file, os.W_OK,
str(p), os.W_OK,
effective_ids=(os.access in os.supports_effective_ids))
else:
try:
fp = open(test_file, 'wb')
except OSError as e:
fp = p.open('wb')
except OSError:
return False
else:
fp.close()
with suppress(OSError):
os.unlink(test_file)
p.unlink()
return True
@@ -118,4 +166,4 @@ if sys.version_info[0:2] <= (3, 5):
else:
universal_open = open
fspath = os.fspath
fspath = os.fspath
+350
View File
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
#
# Copyright (c) 2010, Jonathan Brinley
# Original version from: https://github.com/jbrinley/HocrConverter
#
# Copyright (c) 2013-14, Julien Pfefferkorn
# Modifications
#
# Copyright (c) 2015-16, James R. Barlow
# Set text to transparent
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch
from xml.etree import ElementTree
from PIL import Image
from collections import namedtuple
from math import atan, sin, cos
import re
import argparse
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
class HocrTransformError(Exception):
pass
class HocrTransform():
"""
A class for converting documents from the hOCR format.
For details of the hOCR format, see:
http://kba.cloud/hocr-spec/
"""
box_pattern = re.compile(r'bbox((\s+\d+){4})')
baseline_pattern = re.compile(r'''
baseline \s+
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
([\-\+]?\d+) # +/- int''', re.VERBOSE)
ligatures = str.maketrans({
'': 'ff',
'': 'ffi',
'': 'ffl',
'': 'fi',
'': 'fl',
})
def __init__(self, hocrFileName, dpi):
self.dpi = dpi
self.hocr = ElementTree.parse(hocrFileName)
# if the hOCR file has a namespace, ElementTree requires its use to
# find elements
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
self.xmlns = ''
if matches:
self.xmlns = matches.group(1)
# get dimension in pt (not pixel!!!!) of the OCRed image
self.width, self.height = None, None
for div in self.hocr.findall(
".//%sdiv[@class='ocr_page']" % (self.xmlns)):
coords = self.element_coordinates(div)
pt_coords = self.pt_from_pixel(coords)
self.width = pt_coords.x2 - pt_coords.x1
self.height = pt_coords.y2 - pt_coords.y1
# there shouldn't be more than one, and if there is, we don't want
# it
break
if self.width is None or self.height is None:
raise HocrTransformError("hocr file is missing page dimensions")
def __str__(self):
"""
Return the textual content of the HTML body
"""
if self.hocr is None:
return ''
body = self.hocr.find(".//%sbody" % (self.xmlns))
if body:
return self._get_element_text(body)
else:
return ''
def _get_element_text(self, element):
"""
Return the textual content of the element and its children
"""
text = ''
if element.text is not None:
text += element.text
for child in element.getchildren():
text += self._get_element_text(child)
if element.tail is not None:
text += element.tail
return text
@classmethod
def element_coordinates(cls, element):
"""
Returns a tuple containing the coordinates of the bounding box around
an element
"""
out = (0, 0, 0, 0)
if 'title' in element.attrib:
matches = cls.box_pattern.search(element.attrib['title'])
if matches:
coords = matches.group(1).split()
out = Rect._make(int(coords[n]) for n in range(4))
return out
@classmethod
def baseline(cls, element):
"""
Returns a tuple containing the baseline slope and intercept.
"""
if 'title' in element.attrib:
matches = cls.baseline_pattern.search(element.attrib['title'])
if matches:
return float(matches.group(1)), int(matches.group(2))
return (0, 0)
def pt_from_pixel(self, pxl):
"""
Returns the quantity in PDF units (pt) given quantity in pixels
"""
return Rect._make(
(c / self.dpi * inch) for c in pxl)
@classmethod
def replace_unsupported_chars(cls, s):
"""
Given an input string, returns the corresponding string that:
- is available in the helvetica facetype
- does not contain any ligature (to allow easy search in the PDF file)
"""
return s.translate(cls.ligatures)
def to_pdf(self, outFileName, imageFileName=None, showBoundingboxes=False,
fontname="Helvetica", invisibleText=False, interwordSpaces=False):
"""
Creates a PDF file with an image superimposed on top of the text.
Text is positioned according to the bounding box of the lines in
the hOCR file.
The image need not be identical to the image used to create the hOCR
file.
It can have a lower resolution, different color mode, etc.
"""
# create the PDF file
# page size in points (1/72 in.)
pdf = Canvas(
outFileName, pagesize=(self.width, self.height), pageCompression=1)
# draw bounding box for each paragraph
# light blue for bounding box of paragraph
pdf.setStrokeColorRGB(0, 1, 1)
# light blue for bounding box of paragraph
pdf.setFillColorRGB(0, 1, 1)
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.findall(
".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
elemtxt = self._get_element_text(elem).rstrip()
if len(elemtxt) == 0:
continue
pxl_coords = self.element_coordinates(elem)
pt = self.pt_from_pixel(pxl_coords)
# draw the bbox border
if showBoundingboxes:
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
fill=1)
found_lines = False
for line in self.hocr.findall(
".//%sspan[@class='%s']" % (self.xmlns, "ocr_line")):
found_lines = True
self._do_line(pdf, line, "ocrx_word", fontname, invisibleText,
interwordSpaces, showBoundingboxes)
if not found_lines:
# Tesseract did not report any lines (just words)
root = self.hocr.find(".//%sdiv[@class='%s']" % (self.xmlns, "ocr_page"))
self._do_line(pdf, root, "ocrx_word", fontname, invisibleText,
interwordSpaces, showBoundingboxes)
# put the image on the page, scaled to fill the page
if imageFileName is not None:
pdf.drawImage(imageFileName, 0, 0,
width=self.width, height=self.height)
# finish up the page and save it
pdf.showPage()
pdf.save()
@classmethod
def polyval(cls, poly, x):
return x * poly[0] + poly[1]
def _do_line(self, pdf, line, elemclass, fontname, invisibleText,
interwordSpaces, showBoundingboxes):
pxl_line_coords = self.element_coordinates(line)
line_box = self.pt_from_pixel(pxl_line_coords)
line_height = line_box.y2 - line_box.y1
slope, pxl_intercept = self.baseline(line)
if abs(slope) < 0.005:
slope = 0.0
angle = atan(slope)
cos_a, sin_a = cos(angle), sin(angle)
text = pdf.beginText()
intercept = pxl_intercept / self.dpi * inch
# Don't allow the font to break out of the bounding box. Division by
# cos_a accounts for extra clearance between the glyph's vertical axis
# on a sloped baseline and the edge of the bounding box.
fontsize = (line_height - abs(intercept)) / cos_a
text.setFont(fontname, fontsize)
if invisibleText:
text.setTextRenderMode(3) # Invisible (indicates OCR text)
# Intercept is normally negative, so this places it above the bottom
# of the line box
baseline_y2 = self.height - (line_box.y2 + intercept)
if showBoundingboxes:
# draw the baseline in magenta, dashed
pdf.setDash()
pdf.setStrokeColorRGB(0.95, 0.65, 0.95)
pdf.setLineWidth(0.5)
# negate slope because it is defined as a rise/run in pixel
# coordinates and page coordinates have the y axis flipped
pdf.line(line_box.x1,
baseline_y2,
line_box.x2,
self.polyval((-slope, baseline_y2),
line_box.x2 - line_box.x1))
# light green for bounding box of word/line
pdf.setDash(6, 3)
pdf.setStrokeColorRGB(1, 0, 0)
text.setTextTransform(
cos_a, -sin_a, sin_a, cos_a,
line_box.x1, baseline_y2
)
pdf.setFillColorRGB(0, 0, 0) # text in black
elements = line.findall(
".//%sspan[@class='%s']" % (self.xmlns, elemclass))
for elem in elements:
elemtxt = self._get_element_text(elem).strip()
elemtxt = self.replace_unsupported_chars(elemtxt)
if elemtxt == '':
continue
pxl_coords = self.element_coordinates(elem)
box = self.pt_from_pixel(pxl_coords)
if interwordSpaces:
# if `--interword-spaces` is true, append a space
# to the end of each text element to allow simpler PDF viewers
# such as PDF.js to better recognize words in search and copy
# and paste. Do not remove space from last word in line, even
# though it would look better, because it will interfere with
# naive text extraction. \n does not work either.
elemtxt += ' '
box = Rect._make((
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
line_box.y2))
box_width = box.x2 - box.x1
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
# draw the bbox border
if showBoundingboxes:
pdf.rect(
box.x1,
self.height - line_box.y2,
box_width,
line_height,
fill=0)
# Adjust relative position of cursor
# This is equivalent to:
# text.setTextOrigin(pt.x1, self.height - line_box.y2)
# but the former generates a full text reposition matrix (Tm) in the
# content stream while this issues a "offset" (Td) command.
# .moveCursor() is relative to start of the text line, where the
# "text line" means whatever reportlab defines it as. Do not use
# use .getCursor(), since moveCursor() rather unintuitively plans
# its moves relative to .getStartOfLine().
# For skewed lines, in the text transform we set up a rotated
# coordinate system, so we don't have to account for the
# incremental offset. Surprisingly most PDF viewers can handle this.
cursor = text.getStartOfLine()
dx = box.x1 - cursor[0]
dy = baseline_y2 - cursor[1]
text.moveCursor(dx, dy)
# If reportlab tells us this word is 0 units wide, our best seems
# to be to suppress this text
if font_width > 0:
text.setHorizScale(100 * box_width / font_width)
text.textOut(elemtxt)
pdf.drawText(text)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
parser.add_argument('-b', '--boundingboxes', action="store_true",
default=False, help='Show bounding boxes borders')
parser.add_argument('-r', '--resolution', type=int,
default=300,
help='Resolution of the image that was OCRed')
parser.add_argument('-i', '--image', default=None,
help='Path to the image to be placed above the text')
parser.add_argument('--interword-spaces', action='store_true',
default=False, help='Add spaces between words')
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument(
'outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(args.hocrfile, args.resolution)
hocr.to_pdf(args.outputfile, args.image, args.boundingboxes, interwordSpaces=args.interword_spaces)
@@ -3,6 +3,21 @@
#
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
#
# Python FFI wrapper for Leptonica library
import argparse
@@ -566,35 +581,4 @@ if __name__ == '__main__':
parser_deskew.set_defaults(func=deskew)
args = parser.parse_args()
if get_leptonica_version() != u'leptonica-1.69':
print("Unexpected leptonica version: %s" % getLeptonicaVersion())
args.func(args)
def test_skew_angle():
from PIL import Image, ImageDraw
from tempfile import NamedTemporaryFile
im = Image.new(mode='1', size=(1000, 1000), color=1)
draw = ImageDraw.Draw(im)
for n in range(20):
draw.line([(50, 25 + 50*n), (950, 25 + 50*n)], width=1)
del draw
test_angles = [0.1 * ang for ang in range(1, 10)] + \
[float(ang) for ang in range(1, 7)]
test_angles += [-ang for ang in test_angles]
test_angles = sorted(test_angles)
for rotate_angle in test_angles:
rotated_im = im.rotate(rotate_angle)
with NamedTemporaryFile(prefix='lept-skew', suffix='.png', delete=True) as tmpfile:
rotated_im.save(tmpfile)
pix = pixRead(tmpfile.name)
angle, confidence = pixFindSkew(pix)
print('{0} {1} {2}'.format(rotate_angle, angle, confidence), file=sys.stderr)
args.func(args)
+28
View File
@@ -0,0 +1,28 @@
# © 2017 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
"""Bindings to external libraries"""
import os as _os
try:
import fitz
except ImportError:
fitz = None
if _os.environ.get('_OCRMYPDF_NO_FITZ'):
fitz = None
+10
View File
@@ -0,0 +1,10 @@
# auto-generated file
import _cffi_backend
ffi = _cffi_backend.FFI('ocrmypdf.lib._leptonica',
_version = 0x2601,
_types = b'\x00\x00\x0F\x0D\x00\x00\xBB\x03\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\xBE\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\xBA\x03\x00\x00\x0F\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x02\x03\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x01\x0D\x00\x00\xC7\x03\x00\x00\x1C\x01\x00\x00\x00\x0F\x00\x00\xBD\x0D\x00\x00\x00\x0F\x00\x00\x49\x0D\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x1F\x03\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x49\x11\x00\x00\x49\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x07\x01\x00\x00\x49\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x6E\x11\x00\x00\x6E\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x0D\x01\x00\x00\x01\x03\x00\x00\x9D\x11\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x01\x11\x00\x00\xC6\x03\x00\x00\x65\x03\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x08\x11\x00\x00\x01\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\xC4\x03\x00\x00\xA3\x11\x00\x00\x01\x11\x00\x00\x0D\x01\x00\x00\x00\x0F\x00\x00\xCA\x0D\x00\x00\x10\x11\x00\x00\x00\x0F\x00\x00\xCA\x0D\x00\x00\x9D\x11\x00\x00\x00\x0F\x00\x00\xCA\x0D\x00\x00\xCA\x03\x00\x00\x00\x0F\x00\x00\x00\x09\x00\x00\x01\x09\x00\x00\x02\x09\x00\x00\xBE\x03\x00\x00\x02\x01\x00\x00\x0E\x01\x00\x00\x0B\x01\x00\x00\x05\x01\x00\x00\x03\x01\x00\x00\xBC\x03\x00\x00\xC5\x03\x00\x00\x04\x01\x00\x00\xC7\x03\x00\x00\x08\x01\x00\x00\x0C\x01\x00\x00\x06\x01\x00\x00\x00\x01',
_globals = (b'\x00\x00\xB1\x23boxDestroy',0,b'\x00\x00\x67\x23getLeptonicaVersion',0,b'\x00\x00\xB7\x23lept_free',0,b'\x00\x00\x69\x23makePixelSumTab8',0,b'\x00\x00\x16\x23pixAnd',0,b'\x00\x00\x23\x23pixBackgroundNorm',0,b'\x00\x00\x1B\x23pixCleanBackgroundToWhite',0,b'\x00\x00\x0D\x23pixClipRectangle',0,b'\x00\x00\x8D\x23pixColorFraction',0,b'\x00\x00\x59\x23pixColorMagnitude',0,b'\x00\x00\x0A\x23pixConvertRGBToLuminance',0,b'\x00\x00\x6B\x23pixCorrelationBinary',0,b'\x00\x00\x80\x23pixCountPixels',0,b'\x00\x00\x63\x23pixDeserializeFromMemory',0,b'\x00\x00\x50\x23pixDeskew',0,b'\x00\x00\xB4\x23pixDestroy',0,b'\x00\x00\x0A\x23pixEndianByteSwapNew',0,b'\x00\x00\x00\x23pixFindPageForeground',0,b'\x00\x00\x7B\x23pixFindSkew',0,b'\x00\x00\x2F\x23pixGammaTRC',0,b'\x00\x00\x70\x23pixGetAverageMaskedRGB',0,b'\x00\x00\x36\x23pixGlobalNormRGB',0,b'\x00\x00\x12\x23pixInvert',0,b'\x00\x00\x54\x23pixMaskOverColorPixels',0,b'\x00\x00\x85\x23pixNumSignificantGrayColors',0,b'\x00\x00\x96\x23pixOtsuAdaptiveThreshold',0,b'\x00\x00\x3E\x23pixOtsuThreshOnBackgroundNorm',0,b'\x00\x00\x60\x23pixRead',0,b'\x00\x00\x50\x23pixRemoveColormap',0,b'\x00\x00\x12\x23pixRotate180',0,b'\x00\x00\x50\x23pixRotateOrth',0,b'\x00\x00\x4B\x23pixScale',0,b'\x00\x00\xA0\x23pixSerializeToMemory',0,b'\x00\x00\xA5\x23pixWriteImpliedFormat',0,b'\x00\x00\xAB\x23pixWriteMemPng',0),
_struct_unions = ((b'\x00\x00\x00\xBA\x00\x00\x00\x02Box',b'\x00\x00\x02\x11x',b'\x00\x00\x02\x11y',b'\x00\x00\x02\x11w',b'\x00\x00\x02\x11h',b'\x00\x00\xC7\x11refcount'),(b'\x00\x00\x00\xBB\x00\x00\x00\x02Pix',b'\x00\x00\xC7\x11w',b'\x00\x00\xC7\x11h',b'\x00\x00\xC7\x11d',b'\x00\x00\xC7\x11spp',b'\x00\x00\xC7\x11wpl',b'\x00\x00\xC7\x11refcount',b'\x00\x00\x02\x11xres',b'\x00\x00\x02\x11yres',b'\x00\x00\x02\x11informat',b'\x00\x00\x02\x11special',b'\x00\x00\xBD\x11text',b'\x00\x00\xC3\x11colormap',b'\x00\x00\xC6\x11data'),(b'\x00\x00\x00\xBC\x00\x00\x00\x02PixColormap',b'\x00\x00\xB8\x11array',b'\x00\x00\x02\x11depth',b'\x00\x00\x02\x11nalloc',b'\x00\x00\x02\x11n')),
_typenames = (b'\x00\x00\x00\xBABOX',b'\x00\x00\x00\xBBPIX',b'\x00\x00\x00\xBCPIXCMAP',b'\x00\x00\x00\x1Fl_float32',b'\x00\x00\x00\xBFl_float64',b'\x00\x00\x00\xC1l_int16',b'\x00\x00\x00\x02l_int32',b'\x00\x00\x00\xC0l_int64',b'\x00\x00\x00\xC2l_int8',b'\x00\x00\x00\xC9l_uint16',b'\x00\x00\x00\xC7l_uint32',b'\x00\x00\x00\xC8l_uint64',b'\x00\x00\x00\xC5l_uint8'),
)
@@ -1,4 +1,20 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from cffi import FFI
@@ -183,10 +199,39 @@ pixColorMagnitude(PIX *pixs,
l_int32 bwhite,
l_int32 type);
void
PIX *
pixMaskOverColorPixels(PIX *pixs,
l_int32 threshdiff,
l_int32 mindist);
l_int32
pixGetAverageMaskedRGB(PIX *pixs,
PIX *pixm,
l_int32 x,
l_int32 y,
l_int32 factor,
l_int32 type,
l_float32 *prval,
l_float32 *pgval,
l_float32 *pbval);
PIX *
pixGlobalNormRGB(PIX * pixd,
PIX * pixs,
l_int32 rval,
l_int32 gval,
l_int32 bval,
l_int32 mapval);
PIX *
pixInvert(PIX * pixd,
PIX * pixs);
void
boxDestroy(BOX **pbox);
void lept_free(void *ptr);
void
lept_free(void *ptr);
""")
+104 -6
View File
@@ -1,12 +1,30 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
# Generate a PDFA_def.ps file for Ghostscript >= 9.14
from string import Template
import codecs
from binascii import hexlify
from datetime import datetime
from xml.parsers.expat import ExpatError
import pkg_resources
import PyPDF2 as pypdf
from defusedxml.minidom import parseString as defused_parseString
from unittest.mock import patch
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
@@ -79,12 +97,87 @@ def encode_text_string(s: str) -> str:
return ''
utf16_bytes = s.encode('utf-16be')
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
ascii_hex_bytes = hexlify(b'\xfe\xff' + utf16_bytes)
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
return ascii_hex_str
def encode_pdf_date(d: datetime) -> str:
"""Encode Python datetime object as PDF date string
From Adobe pdfmark manual:
(D:YYYYMMDDHHmmSSOHH'mm')
D: is an optional prefix. YYYY is the year. All fields after the year are
optional. MM is the month (01-12), DD is the day (01-31), HH is the
hour (00-23), mm are the minutes (00-59), and SS are the seconds
(00-59). The remainder of the string defines the relation of local
time to GMT. O is either + for a positive difference (local time is
later than GMT) or - (minus) for a negative difference. HH' is the
absolute value of the offset from GMT in hours, and mm' is the
absolute value of the offset in minutes. If no GMT information is
specified, the relation between the specified time and GMT is
considered unknown. Regardless of whether or not GMT
information is specified, the remainder of the string should specify
the local time.
"""
pdfmark_date_fmt = r'%Y%m%d%H%M%S'
s = d.strftime(pdfmark_date_fmt)
tz = d.strftime('%z')
if tz == 'Z':
s += "+00'00'"
elif tz != '':
sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
else:
raise ValueError("Naive timezone not supported")
return s
def decode_pdf_date(s: str) -> datetime:
pdfmark_date_fmts = (
r'%Y%m%d%H%M%S%z', # +0430 etc
r'%Y%m%d%H%M%S', # no time zone
r'%Y%m%d%H%M%SZ') # trailing Z
if s.startswith('D:'):
s = s[2:]
s = s.replace("'", "") # Remove apos from PDF time strings
for fmt in pdfmark_date_fmts:
try:
return datetime.strptime(s, fmt)
except ValueError:
continue
return None
def _get_pdfmark_dates(pdfmark):
"""Encode dates for pdfmark Postscript. The best way to deal with a
missing date entry is set it to null, because if the key is omitted
Ghostscript will set it to now - we do not want to erase the fact that
the value was unknown. Setting to an empty string breaks Ghostscript
9.22 as reported here:
https://bugs.ghostscript.com/show_bug.cgi?id=699182
"""
for key in ('/CreationDate', '/ModDate'):
if key not in pdfmark:
continue
if pdfmark[key].strip() == '':
yield ' {} null'.format(key)
continue
date_str = pdfmark[key]
if date_str.startswith('D:'):
date_str = date_str[2:]
yield ' {} (D:{})'.format(key, date_str)
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
"""Create a Postscript file for Ghostscript. pdfmark contains the various
objects as strings; these must be encoded in ASCII, and dates have a
special format."""
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
# ERROR: VMerror (-25) on closing pdfwrite device.
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
@@ -93,12 +186,12 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
docinfo_line_template = ' {key} <{value}>'
def docinfo_gen():
yield from _get_pdfmark_dates(pdfmark)
for key in docinfo_keys:
if key in pdfmark and pdfmark[key].strip() != '':
line = docinfo_line_template.format(
key=key, value=encode_text_string(pdfmark[key]))
yield line
docinfo = '\n'.join(docinfo_gen())
t = Template(pdfa_def_template)
@@ -131,9 +224,14 @@ def file_claims_pdfa(filename):
This checks if the XMP metadata contains a PDF/A marker.
"""
pdf = pypdf.PdfFileReader(filename)
xmp = pdf.getXmpMetadata()
try:
# Monkeypatch PyPDF2 to use defusedxml as its XML parser, for safety
with patch('xml.dom.minidom.parseString', new=defused_parseString):
xmp = pdf.getXmpMetadata()
except ExpatError:
return {'pass': False, 'output': 'pdf',
'conformance': 'Invalid XML metadata'}
try:
pdfa_nodes = xmp.getNodesInNamespace(
+85 -31
View File
@@ -1,18 +1,37 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import Popen, PIPE
from decimal import Decimal
from math import hypot, isclose
import re
import sys
import PyPDF2 as pypdf
from collections import namedtuple
from collections.abc import MutableMapping, Mapping
import warnings
from pathlib import Path
from enum import Enum
from .helpers import universal_open
from contextlib import contextmanager
import PyPDF2 as pypdf
from .lib import fitz
from .helpers import universal_open, fspath
matrix_mult = pypdf.pdf.utils.matrixMultiply
@@ -101,7 +120,8 @@ XobjectSettings = namedtuple('XobjectSettings',
InlineSettings = namedtuple('InlineSettings',
['settings', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo', ['xobject_settings', 'inline_images'])
ContentsInfo = namedtuple('ContentsInfo',
['xobject_settings', 'inline_images', 'found_text'])
def _normalize_stack(operations):
@@ -148,6 +168,7 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
ctm = _matrix_from_shorthand(initial_shorthand)
xobject_settings = []
inline_images = []
found_text = False
for n, op in enumerate(_normalize_stack(operations)):
operands, command = op
@@ -177,10 +198,14 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
settings=settings, shorthand=_shorthand_from_matrix(ctm),
stack_depth=len(stack))
inline_images.append(inline)
elif command in (b'Tj', b'TJ', b'"', b"'"):
found_text = True
return ContentsInfo(
xobject_settings=xobject_settings,
inline_images=inline_images)
inline_images=inline_images,
found_text=found_text)
def _get_dpi(ctm_shorthand, image_size):
@@ -525,32 +550,47 @@ def _find_images(*, pdf, container, shorthand=None):
yield from _find_form_xobject_images(pdf, container, contentsinfo)
def _page_has_text(pdf, page):
if not '/Contents' in page:
def _naive_find_text(*, pdf, page):
if not(page.get('/Type') == '/Page' and '/Contents' in page):
# Not a page, or has no /Contents => no text
return False
# Simple test
text = page.extractText()
if text.strip() != '':
# First we check the main content stream
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
contentsinfo = _interpret_contents(contentstream, UNIT_SQUARE)
if contentsinfo.found_text:
return True
# More nuanced test to deal with quirks of Tesseract PDF generation
# Check if there's a Glyphless font
try:
font = page['/Resources']['/Font']
except KeyError:
pass
else:
font_objects = list(font.keys())
for font_object in font_objects:
basefont = font[font_object]['/BaseFont']
if basefont.endswith('GlyphLessFont'):
return True
# Then see if there is a Form XObject with with a content stream
# that might have text. For full completeness we should recursively
# search nested Form XObjects, as we do with images. But that is
# rare.
if '/Resources' in page:
resources = page['/Resources']
if '/XObject' in resources:
for xobj in resources['/XObject']:
candidate = resources['/XObject'][xobj]
if candidate['/Subtype'] != '/Form':
continue
form_xobject = candidate
# Content stream is attached to Form XObject dictionary
contentstream = pypdf.pdf.ContentStream(form_xobject, pdf)
sub_contentsinfo = _interpret_contents(
contentstream, UNIT_SQUARE)
if sub_contentsinfo.found_text:
return True
return False
def _pdf_get_pageinfo(pdf, pageno: int):
def _page_has_text(infile, pageno):
doc = fitz.Document(infile)
text = doc.getPageText(pageno)
if text.strip() != '':
return True
return False
def _pdf_get_pageinfo(pdf, pageno: int, infile):
pageinfo = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
@@ -562,7 +602,10 @@ def _pdf_get_pageinfo(pdf, pageno: int):
page = pdf.pages[pageno]
pageinfo['has_text'] = _page_has_text(pdf, page)
if fitz:
pageinfo['has_text'] = _page_has_text(str(infile), pageno)
else:
pageinfo['has_text'] = _naive_find_text(pdf=pdf, page=page)
width_pt = page.mediaBox.getWidth()
height_pt = page.mediaBox.getHeight()
@@ -596,13 +639,14 @@ def _pdf_get_pageinfo(pdf, pageno: int):
def _pdf_get_all_pageinfo(infile):
with universal_open(infile, 'rb') as f:
pdf = pypdf.PdfFileReader(f)
return [PageInfo(pdf, n) for n in range(pdf.numPages)]
return [PageInfo(pdf, n, infile) for n in range(pdf.numPages)]
class PageInfo:
def __init__(self, pdf, pageno):
def __init__(self, pdf, pageno, infile):
self._pageno = pageno
self._pageinfo = _pdf_get_pageinfo(pdf, pageno)
self._infile = infile
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile)
@property
def pageno(self):
@@ -662,10 +706,6 @@ class PageInfo:
else:
return '1.5'
@property
def images(self):
return self._pageinfo['images']
def __repr__(self):
return (
'<PageInfo '
@@ -683,6 +723,10 @@ class PdfInfo:
def __init__(self, infile):
self._infile = infile
self._pages = _pdf_get_all_pageinfo(infile)
if fitz:
self._toc = fitz.Document(fspath(infile)).getToC()
else:
self._toc = []
@property
def pages(self):
@@ -697,6 +741,16 @@ class PdfInfo:
def has_userunit(self):
return any(page.userunit != 1.0 for page in self.pages)
@property
def filename(self):
if not isinstance(self._infile, (str, Path)):
raise NotImplementedError("can't get filename from stream")
return self._infile
@property
def table_of_contents(self):
return self._toc
def __getitem__(self, item):
return self._pages[item]
+306 -153
View File
@@ -1,36 +1,48 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from contextlib import suppress
from tempfile import mkdtemp
from functools import partial
from shutil import copyfileobj
from pathlib import Path
from datetime import datetime, timezone
import sys
import os
import re
import shutil
import warnings
import multiprocessing
import atexit
import textwrap
import re
import img2pdf
import logging
import argparse
import PyPDF2 as pypdf
from PIL import Image
from PIL import Image
from ruffus import formatter, regex, Pipeline, suffix
from .hocrtransform import HocrTransform
from .pdfinfo import PdfInfo, Encoding, Colorspace
from .pdfa import generate_pdfa_ps, file_claims_pdfa
from .pdfa import generate_pdfa_ps, encode_pdf_date
from .helpers import re_symlink, is_iterable_notstr, page_number
from .exec import ghostscript, tesseract, qpdf
from .exceptions import *
from .lib import fitz
from .exceptions import PdfMergeFailedError, UnsupportedImageFormatError, \
DpiError, PriorOcrFoundError, InputFileError
from . import leptonica
from . import PROGRAM_NAME, VERSION
VECTOR_PAGE_DPI = 400
# -------------
@@ -51,6 +63,8 @@ class JobContext:
def __init__(self):
self.pdfinfo = None
self.options = None
self.work_folder = None
def generate_pdfinfo(self, infile):
self.pdfinfo = PdfInfo(infile)
@@ -156,6 +170,22 @@ def triage_image_file(input_file, output_file, log, options):
raise UnsupportedImageFormatError() from e
def _pdf_guess_version(input_file, search_window=1024):
"""Try to find version signature at start of file.
Not robust enough to deal with appended files.
Returns empty string if not found, indicating file is probably not PDF.
"""
with open(input_file, 'rb') as f:
signature = f.read(1024)
m = re.search(br'%PDF-(\d\.\d)', signature)
if m:
return m.group(1)
return ''
def triage(
input_file,
output_file,
@@ -164,14 +194,12 @@ def triage(
options = context.get_options()
try:
with open(input_file, 'rb') as f:
signature = f.read(1024)
if b'%PDF' in signature:
if options.image_dpi:
log.warning("Argument --image-dpi ignored because the "
"input file is a PDF, not an image.")
re_symlink(input_file, output_file, log)
return
if _pdf_guess_version(input_file):
if options.image_dpi:
log.warning("Argument --image-dpi ignored because the "
"input file is a PDF, not an image.")
re_symlink(input_file, output_file, log)
return
except EnvironmentError as e:
log.error(e)
raise InputFileError() from e
@@ -179,29 +207,43 @@ def triage(
triage_image_file(input_file, output_file, log, options)
def repair_pdf(
def repair_and_parse_pdf(
input_file,
output_file,
log,
context):
options = context.get_options()
qpdf.repair(input_file, output_file, log)
if not options.skip_repair:
log.debug("Beginning qpdf repair...")
qpdf.repair(input_file, output_file, log)
log.debug("Repair OK; beginning parse...")
else:
re_symlink(input_file, output_file, log)
pdfinfo = PdfInfo(output_file)
if pdfinfo.has_userunit and options.output_type == 'pdfa':
log.error("This input file uses a PDF feature that is not supported "
log.error(
"This input file uses a PDF feature that is not supported "
"by Ghostscript, so you cannot use --output-type=pdfa for this "
"file. (Specifically, it uses the PDF-1.6 /UserUnit feature to "
"support very large or small page sizes, and Ghostscript cannot "
"output these files.) Use --output-type=pdf instead."
)
raise InputFileError()
if len(pdfinfo.pages) > 2000 and sys.version_info[0:2] <= (3, 5):
log.warning(
"Performance regressions are known occur with Python 3.5 for "
"high page count files. Python 3.6 or newer is recommended."
)
context.set_pdfinfo(pdfinfo)
log.debug(pdfinfo)
def get_pageinfo(input_file, context):
"Get zero-based page info implied by filename, e.g. 000002.pdf -> 1"
pageno = page_number(input_file) - 1
pageinfo = context.get_pdfinfo()[pageno]
return pageinfo
@@ -236,7 +278,30 @@ def get_canvas_square_dpi(pageinfo, options):
def is_ocr_required(pageinfo, log, options):
page = pageinfo.pageno + 1
ocr_required = True
if not pageinfo.images:
if pageinfo.has_text:
msg = "{0:4d}: page already has text! {1}"
if not options.force_ocr and not options.skip_text:
log.error(msg.format(page,
"aborting (use --force-ocr to force OCR)"))
raise PriorOcrFoundError()
elif options.force_ocr:
log.info(msg.format(page,
"rasterizing text and running OCR anyway"))
ocr_required = True
elif options.skip_text:
log.info(msg.format(page,
"skipping all processing on this page"))
ocr_required = False
elif not pageinfo.images and not options.lossless_reconstruction:
# We found a page with no images and no text. That means it may
# have vector art that the user wants to OCR. If we determined
# lossless reconstruction is not possible then we have to rasterize
# the image. So if OCR is being forced, take that to mean YES, go
# ahead and rasterize. If not forced, then pretend there's no text
# on the page at all so we don't lose anything.
# This could be made smarter by explicitly searching for vector art.
if options.force_ocr and options.oversample:
# The user really wants to reprocess this file
log.info(
@@ -255,23 +320,9 @@ def is_ocr_required(pageinfo, log, options):
else:
log.info(
"{0:4d}: page has no images - "
"skipping all processing on this page".format(page))
ocr_required = False
elif pageinfo.has_text:
msg = "{0:4d}: page already has text! {1}"
if not options.force_ocr and not options.skip_text:
log.error(msg.format(page,
"aborting (use --force-ocr to force OCR)"))
raise PriorOcrFoundError()
elif options.force_ocr:
log.info(msg.format(page,
"rasterizing text and running OCR anyway"))
ocr_required = True
elif options.skip_text:
log.info(msg.format(page,
"skipping all processing on this page"))
"skipping all processing on this page to avoid losing detail. "
"Use --force-ocr if you wish to perform OCR on pages that "
"have vector content.".format(page))
ocr_required = False
if ocr_required and options.skip_big and pageinfo.images:
@@ -285,7 +336,7 @@ def is_ocr_required(pageinfo, log, options):
return ocr_required
def split_pages(
def pre_split_pages(
input_files,
output_files,
log,
@@ -311,20 +362,45 @@ def split_pages(
pdfinfo = context.get_pdfinfo()
npages = len(pdfinfo)
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, context)
# Ruffus needs to see a file for any task it generates, so create
# empty placeholders for every page.
for n in range(npages):
page = Path(work_folder) / '{0:06d}.presplit.pdf'.format(n + 1)
page.touch()
def split_page(
placeholder_file,
output_file,
log,
context):
pageno = page_number(placeholder_file) - 1
input_pdf = context.get_pdfinfo().filename
qpdf.extract_page(input_pdf, output_file, pageno)
def ocr_or_skip(
input_files,
output_files,
log,
context):
options = context.get_options()
work_folder = context.get_work_folder()
pdfinfo = context.get_pdfinfo()
for input_file in input_files:
pageno = page_number(input_file) - 1
pageinfo = pdfinfo[pageno]
alt_suffix = \
'.ocr.page.pdf' if is_ocr_required(pageinfo, log, options) \
else '.skip.page.pdf'
re_symlink(
filename,
input_file,
os.path.join(
work_folder,
os.path.basename(filename)[0:6] + alt_suffix),
os.path.basename(input_file)[0:6] + alt_suffix),
log)
@@ -555,19 +631,18 @@ def select_visible_page_image(
log.debug('{:4d}: JPEG input -> JPEG output'.format(
page_number(image)))
# If all images were JPEGs originally, produce a JPEG as output
im = Image.open(image)
with Image.open(image) as im:
# At this point the image should be a .png, but deskew, unpaper
# might have removed the DPI information. In this case, fall back to
# square DPI used to rasterize. When the preview image was
# rasterized, it was also converted to square resolution, which is
# what we want to give tesseract, so keep it square.
fallback_dpi = get_page_square_dpi(pageinfo, options)
dpi = im.info.get('dpi', (fallback_dpi, fallback_dpi))
# At this point the image should be a .png, but deskew, unpaper might
# have removed the DPI information. In this case, fall back to square
# DPI used to rasterize. When the preview image was rasterized, it
# was also converted to square resolution, which is what we want to
# give tesseract, so keep it square.
fallback_dpi = get_page_square_dpi(pageinfo, options)
dpi = im.info.get('dpi', (fallback_dpi, fallback_dpi))
# Pillow requires integer DPI
dpi = round(dpi[0]), round(dpi[1])
im.save(output_file, format='JPEG', dpi=dpi)
# Pillow requires integer DPI
dpi = round(dpi[0]), round(dpi[1])
im.save(output_file, format='JPEG', dpi=dpi)
else:
re_symlink(image, output_file, log)
@@ -589,25 +664,25 @@ def select_image_layer(
log.debug("{:4d}: page eligible for lossless reconstruction".format(
page_number(page_pdf)))
re_symlink(page_pdf, output_file, log)
else:
pageinfo = get_pageinfo(image, context)
return
# We rasterize a square DPI version of each page because most image
# processing tools don't support rectangular DPI. Use the square DPI
# as it accurately describes the image. It would be possible to
# resample the image at this stage back to non-square DPI to more
# closely resemble the input, except that the hocr renderer does not
# understand non-square DPI. The sandwich renderer would be fine.
dpi = get_page_square_dpi(pageinfo, options)
layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi))
pageinfo = get_pageinfo(image, context)
with open(image, 'rb') as imfile, \
open(output_file, 'wb') as pdf:
log.debug('{:4d}: convert'.format(page_number(page_pdf)))
img2pdf.convert(
imfile, with_pdfrw=False,
layout_fun=layout_fun, outputstream=pdf)
log.debug('{:4d}: convert done'.format(page_number(page_pdf)))
# We rasterize a square DPI version of each page because most image
# processing tools don't support rectangular DPI. Use the square DPI as it
# accurately describes the image. It would be possible to resample the image
# at this stage back to non-square DPI to more closely resemble the input,
# except that the hocr renderer does not understand non-square DPI. The
# sandwich renderer would be fine.
dpi = get_page_square_dpi(pageinfo, options)
layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpi, dpi))
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
log.debug('{:4d}: convert'.format(page_number(page_pdf)))
img2pdf.convert(
imfile, with_pdfrw=False,
layout_fun=layout_fun, outputstream=pdf)
log.debug('{:4d}: convert done'.format(page_number(page_pdf)))
def render_hocr_page(
@@ -622,8 +697,8 @@ def render_hocr_page(
hocrtransform = HocrTransform(hocr, dpi)
hocrtransform.to_pdf(output_file, imageFileName=None,
showBoundingboxes=False, invisibleText=True)
showBoundingboxes=False, invisibleText=True,
interwordSpaces=True)
def flatten_groups(groups):
for obj in groups:
@@ -647,8 +722,8 @@ def render_hocr_debug_page(
hocrtransform = HocrTransform(hocr, dpi)
hocrtransform.to_pdf(output_file, imageFileName=None,
showBoundingboxes=True, invisibleText=False)
showBoundingboxes=True, invisibleText=False,
interwordSpaces=True)
def combine_layers(
infiles,
@@ -660,8 +735,8 @@ def combine_layers(
image = next(ii for ii in flatten_groups(infiles)
if ii.endswith('.image-layer.pdf'))
pdf_text = pypdf.PdfFileReader(open(text, "rb"))
pdf_image = pypdf.PdfFileReader(open(image, "rb"))
pdf_text = pypdf.PdfFileReader(text)
pdf_image = pypdf.PdfFileReader(image)
page_text = pdf_text.getPage(0)
@@ -706,7 +781,7 @@ def combine_layers(
if rotation != 0:
log.info("{0:4d}: rotating image layer {1} degrees".format(
page_number(image), rotation, tx, ty))
page_number(image), rotation))
try:
page_text.mergeRotatedScaledTranslatedPage(
@@ -803,12 +878,8 @@ def get_pdfmark(base_pdf, options):
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'),
}
pdfmark = {k: from_document_info(k) for k in
('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')}
if options.title:
pdfmark['/Title'] = options.title
if options.author:
@@ -826,9 +897,10 @@ def get_pdfmark(base_pdf, options):
renderer_tag = 'OCR'
pdfmark['/Creator'] = '{0} {1} / Tesseract {2} {3}'.format(
PROGRAM_NAME, VERSION,
renderer_tag,
tesseract.version())
PROGRAM_NAME, VERSION,
renderer_tag,
tesseract.version())
pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc))
return pdfmark
@@ -855,12 +927,21 @@ def skip_page(
re_symlink(input_file, output_file, log)
def merge_pages_ghostscript(
def _merge_pages_common(
input_files_groups,
output_file,
log,
context):
options = context.get_options()
"""Determine ordered list of PDF pages to merge. Returns PDF from which
metadata should be drawn if present (for qpdf)."""
input_files = list(f for f in flatten_groups(input_files_groups)
if not f.endswith('.txt'))
metadata_file = next(
(ii for ii in input_files if ii.endswith('.repaired.pdf')), None)
if metadata_file in input_files:
input_files.remove(metadata_file)
def input_file_order(s):
'''Sort order: All rendered pages followed
by their debug page, if any, followed by Postscript stub.
@@ -874,18 +955,35 @@ def merge_pages_ghostscript(
key += 1
return key
input_files = (f for f in flatten_groups(input_files_groups)
if not f.endswith('.txt'))
pdf_pages = sorted(input_files, key=input_file_order)
log.debug("Final pages: " + "\n".join(pdf_pages))
return pdf_pages, metadata_file
def merge_pages_ghostscript(
input_files_groups,
output_file,
log,
context):
options = context.get_options()
pdf_pages, _ = _merge_pages_common(
input_files_groups, output_file, log, context)
input_pdfinfo = context.get_pdfinfo()
ghostscript.generate_pdfa(
pdf_version=input_pdfinfo.min_version,
pdf_pages=pdf_pages,
output_file=output_file,
output_file=output_file + '_toc.pdf',
compression=options.pdfa_image_compression,
log=log,
threads=options.jobs or 1)
threads=options.jobs or 1,
pdfa_part=('1' if options.output_type == 'pdfa-1' else '2'))
if fitz:
doc = fitz.Document(output_file + '_toc.pdf')
doc.setToC(input_pdfinfo.table_of_contents)
doc.save(output_file)
else:
os.replace(output_file + '_toc.pdf', output_file)
def merge_pages_qpdf(
@@ -894,23 +992,8 @@ def merge_pages_qpdf(
log,
context):
options = context.get_options()
input_files = list(f for f in flatten_groups(input_files_groups)
if not f.endswith('.txt'))
metadata_file = next(
(ii for ii in input_files if ii.endswith('.repaired.pdf')))
input_files.remove(metadata_file)
def input_file_order(s):
'''Sort order: All rendered pages followed
by their debug page.'''
key = page_number(s) * 10
if 'debug' in os.path.basename(s):
key += 1
return key
pdf_pages = sorted(input_files, key=input_file_order)
log.debug("Final pages: " + "\n".join(pdf_pages))
pdf_pages, metadata_file = _merge_pages_common(
input_files_groups, output_file, log, context)
reader_metadata = pypdf.PdfFileReader(metadata_file)
pdfmark = get_pdfmark(reader_metadata, options)
@@ -919,6 +1002,8 @@ def merge_pages_qpdf(
first_page = pypdf.PdfFileReader(pdf_pages[0])
writer = pypdf.PdfFileWriter()
# copy version from source
writer._header = b'%PDF-' + _pdf_guess_version(pdf_pages[0])
writer.appendPagesFromReader(first_page)
writer.addMetadata(pdfmark)
writer_file = pdf_pages[0].replace('.pdf', '.metadata.pdf')
@@ -928,7 +1013,38 @@ def merge_pages_qpdf(
pdf_pages[0] = writer_file
qpdf.merge(input_files=pdf_pages, output_file=output_file,
min_version=context.get_pdfinfo().min_version)
min_version=context.get_pdfinfo().min_version,
log=log)
def merge_pages_mupdf(
input_files_groups,
output_file,
log,
context):
assert fitz
options = context.get_options()
pdf_pages, metadata_file = _merge_pages_common(
input_files_groups, output_file, log, context)
doc = fitz.Document()
reader_metadata = pypdf.PdfFileReader(metadata_file)
pdfmark = get_pdfmark(reader_metadata, options)
pdfmark['/Producer'] = 'PyMuPDF ' + fitz.version[0]
pymupdf_metadata = {(k[1].lower() + k[2:]) : v for k, v in pdfmark.items()}
for pdf_page in pdf_pages:
page = fitz.open(pdf_page)
doc.insertPDF(page)
metadata = fitz.open(metadata_file)
toc = metadata.getToC(simple=False)
doc.setToC(toc)
doc.setMetadata(pymupdf_metadata)
doc.save(output_file, garbage=4, deflate=True)
def merge_sidecars(
@@ -936,7 +1052,6 @@ def merge_sidecars(
output_file,
log,
context):
options = context.get_options()
pdfinfo = context.get_pdfinfo()
txt_files = [None] * len(pdfinfo)
@@ -947,21 +1062,30 @@ def merge_sidecars(
txt_files[idx] = infile
def write_pages(stream):
for page_number, txt_file in enumerate(txt_files):
if page_number != 0:
for page_num, txt_file in enumerate(txt_files):
if page_num != 0:
stream.write('\f') # Form feed between pages
if txt_file:
with open(txt_file, 'r') as in_:
stream.write(in_.read())
with open(txt_file, 'r', encoding="utf-8") as in_:
txt = in_.read()
# Tesseract v4 alpha started adding form feeds in
# commit aa6eb6b
# No obvious way to detect what binaries will do this, so
# for consistency just ignore its form feeds and insert our
# own
if txt.endswith('\f'):
stream.write(txt[:-1])
else:
stream.write(txt)
else:
stream.write('[OCR skipped on page {}]'.format(
page_number + 1))
page_num + 1))
if output_file == '-':
write_pages(sys.stdout)
sys.stdout.flush()
else:
with open(output_file, 'w', encoding='utf-8') as out:
with open(output_file, 'w', encoding="utf-8") as out:
write_pages(out)
@@ -972,13 +1096,16 @@ def copy_final(
context):
input_file = next((ii for ii in input_files if ii.endswith('.pdf')))
if output_file == '-':
from shutil import copyfileobj
with open(input_file, 'rb') as input_stream:
with open(input_file, 'rb') as input_stream:
if output_file == '-':
copyfileobj(input_stream, sys.stdout.buffer)
sys.stdout.flush()
else:
shutil.copy(input_file, output_file)
else:
# At this point we overwrite the output_file specified by the user
# use copyfileobj because then we use open() to create the file and
# get the appropriate umask, ownership, etc.
with open(output_file, 'wb') as output_stream:
copyfileobj(input_stream, output_stream)
def build_pipeline(options, work_folder, log, context):
@@ -992,8 +1119,8 @@ def build_pipeline(options, work_folder, log, context):
output=os.path.join(work_folder, 'origin.pdf'),
extras=[log, context])
task_repair_pdf = main_pipeline.transform(
task_func=repair_pdf,
task_repair_and_parse_pdf = main_pipeline.transform(
task_func=repair_and_parse_pdf,
input=task_triage,
filter=suffix('.pdf'),
output='.repaired.pdf',
@@ -1001,16 +1128,31 @@ def build_pipeline(options, work_folder, log, context):
extras=[log, context])
# Split (kwargs for split seems to be broken, so pass plain args)
task_split_pages = main_pipeline.split(
split_pages,
task_repair_pdf,
os.path.join(work_folder, '*.page.pdf'),
task_pre_split_pages = main_pipeline.split(
pre_split_pages,
task_repair_and_parse_pdf,
os.path.join(work_folder, '*.presplit.pdf'),
extras=[log, context])
task_split_pages = main_pipeline.transform(
task_func=split_page,
input=task_pre_split_pages,
filter=suffix('.presplit.pdf'),
output='.page.pdf',
output_dir=work_folder,
extras=[log, context])
task_ocr_or_skip = main_pipeline.split(
ocr_or_skip,
task_split_pages,
[os.path.join(work_folder, '*.ocr.page.pdf'),
os.path.join(work_folder, '*.skip.page.pdf')],
extras=[log, context])
# Rasterize preview
task_rasterize_preview = main_pipeline.transform(
task_func=rasterize_preview,
input=task_split_pages,
input=task_ocr_or_skip,
filter=suffix('.page.pdf'),
output='.preview.jpg',
output_dir=work_folder,
@@ -1020,7 +1162,7 @@ def build_pipeline(options, work_folder, log, context):
# Orient
task_orient_page = main_pipeline.collate(
task_func=orient_page,
input=[task_split_pages, task_rasterize_preview],
input=[task_ocr_or_skip, task_rasterize_preview],
filter=regex(r".*/(\d{6})(\.ocr|\.skip)(?:\.page\.pdf|\.preview\.jpg)"),
output=os.path.join(work_folder, r'\1\2.oriented.pdf'),
extras=[log, context])
@@ -1073,8 +1215,6 @@ def build_pipeline(options, work_folder, log, context):
extras=[log, context])
task_ocr_tesseract_hocr.graphviz(fillcolor='"#00cc66"')
task_ocr_tesseract_hocr.active_if(options.pdf_renderer == 'hocr')
if tesseract.v4():
task_ocr_tesseract_hocr.jobs_limit(2) # Uses multi-core on its own
task_select_visible_page_image = main_pipeline.collate(
task_func=select_visible_page_image,
@@ -1127,8 +1267,6 @@ def build_pipeline(options, work_folder, log, context):
extras=[log, context])
task_ocr_tesseract_textonly_pdf.graphviz(fillcolor='"#ff69b4"')
task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'sandwich')
if tesseract.v4():
task_ocr_tesseract_textonly_pdf.jobs_limit(2)
task_combine_layers = main_pipeline.collate(
task_func=combine_layers,
@@ -1139,7 +1277,8 @@ def build_pipeline(options, work_folder, log, context):
output=os.path.join(work_folder, r'\1.rendered.pdf'),
extras=[log, context])
task_combine_layers.graphviz(fillcolor='"#00cc66"')
task_combine_layers.active_if(options.pdf_renderer == 'hocr' or options.pdf_renderer == 'sandwich')
task_combine_layers.active_if(options.pdf_renderer == 'hocr' or
options.pdf_renderer == 'sandwich')
# Tesseract OCR+PDF
task_ocr_tesseract_and_render_pdf = main_pipeline.collate(
@@ -1151,17 +1290,15 @@ def build_pipeline(options, work_folder, log, context):
extras=[log, context])
task_ocr_tesseract_and_render_pdf.graphviz(fillcolor='"#66ccff"')
task_ocr_tesseract_and_render_pdf.active_if(options.pdf_renderer == 'tesseract')
if tesseract.v4():
task_ocr_tesseract_and_render_pdf.jobs_limit(2) # Uses multi-core
# PDF/A
task_generate_postscript_stub = main_pipeline.transform(
task_func=generate_postscript_stub,
input=task_repair_pdf,
input=task_repair_and_parse_pdf,
filter=formatter(r'\.repaired\.pdf'),
output=os.path.join(work_folder, 'pdfa.ps'),
extras=[log, context])
task_generate_postscript_stub.active_if(options.output_type == 'pdfa')
task_generate_postscript_stub.active_if(options.output_type.startswith('pdfa'))
# Bypass valve
@@ -1183,7 +1320,8 @@ def build_pipeline(options, work_folder, log, context):
task_generate_postscript_stub],
output=os.path.join(work_folder, 'merged.pdf'),
extras=[log, context])
task_merge_pages_ghostscript.active_if(options.output_type == 'pdfa')
task_merge_pages_ghostscript.active_if(
options.output_type.startswith('pdfa'))
task_merge_pages_qpdf = main_pipeline.merge(
task_func=merge_pages_qpdf,
@@ -1191,10 +1329,23 @@ def build_pipeline(options, work_folder, log, context):
task_render_hocr_debug_page,
task_skip_page,
task_ocr_tesseract_and_render_pdf,
task_repair_pdf],
task_repair_and_parse_pdf],
output=os.path.join(work_folder, 'merged.pdf'),
extras=[log, context])
task_merge_pages_qpdf.active_if(options.output_type == 'pdf')
task_merge_pages_qpdf.active_if(
options.output_type == 'pdf' and not fitz)
task_merge_pages_mupdf = main_pipeline.merge(
task_func=merge_pages_mupdf,
input=[task_combine_layers,
task_render_hocr_debug_page,
task_skip_page,
task_ocr_tesseract_and_render_pdf,
task_repair_and_parse_pdf],
output=os.path.join(work_folder, 'merged.pdf'),
extras=[log, context])
task_merge_pages_mupdf.active_if(
options.output_type == 'pdf' and fitz)
task_merge_sidecars = main_pipeline.merge(
task_func=merge_sidecars,
@@ -1206,8 +1357,10 @@ def build_pipeline(options, work_folder, log, context):
task_merge_sidecars.active_if(options.sidecar)
# Finalize
task_copy_final = main_pipeline.merge(
main_pipeline.merge(
task_func=copy_final,
input=[task_merge_pages_ghostscript, task_merge_pages_qpdf],
input=[task_merge_pages_ghostscript,
task_merge_pages_mupdf,
task_merge_pages_qpdf],
output=options.output_file,
extras=[log, context])
+3 -2
View File
@@ -1,4 +1,5 @@
pytest >= 3.0
pytest >= 3.2
pytest-helpers-namespace
pytest-xdist
pytest-cov
pytest-cov
pytest-timeout
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
@@ -0,0 +1,25 @@
a la Waterman
4 ons linzen
3 liter water
3 uien
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in -l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruicdnagel.
Alle uien, kerrie en gout bij de linzen
voegen, Alles aan de kook brengen,. Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gaar Zijn is de soep klaar.
@@ -0,0 +1,3 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
@@ -0,0 +1,4 @@
This should be a perfect circle:
@@ -0,0 +1,3 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
@@ -0,0 +1,4 @@
This should be a perfect circle:
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 4.0.0-beta.1' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.4nwu81i8/000001.ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 296 96 704 504">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 296 96 704 504">
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336"><span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'><strong> </strong></span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 150 592 841 622">
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8"><span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 96'><strong>This</strong></span> <span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 95'><strong>should</strong></span> <span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 95'><strong>be</strong></span> <span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 95'><strong>a</strong></span> <span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 95'><strong>perfect</strong></span> <span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 55'><strong>circle:</strong></span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,3 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
@@ -0,0 +1,4 @@
This should be a perfect circle:
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
@@ -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:
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
e 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!
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
¢ Optional SMPTE time code synchronization.
© 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,
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
— existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
Editing
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
Additional Features
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections,
Creating a Song
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
Composition Without Compromise
The technology you use should never be so complex that
it interferes with the creative process. 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!
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
@@ -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:
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
e 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!
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
¢ Optional SMPTE time code synchronization.
© 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,
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
— existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
Editing
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
Additional Features
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections,
Creating a Song
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
Composition Without Compromise
The technology you use should never be so complex that
it interferes with the creative process. 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!
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
@@ -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:
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
e 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!
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
¢ Optional SMPTE time code synchronization.
© 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,
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
— existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
Editing
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
Additional Features
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections,
Creating a Song
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
Composition Without Compromise
The technology you use should never be so complex that
it interferes with the creative process. 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!
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.0.0-beta.1 with Leptonica
@@ -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:
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
e 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!
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
¢ Optional SMPTE time code synchronization.
© 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,
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
— existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
Editing
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
Additional Features
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections,
Creating a Song
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
Composition Without Compromise
The technology you use should never be so complex that
it interferes with the creative process. 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!
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -0,0 +1 @@
Warning. Invalid resolution 0 dpi. Using 70 instead.
@@ -0,0 +1,6 @@
Page number: 0
Orientation in degrees: 0
Rotate: 0
Orientation confidence: 18.91
Script: Latin
Script confidence: 0.97
@@ -0,0 +1 @@
Warning. Invalid resolution 0 dpi. Using 70 instead.
@@ -0,0 +1,6 @@
Page number: 0
Orientation in degrees: 90
Rotate: 270
Orientation confidence: 17.21
Script: Latin
Script confidence: 1.06
@@ -0,0 +1 @@
Warning. Invalid resolution 0 dpi. Using 70 instead.
@@ -0,0 +1,6 @@
Page number: 0
Orientation in degrees: 180
Rotate: 180
Orientation confidence: 14.79
Script: Latin
Script confidence: 2.87
@@ -0,0 +1 @@
Warning. Invalid resolution 0 dpi. Using 70 instead.
@@ -0,0 +1,6 @@
Page number: 0
Orientation in degrees: 270
Rotate: 90
Orientation confidence: 19.19
Script: Latin
Script confidence: 0.58

Some files were not shown because too many files have changed in this diff Show More