Compare commits

...
267 Commits
Author SHA1 Message Date
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 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
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
James R. Barlow c4831ac00c v5.3 release notes 2017-07-27 00:11:12 -07:00
James R. Barlow 93a954ef9f Fix missing import for Py3.5 2017-07-26 23:40:01 -07:00
James R. Barlow f7ce8f44e9 Weaken the --user-words test so it will pass on Travis 2017-07-26 21:03:51 -07:00
James R. Barlow 0b012697e5 Whitelist the Latin-1 languages that work with HOCR
Omitted French because the rare 'oe' and 'ÿ' glyphs are not in Latin-1.
Basically steer people away from HOCR renderer but avoid a potential
disruptive behavior change.
2017-07-26 21:03:18 -07:00
James R. Barlow 58e357c992 Report location of attempted output_file that fails to write 2017-07-22 17:49:56 -07:00
James R. Barlow 71fbad83ad Fix py3.5 test 2017-07-21 17:01:06 -07:00
James R. Barlow 52483072dc Add a differential test that checks tesseract uses supplied word list 2017-07-21 16:40:20 -07:00
James R. Barlow 7f0b8621f3 Tests: accept rich path objects without having to str() everything 2017-07-21 16:39:22 -07:00
James R. Barlow cd8db60b06 Crash test all renderers, not just two 2017-07-21 14:10:02 -07:00
James R. Barlow 1aa34f5d2e Make some interfaces accepting of both str-paths and Path objects 2017-07-21 13:28:30 -07:00
James R. Barlow dfa1d88ce9 Fix missing user_words/user_patterns from textonly_pdf case 2017-07-20 17:14:04 -07:00
James R. Barlow dd38519f07 Merge branch 'feature/user-words' into develop
# Conflicts:
#	ocrmypdf/exec/tesseract.py
2017-07-20 16:25:20 -07:00
James R. Barlow 098f5d4f0b docs: remove deprecated example of pdftotext 2017-07-20 16:20:17 -07:00
James R. Barlow ffc685d536 docs: envvar markup 2017-07-20 16:19:57 -07:00
James R. Barlow cd1a99a0de Refactor int(os.path.basename(s)[0:6]) -> page_number(s) 2017-06-26 13:29:40 -07:00
James R. Barlow 48e3b267fc Accept PDFs with whitespace ahead of %PDF marker
Noticed in @aagahi 's fork
2017-06-26 13:17:47 -07:00
James R. Barlow 3a7c3417bb Don’t check tags and branch at the same time as Travis doesn’t get this
Travis is weird
2017-06-13 13:14:34 -07:00
James R. Barlow d792ef7222 Give the ‘auto’ renderer setting more test covfefe 2017-06-13 13:13:58 -07:00
James R. Barlow 2c24f67deb Rename “tess4” renderer to “sandwich” and make it default in Tess 3.05.01
Tesseract 3.05.01 backported the textonly_pdf=1 which allows the use
of this superior PDF renderer prior to 4.00 alpha. This means that
the tess4 name is no longer accurate, so call it a sandwich because of
its merge-preserve characteristic. Preserve the tess4 name. Fix the
documentation and tests to reflect this.

Make it the default, because it’s better. It does not have the issues
the “tesseract” renderer does prior to Tess 3.05.00 with rendering
PDFs that Ghostscript corrupts, and it produces better output without
re-rastering.

Deprecate some old stuff to avoid the test suite growing obscenely
large.
2017-06-13 13:09:12 -07:00
James R. Barlow 9e75e28d0c Homebrew needs x11 to compile Pillow 2017-06-13 11:03:26 -07:00
James R. Barlow 3232643809 Support “textonly PDF” renderer in Tesseract 3.05.01 2017-06-13 10:18:08 -07:00
James R. Barlow f7ee9e90ce Document what is meant by the ocrmypdf “API” 2017-06-13 10:15:11 -07:00
James R. Barlow 47298be132 Remove Python <3.5 test 2017-06-13 10:14:28 -07:00
James R. Barlow a88fa83515 Travis: fix deploy conditions for homebrew autobrew 2017-05-31 02:29:32 -07:00
James R. Barlow 12bfe20385 v5.1 release notes 2017-05-29 14:36:50 -07:00
James R. Barlow 3d2f6f0772 Fix tess4 test using old-style pageinfo API 2017-05-29 13:51:21 -07:00
James R. Barlow 1cb607f64b Merge UserUnit 2017-05-29 13:22:55 -07:00
James R. Barlow d3c54fbbde For —rotate-pages, rasterize preview at half DPI instead of 200 DPI
Ensures that time is not wasted on previews at higher resolution than
the input as was sometimes the case
2017-05-29 13:01:18 -07:00
James R. Barlow 28341b755f Refactor common test fixtures 2017-05-29 12:47:55 -07:00
James R. Barlow 4b5cd420e1 Add new test file 2017-05-29 12:16:08 -07:00
James R. Barlow 1d57bcc99e Fix Ghostscript rasterizing of UserUnit pages and related sizing issues 2017-05-29 12:14:10 -07:00
James R. Barlow facdd13879 Ghostscript: refactor image output resizing 2017-05-29 11:42:27 -07:00
James R. Barlow 6e891f91d3 ghostscript, qpdf: Restore API backward compatibility 2017-05-29 11:13:06 -07:00
James R. Barlow 9b50ede977 Partially solve ghostscript rasterize_pdf producing wrong file size
Kludge. Assumes JPEG for now. Messy.
2017-05-25 01:17:43 -07:00
James R. Barlow 82cf010333 Error out if trying to produce PDF/A >200” due to Ghostscript limitation 2017-05-25 00:07:29 -07:00
James R. Barlow 6ff6c8614f —output-type=pdf now outputs /UserUnit PDFs at the correct size
This currently distorts the output size because Tesseract assumes it
 knows the DPI better than we do.

Does not work for Ghostscript, because it emerges that Ghostscript
honors /UserUnit for rasterizing but not in pdfwrite (resolve/wontfix).

https://bugs.ghostscript.com/show_bug.cgi?id=690781

Ghostscript’s output would need to be patched in a PDF/A safe way for
this to work. Temporary route may be to block Ghostscript if
/UserUnit.
2017-05-24 23:26:07 -07:00
James R. Barlow eb1cd38f6c Add an open helper that is compatible with pathlib 2017-05-24 16:19:15 -07:00
James R. Barlow 148b632b4f Prove multiprocessing works, although it is still racy in some places 2017-05-23 16:32:13 -07:00
James R. Barlow 591e213713 Add more dependencies for autobrew 2017-05-23 13:52:28 -07:00
James R. Barlow 75f2262659 Ensure JobContext stuff is actually tested for IPC consistency 2017-05-19 17:57:07 -07:00
James R. Barlow d9005a1074 pdfinfo: replace most remaining dict-style access 2017-05-19 16:17:36 -07:00
James R. Barlow 3e73fa81bf pageinfo: deprecation warning 2017-05-19 16:17:07 -07:00
James R. Barlow ba6e290231 Restore old pageinfo.py to avoid breaking compatibility 2017-05-19 15:49:23 -07:00
James R. Barlow 08e47117a3 Rename pageinfo to pdfinfo 2017-05-19 15:48:23 -07:00
James R. Barlow 532ef38157 /UserUnit is a scalar, not an array 2017-05-19 14:19:50 -07:00
James R. Barlow 4c09875890 docs: upload unpaper Dropbox link, .rst typo blocking macOS install
[ci skip]
2017-05-19 12:18:09 -07:00
James R. Barlow 0e98139712 Upload to upload.pypi.org/legacy as recommend by PyPA
https://github.com/pypa/warehouse/issues/1996#issuecomment-302784126
2017-05-19 12:06:24 -07:00
James R. Barlow 4c04d802d7 Introduce /UserUnit checking 2017-05-19 12:01:19 -07:00
James R. Barlow b3dc404571 Update unpaper.deb link (fixes #171)
*Shakes fist a Dropbox*
2017-05-19 11:28:45 -07:00
James R. Barlow 8694f8d2eb Replace magic strings colorspace and encoding with Enums 2017-05-18 22:32:27 -07:00
James R. Barlow 263f9b79f4 pageinfo: debug stuff 2017-05-18 21:52:55 -07:00
James R. Barlow 56d2aae963 Refactor from ImageInfo index to attribute accessing 2017-05-18 18:39:14 -07:00
James R. Barlow 127706153d Refactor dictionary based image info to ImageInfo 2017-05-18 18:26:31 -07:00
James R. Barlow caee5b1428 Access PageInfo instance variables instead of dictionary 2017-05-18 17:12:04 -07:00
James R. Barlow 6c12e7e944 Refactor pageinfo dictionary to PageInfo() 2017-05-18 16:53:38 -07:00
James R. Barlow cd04ae6949 Refactor PdfInfo(str(filename)) -> PdfInfo(filename) 2017-05-18 16:43:50 -07:00
James R. Barlow 6a0b68298f Refactor pdf_get_all_pageinfo to PdfInfo 2017-05-18 16:31:18 -07:00
James R. Barlow 0a2f732267 docs: Fix restructured text typos 2017-05-16 23:27:10 -07:00
James R. Barlow 4bade99f27 docs: Remark that someone got bash on Windows working 2017-05-16 23:24:34 -07:00
James R. Barlow 0b048cd24e Join the build badge club 2017-05-16 23:24:05 -07:00
James R. Barlow c69ee63d82 Travis, true is a program, not a keyword 2017-05-15 15:12:14 -07:00
James R. Barlow 744fa104d7 v5.0.1 release notes (anticipating) 2017-05-14 23:59:09 -07:00
James R. Barlow e24ff0fd64 Travis: don’t update the homebrew version because we pushed to testpypi 2017-05-14 23:55:40 -07:00
James R. Barlow 5de107d44c tesseract_cache: update explanatory notes 2017-05-14 23:54:09 -07:00
James R. Barlow 131a5b741d tesseract.py: update canned HOCR template to tess 3.05 output
Seems better to not claim the existence of several entities that don’t
exist as the older one does
2017-05-14 23:40:09 -07:00
James R. Barlow 65b89687a9 ghostscript: fix missing “import sys”, only applicable for an exception 2017-05-14 23:38:52 -07:00
James R. Barlow 048ae40e75 Update copyrights 2017-05-14 23:38:28 -07:00
James R. Barlow 234183ecd2 Fix: Tesseract 3.04 is sensitive to order of configuration commands
“txt hocr” is not acceptable and does not produce expected output .txt
while “hocr text” works fine, so switch the order everywhere.

Should fix #169
2017-05-14 23:27:46 -07:00
James R. Barlow fb067dc97b cookbook: more on improving OCR 2017-05-14 23:16:47 -07:00
James R. Barlow a1fea0ce16 docs: link to OCRmyPDF-web 2017-05-14 23:16:30 -07:00
James R. Barlow e1e9135e93 Test suite: tidy up imports 2017-05-14 23:15:29 -07:00
James R. Barlow aff982036b autobrew: fix brew audit error on double blank line 2017-05-14 23:15:02 -07:00
James R. Barlow d087649eab Remove “null deploy script” since “/usr/bin/true” is equivalent 2017-05-12 15:37:02 -07:00
James R. Barlow 7f3fa46a40 v5.0 release notes 2017-05-12 14:14:28 -07:00
James R. Barlow b1f79e4d97 Disable other use redo_ocr 2017-05-12 13:24:30 -07:00
James R. Barlow 115d6df94f Warn user when —image-dpi is supplied but ignored 2017-05-12 12:09:53 -07:00
James R. Barlow 559af9635f —redo-ocr is not implemented, so disable 2017-05-12 12:08:16 -07:00
James R. Barlow cb06359c0b Turn on Tesseract 4 cache in test suite
Travis is too slow without it, and perhaps it’s overly paranoid to
never cache Tess4. Maybe nuke the cache occasionally to be safe…
2017-05-12 11:42:27 -07:00
James R. Barlow 5e26bb29d9 Update requirements files 2017-05-12 11:41:15 -07:00
James R. Barlow b0e95842b8 Fix Travis CI errors while looking around for Tess4 2017-05-12 00:40:00 -07:00
James R. Barlow 08e678f21f rst: Clean up indentation 2017-05-12 00:12:06 -07:00
James R. Barlow c17817810f Update documentation for 3.03 support removal 2017-05-12 00:08:22 -07:00
James R. Barlow ff5c38b1f7 Tell Travis to download Tesseract 4.00 from a PPA for testing 2017-05-11 23:52:13 -07:00
James R. Barlow 64314c1b82 Insist on Python 3.5 wherever we check for it 2017-05-11 23:51:45 -07:00
James R. Barlow 83230097ae Insist on Tesseract 3.04 wherever we check for it 2017-05-11 23:51:28 -07:00
James R. Barlow 8f91acf956 Remove Tesseract 3.02 and 3.03 compatibility shims 2017-05-11 23:50:52 -07:00
James R. Barlow d211722a2f .gitignore the docs Makefile 2017-05-11 23:28:52 -07:00
James R. Barlow 56e6ed1249 Fix missing import; all tests passing! 2017-05-11 23:28:05 -07:00
James R. Barlow 21982cf1cb baiona_gray remove alpha channel 2017-05-11 23:23:37 -07:00
James R. Barlow edc01408da Update the .png files, again, hopefully without corruption 2017-05-11 23:20:50 -07:00
James R. Barlow aee33c87ed Merge release notes 2017-05-11 23:11:12 -07:00
James R. Barlow 0dae1602c7 Fix missing import PIPE 2017-05-11 23:07:20 -07:00
James R. Barlow d926f07ac1 Stop git from corrupting .pngs
Grrr.
2017-05-11 23:07:06 -07:00
James R. Barlow 96045e98f4 Update develop with master changes
We’re well out of the “trivial updates” zone
2017-05-11 22:54:27 -07:00
James R. Barlow 01b7205e2c Ensure skipped pages are explained in sidecars 2017-05-11 00:43:36 -07:00
James R. Barlow c8a4cbcf17 Fix test suite breakage after sidecar feature added
Forgot to update tesseract spoofers to account for change in tesseract
parameters.  Also the change to outputting multiple files in the collate
steps affected how ruffus passes information into downstream consumers
of those files.
2017-05-11 00:17:24 -07:00
James R. Barlow 16b6442b23 Add changes to __main__.py that should have been in last commit 2017-05-10 17:55:42 -07:00
James R. Barlow 183eafa587 Implement sidecar text files (#126) 2017-05-10 15:22:44 -07:00
James R. Barlow 47a2997538 Reorganize —help text 2017-05-10 12:19:56 -07:00
James R. Barlow 37ebcadfa1 Implement —user-words, —user-patterns 2017-05-09 17:54:56 -07:00
James R. Barlow 74d98216f1 Update documentation for Ghostscript behavior 2017-05-09 17:43:39 -07:00
James R. Barlow 4bdebf573e Tell Travis CI to use multiple cores
Let’s see if this helps the build go faster
2017-05-09 17:24:32 -07:00
James R. Barlow 1606b6a383 Add —quiet (fixes #143), stop using ruffus to partially generate argparser 2017-05-09 17:24:06 -07:00
James R. Barlow 2a61902df5 Merge commit 'c4f01de231d22da5cea02c25aa581a965a37640b' 2017-05-09 16:37:55 -07:00
James R. Barlow 01a1c2b576 Implement —pdfa-image-compression to control Ghostscript’s compression
Fixes #163
2017-05-09 16:37:29 -07:00
Ingo Feinererandjbarlow83 c4f01de231 Fix typo "cutput" -> "output" (#164)
[ci skip]
2017-05-09 16:22:10 -07:00
James R. Barlow 63a4a761dd Revert "v4.5.7 release notes"
The change introduced regressions, so find another way to fix.

This reverts commit d077c03686.

[ci skip]
2017-05-08 14:38:04 -07:00
James R. Barlow d077c03686 v4.5.7 release notes 2017-05-06 22:34:54 -07:00
James R. Barlow c97ea1f2a9 Update high DPI test case to confirm the output image is not downsampled 2017-05-06 22:34:01 -07:00
James R. Barlow fd27df2abb Update documentation to warn that transparency is not tested 2017-05-06 22:33:24 -07:00
James R. Barlow bf04f03c4c Fix corrupt test file “typewriter.png”
This file is not currently used in any tests, but could be, so replace
corrupt version with a useful one.
2017-05-06 22:28:34 -07:00
James R. Barlow 93e802f473 Fix issue #163, color and grayscale images JPEG compressed when not needed 2017-05-06 22:27:25 -07:00
James R. Barlow 1464b9087a Try Travis again with null deploy for OSX 2017-05-01 17:37:59 -07:00
James R. Barlow e8cc8fc879 Add travis null_deploy for osx 2017-05-01 17:24:43 -07:00
James R. Barlow fae2119b1e v4.5.6 release notes 2017-05-01 15:55:02 -07:00
James R. Barlow aa859a4139 Fix #156 - NoneType has no ‘getObject’ for pages with no /Contents 2017-05-01 15:46:15 -07:00
James R. Barlow b9b12e2879 Ensure that ocrmypdf stops and reports an error if Ghostscript fails
Past behavior was to continue and let ruffus puke eventually
2017-05-01 15:44:21 -07:00
James R. Barlow cf643c9f43 Fix argparse.ArgumentError needs two positional args 2017-05-01 14:55:47 -07:00
James R. Barlow 5b1a7880a9 Switch to Travis triggering Docker build to skip race condition with PyPI 2017-04-28 17:18:01 -07:00
James R. Barlow 474b6b0500 v4.5.5 release notes 2017-04-28 15:27:41 -07:00
James R. Barlow 6c8c1d8173 Fix #154: KeyError ‘/Contents’ on blank pages with /Contents record 2017-04-28 15:22:05 -07:00
James R. Barlow 6a91fa637f Squash merge improvements to auto-homebrewing macOS version 2017-04-28 15:17:38 -07:00
Jakub Wilkandjbarlow83 2846fb4e31 Remove misplaced flags from re.sub() call (#153)
The 4th argument of re.sub() is maximum number of substitutions,
not flags.

Moreover, re.MULTILINE affects only semantics of ^ and $,
so it wouldn't have any effect on this regular expression.
2017-04-20 00:38:23 -07:00
James R. Barlow 6e907856f2 Refresh requirements 2017-03-29 18:15:29 -07:00
James R. Barlow 8bc6019172 Begin adding new option to redo ocr 2017-03-29 18:15:20 -07:00
James R. Barlow 059f79242e Phase out subprocess.Popen 2017-03-29 18:15:02 -07:00
James R. Barlow 89599b4812 Drop Python 3.4 compatibility 2017-03-29 15:46:53 -07:00
James R. Barlow a9f4047a97 Remove backward compatible API deprecations from v4.x 2017-03-29 15:42:54 -07:00
James R. Barlow 23227ae763 Deprecate old files 2017-03-29 15:41:52 -07:00
81 changed files with 3969 additions and 1312 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__.:
+4 -4
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
@@ -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
@@ -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/
+3
View File
@@ -6,5 +6,8 @@
*.jar binary
*.pdf binary
*.PDF binary
*.png binary
*.jpg binary
.git_archival.txt export-subst
+6 -1
View File
@@ -6,6 +6,7 @@ pyvenv.cfg
tasks.py
.bash_history
.ruffus_history.sqlite
.idea/
# Package building
*.egg-info/
@@ -21,6 +22,7 @@ ocrmypdf/version.py
docs/_build/
docs/_static/
docs/_templates/
docs/Makefile
# Code coverage
.coverage
@@ -36,4 +38,7 @@ tests/cache/
tests/output/
tests/resources/private/
tmp/
pdfbox-app*.jar
pdfbox-app*.jar
.vscode/
IDEAS
_Dockerfile.local
+55 -15
View File
@@ -7,12 +7,14 @@ cache:
- 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:
include:
- os: linux
sudo: required
language: python
python: 3.4
- os: linux
sudo: required
language: python
@@ -28,9 +30,14 @@ matrix:
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then bash .travis/linux_before_install.sh ; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash .travis/osx_before_install.sh ; fi
before_install: |
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
bash .travis/linux_before_install.sh
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
brew update && brew bundle --file=.travis/Brewfile
pip3 install --upgrade pip
pip3 install wheel
fi
install:
- pip3 install .
@@ -38,23 +45,56 @@ install:
script:
- mv ocrmypdf dont_import_this_ocrmypdf
- tesseract --version
- qpdf --version
- pytest
- mv dont_import_this_ocrmypdf ocrmypdf
after_success:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash .travis/osx_brew.sh ; fi
# See https://www.appneta.com/blog/pypi-deployment-with-travis-ci/ for
# steps to set up testpypi deploy for untagged builds if desired
deploy:
provider: pypi
# release for main pypi
# 3.6 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=
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
distributions: "sdist bdist_wheel"
on:
branch: master
tags: true
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux"
skip_upload_docs: true
# test pypi
- provider: pypi
server: https://testpypi.pypi.org/legacy/
user: ocrmypdf-travis
password:
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
distributions: "sdist"
on:
branch: develop
tags: false
condition: $TRAVIS_OS_NAME == "osx"
skip_upload_docs: true
# null deploy for osx
# we really just want to run after_deploy *after* pypi upload is done, but
# after_deploy on runs if a given box deployed
- provider: script
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
+12
View File
@@ -0,0 +1,12 @@
brew 'python'
brew 'ghostscript'
brew 'jbig2dec'
brew 'leptonica'
brew 'libffi'
brew 'libtiff'
brew 'libxml2'
brew 'openjpeg'
brew 'qpdf'
brew 'tesseract'
brew 'unpaper'
+37 -19
View File
@@ -1,3 +1,6 @@
#!/usr/bin/env python3
# © 2017-18 James R. Barlow: github.com/jbarlow83
from string import Template
from subprocess import run, PIPE
import re
@@ -11,35 +14,50 @@ class Ocrmypdf < Formula
${ocrmypdf_url}
${ocrmypdf_sha256}
depends_on :python3
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 "freetype"
depends_on "ghostscript"
depends_on "jpeg"
depends_on "libpng"
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
""")
+12 -5
View File
@@ -1,4 +1,5 @@
#!/bin/bash
# © 2017 James R. Barlow: github.com/jbarlow83
set -euo pipefail
set -x
@@ -7,10 +8,6 @@ sudo add-apt-repository ppa:heyarje/libav-11 -y
sudo apt-get update -qq
sudo apt-get install -y \
ghostscript \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
tesseract-ocr-fra \
qpdf \
poppler-utils \
libavformat56 \
@@ -18,7 +15,17 @@ sudo apt-get install -y \
libavutil54 \
libffi-dev
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
sudo apt-get update
sudo apt-get autoremove -y
sudo apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-eng \
tesseract-ocr-fra \
tesseract-ocr-deu
pip install --upgrade pip
mkdir -p packages
[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb
[ -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
sudo dpkg -i packages/unpaper_6.1-1.deb
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
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
+16 -1
View File
@@ -1,8 +1,23 @@
#!/bin/bash
# © 2017 James R. Barlow: github.com/jbarlow83
set -euo 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
git clone https://$HOMEBREW_OCRMYPDF_TOKEN@github.com/jbarlow83/homebrew-ocrmypdf.git
set -x
pushd homebrew-ocrmypdf
cp ../ocrmypdf.rb Formula/ocrmypdf.rb
git add Formula/ocrmypdf.rb
git commit -m "homebrew-ocrmypdf: automatic release $TRAVIS_BUILD_NUMBER $TRAVIS_TAG"
git push origin master
popd
+4 -1
View File
@@ -1,4 +1,7 @@
Copyright (c) 2013-2016, The OCRmyPDF Authors
Copyright (c) 2014-2017, James R. Barlow
Copyright (c) 2013-2014, Julien Pfefferkorn
Copyright (c) 2013-2017, The OCRmyPDF Authors
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
+6 -3
View File
@@ -7,12 +7,12 @@ include dev_requirements.txt
include .git_archival.txt
# docker
include Dockerfile
include Dockerfile.polyglot
include Dockerfile*
include .dockerignore
# tests
include pytest.ini
include .coveragerc
recursive-include tests *.jpg
recursive-include tests *.png
recursive-include tests *.pdf
@@ -38,11 +38,14 @@ recursive-exclude docs/_build *
recursive-include ocrmypdf/data *
include *.py
exclude tasks.py
exclude debug-runner.py
recursive-exclude .travis *
exclude .travis*
# code
recursive-include ocrmypdf *.py
exclude ocrmypdf/lib/_leptonica.py
include OCRmyPDF.sh
include docker-wrapper.sh
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
##############################################################################
# Copyright (c) 2013-14: fritz-hh from Github (https://github.com/fritz-hh)
##############################################################################
echo "This script is deprecated. Use 'ocrmypdf' instead."
python3 -m ocrmypdf.main "$@"
+17 -2
View File
@@ -1,6 +1,15 @@
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/docker/build/jbarlow83/ocrmypdf.svg
:target: https://hub.docker.com/r/jbarlow83/ocrmypdf/
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched or copy-pasted.
@@ -65,11 +74,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.
@@ -113,6 +121,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 fully supported, but support will end at the end of 2017, to allow f-strings in the code base.
Press & Media
-------------
+4 -4
View File
@@ -1,8 +1,8 @@
check-manifest >= 0.34
check-manifest >= 0.35
twine >= 1.8.1
coverage >= 4.3.1
pytest-xdist >= 1.15.0
GitPython == 2.1.0
coverage >= 4.4
pytest-xdist >= 1.16.0
GitPython == 2.1.3
# Known good versions: 1.11.1
# Known broken versions: 1.15.0
+42 -26
View File
@@ -33,20 +33,27 @@ 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 executable* first.
OCRmyPDF checks the environment variable ``OCRMYPDF_TESSERACT`` for the full path *to the tesseract binary* first.
For example, if you are testing tesseract 4.00 and don't wish to disturb your tesseract 3.04 installation, you can launch OCRmyPDF as follows:
.. envvar:: OCRMYPDF_TESSERACT
Specify the location of the Tesseract binary.
.. envvar:: TESSDATA_PREFIX
A Tesseract environment variable that overrides the path to Tesseract's data files.
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:
.. code-block:: bash
env \
OCRMYPDF_TESSERACT=/home/user/src/tesseract4/api/tesseract \
TESSDATA_PREFIX=/home/user/src/tesseract4 \
ocrmypdf --pdf-renderer tess4 --tesseract-oem 2 input.pdf output.pdf
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.)
* ``TESSDATA_PREFIX`` directs tesseract 4.0 to use LSTM training data. This is a tesseract environment variable.
* ``--pdf-renderer tess4`` takes advantage of new tesseract 4.0 PDF renderer in OCRmyPDF. (Tesseract 4.0 only.)
* ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
Overriding other support programs
"""""""""""""""""""""""""""""""""
@@ -57,7 +64,20 @@ 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.
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.
Changing tesseract configuration variables
""""""""""""""""""""""""""""""""""""""""""
@@ -95,33 +115,29 @@ rendering
Creating a new PDF from other data (such as an existing PDF).
OCRmyPDF has three PDF renderers: ``hocr``, ``tesseract`` and ``tess4``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` always selects ``hocr``.
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.
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.
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.
The ``hocr`` renderer
"""""""""""""""""""""
The ``hocr`` renderer is the default because it works in most cases. In this mode the whole PDF is rasterized, the raster image is run through OCR to generate a .hocr file, which is an HTML-like file that specifies the location of all identified words.
The .hocr file is then rendered as a PDF and merged with the image layer.
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.
This is the only option for tesseract 3.02 and older.
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.
This works in all versions of Tesseract.
The ``tesseract`` renderer
""""""""""""""""""""""""""
The tesseract renderer uses tesseract's capability to produce a PDF directly. In version 3, tesseract automatically combined the image layer and text, meaning that this mode *always* transcodes and loses potentially loses image quality and other PDF information.
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``.
It does a much better job on non-Latin text.
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.
In a future release this will become the "tess3" renderer and ultimately will be dropped.
The ``tess4`` renderer
""""""""""""""""""""""
The tess4 renderer uses tesseract 4.00 alpha's text-only PDF feature added in January 2017. This combines the advantages of the tesseract and hocr renderers, transcoding the image layer only if required by preprocessing options.
Ghostscript PDF/A still sometimes inserts spaces between words when the tess4 renderer is used, affecting search quality. ``--output-pdf pdf`` may be used to avoid this issue.
*This renderer is deprecated and will be removed whenever support for older versions of Tesseract is dropped.*
+72 -2
View File
@@ -90,13 +90,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
+73 -26
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,14 +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
Modify a file in place
""""""""""""""""""""""
@@ -35,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
"""""""""""""""""""""
@@ -44,7 +51,7 @@ 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.
@@ -56,8 +63,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>`.
@@ -65,19 +72,11 @@ Language packs must be installed for all languages specified. See :ref:`Installi
Produce PDF and text file containing OCR text
"""""""""""""""""""""""""""""""""""""""""""""
This produces a file named "output.pdf" and a companion text file named "output.txt". The ``pdftotext`` program from
`Poppler <https://poppler.freedesktop.org/>`_ is used to extract text from the finished PDF.
This produces a file named "output.pdf" and a companion text file named "output.txt".
.. code-block:: bash
ocrmypdf input.pdf - | tee output.pdf | pdftotext - output.txt
.. note::
To get pdftotext, Debian/Ubuntu users may ``apt-get install poppler-utils``
and macOS users may ``brew install poppler`` respectively.
ocrmypdf --sidecar output.txt input.pdf output.pdf
OCR images, not PDFs
--------------------
@@ -86,21 +85,27 @@ Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to
.. code-block:: bash
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
If given a single image as input, OCRmyPDF will try converting it to a PDF on its own. If the DPI specified in the image is incorrect, it can be overridden with ``--image-dpi``:
``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).
.. code-block:: bash
ocrmypdf --image-dpi 300 image.png myfile.pdf
ocrmypdf --image-dpi 300 image.png myfile.pdf
This feature may be removed at some point, because OCRmyPDF does not specialize in converting images to PDFs.
If you have multiple images, you must use ``img2pdf`` to convert the images to PDF.
.. note::
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
tesseract my-image.jpg output-prefix pdf
Image processing
----------------
@@ -119,11 +124,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)
@@ -133,12 +138,54 @@ 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
---------------------
The `Image processing`_ features can improve OCR quality.
Rotating pages and deskewing helps to ensure that the page orientation is correct before OCR begins. Removing the background and/or cleaning the page can also improve results. The ``--oversample DPI`` argument can be specified to resample images to higher resolution before attempting OCR; this can improve results as well.
OCR quality will suffer if the resolution of input images is not correct (since the range of pixel sizes that will be checked for possible fonts will also be incorrect).
+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
+147 -109
View File
@@ -1,28 +1,35 @@
Installation
============
Installing on Debian and Ubuntu
-------------------------------
OCRmyPDF requires Python 3.5 (or newer) and Tesseract 3.04 (or newer).
Installing on Debian and Ubuntu 16.10 or newer
----------------------------------------------
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
.. code-block:: bash
apt-get install ocrmypdf
apt-get install ocrmypdf
Installing on macOS
-------------------
OCRmyPDF is now a standard `Homebrew <https://brew.sh>`_ formula. To install on macOS:
.. code-block:: bash
brew tap jbarlow83/ocrmypdf
brew install ocrmypdf
brew install ocrmypdf
.. note::
Users who previously installed OCRmyPDF on macOS may need to remove the ``pip`` based installation (``pip3 uninstall ocrmypdf``).
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.
.. _Docker:
.. 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
---------------------------
@@ -37,17 +44,17 @@ successfully, your system is ready to download and execute the image:
.. code-block:: bash
docker run hello-world
docker run hello-world
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and macOS has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
.. code-block:: bash
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
Assuming you have a Docker engine running, you can download one of the three available images:
@@ -66,13 +73,13 @@ For example:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf-tess4
docker pull jbarlow83/ocrmypdf-tess4
Then tag it to give a more convenient name, just ocrmypdf:
.. code-block:: bash
docker tag jbarlow83/ocrmypdf-tess4 ocrmypdf
docker tag jbarlow83/ocrmypdf-tess4 ocrmypdf
.. _docker-polyglot:
@@ -82,38 +89,47 @@ You can then run ocrmypdf using the command:
.. code-block:: bash
docker run --rm ocrmypdf --help
docker run --rm ocrmypdf --help
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
.. note:: The working directory should be a writable local volume or Docker may not have permission to access it.
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
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
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
ocrmypdf --version # runs docker version
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
ocrmypdf --version # runs docker version
Or in the wonderful `fish shell <https://fishshell.com/>`_:
.. code-block:: fish
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
funcsave ocrmypdf
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
----------------------------
@@ -126,51 +142,57 @@ Update Homebrew:
.. code-block:: bash
brew update
brew update
Install or upgrade the required Homebrew packages, if any are missing:
.. code-block:: bash
brew install libpng openjpeg jbig2dec libtiff # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2 libffi leptonica
brew install unpaper # optional
brew install libpng openjpeg jbig2dec libtiff # image libraries
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2 libffi leptonica
brew install unpaper # optional
Python 3.4, 3.5 and 3.6 are supported.
Python 3.5 and 3.6 are supported.
Install the required Tesseract OCR engine with the language packs you plan to use:
.. code-block:: bash
brew install tesseract # Option 1: for English, French, German, Spanish
brew install tesseract # Option 1: for English, French, German, Spanish
.. _macos-all-languages:
.. code-block:: bash
brew install tesseract --with-all-languages # Option 2: for all language packs
brew install tesseract --with-all-languages # Option 2: for all language packs
Update the homebrew pip and install Pillow:
.. code-block:: bash
pip3 install --upgrade pip
pip3 install --upgrade pillow
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
or system-wide:
.. code-block:: bash
pip3 install ocrmypdf
The command line program should now be available:
.. code-block:: bash
ocrmypdf --help
ocrmypdf --help
Installing on Ubuntu 16.04 LTS
@@ -180,95 +202,109 @@ No package is currently available for Ubuntu 16.04, but you can install the depe
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
unpaper \
ghostscript \
tesseract-ocr \
qpdf \
python3-pip \
python3-cffi
sudo apt-get update
sudo apt-get install \
unpaper \
ghostscript \
tesseract-ocr \
qpdf \
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
Alternately, system-wide. Note that this may modify the system Python environment:
.. code-block:: bash
sudo pip3 install ocrmypdf
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps.
.. code-block:: bash
python3 -m venv venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip3 install ocrmypdf
python3 -m venv venv-ocrmypdf
source venv-ocrmypdf/bin/activate
pip3 install ocrmypdf
Installing on Ubuntu 14.04 LTS
------------------------------
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because of bugs in Python package installation.
Add new "apt" repositories needed for backports of Ghostscript 9.16 and libav-11, which supports unpaper 6.1. This will replace Ghostscript on your system.
.. code-block:: bash
sudo add-apt-repository ppa:vshn/ghostscript -y
sudo add-apt-repository ppa:heyarje/libav-11 -y
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:
.. code-block:: bash
sudo apt-get update
sudo apt-get update
Install system dependencies:
.. code-block:: bash
sudo apt-get install \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
libavformat56 libavcodec56 libavutil54 \
ghostscript \
tesseract-ocr \
qpdf \
python3-pip \
python3-pil \
python3-pytest \
python3-reportlab
sudo apt-get install \
software-properties-common python-software-properties \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
qpdf
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):
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 pip3 install ocrmypdf
If you wish to install OCRmyPDF to a virtual environment to isolate the system Python, you can
follow these steps. This includes a workaround `for a known, unresolved issue in Ubuntu 14.04's ensurepip
package <http://www.thefourtheye.in/2014/12/Python-venv-problem-with-ensurepip-in-Ubuntu.html>`_:
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 \
ghostscript \
tesseract-ocr \
tesseract-ocr-eng \
libavformat56 libavcodec56 libavutil54 \
wget
Now we need to install ``pip`` and let it install ocrmypdf:
.. 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
wget -O - -o /dev/null https://bootstrap.pypa.io/get-pip.py | python3.6
pip3.6 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):
.. code-block:: bash
wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O unpaper_6.1-1.deb
sudo dpkg -i unpaper_6.1-1.deb
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O unpaper_6.1-1.deb
sudo dpkg -i unpaper_6.1-1.deb
Installing on ArchLinux
-----------------------
The author is aware of an `ArchLinux package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. It seems like the following command might work.
.. code-block:: bash
pacman -S ocrmypdf
Installing on Windows
@@ -283,45 +319,47 @@ The command line syntax to run ocrmypdf from a command prompt will resemble:
.. code-block:: bat
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
`Bash on Ubuntu on Windows <https://github.com/Microsoft/BashOnWindows>`_ should also be a viable route for running the OCRmyPDF Docker container.
Installing HEAD revision from sources
-------------------------------------
If you have ``git`` and Python 3.4 or newer installed, you can install from source. When the ``pip`` installer runs,
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:
.. code-block:: bash
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
pip3 install git+https://github.com/jbarlow83/OCRmyPDF.git
Or, to install in `development mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`_, allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash
pip3 install -e git+https://github.com/jbarlow83/OCRmyPDF.git
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
sudo pip3 install [-e] git+https://github.com/jbarlow83/OCRmyPDF.git
Note that this will alter your system's Python distribution. If you prefer
to not install as superuser, you can install the package in a Python virtual environment:
.. code-block:: bash
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
python3 -m venv
source venv/bin/activate
cd OCRmyPDF
pip3 install .
git clone -b master https://github.com/jbarlow83/OCRmyPDF.git
python3 -m venv
source venv/bin/activate
cd OCRmyPDF
pip3 install .
However, ``ocrmypdf`` will only be accessible on the system PATH after
you activate the virtual environment.
@@ -329,8 +367,8 @@ you activate the virtual environment.
To run the program:
.. code-block:: bash
ocrmypdf --help
ocrmypdf --help
If not yet installed, the script will notify you about dependencies that
need to be installed. The script requires specific versions of the
+19 -13
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,27 +42,27 @@ 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
----------------------------------
There are two routes to manually applying OCR to an existing PDF, both of which destroy information in the original PDF.
A PDF is similar to an HTML file, in that it contains document structure along with images. Sometimes a PDF does nothing more than present a full page image, but often there is additional content that would be lost.
1. Rasterize each page as an image, OCR the images, and combine the output into a PDF. This preserves the appearance of each page, but resamples all images (possibly losing quality, increasing file size, introducing compression artifacts, etc.)
A manual process could work like either of these:
2. Extract each image, OCR, and combine the output into a PDF. This loses the context in which images are used in the PDF, meaning that cropping, rotation and scaling of pages may be lost. Some PDFs use multiple images per page with stencil masks, which would quite difficult to reassemble correctly. This also loses and text or vector art on any pages in a PDF with both scanned and pure digital content.
1. Rasterize each page as an image, OCR the images, and combine the output into a PDF. This preserves the layout of each page, but resamples all images (possibly losing quality, increasing file size, introducing compression artifacts, etc.).
In the case of a PDF that is nothing other than a container of images (no rotation, scaling, cropping, one image per page), the second approach is can be lossless.
2. Extract each image, OCR, and combine the output into a PDF. This loses the context in which images are used in the PDF, meaning that cropping, rotation and scaling of pages may be lost. Some scanned PDFs use multiple images segmented into black and white, grayscale and color regions, with stencil masks to prevent overlap, as this can enhance the appearance of a file while reducing file size. Clearly, reassembling these images will be easy. This also loses and text or vector art on any pages in a PDF with both scanned and pure digital content.
In the case of a PDF that is nothing other than a container of images (no rotation, scaling, cropping, one image per page), the second approach can be lossless.
OCRmyPDF uses several strategies depending on input options and the input PDF itself, but generally speaking it rasterizes a page for OCR and then grafts the OCR back onto the original. As such it can handle complex PDFs and still preserve their contents as much as possible.
@@ -78,6 +78,7 @@ OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these l
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
* PDFs that use transparent layers are not currently checked in the test suite, so they may not work correctly.
OCRmyPDF is also limited by the PDF specification:
@@ -86,6 +87,8 @@ OCRmyPDF is also limited by the PDF specification:
Ghostscript also imposes some limitations:
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
* PDFs containing JPEG 2000-encoded content will be converted to JPEG encoding, which may introduce compression artifacts, if Ghostscript PDF/A is enabled.
* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format.
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
@@ -93,9 +96,12 @@ OCRmyPDF is currently not designed to be used as a Python API; it is designed to
Similar programs
----------------
To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly tested command line OCR PDF conversion tool. If it doesn't meet your needs, consider of these similar open source programs:
To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly tested command line OCR PDF conversion tool. If it does not meet your needs, contributions and suggestions are welcome. If not, consider one of these similar open source programs:
* pdf2pdfocr
* pdfsandwich
* pypdfocr
* pdfbeads
* 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`_).
+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
-----------------
+399 -237
View File
@@ -1,7 +1,169 @@
Release notes
=============
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
OCRmyPDF uses `semantic versioning <http://semver.org/>`_ for its command line interface.
The OCRmyPDF package itself does not contain a public API, although it is fairly stable and breaking changes are usually timed with a major release. A future release will clearly define the stable public API.
v5.6.3
------
- Suppress two debug messages that were too verbose
v5.6.2
------
- Development branch accidentally tagged as release. Do not use.
v5.6.1
------
- Fix issue #219: change how the final output file is created to avoid triggering permission errors when the output is a special file such as ``/dev/null``
- Fix test suite failures due to a qpdf 8.0.0 regression and Python 3.5's handling of symlink
- The "encrypted PDF" error message was different depending on the type of PDF encryption. Now a single clear message appears for all types of PDF encryption.
- ocrmypdf is now in Homebrew. Homebrew users are advised to the version of ocrmypdf in the official homebrew-core formulas rather than the private tap.
- Some linting
v5.6.0
------
- Fix issue #216: preserve "text as curves" PDFs without rasterizing file
- Related to the above, messages about rasterizing are more consistent
- For consistency versions minor releases will now get the trailing .0 they always should have had.
v5.5
----
- Add new argument ``--max-image-mpixels``. Pillow 5.0 now raises an exception when images may be decompression bombs. This argument can be used to override the limit Pillow sets.
- Fix output page cropped when using the sandwich renderer and OCR is skipped on a rotated and image-processed page
- A warning is now issued when old versions of Ghostscript are used in cases known to cause issues with non-Latin characters
- Fix a few parameter validation checks for ``-output-type pdfa-1`` and ``pdfa-2``
v5.4.4
------
- Fix issue #181: fix final merge failure for PDFs with more pages than the system file handle limit (``ulimit -n``)
- Fix issue #200: an uncommon syntax for formatting decimal numbers in a PDF would cause qpdf to issue a warning, which ocrmypdf treated as an error. Now this the warning is relayed.
- Fix an issue where intermediate PDFs would be created at version 1.3 instead of the version of the original file. It's possible but unlikely this had side effects.
- A warning is now issued when older versions of qpdf are used since issues like #200 cause qpdf to infinite-loop
- Address issue #140: if Tesseract outputs invalid UTF-8, escape it and print its message instead of aborting with a Unicode error
- Adding previously unlisted setup requirement, pytest-runner
- Update documentation: fix an error in the example script for Synology with Docker images, improved security guidance, advised ``pip install --user``
v5.4.3
------
- If a subprocess fails to report its version when queried, exit cleanly with an error instead of throwing an exception
- Added test to confirm that the system locale is Unicode-aware and fail early if it's not
- Clarified some copyright information
- Updated pinned requirements.txt so the homebrew formula captures more recent versions
v5.4.2
------
- Fixed a regression from v5.4.1 that caused sidecar files to be created as empty files
v5.4.1
------
- Add workaround for Tesseract v4.00alpha crash when trying to obtain orientation and the latest language packs are installed
v5.4
----
- Change wording of a deprecation warning to improve clarity
- Added option to generate PDF/A-1b output if desired (``--output-type pdfa-1``); default remains PDF/A-2b generation
- Update documentation
v5.3.3
------
- Fixed missing error message that should occur when trying to force ``--pdf-renderer sandwich`` on old versions of Tesseract
- Update copyright information in test files
- Set system ``LANG`` to UTF-8 in Dockerfiles to avoid UTF-8 encoding errors
v5.3.2
------
- Fixed a broken test case related to language packs
v5.3.1
------
- Fixed wrong return code given for missing Tesseract language packs
- Fixed "brew audit" crashing on Travis when trying to auto-brew
v5.3
----
- Added ``--user-words`` and ``--user-patterns`` arguments which are forwarded to Tesseract OCR as words and regular expressions respective to use to guide OCR. Supplying a list of subject-domain words should assist Tesseract with resolving words. (#165)
- Using a non Latin-1 language with the "hocr" renderer now warns about possible OCR quality and recommends workarounds (#176)
- Output file path added to error message when that location is not writable (#175)
- Otherwise valid PDFs with leading whitespace at the beginning of the file are now accepted
v5.2
----
- When using Tesseract 3.05.01 or newer, OCRmyPDF will select the "sandwich" PDF renderer by default, unless another PDF renderer is specified with the ``--pdf-renderer`` argument. The previous behavior was to select ``--pdf-renderer=hocr``.
- The "tesseract" PDF renderer is now deprecated, since it can cause problems with Ghostscript on Tesseract 3.05.00
- The "tess4" PDF renderer has been renamed to "sandwich". "tess4" is now a deprecated alias for "sandwich".
v5.1
----
- Files with pages larger than 200" (5080 mm) in either dimension are now supported with ``--output-type=pdf`` with the page size preserved (in the PDF specification this feature is called UserUnit scaling). Due to Ghostscript limitations this is not available in conjunction with PDF/A output.
v5.0.1
------
- Fixed issue #169, exception due to failure to create sidecar text files on some versions of Tesseract 3.04, including the jbarlow83/ocrmypdf Docker image
v5.0
----
- Backward incompatible changes
+ Support for Python 3.4 dropped. Python 3.5 is now required.
+ Support for Tesseract 3.02 and 3.03 dropped. Tesseract 3.04 or newer is required. Tesseract 4.00 (alpha) is supported.
+ The OCRmyPDF.sh script was removed.
- Add a new feature, ``--sidecar``, which allows creating "sidecar" text files which contain the OCR results in plain text. These OCR text is more reliable than extracting text from PDFs. Closes #126.
- New feature: ``--pdfa-image-compression``, which allows overriding Ghostscript's lossy-or-lossless image encoding heuristic and making all images JPEG encoded or lossless encoded as desired. Fixes #163.
- Fixed issue #143, added ``--quiet`` to suppress "INFO" messages
- Fixed issue #164, a typo
- Removed the command line parameters ``-n`` and ``--just-print`` since they have not worked for some time (reported as Ubuntu bug `#1687308 <https://bugs.launchpad.net/ubuntu/+source/ocrmypdf/+bug/1687308>`_)
v4.5.6
------
- Fixed issue #156, 'NoneType' object has no attribute 'getObject' on pages with no optional /Contents record. This should resolve all issues related to pages with no /Contents record.
- Fixed issue #158, ocrmypdf now stops and terminates if Ghostscript fails on an intermediate step, as it is not possible to proceed.
- Fixed issue #160, exception thrown on certain invalid arguments instead of error message
v4.5.5
------
- Automated update of macOS homebrew tap
- Fixed issue #154, KeyError '/Contents' when searching for text on blank pages that have no /Contents record. Note: incomplete fix for this issue.
v4.5.4
@@ -14,242 +176,242 @@ v4.5.4
v4.5.3
------
- Added a workaround for Ghostscript 9.21 and probably earlier versions would fail with the error message "VMerror -25", due to a Ghostscript bug in XMP metadata handling
- High Unicode characters (U+10000 and up) are no longer accepted for setting metadata on the command line, as Ghostscript may not handle them correctly.
- Fixed an issue where the ``tess4`` renderer would duplicate content onto output pages if tesseract failed or timed out
- Fixed ``tess4`` renderer not recognized when lossless reconstruction is possible
- Added a workaround for Ghostscript 9.21 and probably earlier versions would fail with the error message "VMerror -25", due to a Ghostscript bug in XMP metadata handling
- High Unicode characters (U+10000 and up) are no longer accepted for setting metadata on the command line, as Ghostscript may not handle them correctly.
- Fixed an issue where the ``tess4`` renderer would duplicate content onto output pages if tesseract failed or timed out
- Fixed ``tess4`` renderer not recognized when lossless reconstruction is possible
v4.5.2
------
- Fix issue #147. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information.
- Make "using Tesseract 4.0" warning less ominous
- Set up machinery for homebrew OCRmyPDF tap
- Fix issue #147. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information.
- Make "using Tesseract 4.0" warning less ominous
- Set up machinery for homebrew OCRmyPDF tap
v4.5.1
------
- Fix issue #137, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags
- Fix issue #137, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags
v4.5
----
- Exotic PDFs containing "Form XObjects" are now supported (issue #134; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with Tesseract 3.04 or lower due to issues with Ghostscript corrupting the OCR text in these cases
- Exotic PDFs containing "Form XObjects" are now supported (issue #134; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with Tesseract 3.04 or lower due to issues with Ghostscript corrupting the OCR text in these cases
v4.4.2
------
- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch
- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch
+ This makes supporting the Tesseract 4 image easier
+ This could be a disruptive change for any Docker users who built customized these images with their own changes, and made those changes in a way that depends on Debian and not Ubuntu
+ This makes supporting the Tesseract 4 image easier
+ This could be a disruptive change for any Docker users who built customized these images with their own changes, and made those changes in a way that depends on Debian and not Ubuntu
- OCRmyPDF now prevents running the Tesseract 4 renderer with Tesseract 3.04, which was permitted in v4.4 and v4.4.1 but will not work
- OCRmyPDF now prevents running the Tesseract 4 renderer with Tesseract 3.04, which was permitted in v4.4 and v4.4.1 but will not work
v4.4.1
------
- To prevent a `TIFF output error <https://github.com/python-pillow/Pillow/issues/2206>`_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened
- The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance
- Documentation improvements to describe the ``--tesseract-config`` feature
- Added test cases and fixed error handling for ``--tesseract-config``
- Tweaks to setup.py to deal with issues in the v4.4 release
- To prevent a `TIFF output error <https://github.com/python-pillow/Pillow/issues/2206>`_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened
- The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance
- Documentation improvements to describe the ``--tesseract-config`` feature
- Added test cases and fixed error handling for ``--tesseract-config``
- Tweaks to setup.py to deal with issues in the v4.4 release
v4.4
----
- Tesseract 4.00 is now supported on an experimental basis.
- Tesseract 4.00 is now supported on an experimental basis.
+ A new rendering option ``--pdf-renderer tess4`` exploits Tesseract 4's new text-only output PDF mode. See the documentation on PDF Renderers for details.
+ The ``--tesseract-oem`` argument allows control over the Tesseract 4 OCR engine mode (tesseract's ``--oem``). Use ``--tesseract-oem 2`` to enforce the new LSTM mode.
+ Fixed poor performance with Tesseract 4.00 on Linux
+ A new rendering option ``--pdf-renderer tess4`` exploits Tesseract 4's new text-only output PDF mode. See the documentation on PDF Renderers for details.
+ The ``--tesseract-oem`` argument allows control over the Tesseract 4 OCR engine mode (tesseract's ``--oem``). Use ``--tesseract-oem 2`` to enforce the new LSTM mode.
+ Fixed poor performance with Tesseract 4.00 on Linux
- Fixed an issue that caused corruption of output to stdout in some cases
- Removed test for Pillow JPEG and PNG support, as the minimum supported version of Pillow now enforces this
- OCRmyPDF now tests that the intended destination file is writable before proceeding
- The test suite now requires ``pytest-helpers-namespace`` to run (but not install)
- Significant code reorganization to make OCRmyPDF re-entrant and improve performance. All changes should be backward compatible for the v4.x series.
- Fixed an issue that caused corruption of output to stdout in some cases
- Removed test for Pillow JPEG and PNG support, as the minimum supported version of Pillow now enforces this
- OCRmyPDF now tests that the intended destination file is writable before proceeding
- The test suite now requires ``pytest-helpers-namespace`` to run (but not install)
- Significant code reorganization to make OCRmyPDF re-entrant and improve performance. All changes should be backward compatible for the v4.x series.
+ However, OCRmyPDF's dependency "ruffus" is not re-entrant, so no Python API is available. Scripts should continue to use the command line interface.
+ However, OCRmyPDF's dependency "ruffus" is not re-entrant, so no Python API is available. Scripts should continue to use the command line interface.
v4.3.5
------
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
v4.3.4
------
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
v4.3.3
------
- Fixed PDF/A creation with Ghostscript 9.20 properly
- Fixed an exception on inline stencil masks with a missing optional parameter
- Fixed PDF/A creation with Ghostscript 9.20 properly
- Fixed an exception on inline stencil masks with a missing optional parameter
v4.3.2
------
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
v4.3.1
------
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
- Ability to specify multiple languages for multilingual documents is now advertised in documentation
- Reduced the file sizes of some test resources
- Cleaned up debug output
- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
- Ability to specify multiple languages for multilingual documents is now advertised in documentation
- Reduced the file sizes of some test resources
- Cleaned up debug output
- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed
v4.3
----
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
- Better documentation
- Fixed an issue with PDFs that draw images when the raster stack depth is zero
- ocrmypdf can now redirect its output to stdout for use in a shell pipeline
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
- Better documentation
- Fixed an issue with PDFs that draw images when the raster stack depth is zero
- ocrmypdf can now redirect its output to stdout for use in a shell pipeline
+ This does not improve performance since temporary files are still used for buffering
+ Some output validation is disabled in this mode
+ This does not improve performance since temporary files are still used for buffering
+ Some output validation is disabled in this mode
v4.2.5
------
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
- Removed non-free file milk.pdf
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
- Removed non-free file milk.pdf
v4.2.4
------
- Fixed an error (#90) caused by PDFs that use stencil masks properly
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
- Fixed an error (#90) caused by PDFs that use stencil masks properly
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
v4.2.3
------
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
- Integrated a few fixes to simplify downstream packaging (Debian)
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
- Integrated a few fixes to simplify downstream packaging (Debian)
+ The test suite no longer assumes it is installed
+ If running Linux, skip a test that passes Unicode on the command line
+ The test suite no longer assumes it is installed
+ If running Linux, skip a test that passes Unicode on the command line
- Added a test case to check explicit masks and stencil masks
- Added a test case for indirect objects and linearized PDFs
- Deprecated the OCRmyPDF.sh shell script
- Added a test case to check explicit masks and stencil masks
- Added a test case for indirect objects and linearized PDFs
- Deprecated the OCRmyPDF.sh shell script
v4.2.2
------
- Improvements to documentation
- Improvements to documentation
v4.2.1
------
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
- Implemented stdin streaming
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
- Implemented stdin streaming
v4.2
----
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
+ ``pdfa`` is the default, consistent with past behavior
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
+ ``pdfa`` is the default, consistent with past behavior
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
- ``--force-ocr`` rasterizes pages even if they contain no images
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
- ``--force-ocr`` rasterizes pages even if they contain no images
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
+ fixes issue #82
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
+ fixes issue #82
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
+ Some Ubuntu "PPAs" (backports) are needed to make it work
- Support for some older dependencies dropped
+ Some Ubuntu "PPAs" (backports) are needed to make it work
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
- Support for some older dependencies dropped
- Ghostscript now runs in "safer" mode where possible
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
- Ghostscript now runs in "safer" mode where possible
v4.1.4
------
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
v4.1.3
------
- More helpful error message for PDFs with version 4 security handler
- Update usage instructions for Windows/Docker users
- Fix order of operations for matrix multiplication (no effect on most users)
- Add a few leptonica wrapper functions (no effect on most users)
- More helpful error message for PDFs with version 4 security handler
- Update usage instructions for Windows/Docker users
- Fix order of operations for matrix multiplication (no effect on most users)
- Add a few leptonica wrapper functions (no effect on most users)
v4.1.2
------
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
- More helpful error message for an error related to certain types of malformed PDFs
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
- More helpful error message for an error related to certain types of malformed PDFs
v4.1
----
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
v4.0.7
------
- Minor correction to Ghostscript output settings
- Minor correction to Ghostscript output settings
v4.0.6
------
- Update install instructions
- Provide a sRGB profile instead of using Ghostscript's
- Update install instructions
- Provide a sRGB profile instead of using Ghostscript's
v4.0.5
------
- Remove some verbose debug messages from v4.0.4
- Fixed temporary that wasn't being deleted
- DPI is now calculated correctly for cropped images, along with other image transformations
- Inline images are now checked during DPI calculation instead of rejecting the image
- Remove some verbose debug messages from v4.0.4
- Fixed temporary that wasn't being deleted
- DPI is now calculated correctly for cropped images, along with other image transformations
- Inline images are now checked during DPI calculation instead of rejecting the image
v4.0.4
------
@@ -263,16 +425,16 @@ v4.0.3
New features
^^^^^^^^^^^^
- Page orientations detected are now reported in a summary comment
- Page orientations detected are now reported in a summary comment
Fixes
^^^^^
- Show stack trace if unexpected errors occur
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
abort the file
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
- Show stack trace if unexpected errors occur
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
abort the file
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
v4.0.2
@@ -281,10 +443,10 @@ v4.0.2
Fixes
^^^^^
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
orientation information
- Improved handling of Tesseract errors and crashes
- Fixed use of chmod on Docker that broke most test cases
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
orientation information
- Improved handling of Tesseract errors and crashes
- Fixed use of chmod on Docker that broke most test cases
v4.0.1
@@ -293,7 +455,7 @@ v4.0.1
Fixes
^^^^^
- Fixed a KeyError if tesseract fails to find page orientation information
- Fixed a KeyError if tesseract fails to find page orientation information
v4.0
@@ -302,33 +464,33 @@ v4.0
New features
^^^^^^^^^^^^
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
at image deskewing than unpaper.
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
at image deskewing than unpaper.
Fixes
^^^^^
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
Changes
^^^^^^^
- Logging output is now much easier to read
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
- libffi is now required
- Some changes were made to the Docker and Travis build environments to support libffi
- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01,
the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00.
You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder
to correct the problem.
- Logging output is now much easier to read
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
- libffi is now required
- Some changes were made to the Docker and Travis build environments to support libffi
- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01,
the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00.
You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder
to correct the problem.
v3.2.1
@@ -337,8 +499,8 @@ v3.2.1
Changes
^^^^^^^
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
- Tweaked the Dockerfiles
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
- Tweaked the Dockerfiles
v3.2
@@ -347,19 +509,19 @@ v3.2
New features
^^^^^^^^^^^^
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without
otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
of vector and raster content would see the vector content preserved. Images may still be transcoded
during PDF/A conversion. (``--deskew`` and ``--clean-final`` disable this mode, necessarily.)
- New argument ``--tesseract-pagesegmode`` allows you to pass page segmentation arguments to Tesseract OCR.
This helps for two column text and other situations that confuse Tesseract.
- Added a new "polyglot" version of the Docker image, that generates Tesseract with all languages packs installed,
for the polyglots among us. It is much larger.
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without
otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
of vector and raster content would see the vector content preserved. Images may still be transcoded
during PDF/A conversion. (``--deskew`` and ``--clean-final`` disable this mode, necessarily.)
- New argument ``--tesseract-pagesegmode`` allows you to pass page segmentation arguments to Tesseract OCR.
This helps for two column text and other situations that confuse Tesseract.
- Added a new "polyglot" version of the Docker image, that generates Tesseract with all languages packs installed,
for the polyglots among us. It is much larger.
Changes
^^^^^^^
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
@@ -369,7 +531,7 @@ v3.1.1
Changes
^^^^^^^
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
v3.1
----
@@ -377,17 +539,17 @@ v3.1
Changes
^^^^^^^
- Default output format is now PDF/A-2b instead of PDF/A-1b
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
needed to implement support
- Improved some error messages related to missing input files
- Fixed issue #20 - uppercase .PDF extension not accepted
- Fixed an issue where OCRmyPDF failed to text that certain pages contained previously OCR'ed text,
such as OCR text produced by Tesseract 3.04
- Inserts /Creator tag into PDFs so that errors can be traced back to this project
- Added new option ``--pdf-renderer=auto``, to let OCRmyPDF pick the best PDF renderer.
Currently it always chooses the 'hocrtransform' renderer but that behavior may change.
- Set up Travis CI automatic integration testing
- Default output format is now PDF/A-2b instead of PDF/A-1b
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
needed to implement support
- Improved some error messages related to missing input files
- Fixed issue #20 - uppercase .PDF extension not accepted
- Fixed an issue where OCRmyPDF failed to text that certain pages contained previously OCR'ed text,
such as OCR text produced by Tesseract 3.04
- Inserts /Creator tag into PDFs so that errors can be traced back to this project
- Added new option ``--pdf-renderer=auto``, to let OCRmyPDF pick the best PDF renderer.
Currently it always chooses the 'hocrtransform' renderer but that behavior may change.
- Set up Travis CI automatic integration testing
v3.0
----
@@ -395,55 +557,55 @@ v3.0
New features
^^^^^^^^^^^^
- Easier installation with a Docker container or Python's ``pip`` package manager
- Eliminated many external dependencies, so it's easier to setup
- Now installs ``ocrmypdf`` to ``/usr/local/bin`` or equivalent for system-wide
access and easier typing
- Improved command line syntax and usage help (``--help``)
- Tesseract 3.03+ PDF page rendering can be used instead for better positioning
of recognized text (``--pdf-renderer tesseract``)
- PDF metadata (title, author, keywords) are now transferred to the
output PDF
- PDF metadata can also be set from the command line (``--title``, etc.)
- Automatic repairs malformed input PDFs if possible
- Added test cases to confirm everything is working
- Added option to skip extremely large pages that take too long to OCR and are
often not OCRable (e.g. large scanned maps or diagrams); other pages are still
processed (``--skip-big``)
- Added option to kill Tesseract OCR process if it seems to be taking too long on
a page, while still processing other pages (``--tesseract-timeout``)
- Less common colorspaces (CMYK, palette) are now supported by conversion to RGB
- Multiple images on the same PDF page are now supported
- Easier installation with a Docker container or Python's ``pip`` package manager
- Eliminated many external dependencies, so it's easier to setup
- Now installs ``ocrmypdf`` to ``/usr/local/bin`` or equivalent for system-wide
access and easier typing
- Improved command line syntax and usage help (``--help``)
- Tesseract 3.03+ PDF page rendering can be used instead for better positioning
of recognized text (``--pdf-renderer tesseract``)
- PDF metadata (title, author, keywords) are now transferred to the
output PDF
- PDF metadata can also be set from the command line (``--title``, etc.)
- Automatic repairs malformed input PDFs if possible
- Added test cases to confirm everything is working
- Added option to skip extremely large pages that take too long to OCR and are
often not OCRable (e.g. large scanned maps or diagrams); other pages are still
processed (``--skip-big``)
- Added option to kill Tesseract OCR process if it seems to be taking too long on
a page, while still processing other pages (``--tesseract-timeout``)
- Less common colorspaces (CMYK, palette) are now supported by conversion to RGB
- Multiple images on the same PDF page are now supported
Changes
^^^^^^^
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
- OCR text is now rendered in the PDF as invisible text. Previous versions of OCRmyPDF
incorrectly rendered visible text with an image on top.
- All "tasks" in the pipeline can be executed in parallel on any
available CPUs, increasing performance
- The ``-o DPI`` argument has been phased out, in favor of ``--oversample DPI``, in
case we need ``-o OUTPUTFILE`` in the future
- Removed several dependencies, so it's easier to install. We no
longer use:
- GNU parallel_
- ImageMagick_
- Python 2.7
- Poppler
- MuPDF_ tools
- shell scripts
- Java and JHOVE_
- libxml2
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
- OCR text is now rendered in the PDF as invisible text. Previous versions of OCRmyPDF
incorrectly rendered visible text with an image on top.
- All "tasks" in the pipeline can be executed in parallel on any
available CPUs, increasing performance
- The ``-o DPI`` argument has been phased out, in favor of ``--oversample DPI``, in
case we need ``-o OUTPUTFILE`` in the future
- Removed several dependencies, so it's easier to install. We no
longer use:
- GNU parallel_
- ImageMagick_
- Python 2.7
- Poppler
- MuPDF_ tools
- shell scripts
- Java and JHOVE_
- libxml2
- Some new external dependencies are required or optional, compared to v2.x:
- Some new external dependencies are required or optional, compared to v2.x:
- Ghostscript 9.14+
- qpdf_ 5.0.0+
- Unpaper_ 6.1 (optional)
- some automatically managed Python packages
- Ghostscript 9.14+
- qpdf_ 5.0.0+
- Unpaper_ 6.1 (optional)
- some automatically managed Python packages
.. _ruffus: http://www.ruffus.org.uk/index.html
.. _parallel: https://www.gnu.org/software/parallel/
@@ -456,87 +618,87 @@ Changes
Release candidates
^^^^^^^^^^^^^^^^^^
- rc9:
- rc9:
- fix issue #118: report error if ghostscript iccprofiles are missing
- fixed another issue related to #111: PDF rasterized to palette file
- add support image files with a palette
- don't try to validate PDF file after an exception occurs
- fix issue #118: report error if ghostscript iccprofiles are missing
- fixed another issue related to #111: PDF rasterized to palette file
- add support image files with a palette
- don't try to validate PDF file after an exception occurs
- rc8:
- rc8:
- fix issue #111: exception thrown if PDF is missing DocumentInfo dictionary
- fix issue #111: exception thrown if PDF is missing DocumentInfo dictionary
- rc7:
- rc7:
- fix error when installing direct from pip, "no such file 'requirements.txt'"
- fix error when installing direct from pip, "no such file 'requirements.txt'"
- rc6:
- rc6:
- dropped libxml2 (Python lxml) since Python 3's internal XML parser is sufficient
- set up Docker container
- fix Unicode errors if recognized text contains Unicode characters and system locale is not UTF-8
- dropped libxml2 (Python lxml) since Python 3's internal XML parser is sufficient
- set up Docker container
- fix Unicode errors if recognized text contains Unicode characters and system locale is not UTF-8
- rc5:
- rc5:
- dropped Java and JHOVE in favour of qpdf
- improved command line error output
- additional tests and bug fixes
- tested on Ubuntu 14.04 LTS
- dropped Java and JHOVE in favour of qpdf
- improved command line error output
- additional tests and bug fixes
- tested on Ubuntu 14.04 LTS
- rc4:
- rc4:
- dropped MuPDF in favour of qpdf
- fixed some installer issues and errors in installation instructions
- improve performance: run Ghostscript with multithreaded rendering
- improve performance: use multiple cores by default
- bug fix: checking for wrong exception on process timeout
- dropped MuPDF in favour of qpdf
- fixed some installer issues and errors in installation instructions
- improve performance: run Ghostscript with multithreaded rendering
- improve performance: use multiple cores by default
- bug fix: checking for wrong exception on process timeout
- rc3: skipping version number intentionally to avoid confusion with Tesseract
- rc2: first release for public testing to test-PyPI, Github
- rc1: testing release process
- rc3: skipping version number intentionally to avoid confusion with Tesseract
- rc2: first release for public testing to test-PyPI, Github
- rc1: testing release process
Compatibility notes
-------------------
- ``./OCRmyPDF.sh`` script is still available for now
- Stacking the verbosity option like ``-vvv`` is no longer supported
- ``./OCRmyPDF.sh`` script is still available for now
- Stacking the verbosity option like ``-vvv`` is no longer supported
- The configuration file ``config.sh`` has been removed. Instead, you can
feed a file to the arguments for common settings:
- The configuration file ``config.sh`` has been removed. Instead, you can
feed a file to the arguments for common settings:
::
ocrmypdf input.pdf output.pdf @settings.txt
ocrmypdf input.pdf output.pdf @settings.txt
where ``settings.txt`` contains *one argument per line*, for example:
::
-l
deu
--author
A. Merkel
--pdf-renderer
tesseract
-l
deu
--author
A. Merkel
--pdf-renderer
tesseract
Fixes
^^^^^
- Handling of filenames containing spaces: fixed
- Handling of filenames containing spaces: fixed
Notes and known issues
^^^^^^^^^^^^^^^^^^^^^^
- Some dependencies may work with lower versions than tested, so try
overriding dependencies if they are "in the way" to see if they work.
- Some dependencies may work with lower versions than tested, so try
overriding dependencies if they are "in the way" to see if they work.
- ``--pdf-renderer tesseract`` will output files with an incorrect page size in Tesseract 3.03,
due to a bug in Tesseract.
- ``--pdf-renderer tesseract`` will output files with an incorrect page size in Tesseract 3.03,
due to a bug in Tesseract.
- PDF files containing "inline images" are not supported and won't be for the 3.0 release. Scanned
images almost never contain inline images.
- PDF files containing "inline images" are not supported and won't be for the 3.0 release. Scanned
images almost never contain inline images.
v2.2-stable (2014-09-29)
+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.
+5 -10
View File
@@ -1,15 +1,10 @@
# © 2017 James R. Barlow: github.com/jbarlow83
import pkg_resources
PROGRAM_NAME = 'ocrmypdf'
VERSION = pkg_resources.get_distribution('ocrmypdf').version
# These imports are for v4.x backward compatibility for consumers of ocrmypdf
# (if any). They are deprecated and will be removed in v5.x.
from .exec import ghostscript, qpdf, tesseract, unpaper, get_program
from .exceptions import ExitCode
from .helpers import page_number, is_iterable_notstr
# Official PEP 396
__version__ = pkg_resources.get_distribution('ocrmypdf').version
VERSION = __version__
+294 -136
View File
@@ -1,37 +1,35 @@
#!/usr/bin/env python3
# © 2015-16 James R. Barlow: github.com/jbarlow83
# © 2015-17 James R. Barlow: github.com/jbarlow83
from contextlib import suppress
from tempfile import mkdtemp
from collections.abc import Sequence
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 .exec import tesseract, qpdf, ghostscript
from . import PROGRAM_NAME, VERSION
from .exceptions import *
from .exceptions import ExitCode, ExitCodeException, MissingDependencyError
from . import exceptions as ocrmypdf_exceptions
from ._unicodefun import verify_python3_env
warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
@@ -39,8 +37,11 @@ warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
# -------------
# External dependencies
MINIMUM_TESS_VERSION = '3.02.02'
MINIMUM_TESS_VERSION = '3.04'
HOCR_OK_LANGS = frozenset([
'eng', 'deu', 'spa', 'ita', 'por'
])
def complain(message):
print(*textwrap.wrap(message), file=sys.stderr)
@@ -50,6 +51,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 "
@@ -60,14 +66,9 @@ if tesseract.version() < MINIMUM_TESS_VERSION:
# -------------
# Parser
parser = cmdline.get_argparse(
parser = argparse.ArgumentParser(
prog=PROGRAM_NAME,
version=VERSION,
fromfile_prefix_chars='@',
ignored_args=[
'touch_files_only', 'recreate_database', 'checksum_file_name',
'key_legend_in_graph', 'draw_graph_horizontally', 'flowchart_format',
'forced_tasks', 'target_tasks', 'use_threads', 'jobs', 'log_file'],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
Generates a searchable PDF or PDF/A from a regular PDF.
@@ -119,73 +120,101 @@ Online documentation is located at:
""")
parser.add_argument(
'input_file',
'input_file', metavar="input_pdf_or_image",
help="PDF file containing the images to be OCRed (or '-' to read from "
"standard input)")
parser.add_argument(
'output_file',
help="output searchable PDF file (or '-' to write to standard output)")
'output_file', metavar="output_pdf",
help="Output searchable PDF file (or '-' to write to standard output). "
"Existing files will be ovewritten. If same as input file, the "
"input file will be updated only if processing is successful.")
parser.add_argument(
'-l', '--language', action='append',
help="Language(s) of the file to be OCRed (see tesseract --list-langs for "
"all language packs installed in your system). To specify multiple "
"languages, join them with '+' or issue this argument once for each "
"language.")
parser.add_argument(
'-j', '--jobs', metavar='N', type=int,
help="Use up to N CPU cores simultaneously (default: use all)")
"all language packs installed in your system). Use -l eng+deu for "
"multiple languages.")
parser.add_argument(
'--image-dpi', metavar='DPI', type=int,
help="for input image instead of PDF, use this DPI instead of file's")
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
# bool('\0') is True in Python
parser.add_argument(
'--sidecar', nargs='?', const='\0', default=None, metavar='FILE',
help="Generate sidecar text files that contain the same text recognized "
"by Tesseract. This may be useful for building a OCR text database. "
"If FILE is omitted, the sidecar file be named {output_file}.txt "
"If FILE is set to '-', the sidecar is written to stdout (a "
"convenient way to preview OCR quality). The output file and sidecar "
"may not both use stdout at the same time.")
parser.add_argument(
'--version', action='version', version=VERSION,
help="Print program version and exit")
jobcontrol = parser.add_argument_group(
"Job control options")
jobcontrol.add_argument(
'-j', '--jobs', metavar='N', type=int,
help="Use up to N CPU cores simultaneously (default: use all).")
jobcontrol.add_argument(
'-q', '--quiet', action='store_true', help="Suppress INFO messages")
jobcontrol.add_argument(
'-v', '--verbose', const="+", default=[], nargs='?', action="append",
help="Print more verbose messages for each additional verbose level")
metadata = parser.add_argument_group(
"Metadata options",
"Set output PDF/A metadata (default: use input document's metadata)")
"Set output PDF/A metadata (default: copy input document's metadata)")
metadata.add_argument(
'--title', type=str,
help="set document title (place multiple words in quotes)")
help="Set document title (place multiple words in quotes)")
metadata.add_argument(
'--author', type=str,
help="set document author")
help="Set document author")
metadata.add_argument(
'--subject', type=str,
help="set document subject description")
help="Set document subject description")
metadata.add_argument(
'--keywords', type=str,
help="set document keywords")
help="Set document keywords")
preprocessing = parser.add_argument_group(
"Image preprocessing options",
"Options to improve the quality of the final PDF and OCR")
preprocessing.add_argument(
'-r', '--rotate-pages', action='store_true',
help="automatically rotate pages based on detected text orientation")
help="Automatically rotate pages based on detected text orientation")
preprocessing.add_argument(
'--remove-background', action='store_true',
help="attempt to remove background from gray or color pages, setting it "
help="Attempt to remove background from gray or color pages, setting it "
"to white ")
preprocessing.add_argument(
'-d', '--deskew', action='store_true',
help="deskew each page before performing OCR")
help="Deskew each page before performing OCR")
preprocessing.add_argument(
'-c', '--clean', action='store_true',
help="clean pages from scanning artifacts before performing OCR, and send "
help="Clean pages from scanning artifacts before performing OCR, and send "
"the cleaned page to OCR, but do not include the cleaned page in "
"the output ")
"the output")
preprocessing.add_argument(
'-i', '--clean-final', action='store_true',
help="clean page as above, and incorporate the cleaned image in the final "
"PDF")
help="Clean page as above, and incorporate the cleaned image in the final "
"PDF. Might remove desired content.")
preprocessing.add_argument(
'--oversample', metavar='DPI', type=int, default=0,
help="oversample images to at least the specified DPI, to improve OCR "
help="Oversample images to at least the specified DPI, to improve OCR "
"results slightly")
ocrsettings = parser.add_argument_group(
@@ -193,65 +222,101 @@ ocrsettings = parser.add_argument_group(
"Control how OCR is applied")
ocrsettings.add_argument(
'-f', '--force-ocr', action='store_true',
help="rasterize any fonts or vector objects on each page, apply OCR, and "
help="Rasterize any fonts or vector objects on each page, apply OCR, and "
"save the rastered output (this rewrites the PDF)")
ocrsettings.add_argument(
'-s', '--skip-text', action='store_true',
help="skip OCR on any pages that already contain text, but include the "
help="Skip OCR on any pages that already contain text, but include the "
"page in final output; useful for PDFs that contain a mix of "
"images, text pages, and/or previously OCRed pages")
# ocrsettings.add_argument(
# '--redo-ocr', action='store_true',
# help="removing any existing OCR text, but otherwise preserve mixed PDF "
# "pages")
ocrsettings.add_argument(
'--skip-big', type=float, metavar='MPixels',
help="skip OCR on pages larger than the specified amount of megapixels, "
help="Skip OCR on pages larger than the specified amount of megapixels, "
"but include skipped pages in final output")
advanced = parser.add_argument_group(
"Advanced",
"Advanced options for power users")
"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")
help="Additional Tesseract configuration files -- see documentation")
advanced.add_argument(
'--tesseract-pagesegmode', action='store', type=int, metavar='PSM',
choices=range(0, 14),
help="set Tesseract page segmentation mode (see tesseract --help)")
help="Set Tesseract page segmentation mode (see tesseract --help)")
advanced.add_argument(
'--tesseract-oem', action='store', type=int, metavar='MODE',
choices=range(0, 4),
help=("set Tesseract 4.0 OCR engine mode: "
help=("Set Tesseract 4.0 OCR engine mode: "
"0 - original Tesseract only; "
"1 - neural nets LSTM only; "
"2 - Tesseract + LSTM; "
"3 - default.")
)
advanced.add_argument(
'--pdf-renderer', choices=['auto', 'tesseract', 'hocr', 'tess4'], default='auto',
help="choose OCR PDF renderer - the default option is to let OCRmyPDF "
"choose. The 'tesseract' PDF renderer is more accurate and does a "
"better job and document structure such as recognizing columns. It "
"also does a better job on non-Latin languages. However, it does "
"not work as well when older versions of Tesseract or Ghostscript "
"are installed, and some combinations of arguments to do not work "
"with --pdf-renderer tesseract. The 'tess4' PDF renderer is similar "
"to 'tesseract', requires tesseract 4, and gives superior results.")
'--pdf-renderer',
choices=['auto', 'tesseract', 'hocr', 'tess4', 'sandwich'], default='auto',
help="Choose OCR PDF renderer - the default option is to let OCRmyPDF "
"choose."
"auto - let OCRmyPDF choose; "
"sandwich - default renderer for Tesseract 3.05.01 and newer; "
"hocr - default renderer for older versions of Tesseract; "
"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',
help='give up on OCR after the timeout, but copy the preprocessed page '
help='Give up on OCR after the timeout, but copy the preprocessed page '
'into the final output')
advanced.add_argument(
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
help="only rotate pages when confidence is above this value (arbitrary "
help="Only rotate pages when confidence is above this value (arbitrary "
"units reported by tesseract)")
advanced.add_argument(
'--pdfa-image-compression', choices=['auto', 'jpeg', 'lossless'],
default='auto',
help="Specify how to compress images in the output PDF/A. 'auto' lets "
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
"JPEG compression. 'lossless' uses PNG-style lossless compression "
"for all images. Monochrome images are always compressed using a "
"lossless codec. Compression settings "
"are applied to all pages, including those for which OCR was "
"skipped. Not supported for --output-type=pdf ; that setting "
"preserves the original compression of all images.")
advanced.add_argument(
'--user-words', metavar='FILE',
help="Specify the location of the Tesseract user words file. This is a "
"list of words Tesseract should consider while performing OCR in "
"addition to its standard language dictionaries. This can improve "
"OCR quality especially for specialized and technical documents.")
advanced.add_argument(
'--user-patterns', metavar='FILE',
help="Specify the location of the Tesseract user patterns file.")
debugging = parser.add_argument_group(
"Debugging",
"Arguments to help with troubleshooting and debugging")
debugging.add_argument(
'-k', '--keep-temporary-files', action='store_true',
help="keep temporary files (helpful for debugging)")
help="Keep temporary files (helpful for debugging)")
debugging.add_argument(
'-g', '--debug-rendering', action='store_true',
help="render each page twice with debug information on second page")
help="Render each page twice with debug information on second page")
debugging.add_argument(
'--flowchart', type=str,
help="Generate the pipeline execution flowchart")
def check_options_languages(options, _log):
@@ -262,45 +327,70 @@ 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(msg)
raise MissingDependencyError(msg)
def check_options_output(options, log):
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'hocr'
if tesseract.has_textonly_pdf():
options.pdf_renderer = 'sandwich'
else:
options.pdf_renderer = 'hocr'
if options.pdf_renderer in ('tesseract', 'tess4'):
if tesseract.version() < '3.05':
log.warning(
"The setting --pdf-renderer=tesseract is not recommend for "
" use with tesseract versions less than 3.05, because it "
" produces OCR text that is incompatible with Ghostscript and "
" some other software.")
elif tesseract.version() == '4.00.00alpha':
log.warning(
"The setting --pdf-renderer={tesseract,tess4} is not"
" recommended for builds of tesseract 4.00.00alpha older than"
" February 2017. Make sure you are using a recent build.")
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.debug_rendering and options.pdf_renderer == 'tesseract':
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 == 'tesseract':
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(
"Ignoring --debug-rendering because it is not supported with"
"--pdf-renderer=tesseract.")
"Ignoring --debug-rendering because it requires --pdf-renderer=hocr")
lossless_reconstruction = False
if options.pdf_renderer in ('hocr', 'tess4'):
if options.pdf_renderer in ('hocr', 'sandwich'):
if not any((options.deskew, options.clean_final, options.force_ocr,
options.remove_background)):
lossless_reconstruction = True
options.lossless_reconstruction = lossless_reconstruction
def check_options_sidecar(options, log):
if options.sidecar == '\0':
if options.output_file == '-':
raise argparse.ArgumentError(
None,
"--sidecar filename must be specified when output file is "
"stdout.")
options.sidecar = options.output_file + '.txt'
def check_options_preprocessing(options, log):
if any((options.clean, options.clean_final)):
from .exec import unpaper
@@ -324,20 +414,42 @@ def check_options_preprocessing(options, log):
def check_options_ocr_behavior(options, log):
if options.force_ocr and options.skip_text:
raise argparse.ArgumentError(
None,
"Error: --force-ocr and --skip-text are mutually incompatible.")
if set(options.language) & {'chi_sim', 'chi_tra'} and \
(options.pdf_renderer == 'hocr' or options.output_type == 'pdfa'):
log.warning(
"Your settings are known to cause problems with OCR of Chinese text. "
"Try adding these arguments: "
" ocrmypdf --pdf-renderer tesseract --output-type pdf")
# 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 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. ")
if tesseract.has_textonly_pdf():
msg += (
"Use --pdf-renderer auto (the default) to avoid this issue.")
else:
msg += (
"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.")
"Tesseract v4.x.alpha found.")
if options.tesseract_oem and not tesseract.v4():
log.warning(
"--tesseract-oem requires Tesseract 4.x -- argument ignored")
@@ -345,13 +457,19 @@ def check_options_advanced(options, log):
raise MissingDependencyError(
"--pdf-renderer tess4 requires Tesseract 4.x "
"commit 3d9fb3b or later")
if options.pdfa_image_compression != 'auto' and \
options.output_type.startswith('pdfa'):
log.warning(
"--pdfa-image-compression argument has no effect when "
"--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(
@@ -366,6 +484,7 @@ def check_options(options, log):
check_options_languages(options, log)
check_options_metadata(options, log)
check_options_output(options, log)
check_options_sidecar(options, log)
check_options_preprocessing(options, log)
check_options_ocr_behavior(options, log)
check_options_advanced(options, log)
@@ -384,8 +503,9 @@ def check_options(options, log):
# Logging
def logging_factory(logger_name, listargs):
log_file_name, verbose = listargs
def logging_factory(logger_name, logger_args):
verbose = logger_args['verbose']
quiet = logger_args['quiet']
root_logger = logging.getLogger(logger_name)
root_logger.setLevel(logging.DEBUG)
@@ -395,6 +515,8 @@ def logging_factory(logger_name, listargs):
handler.setFormatter(formatter_)
if verbose:
handler.setLevel(logging.DEBUG)
elif quiet:
handler.setLevel(logging.WARNING)
else:
handler.setLevel(logging.INFO)
root_logger.addHandler(handler)
@@ -420,7 +542,7 @@ def available_cpu_count():
def cleanup_ruffus_error_message(msg):
msg = re.sub(r'\s+', r' ', msg, re.MULTILINE)
msg = re.sub(r'\s+', r' ', msg)
msg = re.sub(r"\((.+?)\)", r'\1', msg)
msg = msg.strip()
return msg
@@ -429,31 +551,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
@@ -464,24 +602,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)
@@ -547,6 +681,20 @@ 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 run_pipeline():
options = parser.parse_args()
options.verbose_abbreviated_path = 1
@@ -554,13 +702,29 @@ def run_pipeline():
if not check_closed_streams(options):
return ExitCode.bad_args
logger_args = {'verbose': options.verbose, 'quiet': options.quiet}
_log, _log_mutex = proxy_logger.make_shared_logger_and_proxy(
logging_factory, __name__, [None, options.verbose])
logging_factory, __name__, logger_args)
_log.debug('ocrmypdf ' + VERSION)
_log.debug('tesseract ' + tesseract.version())
_log.debug('qpdf ' + qpdf.version())
check_options(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
# 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.)
@@ -594,15 +758,16 @@ def run_pipeline():
file."""))
return ExitCode.bad_args
elif not is_file_writable(options.output_file):
_log.error(textwrap.dedent("""\
Cutput file location is not writable."""))
_log.error(
"Output file location (" + options.output_file + ") " +
"is not a writable file.")
return ExitCode.file_access_error
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)
@@ -633,16 +798,21 @@ def run_pipeline():
_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:
_log.error(e)
return ExitCode.other_error
if options.output_file != '-':
if options.output_type == 'pdfa':
if options.flowchart:
_log.info("Flowchart saved to {}".format(options.flowchart))
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)'
@@ -650,29 +820,17 @@ 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")
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].get('rotated', 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)
+2 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
@@ -20,7 +19,7 @@ class ExitCode(IntEnum):
class ExitCodeException(Exception):
pass
exit_code = ExitCode.other_error
class PdfMergeFailedError(ExitCodeException):
@@ -48,7 +47,7 @@ class InputFileError(ExitCodeException):
class SubprocessOutputError(ExitCodeException):
exit_code = ExitCode.other_error
exit_code = ExitCode.child_process_error
class EncryptedPdfError(ExitCodeException):
+38 -1
View File
@@ -1,11 +1,48 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
"""Wrappers to manage subprocess calls"""
import os
import re
import sys
from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
def get_program(name):
"Check environment variables for overrides to this program"
envvar = 'OCRMYPDF_' + name.upper()
return os.environ.get(envvar, name)
def get_version(program, *,
version_arg='--version', regex=r'(\d+(\.\d+)*)'):
"Get the version of the specified program, "
args_prog = [
get_program(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 get_program(program) == program:
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
else:
raise MissingDependencyError(
"Could not find program '{}'".format(
get_program(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
+102 -48
View File
@@ -1,35 +1,49 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2017 James R. Barlow: github.com/jbarlow83
from tempfile import NamedTemporaryFile
from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError, \
check_output
from subprocess import run, PIPE, STDOUT, CalledProcessError
from shutil import copy
from functools import lru_cache
from . import get_program
from ..pdfa import SRGB_ICC_PROFILE
import re
import sys
from PIL import Image
from . import get_program, 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 _gs_error_reported(stream):
return re.search(r'error', stream, flags=re.IGNORECASE)
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
pageno=1):
pageno=1, page_dpi=None):
"""
Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
The image is sized to match the integer pixels dimensions implied by
(xres, yres) even if those numbers are noninteger. The image's DPI will
be overridden with the values in page_dpi.
:param input_file: pathlike
:param output_file: pathlike
:param xres: resolution at which to rasterize page
:param yres:
:param raster_device:
:param log:
:param pageno: page number to rasterize (beginning at page 1)
:param page_dpi: resolution tuple (x, y) overriding output image DPI
:return:
"""
res = xres, yres
int_res = round(xres), round(yres)
if not page_dpi:
page_dpi = res
with NamedTemporaryFile(delete=True) as tmp:
args_gs = [
get_program('gs'),
@@ -40,50 +54,89 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
'-sDEVICE=%s' % raster_device,
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
'-r{0}x{1}'.format(str(int_res[0]), str(int_res[1])),
'-o', tmp.name,
'-r{0}x{1}'.format(str(round(xres)), str(round(yres))),
input_file
fspath(input_file)
]
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
else:
log.debug(p.stdout)
if p.returncode != 0:
log.error('Ghostscript rasterizing failed')
raise SubprocessOutputError()
# Ghostscript only accepts integers for output resolution
# 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]), \
round(im.size[1] / int_res[1] * res[1])
if expected_size != im.size or page_dpi != (xres, yres):
log.debug(
"Ghostscript: resize output image {} -> {}".format(
im.size, expected_size))
im.resize(expected_size).save(
fspath(output_file), dpi=page_dpi)
else:
copy(tmp.name, fspath(output_file))
def generate_pdfa(pdf_pages, output_file, compression, log,
threads=1, pdf_version='1.5', pdfa_part='2'):
compression_args = []
if compression == 'jpeg':
compression_args = [
"-dAutoFilterColorImages=false",
"-dColorImageFilter=/DCTEncode",
"-dAutoFilterGrayImages=false",
"-dGrayImageFilter=/DCTEncode",
]
elif compression == 'lossless':
compression_args = [
"-dAutoFilterColorImages=false",
"-dColorImageFilter=/FlateEncode",
"-dAutoFilterGrayImages=false",
"-dGrayImageFilter=/FlateEncode",
]
else:
compression_args = [
"-dAutoFilterColorImages=true",
"-dAutoFilterGrayImages=true",
]
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
stdout, _ = p.communicate()
if 'error' in stdout:
log.error(stdout) # Ghostscript puts errors in stdout
else:
log.debug(stdout)
if p.returncode == 0:
copy(tmp.name, output_file)
else:
log.error('Ghostscript rendering failed')
def generate_pdfa(pdf_pages, output_file, log, threads=1):
with NamedTemporaryFile(delete=True) as gs_pdf:
args_gs = [
get_program("gs"),
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
'-dNumRenderingThreads=' + str(threads),
"-dCompatibilityLevel=" + str(pdf_version),
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=/RGB",
"-sProcessColorModel=DeviceRGB",
"-sProcessColorModel=DeviceRGB"
] + compression_args + [
"-dJPEGQ=95",
"-dPDFA=2",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
args_gs.extend(pdf_pages)
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
stdout, _ = p.communicate()
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
if 'error' in stdout or 'ERROR' in stdout:
log.error(stdout)
elif 'overprint mode not set' in stdout:
if _gs_error_reported(p.stdout):
log.error(p.stdout)
elif 'overprint mode not set' in p.stdout:
# Unless someone is going to print PDF/A documents on a
# magical sRGB printer I can't see the removal of overprinting
# being a problem....
@@ -92,11 +145,12 @@ def generate_pdfa(pdf_pages, output_file, log, threads=1):
"input file to complete PDF/A conversion. "
)
else:
log.debug(stdout)
log.debug(p.stdout)
if p.returncode == 0:
# Ghostscript does not change return code when it fails to create
# PDF/A - check PDF/A status elsewhere
copy(gs_pdf.name, output_file)
else:
log.error('Ghostscript PDF/A failed')
log.error('Ghostscript PDF/A rendering failed')
raise SubprocessOutputError()
+100 -30
View File
@@ -1,34 +1,21 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2017 James R. Barlow: github.com/jbarlow83
from subprocess import CalledProcessError, check_output, STDOUT, check_call
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_program
from . import get_program, get_version
from ..helpers import re_symlink
@lru_cache(maxsize=1)
def version():
args_qpdf = [
get_program('qpdf'),
'--version'
]
try:
versions = check_output(
args_qpdf, close_fds=True, universal_newlines=True,
stderr=STDOUT)
except CalledProcessError 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 (.+)', versions).group(1)
return qpdf_version
return get_version('qpdf', regex=r'qpdf version (.+)')
def check(input_file, log=None):
@@ -42,7 +29,8 @@ def check(input_file, log=None):
import logging as log
try:
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
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(
@@ -71,16 +59,14 @@ def repair(input_file, output_file, log):
get_program('qpdf'), input_file, output_file
]
try:
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
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)
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(
@@ -96,9 +82,9 @@ def repair(input_file, output_file, log):
def get_npages(input_file, log):
try:
pages = check_output(
pages = run(
[get_program('qpdf'), '--show-npages', input_file],
universal_newlines=True, close_fds=True)
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)
@@ -118,16 +104,100 @@ def split_pages(input_file, work_folder, npages):
'--pages', input_file, '{0}'.format(n + 1), '--',
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
]
check_call(args_qpdf)
run(args_qpdf, check=True)
def merge(input_files, output_file):
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 = [
get_program('qpdf'), input_files[0], '--pages'
get_program('qpdf')
] + version_arg + [
input_files[0], '--pages'
] + input_files + ['--', output_file]
check_call(args_qpdf)
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)
+124 -113
View File
@@ -1,93 +1,74 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2017 James R. Barlow: github.com/jbarlow83
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
import PyPDF2 as pypdf
from subprocess import PIPE, CalledProcessError, \
TimeoutExpired, check_output, STDOUT
from subprocess import Popen, PIPE, CalledProcessError, \
TimeoutExpired, check_output, STDOUT, DEVNULL
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number
from . import get_program, get_version
OrientationConfidence = namedtuple(
'OrientationConfidence',
('angle', 'confidence'))
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.02.02' />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.05.00' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "x.tif"; bbox 0 0 {0} {1}; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 0 1 {0} {1}">
<p class='ocr_par' dir='ltr' id='par_1' title="bbox 0 1 {0} {1}">
<span class='ocr_line' id='line_1' title="bbox 0 1 {0} {1}"><span class='ocrx_word' id='word_1' title="bbox 0 1 {0} {1}"> </span>
</span>
</p>
</div>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "_blank.png"; bbox 0 0 {0} {1}; ppageno 0'>
</div>
</body>
</html>'''
</html>
"""
@lru_cache(maxsize=1)
def version():
args_tess = [
get_program('tesseract'),
'--version'
]
try:
versions = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
except CalledProcessError 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)
def has_textonly_pdf():
if version() == '4.00.00alpha':
# textonly_pdf added during the 4.00.00alpha cycle, so we must test
# more carefully to see if it is present
args_tess = [
get_program('tesseract'),
'--print-parameters'
]
try:
params = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
except CalledProcessError as e:
print("Could not --print-parameters from tesseract",
file=sys.stderr)
raise MissingDependencyError from e
if 'textonly_pdf' in params:
return True
else:
return v4()
"""Does Tesseract have textonly_pdf capability?
Available in 3.05.01, and v4.00.00alpha since January 2017. Best to
parse the parameter list
"""
args_tess = [
get_program('tesseract'),
'--print-parameters'
]
params = ''
try:
params = check_output(
args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
print("Could not --print-parameters from tesseract",
file=sys.stderr)
raise MissingDependencyError from e
if 'textonly_pdf' in params:
return True
return False
def psm():
@@ -103,8 +84,7 @@ def languages():
]
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:
@@ -116,12 +96,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'),
]
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
@@ -129,7 +109,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'
@@ -137,19 +117,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:
@@ -174,8 +153,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
@@ -200,7 +188,7 @@ def page_timedout(log, input_file):
log.warning(prefix + " took too long to OCR - skipping")
def _generate_null_hocr(output_hocr, image):
def _generate_null_hocr(output_hocr, output_sidecar, image):
"""Produce a .hocr file that reports no text detected on a page that is
the same size as the input image."""
from PIL import Image
@@ -210,70 +198,73 @@ def _generate_null_hocr(output_hocr, image):
with open(output_hocr, 'w', encoding="utf-8") as f:
f.write(HOCR_TEMPLATE.format(w, h))
with open(output_sidecar, 'w', encoding='utf-8') as f:
f.write('[skipped page]')
def generate_hocr(input_file, output_hocr, language: list, engine_mode,
def generate_hocr(input_file, output_files, language: list, engine_mode,
tessconfig: list,
timeout: float, pagesegmode: int, log):
timeout: float, pagesegmode: int, user_words, user_patterns,
log):
badxml = os.path.splitext(output_hocr)[0] + '.badxml'
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
prefix = os.path.splitext(output_hocr)[0]
args_tesseract = tess_base_args(language, engine_mode)
if pagesegmode is not None:
args_tesseract.extend([psm(), str(pagesegmode)])
if user_words:
args_tesseract.extend(['--user-words', user_words])
if user_patterns:
args_tesseract.extend(['--user-patterns', user_patterns])
# Reminder: test suite tesseract spoofers will break after any changes
# to the number of order parameters here
# Tesseract 3.04 requires the order here to be "hocr txt" and will fail
# on "txt hocr"
args_tesseract.extend([
input_file,
badxml,
'hocr'
prefix,
'hocr',
'txt'
] + tessconfig)
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
# it does not have a valid hOCR file.
page_timedout(log, input_file)
_generate_null_hocr(output_hocr, input_file)
_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:
_generate_null_hocr(output_hocr, input_file)
if b'Image too large' in e.output:
_generate_null_hocr(output_hocr, output_sidecar, input_file)
return
raise e from e
else:
tesseract_log_output(log, stdout, input_file)
if os.path.exists(badxml + '.html'):
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
shutil.move(badxml + '.html', badxml)
elif os.path.exists(badxml + '.hocr'):
# Tesseract 3.03 appends suffix ".hocr" on its own (.badxml.hocr)
shutil.move(badxml + '.hocr', badxml)
# Tesseract 3.03 inserts source filename into hocr file without
# escaping it, creating invalid XML and breaking the parser.
# As a workaround, rewrite the hocr file, replacing the filename
# with a space. Don't know if Tesseract 3.02 does the same.
regex_nested_single_quotes = re.compile(
r"""title='image "([^"]*)";""")
with open(badxml, mode='r', encoding='utf-8') as f_in, \
open(output_hocr, mode='w', encoding='utf-8') as f_out:
for line in f_in:
line = regex_nested_single_quotes.sub(
r"""title='image " ";""", line)
f_out.write(line)
# The sidecar text file will get the suffix .txt; rename it to
# whatever caller wants it named
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_sidecar)
def use_skip_page(text_only, skip_pdf, output_pdf):
def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
with open(output_text, 'w') as f:
f.write('[skipped page]')
if not text_only:
os.symlink(skip_pdf, output_pdf)
return
@@ -287,18 +278,24 @@ def use_skip_page(text_only, skip_pdf, output_pdf):
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)
def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
engine_mode, text_only: bool,
tessconfig: list, timeout: float, pagesegmode: int, log):
def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
language: list, engine_mode, text_only: bool,
tessconfig: list, timeout: float, pagesegmode: int,
user_words, user_patterns, log):
'''Use Tesseract to render a PDF.
input_image -- image to analyze
skip_pdf -- if we time out, use this file as output
output_pdf -- file to generate
output_text -- OCR text file
language -- list of languages to consider
engine_mode -- engine mode argument for tess v4
text_only -- enable tesseract text only mode?
@@ -315,27 +312,41 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
if text_only:
args_tesseract.extend(['-c', 'textonly_pdf=1'])
if user_words:
args_tesseract.extend(['--user-words', user_words])
if user_patterns:
args_tesseract.extend(['--user-patterns', user_patterns])
prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes
# Reminder: test suite tesseract spoofers might break after any changes
# to the number of order parameters here
args_tesseract.extend([
input_image,
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
'pdf'
prefix,
'pdf',
'txt'
] + tessconfig)
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:
page_timedout(log, input_image)
use_skip_page(text_only, skip_pdf, output_pdf)
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:
use_skip_page(text_only, skip_pdf, output_pdf)
if b'Image too large' in e.output:
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
return
raise e from e
else:
+7 -15
View File
@@ -1,27 +1,14 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# 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_program, get_version
try:
@@ -31,6 +18,11 @@ 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'),
+64 -8
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
from functools import partial
from collections.abc import Iterable
from contextlib import suppress
from contextlib import suppress, contextmanager
from pathlib import Path
import sys
import os
@@ -32,7 +32,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):
@@ -63,17 +63,73 @@ 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):
resolve = lambda: Path(os.path.realpath(str(p)))
else:
resolve = lambda: p.resolve(strict=False)
p = resolve()
# 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
if sys.version_info[0:2] <= (3, 5):
def universal_open(p, *args, **kwargs):
"Work around Python 3.5's inability to open(pathlib.Path())"
try:
return p.open(*args, **kwargs)
except AttributeError:
return open(p, *args, **kwargs)
def fspath(path):
import pathlib
'''https://www.python.org/dev/peps/pep-0519/#os'''
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path = path_type.__fspath__(path)
except AttributeError:
# Added for Python 3.5 support.
if isinstance(path, pathlib.Path):
return str(path)
elif hasattr(path_type, '__fspath__'):
raise
else:
if isinstance(path, (str, bytes)):
return path
else:
raise TypeError("expected __fspath__() to return str or bytes, "
"not " + type(path).__name__)
raise TypeError(
"expected str, bytes, pathlib.Path or os.PathLike object, not "
+ path_type.__name__)
else:
universal_open = open
fspath = os.fspath
+28 -7
View File
@@ -1,12 +1,33 @@
#!/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
##############################################################################
# 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
+8 -37
View File
@@ -14,6 +14,7 @@ from ctypes.util import find_library
from .lib._leptonica import ffi
from functools import lru_cache
from enum import Enum
from .helpers import fspath
lept = ffi.dlopen(find_library('lept'))
@@ -208,26 +209,27 @@ class Pix:
return 'P'
@classmethod
def read(cls, filename):
def read(cls, path):
"""Load an image file into a PIX object.
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
loading fails then the object will wrap a C null pointer.
"""
filename = fspath(path)
with LeptonicaErrorTrap():
return cls(lept.pixRead(
filename.encode(sys.getfilesystemencoding())))
return cls(lept.pixRead(os.fsencode(filename)))
def write_implied_format(
self, filename, jpeg_quality=0, jpeg_progressive=0):
self, path, jpeg_quality=0, jpeg_progressive=0):
"""Write pix to the filename, with the extension indicating format.
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
"""
filename = fspath(path)
with LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
filename.encode(sys.getfilesystemencoding()),
os.fsencode(filename),
self._pix, jpeg_quality, jpeg_progressive)
def topil(self):
@@ -564,35 +566,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)
+1
View File
@@ -1 +1,2 @@
"""Bindings to external libraries"""
# © 2017 James R. Barlow: github.com/jbarlow83
+34 -2
View File
@@ -1,3 +1,6 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
from cffi import FFI
ffibuilder = FFI()
@@ -181,10 +184,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);
""")
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env python3
# © 2015-16 James R. Barlow: github.com/jbarlow83
# This file is now an alias for __main__
# Consider removing in future releases
from ocrmypdf.__main__ import *
+9 -9
View File
@@ -3,19 +3,16 @@
from subprocess import Popen, PIPE
from decimal import Decimal
from math import hypot
from math import hypot, isclose
import re
import sys
import PyPDF2 as pypdf
from collections import namedtuple
import warnings
try:
from math import isclose
except ImportError:
def isclose(a, b, rel_tol=1e-9):
"Python 3.4 does not have math.isclose()"
diff = abs(b - a)
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
warnings.warn("ocrmypdf.pageinfo is deprecated'; use ocrmypdf.pdfinfo",
DeprecationWarning)
matrix_mult = pypdf.pdf.utils.matrixMultiply
@@ -430,7 +427,7 @@ def _find_images(pdf, container, shorthand=None):
"""
if container.get('/Type') == '/Page':
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)
@@ -464,6 +461,9 @@ def _find_images(pdf, container, shorthand=None):
def _page_has_text(pdf, page):
if not '/Contents' in page:
return False
# Simple test
text = page.extractText()
if text.strip() != '':
-5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
#
# Generate a PDFA_def.ps file for Ghostscript >= 9.14
@@ -122,10 +121,6 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
f.write(ps)
# The old name is generate_pdfa_def -- now deprecated
generate_pdfa_def = generate_pdfa_ps
def file_claims_pdfa(filename):
"""Determines if the file claims to be PDF/A compliant
+725
View File
@@ -0,0 +1,725 @@
#!/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
from collections.abc import MutableMapping, Mapping
import warnings
from pathlib import Path
from enum import Enum
from .helpers import universal_open
matrix_mult = pypdf.pdf.utils.matrixMultiply
Colorspace = Enum('Colorspace',
'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
Encoding = Enum('Encoding',
'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + \
'runlength')
FRIENDLY_COLORSPACE = {
'/DeviceGray': Colorspace.gray,
'/CalGray': Colorspace.gray,
'/DeviceRGB': Colorspace.rgb,
'/CalRGB': Colorspace.rgb,
'/DeviceCMYK': Colorspace.cmyk,
'/Lab': Colorspace.lab,
'/ICCBased': Colorspace.icc,
'/Indexed': Colorspace.index,
'/Separation': Colorspace.sep,
'/DeviceN': Colorspace.devn,
'/Pattern': Colorspace.pattern,
'/G': Colorspace.gray, # Abbreviations permitted in inline images
'/RGB': Colorspace.rgb,
'/CMYK': Colorspace.cmyk,
'/I': Colorspace.index,
}
FRIENDLY_ENCODING = {
'/CCITTFaxDecode': Encoding.ccitt,
'/DCTDecode': Encoding.jpeg,
'/JPXDecode': Encoding.jpeg2000,
'/JBIG2Decode': Encoding.jbig2,
'/CCF': Encoding.ccitt, # Abbreviations permitted in inline images
'/DCT': Encoding.jpeg,
'/AHx': Encoding.asciihex,
'/A85': Encoding.ascii85,
'/LZW': Encoding.lzw,
'/Fl': Encoding.flate,
'/RL': Encoding.runlength
}
FRIENDLY_COMP = {
Colorspace.gray: 1,
Colorspace.rgb: 3,
Colorspace.cmyk: 4,
Colorspace.lab: 3,
Colorspace.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 unit of default user space (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
class ImageInfo:
DPI_PREC = Decimal('1.000')
def __init__(self, *, name='', pdfimage=None, inline=None,
shorthand=None):
self._name = name
self._shorthand = shorthand
if inline:
# Fixme does not work for inline images with non abbreviated
# fields
self._origin = 'inline'
self._width = inline.settings['/W']
self._height = inline.settings['/H']
self._bpc = inline.settings.get('/BPC', 8)
try:
self._color = FRIENDLY_COLORSPACE[inline.settings['/CS']]
except Exception:
self._color = '-'
self._comp = FRIENDLY_COMP.get(self._color, '?')
if '/F' in inline.settings:
filter_ = inline.settings['/F']
if isinstance(filter_, pypdf.generic.ArrayObject):
filter_ = filter_[0]
self._enc = FRIENDLY_ENCODING.get(filter_, 'image')
else:
self._enc = 'image'
elif pdfimage:
self._origin = 'xobject'
self._width = pdfimage['/Width']
self._height = pdfimage['/Height']
if '/BitsPerComponent' in pdfimage:
self._bpc = pdfimage['/BitsPerComponent']
else:
self._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:
self._type = 'stencil' if pdfimage['/ImageMask'].value \
else 'image'
else:
self._type = 'image'
if '/Filter' in pdfimage:
filter_ = pdfimage['/Filter']
if isinstance(filter_, pypdf.generic.ArrayObject):
filter_ = filter_[0]
self._enc = FRIENDLY_ENCODING.get(filter_, 'image')
else:
self._enc = 'image'
if '/ColorSpace' in pdfimage:
cs = pdfimage['/ColorSpace']
if isinstance(cs, pypdf.generic.ArrayObject):
cs = cs[0]
self._color = FRIENDLY_COLORSPACE.get(cs, '-')
else:
self._color = FRIENDLY_COLORSPACE[Colorspace.jpeg2000] \
if self._enc == Encoding.jpeg2000 else '?'
self._comp = FRIENDLY_COMP.get(self._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 self._comp == '?' and self._enc in (Encoding.ccitt, 'jbig2'):
self._comp = FRIENDLY_COMP[Colorspace.gray]
@property
def name(self):
return self._name
@property
def type_(self):
return self._type
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@property
def bpc(self):
return self._bpc
@property
def color(self):
return self._color
@property
def comp(self):
return self._comp
@property
def enc(self):
return self._enc
@property
def xres(self):
return _get_dpi(self._shorthand, (self._width, self._height))[0]
@property
def yres(self):
return _get_dpi(self._shorthand, (self._width, self._height))[1]
def __getitem__(self, item):
warnings.warn("ImageInfo.__getitem__", DeprecationWarning)
if item in ('name', 'width', 'height', 'bpc', 'color', 'comp', 'enc'):
return getattr(self, item)
elif item == 'dpi_w':
return Decimal(self.xres).quantize(self.DPI_PREC)
elif item == 'dpi_h':
return Decimal(self.yres).quantize(self.DPI_PREC)
elif item == 'dpi':
return Decimal(self.xres * self.yres).sqrt().quantize(
self.DPI_PREC)
else:
raise KeyError(item)
def __repr__(self):
class_locals = {attr: getattr(self, attr, None) for attr in dir(self)
if not attr.startswith('_')}
return (
"<ImageInfo '{name}' {type_} {width}x{height} {color} "
"{comp} {bpc} {enc} {xres}x{yres}>").format(**class_locals)
def _find_inline_images(contentsinfo):
"Find inline images in the contentstream"
for n, inline in enumerate(contentsinfo.inline_images):
yield ImageInfo(name='inline-%02d' % n, shorthand=inline.shorthand,
inline=inline)
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':
pdfimage = candidate
yield (pdfimage, 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):
# For each image that is drawn on this, check if we drawing the
# current image - yes this is O(n^2), but n == 1 almost always
for draw in contentsinfo.xobject_settings:
if draw.name != xobj:
continue
if draw.stack_depth == 0 and _is_unit_square(draw.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
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=
draw.shorthand)
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=pdf, container=form_xobject, shorthand=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(pdf, pageno: int):
pageinfo = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
if isinstance(pdf, Path):
pdf = pypdf.PdfFileReader(str(pdf))
elif isinstance(pdf, str):
pdf = pypdf.PdfFileReader(pdf)
page = pdf.pages[pageno]
pageinfo['has_text'] = _page_has_text(pdf, page)
width_pt = page.mediaBox.getWidth()
height_pt = page.mediaBox.getHeight()
userunit = page.get('/UserUnit', Decimal(1.0))
pageinfo['userunit'] = userunit
pageinfo['width_inches'] = width_pt * userunit / Decimal(72.0)
pageinfo['height_inches'] = height_pt * userunit / Decimal(72.0)
try:
pageinfo['rotate'] = int(page['/Rotate'])
except KeyError:
pageinfo['rotate'] = 0
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
pageinfo['images'] = [im for im in
_find_images(pdf=pdf, container=page,
shorthand=userunit_shorthand)]
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):
with universal_open(infile, 'rb') as f:
pdf = pypdf.PdfFileReader(f)
return [PageInfo(pdf, n) for n in range(pdf.numPages)]
class PageInfo:
def __init__(self, pdf, pageno):
self._pageno = pageno
self._pageinfo = _pdf_get_pageinfo(pdf, pageno)
@property
def pageno(self):
return self._pageno
@property
def has_text(self):
return self._pageinfo['has_text']
@property
def width_inches(self):
return self._pageinfo['width_inches']
@property
def height_inches(self):
return self._pageinfo['height_inches']
@property
def width_pixels(self):
return int(round(self.width_inches * self.xres))
@property
def height_pixels(self):
return int(round(self.height_inches * self.yres))
@property
def rotation(self):
return self._pageinfo.get('rotate', None)
@rotation.setter
def rotation(self, value):
if value in (0, 90, 180, 270, 360, -90, -180, -270):
self._pageinfo['rotate'] = value
else:
raise ValueError("rotation must be a cardinal angle")
@property
def images(self):
return self._pageinfo['images']
@property
def xres(self):
return self._pageinfo.get('xres', None)
@property
def yres(self):
return self._pageinfo.get('yres', None)
@property
def userunit(self):
return self._pageinfo.get('userunit', None)
@property
def min_version(self):
if self.userunit is not None:
return '1.6'
else:
return '1.5'
def __repr__(self):
return (
'<PageInfo '
'pageno={} {}"x{}" rotation={} res={}x{} has_text={}>').format(
self.pageno, self.width_inches, self.height_inches,
self.rotation,
self.xres, self.yres, self.has_text
)
class PdfInfo:
"""Get summary information about a PDF
"""
def __init__(self, infile):
self._infile = infile
self._pages = _pdf_get_all_pageinfo(infile)
@property
def pages(self):
return self._pages
@property
def min_version(self):
# The minimum PDF is the maximum version that any particular page needs
return max(page.min_version for page in self.pages)
@property
def has_userunit(self):
return any(page.userunit != 1.0 for page in self.pages)
def __getitem__(self, item):
return self._pages[item]
def __len__(self):
return len(self._pages)
def __repr__(self):
return "<PdfInfo('...'), page count={}>".format(len(self))
# def __getstate__(self):
# state = {'_infile': self._infile}
# return state
#
# def __setstate__(self, state):
# self._infile = state['_infile']
# self._pages = _pdf_get_all_pageinfo(self._infile)
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()
+280 -118
View File
@@ -1,32 +1,24 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
from contextlib import suppress
from tempfile import mkdtemp
from functools import partial
from shutil import copyfileobj
import sys
import os
import re
import shutil
import warnings
import multiprocessing
import atexit
import textwrap
import img2pdf
import logging
import argparse
import re
import PyPDF2 as pypdf
from PIL import Image
from ruffus import formatter, regex, Pipeline, suffix
from .hocrtransform import HocrTransform
from .pageinfo import pdf_get_all_pageinfo
from .pdfa import generate_pdfa_ps, file_claims_pdfa
from .pdfinfo import PdfInfo, Encoding, Colorspace
from .pdfa import generate_pdfa_ps
from .helpers import re_symlink, is_iterable_notstr, page_number
from .exec import ghostscript, tesseract, qpdf
from .exceptions import *
from .exceptions import PdfMergeFailedError, UnsupportedImageFormatError, \
DpiError, PriorOcrFoundError, InputFileError
from . import leptonica
from . import PROGRAM_NAME, VERSION
@@ -50,7 +42,12 @@ class JobContext:
"""
def __init__(self):
self.pdfinfo = []
self.pdfinfo = None
self.options = None
self.work_folder = None
def generate_pdfinfo(self, infile):
self.pdfinfo = PdfInfo(infile)
def get_pdfinfo(self):
"What we know about the input PDF"
@@ -72,8 +69,8 @@ class JobContext:
self.work_folder = work_folder
from multiprocessing.managers import BaseManager
class JobContextManager(BaseManager):
from multiprocessing.managers import SyncManager
class JobContextManager(SyncManager):
pass
@@ -153,23 +150,40 @@ 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,
log,
context):
options = context.get_options()
try:
with open(input_file, 'rb') as f:
signature = f.read(4)
if signature == b'%PDF':
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
options = context.get_options()
triage_image_file(input_file, output_file, log, options)
@@ -178,38 +192,83 @@ def repair_pdf(
output_file,
log,
context):
options = context.get_options()
qpdf.repair(input_file, output_file, log)
pdfinfo = pdf_get_all_pageinfo(output_file)
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 "
"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()
context.set_pdfinfo(pdfinfo)
log.debug(pdfinfo)
def get_pageinfo(input_file, context):
pageno = int(os.path.basename(input_file)[0:6]) - 1
pageno = page_number(input_file) - 1
pageinfo = context.get_pdfinfo()[pageno]
return pageinfo
def get_page_dpi(pageinfo, options):
"Get the DPI when nonsquare DPI is tolerable"
xres = max(pageinfo.get('xres', VECTOR_PAGE_DPI), options.oversample or 0)
yres = max(pageinfo.get('yres', VECTOR_PAGE_DPI), options.oversample or 0)
xres = max(pageinfo.xres or VECTOR_PAGE_DPI, options.oversample or 0)
yres = max(pageinfo.yres or VECTOR_PAGE_DPI, options.oversample or 0)
return (float(xres), float(yres))
def get_page_square_dpi(pageinfo, options):
"Get the DPI when we require xres == yres"
"Get the DPI when we require xres == yres, scaled to physical units"
xres = pageinfo.xres or 0
yres = pageinfo.yres or 0
userunit = pageinfo.userunit or 1
return float(max(
pageinfo.get('xres', VECTOR_PAGE_DPI),
pageinfo.get('yres', VECTOR_PAGE_DPI),
(xres * userunit) or VECTOR_PAGE_DPI,
(yres * userunit) or VECTOR_PAGE_DPI,
options.oversample or 0))
def get_canvas_square_dpi(pageinfo, options):
"""Get the DPI when we require xres == yres, in Postscript units"""
return float(max(
(pageinfo.xres) or VECTOR_PAGE_DPI,
(pageinfo.yres) or VECTOR_PAGE_DPI,
options.oversample or 0))
def is_ocr_required(pageinfo, log, options):
page = pageinfo['pageno'] + 1
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(
@@ -228,27 +287,13 @@ def is_ocr_required(pageinfo, log, options):
else:
log.info(
"{0:4d}: page has no images - "
"skipping all processing on this page".format(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
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"))
ocr_required = False
if ocr_required and options.skip_big and pageinfo['images']:
pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels']
if ocr_required and options.skip_big and pageinfo.images:
pixel_count = pageinfo.width_pixels * pageinfo.height_pixels
if pixel_count > (options.skip_big * 1000000):
ocr_required = False
log.warning(
@@ -306,13 +351,14 @@ def rasterize_preview(
output_file,
log,
context):
pageinfo = get_pageinfo(input_file, context)
options = context.get_options()
canvas_dpi = get_canvas_square_dpi(pageinfo, options) / 2
page_dpi = get_page_square_dpi(pageinfo, options) / 2
ghostscript.rasterize_pdf(
input_file=input_file,
output_file=output_file,
xres=200,
yres=200,
raster_device='jpeggray',
log=log)
input_file, output_file, xres=canvas_dpi, yres=canvas_dpi,
raster_device='jpeggray', log=log, page_dpi=(page_dpi, page_dpi))
def orient_page(
@@ -378,9 +424,9 @@ def orient_page(
with open(output_file, 'wb') as out:
writer.write(out)
pageno = int(os.path.basename(page_pdf)[0:6]) - 1
pageno = page_number(page_pdf) - 1
pdfinfo = context.get_pdfinfo()
pdfinfo[pageno]['rotated'] = orient_conf.angle
pdfinfo[pageno].rotation = orient_conf.angle
context.set_pdfinfo(pdfinfo)
@@ -393,26 +439,28 @@ def rasterize_with_ghostscript(
pageinfo = get_pageinfo(input_file, context)
device = 'png16m' # 24-bit
if pageinfo['images']:
if all(image['comp'] == 1 for image in pageinfo['images']):
if all(image['bpc'] == 1 for image in pageinfo['images']):
if pageinfo.images:
if all(image.comp == 1 for image in pageinfo.images):
if all(image.bpc == 1 for image in pageinfo.images):
device = 'pngmono'
elif all(image['bpc'] > 1 and image['color'] == 'index'
for image in pageinfo['images']):
elif all(image.bpc > 1 and image.color == Colorspace.index
for image in pageinfo.images):
device = 'png256'
elif all(image['bpc'] > 1 and image['color'] == 'gray'
for image in pageinfo['images']):
elif all(image.bpc > 1 and image.color == Colorspace.gray
for image in pageinfo.images):
device = 'pnggray'
log.debug("Rasterize {0} with {1}".format(
os.path.basename(input_file), device))
# Produce the page image with square resolution or else deskew and OCR
# will not work properly
dpi = get_page_square_dpi(pageinfo, options)
# will not work properly.
canvas_dpi = get_canvas_square_dpi(pageinfo, options)
page_dpi = get_page_square_dpi(pageinfo, options)
ghostscript.rasterize_pdf(
input_file, output_file, xres=dpi, yres=dpi, raster_device=device,
log=log)
input_file, output_file, xres=canvas_dpi, yres=canvas_dpi,
raster_device=device, log=log, page_dpi=(page_dpi, page_dpi))
def preprocess_remove_background(
@@ -427,11 +475,11 @@ def preprocess_remove_background(
pageinfo = get_pageinfo(input_file, context)
if any(image['bpc'] > 1 for image in pageinfo['images']):
if any(image.bpc > 1 for image in pageinfo.images):
leptonica.remove_background(input_file, output_file)
else:
log.info("{0:4d}: background removal skipped on mono page".format(
pageinfo['pageno']))
pageinfo.pageno))
re_symlink(input_file, output_file, log)
@@ -472,7 +520,7 @@ def select_ocr_image(
infiles,
output_file,
log,
contenxt):
context):
"""Select the image we send for OCR. May not be the same as the display
image depending on preprocessing."""
@@ -483,18 +531,20 @@ def select_ocr_image(
def ocr_tesseract_hocr(
input_file,
output_file,
output_files,
log,
context):
options = context.get_options()
tesseract.generate_hocr(
input_file=input_file,
output_hocr=output_file,
output_files=output_files,
language=options.language,
engine_mode=options.tesseract_oem,
tessconfig=options.tesseract_config,
timeout=options.tesseract_timeout,
pagesegmode=options.tesseract_pagesegmode,
user_words=options.user_words,
user_patterns=options.user_patterns,
log=log
)
@@ -518,8 +568,10 @@ def select_visible_page_image(
image = next(ii for ii in infiles if ii.endswith(image_suffix))
pageinfo = get_pageinfo(image, context)
if pageinfo['images'] and \
all(im['enc'] == 'jpeg' for im in pageinfo['images']):
if pageinfo.images and \
all(im['enc'] == 'jpeg' for im in pageinfo.images):
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)
@@ -563,7 +615,7 @@ def select_image_layer(
# 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 tess4 renderer would be fine.
# 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))
@@ -577,12 +629,12 @@ def select_image_layer(
def render_hocr_page(
input_file,
infiles,
output_file,
log,
context):
options = context.get_options()
hocr = input_file
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
pageinfo = get_pageinfo(hocr, context)
dpi = get_page_square_dpi(pageinfo, options)
@@ -591,14 +643,22 @@ def render_hocr_page(
showBoundingboxes=False, invisibleText=True)
def flatten_groups(groups):
for obj in groups:
if is_iterable_notstr(obj):
yield from obj
else:
yield obj
def render_hocr_debug_page(
infiles,
output_file,
log,
context):
options = context.get_options()
hocr = next(ii for ii in infiles if ii.endswith('.hocr'))
image = next(ii for ii in infiles if ii.endswith('.image'))
hocr = next(ii for ii in flatten_groups(infiles) if ii.endswith('.hocr'))
image = next(ii for ii in flatten_groups(infiles) if ii.endswith('.image'))
pageinfo = get_pageinfo(image, context)
dpi = get_page_square_dpi(pageinfo, options)
@@ -613,8 +673,10 @@ def combine_layers(
output_file,
log,
context):
text = next(ii for ii in infiles if ii.endswith('.text.pdf'))
image = next(ii for ii in infiles if ii.endswith('.image-layer.pdf'))
text = next(ii for ii in flatten_groups(infiles)
if ii.endswith('.text.pdf'))
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"))
@@ -662,7 +724,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(
@@ -674,57 +736,77 @@ def combine_layers(
pdf_output = pypdf.PdfFileWriter()
pdf_output.addPage(page_text)
# If the input was scaled, re-apply the scaling
pageinfo = get_pageinfo(text, context)
if pageinfo.userunit != 1:
page_text[pypdf.generic.NameObject('/UserUnit')] = pageinfo.userunit
pdf_output._header = b'%PDF-1.6' # Hack header to correct version
with open(output_file, "wb") as out:
pdf_output.write(out)
def ocr_tesseract_and_render_pdf(
infiles,
output_file,
outfiles,
log,
context):
options = context.get_options()
input_image = next((ii for ii in infiles if ii.endswith('.image')), '')
input_pdf = next((ii for ii in infiles if ii.endswith('.pdf')))
output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf')))
output_text = next((ii for ii in outfiles if ii.endswith('.txt')))
if not input_image:
# Skipping this page
re_symlink(input_pdf, output_file, log)
re_symlink(input_pdf, output_pdf, log)
with open(output_text, 'w') as f:
f.write('[skipped page]')
return
tesseract.generate_pdf(
input_image=input_image,
skip_pdf=input_pdf,
output_pdf=output_file,
output_pdf=output_pdf,
output_text=output_text,
language=options.language,
engine_mode=options.tesseract_oem,
text_only=False,
tessconfig=options.tesseract_config,
timeout=options.tesseract_timeout,
pagesegmode=options.tesseract_pagesegmode,
user_words=options.user_words,
user_patterns=options.user_patterns,
log=log)
def ocr_tesseract_textonly_pdf(
infiles,
output_file,
outfiles,
log,
context):
options = context.get_options()
input_image = next((ii for ii in infiles if ii.endswith('.ocr.png')), '')
if not input_image:
raise ValueError("No image rendered?")
skip_pdf = next((ii for ii in infiles if ii.endswith('.pdf')))
output_pdf = next((ii for ii in outfiles if ii.endswith('.pdf')))
output_text = next((ii for ii in outfiles if ii.endswith('.txt')))
tesseract.generate_pdf(
input_image=input_image,
skip_pdf=skip_pdf,
output_pdf=output_file,
output_pdf=output_pdf,
output_text=output_text,
language=options.language,
engine_mode=options.tesseract_oem,
text_only=True,
tessconfig=options.tesseract_config,
timeout=options.tesseract_timeout,
pagesegmode=options.tesseract_pagesegmode,
user_words=options.user_words,
user_patterns=options.user_patterns,
log=log)
@@ -754,10 +836,17 @@ def get_pdfmark(base_pdf, options):
if options.subject:
pdfmark['/Subject'] = options.subject
pdfmark['/Creator'] = '{0} {1} / Tesseract OCR{2} {3}'.format(
PROGRAM_NAME, VERSION,
'+PDF' if options.pdf_renderer == 'tesseract' else '',
tesseract.version())
if options.pdf_renderer == 'tesseract':
renderer_tag = 'OCR+PDF'
elif options.pdf_renderer == 'sandwich':
renderer_tag = 'OCR-PDF'
else:
renderer_tag = 'OCR'
pdfmark['/Creator'] = '{0} {1} / Tesseract {2} {3}'.format(
PROGRAM_NAME, VERSION,
renderer_tag,
tesseract.version())
return pdfmark
@@ -785,7 +874,7 @@ def skip_page(
def merge_pages_ghostscript(
input_files,
input_files_groups,
output_file,
log,
context):
@@ -798,22 +887,35 @@ def merge_pages_ghostscript(
right that way.'''
if s.endswith('.ps'):
return 99999999
key = int(os.path.basename(s)[0:6]) * 10
key = page_number(s) * 10
if 'debug' in os.path.basename(s):
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))
ghostscript.generate_pdfa(pdf_pages, output_file, log, options.jobs or 1)
input_pdfinfo = context.get_pdfinfo()
ghostscript.generate_pdfa(
pdf_version=input_pdfinfo.min_version,
pdf_pages=pdf_pages,
output_file=output_file,
compression=options.pdfa_image_compression,
log=log,
threads=options.jobs or 1,
pdfa_part=('1' if options.output_type == 'pdfa-1' else '2'))
def merge_pages_qpdf(
input_files,
input_files_groups,
output_file,
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)
@@ -821,7 +923,7 @@ def merge_pages_qpdf(
def input_file_order(s):
'''Sort order: All rendered pages followed
by their debug page.'''
key = int(os.path.basename(s)[0:6]) * 10
key = page_number(s) * 10
if 'debug' in os.path.basename(s):
key += 1
return key
@@ -836,6 +938,7 @@ def merge_pages_qpdf(
first_page = pypdf.PdfFileReader(pdf_pages[0])
writer = pypdf.PdfFileWriter()
writer._header = b'%PDF-' + _pdf_guess_version(pdf_pages[0]) # copy version from source
writer.appendPagesFromReader(first_page)
writer.addMetadata(pdfmark)
writer_file = pdf_pages[0].replace('.pdf', '.metadata.pdf')
@@ -844,7 +947,51 @@ def merge_pages_qpdf(
pdf_pages[0] = writer_file
qpdf.merge(pdf_pages, output_file)
qpdf.merge(input_files=pdf_pages, output_file=output_file,
min_version=context.get_pdfinfo().min_version,
log=log)
def merge_sidecars(
input_files_groups,
output_file,
log,
context):
pdfinfo = context.get_pdfinfo()
txt_files = [None] * len(pdfinfo)
for infile in flatten_groups(input_files_groups):
if infile.endswith('.txt'):
idx = page_number(infile) - 1
txt_files[idx] = infile
def write_pages(stream):
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', 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_num + 1))
if output_file == '-':
write_pages(sys.stdout)
sys.stdout.flush()
else:
with open(output_file, 'w', encoding="utf-8") as out:
write_pages(out)
def copy_final(
@@ -854,13 +1001,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):
@@ -951,7 +1101,7 @@ def build_pipeline(options, work_folder, log, context):
task_func=ocr_tesseract_hocr,
input=task_select_ocr_image,
filter=suffix(".ocr.png"),
output=".hocr",
output=[".hocr", ".txt"],
extras=[log, context])
task_ocr_tesseract_hocr.graphviz(fillcolor='"#00cc66"')
task_ocr_tesseract_hocr.active_if(options.pdf_renderer == 'hocr')
@@ -978,13 +1128,13 @@ def build_pipeline(options, work_folder, log, context):
task_select_image_layer.graphviz(
fillcolor='"#00cc66"', shape='diamond')
task_select_image_layer.active_if(
options.pdf_renderer == 'hocr' or options.pdf_renderer == 'tess4')
options.pdf_renderer == 'hocr' or options.pdf_renderer == 'sandwich')
task_render_hocr_page = main_pipeline.transform(
task_func=render_hocr_page,
input=task_ocr_tesseract_hocr,
filter=suffix('.hocr'),
output='.text.pdf',
filter=regex(r".*/(\d{6})(?:\.hocr)"),
output=os.path.join(work_folder, r'\1.text.pdf'),
extras=[log, context])
task_render_hocr_page.graphviz(fillcolor='"#00cc66"')
task_render_hocr_page.active_if(options.pdf_renderer == 'hocr')
@@ -1004,10 +1154,11 @@ def build_pipeline(options, work_folder, log, context):
task_func=ocr_tesseract_textonly_pdf,
input=[task_select_ocr_image, task_orient_page],
filter=regex(r".*/(\d{6})(?:\.ocr.png|\.ocr\.oriented\.pdf)"),
output=os.path.join(work_folder, r'\1.text.pdf'),
output=[os.path.join(work_folder, r'\1.text.pdf'),
os.path.join(work_folder, r'\1.text.txt')],
extras=[log, context])
task_ocr_tesseract_textonly_pdf.graphviz(fillcolor='"#ff69b4"')
task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'tess4')
task_ocr_tesseract_textonly_pdf.active_if(options.pdf_renderer == 'sandwich')
if tesseract.v4():
task_ocr_tesseract_textonly_pdf.jobs_limit(2)
@@ -1020,14 +1171,16 @@ 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 == 'tess4')
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(
task_func=ocr_tesseract_and_render_pdf,
input=[task_select_visible_page_image, task_orient_page],
filter=regex(r".*/(\d{6})(?:\.image|\.ocr\.oriented\.pdf)"),
output=os.path.join(work_folder, r'\1.rendered.pdf'),
output=[os.path.join(work_folder, r'\1.rendered.pdf'),
os.path.join(work_folder, r'\1.rendered.txt')],
extras=[log, context])
task_ocr_tesseract_and_render_pdf.graphviz(fillcolor='"#66ccff"')
task_ocr_tesseract_and_render_pdf.active_if(options.pdf_renderer == 'tesseract')
@@ -1041,7 +1194,7 @@ def build_pipeline(options, work_folder, log, context):
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
@@ -1063,7 +1216,7 @@ 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,
@@ -1076,8 +1229,17 @@ def build_pipeline(options, work_folder, log, context):
extras=[log, context])
task_merge_pages_qpdf.active_if(options.output_type == 'pdf')
task_merge_sidecars = main_pipeline.merge(
task_func=merge_sidecars,
input=[task_ocr_tesseract_hocr,
task_ocr_tesseract_and_render_pdf,
task_ocr_tesseract_textonly_pdf],
output=options.sidecar,
extras=[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],
output=options.output_file,
+5 -5
View File
@@ -2,8 +2,8 @@
# setup.py lists a separate set of requirements that are looser to simplify
# installation
ruffus == 2.6.3
Pillow == 4.0.0
reportlab == 3.3.0
PyPDF2 == 1.26
img2pdf == 0.2.3
cffi == 1.9.1
Pillow == 5.0.0
reportlab == 3.4.0
PyPDF2 == 1.26.0
img2pdf == 0.2.4
cffi == 1.11.2
+2 -1
View File
@@ -1,5 +1,5 @@
[bdist_wheel]
python-tag = py34
python-tag = py35
[aliases]
test=pytest
@@ -11,3 +11,4 @@ ignore =
[tool:pytest]
norecursedirs = lib .pc .git output cache resources
testpaths = tests
addopts = -n auto
+13 -7
View File
@@ -5,8 +5,8 @@
from __future__ import print_function, unicode_literals
import sys
if sys.version_info < (3, 4):
print("Python 3.4 or newer is required", file=sys.stderr)
if sys.version_info < (3, 5):
print("Python 3.5 or newer is required", file=sys.stderr)
sys.exit(1)
from setuptools import setup, find_packages # nopep8
@@ -155,7 +155,7 @@ if not forced and command.startswith('install') or \
command in ['check', 'test', 'nosetests', 'easy_install']:
check_external_program(
program='tesseract',
need_version='3.03', # limited by Travis CI / Ubuntu 14.04 backports
need_version='3.04', # using backport for Travis CI
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
)
check_external_program(
@@ -185,9 +185,15 @@ 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',
@@ -195,8 +201,6 @@ setup(
packages=find_packages(exclude=["tests", "tests.*"]),
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Development Status :: 5 - Production/Stable",
@@ -213,9 +217,11 @@ setup(
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
],
python_requires='>=3.5',
setup_requires=[
'setuptools_scm', # so that version will work
'cffi>=1.9.1' # to build the leptonica module
'cffi>=1.9.1', # to build the leptonica module
'pytest-runner' # to enable python setup.py test
],
use_scm_version={'version_scheme': 'post-release'},
cffi_modules=[
@@ -224,7 +230,7 @@ setup(
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.2.0', # oldest released version with sane image handling
'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
+5 -2
View File
@@ -1,2 +1,5 @@
pytest >= 3.0
pytest-helpers-namespace
pytest >= 3.2
pytest-helpers-namespace
pytest-xdist
pytest-cov
pytest-timeout
+17 -5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
import sys
@@ -66,6 +65,18 @@ def spoof(**kwargs):
return env
@pytest.fixture
def spoof_tesseract_noop():
return spoof(tesseract='tesseract_noop.py')
@pytest.fixture
def spoof_tesseract_cache():
if running_in_docker():
return os.environ.copy()
return spoof(tesseract="tesseract_cache.py")
@pytest.fixture
def resources():
return Path(TESTS_ROOT) / 'resources'
@@ -117,7 +128,8 @@ def run_ocrmypdf(input_file, output_file, *args, env=None):
if env is None:
env = os.environ
p_args = OCRMYPDF + list(args) + [str(input_file), str(output_file)]
p_args = OCRMYPDF + [str(arg) for arg in args] + \
[str(input_file), str(output_file)]
p = Popen(
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
universal_newlines=True, env=env)
@@ -129,7 +141,7 @@ def run_ocrmypdf(input_file, output_file, *args, env=None):
@pytest.helpers.register
def first_page_dimensions(pdf):
from ocrmypdf import pageinfo
info = pageinfo.pdf_get_all_pageinfo(str(pdf))
from ocrmypdf import pdfinfo
info = pdfinfo.PdfInfo(pdf)
page0 = info[0]
return (page0['width_inches'], page0['height_inches'])
return (page0.width_inches, page0.height_inches)
+30 -6
View File
@@ -33,7 +33,10 @@ In some cases they were converted from one image format to another without other
- Creative Commons BY-SA 3.0
* - typewriter.png, 2400dpi.pdf
- `Wikimedia: Triumph typewrtier text Linzensoep`_
* Creative Commons BY-SA 2.5
- Creative Commons BY-SA 2.5
* - baiona.png
- `Wikimedia: Baionako udalerri mugakideak`_
- Creative Commons BY-SA 4.0
Files generated for this project
@@ -54,10 +57,13 @@ under the terms of the license in LICENSE.rst.
- test image with 200 x 100 DPI resolution
* - blank.pdf
- @jbarlow83
- blank PDF
- blank PDF generated by Adobe Illustrator CC 17, containing a lot of application-specific metadata/bloat
* - cmyk.pdf
- @jbarlow83
- a CMYK image created in Photoshop
* - crom.png
- @jbarlow83
- test for non-dictionary words
* - enormous.pdf
- @jbarlow83
- very large PDF page
@@ -66,7 +72,7 @@ under the terms of the license in LICENSE.rst.
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
* - formxobject.pdf
- @jbarlow83
- hand-crafted exotic PDF containing an image inside a Form XObject
- hand-crafted PDF containing an image inside a Form XObject
* - francais.pdf
- @jbarlow83
- a page containing French accents (diacritics)
@@ -85,11 +91,24 @@ under the terms of the license in LICENSE.rst.
* - overlay.pdf
- @maxandersen
- PDF file generated by PDFPen pro that triggered content stream parse errors
* - negzero.pdf
- @jbarlow83
- copy of formxobject.pdf with token that qpdf doesn't like
* - no_contents.pdf
- @jbarlow83
- synthetic PDF with a blank page that has no /Contents entry
* - trivial.pdf
- @jbarlow83
- smallest possible valid PDF-1.3 with all required fields
* - vector.pdf
- @Catscratch
- a PDF with vector art and text rendered as curves with no fonts
Assemblies
==========
These test resources are assemblies from other previously mentioned files, released under the same license terms as their input files.
These test resources are assemblies or derivatives from other previously mentioned files, released under the same license terms as their input files.
- cardinal.pdf (four cardinal directions, baked-in rotated copies of LinnSequencer.jpg)
- ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding)
@@ -100,7 +119,10 @@ These test resources are assemblies from other previously mentioned files, relea
- palette.pdf (congress.jpg, converted to a 256-color palette)
- rotated_skew.pdf (a /Rotate'd and skewed document from LinnSequencer.jpg)
- skew.pdf (from LinnSequencer.jpg, skew simulated by adjusting the transformation matrix)
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2)
- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2, password is "password")
- baiona_gray.png (from baiona.png)
- poster.pdf (from LinnSequencer.jpg)
.. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg
@@ -115,4 +137,6 @@ These test resources are assemblies from other previously mentioned files, relea
.. _`Linux (Wikipedia Article)`: https://de.wikipedia.org/wiki/Linux
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
.. _`Wikimedia: Triumph typewrtier text Linzensoep`: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
.. _`Wikimedia: Baionako udalerri mugakideak`: https://commons.wikimedia.org/wiki/File:Baionako_udalerri_mugakideak.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
%PDF-1.3
%¿÷¢þ
1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj
3 0 obj
<< /MediaBox [ 0 0 720 720 ] /Parent 2 0 R /Resources << >> /Type /Page >>
endobj
xref
0 4
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
trailer << /Root 1 0 R /Size 4 /ID [<52bba3c78160d0c6e851b59110e5d076><52bba3c78160d0c6e851b59110e5d076>] >>
startxref
213
%%EOF
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
%PDF-1.3
%¿÷¢þ
1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj
3 0 obj
<< /Contents 4 0 R /MediaBox [ 0 0 300 300 ] /Parent 2 0 R /Resources << >> /Type /Page >>
endobj
4 0 obj
<< /Length 0 >>
stream
endstream
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
0000000229 00000 n
trailer << /Root 1 0 R /Size 5 /ID [<8088c23e0edc07ef4fbed3daa55f52ed><8088c23e0edc07ef4fbed3daa55f52ed>] >>
startxref
277
%%EOF
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.
+3 -1
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
import os
from subprocess import check_call
@@ -20,7 +22,7 @@ not permitted in PDF/A-2, overprint mode not set"""
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
gs_args = ['gs'] + sys.argv[1:]
+3 -1
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
import os
@@ -15,7 +17,7 @@ def real_ghostscript(argv):
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# Unless some argument is calling for PDFA generation, forward to
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
import os
"""Replicate Ghostscript raster failure while allowing rendering"""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# For any rendering calls (device == pdfwrite) call real ghostscript
if '-sDEVICE=pdfwrite' in sys.argv:
real_ghostscript(sys.argv)
return
# Fail
print("ERROR: Ghost story archive not found")
sys.exit(1)
if __name__ == '__main__':
main()
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# © 2016-18 James R. Barlow: github.com/jbarlow83
import sys
import os
"""Replicate Ghostscript render failure while allowing rasterizing"""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# For any rasterize calls (device != pdfwrite) call real ghostscript
if '-sDEVICE=pdfwrite' not in sys.argv:
real_ghostscript(sys.argv)
return
# Fail
print("ERROR: Casper is not a friendly ghost")
sys.exit(1)
if __name__ == '__main__':
main()
+5
View File
@@ -1,8 +1,13 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
def main():
if sys.argv[-1] == '--version':
print('qpdf version 7.0.0')
sys.exit(0)
print('qpdf dummy')
sys.exit(2)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
import sys
import img2pdf
import PyPDF2 as pypdf
from PIL import Image
"""Tesseract bad utf8 spoof
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
text because tesseract seems to do that in some cases related to
language pack version mismatches
"""
VERSION_STRING = '''tesseract 3.05.01
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
SPOOFED
'''
# Japanese "Invalid UTF-8" encoded in Shift JIS
BAD_UTF8 = b'\x96\xb3\x8c\xf8\x82\xc8UTF-8\x0a'
def main():
if sys.argv[1] == '--version':
print(VERSION_STRING, file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--print-parameters':
print("Some parameters", file=sys.stderr)
print("textonly_pdf\t1\tSome help text")
sys.exit(0)
elif sys.argv[-2] in ('hocr', 'pdf'):
sys.stdout.buffer.write(BAD_UTF8)
sys.exit(1)
elif sys.argv[-1] == 'stdout':
# input file is at sys.argv[-2] but we don't look at it
print("""Orientation: 0
Orientation in degrees: 0
Orientation confidence: 100.00
Script: 1
Script confidence: 100.00""", file=sys.stderr)
else:
print("Spoof doesn't understand arguments", file=sys.stderr)
print(sys.argv, file=sys.stderr)
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main()
+11 -6
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
VERSION_STRING = '''tesseract 3.04.00
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
VERSION_STRING = '''tesseract 3.05.01
leptonica-1.74.4
libjpeg 9b : libpng 1.6.32 : libtiff 4.0.8 : zlib 1.2.8
SPOOFED: return error claiming image too big
'''
@@ -18,13 +20,16 @@ def main():
print(VERSION_STRING, file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
print('List of available languages (1):\neng\n', file=sys.stderr)
sys.exit(0)
elif sys.argv[-1] == 'hocr':
elif sys.argv[1] == '--print-parameters':
print('A parameter list would go here\ntextonly_pdf 0\n', file=sys.stderr)
sys.exit(0)
elif sys.argv[-2] == 'hocr':
print("Image too large: (33830, 14959)\n"
"Error during processing.", file=sys.stderr)
sys.exit(1)
elif sys.argv[-1] == 'pdf':
elif sys.argv[-2] == 'pdf':
print("Image too large: (33830, 14959)\n"
"Error during processing.", file=sys.stderr)
sys.exit(1)
+40 -12
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
import os
import hashlib
@@ -9,18 +11,27 @@ import subprocess
"""Cache output of tesseract to speed up test suite
The cache is keyed by a hash that includes the tesseract version, some of
the command line, and the binary dump of the input file. The output file,
stdout, and stderr are replicated on a cache hit.
the command line, and the binary dump of the input file, and this file itself.
Therefore any updates to this file invalidate cache. Uses SHA-1 because it is
fast and defeating a hash collision here is not exactly a priority. :P
The output files, stdout, and stderr are replicated on a cache hit. The output
files are either a .pdf and .txt or .hocr and .txt.
Page orientation checks are also cached (-psm 0 stdout)
Errors and crashes are not cached.
Errors and crashes are not cached. If the arguments don't match a known
caching template then real tesseract is called with the same arguments.
Things not checked:
-changes to tesseract installation that don't affect --version
Assumes Tesseract 3.04 or higher.
Will fail on Tesseract 3.02.02 in "hocr" mode because it doesn't produce
the incorrect file extension.
the incorrect file extension. Will fail on 3.03 because that has no sidecar
text support. Will fail to replicate a 3.04 bug if wrong parameter order is
given.
"""
@@ -35,7 +46,13 @@ def real_tesseract():
return # Not reachable
def main():
operation = sys.argv[-1]
operation = sys.argv[-2]
sidecar = False
if sys.argv[-1] == 'txt':
sidecar = True
elif sys.argv[-1] == 'stdout':
operation = 'stdout'
# For anything unexpected operation, defer to real tesseract binary
# Currently this includes all use of "--tesseract-config"
if operation != 'hocr' and operation != 'pdf' and operation != 'stdout':
@@ -53,11 +70,6 @@ def main():
['tesseract', '--version'],
stderr=subprocess.STDOUT)
if b'4.00.00alpha' in tess_version:
# Tesseract 4.x alpha is a moving target, don't cache it
real_tesseract()
return
m.update(tess_version)
# Insert this source file into the hash function, to ensure that any
@@ -73,6 +85,12 @@ def main():
except ValueError:
m.update(b'default-lang')
try:
textonly = sys.argv[sys.argv.index('-c') + 1]
m.update(textonly.encode())
except ValueError:
m.update(b'textonly_pdf=0')
psm_arg = ''
if '--psm' in sys.argv:
psm_arg = '--psm'
@@ -92,16 +110,22 @@ def main():
return
if operation == 'stdout':
# tesseract [--options] ... input stdout
input_file = sys.argv[-2]
output_file = 'stdout'
sidecar_file = ''
else:
input_file = sys.argv[-3]
output_file = sys.argv[-2]
# tesseract [--options] ... input output txt hocr|pdf
input_file = sys.argv[-4]
output_file = sys.argv[-3]
sidecar_file = sys.argv[-3]
if operation == 'hocr':
output_file += '.hocr'
sidecar_file += '.txt'
elif operation == 'pdf':
output_file += '.pdf'
sidecar_file += '.txt'
with open(input_file, 'rb') as f:
m.update(f.read())
@@ -112,6 +136,8 @@ def main():
print("Tesseract cache hit", file=sys.stderr)
if operation != 'stdout':
shutil.copy(cache_name, output_file)
if sidecar:
shutil.copy(cache_name + '.sidecar', sidecar_file)
# Replicate output
with open(cache_name + '.stdout', 'rb') as f:
@@ -149,6 +175,8 @@ def main():
shutil.copy(output_file, cache_name)
else:
print("Could not find output file", file=sys.stderr)
if sidecar and os.path.exists(sidecar_file):
shutil.copy(sidecar_file, cache_name + '.sidecar')
else:
open(cache_name, 'w').close()
+10 -5
View File
@@ -1,12 +1,13 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
import os
import signal
VERSION_STRING = '''tesseract 3.04.00
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
VERSION_STRING = '''tesseract 3.05.01
leptonica-1.74.4
libjpeg 9b : libpng 1.6.32 : libtiff 4.0.8 : zlib 1.2.8
SPOOFED: CRASH ON OCR or -psm 0
'''
@@ -28,10 +29,14 @@ def main():
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
sys.exit(0)
elif sys.argv[-1] == 'hocr':
elif sys.argv[1] == '--print-parameters':
print('A parameter list would go here\ntextonly_pdf 0\n',
file=sys.stderr)
sys.exit(0)
elif sys.argv[-2] == 'hocr':
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
sys.exit(128 + signal.SIGSEGV)
elif sys.argv[-1] == 'pdf':
elif sys.argv[-2] == 'pdf':
print("KABOOM! Tesseract failed for some reason", file=sys.stderr)
sys.exit(128 + signal.SIGSEGV)
elif sys.argv[-1] == 'stdout':
+35 -10
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
import img2pdf
import PyPDF2 as pypdf
from PIL import Image
@@ -16,7 +18,7 @@ In orientation check mode, report the orientation is upright.
"""
VERSION_STRING = '''tesseract 3.04.00
VERSION_STRING = '''tesseract 3.05.01
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
SPOOFED
@@ -52,19 +54,42 @@ def main():
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
sys.exit(0)
elif sys.argv[-1] == 'hocr':
inputf = sys.argv[-3]
output = sys.argv[-2]
elif sys.argv[1] == '--print-parameters':
print("Some parameters", file=sys.stderr)
print("textonly_pdf\t1\tSome help text")
sys.exit(0)
elif sys.argv[-2] == 'hocr':
inputf = sys.argv[-4]
output = sys.argv[-3]
with Image.open(inputf) as im, \
open(output + '.hocr', 'w', encoding='utf-8') as f:
w, h = im.size
f.write(HOCR_TEMPLATE.format(str(w), str(h)))
elif sys.argv[-1] == 'pdf':
inputf = sys.argv[-3]
output = sys.argv[-2]
pdf_bytes = img2pdf.convert([inputf], dpi=300)
with open(output + '.pdf', 'wb') as f:
f.write(pdf_bytes)
with open(output + '.txt', 'w') as f:
f.write('')
elif sys.argv[-2] == 'pdf':
if 'textonly_pdf=1' in sys.argv:
inputf = sys.argv[-4]
output = sys.argv[-3]
with Image.open(inputf) as im:
dpi = im.info['dpi']
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
ptsize = pagesize[0] * 72, pagesize[1] * 72
pdf_out = pypdf.PdfFileWriter()
pdf_out.addBlankPage(ptsize[0], ptsize[1])
with open(output + '.pdf', 'wb') as f:
pdf_out.write(f)
with open(output + '.txt', 'w') as f:
f.write('')
else:
inputf = sys.argv[-4]
output = sys.argv[-3]
pdf_bytes = img2pdf.convert([inputf], dpi=300)
with open(output + '.pdf', 'wb') as f:
f.write(pdf_bytes)
with open(output + '.txt', 'w') as f:
f.write('')
elif sys.argv[-1] == 'stdout':
inputf = sys.argv[-2]
print("""Orientation: 0
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
import sys
def main():
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
from ocrmypdf import hocrtransform
+418 -123
View File
@@ -1,34 +1,34 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2015-17 James R. Barlow: github.com/jbarlow83
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
from pathlib import Path
import os
import shutil
from contextlib import suppress
import sys
import resource
import pytest
from ocrmypdf.pageinfo import pdf_get_all_pageinfo
from ocrmypdf.pdfinfo import PdfInfo, Colorspace, Encoding
import PyPDF2 as pypdf
from ocrmypdf.exceptions import ExitCode
from ocrmypdf import leptonica
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.exec import ghostscript
from ocrmypdf.exec import ghostscript, tesseract, qpdf
import logging
from math import isclose
import PIL
# pytest.helpers is dynamic
# pylint: disable=no-member
# pylint: disable=w0612
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof
@pytest.fixture
def spoof_tesseract_noop():
return spoof(tesseract='tesseract_noop.py')
@pytest.fixture
def spoof_tesseract_cache():
if pytest.helpers.running_in_docker():
return os.environ.copy()
return spoof(tesseract="tesseract_cache.py")
RENDERERS = ['hocr', 'tesseract']
if tesseract.has_textonly_pdf():
RENDERERS.append('sandwich')
@pytest.fixture
@@ -51,6 +51,21 @@ def spoof_no_tess_pdfa_warning():
return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py')
@pytest.fixture
def spoof_no_tess_gs_render_fail():
return spoof(tesseract='tesseract_noop.py', gs='gs_render_failure.py')
@pytest.fixture
def spoof_no_tess_gs_raster_fail():
return spoof(tesseract='tesseract_noop.py', gs='gs_raster_failure.py')
@pytest.fixture
def spoof_tess_bad_utf8():
return spoof(tesseract='tesseract_badutf8.py')
@pytest.fixture
def spoof_qpdf_always_error():
return spoof(qpdf='qpdf_dummy_return2.py')
@@ -63,24 +78,23 @@ def test_quick(spoof_tesseract_cache, resources, outpdf):
def test_deskew(spoof_tesseract_noop, resources, outdir):
# Run with deskew
deskewed_pdf = check_ocrmypdf(
resources / 'skew.pdf', outdir / 'skew.pdf', '-d', '-v', '1',
resources / 'skew.pdf', outdir / 'skew.pdf', '-d',
env=spoof_tesseract_noop)
# Now render as an image again and use Leptonica to find the skew angle
# to confirm that it was deskewed
from ocrmypdf.exec.ghostscript import rasterize_pdf
import logging
log = logging.getLogger()
deskewed_png = outdir / 'deskewed.png'
rasterize_pdf(
str(deskewed_pdf),
str(deskewed_png),
ghostscript.rasterize_pdf(
deskewed_pdf,
deskewed_png,
xres=150,
yres=150,
raster_device='pngmono',
log=log)
log=log,
pageno=1)
from ocrmypdf.leptonica import Pix
pix = Pix.read(str(deskewed_png))
@@ -109,20 +123,18 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
'--image-dpi', '150',
env=spoof_tesseract_noop)
from ocrmypdf.exec.ghostscript import rasterize_pdf
import logging
log = logging.getLogger()
output_png = outdir / 'remove_bg.png'
rasterize_pdf(
str(output_pdf),
str(output_png),
ghostscript.rasterize_pdf(
output_pdf,
output_png,
xres=100,
yres=100,
raster_device='png16m',
log=log)
log=log,
pageno=1)
# The output image should contain pure white and black
im = Image.open(output_png)
@@ -133,18 +145,22 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
@pytest.mark.parametrize(
"pdf",
['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf'])
@pytest.mark.parametrize("renderer", ['hocr', 'tesseract'])
@pytest.mark.parametrize("renderer", ['auto', 'tesseract'])
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
def test_exotic_image(spoof_tesseract_cache, pdf, renderer, output_type,
resources, outdir):
outfile = outdir / 'test_{0}_{1}.pdf'.format(pdf, renderer)
check_ocrmypdf(
resources / pdf,
outdir / 'test_{0}_{1}.pdf'.format(pdf, renderer),
outfile,
'-dc',
'-v', '1',
'--output-type', output_type,
'--sidecar',
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
assert outfile.with_suffix('.pdf.txt').exists()
@pytest.mark.parametrize("output_type", [
'pdfa', 'pdf'
@@ -211,20 +227,17 @@ def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf):
assert p.returncode == ExitCode.bad_args, err
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
oversampled_pdf = check_ocrmypdf(
resources / 'skew.pdf', outpdf, '--oversample', '350',
'-f',
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
pdfinfo = pdf_get_all_pageinfo(str(oversampled_pdf))
pdfinfo = PdfInfo(oversampled_pdf)
print(pdfinfo[0]['xres'])
assert abs(pdfinfo[0]['xres'] - 350) < 1
print(pdfinfo[0].xres)
assert abs(pdfinfo[0].xres - 350) < 1
def test_repeat_ocr(resources, no_outpdf):
@@ -235,8 +248,8 @@ def test_repeat_ocr(resources, no_outpdf):
def test_force_ocr(spoof_tesseract_cache, resources, outpdf):
out = check_ocrmypdf(resources / 'graph_ocred.pdf', outpdf, '-f',
env=spoof_tesseract_cache)
pdfinfo = pdf_get_all_pageinfo(out)
assert pdfinfo[0]['has_text']
pdfinfo = PdfInfo(out)
assert pdfinfo[0].has_text
def test_skip_ocr(spoof_tesseract_cache, resources, outpdf):
@@ -257,10 +270,6 @@ def test_argsfile(spoof_tesseract_noop, resources, outdir):
def check_monochrome_correlation(
outdir,
reference_pdf, reference_pageno, test_pdf, test_pageno):
import ocrmypdf.exec.ghostscript as ghostscript
import logging
gslog = logging.getLogger()
reference_png = outdir / '{}.ref{:04d}.png'.format(
@@ -273,9 +282,7 @@ def check_monochrome_correlation(
print(png)
return
ghostscript.rasterize_pdf(
str(pdf),
str(png),
xres=100, yres=100,
pdf, png, xres=100, yres=100,
raster_device='pngmono', log=gslog, pageno=pageno)
rasterize(reference_pdf, reference_pageno, reference_png)
@@ -308,10 +315,7 @@ def test_monochrome_correlation(resources, outdir):
assert corr > 0.90
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_autorotate(spoof_tesseract_cache, renderer, resources, outdir):
# cardinal.pdf contains four copies of an image rotated in each cardinal
# direction - these ones are "burned in" not tagged with /Rotate
@@ -346,25 +350,22 @@ def test_autorotate_threshold(
assert eval(correlation_test)
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer',RENDERERS)
def test_ocr_timeout(renderer, resources, outpdf):
out = check_ocrmypdf(resources / 'skew.pdf', outpdf,
'--tesseract-timeout', '1.0')
pdfinfo = pdf_get_all_pageinfo(str(out))
assert not pdfinfo[0]['has_text']
pdfinfo = PdfInfo(out)
assert not pdfinfo[0].has_text
def test_skip_big(spoof_tesseract_cache, resources, outpdf):
out = check_ocrmypdf(resources / 'enormous.pdf', outpdf,
'--skip-big', '10', env=spoof_tesseract_cache)
pdfinfo = pdf_get_all_pageinfo(str(out))
assert not pdfinfo[0]['has_text']
pdfinfo = PdfInfo(out)
assert not pdfinfo[0].has_text
@pytest.mark.parametrize('renderer', ['hocr', 'tesseract'])
@pytest.mark.parametrize('renderer', RENDERERS)
@pytest.mark.parametrize('output_type', ['pdf', 'pdfa'])
def test_maximum_options(spoof_tesseract_cache, renderer, output_type,
resources, outpdf):
@@ -415,9 +416,13 @@ def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash, resources,
@pytest.mark.skipif(
pytest.helpers.is_macos() and pytest.helpers.running_in_travis(),
reason="takes too long to install language packs in Travis macOS homebrew")
def test_french(spoof_tesseract_cache, resources, outpdf):
def test_french(spoof_tesseract_cache, resources, outdir):
# Produce a sidecar too - implicit test that system locale is set up
# properly
sidecar = outdir / 'francais.txt'
p, out, err = run_ocrmypdf(
resources / 'francais.pdf', outpdf, '-l', 'fra',
resources / 'francais.pdf', outdir / 'francais.pdf', '-l', 'fra',
'--sidecar', sidecar,
env=spoof_tesseract_cache)
print(os.environ)
assert p.returncode == ExitCode.ok, \
@@ -427,12 +432,12 @@ def test_french(spoof_tesseract_cache, resources, outpdf):
def test_klingon(resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'francais.pdf', outpdf, '-l', 'klz')
assert p.returncode == ExitCode.bad_args
assert p.returncode == ExitCode.missing_dependency
def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'missing_docinfo.pdf', outpdf, '-l', 'eng', '-c',
resources / 'missing_docinfo.pdf', outpdf, '-l', 'eng', '--skip-text',
env=spoof_tesseract_noop)
assert p.returncode == ExitCode.ok, err
@@ -479,7 +484,7 @@ def test_encrypted(resources, no_outpdf):
p, out, err = run_ocrmypdf(
resources / 'skew-encrypted.pdf', no_outpdf)
assert p.returncode == ExitCode.encrypted_pdf
assert out.find('password')
assert out.find('encrypted')
@pytest.mark.parametrize('renderer', [
@@ -494,10 +499,7 @@ def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf):
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_tesseract_crash(renderer, spoof_tesseract_crash,
resources, no_outpdf):
p, out, err = run_ocrmypdf(
@@ -520,15 +522,14 @@ def test_tesseract_crash_autorotate(spoof_tesseract_crash,
print(err)
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error,
resources, outpdf):
check_ocrmypdf(
resources / 'hugemono.pdf', outpdf, '-r',
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
'--pdf-renderer', renderer,
'--max-image-mpixels', '0',
env=spoof_tesseract_big_image_error)
def test_no_unpaper(resources, no_outpdf):
@@ -557,32 +558,28 @@ def test_algo4(resources, no_outpdf):
def test_non_square_resolution(renderer, spoof_tesseract_cache,
resources, outpdf):
# Confirm input image is non-square resolution
in_pageinfo = pdf_get_all_pageinfo(str(resources / 'aspect.pdf'))
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
assert in_pageinfo[0].xres != in_pageinfo[0].yres
check_ocrmypdf(
resources / 'aspect.pdf', outpdf,
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(str(outpdf))
out_pageinfo = PdfInfo(outpdf)
# Confirm resolution was kept the same
assert in_pageinfo[0]['xres'] == out_pageinfo[0]['xres']
assert in_pageinfo[0]['yres'] == out_pageinfo[0]['yres']
assert in_pageinfo[0].xres == out_pageinfo[0].xres
assert in_pageinfo[0].yres == out_pageinfo[0].yres
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract'
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_convert_to_square_resolution(renderer, spoof_tesseract_cache,
resources, outpdf):
from math import isclose
# Confirm input image is non-square resolution
in_pageinfo = pdf_get_all_pageinfo(str(resources / 'aspect.pdf'))
assert in_pageinfo[0]['xres'] != in_pageinfo[0]['yres']
in_pageinfo = PdfInfo(resources / 'aspect.pdf')
assert in_pageinfo[0].xres != in_pageinfo[0].yres
# --force-ocr requires means forced conversion to square resolution
check_ocrmypdf(
@@ -590,25 +587,25 @@ def test_convert_to_square_resolution(renderer, spoof_tesseract_cache,
'--force-ocr',
'--pdf-renderer', renderer, env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(str(outpdf))
out_pageinfo = PdfInfo(outpdf)
in_p0, out_p0 = in_pageinfo[0], out_pageinfo[0]
# Resolution show now be equal
assert out_p0['xres'] == out_p0['yres']
assert out_p0.xres == out_p0.yres
# Page size should match input page size
assert isclose(in_p0['width_inches'],
out_p0['width_inches'])
assert isclose(in_p0['height_inches'],
out_p0['height_inches'])
assert isclose(in_p0.width_inches,
out_p0.width_inches)
assert isclose(in_p0.height_inches,
out_p0.height_inches)
# Because we rasterized the page to produce a new image, it should occupy
# the entire page
out_im_w = out_p0['images'][0]['width'] / out_p0['images'][0]['dpi_w']
out_im_h = out_p0['images'][0]['height'] / out_p0['images'][0]['dpi_h']
assert isclose(out_p0['width_inches'], out_im_w)
assert isclose(out_p0['height_inches'], out_im_h)
out_im_w = out_p0.images[0]['width'] / out_p0.images[0]['dpi_w']
out_im_h = out_p0.images[0]['height'] / out_p0.images[0]['dpi_h']
assert isclose(out_p0.width_inches, out_im_w)
assert isclose(out_p0.height_inches, out_im_h)
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
@@ -624,8 +621,8 @@ def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf):
'--pdf-renderer', 'hocr',
env=spoof_tesseract_cache)
out_pageinfo = pdf_get_all_pageinfo(str(out))
assert out_pageinfo[0]['images'][0]['enc'] == 'jbig2'
out_pageinfo = PdfInfo(out)
assert out_pageinfo[0].images[0].enc == Encoding.jbig2
def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
@@ -698,27 +695,27 @@ def test_rotated_skew_timeout(resources, outpdf):
"""
input_file = str(resources / 'rotated_skew.pdf')
in_pageinfo = pdf_get_all_pageinfo(input_file)[0]
in_pageinfo = PdfInfo(input_file)[0]
assert in_pageinfo['height_pixels'] < in_pageinfo['width_pixels'], \
assert in_pageinfo.height_pixels < in_pageinfo.width_pixels, \
"Expected the input page to be landscape"
assert in_pageinfo['rotate'] == 90, "Expected a rotated page"
assert in_pageinfo.rotation == 90, "Expected a rotated page"
out = check_ocrmypdf(
input_file, outpdf,
'--pdf-renderer', 'hocr',
'--deskew', '--tesseract-timeout', '0')
out_pageinfo = pdf_get_all_pageinfo(str(out))[0]
out_pageinfo = PdfInfo(out)[0]
assert out_pageinfo['height_pixels'] > out_pageinfo['width_pixels'], \
assert out_pageinfo.height_pixels > out_pageinfo.width_pixels, \
"Expected the output page to be portrait"
assert out_pageinfo['rotate'] == 0, \
assert out_pageinfo.rotation == 0, \
"Expected no page rotation for output"
assert in_pageinfo['width_pixels'] == out_pageinfo['height_pixels'] and \
in_pageinfo['height_pixels'] == out_pageinfo['width_pixels'], \
assert in_pageinfo.width_pixels == out_pageinfo.height_pixels and \
in_pageinfo.height_pixels == out_pageinfo.width_pixels, \
"Expected page rotation to be baked in"
@@ -739,6 +736,11 @@ def test_very_high_dpi(spoof_tesseract_cache, resources, outpdf):
"Checks for a Decimal quantize error with high DPI, etc"
check_ocrmypdf(resources / '2400dpi.pdf', outpdf,
env=spoof_tesseract_cache)
pdfinfo = PdfInfo(outpdf)
image = pdfinfo[0].images[0]
assert isclose(image.xres, image.yres)
assert isclose(image.xres, 2400)
def test_overlay(spoof_tesseract_noop, resources, outpdf):
@@ -771,28 +773,22 @@ language_model_penalty_non_freq_dict_word 0
check_ocrmypdf(
resources / 'ccitt.pdf', outdir / 'out.pdf',
'--tesseract-config', str(cfg_file))
'--tesseract-config', cfg_file)
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_tesseract_config_notfound(renderer, resources, outdir):
cfg_file = outdir / 'nofile.cfg'
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outdir / 'out.pdf',
'--pdf-renderer', renderer,
'--tesseract-config', str(cfg_file))
'--tesseract-config', cfg_file)
assert "Can't open" in err, "No error message about missing config file"
assert p.returncode == ExitCode.ok
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_tesseract_config_invalid(renderer, resources, outdir):
cfg_file = outdir / 'test.cfg'
with cfg_file.open('w') as f:
@@ -803,22 +799,49 @@ THIS FILE IS INVALID
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outdir / 'out.pdf',
'--pdf-renderer', renderer,
'--tesseract-config', str(cfg_file))
'--tesseract-config', cfg_file)
assert "parameter not found" in err, "No error message"
assert p.returncode == ExitCode.invalid_config
def test_user_words(resources, outdir):
word_list = outdir / 'wordlist.txt'
sidecar_before = outdir / 'sidecar_before.txt'
sidecar_after = outdir / 'sidecar_after.txt'
# Don't know how to make this test pass on various versions and platforms
# so weaken to merely testing that the argument is accepted
consistent = False
if consistent:
check_ocrmypdf(
resources / 'crom.png', outdir / 'out.pdf',
'--image-dpi', 150,
'--sidecar', sidecar_before
)
assert 'cromulent' not in sidecar_before.open().read()
with word_list.open('w') as f:
f.write('cromulent\n') # a perfectly cromulent word
check_ocrmypdf(
resources / 'crom.png', outdir / 'out.pdf',
'--image-dpi', 150,
'--sidecar', sidecar_after,
'--user-words', word_list
)
if consistent:
assert 'cromulent' in sidecar_after.open().read()
def test_form_xobject(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'formxobject.pdf', outpdf,
'--force-ocr',
env=spoof_tesseract_noop)
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
])
@pytest.mark.parametrize('renderer', RENDERERS)
def test_pagesize_consistency(renderer, resources, outpdf):
from math import isclose
@@ -843,4 +866,276 @@ def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'blank.pdf', outpdf,
'--skip-big', '5',
'--force-ocr',
env=spoof_tesseract_noop)
env=spoof_tesseract_noop)
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'blank.pdf', outpdf,
env=spoof_no_tess_gs_render_fail)
print(err)
assert p.returncode == ExitCode.child_process_error
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outpdf,
env=spoof_no_tess_gs_raster_fail)
print(err)
assert p.returncode == ExitCode.child_process_error
@pytest.mark.skipif('8.0.0' <= qpdf.version() <= '8.0.1',
reason="qpdf regression")
def test_no_contents(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'no_contents.pdf', outpdf, '--force-ocr',
env=spoof_tesseract_noop)
@pytest.mark.parametrize('image', [
'baiona.png',
'baiona_gray.png',
'congress.jpg'
])
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
resources, image, outpdf):
from PIL import Image
input_file = str(resources / image)
output_file = str(outpdf)
im = Image.open(input_file)
# Runs: ocrmypdf - output.pdf < testfile
with open(input_file, 'rb') as input_stream:
p_args = ocrmypdf_exec + [
'--image-dpi', '150', '--output-type', 'pdf', '-', output_file]
p = Popen(
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
stdin=input_stream, env=spoof_tesseract_noop)
out, err = p.communicate()
assert p.returncode == ExitCode.ok
pdfinfo = PdfInfo(output_file)
pdfimage = pdfinfo[0].images[0]
if input_file.endswith('.png'):
assert pdfimage.enc != Encoding.jpeg, \
"Lossless compression changed to lossy!"
elif input_file.endswith('.jpg'):
assert pdfimage.enc == Encoding.jpeg, \
"Lossy compression changed to lossless!"
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
assert pdfimage.color == Colorspace.rgb, \
"Colorspace changed"
elif im.mode.startswith('L'):
assert pdfimage.color == Colorspace.gray, \
"Colorspace changed"
@pytest.mark.parametrize('image,compression', [
('baiona.png', 'jpeg'),
('baiona_gray.png', 'lossless'),
('congress.jpg', 'lossless')
])
def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
resources, image, compression, outpdf):
from PIL import Image
input_file = str(resources / image)
output_file = str(outpdf)
im = Image.open(input_file)
# Runs: ocrmypdf - output.pdf < testfile
with open(input_file, 'rb') as input_stream:
p_args = ocrmypdf_exec + [
'--image-dpi', '150', '--output-type', 'pdfa',
'--pdfa-image-compression', compression,
'-', output_file]
p = Popen(
p_args, close_fds=True, stdout=PIPE, stderr=PIPE,
stdin=input_stream, env=spoof_tesseract_noop)
out, err = p.communicate()
assert p.returncode == ExitCode.ok
pdfinfo = PdfInfo(output_file)
pdfimage = pdfinfo[0].images[0]
if compression == "jpeg":
assert pdfimage.enc == Encoding.jpeg
elif compression == 'lossless':
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
assert pdfimage.color == Colorspace.rgb, \
"Colorspace changed"
elif im.mode.startswith('L'):
assert pdfimage.color == Colorspace.gray, \
"Colorspace changed"
def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
sidecar = outpdf + '.txt'
check_ocrmypdf(
resources / 'multipage.pdf', outpdf,
'--skip-text',
'--sidecar', sidecar,
env=spoof_tesseract_cache)
pdfinfo = PdfInfo(resources / 'multipage.pdf')
num_pages = len(pdfinfo)
with open(sidecar, 'r') as f:
ocr_text = f.read()
# There should a formfeed between each pair of pages, so the count of
# formfeeds is the page count less one
assert ocr_text.count('\f') == num_pages - 1, \
"Sidecar page count does not match PDF page count"
def test_sidecar_nonempty(spoof_tesseract_cache, resources, outpdf):
sidecar = outpdf + '.txt'
check_ocrmypdf(
resources / 'ccitt.pdf', outpdf,
'--sidecar', sidecar,
env=spoof_tesseract_cache
)
with open(sidecar, 'r') as f:
ocr_text = f.read()
assert 'the' in ocr_text
def test_pdfa_1(spoof_tesseract_cache, resources, outpdf):
check_ocrmypdf(
resources / 'ccitt.pdf', outpdf,
'--output-type', 'pdfa-1',
env=spoof_tesseract_cache
)
pdfa_info = file_claims_pdfa(outpdf)
assert pdfa_info['conformance'] == 'PDF/A-1B'
def test_bad_locale():
env = os.environ.copy()
env['LC_ALL'] = 'C'
p, out, err = run_ocrmypdf(
'a', 'b', env=env
)
assert out == '', "stdout not clean"
assert p.returncode != 0
assert 'configured to use ASCII as encoding' in err, "should whine"
@pytest.mark.parametrize('renderer', RENDERERS)
def test_bad_utf8(spoof_tess_bad_utf8, renderer, resources, no_outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', no_outpdf,
'--pdf-renderer', renderer,
env=spoof_tess_bad_utf8
)
assert out == '', "stdout not clean"
assert p.returncode != 0
assert 'not utf-8' in err, "should whine about utf-8"
assert '\\x96' in err, 'should repeat backslash encoded output'
@pytest.mark.skipif(
not tesseract.has_textonly_pdf(),
reason="issue only affects sandwich")
def test_rotate_deskew_timeout(resources, outdir):
check_ocrmypdf(
resources / 'rotated_skew.pdf',
outdir / 'deskewed.pdf',
'--deskew',
'--tesseract-timeout', '0',
'--pdf-renderer', 'sandwich'
)
correlation = check_monochrome_correlation(
outdir,
reference_pdf=resources / 'ccitt.pdf',
reference_pageno=1,
test_pdf=outdir / 'deskewed.pdf',
test_pageno=1)
# Confirm that the page still got deskewed
assert correlation > 0.50
@pytest.mark.skipif(
PIL.PILLOW_VERSION < '5.0.0',
reason="Pillow < 5.0.0 doesn't raise the exception")
def test_decompression_bomb(resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'hugemono.pdf',
outpdf
)
assert 'decompression bomb' in err
p, out, err = run_ocrmypdf(
resources / 'hugemono.pdf',
outpdf,
'--max-image-mpixels', '2000'
)
assert p.returncode == 0
def test_text_curves(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(
resources / 'vector.pdf', outpdf, env=spoof_tesseract_noop)
info = PdfInfo(outpdf)
assert len(info.pages[0].images) == 0, "added images to the vector PDF"
check_ocrmypdf(
resources / 'vector.pdf', outpdf, '--force-ocr',
env=spoof_tesseract_noop)
info = PdfInfo(outpdf)
assert len(info.pages[0].images) != 0, "force did not rasterize"
def test_dev_null(spoof_tesseract_noop, resources):
p, out, err = run_ocrmypdf(
resources / 'trivial.pdf',
os.devnull,
'--force-ocr',
env=spoof_tesseract_noop
)
assert p.returncode == 0, "could not send output to /dev/null"
assert len(out) == 0, "wrote to stdout"
def test_output_is_dir(spoof_tesseract_noop, resources, outdir):
p, out, err = run_ocrmypdf(
resources / 'trivial.pdf',
outdir,
'--force-ocr',
env=spoof_tesseract_noop
)
assert p.returncode == ExitCode.file_access_error
assert 'is not a writable file' in err
def test_output_is_symlink(spoof_tesseract_noop, resources, outdir):
sym = Path(outdir / 'this_is_a_symlink')
sym.symlink_to(outdir / 'out.pdf')
p, out, err = run_ocrmypdf(
resources / 'trivial.pdf',
sym,
'--force-ocr',
env=spoof_tesseract_noop
)
assert p.returncode == ExitCode.ok, err
assert (outdir / 'out.pdf').stat().st_size > 0, 'target file not created'
+47
View File
@@ -0,0 +1,47 @@
from ocrmypdf.pdfinfo import PdfInfo, PageInfo
from ocrmypdf.pipeline import JobContext, JobContextManager
from multiprocessing import Process
from multiprocessing.managers import BaseProxy
def test_jobcontext_proxy(resources):
# Prove that managers are set up correctly to share state among processes
manager = JobContextManager()
manager.register('JobContext', JobContext)
# Start the manager in a child process (or maybe thread)
manager.start()
# Tell the manager process to retrieve pdf info
context = manager.JobContext()
context.generate_pdfinfo(resources / 'graph.pdf')
# Get a copy of that information for this process
pdfinfo = context.get_pdfinfo()
assert len(pdfinfo) == 1
assert pdfinfo[0].rotation == 0
# Update information and send back to manager
pdfinfo[0].rotation = 90
context.set_pdfinfo(pdfinfo)
# Retrieve again, ensure it stayed changed
pdfinfo2 = context.get_pdfinfo()
assert pdfinfo2[0].rotation == 90
# Start a new process which gets its own proxy object
def client(context):
assert isinstance(context, BaseProxy)
pdfinfo = context.get_pdfinfo()
page = pdfinfo[0]
assert page.rotation == 90
page.rotation += 90
context.set_pdfinfo(pdfinfo)
p = Process(target=client, args=(context,))
p.start()
p.join()
assert p.exitcode == 0, "Child process failed"
assert context.get_pdfinfo()[0].rotation == 180
+45 -31
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
from ocrmypdf import pageinfo
from ocrmypdf import pdfinfo
from reportlab.pdfgen.canvas import Canvas
from PIL import Image
from tempfile import NamedTemporaryFile
from math import isclose
from ocrmypdf.pdfinfo import Colorspace, Encoding
from contextlib import suppress
import os
import shutil
@@ -26,16 +27,15 @@ def test_single_page_text(outdir):
pdf.showPage()
pdf.save()
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
info = pdfinfo.PdfInfo(filename)
assert len(pdfinfo) == 1
page = pdfinfo[0]
assert len(info) == 1
page = info[0]
assert page['has_text']
assert len(page['images']) == 0
assert page.has_text
assert len(page.images) == 0
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs Path.read_bytes")
def test_single_page_image(outdir):
filename = outdir / 'image-mono.pdf'
@@ -54,21 +54,21 @@ def test_single_page_image(outdir):
layout_fun=layout_fun)
filename.write_bytes(pdf_bytes)
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
info = pdfinfo.PdfInfo(filename)
assert len(pdfinfo) == 1
page = pdfinfo[0]
assert len(info) == 1
page = info[0]
assert not page['has_text']
assert len(page['images']) == 1
assert not page.has_text
assert len(page.images) == 1
pdfimage = page['images'][0]
assert pdfimage['width'] == 8
assert pdfimage['color'] == 'gray'
pdfimage = page.images[0]
assert pdfimage.width == 8
assert pdfimage.color == Colorspace.gray
# DPI in a 1"x1" is the image width
assert abs(pdfimage['dpi_w'] - 8) < 1e-5
assert abs(pdfimage['dpi_h'] - 8) < 1e-5
assert isclose(pdfimage.xres, 8)
assert isclose(pdfimage.yres, 8)
def test_single_page_inline_image(outdir):
@@ -84,27 +84,41 @@ def test_single_page_inline_image(outdir):
pdf.showPage()
pdf.save()
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
print(pdfinfo)
pdfimage = pdfinfo[0]['images'][0]
assert (pdfimage['dpi_w'] - 8) < 1e-5
assert pdfimage['color'] != '-'
assert pdfimage['width'] == 8
pdf = pdfinfo.PdfInfo(filename)
print(pdf)
pdfimage = pdf[0].images[0]
assert isclose(pdfimage.xres, 8)
assert pdfimage.color == Colorspace.rgb # reportlab produces color image
assert pdfimage.width == 8
def test_jpeg(resources, outdir):
filename = resources / 'c02-22.pdf'
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
pdf = pdfinfo.PdfInfo(filename)
pdfimage = pdfinfo[0]['images'][0]
assert pdfimage['enc'] == 'jpeg'
assert (pdfimage['dpi_w'] - 150) < 1e-5
pdfimage = pdf[0].images[0]
assert pdfimage.enc == Encoding.jpeg
assert isclose(pdfimage.xres, 150)
def test_form_xobject(resources):
filename = resources / 'formxobject.pdf'
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
pdfimage = pdfinfo[0]['images'][0]
assert pdfimage['width'] == 50
pdf = pdfinfo.PdfInfo(filename)
pdfimage = pdf[0].images[0]
assert pdfimage.width == 50
def test_no_contents(resources):
filename = resources / 'no_contents.pdf'
pdf = pdfinfo.PdfInfo(filename)
assert len(pdf[0].images) == 0
assert pdf[0].has_text == False
def test_oversized_page(resources):
pdf = pdfinfo.PdfInfo(resources / 'poster.pdf')
image = pdf[0].images[0]
assert image.width * image.xres > 200, "this is supposed to be oversized"
+55
View File
@@ -0,0 +1,55 @@
# © 2017 James R. Barlow: github.com/jbarlow83
import logging
import resource
import pytest
from ocrmypdf.exec import ghostscript, tesseract, qpdf
from ocrmypdf.pdfinfo import PdfInfo
@pytest.mark.skipif(
qpdf.version() < '7.0.0',
reason="negzero.pdf crashes earlier versions")
def test_qpdf_negative_zero(resources, outpdf):
negzero = resources / 'negzero.pdf'
hugemono = resources / 'hugemono.pdf'
# raises exception on err
qpdf.merge([str(negzero), str(hugemono)], outpdf, log=logging.getLogger())
@pytest.mark.timeout(15)
@pytest.mark.parametrize('max_files,skip', [
(2, 0), # Can we merge correctly without opening more than 2 files at once?
(16, 0), # And does this work properly when we can one-shot it?
(2, 1), # Or playing with even/odd
(3, 0) # Or odd step size
])
def test_qpdf_merge_correctness(resources, outpdf, max_files, skip):
# All of these must be only one page long
inputs = [
'2400dpi.pdf', 'aspect.pdf', 'blank.pdf', 'ccitt.pdf',
'linn.pdf', 'masks.pdf', 'poster.pdf', 'overlay.pdf',
'skew.pdf', 'trivial.pdf']
input_files = [str(resources / f) for f in inputs]
qpdf.merge(
input_files[skip:], outpdf, log=logging.getLogger(),
max_files=max_files)
assert len(PdfInfo(outpdf).pages) == len(input_files[skip:])
@pytest.mark.timeout(15)
@pytest.mark.skipif(
True,
reason='qpdf binary cannot open multiple files multiple times')
def test_page_merge_ulimit(resources, outpdf):
# Ensure we can merge pages without opening one file descriptor per page
ulimits = resource.getrlimit(resource.RLIMIT_NOFILE)
page_count = ulimits[0]
print(page_count)
input_files = [str(resources / 'trivial.pdf')] * page_count
qpdf.merge(input_files, outpdf, log=logging.getLogger())
+14 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
import pytest
@@ -11,14 +10,26 @@ pytestmark = pytest.mark.skipif(tesseract.v4(),
reason="tesseract 3.x required")
def test_textonly_pdf_on_tess3(resources, no_outpdf):
@pytest.mark.skipif(tesseract.has_textonly_pdf(),
reason="check that missing dep is reported on old tess3")
def test_textonly_pdf_on_older_tess3(resources, no_outpdf):
p, _, _ = pytest.helpers.run_ocrmypdf(
resources / 'linn.pdf',
no_outpdf, '--pdf-renderer', 'tess4')
no_outpdf, '--pdf-renderer', 'sandwich')
assert p.returncode == ExitCode.missing_dependency
@pytest.mark.skipif(not tesseract.has_textonly_pdf(),
reason="check that feature is exercised on new test3")
def test_textonly_pdf_on_newer_tess3(resources, no_outpdf):
p, _, _ = pytest.helpers.run_ocrmypdf(
resources / 'linn.pdf',
no_outpdf, '--pdf-renderer', 'sandwich')
assert p.returncode == ExitCode.ok
def test_oem_on_tess3(resources, no_outpdf):
p, _, err = pytest.helpers.run_ocrmypdf(
resources / 'aspect.pdf',
+41 -24
View File
@@ -1,29 +1,43 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
import pytest
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec import tesseract
from ocrmypdf import pageinfo
from ocrmypdf import pdfinfo
import sys
import os
import PyPDF2 as pypdf
from contextlib import contextmanager
# pylint: disable=no-member
spoof = pytest.helpers.spoof
def tess4_possible_location():
"""The location of tesseract 4 may be OCRMYPDF_TESS4, OCRMYPDF_TESSERACT,
or the installed version on PATH."""
return os.environ.get('OCRMYPDF_TESS4') or \
os.environ.get('OCRMYPDF_TESSERACT') or \
'tesseract'
@pytest.fixture
def ensure_tess4():
return spoof(tesseract=tess4_possible_location())
if tesseract.v4():
# Either "tesseract" on $PATH is already v4, or
# OCRMYPDF_TESSERACT is tess4 already
return os.environ.copy()
if os.environ.get('OCRMYPDF_TESS4'):
# OCRMYPDF_TESS4 is a hint environment variable that tells us to look
# somewhere special for tess4 if and only if we need it. This allows
# setting OCRMYPDF_TESS4 to test tess4 and PATH to point to tess3
# on a system with both installed.
env = os.environ.copy()
env['OCRMYPDF_TESSERACT'] = env['OCRMYPDF_TESS4']
return env
raise EnvironmentError("Can't find Tesseract 4")
@contextmanager
def modified_os_environ(env):
old_env = os.environ.copy()
os.environ = env
yield
os.environ = old_env
def tess4_available():
@@ -31,13 +45,16 @@ def tess4_available():
official "tesseract" on PATH
"""
old_environ = os.environ.copy()
try:
os.environ['OCRMYPDF_TESSERACT'] = tess4_possible_location()
return tesseract.v4() and tesseract.has_textonly_pdf()
finally:
os.environ = old_environ
# ensure_tess4 locates the tess4 binary we are going to check
env = ensure_tess4()
with modified_os_environ(env):
# Now jump into this environment and make sure it really is Tess4
return tesseract.v4() and tesseract.has_textonly_pdf()
except EnvironmentError:
pass
return False
# Skip all tests in this file if not tesseract 4
pytestmark = pytest.mark.skipif(
@@ -53,10 +70,10 @@ def test_textonly_pdf(ensure_tess4, resources, outdir):
check_ocrmypdf(
resources / 'linn.pdf',
outdir / 'linn_textonly.pdf', '--pdf-renderer', 'tess4',
'--sidecar', 'foo',
env=ensure_tess4)
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
def test_pagesize_consistency_tess4(ensure_tess4, resources, outpdf):
from math import isclose
@@ -89,14 +106,14 @@ def test_skip_pages_does_not_replicate(
env=ensure_tess4
)
info_in = pageinfo.pdf_get_all_pageinfo(str(infile))
info_in = pdfinfo.PdfInfo(infile)
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
info = pdfinfo.PdfInfo(outpdf)
for page in info:
assert len(page['images']) == 1, "skipped page was replicated"
assert len(page.images) == 1, "skipped page was replicated"
for n in range(len(info_in)):
assert info[n]['width_inches'] == info_in[n]['width_inches']
assert info[n].width_inches == info_in[n].width_inches
def test_content_preservation(ensure_tess4, resources, outpdf):
@@ -108,6 +125,6 @@ def test_content_preservation(ensure_tess4, resources, outpdf):
env=ensure_tess4
)
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
info = pdfinfo.PdfInfo(outpdf)
page = info[0]
assert len(page['images']) > 1, "masked were rasterized"
assert len(page.images) > 1, "masks were rasterized"
+44
View File
@@ -0,0 +1,44 @@
# © 2017 James R. Barlow: github.com/jbarlow83
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
import os
import shutil
import pytest
from ocrmypdf.pdfinfo import PdfInfo, Colorspace, Encoding
import PyPDF2 as pypdf
from ocrmypdf.exceptions import ExitCode
from ocrmypdf import leptonica
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.exec import ghostscript
import logging
from math import isclose
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof
@pytest.fixture
def poster(resources):
return resources / 'poster.pdf'
def test_userunit_ghostscript_fails(poster, no_outpdf):
p, out, err = run_ocrmypdf(poster, no_outpdf, '--output-type=pdfa')
assert p.returncode == ExitCode.input_file
def test_userunit_qpdf_passes(spoof_tesseract_cache, poster, outpdf):
before = PdfInfo(poster)
check_ocrmypdf(poster, outpdf, '--output-type=pdf',
env=spoof_tesseract_cache)
after = PdfInfo(outpdf)
assert isclose(before[0].width_inches, after[0].width_inches)
def test_rotate_interaction(spoof_tesseract_cache, poster, outpdf):
check_ocrmypdf(poster, outpdf, '--output-type=pdf',
'--rotate-pages',
env=spoof_tesseract_cache)