Compare commits

...
160 Commits
Author SHA1 Message Date
James R. Barlow 054c0773a3 Update completions 2019-12-29 02:40:55 -08:00
James R. Barlow 89aa78b724 docs: fix obsolete statement to "brew install tesseract-lang"
Closes #469
2019-12-29 02:37:10 -08:00
James R. Barlow 708113a514 Windows: Remove Program Files cache from ocrmypdf.exec
@lru_cache doesn't work here, so let's just remove it.
2019-12-29 02:36:20 -08:00
James R. Barlow 95ef5410c2 azure: tweak windows script 2019-12-28 16:12:53 -08:00
James R. Barlow 868b3b4abd exec/init: os.get_exec_path() returns list not str 2019-12-28 16:11:08 -08:00
James R. Barlow 045bdff95a azure: homebrew broke something to do with python@2? 2019-12-28 16:10:43 -08:00
James R. Barlow d12b27ac1d v9.3.0 release notes 2019-12-28 15:42:24 -08:00
James R. Barlow e4e00de79f Add improved example demonstrating watched folder functionality
Closes #466
2019-12-28 15:37:42 -08:00
James R. Barlow a53a3937c2 Fix exception on parsing Ghostscript error messages 2019-12-20 11:25:45 -08:00
James R. Barlow 343424b4d2 azure: only publish code coverage for macOS
macOS (due to Homebrew) currently has the most comprehensive code
coverage. Azure's code coverage feature does not merge code coverage,
so last task to finish wins.
2019-12-20 10:56:10 -08:00
James R. Barlow c5edff2c2f Sort imports 2019-12-19 15:31:18 -08:00
James R. Barlow 8c5f8b8ddd Add isort to precommit 2019-12-19 15:29:38 -08:00
James R. Barlow 39da931a56 Look in Program Files for executables and liblept5.dll 2019-12-19 12:11:32 -08:00
James R. Barlow 9fe354359b Generally update documentation about available platforms 2019-12-19 00:27:37 -08:00
James R. Barlow facc4750bc Remove command line qpdf from azure and travis 2019-12-12 00:14:05 -08:00
James R. Barlow 437c235738 v9.2.0 release notes and docs 2019-12-11 13:13:51 -08:00
James R. Barlow 9559b0b186 Use pikepdf to perform qpdf.check() 2019-12-11 01:21:15 -08:00
James R. Barlow 91456e19a4 pdfa.py: Fix misleading comment 2019-12-11 01:05:47 -08:00
James R. Barlow a2d89f67c4 Improve help messages for Windows 2019-12-10 01:44:00 -08:00
James R. Barlow f34130d193 Fixed case where page image was not converted to JPEG
If a preprocessing option was used,
and all original images on the page were JPEGs,
and --output-type=pdf, then
images would saved as Flate instead
of converted to JPEG.
2019-12-10 01:07:59 -08:00
James R. Barlow c5571388e2 Improve test coverage of _sync.py 2019-12-10 01:06:27 -08:00
James R. Barlow 9af59c0d6d docs: improvements for Windows 2019-12-09 21:39:01 -08:00
James R. Barlow 55ae838cb7 azure: fix extra build step 2019-12-09 21:11:50 -08:00
James R. Barlow c434b97f55 docs: more install notes 2019-12-09 16:18:01 -08:00
James R. Barlow 607eee198d tests: split out preprocessing tests 2019-12-09 16:18:01 -08:00
James R. Barlow 5e2a7f8a56 tests: speed up several slow tests 2019-12-09 16:17:57 -08:00
James R. Barlow fd9550acda Add Azure Pipelines CI/CD 2019-12-09 16:17:33 -08:00
James R. Barlow b354511ac9 ghostscript: document need to write to stdout when using txtwrite 2019-12-09 16:17:10 -08:00
James R. Barlow 7be293f628 Address tests that fail on Windows with Python 3.7 or 3.6 2019-12-09 16:17:10 -08:00
James R. Barlow 65855dc14c Fix close_fds=True on Windows Python 3.6 2019-12-09 16:17:10 -08:00
James R. Barlow cac4a8b9b6 Suppress duplicate error messages from Ghostscript 2019-12-06 15:14:15 -08:00
James R. Barlow 17d97b354a Ignore mypy cache 2019-12-06 15:11:10 -08:00
James R. Barlow 1c1b60fa9f Add typing hints for ocr() function 2019-12-06 15:10:54 -08:00
James R. Barlow 6b745d892f Merge branch 'windows' 2019-12-06 15:09:09 -08:00
James R. Barlow fbf271a3ec Remove Tesseract < 4.0 specific check 2019-12-06 15:06:02 -08:00
James R. Barlow 8077718804 Possible fix to loss of log adapter state 2019-12-06 15:05:58 -08:00
James R. Barlow 66bda3420a docs: cause about using Windows in production 2019-12-06 15:03:20 -08:00
James R. Barlow f6510e2b15 Document function of symlink shim 2019-12-06 15:00:12 -08:00
James R. Barlow 51abd79136 Tesseract no longer posts an error message if config file not found 2019-12-04 21:35:28 -08:00
James R. Barlow 5607429d9a tests: error message from tesseract change 2019-12-04 21:31:01 -08:00
James R. Barlow b8b7ecfe7f Fix DecompressionBomb related errors due to Windows process differences 2019-12-04 21:10:27 -08:00
James R. Barlow d4abe88452 docs: sketch Windows install procedure 2019-12-04 21:01:09 -08:00
James R. Barlow cb3cfaa055 Add Windows install advice 2019-12-04 21:01:09 -08:00
James R. Barlow 9db01c7ff5 Remove test_bad_utf8
Due to difficulties of getting this to work on Python 3.8, Windows, and
high probability that this behavior is now gone from Tesseract 4.0+.

Originally added in 2017.
2019-12-04 21:01:09 -08:00
James R. Barlow d0301813cc ghosttext: mention page number differences 2019-12-04 21:01:09 -08:00
James R. Barlow cff37bf681 Make test_german more Windows-friendly 2019-12-04 21:01:09 -08:00
James R. Barlow 66d04dd6e3 Don't expect filenames to be replicated on NT 2019-12-04 21:01:09 -08:00
James R. Barlow 06a1f987d4 Use _OCRMYPDF_TEST_PATH for testing and .py stubs to simulate symlinks 2019-12-04 21:01:06 -08:00
James R. Barlow e51e21c6b6 ghostscript: Refactor checking for executable name on Windows 2019-12-04 21:01:06 -08:00
James R. Barlow c5fa72bd4e ghostscript: use run(check=True) for more consistent error handling 2019-12-04 21:01:00 -08:00
James R. Barlow bf99587aa1 ghostscript: use correct executable name on Windows 2019-12-04 17:37:38 -08:00
James R. Barlow d249aef57d ghostscript: don't use NamedTemporaryFile
Temporary files are more awkward for Windows.
2019-12-04 17:37:38 -08:00
James R. Barlow 43ab7c88d7 Remove os_environ() context manager 2019-12-04 17:37:38 -08:00
James R. Barlow ca9669742d Move gs tests to test_ghostscript 2019-12-04 17:14:27 -08:00
James R. Barlow 8a1dddc3ee Don't worry about closed streams on Windows 2019-12-04 17:14:27 -08:00
James R. Barlow 0cd424ffcb Enforce str-only environment for Windows since it's more strict 2019-12-04 17:14:27 -08:00
James R. Barlow fde550f9a7 test: Replace many instances of run_ocrmypdf in subprocess with inline 2019-12-04 17:14:27 -08:00
James R. Barlow a3726e4ce3 Fix test_metadata: use mmap in a Windows and POSIX compatible way 2019-12-04 17:13:52 -08:00
James R. Barlow 4ab0a8ff35 Fix test_single_page_inline_image - remove temp file 2019-12-04 17:13:51 -08:00
James R. Barlow 37f6f72df3 tests: a few Windows fixes 2019-12-04 17:13:51 -08:00
James R. Barlow 3f92867ae6 Fix TypeError "environment can only contain strings"
Apparently Windows Python doesn't coerce pathlib.Path to str.
2019-12-04 17:13:51 -08:00
James R. Barlow e63503d64b Fix difference in Windows error message breaking test_no_languages 2019-12-04 17:13:51 -08:00
James R. Barlow 17d20309c7 leptonica: fix missing Leptonica error message for Windows
Since it has the unintuitive fix of adding Tesseract to PATH.
2019-12-04 17:13:51 -08:00
James R. Barlow fe7c69ce95 leptonica: don't open files by name; use memory buffers
Avoids encoding issues and makes error trap unnecessary in some cases.
2019-12-04 17:13:51 -08:00
James R. Barlow 9baccee8c5 leptonica: Handle API change for pixFindPageForeground 2019-12-04 17:13:51 -08:00
James R. Barlow d5bb9929f3 leptonica: Use Windows name for DLL
Thanks to @dibu28
2019-12-04 17:13:51 -08:00
James R. Barlow 72d3ee3a87 Refactor symlink usage to support Windows 2019-12-04 17:13:51 -08:00
James R. Barlow 17c419dfcb compile_leptonica: move to correct location 2019-12-04 17:04:08 -08:00
James R. Barlow 84cc49b14b black: don't reformat _leptonica.py 2019-12-04 17:04:08 -08:00
James R. Barlow b7f63bc93d Make devnull check compatible with Windows 2019-12-04 17:04:08 -08:00
James R. Barlow ad9a3b5302 Update version of pdfminer.six supported 2019-12-04 17:04:08 -08:00
James R. Barlow 4e4bcaf243 Improve pre-commit checks 2019-11-25 14:38:23 -08:00
James R. Barlow 11afe3507f black: don't reformat _leptonica.py 2019-11-25 14:34:36 -08:00
James R. Barlow 7691ba8535 v9.1.1 release notes 2019-11-18 15:17:00 -08:00
James R. Barlow b787a369ee Fix reference to Alpine apk add 2019-11-18 15:13:42 -08:00
James R. Barlow 9fb8b267af docker: use get-pip to install pip
Smaller download, needed for manylinux2010.
2019-11-16 15:21:45 -08:00
James R. Barlow 0a08d6ce1f Update version of pdfminer.six supported 2019-11-13 01:45:06 -08:00
James R. Barlow f517efe819 docs: wsl - get-pip.py 2019-11-12 15:01:15 -08:00
James R. Barlow 5f5421f23d test: further fixes to test_report_file_size 2019-11-12 01:14:21 -08:00
James R. Barlow 703b6db95c test: fix test_report_file_size 2019-11-11 23:00:08 -08:00
James R. Barlow 000040d497 v9.1.0 release notes 2019-11-11 22:39:33 -08:00
James R. Barlow 5bd6665b49 Use pikepdf 1.7.0 to improve Python 3.8 support 2019-11-11 22:36:38 -08:00
James R. Barlow 1c303afe21 docs: fix installation instructions for pikepdf manylinux2010 wheels 2019-11-11 22:22:30 -08:00
James R. Barlow 11a5c80917 travis: enable Py 3.8 2019-11-11 22:19:15 -08:00
James R. Barlow 9b2ab92913 tesseract: fix exception when logger is RootLogger 2019-11-11 22:19:01 -08:00
James R. Barlow 0c4b69ec5a Fix lint warning about missing cur_item 2019-11-09 14:56:43 -08:00
James R. Barlow 45bea1c0e0 Import and docstring cleanup 2019-11-09 14:56:30 -08:00
James R. Barlow db914d4cd1 Report missing optional dependencies as possible cause of file size increase 2019-11-09 14:21:57 -08:00
James R. Barlow df4a8faecd docs: mention systemd for batches 2019-11-08 03:24:54 -08:00
James R. Barlow 1273e7aeda docs: document optimization 2019-11-08 03:22:28 -08:00
James R. Barlow e13a673b1a docs: mention how to suppress progbar 2019-11-08 02:59:02 -08:00
James R. Barlow 979b0bcaed tesseract: refactor logging 2019-11-05 15:38:09 -08:00
James R. Barlow 3438afaffe Support pdfminer.six 20191020 2019-11-04 03:15:59 -08:00
James R. Barlow 681fa039cc Update release notes; disable Py3.8 test again 2019-11-04 03:00:15 -08:00
James R. Barlow 69e80f1545 docker-compose.test does not seem to be ready for production use 2019-11-04 02:58:57 -08:00
James R. Barlow 983835cce4 docs: add remark about optimizing without OCR 2019-11-04 02:32:29 -08:00
James R. Barlow 6c23b137e2 Docker: relocate dockerfile 2019-11-04 02:27:30 -08:00
James R. Barlow d656b2b3f2 docs: remove comment about Ubuntu image
[ci skip]
2019-11-04 02:08:42 -08:00
James R. Barlow 031b800aac Docker autotest: fix, maybe? 2019-11-04 02:04:07 -08:00
James R. Barlow 05eb85ee77 Docker: try adding automated test 2019-11-04 01:23:54 -08:00
James R. Barlow 4da5214ca9 Drop support for unpaper 6.1 on Ubuntu 14.04 2019-11-04 00:09:04 -08:00
James R. Barlow 1ee829dd59 Travis: enable Python 3.8 testing 2019-11-04 00:05:18 -08:00
James R. Barlow 99db5d91ae Fix issue "MANIFEST.in exists" by removing MANIFEST.in
MANIFEST.in is always an issue
2019-11-04 00:03:49 -08:00
James R. Barlow 3a4490ee36 Dockerfile: fix jbig2 not copied over 2019-11-03 23:52:08 -08:00
James R. Barlow a492e3b472 Dockerfile: fix errors are trying to build unneeded cached wheels 2019-11-03 23:51:55 -08:00
James R. Barlow c3719d3b72 Dockerfile: remove venv from Ubuntu image; tweak reqs 2019-11-03 23:39:40 -08:00
James R. Barlow ad48fc6415 Remove Alpine Docker image 2019-11-03 22:35:15 -08:00
James R. Barlow 7f8018ffde Mention that v9.0.4 requires a source install for Py3.8 for now, due to lack of CI availability 2019-11-03 01:49:36 -08:00
James R. Barlow 80651fe12c Fix test suite error 2019-10-24 18:17:03 -07:00
James R. Barlow a58209e895 Disable Py3.8 for now 2019-10-24 18:16:47 -07:00
James R. Barlow 775b958c55 Update release notes 2019-10-24 16:58:39 -07:00
James R. Barlow cdcdd16865 Require Pillow 6.2.0 based on security vulnerability report in older versions 2019-10-23 12:27:29 -07:00
James R. Barlow b332d76782 Mention when we default to English and the system locale is not English
Closes #337
2019-10-22 01:49:38 -07:00
James R. Barlow 3660007fc8 travis: Python 3.8, osx_image 2019-10-20 04:06:13 -07:00
James R. Barlow b55d7e57af Python 3.8 updates 2019-10-20 03:20:54 -07:00
James R. Barlow 6e99e7b346 Use lstm_use_matrix for --user-words,patterns 2019-10-20 00:49:11 -07:00
James R. Barlow 4d26867dee Delinting 2019-09-20 17:17:11 -07:00
James R. Barlow 78e8bf9cbf Use at most 3 Tesseract threads
Based on a user suggestion and
tesseract-ocr/tesseract#2611, I reviewed thread limits and found that
thread limit of 3 is still beneficial, but not 4.

> time env OMP_THREAD_LIMIT=2 tesseract omp4.png stdout >/dev/null
Warning: Invalid resolution 0 dpi. Using 70 instead.
Estimating resolution as 143
116.67user 1.67system 1:26.26elapsed 137%CPU (0avgtext+0avgdata 356752maxresident)k
2213inputs+0outputs (18major+131059minor)pagefaults 0swaps
> time env OMP_THREAD_LIMIT=3 tesseract omp4.png stdout >/dev/null
Warning: Invalid resolution 0 dpi. Using 70 instead.
Estimating resolution as 143
136.89user 1.63system 1:19.56elapsed 174%CPU (0avgtext+0avgdata 356784maxresident)k
821inputs+0outputs (0major+131080minor)pagefaults 0swaps
> time env OMP_THREAD_LIMIT=4 tesseract omp4.png stdout >/dev/null
Warning: Invalid resolution 0 dpi. Using 70 instead.
Estimating resolution as 143
161.31user 1.51system 1:18.80elapsed 206%CPU (0avgtext+0avgdata 356632maxresident)k
8477inputs+0outputs (12major+131074minor)pagefaults 0swaps
> time env OMP_THREAD_LIMIT=8 tesseract omp4.png stdout >/dev/null
Warning: Invalid resolution 0 dpi. Using 70 instead.
Estimating resolution as 143
160.30user 1.62system 1:18.01elapsed 207%CPU (0avgtext+0avgdata 356640maxresident)k
821inputs+0outputs (0major+131078minor)pagefaults 0swaps
2019-09-20 17:12:36 -07:00
James R. Barlow de61530d4d docs: fix intermediate file list for v9 2019-09-20 17:02:35 -07:00
James R. Barlow c149f860b5 Add contributing guide 2019-09-20 17:02:22 -07:00
James R. Barlow 68c852acec Remove test_tesseract_config_invalid from suite
Also causes problems in CI
2019-09-18 13:28:02 -07:00
James R. Barlow a8565bac6e Fix any False in the ocrmypdf.ocr() API being set to True 2019-09-15 01:47:31 -07:00
James R. Barlow 6e8b0c3194 Fix py36 test including 37 2019-09-15 01:47:10 -07:00
James R. Barlow ff860e8362 Fix black settings in pyproject.toml 2019-09-15 01:46:13 -07:00
James R. Barlow cf4b04c5d1 optimize: work around pikepdf 1.6.3 limitation with indexed ICCbased colorspaces 2019-09-11 12:56:27 -07:00
James R. Barlow 078bc2abe9 pdfa: assume 3 RGB channels always 2019-09-11 12:55:38 -07:00
James R. Barlow d7b7ca0574 v9.0.3 notes; Remove test_tesseract_config_notfound from suite 2019-09-05 13:39:43 -07:00
James R. Barlow 17ac9d7a9a Embed ICC profile in .ps (fixing Ghostscript 9.28 compatibility)
Previously we included the
   filename, which required Postscript to run with file access enabled. For
   security, Ghostscript 9.28 enables ``-dSAFER`` and as such, no longer
   permits access to any file by default. This fix is necessary for
   compatibility with Ghostscript 9.28.

We use ASCII85 for a slightly more compact representation.
2019-09-05 13:17:26 -07:00
James R. Barlow a2a197ce4c v9.0.2 release notes 2019-09-04 02:34:21 -07:00
James R. Barlow 944d59e5ad Fix --print-parameters issue when chi_sim is not installed 2019-09-04 01:17:52 -07:00
James R. Barlow 1c3e90a892 optimize: solve monochrome by converting to G4 2019-09-04 00:51:47 -07:00
James R. Barlow c728836956 Adjust test requirements 2019-09-04 00:50:48 -07:00
James R. Barlow 0d80fab339 Remove restriction on pytest < 5 2019-09-03 23:47:55 -07:00
James R. Barlow a650caa599 optimize: don't consider 1bpp images for PNG optimization 2019-09-03 23:47:20 -07:00
James R. Barlow c6caff90a1 optimize: only re-insert pngs after pngquant
Previously we attempted to reinsert all PNGs, but it appears to be
unlikely that Leptonica's API is actually capable of optimizing the PNG
before it inserts it.

In any event qpdf has gained image optimization capabilities as well
which we coudld borrow.
2019-09-03 23:46:25 -07:00
James R. Barlow 671c88d3b5 optimize: exclude images with custom Decode tables 2019-09-03 23:37:23 -07:00
James R. Barlow b2cfaedf91 optimize: Don't reinsert 1bpp images
There seems to be version to version inconsistencies between
Leptonica's photometric interpretation of 1bpp images, in
particular commit a0692307 introduces a change to force transcoding
in this situation.

However, I never entirely got to the bottom of where the problem
is, and in any event 1bpp images are probably better optimized
by JBIG2 than pngquant, so we're going to stop running them through
pngquant.
2019-09-03 23:26:13 -07:00
James R. Barlow 19ba3ae011 Allow test_german to xfail if deu language is not installed 2019-09-03 17:38:54 -07:00
James R. Barlow feff1e38bb Use context managers to ensure Pillow images are closed 2019-09-03 17:19:12 -07:00
James R. Barlow c8d6ea6b10 Fix tests broken by --print-parameters change 2019-09-03 17:17:24 -07:00
James R. Barlow b0d9775343 Attempt to resolve black-inversion issue 2019-08-31 01:25:36 -07:00
James R. Barlow 462bfb84fb install: affirm that we now require Tesseract beta 2019-08-31 01:24:31 -07:00
James R. Barlow 11ef78a891 Fix running without eng.traineddata installed raises exception 2019-08-27 14:54:03 -07:00
James R. Barlow 638eb556ef Reactivate user-words test that was always skipped 2019-08-27 14:52:59 -07:00
James R. Barlow fdefcd8af2 travis: Make 3.7 the build leader/deployer 2019-08-26 13:30:07 -07:00
James R. Barlow 09457edad3 alpine: use jbig2enc@community 2019-08-26 12:49:47 -07:00
James R. Barlow 6460a7eb3e docs: leptonica.com -> .org 2019-08-26 12:07:34 -07:00
James R. Barlow 707ebeb151 docs: installation updates 2019-08-11 18:48:56 -07:00
James R. Barlow e9bc093842 v9.0.1 release notes 2019-08-11 17:14:11 -07:00
James R. Barlow 2eeaca1168 travis: make minimal config even more minimal 2019-08-11 17:13:55 -07:00
James R. Barlow 7755c5c5a7 tests: fix interpretation of None as omitted argument 2019-08-11 16:58:22 -07:00
James R. Barlow 793348a47c tests: mark test as requiring pngquant 2019-08-11 16:58:22 -07:00
James R. Barlow b241f66919 travis: Add a minimal Ubuntu config 2019-08-11 16:58:06 -07:00
James R. Barlow 8ad034a678 docs: update install on FreeBSD to point to ports 2019-08-11 15:50:52 -07:00
James R. Barlow a1a7b973e9 tests: split out stdin/stdout tests 2019-08-09 01:23:49 -07:00
James R. Barlow 7bfcd0a9d5 Use pikepdf 1.6.1 2019-08-09 01:12:13 -07:00
James R. Barlow f276c4ef1e Alpine Docker: jbig2enc moved from testing to community 2019-08-09 01:09:18 -07:00
James R. Barlow 77bbc22c50 Ensure --image-dpi on non-image produces a warning 2019-08-09 01:08:16 -07:00
James R. Barlow a6805ed343 Travis: remove vestiges of pdfminer being optional on osx 2019-07-30 00:42:38 -07:00
James R. Barlow c4afc5c242 Add missing item from v9.0.0 release notes 2019-07-30 00:39:14 -07:00
97 changed files with 2653 additions and 1415 deletions
+23 -22
View File
@@ -1,6 +1,6 @@
# OCRmyPDF
#
FROM ubuntu:19.04 as base
FROM ubuntu:19.10 as base
FROM base as builder
@@ -10,36 +10,37 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool \
libleptonica-dev \
zlib1g-dev \
ocrmypdf \
pngquant \
python3-pip \
python3-venv \
tesseract-ocr \
unpaper \
wget \
python3 \
python3-distutils \
ca-certificates \
curl \
git
# Get the latest pip (Ubuntu version doesn't support manylinux2010)
RUN \
curl https://bootstrap.pypa.io/get-pip.py | python3
# Compile and install jbig2
# Needs libleptonica-dev, zlib1g-dev
RUN \
mkdir jbig2 \
&& wget -q https://github.com/agl/jbig2enc/archive/0.29.tar.gz -O - | \
&& curl -L https://github.com/agl/jbig2enc/archive/0.29.tar.gz | \
tar xz -C jbig2 --strip-components=1 \
&& cd jbig2 \
&& ./autogen.sh && ./configure && make && make install \
&& cd .. \
&& rm -rf jbig2
RUN python3 -m venv /appenv
COPY . /app
WORKDIR /app
RUN . /appenv/bin/activate; \
pip install --upgrade pip \
&& pip install .
RUN pip3 install --no-cache-dir \
-r requirements/main.txt \
-r requirements/webservice.txt \
-r requirements/test.txt \
-r requirements/watcher.txt \
.
FROM base
@@ -53,7 +54,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
zlib1g \
pngquant \
python3 \
python3-venv \
qpdf \
tesseract-ocr \
tesseract-ocr-chi-sim \
@@ -62,11 +62,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-fra \
tesseract-ocr-por \
tesseract-ocr-spa \
unpaper \
wget
unpaper
WORKDIR /app
COPY --from=builder /usr/local/lib/ /usr/local/lib/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
# Copy
COPY --from=builder /app/misc/webservice.py /app/
COPY --from=builder /app/misc/watcher.py /app/
# Copy minimal project files to get the test suite.
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
@@ -74,7 +78,4 @@ COPY --from=builder /app/requirements /app/requirements
COPY --from=builder /app/tests /app/tests
COPY --from=builder /app/src /app/src
COPY --from=builder /appenv /appenv
COPY --from=builder /usr/local /usr/local
ENTRYPOINT ["/appenv/bin/ocrmypdf"]
ENTRYPOINT ["/usr/local/bin/ocrmypdf"]
-91
View File
@@ -1,91 +0,0 @@
FROM alpine:3.9 as base
FROM base as builder
ENV LANG=C.UTF-8
# Normally:
# echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
RUN \
echo -e '@testing http://nl.alpinelinux.org/alpine/edge/testing\n@community http://nl.alpinelinux.org/alpine/edge/community'\
>> /etc/apk/repositories \
# Add runtime dependencies
&& apk add --update \
python3-dev \
py3-setuptools \
jbig2enc@testing \
ghostscript \
qpdf@community \
qpdf-dev@community \
tesseract-ocr \
unpaper \
pngquant \
libxml2-dev \
libxslt-dev \
zlib-dev \
libffi-dev \
leptonica-dev \
binutils \
&& pip3 install --upgrade pip \
# Install pybind11 for pikepdf
&& pip3 install pybind11 \
# Install flask for the webservice
&& pip3 install flask \
# Add build dependencies
&& apk add --virtual build-dependencies \
build-base \
git
COPY . /app
WORKDIR /app
RUN pip3 install .
FROM base
ENV LANG=C.UTF-8
# Normally:
# echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
RUN \
echo -e '@testing http://nl.alpinelinux.org/alpine/edge/testing\n@community http://nl.alpinelinux.org/alpine/edge/community'\
>> /etc/apk/repositories \
# Add runtime dependencies
&& apk add --update \
python3 \
jbig2enc@testing \
ghostscript \
qpdf@community \
qpdf-dev@community \
tesseract-ocr \
tesseract-ocr-data-deu \
tesseract-ocr-data-chi_sim \
unpaper \
pngquant \
libxml2 \
libxslt \
zlib \
libffi \
leptonica-dev \
binutils \
&& mkdir /app
WORKDIR /app
# Copy build artifacts (python site-packages)
COPY --from=builder /usr/lib/python3.6/site-packages /usr/lib/python3.6/site-packages
COPY --from=builder /usr/bin/ocrmypdf /usr/bin/dumppdf.py /usr/bin/latin2ascii.py /usr/bin/pdf2txt.py /usr/bin/img2pdf /usr/bin/chardetect /usr/bin/
# Copy
COPY --from=builder /app/misc/webservice.py /app/
# Copy minimal project files to get the test suite.
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
COPY --from=builder /app/requirements /app/requirements
COPY --from=builder /app/tests /app/tests
COPY --from=builder /app/src /app/src
ENTRYPOINT ["/usr/bin/ocrmypdf"]
+1
View File
@@ -7,6 +7,7 @@
*.pyc
*.sublime-*
*.DS_Store
.mypy_cache/
# Package building
.eggs/
+20 -3
View File
@@ -1,6 +1,23 @@
repos:
- repo: https://github.com/ambv/black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/asottile/seed-isort-config
rev: v1.9.3
hooks:
- id: seed-isort-config
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
language_version: python3.7
- id: black
language_version: python3.7
exclude: ^src/ocrmypdf/lib/_leptonica.py
+106 -93
View File
@@ -1,7 +1,11 @@
branches:
except:
- azure
cache:
pip: true
directories:
- $HOME/Library/Caches/Homebrew
- $HOME/Library/Caches/Homebrew
matrix:
include:
@@ -12,27 +16,54 @@ matrix:
python: "3.6"
env:
- DIST=trusty
addons: &trusty_apt
- MINIMAL=true
addons:
apt:
update: true
sources:
- sourceline: 'ppa:alex-p/tesseract-ocr'
- sourceline: 'ppa:heyarje/libav-11'
- sourceline: 'ppa:vshn/ghostscript'
- sourceline: "ppa:alex-p/tesseract-ocr"
- sourceline: "ppa:vshn/ghostscript"
packages:
- ghostscript
- libavcodec56
- libavformat56
- libavutil54
- libexempi3
- libffi-dev
- pngquant
- poppler-utils
- qpdf
- tesseract-ocr
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- ghostscript
- libffi-dev
- tesseract-ocr
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
before_install: |
pip3 install --upgrade pip
pip3 install --upgrade wheel
- os: linux
dist: trusty
sudo: required
language: python
python: "3.6"
env:
- DIST=trusty
addons:
apt:
update: true
sources:
- sourceline: "ppa:alex-p/tesseract-ocr"
- sourceline: "ppa:heyarje/libav-11"
- sourceline: "ppa:vshn/ghostscript"
packages:
- ghostscript
- libavcodec56
- libavformat56
- libavutil54
- libffi-dev
- tesseract-ocr
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- libexempi3 # --- optional extras from here ---
- pngquant
- poppler-utils
before_install: |
mkdir -p bin packages
pip3 install --upgrade pip
pip3 install --upgrade wheel
- os: linux
dist: xenial
sudo: required
@@ -44,41 +75,43 @@ matrix:
apt:
update: true
sources:
- sourceline: 'ppa:alex-p/tesseract-ocr'
- sourceline: "ppa:alex-p/tesseract-ocr"
packages:
- ghostscript
- libexempi3
- libffi-dev
- pngquant
- poppler-utils
- qpdf
- tesseract-ocr
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- unpaper
- os: osx
osx_image: xcode9.2
language: generic
addons:
homebrew:
update: true
packages:
- exempi
- ghostscript
- jbig2enc
- leptonica
- openjpeg
- libexempi3
- libffi-dev
- pngquant
- python
- qpdf
- tesseract
- poppler-utils
- tesseract-ocr
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- unpaper
- os: osx
osx_image: xcode9.2
language: generic
- os: linux
dist: bionic
sudo: required
language: python
python: "3.8"
env:
- ADD_PDFMINER=1
- DIST=bionic
addons:
apt:
update: true
sources:
- sourceline: "ppa:alex-p/tesseract-ocr"
packages:
- ghostscript
- libexempi3
- libffi-dev
- pngquant
- poppler-utils
- tesseract-ocr
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- unpaper
- os: osx
language: generic
addons:
homebrew:
update: true
@@ -93,54 +126,34 @@ matrix:
- qpdf
- tesseract
- unpaper
before_install: |
pip3 install --upgrade pip
pip3 install wheel
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
before_install: |
mkdir -p bin
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
pip3 install --upgrade pip
pip3 install --upgrade wheel
if [[ "$DIST" == "trusty" ]]; then
mkdir -p packages
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
fi
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
pip3 install --upgrade pip
pip3 install wheel
fi
- rm -f $HOME/.cache/pip/log/debug.log
install:
- export PATH=$PWD/bin:$PATH
- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251
- pip3 install -r requirements/main.txt
- pip3 install --no-deps .
- |
if [[ "$ADD_PDFMINER" == "1" ]]; then
pip3 install --no-deps .[pdfminer]
fi
- pip3 install -r requirements/test.txt
- mkdir -p bin
- export PATH=$PWD/bin:$PATH
- pip3 install -r requirements/main.txt -r requirements/test.txt .
script:
- tesseract --version
- qpdf --version
- pytest -n auto
deploy:
# 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
user: ocrmypdf-travis
password:
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
- tesseract --version
- pytest -n auto
# deploy:
# # release for main pypi
# # 3.7 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
# user: ocrmypdf-travis
# password:
# secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
# distributions: "sdist bdist_wheel"
# on:
# branch: master
# tags: true
# condition: $TRAVIS_PYTHON_VERSION == "3.7" && $TRAVIS_OS_NAME == "linux"
# skip_upload_docs: true
-43
View File
@@ -1,43 +0,0 @@
# requirements
recursive-include requirements *
# git
include .git_archival.txt
# docker
include .dockerignore
recursive-include .docker *
# tests
include .coveragerc
recursive-include tests *.bin
recursive-include tests *.jpg
recursive-include tests *.jsonl
recursive-include tests *.png
recursive-include tests *.pdf
recursive-include tests *.py
recursive-include tests *.rst
recursive-include tests *.txt
recursive-exclude tests/resources/private *
# documentation
include LICENSE
include *.rst
recursive-exclude .github *
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs *.svg
recursive-exclude docs/_build *
# support files
recursive-include src/ocrmypdf/data *
include *.py
exclude tasks.py
recursive-exclude .travis *
exclude .travis*
# code
exclude src/ocrmypdf/lib/_leptonica.py
exclude scratch.py
+10 -12
View File
@@ -1,6 +1,8 @@
<img src="docs/images/logo.svg" width="240" alt="OCRmyPDF">
[![Travis build status][travis]](https://travis-ci.org/jbarlow83/OCRmyPDF) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs]
[![Build Status][azure]](https://dev.azure.com/jim0585/ocrmypdf/_build/latest?definitionId=2&branchName=master) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
[azure]: https://dev.azure.com/jim0585/ocrmypdf/_apis/build/status/jbarlow83.OCRmyPDF?branchName=master
[travis]: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master "Travis build status"
@@ -10,6 +12,8 @@
[docs]: https://readthedocs.org/projects/ocrmypdf/badge/?version=latest "RTD"
[pyversions]: https://img.shields.io/pypi/pyversions/ocrmypdf "Supported Python versions"
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.
```bash
@@ -46,7 +50,7 @@ For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/
Motivation
----------
I searched the web for a free command line tool to OCR PDF files on Linux/UNIX: I found many, but none of them were really satisfying.
I searched the web for a free command line tool to OCR PDF files: I found many, but none of them were really satisfying.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
@@ -61,7 +65,7 @@ I searched the web for a free command line tool to OCR PDF files on Linux/UNIX:
Installation
------------
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
Linux, Windows, macOS and FreeBSD are supported. Docker images are also available.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
@@ -75,7 +79,7 @@ and users of Fedora 29 or later may simply
dnf install ocrmypdf
```
and macOS users with Homebrew may simply
and Homebrew users (macOS, Linux, Windows Subsystem for Linux) may simply
```bash
brew install ocrmypdf
@@ -109,18 +113,12 @@ ocrmypdf --help
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
If you detect an issue, please:
- Check whether your issue is already known
- If no problem report exists on github, please create one here: <https://github.com/jbarlow83/OCRmyPDF/issues>
- Describe your problem thoroughly
- Append the console output of the script when running the debug mode (`-v 1` option)
- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox)
Please report issues on our [GitHub issues](https://github.com/jbarlow83/OCRmyPDF/issues) page, and follow the issue template for quick response.
Requirements
------------
Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD.
Press & Media
-------------
+251
View File
@@ -0,0 +1,251 @@
trigger:
tags:
include:
- v*
branches:
include:
- "*"
exclude:
- "travis"
stages:
- stage: "Test"
jobs:
- job: Windows
pool:
vmImage: "vs2017-win2016"
strategy:
matrix:
Python36:
python.version: "3.6"
Python37:
python.version: "3.7"
Python38:
python.version: "3.8"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- pwsh: |
choco install --yes --no-progress --pre tesseract
choco install --yes --no-progress python3
choco install --yes --no-progress ghostscript
choco install --yes --no-progress pngquant
displayName: "Install system packages"
- pwsh: |
refreshenv
python -m pip install --upgrade pip wheel
python -m pip install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- pwsh: |
refreshenv
$env:pathext += ';.py'
# -n auto helps Windows
python -m pytest -n auto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- job: "Ubuntu_1804"
pool:
vmImage: "ubuntu-18.04"
strategy:
matrix:
Python36:
python.version: "3.6"
Python37:
python.version: "3.7"
Python38:
python.version: "3.8"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- bash: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
python3-software-properties \
curl \
ghostscript \
img2pdf \
libexempi3 \
libffi-dev \
liblept5 \
libsm6 libxext6 libxrender-dev \
pngquant \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
unpaper \
zlib1g
displayName: "Install system packages"
- bash: |
curl https://bootstrap.pypa.io/get-pip.py | python3
pip3 install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- bash: |
tesseract --version
displayName: "Record versions"
- bash: |
# -n auto is slower on Linux and breaks on Python 3.8
pytest -n0 --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- job: "Ubuntu_1604"
pool:
vmImage: "ubuntu-16.04"
strategy:
matrix:
Python36:
python.version: "3.6"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- bash: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
software-properties-common
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ghostscript \
img2pdf \
libexempi3 \
libffi-dev \
liblept5 \
libsm6 libxext6 libxrender-dev \
pngquant \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
unpaper \
zlib1g
displayName: "Install system packages"
- bash: |
curl https://bootstrap.pypa.io/get-pip.py | python3
pip3 install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- bash: |
tesseract --version
displayName: "Record versions"
- bash: |
# -n auto is slower on Linux and breaks on Python 3.8
pytest -n0 --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- job: "macOS_Mojave"
pool:
vmImage: "macos-10.14"
strategy:
matrix:
Python37:
python.version: "3.7"
Python38:
python.version: "3.8"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- bash: |
brew update
brew unlink python@2
brew install \
exempi \
ghostscript \
jbig2enc \
leptonica \
openjpeg \
pngquant \
tesseract \
unpaper
displayName: "Install system packages"
- bash: |
pip3 install --upgrade pip
pip3 install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- bash: |
tesseract --version
displayName: "Record versions"
- bash: pytest -nauto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
- stage: "Artifacts"
jobs:
- job: "sdist_wheel"
pool:
vmImage: "ubuntu-18.04"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "3.7"
- bash: |
python -m pip install --upgrade pip wheel
python setup.py sdist bdist_wheel
- publish: dist
artifact: sdist_wheel
- stage: "Deploy"
jobs:
- deployment: "PyPI"
pool:
vmImage: "ubuntu-18.04"
environment: "deploy"
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: sdist_wheel
- script: |
mkdir -p dist
mv $(Pipeline.Workspace)/sdist_wheel/* dist
displayName: "Move dist files"
- task: UsePythonVersion@0
inputs:
versionSpec: "3.8"
architecture: x64
- script: |
pip install --upgrade twine
displayName: "Generate artifacts"
- script: |
cat <<FILE >.pypirc
[distutils]
index-servers =
pypi
[pypi]
username: __token__
password: $(TOKEN_PYPI)
FILE
displayName: "Generate PyPI auth file"
- script: |
python -m twine upload --config-file .pypirc dist/*
displayName: "Upload to PyPI"
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))
- script: |
curl -X POST -d "token=$(TOKEN_RTD)" https://readthedocs.org/api/v2/webhook/pikepdf/39557/
displayName: "Trigger ReadTheDocs"
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/tags/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')))
+2 -2
View File
@@ -79,7 +79,7 @@ Copyright: held by the contributors to the Wikipedia article "Optical character
(epson.pdf generated from Wikipedia article as of 2016-09-14)
License: CC-BY-SA-3.0
Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf
Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf tests/resources/3small.pdf
Copyright: (C) 2005 Ellywa
License: GFDL-1.2+ or CC-BY-SA-1.0 or CC-BY-SA-2.0 or CC-BY-SA-2.5 or CC-BY-SA-3.0
@@ -87,7 +87,7 @@ Files: tests/resources/overlay.pdf
Copyright: (C) 2017 Max Anderson
License: Expat
Files: tests/resources/baiona*.png
Files: tests/resources/baiona*.png tests/resources/3small.pdf
Copyright: (C) 2014 Euskaldunaa
License: CC-BY-SA-4.0
+17 -9
View File
@@ -322,15 +322,23 @@ working files on a per page basis have the page number as a prefix
(starting with page 1), an infix indicates the processing stage, and a
suffix indicates the file type. Some important files include:
- ``.page.png`` - what the input page looks like
- ``.image`` - the image we will show the user if we are in a mode that
changes the final appearance; may be in one of several image formats
- ``.text.pdf`` - the OCR file; this will load as a blank page but
should have visible text if checked with a tool like pdftotext or
pdfminder.six
- ``.ocr.png`` - the file that is sent to Tesseract for OCR; depending
- ``_rasterize.png`` - what the input page looks like
- ``_ocr.png`` - the file that is sent to Tesseract for OCR; depending
on arguments this may differ from the presentation image
- ``layers.rendered.pdf`` - the composite PDF, before metadata repair
and optimization
- ``_pp_deskew.png`` - the image, after deskewing
- ``_pp_clean.png`` - the image, after cleaning with unpaper
- ``_ocr_tess.pdf`` - the OCR file; appears as a blank page with invisible
text embedded
- ``_ocr_tess.txt`` - the OCR text (not necessarily all text on the page,
if the page is mixed format)
- ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo
data structure
- ``graft_layers.pdf`` - the rendered PDF with OCR layers grafted on
- ``pdfa.pdf`` - ``graft_layers.pdf`` after conversion to PDF/A
- ``pdfa.ps`` - a PostScript file used by Ghostscript for PDF/A conversion
- ``optimize.pdf`` - the PDF generated before optimization
- ``optimize.out.pdf`` - the PDF generated by optimization
- ``origin`` - the input file
- ``origin.pdf`` - the input file or the input image converted to PDF
- ``images/*`` - images extracted during the optimization process; here
the prefix indicates a PDF object ID not a page number
+2 -1
View File
@@ -71,7 +71,8 @@ Progress monitoring
OCRmyPDF uses the ``tqdm`` package to implement its progress bars.
:func:`ocrmypdf.configure_logging` will set up logging output to
``sys.stderr`` in a way that is compatible with the display of the
progress bar.
progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable
the progress bar.
Exceptions
----------
+69 -12
View File
@@ -47,7 +47,7 @@ where the PDFs are stored):
.. code-block:: bash
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf-alpine '<container dir>/{}' '<container dir>/{}' \;
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf '<container dir>/{}' '<container dir>/{}' \;
This only runs one ``ocrmypdf`` process at a time. This variation uses
``find`` to create a directory list and ``parallel`` to parallelize runs
@@ -57,6 +57,12 @@ of ``ocrmypdf``, again updating files in place.
find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}'
In a Windows batch file, use
.. code-block:: bat
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
Sample script
-------------
@@ -67,13 +73,15 @@ processing.
#!/usr/bin/env python3
# Walk through directory tree, replacing all files with OCR'd version
# Contributed by DeliciousPickle@github
# Original version by DeliciousPickle@github; modified
import logging
import os
import subprocess
import sys
import ocrmypdf
script_dir = os.path.dirname(os.path.realpath(__file__))
print(script_dir + '/ocr-tree.py: Start')
@@ -91,6 +99,8 @@ processing.
level=logging.INFO, format='%(asctime)s %(message)s',
filename=log_file, filemode='w')
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
for dir_name, subdirs, file_list in os.walk(start_dir):
logging.info('\n')
logging.info(dir_name + '\n')
@@ -100,14 +110,10 @@ processing.
if file_ext == '.pdf':
full_path = dir_name + '/' + filename
print(full_path)
cmd = ["ocrmypdf", "--deskew", filename, filename]
logging.info(cmd)
proc = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout
if proc.returncode == 6:
result = ocrmypdf.ocr(filename, filename, deskew=True)
if result == ocrmypdf.ExitCode.already_done_ocr:
print("Skipped document because it already contained text")
elif proc.returncode == 0:
elif result == ocrmypdf.ExitCode.ok:
print("OCR complete")
logging.info(result)
@@ -192,6 +198,54 @@ and all inquiries are appreciated.
Hot (watched) folders
=====================
Watched folders with Docker
---------------------------
The OCRmyPDF Docker image includes a watcher service. This service can
be launched as follows:
.. code-block:: bash
docker run \
-v <path to files to convert>:/input \
-v <path to store results>:/output \
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
-it --entrypoint python3 \
jbarlow83/ocrmypdf \
watcher.py
This service will watch for a file that matches ``/input/\*.pdf`` and will
convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
.. csv-table:: watcher.py parameters for Docker
:header: "Parameter", "Description"
:widths: 50, 50
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "This will place files in the output in {output}/{year}/{month}/{filename}"
This service relies on polling to check for changes to the filesystem. It
may not be suitable for some environments, such as filesystems shared on a
slow network.
Watched folders with watcher.py
-------------------------------
The watcher service may also be run natively.
.. code-block:: bash
pip3 install -r reqs/watcher.txt
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
python3 watcher.py
Watched folders with CLI
------------------------
To set up a "hot folder" that will trigger OCR for every file inserted,
use a program like Python
`watchdog <https://pypi.python.org/pypi/watchdog>`__ (supports all major
@@ -219,12 +273,12 @@ told to run ``ocrmypdf`` on any .pdf added to the current directory
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
. # don't forget the final dot
For more complex behavior you can write a Python script around to use
the watchdog API.
On file servers, you could configure watchmedo as a system service so it
will run all the time.
For more complex behavior you can write a Python script around to use
the watchdog API. You can refer to the watcher.py script as an example.
Caveats
-------
@@ -244,6 +298,9 @@ Caveats
Alternatives
------------
- On Linux, `systemd user services <https://wiki.archlinux.org/index.php/Systemd/User>`__
can be configured to automatically perform OCR on a collection of files.
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
powerful alternative to ``watchmedo``.
+57
View File
@@ -0,0 +1,57 @@
=======================
Contributing guidelines
=======================
Contributions are welcome!
Big changes
===========
Please open a new issue to discuss or propose a major change. Not only is it fun
to discuss big ideas, but we might save each other's time too. Perhaps some of the
work you're contemplating is already half-done in a development branch.
Code style
==========
We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The
settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull
requests should follow the style guide. One difference we use from "black" style
is that strings shown to the user are always in double quotes (``"``) and strings
for internal uses are in single quotes (``'``).
Tests
=====
New features should come with tests that confirm their correctness.
New Python dependencies
=======================
If you are proposing a change that will require a new Python dependency, we
prefer dependencies that are already packaged by Debian or Red Hat. This makes
life much easier for our downstream package maintainers.
Python dependencies must also be GPLv3 compatible.
New non-Python dependencies
===========================
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
its functionality. In general we prefer to avoid adding new external programs.
Style guide: Is it OCRmyPDF or ocrmypdf?
========================================
The program/project is OCRmyPDF and the name of the executable or library is ocrmypdf.
Known ports/packagers
=====================
OCRmyPDF has been ported to many platforms already. If you are interesting in
porting to a new platform, check with
`Repology <https://repology.org/projects/?search=ocrmypdf>`__ to see the status
of that platform.
Packager maintainers, please ensure that the command line completion scripts in
``misc/`` are installed.
+11 -2
View File
@@ -165,8 +165,8 @@ might remove desirable content, especially from poor quality scans.
- ``--deskew`` will correct pages were scanned at a skewed angle by
rotating them back into place. Skew determination and correction is
performed using `Postl's variance of line
sums <http://www.leptonica.com/skew-measurement.html>`__ algorithm as
implemented in `Leptonica <http://www.leptonica.com/index.html>`__.
sums <http://www.leptonica.org/skew-measurement.html>`__ algorithm as
implemented in `Leptonica <http://www.leptonica.org/index.html>`__.
- ``--clean`` uses
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
pages before OCR, but does not alter the final output. This makes it
@@ -216,6 +216,15 @@ processing or PDF/A conversion.
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
Optimize images without performing OCR
--------------------------------------
You can also optimize all images without performing any OCR:
.. code-block:: bash
ocrmypdf --tesseract-timeout=0 --optimize 3 --skip-text input.pdf output.pdf
Redo existing OCR
=================
+24 -29
View File
@@ -22,21 +22,20 @@ Installing the Docker image
If you have `Docker <https://docs.docker.com/>`__ installed on your
system, you can install a Docker image of the latest release.
The recommended OCRmyPDF Docker image is currently named
``ocrmypdf-alpine``:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf-alpine
Follow the Docker installation instructions for your platform. If you
can run this command successfully, your system is ready to download and
If you can run this command successfully, your system is ready to download and
execute the image:
.. code-block:: bash
docker run hello-world
The recommended OCRmyPDF Docker image is currently named ``ocrmypdf``:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
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
@@ -51,6 +50,9 @@ CPUs:
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
See the Docker documentation for
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__.
Using the Docker image on the command line
==========================================
@@ -63,7 +65,7 @@ To start a Docker container (instance of the image):
.. code-block:: bash
docker tag jbarlow83/ocrmypdf-alpine ocrmypdf
docker tag jbarlow83/ocrmypdf ocrmypdf
docker run --rm -i ocrmypdf (... all other arguments here...)
For convenience, create a shell alias to hide the Docker command. It is
@@ -103,12 +105,12 @@ on the public one:
.. code-block:: dockerfile
FROM jbarlow83/ocrmypdf-alpine
FROM jbarlow83/ocrmypdf
# Add French
RUN apk add tesseract-ocr-data-fra
RUN apt install tesseract-ocr-fra
You can also copy training data to ``/usr/share/tessdata``.
You can also copy training data to ``/usr/share/tesseract-ocr/<tesseract version>/tessdata``.
Executing the test suite
========================
@@ -117,17 +119,16 @@ The OCRmyPDF test suite is installed with image. To run it:
.. code-block:: bash
docker run --entrypoint python3 jbarlow83/ocrmypdf-alpine setup.py test
docker run --entrypoint python3 jbarlow83/ocrmypdf -m pytest
Accessing the shell
===================
``bash`` is not installed in the image. To use the busybox shell in the
Docker image:
To use the bash shell in the Docker image:
.. code-block:: bash
docker run -it --entrypoint busybox jbarlow83/ocrmypdf-alpine sh
docker run -it --entrypoint bash jbarlow83/ocrmypdf
Using the OCRmyPDF web service wrapper
======================================
@@ -137,7 +138,12 @@ service. The webservice may be launched as follows:
.. code-block:: bash
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf-alpine webservice.py
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf webservice.py
This will configure the machine to listen on port 5000. On Linux machines
this is port 5000 of localhost. On macOS or Windows machines running
Docker, this is port 5000 of the virtual machine that runs your Docker
images. You can find its IP address using the command ``docker-machine ip``.
Unlike command line usage this program will open a socket and wait for
connections.
@@ -162,14 +168,3 @@ also licensed in this way.
In addition to the above, please read our
:ref:`general remarks on using OCRmyPDF as a service <ocr-service>`.
Ubuntu-based Docker image
=========================
A Ubuntu-based OCRmyPDF image is also available. The main advantage this
image offers is that it supports manylinux Python wheels (which are not
supported on Alpine Linux). This may be useful for plugins.
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
+10 -2
View File
@@ -4,7 +4,9 @@ OCRmyPDF documentation
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched.
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.
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
@@ -12,6 +14,7 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat
introduction
release_notes
installation
optimizer
languages
jbig2
@@ -22,11 +25,16 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat
cookbook
docker
advanced
api
batch
security
errors
.. toctree::
:caption: Developers
:maxdepth: 2
api
contributing
Indices and tables
==================
+128 -63
View File
@@ -8,11 +8,20 @@ Installing OCRmyPDF
|latest|
The easiest way to install OCRmyPDF is to follow the steps for your operating
system/platform, although sometimes this version may be out of date.
system/platform, although sometimes this version may be out of date. This
installation guide provides information allowing you to compare the current
version to the one provided by your platform.
If you want to use the latest version of OCRmyPDF, your best bet is to install
the most recent version your platform provides, and then upgrade that version by
installing the Python binary wheels.
If you want to use the latest version of OCRmyPDF and all of its optional
dependencies, the easiest way to get that is install the Homebrew package. Homebrew
is best known as a macOS package manger, but also works for
`Linux and Windows Subsystem for Linux <https://docs.brew.sh/Homebrew-on-Linux>`__.
After Homebrew is installed, simply run ``brew install ocrmypdf``.
You can also use the more detailed procedures here to manually install OCRmyPDF
from source or with the ``pip`` package manager for binary wheels. The reason
for these varied steps is that OCRmyPDF requires third-party executables that are
not part of Python.
.. contents:: Platform-specific steps
:depth: 2
@@ -21,7 +30,7 @@ installing the Python binary wheels.
Installing on Linux
===================
Debian and Ubuntu 16.10 or newer
Debian and Ubuntu 18.04 or newer
--------------------------------
.. |deb-stable| image:: https://repology.org/badge/version-for-repo/debian_stable/ocrmypdf.svg
@@ -33,28 +42,30 @@ Debian and Ubuntu 16.10 or newer
.. |deb-unstable| image:: https://repology.org/badge/version-for-repo/debian_unstable/ocrmypdf.svg
:alt: Debian unstable
.. |ubu-1710| image:: https://repology.org/badge/version-for-repo/ubuntu_17_10/ocrmypdf.svg
:alt: Ubuntu 17.10
.. |ubu-1804| image:: https://repology.org/badge/version-for-repo/ubuntu_18_04/ocrmypdf.svg
:alt: Ubuntu 18.04 LTS
.. |ubu-1810| image:: https://repology.org/badge/version-for-repo/ubuntu_18_10/ocrmypdf.svg
:alt: Ubuntu 18.10
.. |ubu-1904| image:: https://repology.org/badge/version-for-repo/ubuntu_19_04/ocrmypdf.svg
:alt: Ubuntu 19.04
+-------------------------------------------+
| **OCRmyPDF versions in Debian & Ubuntu** |
+-------------------------------------------+
| |latest| |
+-------------------------------------------+
| |deb-stable| |deb-testing| |deb-unstable| |
+-------------------------------------------+
| |ubu-1710| |ubu-1804| |ubu-1810| |
+-------------------------------------------+
.. |ubu-1910| image:: https://repology.org/badge/version-for-repo/ubuntu_19_10/ocrmypdf.svg
:alt: Ubuntu 19.10
Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may
simply
+-----------------------------------------------+
| **OCRmyPDF versions in Debian & Ubuntu** |
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |deb-stable| |deb-testing| |deb-unstable| |
+-----------------------------------------------+
| |ubu-1804| |ubu-1810| |ubu-1904| |ubu-1910| |
+-----------------------------------------------+
Users of Debian 9 ("stretch") or later or Ubuntu 18.04 or later, including users
of Windows Subsystem for Linux, may simply
.. code-block:: bash
@@ -64,7 +75,8 @@ As indicated in the table above, Debian and Ubuntu releases may lag
behind the latest version. If the version available for your platform is
out of date, you could opt to install the latest version from source.
See `Installing HEAD revision from
sources <#installing-head-revision-from-sources>`__.
sources <#installing-head-revision-from-sources>`__. Ubuntu 16.10 to 17.10
inclusive also had ocrmypdf, but these versions are end of life.
For full details on version availability for your platform, check the
`Debian Package Tracker <https://tracker.debian.org/pkg/ocrmypdf>`__ or
@@ -81,20 +93,22 @@ For full details on version availability for your platform, check the
Fedora 29 or newer
------------------
.. |fedora-29| image:: https://repology.org/badge/version-for-repo/fedora29/ocrmypdf.svg
.. |fedora-29| image:: https://repology.org/badge/version-for-repo/fedora_29/ocrmypdf.svg
:alt: Fedora 29
.. |fedora-30| image:: https://repology.org/badge/version-for-repo/fedora_30/ocrmypdf.svg
:alt: Fedora 30
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
+------------------------------+
| **OCRmyPDF version** |
+------------------------------+
| |latest| |
+------------------------------+
| |fedora-29| |fedora-rawhide| |
+------------------------------+
+-----------------------------------------------+
| **OCRmyPDF version** |
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |fedora-29| |fedora-30| |fedora-rawhide| |
+-----------------------------------------------+
Users of Fedora 29 later may simply
@@ -129,8 +143,7 @@ first install the system version to get most of the dependencies:
sudo apt-get update
sudo apt-get install \
ocrmypdf \
python3-pip
ocrmypdf
There are a few system dependency changes since ocrmypdf 6.1.2. Let's
get these, too.
@@ -141,13 +154,19 @@ get these, too.
libxml2 \
pngquant
We will need a newer version of ``pip`` then was available for Ubuntu 18.04:
.. code-block:: bash
wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py
Then install the most recent ocrmypdf for the local user and set the
user's ``PATH`` to check for the user's Python packages.
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
python3 -m pip install --user ocrmypdf
To add JBIG2 encoding, see :ref:`jbig2`.
@@ -267,15 +286,11 @@ Now we need to install ``pip`` and let it install ocrmypdf:
curl https://bootstrap.pypa.io/ez_setup.py -o - | python3.6 && python3.6 -m easy_install pip
pip3.6 install ocrmypdf
These installation instructions omit the optional dependency
``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04.
The author could not find a backport of ``unpaper``, and created a .deb
package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
wget -q 'https://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
The optional dependency ``unpaper`` is only available at 0.4.2 in Ubuntu 14.04,
and no backports are available. Previously the author maintained a backported
.deb package for unpaper 6.1, but since Ubuntu 14.04 is now end of life, this is
not supported. As such, ``unpaper`` is not available on Ubuntu 14.04 or must by
compiled by hand.
To add JBIG2 encoding, see :ref:`jbig2`.
@@ -297,6 +312,19 @@ the following command.
If you have any difficulties with installation, check the repository
package page.
Alpine Linux
------------
.. image:: https://repology.org/badge/version-for-repo/alpine_edge/ocrmypdf.svg
:alt: Alpine Linux
:target: https://repology.org/metapackage/ocrmypdf
To install OCRmyPDF for Alpine Linux:
.. code-block:: bash
apk add ocrmypdf
Other Linux packages
--------------------
@@ -374,7 +402,7 @@ packs. If you need other languages you can optionally install them all:
.. code-block:: bash
brew install tesseract --with-all-languages # Option 2: for all language packs
brew install tesseract-lang # Option 2: for all language packs
Update the homebrew pip:
@@ -403,29 +431,75 @@ The command line program should now be available:
Installing on FreeBSD
=====================
FreeBSD 11.2 is known to work. Other versions likely work but have not
been tested.
.. image:: https://repology.org/badge/version-for-repo/freebsd/python:ocrmypdf.svg
:alt: FreeBSD
:target: https://repology.org/project/python:ocrmypdf/versions
In general it should work to:
FreeBSD 11.2, 11.3, 12.0-RELEASE and 13.0-CURRENT are supported. Other
versions likely work but have not been tested.
#. `Install and build
pikepdf <https://pikepdf.readthedocs.io/en/latest/installation.html#installing-on-freebsd-11-2>`__.
#. Install the equivalent list of dependencies for Linux.
.. code-block:: bash
pkg install py36-ocrmypdf
To install a more recent version, you could attempt to first install the system
version with ``pkg``, then use ``pip install --user ocrmypdf``.
Installing the Docker image
===========================
For some users, installing the Docker image will be easier than
installing all of OCRmyPDF's dependencies. For Windows, it is the only
option.
installing all of OCRmyPDF's dependencies.
See `OCRmyPDF Docker Image <docker>`__ for more information.
Installing on Windows
=====================
Direct installation on Windows is not currently possible, but it works well in
Windows Subsystem for Linux:
.. warning::
Native Windows support is new. Consider it "beta" software. Some
functionality is missing or may be more difficult to enable. If you need a
production-ready solution, use Windows Subsystem for Linux or a Docker
image.
.. note::
Administrator privileges will be required for some of these steps.
You must install the following for Windows:
* Python 3.7 (64-bit)
* Tesseract 4.0 or later
* Ghostscript 9.50 or later
You can install these with the Chocolatey package manager:
* ``choco install python3``
* ``choco install --pre tesseract``
* ``choco install ghostscript``
Also consider adding:
* ``choco install pngquant``
Windows 10 64-bit and 64-bit versions of applications are recommended. Earlier
versions of Windows and 32-bit versions of these programs are not tested, and not
supported at this time.
OCRmyPDF will check for Tesseract-OCR and Ghostscript in your Program Files folder.
If they are in some other location, you may need to modify the ``PATH``
environment variable so Tesseract, Ghostscript, and other any optional executables can
be found. You can enter it in the command line or
`follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
to make the change persistent and system-wide.
You may then use pip to install ocrmypdf:
* ``pip install ocrmypdf``
Installing on Windows Subsystem for Linux
=========================================
#. Install Ubuntu 18.04 for Windows Subsystem for Linux, if not already installed.
#. Follow the procedure to install :ref:`OCRmyPDF on Ubuntu 18.04 <ubuntu-lts-latest>`.
@@ -444,15 +518,6 @@ Then confirm that the expected version from PyPI (|latest|) is installed:
You can then run OCRmyPDF in the Windows command prompt or Powershell, prefixing
``wsl``, and call it from Windows programs or batch files.
Why no native Windows?
^^^^^^^^^^^^^^^^^^^^^^
It would probably not be too difficult to port on Windows. The main
reason this has been avoided is the difficulty of packaging and
installing the various non-Python dependencies: Tesseract, QPDF,
Ghostscript, Leptonica. Pull requests to add or improve Windows support
would be quite welcome.
Docker
^^^^^^
@@ -503,11 +568,11 @@ manager. ``pip`` cannot provide them.
- Python 3.6 or newer
- Ghostscript 9.15 or newer
- qpdf 8.1.0 or newer
- Tesseract 4.0.0-alpha or newer
- Tesseract 4.0.0-beta or newer
As of ocrmypdf 7.2.1, the following versions are recommended:
- Python 3.7
- Python 3.7 or 3.8
- Ghostscript 9.23 or newer
- qpdf 8.2.1
- Tesseract 4.0.0 or newer
+2 -2
View File
@@ -2,7 +2,7 @@
Introduction
============
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
OCRmyPDF is a Python 3 application and library that adds OCR layers to PDFs.
About OCR
=========
@@ -208,7 +208,7 @@ consider one of these similar open source programs:
Web front-ends
==============
The Docker image ``ocrmypdf-alpine`` provides a web service front-end
The Docker image ``ocrmypdf`` provides a web service front-end
that allows files to submitted over HTTP and the results "downloaded".
This is an HTTP server intended to simplify web services deployments; it
is not intended to be deployed on the public internet and no real
+5
View File
@@ -57,3 +57,8 @@ Docker users
Users of the OCRmyPDF Docker image should install language packs into a
derived Docker image as
:ref:`described in that section <docker-lang-packs>`.
Windows users
=============
The Tesseract installer provided by Chocolatey already includes 100 languages.
+75
View File
@@ -0,0 +1,75 @@
================
PDF optimization
================
OCRmyPDF includes an image-oriented PDF optimizer. By default, the optimizer
runs with safe settings with the goal of improving compression at no loss of
quality. At higher optimization levels, lossy optimizations may be applied and
tuned. Optimization occurs after OCR, and only if OCR succeeded. It does not
perform other possible optimizations such as deduplicating resources,
consolidating fonts, simplifying vector drawings, or anything of that nature.
Optimization ranges from ``-O0`` through ``-O3``, where ``0`` disables
optimization and ``3`` implements all options. ``1``, the default, performs only
safe and lossless optimizations. (This is similar to GCC's optimization
parameter.) The exact type of optimizations performed will vary over time.
PDF optimization requires third-party, optional tools for certain optimizations.
If these are not installed or cannot be found by OCRmyPDF, optimization will not
be as good.
Optimizations that always occurs
================================
OCRmyPDF will automatically replace obsolete or inferior compression schemes
such as RLE or LZW with superior schemes such as Deflate and converting
monochrome images to CCITT G4. Since this is harmless it always occurs and there
is no way to disable it. Other non-image compressed objects are compressed as
well.
Fast web view
=============
OCRmyPDF automatically optimizes PDFs for "fast web view" in Adobe Acrobat's
parlance, or equivalently, linearizes PDFs so that the resources they reference
are presented in the order a viewer needs them for sequential display. This
reduces the latency of viewing a PDF both online and from local storage. This
actually slightly increases the file size.
To disable this optimization and all others, use ``ocrmypdf --optimize 0 ...``
or the shorthand ``-O0``.
Lossless optimizations
======================
At optimization level ``-O1`` (the default), OCRmyPDF will also attempt lossless
image optimization.
If a JBIG2 encoder is available, then monochrome images will be converted to
JBIG2, with the potential for huge savings on large black and white images,
since JBIG2 is far more efficient than any other monochrome (bi-level)
compression. (All known US patents related to JBIG2 have probably expired, but
it remains the responsibility of the user to supply a JBIG2 encoder such as
`jbig2enc <https://github.com/agl/jbig2enc>`__. OCRmyPDF does not implement
JBIG2 encoding on its own.)
OCRmyPDF currently does not attempt to recompress losslessly compressed objects
more aggressively.
Lossy optimizations
===================
At optimization level ``-O2`` and ``-O3``, OCRmyPDF will some attempt lossy
image optimization.
If ``pngquant`` is installed, OCRmyPDF will use it to perform quantize paletted
images to reduce their size.
The quality of JPEGs may be lowered, on the assumption that a lower quality
image may be suitable for storage after OCR.
It is not possible to optimize all image types. Uncommon image types may be
skipped by the optimizer.
OCRmyPDF provides :ref:`lossy mode JBIG2 <jbig2-lossy>` as an advanced feature
that additional requires the argument ``--jbig2-lossy``.
+108
View File
@@ -13,6 +13,112 @@ Note that it is licensed under GPLv3, so scripts that
``import ocrmypdf`` and are released publicly should probably also be
licensed under GPLv3.
v9.3.0
======
- Improved native Windows support: we now check in the obvious places in
the "Program Files" folders installations of Tesseract and Ghostscript,
rather than relying on the user to edit ``PATH`` to specify their location.
The ``PATH`` environment variable can still be used to differentiate when
multiple installations are present or the programs are installed to non-
standard locations.
- Fixed an exception on parsing Ghostscript error messages.
- Added an improved example demonstrating how to set up a watched folder
for automated OCR processing (thanks to @ianalexander for the contribution).
v9.2.0
======
- Native Windows is now supported.
- Continuous integration moved to Azure Pipelines.
- Improved test coverage and speed of tests.
- Fixed an issue where a page that was originally a JPEG would be saved as a
PNG, increasing file size. This occurred only when a preprocessing option
was selected along with ``--output-type=pdf`` and all images on the original
page were JPEGs. Regression since v7.0.0.
- OCRmyPDF no longer depends on the QPDF executable ``qpdf`` or ``libqpdf``.
It uses pikepdf (which in turn depends on ``libqpdf``). Package maintainers
should adjust dependencies so that OCRmyPDF no longer calls for libqpdf on
its own. For users of Python binary wheels, this change means a separate
installation of QPDF is no longer necessary. This change is mainly to
simplify installation on Windows.
- Fixed a rare case where log messages from Tesseract would be discarded.
- Fixed incorrect function signature for pixFindPageForeground, causing
exceptions on certain platforms/Leptonica versions.
v9.1.1
======
- Expand the range of pdfminer.six versions that are supported.
- Fixed Docker build when using pikepdf 1.7.0.
- Fixed documentation to recommend using pip from get-pip.py.
v9.1.0
======
- Improved diagnostics when file size increases at output. Now warns if JBIG2
or pngquant were not available.
- pikepdf 1.7.0 is now required, to pick up changes that remove the need for
a source install on Linux systems running Python 3.8.
v9.0.5
======
- The Alpine Docker image (jbarlow83/ocrmypdf-alpine) has been dropped due to
the difficulties of supporting Alpine Linux.
- The primary Docker image (jbarlow83/ocrmypdf) has been improved to take on
the extra features that used to be exclusive to the Alpine image.
- No changes to application code.
- pdfminer.six version 20191020 is now supported.
v9.0.4
======
- Fixed compatibility with Python 3.8 (but requires source install for the moment).
- Fixed Tesseract settings for ``--user-words`` and ``--user-patterns``.
- Changed to pikepdf 1.6.5 (for Python 3.8).
- Changed to Pillow 6.2.0 (to mitigate a security vulnerability in earlier Pillow).
- A debug message now mentions when English is automatically selected if the locale
is not English.
v9.0.3
======
- Embed an encoded version of the sRGB ICC profile in the intermediate
Postscript file (used for PDF/A conversion). Previously we included the
filename, which required Postscript to run with file access enabled. For
security, Ghostscript 9.28 enables ``-dSAFER`` and as such, no longer
permits access to any file by default. This fix is necessary for
compatibility with Ghostscript 9.28.
- Exclude a test that sometimes times out and fails in continuous integration
from the standard test suite.
v9.0.2
======
- The image optimizer now skips optimizing flate (PNG) encoded images in some
situations where the optimization effort was likely wasted.
- The image optimizer now ignores images that specify arbitrary decode arrays,
since these are rare.
- Fixed an issue that caused inversion of black and white in monochrome images.
We are not certain but the problem seems to be linked to Leptonica 1.76.0 and
older.
- Fixed some cases where the test suite failed if
English or German Tesseract language packs were not installed.
- Fixed a runtime error if the Tesseract English language is not installed.
- Improved explicit closing of Pillow images after use.
- Actually fixed of Alpine Docker image build.
- Changed to pikepdf 1.6.3.
v9.0.1
======
- Fixed test suite failing when either of optional dependencies unpaper and
pngquant were missing.
- Attempted fix of Alpine Docker image build.
- Documented that FreeBSD ports are now available.
- Changed to pikepdf 1.6.1.
v9.0.0
======
@@ -59,6 +165,8 @@ v9.0.0
- Updated Docker images to use newer versions.
- Fixed images encoded as JBIG2 with a colorspace other than ``/DeviceGray``
were not interpreted correctly.
- Fixed a OCR text-image registration (i.e. alignment) problem when the page
when MediaBox had a nonzero corner.
v8.3.2
======
+3 -2
View File
@@ -58,7 +58,7 @@ _ocrmypdf()
COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) )
return
;;
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages)
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages|--fast-web-view)
# argument required but no completions available
return
;;
@@ -76,7 +76,8 @@ _ocrmypdf()
--max-image-mpixels --tesseract-config --tesseract-pagesegmode
--help --tesseract-oem --pdf-renderer --tesseract-timeout
--rotate-pages-threshold --pdfa-image-compression --user-words
--user-patterns --keep-temporary-files --output-type' \
--user-patterns --keep-temporary-files --output-type
--no-progress-bar --pages --fast-web-view' \
-- "$cur" ) )
return
else
+3
View File
@@ -59,6 +59,8 @@ function __fish_ocrmypdf_verbose
end
complete -c ocrmypdf -x -s v -l verbose -a '(__fish_ocrmypdf_verbose)' -d "set verbosity level"
complete -c ocrmypdf -x -l no-progress-bar -d "disable the progress bar"
function __fish_ocrmypdf_pdfa_compression
echo -e "auto\t"(_ "let Ghostscript decide how to compress images")
echo -e "jpeg\t"(_ "convert color and grayscale images to JPEG")
@@ -111,5 +113,6 @@ complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence"
complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
+70
View File
@@ -0,0 +1,70 @@
# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import time
from datetime import datetime
from pathlib import Path
from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
import ocrmypdf
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', False))
PATTERNS = ['*.pdf']
def execute_ocrmypdf(file_path):
filename = Path(file_path).name
if OUTPUT_DIRECTORY_YEAR_MONTH:
today = datetime.today()
output_directory_year_month = Path(
f'{OUTPUT_DIRECTORY}/{today.year}/{today.month}'
)
if not output_directory_year_month.exists():
output_directory_year_month.mkdir(parents=True, exist_ok=True)
output_path = Path(output_directory_year_month) / filename
else:
output_path = Path(OUTPUT_DIRECTORY) / filename
print(f'New file: {file_path}.\nAttempting to OCRmyPDF to: {output_path}')
ocrmypdf.ocr(file_path, output_path)
class HandleObserverEvent(PatternMatchingEventHandler):
def on_any_event(self, event):
if event.event_type in ['created', 'modified']:
execute_ocrmypdf(event.src_path)
if __name__ == "__main__":
print(
f"Starting OCRmyPDF watcher with config:\n"
f"Input Directory: {INPUT_DIRECTORY}\n"
f"Output Directory: {OUTPUT_DIRECTORY}\n"
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}"
)
handler = HandleObserverEvent(patterns=PATTERNS)
observer = Observer()
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
+3 -1
View File
@@ -10,7 +10,8 @@ build-backend = "setuptools.build_meta"
[tool.black]
line-length = 88
py36 = true
target-version = ["py36",
"py37", "py38"]
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
@@ -28,5 +29,6 @@ exclude = '''
| docs
| misc
| \.egg-info
| src/ocrmypdf/lib/_leptonica.py
)/
'''
-2
View File
@@ -1,4 +1,2 @@
check-manifest >= 0.35
twine >= 1.8.1
coverage >= 4.5
GitPython == 2.1.3
+6 -9
View File
@@ -1,13 +1,10 @@
# requirements.txt can be used to replicate the developer's build environment
# setup.py lists a separate set of requirements that are looser to simplify
# installation
chardet == 3.0.4
cffi == 1.12.2
cffi == 1.13.2
img2pdf == 0.3.3
pdfminer.six == 20181108
pikepdf == 1.6.0
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.19
python-xmp-toolkit == 2.0.1
reportlab == 3.5.13
tqdm == 4.32.1
pdfminer.six == 20191110
pikepdf == 1.8.1
Pillow >= 6.2.0
reportlab == 3.5.32
tqdm == 4.37.0
+3 -3
View File
@@ -1,8 +1,8 @@
pytest >= 4.4.1, < 5
pytest >= 5.0.0
pytest-helpers-namespace >= 2019.1.8
pytest-xdist == 1.28.0
pytest-xdist >= 1.29.0 # For DumpError fix
pytest-cov >= 2.6.1
python-xmp-toolkit # requires apt-get install libexempi3
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
# or brew install exempi
PyPDF2 >= 1.26.0
#PyMuPDF == 1.13.4 # optional
+1
View File
@@ -0,0 +1 @@
watchdog >= 0.8.2, < 1.0
+1
View File
@@ -0,0 +1 @@
Flask >= 1, < 2
+2
View File
@@ -22,6 +22,8 @@ include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
known_first_party = ocrmypdf
known_third_party = PIL,PyPDF2,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
[metadata]
license_file = LICENSE
+5 -5
View File
@@ -68,6 +68,7 @@ setup(
classifiers=[
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
@@ -75,6 +76,7 @@ setup(
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows :: Windows 10",
"Operating System :: POSIX",
"Operating System :: POSIX :: BSD",
"Operating System :: POSIX :: Linux",
@@ -95,11 +97,9 @@ setup(
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
'pdfminer.six == 20181108',
'pikepdf >= 1.6.0, < 2',
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
'pdfminer.six >= 20181108, <= 20191110',
'pikepdf >= 1.8.1, < 2',
'Pillow >= 6.2.0',
'reportlab >= 3.3.0', # oldest released version with sane image handling
'tqdm >= 4',
],
+1 -1
View File
@@ -21,7 +21,7 @@ from pathlib import Path
import pikepdf
MAX_REPLACE_PAGES = int(os.environ.get('_OCRMYPDF_MAX_REPLACE_PAGES', 100))
MAX_REPLACE_PAGES = 100
def _update_page_resources(*, page, font, font_key, procset):
+4 -4
View File
@@ -100,14 +100,14 @@ def cleanup_working_files(work_folder, options):
class LogNameAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
# return '[%s] %s' % (self.extra['filename'], msg), kwargs
# return '[%s] %s' % (self.extra['input_filename'], msg), kwargs
return '%s' % (msg,), kwargs
class LogNamePageAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
return (
#'[%s:%05u] %s' % (self.extra['filename'], self.extra['page'], msg),
#'[%s:%05u] %s' % (self.extra['input_filename'], self.extra['page'], msg),
'%4u: %s' % (self.extra['page'], msg),
kwargs,
)
@@ -116,9 +116,9 @@ class LogNamePageAdapter(logging.LoggerAdapter):
def make_logger(options=None, prefix='ocrmypdf', filename=None, page=None):
log = logging.getLogger(prefix)
if filename and page:
adapter = LogNamePageAdapter(log, dict(filename=filename, page=page))
adapter = LogNamePageAdapter(log, dict(input_filename=filename, page=page))
elif filename:
adapter = LogNameAdapter(log, dict(filename=filename))
adapter = LogNameAdapter(log, dict(input_filename=filename))
else:
adapter = log
return adapter
+11 -12
View File
@@ -37,26 +37,26 @@ from .exceptions import (
UnsupportedImageFormatError,
)
from .exec import ghostscript, tesseract
from .helpers import re_symlink
from .helpers import safe_symlink
from .hocrtransform import HocrTransform
from .optimize import optimize
from .pdfa import generate_pdfa_ps
from .pdfinfo import Colorspace, PdfInfo
from .pdfinfo import Colorspace, Encoding, PdfInfo
VECTOR_PAGE_DPI = 400
def triage_image_file(input_file, output_file, options, log):
log.info("Input file is not a PDF, checking if it is an image...")
try:
log.info("Input file is not a PDF, checking if it is an image...")
im = Image.open(input_file)
except EnvironmentError as e:
# Recover the original filename
log.error(str(e).replace(input_file, options.input_file))
raise UnsupportedImageFormatError() from e
else:
log.info("Input file is an image")
with im:
log.info("Input file is an image")
if 'dpi' in im.info:
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
log.info("Image size: (%d, %d)" % im.size)
@@ -85,11 +85,10 @@ def triage_image_file(input_file, output_file, options, log):
if 'iccprofile' not in im.info:
if im.mode == 'RGB':
log.info('Input image has no ICC profile, assuming sRGB')
log.info("Input image has no ICC profile, assuming sRGB")
elif im.mode == 'CMYK':
log.info('Input CMYK image has no ICC profile, not usable')
log.error("Input CMYK image has no ICC profile, not usable")
raise UnsupportedImageFormatError()
im.close()
try:
log.info("Image seems valid. Try converting to PDF...")
@@ -129,11 +128,11 @@ def triage(input_file, output_file, options, log):
if _pdf_guess_version(input_file):
if options.image_dpi:
log.warning(
"Argument --image-dpi ignored because the "
"Argument --image-dpi is being ignored because the "
"input file is a PDF, not an image."
)
# Origin file is a pdf create a symlink with pdf extension
re_symlink(input_file, output_file)
safe_symlink(input_file, output_file)
return output_file
except EnvironmentError as e:
log.error(e)
@@ -558,7 +557,7 @@ def ocr_tesseract_hocr(input_file, page_context):
def should_visible_page_image_use_jpg(pageinfo):
# If all images were JPEGs originally, produce a JPEG as output
return pageinfo.images and all(im.enc == 'jpeg' for im in pageinfo.images)
return pageinfo.images and all(im.enc == Encoding.jpeg for im in pageinfo.images)
def create_visible_page_jpg(image, page_context):
@@ -702,7 +701,7 @@ def convert_to_pdfa(input_pdf, input_ps_stub, context):
if modified:
pdf_file.save(fix_docinfo_file)
else:
os.symlink(input_pdf, fix_docinfo_file)
safe_symlink(input_pdf, fix_docinfo_file)
ghostscript.generate_pdfa(
pdf_version=input_pdfinfo.min_version,
+28 -12
View File
@@ -25,6 +25,7 @@ import threading
from collections import namedtuple
from tempfile import mkdtemp
import PIL
from tqdm import tqdm
from ._graft import OcrGrafter
@@ -176,7 +177,7 @@ def post_process(pdf_file, context):
return optimize_pdf(pdf_out, context)
def worker_init(queue):
def worker_init(queue, max_pixels):
"""Initialize a process pool worker"""
# Ignore SIGINT (our parent process will kill us gracefully)
@@ -188,9 +189,15 @@ def worker_init(queue):
root.handlers = []
root.addHandler(h)
# In Windows, child process will not inherit our change to this value in
# the parent process, so ensure workers get it set
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
def worker_thread_init(queue):
pass
def worker_thread_init(_queue, max_pixels):
# This is probably not needed since threads should all see the same memory,
# but done for consistency.
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
def log_listener(queue):
@@ -225,15 +232,15 @@ def exec_concurrent(context):
if max_workers > 1:
context.log.info("Start processing %d pages concurrent", max_workers)
# Tesseract 4.0 is multithreaded, and we also run multiple workers. We want to
# avoid the situation where we end up trying to run NxN jobs on N CPU cores,
# as that gives poor performance. Performance testing shows we're better off
# Tesseract 4.x can be multithreaded, and we also run multiple workers. We want
# to manage how many threads it uses to avoid creating total threads than cores.
# Performance testing shows we're better off
# parallelizing ocrmypdf and forcing Tesseract to be single threaded, which we
# get by setting the envvar OMP_THREAD_LIMIT to 1. But if the page count of the
# input file is small, then we allow Tesseract to use threads, subject to the
# constraint: (ocrmypdf workers) * (tesseract threads) <= max_workers and limiting
# Tesseract to 4 threads.
tess_threads = min(4, context.options.jobs // max_workers)
# constraint: (ocrmypdf workers) * (tesseract threads) <= max_workers.
# As of Tesseract 4.1, 3 threads is the most effective on a 4 core/8 thread system.
tess_threads = min(3, context.options.jobs // max_workers)
if context.options.tesseract_env is None:
context.options.tesseract_env = os.environ.copy()
context.options.tesseract_env.setdefault('OMP_THREAD_LIMIT', str(tess_threads))
@@ -261,7 +268,9 @@ def exec_concurrent(context):
unit_scale=0.5,
disable=not context.options.progress_bar,
) as pbar, Pool(
processes=max_workers, initializer=initializer, initargs=(log_queue,)
processes=max_workers,
initializer=initializer,
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
) as pool:
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
while True:
@@ -301,7 +310,14 @@ def exec_concurrent(context):
class NeverRaise(Exception):
"""An exception that is never raised"""
pass
pass # pylint: disable=unnecessary-pass
def samefile(f1, f2):
if os.name == 'nt':
return f1 == f2
else:
return os.path.samefile(f1, f2)
def run_pipeline(options, api=False):
@@ -339,7 +355,7 @@ def run_pipeline(options, api=False):
if options.output_file == '-':
log.info("Output sent to stdout")
elif os.path.samefile(options.output_file, os.devnull):
elif samefile(options.output_file, os.devnull):
pass # Say nothing when sending to dev null
else:
if options.output_type.startswith('pdfa'):
+34 -5
View File
@@ -17,6 +17,7 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import locale
import logging
import os
import sys
@@ -41,12 +42,13 @@ from .exec import (
tesseract,
unpaper,
)
from .helpers import is_file_writable, is_iterable_notstr, monotonic, re_symlink
from .helpers import is_file_writable, is_iterable_notstr, monotonic, safe_symlink
# -------------
# External dependencies
HOCR_OK_LANGS = frozenset(['eng', 'deu', 'spa', 'ita', 'por'])
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
log = logging.getLogger(__name__)
@@ -56,9 +58,21 @@ log = logging.getLogger(__name__)
verify_python3_env()
def check_platform():
if os.name == 'nt' and sys.maxsize <= 2 ** 32: # pragma: no cover
# 32-bit interpreter on Windows
log.error(
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
"Please use a 64-bit (x86-64) version of Python."
)
def check_options_languages(options):
if not options.language:
options.language = ['eng'] # Enforce English hegemony
options.language = [DEFAULT_LANGUAGE]
system_lang = locale.getlocale()[0]
if system_lang and not system_lang.startswith('en'):
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
# Support v2.x "eng+deu" language syntax
if '+' in options.language[0]:
@@ -107,7 +121,7 @@ def check_options_output(options):
options.pdf_renderer = 'sandwich'
if options.pdf_renderer == 'sandwich' and not tesseract.has_textonly_pdf(
options.tesseract_env
options.tesseract_env, languages
):
raise MissingDependencyError(
"You are using an alpha version of Tesseract 4.0 that does not support "
@@ -287,6 +301,7 @@ def check_options_pillow(options):
def check_options(options):
check_platform()
check_options_languages(options)
check_options_metadata(options)
check_options_output(options)
@@ -369,7 +384,7 @@ def create_input_file(options, work_folder):
else:
try:
target = os.path.join(work_folder, 'origin')
re_symlink(options.input_file, target)
safe_symlink(options.input_file, target)
return target
except FileNotFoundError:
raise InputFileError(f"File not found - {options.input_file}")
@@ -413,6 +428,20 @@ def report_output_file_size(options, input_file, output_file):
f"The argument --{arg.replace('_', '-')} was issued, causing transcoding."
)
if options.optimize == 0:
reasons.append("Optimization was disabled.")
else:
image_optimizers = {
'jbig2': jbig2enc.available(),
'pngquant': pngquant.available(),
}
for name, available in image_optimizers.items():
if not available:
reasons.append(
f"The optional dependency '{name}' was not found, so some image "
f"optimizations could not be attempted."
)
if reasons:
explanation = "Possible reasons for this include:\n" + '\n'.join(reasons) + "\n"
else:
@@ -427,7 +456,7 @@ def report_output_file_size(options, input_file, output_file):
def check_dependency_versions(options):
check_external_program(
program='tesseract',
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'},
package={'linux': 'tesseract-ocr'},
version_checker=tesseract.version,
need_version='4.0.0', # using backport for Travis CI
)
+58 -46
View File
@@ -18,8 +18,10 @@
import logging
import os
import sys
import warnings
from enum import IntEnum
from pathlib import Path
from typing import Dict, List, Optional
from tqdm import tqdm
@@ -33,7 +35,7 @@ class TqdmConsole:
def __init__(self, file):
self.file = file
self.py36 = sys.version_info >= (3, 6)
self.py36 = sys.version_info[0:2] == (3, 6)
def write(self, msg):
# When no progress bar is active, tqdm.write() routes to print()
@@ -122,13 +124,23 @@ def create_options(*, input_file, output_file, **kwargs):
for arg, val in kwargs.items():
if val is None:
continue
if arg == 'tesseract_env':
# These arguments with special handling for which we bypass
# argparse
if arg in {'tesseract_env', 'progress_bar'}:
deferred.append((arg, val))
continue
cmd_style_arg = arg.replace('_', '-')
cmdline.append(f"--{cmd_style_arg}")
# Booleans are special: add only if True, omit for False
if isinstance(val, bool):
if val:
cmdline.append(f"--{cmd_style_arg}")
continue
# We have a parameter
cmdline.append(f"--{cmd_style_arg}")
if isinstance(val, (int, float)):
cmdline.append(str(val))
elif isinstance(val, str):
@@ -148,58 +160,58 @@ def create_options(*, input_file, output_file, **kwargs):
# If we are running a Tesseract spoof, ensure it knows what the input file is
if os.environ.get('PYTEST_CURRENT_TEST') and options.tesseract_env:
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = input_file
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
return options
def ocr( # pylint: disable=unused-argument
input_file,
output_file,
input_file: os.PathLike,
output_file: os.PathLike,
*,
language=None,
image_dpi=None,
language: List[str] = None,
image_dpi: int = None,
output_type=None,
sidecar=None,
jobs=None,
use_threads=None,
title=None,
author=None,
subject=None,
keywords=None,
rotate_pages=None,
remove_background=None,
deskew=None,
clean=None,
clean_final=None,
unpaper_args=None,
oversample=None,
remove_vectors=None,
threshold=None,
force_ocr=None,
skip_text=None,
redo_ocr=None,
skip_big=None,
optimize=None,
jpg_quality=None,
png_quality=None,
jbig2_lossy=None,
jbig2_page_group_size=None,
pages=None,
max_image_mpixels=None,
tesseract_config=None,
tesseract_pagesegmode=None,
tesseract_oem=None,
sidecar: os.PathLike = None,
jobs: int = None,
use_threads: bool = None,
title: str = None,
author: str = None,
subject: str = None,
keywords: str = None,
rotate_pages: bool = None,
remove_background: bool = None,
deskew: bool = None,
clean: bool = None,
clean_final: bool = None,
unpaper_args: str = None,
oversample: int = None,
remove_vectors: bool = None,
threshold: bool = None,
force_ocr: bool = None,
skip_text: bool = None,
redo_ocr: bool = None,
skip_big: float = None,
optimize: int = None,
jpg_quality: int = None,
png_quality: int = None,
jbig2_lossy: bool = None,
jbig2_page_group_size: int = None,
pages: str = None,
max_image_mpixels: float = None,
tesseract_config: List[str] = None,
tesseract_pagesegmode: int = None,
tesseract_oem: int = None,
pdf_renderer=None,
tesseract_timeout=None,
rotate_pages_threshold=None,
tesseract_timeout: float = None,
rotate_pages_threshold: float = None,
pdfa_image_compression=None,
user_words=None,
user_patterns=None,
fast_web_view=None,
keep_temporary_files=None,
progress_bar=None,
tesseract_env=None,
user_words: os.PathLike = None,
user_patterns: os.PathLike = None,
fast_web_view: float = None,
keep_temporary_files: bool = None,
progress_bar: bool = None,
tesseract_env: Dict[str, str] = None,
):
"""Run OCRmyPDF on one PDF or image.
+95 -3
View File
@@ -20,17 +20,73 @@
import logging
import os
import re
import shutil
import sys
from collections.abc import Mapping
from subprocess import PIPE, STDOUT, CalledProcessError, run
from functools import lru_cache
from subprocess import PIPE, STDOUT, CalledProcessError
from subprocess import run as subprocess_run
from ..exceptions import ExitCode, MissingDependencyError
log = logging.Logger(__name__)
def _get_program(args, env=None):
program = args[0]
test_path = env.get('_OCRMYPDF_TEST_PATH', '')
if test_path:
program = shutil.which(program, path=test_path)
return program
def run(args, *, env=None, **kwargs):
"""Wrapper around subprocess.run()
The main purpose of this wrapper is to allow us to substitute the main program
for a spoof in the test suite. The hidden variable _OCRMYPDF_TEST_PATH replaces
the main PATH as a location to check for programs to run.
Secondly we have to account for behavioral differences in Windows in particular.
Creating symbolic links in Windows requires administrator privileges and
may not work if for some reason we're using a FAT file system or the temporary
folder is on a different drive from the working folder. The test suite
works around this by creating shim Python scripts that perform the same function
as a symbolic link, but those shims require support on this side, to ensure
we call them with Python.
"""
if not env:
env = os.environ
# Search in spoof path if necessary
program = _get_program(args, env)
# If we are running a .py on Windows, ensure we call it with this Python
# (to support test suite shims)
if os.name == 'nt' and program.lower().endswith('.py'):
args = [sys.executable, program] + args[1:]
else:
args = [program] + args[1:]
if os.name == 'nt':
paths = os.pathsep.join(os.get_exec_path(env))
if not shutil.which(args[0], path=paths):
shimmed_path = shim_paths_with_program_files(env)
new_args0 = shutil.which(args[0], path=shimmed_path)
if new_args0:
args[0] = new_args0
log.debug(args)
if sys.version_info < (3, 7) and os.name == 'nt':
# Can't use close_fds=True on Windows with Python 3.6 or older
# https://bugs.python.org/issue19575, etc.
kwargs['close_fds'] = False
return subprocess_run(args, env=env, **kwargs)
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
"Get the version of the specified program"
"""Get the version of the specified program"""
args_prog = [program, version_arg]
try:
proc = run(
@@ -66,6 +122,32 @@ def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env
return version
def shim_paths_with_program_files(env=None):
if not env:
env = os.environ
program_files = env.get('PROGRAMFILES', '')
if not program_files:
return env.get('PATH', '')
paths = []
try:
for dirname in os.listdir(program_files):
if dirname.lower() == 'tesseract-ocr':
paths.append(os.path.join(program_files, dirname))
elif dirname.lower() == 'gs':
try:
latest_gs = max(
os.listdir(os.path.join(program_files, dirname)),
key=lambda d: float(d[2:]),
)
except (FileNotFoundError, NotADirectoryError):
continue
paths.append(os.path.join(program_files, dirname, latest_gs, 'bin'))
except EnvironmentError:
pass
paths.extend(path for path in os.get_exec_path(env) if path not in set(paths))
return os.pathsep.join(paths)
missing_program = '''
The program '{program}' could not be executed or was not found on your
system PATH.
@@ -111,23 +193,33 @@ On RPM-based systems (Red Hat, Fedora), search for instructions on
installing the RPM for {program}.
'''
windows_install_advice = '''
If not already installed, install the Chocolatey package manager. Then use
a command prompt to install the missing package:
choco install {package}
'''
def _get_platform():
if sys.platform.startswith('freebsd'):
return 'freebsd'
elif sys.platform.startswith('linux'):
return 'linux'
elif sys.platform.startswith('win'):
return 'windows'
return sys.platform
def _error_trailer(program, package, **kwargs):
if isinstance(package, Mapping):
package = package[_get_platform()]
package = package.get(_get_platform(), program)
if _get_platform() == 'darwin':
log.info(osx_install_advice.format(**locals()))
elif _get_platform() == 'linux':
log.info(linux_install_advice.format(**locals()))
elif _get_platform() == 'windows':
log.info(windows_install_advice.format(**locals()))
def _error_missing_program(program, package, required_for, recommended):
+150 -97
View File
@@ -15,25 +15,51 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
"""Interface to Ghostscript executable"""
import logging
import os
import re
import warnings
from contextlib import suppress
from functools import lru_cache
from io import BytesIO
from os import fspath
from shutil import copy
from subprocess import PIPE, STDOUT, run
from tempfile import NamedTemporaryFile
from pathlib import Path
from shutil import which
from subprocess import PIPE, CalledProcessError
from PIL import Image
from ..exceptions import SubprocessOutputError
from . import get_version
from ..exceptions import MissingDependencyError, SubprocessOutputError
from . import get_version, run
gslog = logging.getLogger()
GS = 'gs'
if os.name == 'nt':
GS = which('gswin64c')
if not GS:
GS = which('gswin32c')
if not GS:
raise MissingDependencyError(
"""
---------------------------------------------------------------------
This error normally occurs when ocrmypdf can't Ghostscript. Please
ensure Ghostscript is installed and its location is added to the
system PATH environment variable.
For details see:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
)
GS = Path(GS).stem
@lru_cache(maxsize=1)
def version():
return get_version('gs')
return get_version(GS)
def jpeg_passthrough_available():
@@ -78,9 +104,12 @@ def extract_text(input_file, pageno=1):
else:
pages = []
# Note due to bug https://bugs.ghostscript.com/show_bug.cgi?id=701971
# Ghostscript <= 9.50 will truncate output unless we write to stdout, so
# don't write to a file.
args_gs = (
[
'gs',
GS,
'-dQUIET',
'-dSAFER',
'-dBATCH',
@@ -89,14 +118,15 @@ def extract_text(input_file, pageno=1):
'-dTextFormat=0',
]
+ pages
+ ['-o', '-', fspath(input_file)]
+ ['-o', '-', fspath(input_file), "-sstdout=%stderr"]
)
p = run(args_gs, stdout=PIPE, stderr=PIPE)
if p.returncode != 0:
try:
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
except CalledProcessError as e:
raise SubprocessOutputError(
'Ghostscript text extraction failed\n%s\n%s\n%s'
% (input_file, p.stdout.decode(), p.stderr.decode())
'Ghostscript text extraction failed\n%s\n%s'
% (input_file, e.stderr.decode(errors='replace'))
)
return p.stdout
@@ -138,54 +168,58 @@ def rasterize_pdf(
if not log:
log = gslog
with NamedTemporaryFile(delete=True) as tmp:
args_gs = (
[
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
f'-sDEVICE={raster_device}',
f'-dFirstPage={pageno}',
f'-dLastPage={pageno}',
f'-r{res[0]:f}x{res[1]:f}',
]
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ [
'-o',
tmp.name,
'-dAutoRotatePages=/None', # Probably has no effect on raster
'-f',
fspath(input_file),
]
)
args_gs = (
[
GS,
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
f'-sDEVICE={raster_device}',
f'-dFirstPage={pageno}',
f'-dLastPage={pageno}',
f'-r{res[0]:f}x{res[1]:f}',
]
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ [
'-o',
'-',
'-sstdout=%stderr',
'-dAutoRotatePages=/None', # Probably has no effect on raster
'-f',
fspath(input_file),
]
)
log.debug(args_gs)
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
elif p.stdout:
log.debug(p.stdout)
log.debug(args_gs)
try:
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
except CalledProcessError as e:
with suppress(OSError):
Path(output_file).unlink() # no unfinished files
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript rasterizing failed')
else:
stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr):
log.error(stderr)
elif stderr:
log.debug(stderr)
if p.returncode != 0:
raise SubprocessOutputError('Ghostscript rasterizing failed')
tmp.seek(0)
with Image.open(tmp) as im:
if rotation is not None:
log.debug("Rotating output by %i", rotation)
# rotation is a clockwise angle and Image.ROTATE_* is
# counterclockwise so this cancels out the rotation
if rotation == 90:
im = im.transpose(Image.ROTATE_90)
elif rotation == 180:
im = im.transpose(Image.ROTATE_180)
elif rotation == 270:
im = im.transpose(Image.ROTATE_270)
if rotation % 180 == 90:
page_dpi = page_dpi[1], page_dpi[0]
im.save(fspath(output_file), dpi=page_dpi)
with Image.open(BytesIO(p.stdout)) as im:
if rotation is not None:
log.debug("Rotating output by %i", rotation)
# rotation is a clockwise angle and Image.ROTATE_* is
# counterclockwise so this cancels out the rotation
if rotation == 90:
im = im.transpose(Image.ROTATE_90)
elif rotation == 180:
im = im.transpose(Image.ROTATE_180)
elif rotation == 270:
im = im.transpose(Image.ROTATE_270)
if rotation % 180 == 90:
page_dpi = page_dpi[1], page_dpi[0]
im.save(fspath(output_file), dpi=page_dpi)
def generate_pdfa(
@@ -193,7 +227,7 @@ def generate_pdfa(
output_file,
compression,
log,
threads=1,
threads=None, # deprecated parameter
pdf_version='1.5',
pdfa_part='2',
):
@@ -216,6 +250,10 @@ def generate_pdfa(
"""
if not log:
log = gslog
if threads is not None:
warnings.warn(
"use of deprecated parameter 'threads'", category=DeprecationWarning
)
compression_args = []
if compression == 'jpeg':
@@ -249,36 +287,58 @@ def generate_pdfa(
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
compression_args.append('-dPassThroughJPEGImages=false')
with NamedTemporaryFile(delete=True) as gs_pdf:
# nb no need to specify ProcessColorModel when ColorConversionStrategy
# is set; see:
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
args_gs = (
[
"gs",
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
"-dCompatibilityLevel=" + str(pdf_version),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
]
+ compression_args
+ [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
log.debug(args_gs)
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
elif 'overprint mode not set' in p.stdout:
# nb no need to specify ProcessColorModel when ColorConversionStrategy
# is set; see:
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
args_gs = (
[
GS,
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
"-dSAFER",
"-dCompatibilityLevel=" + str(pdf_version),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
]
+ compression_args
+ [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-o",
"-",
"-sstdout=%stderr",
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
log.debug(args_gs)
try:
with Path(output_file).open('wb') as output:
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create
# PDF/A - check PDF/A status elsewhere
with suppress(OSError):
Path(output_file).unlink()
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
else:
stderr = p.stderr.decode('utf-8', errors='replace')
if _gs_error_reported(stderr):
last_part = None
repcount = 0
for part in stderr.split('****'):
if part != last_part:
if repcount > 1:
log.error(f"(previous error message repeated {repcount} times)")
repcount = 0
log.error(part)
else:
repcount += 1
last_part = part
elif 'overprint mode not set' in stderr:
# 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....
@@ -287,11 +347,4 @@ def generate_pdfa(
"input file to complete PDF/A conversion. "
)
else:
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, fspath(output_file))
else:
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
log.debug(stderr)
+4 -2
View File
@@ -15,11 +15,13 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
"""Interface to jbig2 executable"""
from functools import lru_cache
from subprocess import PIPE, run
from subprocess import PIPE
from ..exceptions import MissingDependencyError
from . import get_version
from . import get_version, run
@lru_cache(maxsize=1)
+3 -2
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
"""Interface to pngquant executable"""
from functools import lru_cache
from subprocess import run
from tempfile import NamedTemporaryFile
@@ -40,8 +42,7 @@ def available():
def quantize(input_file, output_file, quality_min, quality_max):
if input_file.endswith('.jpg'):
im = Image.open(input_file)
with NamedTemporaryFile(suffix='.png') as tmp:
with Image.open(input_file) as im, NamedTemporaryFile(suffix='.png') as tmp:
im.save(tmp)
args = [
'pngquant',
+35 -23
View File
@@ -15,35 +15,47 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from functools import lru_cache
from os import fspath
from subprocess import PIPE, STDOUT, CalledProcessError, run
"""Interface to qpdf executable"""
from . import get_version
from io import StringIO
import pikepdf
@lru_cache(maxsize=1)
def version():
return get_version('qpdf', regex=r'qpdf version (.+)')
return pikepdf.__libqpdf_version__
def check(input_file, log=None):
args_qpdf = ['qpdf', '--check', fspath(input_file)]
if log is None:
import logging as log
pdf = None
try:
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True, check=True)
except CalledProcessError as e:
if e.returncode == 2:
log.error("%s: not a valid PDF, and could not repair it.", input_file)
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
log.info("qpdf --check returned warnings:")
log.info(e.output)
else:
log.warning(e.output)
pdf = pikepdf.open(input_file)
except pikepdf.PdfError as e:
if log:
log.error(e)
return False
return True
else:
messages = pdf.check()
for msg in messages:
if 'error' in msg.lower():
log.error(msg)
else:
log.warning(msg)
sio = StringIO()
linearize = None
try:
pdf.check_linearization(sio)
except RuntimeError:
pass
else:
linearize = sio.getvalue()
if linearize:
log.warning(linearize)
if not messages and not linearize:
return True
return False
finally:
if pdf:
pdf.close()
+38 -31
View File
@@ -15,22 +15,23 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
"""Interface to Tesseract executable"""
import logging
import os
import shutil
import sys
from collections import namedtuple
from contextlib import suppress
from functools import lru_cache
from os import fspath
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired, run
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
from ..exceptions import (
MissingDependencyError,
SubprocessOutputError,
TesseractConfigError,
)
from ..helpers import page_number
from . import get_version
from ..helpers import page_number, safe_symlink
from . import get_version, run
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
@@ -52,6 +53,12 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
"""
class TesseractLoggerAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
kwargs['extra'] = self.extra
return '[tesseract] %s' % (msg), kwargs
def version(tesseract_env=None):
return get_version('tesseract', regex=r'tesseract\s(.+)', env=tesseract_env)
@@ -61,13 +68,13 @@ def v4(tesseract_env=None):
return version(tesseract_env) >= '4'
def has_textonly_pdf(tesseract_env=None):
def has_textonly_pdf(tesseract_env=None, langs=None):
"""Does Tesseract have textonly_pdf capability?
Available in v4.00.00alpha since January 2017. Best to
parse the parameter list
parse the parameter list.
"""
args_tess = ['tesseract', '--print-parameters', 'pdf']
args_tess = tess_base_args(langs, engine_mode=None) + ['--print-parameters', 'pdf']
params = ''
try:
proc = run(
@@ -121,9 +128,10 @@ def languages(tesseract_env=None):
except CalledProcessError as e:
raise MissingDependencyError(lang_error(e.output)) from e
for line in output.splitlines():
if line.startswith('Error'):
raise MissingDependencyError(lang_error(output))
header, *rest = output.splitlines()
if not header.startswith('List of available languages'):
raise MissingDependencyError(lang_error(output))
return set(lang.strip() for lang in rest)
@@ -131,7 +139,7 @@ def tess_base_args(langs, engine_mode):
args = ['tesseract']
if langs:
args.extend(['-l', '+'.join(langs)])
if engine_mode is not None and v4():
if engine_mode is not None:
args.extend(['--oem', str(engine_mode)])
return args
@@ -179,19 +187,15 @@ def get_orientation(input_file, engine_mode, timeout: float, log, tesseract_env=
return oc
def tesseract_log_output(log, stdout, input_file):
prefix = "[tesseract] "
def tesseract_log_output(mainlog, stdout, input_file):
log = TesseractLoggerAdapter(
mainlog, extra=mainlog.extra if hasattr(mainlog, 'extra') else None
)
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')
text = stdout.decode('utf-8', 'ignore')
lines = text.splitlines()
for line in lines:
@@ -200,25 +204,25 @@ def tesseract_log_output(log, stdout, input_file):
elif line.startswith("Warning in pixReadMem"):
continue
elif 'diacritics' in line:
log.warning(prefix + "lots of diacritics - possibly poor OCR")
log.warning("lots of diacritics - possibly poor OCR")
elif line.startswith('OSD: Weak margin'):
log.warning(prefix + "unsure about page orientation")
log.warning("unsure about page orientation")
elif 'Error in pixScanForForeground' in line:
pass # Appears to be spurious/problem with nonwhite borders
elif 'Error in boxClipToRectangle' in line:
pass # Always appears with pixScanForForeground message
elif 'parameter not found: ' in line.lower():
log.error(prefix + line.strip())
log.error(line.strip())
problem = line.split('found: ')[1]
raise TesseractConfigError(problem)
elif 'error' in line.lower() or 'exception' in line.lower():
log.error(prefix + line.strip())
log.error(line.strip())
elif 'warning' in line.lower():
log.warning(prefix + line.strip())
log.warning(line.strip())
elif 'read_params_file' in line.lower():
log.error(prefix + line.strip())
log.error(line.strip())
else:
log.info(prefix + line.strip())
log.info(line.strip())
def page_timedout(log, input_file, timeout):
@@ -233,8 +237,8 @@ def _generate_null_hocr(output_hocr, output_sidecar, image):
the same size as the input image."""
from PIL import Image
im = Image.open(image)
w, h = im.size
with Image.open(image) as im:
w, h = im.size
with open(output_hocr, 'w', encoding="utf-8") as f:
f.write(HOCR_TEMPLATE.format(w, h))
@@ -271,6 +275,9 @@ def generate_hocr(
if user_patterns:
args_tesseract.extend(['--user-patterns', user_patterns])
if user_words or user_patterns:
args_tesseract.extend(['-c', 'lstm_use_matrix=1'])
# Reminder: test suite tesseract spoofers will break after any changes
# to the number of order parameters here
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
@@ -314,7 +321,7 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
# Substitute a "skipped page"
with suppress(FileNotFoundError):
os.remove(output_pdf) # In case it was partially created
os.symlink(skip_pdf, output_pdf)
safe_symlink(skip_pdf, output_pdf)
return
# Or normally, just write a 0 byte file to the output to indicate a skip
@@ -358,7 +365,7 @@ def generate_pdf(
if pagesegmode is not None:
args_tesseract.extend(['--psm', str(pagesegmode)])
if text_only and has_textonly_pdf(tesseract_env):
if text_only and has_textonly_pdf(tesseract_env, language):
args_tesseract.extend(['-c', 'textonly_pdf=1'])
if user_words:
+25 -25
View File
@@ -18,10 +18,10 @@
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
"""Interface to unpaper executable"""
import os
import shlex
import subprocess
import sys
from functools import lru_cache
from subprocess import PIPE, STDOUT, CalledProcessError
from tempfile import TemporaryDirectory
@@ -30,6 +30,7 @@ from PIL import Image
from ..exceptions import MissingDependencyError, SubprocessOutputError
from . import get_version
from . import run as external_run
@lru_cache(maxsize=1)
@@ -42,33 +43,30 @@ def run(input_file, output_file, dpi, log, mode_args):
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
im = Image.open(input_file)
if im.mode not in SUFFIXES.keys():
log.info("Converting image to other colorspace")
with TemporaryDirectory() as tmpdir, Image.open(input_file) as im:
if im.mode not in SUFFIXES.keys():
log.info("Converting image to other colorspace")
try:
if im.mode == 'P' and len(im.getcolors()) == 2:
im = im.convert(mode='1')
else:
im = im.convert(mode='RGB')
except IOError as e:
im.close()
raise MissingDependencyError(
"Could not convert image with type " + im.mode
) from e
try:
if im.mode == 'P' and len(im.getcolors()) == 2:
im = im.convert(mode='1')
else:
im = im.convert(mode='RGB')
except IOError as e:
im.close()
suffix = SUFFIXES[im.mode]
except KeyError:
raise MissingDependencyError(
"Could not convert image with type " + im.mode
"Failed to convert image to a supported format."
) from e
try:
suffix = SUFFIXES[im.mode]
except KeyError:
im.close()
raise MissingDependencyError(
"Failed to convert image to a supported format."
) from e
with TemporaryDirectory() as tmpdir:
input_pnm = os.path.join(tmpdir, f'input{suffix}')
output_pnm = os.path.join(tmpdir, f'output{suffix}')
im.save(input_pnm, format='PPM')
im.close()
# To prevent any shenanigans from accepting arbitrary parameters in
# --unpaper-args, we:
@@ -79,7 +77,7 @@ def run(input_file, output_file, dpi, log, mode_args):
# their unpaper arguments (whether intentionally or otherwise)
args_unpaper.extend([input_pnm, output_pnm])
try:
proc = subprocess.run(
proc = external_run(
args_unpaper,
check=True,
close_fds=True,
@@ -95,10 +93,12 @@ def run(input_file, output_file, dpi, log, mode_args):
log.debug(proc.stdout)
# unpaper sets dpi to 72; fix this
try:
Image.open(output_pnm).save(output_file, dpi=(dpi, dpi))
with Image.open(output_pnm) as imout:
imout.save(output_file, dpi=(dpi, dpi))
except (FileNotFoundError, OSError):
raise SubprocessOutputError(
"unpaper: failed to produce the expected output file. Called with: "
"unpaper: failed to produce the expected output file. "
+ " Called with: "
+ str(args_unpaper)
) from None
+9 -3
View File
@@ -18,6 +18,7 @@
import logging
import multiprocessing
import os
import shutil
import warnings
from collections.abc import Iterable
from contextlib import suppress
@@ -27,14 +28,14 @@ from pathlib import Path
log = logging.getLogger(__name__)
def re_symlink(input_file, soft_link_name, *args, **kwargs):
def safe_symlink(input_file, soft_link_name, *args, **kwargs):
"""
Helper function: relinks soft symbolic link if necessary
"""
if len(args) == 1 and isinstance(args[0], logging.Logger):
log.warning("Deprecated: re_symlink(,log)")
log.warning("Deprecated: safe_symlink(,log)")
if 'log' in kwargs:
log.warning('Deprecated: re_symlink(...log=)')
log.warning('Deprecated: safe_symlink(...log=)')
input_file = os.fspath(input_file)
soft_link_name = os.fspath(soft_link_name)
@@ -60,6 +61,11 @@ def re_symlink(input_file, soft_link_name, *args, **kwargs):
if not os.path.exists(input_file):
raise FileNotFoundError(f"trying to create a broken symlink to {input_file}")
if os.name == 'nt':
# Don't actually use symlinks on Windows due to permission issues
shutil.copyfile(input_file, soft_link_name)
return
log.debug("os.symlink(%s, %s)", input_file, soft_link_name)
# Create symbolic link using absolute path
+45 -17
View File
@@ -33,13 +33,34 @@ from io import BytesIO
from os import fspath
from tempfile import TemporaryFile
from .exceptions import MissingDependencyError
from .exec import shim_paths_with_program_files
from .lib._leptonica import ffi
# pylint: disable=protected-access
logger = logging.getLogger(__name__)
lept = ffi.dlopen(find_library('lept'))
if os.name == 'nt':
libname = 'liblept-5'
os.environ['PATH'] = shim_paths_with_program_files()
else:
libname = 'lept'
_libpath = find_library(libname)
if not _libpath and os.name == 'nt':
raise MissingDependencyError(
"""
---------------------------------------------------------------------
This error normally occurs when ocrmypdf can't find a file named
liblept-5.dll (Leptonica). Please ensure Tesseract-OCR is installed
and its location is added to the system PATH environment variable.
For details see:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
)
lept = ffi.dlopen(_libpath)
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
@@ -292,9 +313,11 @@ class Pix(LeptonicaObject):
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(os.fsencode(filename)))
with open(path, 'rb') as py_file:
data = py_file.read()
buffer = ffi.from_buffer(data)
with _LeptonicaErrorTrap():
return cls(lept.pixReadMem(buffer, len(buffer)))
def write_implied_format(self, path, jpeg_quality=0, jpeg_progressive=0):
"""Write pix to the filename, with the extension indicating format.
@@ -302,11 +325,19 @@ class Pix(LeptonicaObject):
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(
os.fsencode(filename), self._cdata, jpeg_quality, jpeg_progressive
)
lept_format = lept.getImpliedFileFormat(os.fsencode(path))
with open(path, 'wb') as py_file:
data = ffi.new('l_uint8 **pdata')
size = ffi.new('size_t *psize')
with _LeptonicaErrorTrap():
if lept_format == lept.L_JPEG_ENCODE:
lept.pixWriteMemJpeg(
data, size, self._cdata, jpeg_quality, jpeg_progressive
)
else:
lept.pixWriteMem(data, size, self._cdata, lept_format)
buffer = ffi.buffer(data[0], size[0])
py_file.write(buffer)
@classmethod
def frompil(self, pillow_image):
@@ -502,17 +533,14 @@ class Pix(LeptonicaObject):
display=0,
pdfdir=ffi.NULL,
):
if get_leptonica_version() < 'leptonica-1.76':
# Leptonica 1.76 changed the API for pixFindPageForeground; we don't
# support the old version
raise LeptonicaError("Not available in this version of Leptonica")
with _LeptonicaErrorTrap():
cropbox = Box(
lept.pixFindPageForeground(
self._cdata,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir,
self._cdata, threshold, mindist, erasedist, showmorph, ffi.NULL
)
)
File diff suppressed because one or more lines are too long
+34 -9
View File
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
from cffi import FFI
ffibuilder = FFI()
@@ -74,6 +76,17 @@ struct Pixa
};
typedef struct Pixa PIXA;
/*! Array of compressed pix */
struct PixaComp
{
l_int32 n; /*!< number of PixComp in ptr array */
l_int32 nalloc; /*!< number of PixComp ptrs allocated */
l_int32 offset; /*!< indexing offset into ptr array */
struct PixComp **pixc; /*!< the array of ptrs to PixComp */
struct Boxa *boxa; /*!< array of boxes */
};
typedef struct PixaComp PIXAC;
struct Box
{
l_int32 x;
@@ -210,9 +223,15 @@ ffibuilder.cdef(
"""
PIX * pixRead ( const char *filename );
PIX * pixReadMem ( const l_uint8 *data, size_t size );
PIX * pixReadStream ( FILE *fp, l_int32 hint );
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
l_int32 getImpliedFileFormat ( const char *filename );
l_ok pixWriteStream ( FILE *fp, PIX *pix, l_int32 format );
l_ok pixWriteStreamJpeg ( FILE *fp, PIX *pixs, l_int32 quality, l_int32 progressive );
l_ok pixWriteMem ( l_uint8 **pdata, size_t *psize, PIX *pix, l_int32 format );
l_ok pixWriteMemJpeg ( l_uint8 **pdata, size_t *psize, PIX *pix, l_int32 quality, l_int32 progressive );
l_int32
pixWriteMemPng(l_uint8 **pdata,
size_t *psize,
@@ -294,14 +313,12 @@ pixCleanBackgroundToWhite(PIX *pixs,
l_int32 whiteval);
BOX *
pixFindPageForeground(PIX *pixs,
l_int32 threshold,
l_int32 mindist,
l_int32 erasedist,
l_int32 pagenum,
l_int32 showmorph,
l_int32 display,
const char *pdfdir);
pixFindPageForeground ( PIX *pixs,
l_int32 threshold,
l_int32 mindist,
l_int32 erasedist,
l_int32 showmorph,
PIXAC *pixac );
PIX *
pixClipRectangle(PIX *pixs,
@@ -414,7 +431,10 @@ pixExtractBarcodes(PIX *pixs,
l_int32 debugflag);
BOXA *
pixLocateBarcodes ( PIX *pixs, l_int32 thresh, PIX **ppixb, PIX **ppixm );
pixLocateBarcodes ( PIX *pixs,
l_int32 thresh,
PIX **ppixb,
PIX **ppixm );
SARRAY *
pixReadBarcodes(PIXA *pixa,
@@ -491,3 +511,8 @@ ffibuilder.set_source("ocrmypdf.lib._leptonica", None)
if __name__ == '__main__':
ffibuilder.compile(verbose=True)
if Path('ocrmypdf/lib/_leptonica.py').exists() and Path('src/ocrmypdf').exists():
output = Path('ocrmypdf/lib/_leptonica.py')
output.rename('src/ocrmypdf/lib/_leptonica.py')
Path('ocrmypdf/lib').rmdir()
Path('ocrmypdf').rmdir()
+100 -61
View File
@@ -31,7 +31,7 @@ from . import leptonica
from ._jobcontext import PDFContext
from .exceptions import OutputFileAccessError
from .exec import jbig2enc, pngquant
from .helpers import re_symlink
from .helpers import safe_symlink
DEFAULT_JPEG_QUALITY = 75
DEFAULT_PNG_QUALITY = 70
@@ -73,6 +73,9 @@ def extract_image_filter(pike, root, log, image, xref):
if filtdp[0] == Name.JPXDecode:
return None # Don't do JPEG2000
if Name.Decode in image:
return None # Don't mess with custom Decode tables
return pim, filtdp
@@ -104,6 +107,15 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
return None
pim, filtdp = result
# Don't try to PNG-optimize 1bpp images, since JBIG2 does it better.
if pim.bits_per_component == 1:
return None
try:
pim.indexed # pikepdf 1.6.3 can't handle [/Indexed [/Array...]]
except NotImplementedError:
return None
if filtdp[0] == Name.DCTDecode and options.optimize >= 2:
# This is a simple heuristic derived from some training data, that has
# about a 70% chance of guessing whether the JPEG is high quality,
@@ -343,6 +355,7 @@ def transcode_jpegs(pike, jpegs, root, log, options):
def transcode_pngs(pike, images, image_name_fn, root, log, options):
modified = set()
if options.optimize >= 2:
png_quality = (
max(10, options.png_quality - 10),
@@ -363,6 +376,7 @@ def transcode_pngs(pike, images, image_name_fn, root, log, options):
png_quality[1],
)
)
modified.add(xref)
with tqdm(
desc="PNGs",
total=len(futures),
@@ -372,10 +386,14 @@ def transcode_pngs(pike, images, image_name_fn, root, log, options):
for _future in concurrent.futures.as_completed(futures):
pbar.update()
for xref in images:
for xref in modified:
im_obj = pike.get_object(xref, 0)
try:
compdata = leptonica.CompressedData.open(png_name(root, xref))
pix = leptonica.Pix.open(png_name(root, xref))
if pix.mode == '1':
compdata = pix.generate_pdf_ci_data(leptonica.lept.L_G4_ENCODE, 0)
else:
compdata = leptonica.CompressedData.open(png_name(root, xref))
except leptonica.LeptonicaError as e:
# Most likely this means file not found, i.e. quantize did not
# produce an improved version
@@ -391,69 +409,90 @@ def transcode_pngs(pike, images, image_name_fn, root, log, options):
f"{len(compdata)} > {int(im_obj.stream_dict.Length)}"
)
continue
if compdata.type == leptonica.lept.L_FLATE_ENCODE:
return rewrite_png(pike, im_obj, compdata, log)
elif compdata.type == leptonica.lept.L_G4_ENCODE:
return rewrite_png_as_g4(pike, im_obj, compdata, log)
# When a PNG is inserted into a PDF, we more or less copy the IDAT section from
# the PDF and transfer the rest of the PNG headers to PDF image metadata.
# One thing we have to do is tell the PDF reader whether a predictor was used
# on the image before Flate encoding. (Typically one is.)
# According to Leptonica source, PDF readers don't actually need us
# to specify the correct predictor, they just need a value of either:
# 1 - no predictor
# 10-14 - there is a predictor
# Leptonica's compdata->predictor only tells TRUE or FALSE
# From there the PNG decoder can infer the rest from the file.
# In practice the predictor should be Paeth, 14, so we'll use that.
# See:
# - PDF RM 7.4.4.4 Table 10
# - https://github.com/DanBloomberg/leptonica/blob/master/src/pdfio2.c#L757
predictor = 14 if compdata.predictor > 0 else 1
dparms = Dictionary(Predictor=predictor)
if predictor > 1:
dparms.BitsPerComponent = compdata.bps # Yes, this is redundant
dparms.Colors = compdata.spp
dparms.Columns = compdata.w
im_obj.BitsPerComponent = compdata.bps
im_obj.Width = compdata.w
im_obj.Height = compdata.h
def rewrite_png_as_g4(pike, im_obj, compdata, log):
im_obj.BitsPerComponent = 1
im_obj.Width = compdata.w
im_obj.Height = compdata.h
if compdata.ncolors > 0:
# .ncolors is the number of colors in the palette, not the number of
# colors used in a true color image
palette_pdf_string = compdata.get_palette_pdf_string()
palette_data = pikepdf.Object.parse(palette_pdf_string)
palette_stream = pikepdf.Stream(pike, bytes(palette_data))
palette = [
Name.Indexed,
Name.DeviceRGB,
compdata.ncolors - 1,
palette_stream,
]
cs = palette
else:
if compdata.spp == 1:
# PDF interprets binary-1 as black in 1bpp, but PNG sets
# black to 0 for 1bpp. Create a palette that informs the PDF
# of the mapping - seems cleaner to go this way but pikepdf
# needs to be patched to support it.
# palette = [Name.Indexed, Name.DeviceGray, 1, b"\xff\x00"]
# cs = palette
cs = Name.DeviceGray
elif compdata.spp == 3:
cs = Name.DeviceRGB
elif compdata.spp == 4:
cs = Name.DeviceCMYK
if compdata.bps == 1:
im_obj.Decode = [1, 0] # Bit of a kludge but this inverts photometric too
im_obj.ColorSpace = cs
im_obj.write(compdata.read(), filter=Name.FlateDecode, decode_parms=dparms)
im_obj.write(compdata.read())
log.debug(f"PNG to G4 {im_obj.objgen}")
if Name.Predictor in im_obj:
del im_obj.Predictor
if Name.DecodeParms in im_obj:
del im_obj.DecodeParms
im_obj.DecodeParms = Dictionary(
K=-1, BlackIs1=bool(compdata.minisblack), Columns=compdata.w
)
im_obj.Filter = Name.CCITTFaxDecode
return
def rewrite_png(pike, im_obj, compdata, log):
# When a PNG is inserted into a PDF, we more or less copy the IDAT section from
# the PDF and transfer the rest of the PNG headers to PDF image metadata.
# One thing we have to do is tell the PDF reader whether a predictor was used
# on the image before Flate encoding. (Typically one is.)
# According to Leptonica source, PDF readers don't actually need us
# to specify the correct predictor, they just need a value of either:
# 1 - no predictor
# 10-14 - there is a predictor
# Leptonica's compdata->predictor only tells TRUE or FALSE
# 10-14 means the actual predictor is specified in the data, so for any
# number >= 10 the PDF reader will use whatever the PNG data specifies.
# In practice Leptonica should use Paeth, 14, but 15 seems to be the
# designated value for "optimal". So we will use 15.
# See:
# - PDF RM 7.4.4.4 Table 10
# - https://github.com/DanBloomberg/leptonica/blob/master/src/pdfio2.c#L757
predictor = 15 if compdata.predictor > 0 else 1
dparms = Dictionary(Predictor=predictor)
if predictor > 1:
dparms.BitsPerComponent = compdata.bps # Yes, this is redundant
dparms.Colors = compdata.spp
dparms.Columns = compdata.w
im_obj.BitsPerComponent = compdata.bps
im_obj.Width = compdata.w
im_obj.Height = compdata.h
log.debug(
f"PNG {im_obj.objgen}: palette={compdata.ncolors} spp={compdata.spp} bps={compdata.bps}"
)
if compdata.ncolors > 0:
# .ncolors is the number of colors in the palette, not the number of
# colors used in a true color image. The palette string is always
# given as RGB tuples even when the image is grayscale; see
# https://github.com/DanBloomberg/leptonica/blob/master/src/colormap.c#L2067
palette_pdf_string = compdata.get_palette_pdf_string()
palette_data = pikepdf.Object.parse(palette_pdf_string)
palette_stream = pikepdf.Stream(pike, bytes(palette_data))
palette = [Name.Indexed, Name.DeviceRGB, compdata.ncolors - 1, palette_stream]
cs = palette
else:
# ncolors == 0 means we are using a colorspace without a palette
if compdata.spp == 1:
cs = Name.DeviceGray
elif compdata.spp == 3:
cs = Name.DeviceRGB
elif compdata.spp == 4:
cs = Name.DeviceCMYK
im_obj.ColorSpace = cs
im_obj.write(compdata.read(), filter=Name.FlateDecode, decode_parms=dparms)
def optimize(input_file, output_file, context, save_settings):
log = context.log
options = context.options
if options.optimize == 0:
re_symlink(input_file, output_file)
safe_symlink(input_file, output_file)
return
if options.jpeg_quality == 0:
@@ -499,7 +538,7 @@ def optimize(input_file, output_file, context, save_settings):
pike.remove_unreferenced_resources()
pike.save(output_file, **save_settings)
else:
re_symlink(target_file, output_file)
safe_symlink(target_file, output_file)
def main(infile, outfile, level, jobs=1):
@@ -510,11 +549,11 @@ def main(infile, outfile, level, jobs=1):
"""Emulate ocrmypdf's options"""
def __init__(
self, input_file, jobs, optimize, jpeg_quality, png_quality, jb2lossy
self, input_file, jobs, optimize_, jpeg_quality, png_quality, jb2lossy
):
self.input_file = input_file
self.jobs = jobs
self.optimize = optimize
self.optimize = optimize_
self.jpeg_quality = jpeg_quality
self.png_quality = png_quality
self.jbig2_page_group_size = 0
@@ -525,7 +564,7 @@ def main(infile, outfile, level, jobs=1):
options = OptimizeOptions(
input_file=infile,
jobs=jobs,
optimize=int(level),
optimize_=int(level),
jpeg_quality=0, # Use default
png_quality=0,
jb2lossy=False,
+9 -30
View File
@@ -31,8 +31,7 @@ Ghostscript's handling of pdfmark.
"""
import os
from binascii import hexlify
import base64
from pathlib import Path
from string import Template
@@ -48,26 +47,13 @@ SRGB_ICC_PROFILE = pkg_resources.resource_filename('ocrmypdf', ICC_PROFILE_RELPA
# files, from the Ghostscript documentation. Lines beginning with % are
# comments. Python substitution variables have a '$' prefix.
pdfa_def_template = u"""%!
% Define entries in the document Info dictionary :
% Define an ICC profile :
/ICCProfile $icc_profile
def
% Define an ICC profile :
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
[{icc_PDFA}
<<
/N currentpagedevice /ProcessColorModel known {
currentpagedevice /ProcessColorModel get dup /DeviceGray eq
{pop 1} {
/DeviceRGB eq
{3}{4} ifelse
} ifelse
} {
(ERROR, unable to determine ProcessColorModel) == flush
} ifelse
>> /PUT pdfmark
[{icc_PDFA} ICCProfile (r) file /PUT pdfmark
[{icc_PDFA} << /N 3 >> /PUT pdfmark
[{icc_PDFA} ICCProfile /PUT pdfmark
% Define the output intent dictionary :
@@ -96,24 +82,16 @@ def generate_pdfa_ps(target_filename, icc='sRGB'):
:param target_filename: filename to save
:param icc: ICC identifier such as 'sRGB'
:returns: a string containing the entire pdfmark
"""
if icc == 'sRGB':
icc_profile = SRGB_ICC_PROFILE
else:
raise NotImplementedError("Only supporting sRGB")
# pdfmark must contain the full path to the ICC profile, and pdfmark must be
# also encoded in ASCII. ocrmypdf can be installed anywhere, including to
# paths that have a non-ASCII character in the filename. Ghostscript
# accepts hex-encoded strings and converts them to byte strings, so
# we encode the path with fsencode() and use the hex representation.
# UTF-16 not accepted here. (Even though ASCII encodable is the usual case,
# do this always to avoid making it a rare conditional.)
bytes_icc_profile = os.fsencode(icc_profile)
hex_icc_profile = hexlify(bytes_icc_profile)
icc_profile = '<' + hex_icc_profile.decode('ascii') + '>'
# Read the ICC profile, encode as ASCII85 and convert to a string which we
# will insert in the .ps file
bytes_icc_profile = Path(icc_profile).read_bytes()
icc_profile = base64.a85encode(bytes_icc_profile, adobe=True).decode('ascii')
t = Template(pdfa_def_template)
ps = t.substitute(icc_profile=icc_profile, icc_identifier=icc)
@@ -121,6 +99,7 @@ def generate_pdfa_ps(target_filename, icc='sRGB'):
# We should have encoded everything to pure ASCII by this point, and
# to be safe, only allow ASCII in PostScript
Path(target_filename).write_text(ps, encoding='ascii')
return target_filename
def file_claims_pdfa(filename):
+1
View File
@@ -96,6 +96,7 @@ def extract_text_xml(infile, pdf, pageno=None, log=gslog):
page_count_difference = len(pdf.pages) - len(page_xml)
if page_count_difference != 0:
log.error("The number of pages in the input file is inconsistent.")
log.error(f"Expected {len(pdf.pages)}, txtwrite says {len(page_xml)}")
if page_count_difference > 0:
page_xml.extend([None] * page_count_difference)
return page_xml
+39 -33
View File
@@ -20,6 +20,7 @@ from math import copysign
from pathlib import Path
from unittest.mock import patch
import pdfminer
import pdfminer.encodingdb
import pdfminer.pdfdevice
import pdfminer.pdfinterp
@@ -36,51 +37,54 @@ from ..exceptions import EncryptedPdfError
STRIP_NAME = re.compile(r'[0-9]+')
#
# Unconditional pdfminer patches
# pdfminer 20181108 patches
#
if pdfminer.__version__ == '20181108':
def name2unicode(name):
"""Fix pdfminer's name2unicode function
def name2unicode(name):
"""Fix pdfminer's name2unicode function
Font cids that are mapped to names of the form /g123 seem to be, by convention
characters with no corresponding Unicode entry. These can be subsetted fonts
or symbolic fonts. There seems to be no way to map /g123 fonts to Unicode,
barring a ToUnicode data structure.
"""
if name in glyphname2unicode:
return glyphname2unicode[name]
if name.startswith('g') or name.startswith('a'):
raise KeyError(name)
if name.startswith('uni'):
try:
return chr(int(name[3:], 16))
except ValueError: # Not hexadecimal
Font cids that are mapped to names of the form /g123 seem to be, by convention
characters with no corresponding Unicode entry. These can be subsetted fonts
or symbolic fonts. There seems to be no way to map /g123 fonts to Unicode,
barring a ToUnicode data structure.
"""
if name in glyphname2unicode:
return glyphname2unicode[name]
if name.startswith('g') or name.startswith('a'):
raise KeyError(name)
m = STRIP_NAME.search(name)
if not m:
raise KeyError(name)
return chr(int(m.group(0)))
if name.startswith('uni'):
try:
return chr(int(name[3:], 16))
except ValueError: # Not hexadecimal
raise KeyError(name)
m = STRIP_NAME.search(name)
if not m:
raise KeyError(name)
return chr(int(m.group(0)))
pdfminer.encodingdb.name2unicode = name2unicode
pdfminer.encodingdb.name2unicode = name2unicode
original_PDFFont_init = PDFFont.__init__
original_PDFFont_init = PDFFont.__init__
def PDFFont__init__(self, descriptor, widths, default_width=None):
original_PDFFont_init(self, descriptor, widths, default_width)
# PDF spec says descent should be negative
# A font with a positive descent implies it floats entirely above the
# baseline, i.e. it's not really a baseline anymore. I have fonts that
# claim a positive descent, but treating descent as positive always seems
# to misposition text.
if self.descent > 0:
self.descent = -self.descent
PDFFont.__init__ = PDFFont__init__
def PDFFont__init__(self, descriptor, widths, default_width=None):
original_PDFFont_init(self, descriptor, widths, default_width)
# PDF spec says descent should be negative
# A font with a positive descent implies it floats entirely above the
# baseline, i.e. it's not really a baseline anymore. I have fonts that
# claim a positive descent, but treating descent as positive always seems
# to misposition text.
if self.descent > 0:
self.descent = -self.descent
#
# end of pdfminer 20181108 patches
#
PDFFont.__init__ = PDFFont__init__
original_PDFSimpleFont_init = PDFSimpleFont.__init__
@@ -97,6 +101,7 @@ def PDFSimpleFont__init__(self, descriptor, widths, spec):
PDFSimpleFont.__init__ = PDFSimpleFont__init__
#
# pdfminer patches when creator is PScript5.dll
#
@@ -207,6 +212,7 @@ class TextPositionTracker(PDFLayoutAnalyzer):
super().__init__(rsrcmgr, pageno, laparams)
self.textstate = None
self.result = None
self.cur_item = None # not defined in pdfminer code as it should be
def begin_page(self, page, ctm):
super().begin_page(page, ctm)
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.1.0 with Leptonica
@@ -0,0 +1,30 @@
Tarnose
Bokale oa
Lehuntze
Mugerre
Milafranga Komunikabideak
BAIONA i zeettnansise —
1 Trenbideak -- ~~~
t\ Basusarri — spmsans20141004 se: . a ~
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.1.0 with Leptonica
@@ -0,0 +1,2 @@
Covfefe is a perfectly cromulent word.
@@ -0,0 +1 @@
Tesseract Open Source OCR Engine v4.1.0 with Leptonica
@@ -0,0 +1,27 @@
Linzensoep a la Waterman
4 ons linzen
3 liter water
3 uien
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in -l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruicdnagel.
Alle uien, kerrie en gout bij de linzen
voegen, Alles aan de kook brengen,. Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gaar Zijn is de soep klaar.
+3
View File
@@ -66,3 +66,6 @@
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cmyk.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.2 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.3", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.0.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.0.10 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.6.0-x86_64-i386-64bit", "python": "3.7.4", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.1.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.5", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.1.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.5", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "tesseract 4.1.0 leptonica-1.78.0 libgif 5.1.4 : libjpeg 9c : libpng 1.6.37 : libtiff 4.1.0 : zlib 1.2.11 : libwebp 1.0.3 : libopenjp2 2.3.1 Found AVX2 Found AVX Found SSE ", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.5", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
+58 -35
View File
@@ -15,16 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import ast
import os
import platform
import sys
from contextlib import contextmanager
from pathlib import Path
from subprocess import PIPE, run
from ocrmypdf import api, cli
import pytest
from ocrmypdf import api, cli
pytest_plugins = ['helpers_namespace']
try:
@@ -81,6 +82,23 @@ PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
OCRMYPDF = [sys.executable, '-m', 'ocrmypdf']
WINDOWS_SHIM_TEMPLATE = """
# This is a shim for Windows that has the same effect as a symlink to the target .py
# file
import os
import subprocess
import sys
args = [sys.executable, {spoofer}, *sys.argv[1:]]
p = subprocess.run(args, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sys.stdout.buffer.write(p.stdout)
sys.stderr.buffer.write(p.stderr)
sys.exit(p.returncode)
"""
assert ast.parse(WINDOWS_SHIM_TEMPLATE.format(spoofer=repr(r"C:\\Temp\\file.py")))
@pytest.helpers.register
def spoof(tmp_path_factory, **kwargs):
"""Modify PATH to override subprocess executables
@@ -98,39 +116,27 @@ def spoof(tmp_path_factory, **kwargs):
for replace_program, with_spoof in kwargs.items():
spoofer = Path(SPOOF_PATH) / with_spoof
spoofer.chmod(0o755)
(tmpdir / replace_program).symlink_to(spoofer)
env['_OCRMYPDF_SAVE_PATH'] = env['PATH']
env['PATH'] = str(tmpdir) + ":" + env['PATH']
if os.name != 'nt':
spoofer.chmod(0o755)
(tmpdir / replace_program).symlink_to(spoofer)
else:
py_file = WINDOWS_SHIM_TEMPLATE.format(
spoofer=repr(os.fspath(spoofer.absolute()))
)
if replace_program == 'gs':
programs = ['gswin64c', 'gswin32c']
else:
programs = [replace_program]
for prog in programs:
(tmpdir / f'{prog}.py').write_text(py_file, encoding='utf-8')
env['_OCRMYPDF_TEST_PATH'] = str(tmpdir) + os.pathsep + env['PATH']
if os.name == 'nt':
if '.py' not in env['PATHEXT'].lower():
raise EnvironmentError("PATHEXT is not configured to support .py")
return env
@pytest.helpers.register
@contextmanager
def os_environ(new_env):
old_env = os.environ.copy()
if new_env is None:
new_env = {}
for k, v in new_env.items():
if k != 'PYTEST_CURRENT_TEST':
os.environ[k] = v
yield
new_keys = set(os.environ.copy()) - set(old_env)
for k in new_keys:
if k != 'PYTEST_CURRENT_TEST':
del os.environ[k]
for k in old_env:
if k != 'PYTEST_CURRENT_TEST':
os.environ[k] = old_env[k]
for k, v in os.environ.copy().items():
if k != 'PYTEST_CURRENT_TEST':
assert v == old_env[k]
@pytest.fixture(scope='session')
def spoof_tesseract_noop(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
@@ -176,15 +182,14 @@ def no_outpdf(tmp_path):
def check_ocrmypdf(input_file, output_file, *args, env=None):
"""Run ocrmypdf and confirmed that a valid file was created"""
# p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env)
options = cli.parser.parse_args(
[str(input_file), str(output_file)] + [str(arg) for arg in args]
[str(input_file), str(output_file)]
+ [str(arg) for arg in args if arg is not None]
)
api.check_options(options)
if env:
options.tesseract_env = env
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = input_file
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
result = api.run_pipeline(options, api=True)
assert result == 0
@@ -194,6 +199,24 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
return output_file
@pytest.helpers.register
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
"Run ocrmypdf and let caller deal with results"
options = cli.parser.parse_args(
[str(input_file), str(output_file)]
+ [str(arg) for arg in args if arg is not None]
)
api.check_options(options)
if env:
options.tesseract_env = env.copy()
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
if options.tesseract_env:
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
return api.run_pipeline(options, api=False)
@pytest.helpers.register
def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=True):
"Run ocrmypdf and let caller deal with results"
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

+40
View File
@@ -0,0 +1,40 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# 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.
"""Find Ghostscript executable"""
import os
import shutil
def real_ghostscript(argv):
if os.name != 'nt':
gs = shutil.which('gs')
gs_args = [gs] + argv[1:]
os.execv(gs_args[0], gs_args)
else:
gs = shutil.which('gswin64c')
if not gs:
gs = shutil.which('gswin32c')
os.execv(gs, argv[1:])
return # Not reachable
+1 -7
View File
@@ -25,23 +25,17 @@ import os
import sys
from subprocess import check_call
from gs import real_ghostscript
"""Replicate one type of Ghostscript feature elision warning during
PDF/A creation."""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
not permitted in PDF/A-2, overprint mode not set"""
def main():
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__file__))
+2 -7
View File
@@ -23,19 +23,14 @@
import os
import sys
from gs import real_ghostscript
"""Replicate Ghostscript PDF/A conversion failure by suppressing some
arguments"""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
def main():
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__file__))
+2 -7
View File
@@ -24,17 +24,12 @@
import os
import sys
from gs import real_ghostscript
"""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():
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__file__))
+1 -6
View File
@@ -25,15 +25,10 @@
import os
import sys
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
from gs import real_ghostscript
def main():
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__file__))
+1 -2
View File
@@ -22,7 +22,6 @@
import sys
"""Tesseract bad utf8 spoof
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
@@ -52,7 +51,7 @@ 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] == '--print-parameters':
elif sys.argv[-2] == '--print-parameters':
print("Some parameters", file=sys.stderr)
print("textonly_pdf\t1\tSome help text")
sys.exit(0)
+1 -1
View File
@@ -44,7 +44,7 @@ def main():
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng\n', file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--print-parameters':
elif sys.argv[-2] == '--print-parameters':
print('A parameter list would go here\ntextonly_pdf 0\n', file=sys.stderr)
sys.exit(0)
elif sys.argv[-2] == 'hocr':
+2 -3
View File
@@ -59,9 +59,6 @@ import subprocess
import sys
from pathlib import Path
if '_OCRMYPDF_SAVE_PATH' in os.environ:
os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH']
__version__ = subprocess.check_output(
['tesseract', '--version'], stderr=subprocess.STDOUT
).decode()
@@ -100,6 +97,8 @@ def main():
# Convert non-standard but supported -psm to --psm
sys.argv = ['--psm' if arg == '-psm' else arg for arg in sys.argv]
if '_OCRMYPDF_TEST_INFILE' not in os.environ:
real_tesseract() # test not properly set up
source = os.environ['_OCRMYPDF_TEST_INFILE'] # required
args = parser.parse_args()
+1 -1
View File
@@ -50,7 +50,7 @@ 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] == '--print-parameters':
elif sys.argv[-2] == '--print-parameters':
print('A parameter list would go here\ntextonly_pdf 0\n', file=sys.stderr)
sys.exit(0)
elif sys.argv[-2] == 'hocr':
+1 -1
View File
@@ -76,7 +76,7 @@ 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] == '--print-parameters':
elif sys.argv[-2] == '--print-parameters':
print("Some parameters", file=sys.stderr)
print("textonly_pdf\t1\tSome help text")
sys.exit(0)
+33
View File
@@ -0,0 +1,33 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import pytest
import ocrmypdf
check_ocrmypdf = pytest.helpers.check_ocrmypdf
@pytest.fixture
def acroform(resources):
return resources / 'acroform.pdf'
def test_acroform_and_redo(acroform, caplog, no_outpdf):
with pytest.raises(ocrmypdf.exceptions.InputFileError):
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
assert '--redo-ocr is not currently possible' in caplog.text
+6 -1
View File
@@ -15,10 +15,15 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import run, PIPE
from subprocess import PIPE, run
import pytest
pytestmark = pytest.mark.skipif(
pytest.helpers.running_in_docker(), # pylint: disable=no-member
reason="docker can't complete",
)
def test_fish():
try:
+63
View File
@@ -22,8 +22,42 @@ import pikepdf
import pytest
from PIL import Image
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec.ghostscript import rasterize_pdf
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
spoof = pytest.helpers.spoof
@pytest.fixture(scope='session')
def spoof_no_tess_gs_render_fail(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
)
@pytest.fixture(scope='session')
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
)
@pytest.fixture(scope='session')
def spoof_no_tess_no_pdfa(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
)
@pytest.fixture(scope='session')
def spoof_no_tess_pdfa_warning(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
)
@pytest.fixture
def linn(resources):
@@ -79,3 +113,32 @@ def test_rasterize_rotated(linn, outdir, caplog):
with Image.open(outdir / 'out.png') as im:
assert im.size == (target_size[1], target_size[0])
assert im.info['dpi'] == (target_dpi[1], target_dpi[0])
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
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa
)
assert (
p.returncode == ExitCode.pdfa_conversion_failed
), "Unexpected return when PDF/A fails"
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
+3 -6
View File
@@ -16,13 +16,12 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import os
from unittest.mock import patch
import pikepdf
import pytest
import ocrmypdf
import pikepdf
os_environ = pytest.helpers.os_environ
def test_no_glyphless_graft(resources, outdir):
@@ -33,9 +32,7 @@ def test_no_glyphless_graft(resources, outdir):
pdf.pages.extend(pdf_cmyk.pages)
pdf.save(outdir / 'test.pdf')
env = os.environ.copy()
env['_OCRMYPDF_MAX_REPLACE_PAGES'] = '2'
with os_environ(env):
with patch('ocrmypdf._graft.MAX_REPLACE_PAGES', 2):
ocrmypdf.ocr(
outdir / 'test.pdf', outdir / 'out.pdf', deskew=True, tesseract_timeout=0
)
+92
View File
@@ -0,0 +1,92 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from unittest.mock import patch
import img2pdf
import pikepdf
import pytest
from PIL import Image
import ocrmypdf
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
@pytest.fixture
def baiona(resources):
return Image.open(resources / 'baiona_gray.png')
def test_image_to_pdf(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(
resources / 'crom.png', outpdf, '--image-dpi', '200', env=spoof_tesseract_noop
)
def test_no_dpi_info(caplog, baiona, outdir, no_outpdf):
im = baiona
assert 'dpi' not in im.info
input_image = outdir / 'baiona_no_dpi.png'
im.save(input_image)
rc = run_ocrmypdf_api(input_image, no_outpdf)
assert rc == ocrmypdf.ExitCode.input_file
assert "--image-dpi" in caplog.text
def test_dpi_not_credible(caplog, baiona, outdir, no_outpdf):
im = baiona
assert 'dpi' not in im.info
input_image = outdir / 'baiona_no_dpi.png'
im.save(input_image, dpi=(30, 30))
rc = run_ocrmypdf_api(input_image, no_outpdf)
assert rc == ocrmypdf.ExitCode.input_file
assert "not credible" in caplog.text
def test_cmyk_no_icc(caplog, resources, no_outpdf):
rc = run_ocrmypdf_api(resources / 'baiona_cmyk.jpg', no_outpdf)
assert rc == ocrmypdf.ExitCode.input_file
assert "no ICC profile" in caplog.text
def test_img2pdf_fails(resources, no_outpdf):
with patch(
'ocrmypdf._pipeline.img2pdf.convert', side_effect=img2pdf.ImageOpenError()
):
rc = run_ocrmypdf_api(
resources / 'baiona_gray.png', no_outpdf, '--image-dpi', '200'
)
assert rc == ocrmypdf.ExitCode.input_file
def test_jpeg_in_jpeg_out(resources, outpdf, spoof_tesseract_noop):
check_ocrmypdf(
resources / 'congress.jpg',
outpdf,
'--image-dpi',
'100',
'--output-type',
'pdf', # specifically check pdf because Ghostscript may convert to JPEG
'--remove-background',
env=spoof_tesseract_noop,
)
with pikepdf.open(outpdf) as pdf:
assert next(pdf.pages[0].images.values()).Filter == pikepdf.Name.DCTDecode
+10 -15
View File
@@ -16,8 +16,8 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from os import fspath
import os
from os import fspath
from pickle import dumps, loads
from unittest.mock import patch
@@ -38,7 +38,8 @@ def test_colormap_backgroundnorm(resources):
def crom_pix(resources):
pix = lept.Pix.open(resources / 'crom.png')
im = Image.open(resources / 'crom.png')
return pix, im
yield pix, im
im.close()
def test_pix_basic(crom_pix):
@@ -62,6 +63,10 @@ def test_pix_otsu(crom_pix):
assert im1bpp.mode == '1'
@pytest.mark.skipif(
lept.get_leptonica_version() < 'leptonica-1.76',
reason="needs new leptonica for API change",
)
def test_crop(resources):
pix = lept.Pix.open(resources / 'linn.png')
foreground = pix.crop_to_foreground()
@@ -89,16 +94,6 @@ def test_leptonica_compile(tmp_path):
ffibuilder.compile(tmpdir=fspath(tmp_path), target=fspath(tmp_path / 'lepttest.*'))
def test_with_stderr(capsys):
# pytest redirects stderr too; we must disable this for the test to be valid
with capsys.disabled():
with pytest.raises(FileNotFoundError):
lept.Pix.open("does_not_exist1")
def test_without_stderr(capsys):
# pytest redirects stderr too; we must disable this for the test to be valid
with capsys.disabled():
with patch('sys.stderr', new=None):
with pytest.raises(FileNotFoundError):
lept.Pix.open("does_not_exist2")
def test_file_not_found():
with pytest.raises(FileNotFoundError):
lept.Pix.open("does_not_exist1")
+97 -420
View File
@@ -1,4 +1,4 @@
# © 2015-17 James R. Barlow: github.com/jbarlow83
# © 2015-19 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
@@ -15,23 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import shutil
import sys
from math import isclose
from pathlib import Path
from subprocess import DEVNULL, PIPE, run, Popen
from subprocess import PIPE, run
from unittest.mock import patch
import pikepdf
import PIL
import pytest
from PIL import Image
import ocrmypdf
import pikepdf
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.exec import ghostscript, qpdf, tesseract
from ocrmypdf.leptonica import Pix
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
@@ -40,6 +38,7 @@ from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
spoof = pytest.helpers.spoof
@@ -56,133 +55,10 @@ def spoof_tesseract_big_image_error(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_big_image_error.py')
@pytest.fixture(scope='session')
def spoof_no_tess_no_pdfa(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
)
@pytest.fixture(scope='session')
def spoof_no_tess_pdfa_warning(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
)
@pytest.fixture(scope='session')
def spoof_no_tess_gs_render_fail(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
)
@pytest.fixture(scope='session')
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
return spoof(
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
)
@pytest.fixture(scope='session')
def spoof_tess_bad_utf8(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
def test_quick(spoof_tesseract_cache, resources, outpdf):
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_tesseract_cache)
def test_deskew(spoof_tesseract_noop, resources, outdir):
# Run with deskew
deskewed_pdf = check_ocrmypdf(
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
log = logging.getLogger()
deskewed_png = outdir / 'deskewed.png'
ghostscript.rasterize_pdf(
deskewed_pdf,
deskewed_png,
xres=150,
yres=150,
raster_device='pngmono',
log=log,
pageno=1,
)
pix = Pix.open(deskewed_png)
skew_angle, _skew_confidence = pix.find_skew()
print(skew_angle)
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
def test_remove_background(spoof_tesseract_noop, resources, outdir):
# Ensure the input image does not contain pure white/black
im = Image.open(resources / 'congress.jpg')
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
output_pdf = check_ocrmypdf(
resources / 'congress.jpg',
outdir / 'test_remove_bg.pdf',
'--remove-background',
'--image-dpi',
'150',
env=spoof_tesseract_noop,
)
log = logging.getLogger()
output_png = outdir / 'remove_bg.png'
ghostscript.rasterize_pdf(
output_pdf,
output_png,
xres=100,
yres=100,
raster_device='png16m',
log=log,
pageno=1,
)
# The output image should contain pure white and black
im = Image.open(output_png)
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
# This will run 5 * 2 * 2 = 20 test cases
@pytest.mark.parametrize(
"pdf", ['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf']
)
@pytest.mark.parametrize("renderer", ['sandwich', 'hocr'])
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
def test_exotic_image(
spoof_tesseract_cache, pdf, renderer, output_type, resources, outdir
):
outfile = outdir / f'test_{pdf}_{renderer}.pdf'
check_ocrmypdf(
resources / pdf,
outfile,
'-dc' if pytest.helpers.have_unpaper() else '-d',
'-v',
'1',
'--output-type',
output_type,
'--sidecar',
'--skip-text',
'--pdf-renderer',
renderer,
env=spoof_tesseract_cache,
)
assert outfile.with_suffix('.pdf.txt').exists()
@pytest.mark.parametrize('renderer', RENDERERS)
def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
oversampled_pdf = check_ocrmypdf(
@@ -203,8 +79,8 @@ def test_oversample(spoof_tesseract_cache, renderer, resources, outpdf):
def test_repeat_ocr(resources, no_outpdf):
p, _, _ = run_ocrmypdf(resources / 'graph_ocred.pdf', no_outpdf)
assert p.returncode != 0
result = run_ocrmypdf_api(resources / 'graph_ocred.pdf', no_outpdf)
assert result == ExitCode.already_done_ocr
def test_force_ocr(spoof_tesseract_cache, resources, outpdf):
@@ -223,7 +99,7 @@ def test_skip_ocr(spoof_tesseract_cache, resources, outpdf):
assert pdfinfo[0].has_text
def test_redo_ocr(spoof_tesseract_cache, resources, outpdf):
def test_redo_ocr(resources, outpdf):
in_ = resources / 'graph_ocred.pdf'
before = PdfInfo(in_, detailed_page_analysis=True)
out = outpdf
@@ -306,34 +182,34 @@ def test_maximum_options(
)
def test_tesseract_missing_tessdata(resources, no_outpdf):
def test_tesseract_missing_tessdata(resources, no_outpdf, tmpdir):
env = os.environ.copy()
env['TESSDATA_PREFIX'] = '/tmp'
env['TESSDATA_PREFIX'] = os.fspath(tmpdir)
p, _, err = run_ocrmypdf(
resources / 'graph_ocred.pdf', no_outpdf, '-v', '1', '--skip-text', env=env
returncode = run_ocrmypdf_api(
resources / 'graph.pdf', no_outpdf, '-v', '1', '--skip-text', env=env
)
assert p.returncode == ExitCode.missing_dependency, err
assert returncode == ExitCode.missing_dependency
def test_invalid_input_pdf(resources, no_outpdf):
p, out, err = run_ocrmypdf(resources / 'invalid.pdf', no_outpdf)
assert p.returncode == ExitCode.input_file, err
result = run_ocrmypdf_api(resources / 'invalid.pdf', no_outpdf)
assert result == ExitCode.input_file
def test_blank_input_pdf(resources, outpdf):
p, out, err = run_ocrmypdf(resources / 'blank.pdf', outpdf)
assert p.returncode == ExitCode.ok
result = run_ocrmypdf_api(resources / 'blank.pdf', outpdf)
assert result == ExitCode.ok
def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash, resources, no_outpdf):
# As a correctness test, make sure that --force-ocr on a PDF with no
# content still triggers tesseract. If tesseract crashes, then it was
# called.
p, _, err = run_ocrmypdf(
p, _, _ = run_ocrmypdf(
resources / 'blank.pdf', no_outpdf, '--force-ocr', env=spoof_tesseract_crash
)
assert p.returncode == ExitCode.child_process_error, err
assert p.returncode == ExitCode.child_process_error
assert not os.path.exists(no_outpdf)
@@ -346,28 +222,29 @@ def test_german(spoof_tesseract_cache, resources, outdir):
# properly. It is fine that we are testing -l deu on a French file because
# we are exercising the functionality not going for accuracy.
sidecar = outdir / 'francais.txt'
p, out, err = run_ocrmypdf(
resources / 'francais.pdf',
outdir / 'francais.pdf',
'-l',
'deu', # more commonly installed
'--sidecar',
sidecar,
env=spoof_tesseract_cache,
)
print(os.environ)
assert (
p.returncode == ExitCode.ok
), "This test may fail if Tesseract language packs are missing"
try:
check_ocrmypdf(
resources / 'francais.pdf',
outdir / 'francais.pdf',
'-l',
'deu', # more commonly installed
'--sidecar',
sidecar,
env=spoof_tesseract_cache,
)
except MissingDependencyError:
if 'deu' not in tesseract.languages():
pytest.xfail(reason="tesseract-deu language pack not installed")
raise
def test_klingon(resources, outpdf):
p, out, err = run_ocrmypdf(resources / 'francais.pdf', outpdf, '-l', 'klz')
p, _, _ = run_ocrmypdf(resources / 'francais.pdf', outpdf, '-l', 'klz')
assert p.returncode == ExitCode.missing_dependency
def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf):
p, out, err = run_ocrmypdf(
result = run_ocrmypdf_api(
resources / 'missing_docinfo.pdf',
outpdf,
'-l',
@@ -375,7 +252,7 @@ def test_missing_docinfo(spoof_tesseract_noop, resources, outpdf):
'--skip-text',
env=spoof_tesseract_noop,
)
assert p.returncode == ExitCode.ok, err
assert result == ExitCode.ok
def test_uppercase_extension(spoof_tesseract_noop, resources, outdir):
@@ -386,24 +263,25 @@ def test_uppercase_extension(spoof_tesseract_noop, resources, outdir):
)
def test_input_file_not_found(no_outpdf):
def test_input_file_not_found(caplog, no_outpdf):
input_file = "does not exist.pdf"
p, out, err = run_ocrmypdf(input_file, no_outpdf)
assert p.returncode == ExitCode.input_file
assert input_file in out or input_file in err
result = run_ocrmypdf_api(input_file, no_outpdf)
assert result == ExitCode.input_file
assert input_file in caplog.text
def test_input_file_not_a_pdf(no_outpdf):
def test_input_file_not_a_pdf(caplog, no_outpdf):
input_file = __file__ # Try to OCR this file
p, out, err = run_ocrmypdf(input_file, no_outpdf)
assert p.returncode == ExitCode.input_file
assert input_file in out or input_file in err
result = run_ocrmypdf_api(input_file, no_outpdf)
assert result == ExitCode.input_file
if os.name != 'nt': # name will be mangled with \\'s on nt
assert input_file in caplog.text
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('encrypted')
def test_encrypted(resources, caplog, no_outpdf):
result = run_ocrmypdf_api(resources / 'skew-encrypted.pdf', no_outpdf)
assert result == ExitCode.encrypted_pdf
assert 'encryption must be removed' in caplog.text
@pytest.mark.parametrize('renderer', RENDERERS)
@@ -422,8 +300,8 @@ def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf):
@pytest.mark.parametrize('renderer', RENDERERS)
def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf):
p, out, err = run_ocrmypdf(
def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf, caplog):
p, _, err = run_ocrmypdf(
resources / 'ccitt.pdf',
no_outpdf,
'-v',
@@ -434,7 +312,7 @@ def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf):
)
assert p.returncode == ExitCode.child_process_error
assert not os.path.exists(no_outpdf)
assert "ERROR" in err
assert "SubprocessOutputError" in err
def test_tesseract_crash_autorotate(spoof_tesseract_crash, resources, no_outpdf):
@@ -472,70 +350,6 @@ def test_algo4(resources, spoof_tesseract_noop, outpdf):
assert p.returncode == ExitCode.encrypted_pdf
@pytest.mark.parametrize('renderer', RENDERERS)
def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpdf):
# Confirm input image is non-square resolution
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 = 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
@pytest.mark.parametrize('renderer', RENDERERS)
def test_convert_to_square_resolution(
renderer, spoof_tesseract_cache, resources, outpdf
):
# Confirm input image is non-square resolution
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(
resources / 'aspect.pdf',
outpdf,
'--force-ocr',
'--pdf-renderer',
renderer,
env=spoof_tesseract_cache,
)
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
# 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)
# 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].xres
out_im_h = out_p0.images[0].height / out_p0.images[0].yres
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):
check_ocrmypdf(
resources / 'crom.png', outpdf, '--image-dpi', '200', env=spoof_tesseract_noop
)
def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf):
out = check_ocrmypdf(
resources / 'jbig2.pdf',
@@ -550,68 +364,6 @@ def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf):
assert out_pageinfo[0].images[0].enc == Encoding.jbig2
def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
# Runs: ocrmypdf - output.pdf < testfile.pdf
with open(input_file, 'rb') as input_stream:
p_args = ocrmypdf_exec + ['-', output_file]
p = run(
p_args,
stdout=PIPE,
stderr=PIPE,
stdin=input_stream,
env=spoof_tesseract_noop,
)
assert p.returncode == ExitCode.ok
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
# Runs: ocrmypdf francais.pdf - > test_stdout.pdf
with open(output_file, 'wb') as output_stream:
p_args = ocrmypdf_exec + [input_file, '-']
p = run(
p_args,
stdout=output_stream,
stderr=PIPE,
stdin=DEVNULL,
env=spoof_tesseract_noop,
)
assert p.returncode == ExitCode.ok
assert qpdf.check(output_file, log=None)
@pytest.mark.skipif(
sys.version_info[0:3] >= (3, 6, 4), reason="issue fixed in Python 3.6.4"
)
def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
def evil_closer():
os.close(0)
os.close(1)
p_args = ocrmypdf_exec + [input_file, output_file]
p = Popen( # pylint: disable=subprocess-popen-preexec-fn
p_args,
close_fds=True,
stdout=None,
stderr=PIPE,
stdin=None,
env=spoof_tesseract_noop,
preexec_fn=evil_closer,
)
out, err = p.communicate()
print(err.decode())
assert p.returncode == ExitCode.ok
def test_masks(spoof_tesseract_noop, resources, outpdf):
assert (
ocrmypdf.ocr(
@@ -625,19 +377,6 @@ def test_linearized_pdf_and_indirect_object(spoof_tesseract_noop, resources, out
check_ocrmypdf(resources / 'epson.pdf', outpdf, env=spoof_tesseract_noop)
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa
)
assert (
p.returncode == ExitCode.pdfa_conversion_failed
), "Unexpected return when PDF/A fails"
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
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)
@@ -655,7 +394,7 @@ def test_overlay(spoof_tesseract_noop, resources, outpdf):
def test_destination_not_writable(spoof_tesseract_noop, resources, outdir):
if os.getuid() == 0 or os.geteuid() == 0:
if os.name != 'nt' and (os.getuid() == 0 or os.geteuid() == 0):
pytest.xfail(reason="root can write to anything")
protected_file = outdir / 'protected.pdf'
protected_file.touch()
@@ -678,26 +417,16 @@ language_model_penalty_non_freq_dict_word 0
)
check_ocrmypdf(
resources / 'ccitt.pdf', outdir / 'out.pdf', '--tesseract-config', cfg_file
)
@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',
resources / '3small.pdf',
outdir / 'out.pdf',
'--pdf-renderer',
renderer,
'--tesseract-config',
cfg_file,
'--pages',
'1',
)
assert "Can't open" in err, "No error message about missing config file"
assert p.returncode == ExitCode.ok, err
@pytest.mark.slow # This test sometimes times out in CI
@pytest.mark.parametrize('renderer', RENDERERS)
def test_tesseract_config_invalid(renderer, resources, outdir):
cfg_file = outdir / 'test.cfg'
@@ -716,30 +445,18 @@ THIS FILE IS INVALID
'--tesseract-config',
cfg_file,
)
assert "parameter not found" in err.lower(), "No error message"
assert (
"parameter not found" in err.lower()
or "error occurred while parsing" in err.lower()
), "No error message"
assert p.returncode == ExitCode.invalid_config
@pytest.mark.skipif(tesseract.v4(), reason='arg has no effect in 4.0-beta1')
def test_user_words(resources, outdir):
@pytest.mark.skipif(not tesseract.has_user_words(), reason='not functional until 4.1.0')
def test_user_words_ocr(resources, outdir):
# Does not actually test if --user-words causes output to differ
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()
sidecar_after = outdir / 'sidecar.txt'
with word_list.open('w') as f:
f.write('cromulent\n') # a perfectly cromulent word
@@ -755,9 +472,6 @@ def test_user_words(resources, outdir):
word_list,
)
if consistent:
assert 'cromulent' in sidecar_after.open().read()
def test_form_xobject(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(
@@ -770,7 +484,7 @@ def test_pagesize_consistency(renderer, resources, outpdf):
first_page_dimensions = pytest.helpers.first_page_dimensions
infile = resources / 'linn.pdf'
infile = resources / '3small.pdf'
before_dims = first_page_dimensions(infile)
@@ -783,12 +497,14 @@ def test_pagesize_consistency(renderer, resources, outpdf):
'--deskew',
'--remove-background',
'--clean-final' if pytest.helpers.have_unpaper() else None,
'--pages',
'1',
)
after_dims = first_page_dimensions(outpdf)
assert isclose(before_dims[0], after_dims[0])
assert isclose(before_dims[1], after_dims[1])
assert isclose(before_dims[0], after_dims[0], rel_tol=1e-4)
assert isclose(before_dims[1], after_dims[1], rel_tol=1e-4)
def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
@@ -802,22 +518,6 @@ def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
)
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 on pages with no contents",
@@ -878,6 +578,7 @@ def test_compression_preserved(
assert pdfimage.color == Colorspace.rgb, "Colorspace changed"
elif im.mode.startswith('L'):
assert pdfimage.color == Colorspace.gray, "Colorspace changed"
im.close()
@pytest.mark.parametrize(
@@ -939,12 +640,13 @@ def test_compression_changed(
assert pdfimage.color == Colorspace.rgb, "Colorspace changed"
elif im.mode.startswith('L'):
assert pdfimage.color == Colorspace.gray, "Colorspace changed"
im.close()
def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
sidecar = outpdf.with_suffix('.txt')
check_ocrmypdf(
resources / 'multipage.pdf',
resources / '3small.pdf',
outpdf,
'--skip-text',
'--sidecar',
@@ -952,10 +654,10 @@ def test_sidecar_pagecount(spoof_tesseract_cache, resources, outpdf):
env=spoof_tesseract_cache,
)
pdfinfo = PdfInfo(resources / 'multipage.pdf')
pdfinfo = PdfInfo(resources / '3small.pdf')
num_pages = len(pdfinfo)
with open(sidecar, 'r') as f:
with open(sidecar, 'r', encoding='utf-8') as f:
ocr_text = f.read()
# There should a formfeed between each pair of pages, so the count of
@@ -971,7 +673,7 @@ def test_sidecar_nonempty(spoof_tesseract_cache, resources, outpdf):
resources / 'ccitt.pdf', outpdf, '--sidecar', sidecar, env=spoof_tesseract_cache
)
with open(sidecar, 'r') as f:
with open(sidecar, 'r', encoding='utf-8') as f:
ocr_text = f.read()
assert 'the' in ocr_text
@@ -993,36 +695,6 @@ def test_pdfa_n(spoof_tesseract_cache, pdfa_level, resources, outpdf):
assert pdfa_info['conformance'] == f'PDF/A-{pdfa_level}B'
@pytest.mark.skipif(sys.version_info >= (3, 7, 0), reason='better utf-8')
@pytest.mark.skipif(
Path('/etc/alpine-release').exists(), reason="invalid test on alpine"
)
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(
PIL.__version__ < '5.0.0', reason="Pillow < 5.0.0 doesn't raise the exception"
)
@@ -1038,25 +710,18 @@ def test_decompression_bomb(resources, outpdf):
def test_text_curves(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'vector.pdf', outpdf, env=spoof_tesseract_noop)
with patch('ocrmypdf._pipeline.VECTOR_PAGE_DPI', 100):
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"
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
)
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"
info = PdfInfo(outpdf)
assert len(info.pages[0].images) != 0, "force did not rasterize"
def test_output_is_dir(spoof_tesseract_noop, resources, outdir):
@@ -1067,6 +732,7 @@ def test_output_is_dir(spoof_tesseract_noop, resources, outdir):
assert 'is not a writable file' in err
@pytest.mark.skipif(os.name == 'nt', reason="symlink needs admin permissions")
def test_output_is_symlink(spoof_tesseract_noop, resources, outdir):
sym = Path(outdir / 'this_is_a_symlink')
sym.symlink_to(outdir / 'out.pdf')
@@ -1125,3 +791,14 @@ def test_fast_web_view(
)
with pikepdf.open(outpdf) as pdf:
assert pdf.is_linearized == expected
def test_image_dpi_not_image(caplog, spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(
resources / 'trivial.pdf',
outpdf,
'--image-dpi',
'100',
env=spoof_tesseract_noop,
)
assert '--image-dpi is being ignored' in caplog.text
+7 -8
View File
@@ -17,21 +17,22 @@
import datetime
from datetime import timezone
import logging
import mmap
import os
from datetime import timezone
from os import fspath
from pathlib import Path
from shutil import copyfile, move
from unittest.mock import MagicMock, patch
import pytest
import pikepdf
import pytest
from pikepdf.models.metadata import decode_pdf_date
from ocrmypdf._jobcontext import PDFContext
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
from pikepdf.models.metadata import decode_pdf_date
try:
import fitz
@@ -330,10 +331,8 @@ def test_prevent_gs_invalid_xml(resources, outdir):
str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps'), context
)
with open(outdir / 'pdfa.pdf', 'rb') as f:
with mmap.mmap(
f.fileno(), 0, flags=mmap.MAP_PRIVATE, prot=mmap.PROT_READ
) as mm:
with open(outdir / 'pdfa.pdf', 'r+b') as f:
with mmap.mmap(f.fileno(), 0) as mm:
# Since the XML may be invalid, we scan instead of actually feeding it
# to a parser.
XMP_MAGIC = b'W5M0MpCehiHzreSzNTczkc9d'
+9 -8
View File
@@ -19,10 +19,10 @@ import logging
from os import fspath
from pathlib import Path
import pikepdf
import pytest
from PIL import Image
import pikepdf
from ocrmypdf import optimize as opt
from ocrmypdf.exec import jbig2enc, pngquant
from ocrmypdf.exec.ghostscript import rasterize_pdf
@@ -48,13 +48,14 @@ def test_mono_not_inverted(resources, outdir):
xres=10,
yres=10,
raster_device='pnggray',
log=logging.getLogger(name='test_mono_flip'),
log=logging.getLogger(name='test_mono_not_inverted'),
)
im = Image.open(fspath(outdir / 'im.png'))
assert im.getpixel((0, 0)) == 255, "Expected white background"
with Image.open(fspath(outdir / 'im.png')) as im:
assert im.getpixel((0, 0)) == 255, "Expected white background"
@pytest.mark.skipif(not pngquant.available(), reason='need pngquant')
def test_jpg_png_params(resources, outpdf, spoof_tesseract_noop):
check_ocrmypdf(
resources / 'crom.png',
@@ -109,10 +110,10 @@ def test_flate_to_jbig2(resources, outdir, spoof_tesseract_noop):
# This test requires an image that pngquant is capable of converting to
# to 1bpp - so use an existing 1bpp image, convert up, confirm it can
# convert down
im = Image.open(fspath(resources / 'typewriter.png'))
assert im.mode in ('1', 'P')
im = im.convert('L')
im.save(fspath(outdir / 'type8.png'))
with Image.open(fspath(resources / 'typewriter.png')) as im:
assert im.mode in ('1', 'P')
im = im.convert('L')
im.save(fspath(outdir / 'type8.png'))
check_ocrmypdf(
outdir / 'type8.png',
+1 -1
View File
@@ -19,8 +19,8 @@ import pytest
import ocrmypdf
from ocrmypdf._validation import _pages_from_ranges
from ocrmypdf.pdfinfo import PdfInfo
from ocrmypdf.exceptions import BadArgsError
from ocrmypdf.pdfinfo import PdfInfo
@pytest.mark.parametrize(
+14 -14
View File
@@ -20,11 +20,11 @@ from math import isclose
from tempfile import NamedTemporaryFile
import img2pdf
import pikepdf
import pytest
from PIL import Image
from reportlab.pdfgen.canvas import Canvas
import pikepdf
from ocrmypdf import pdfinfo
from ocrmypdf.pdfinfo import Colorspace, Encoding
@@ -91,21 +91,21 @@ def test_single_page_image(outdir):
def test_single_page_inline_image(outdir):
filename = outdir / 'image-mono-inline.pdf'
pdf = Canvas(str(filename), pagesize=(8 * 72, 6 * 72))
with NamedTemporaryFile() as im_tmp:
im = Image.new('1', (8, 8), 0)
for n in range(8):
im.putpixel((n, n), 1)
im.save(im_tmp.name, format='PNG')
# Draw image in a 72x72 pt or 1"x1" area
pdf.drawInlineImage(im_tmp.name, 0, 0, width=72, height=72)
pdf.showPage()
pdf.save()
pdf = pdfinfo.PdfInfo(filename)
print(pdf)
pdfimage = pdf[0].images[0]
im = Image.new('1', (8, 8), 0)
for n in range(8):
im.putpixel((n, n), 1)
# Draw image in a 72x72 pt or 1"x1" area
pdf.drawInlineImage(im, 0, 0, width=72, height=72)
pdf.showPage()
pdf.save()
info = pdfinfo.PdfInfo(filename)
print(info)
pdfimage = info[0].images[0]
assert isclose(pdfimage.xres, 8)
assert pdfimage.color == Colorspace.rgb # reportlab produces color image
assert pdfimage.color == Colorspace.gray
assert pdfimage.width == 8
+185
View File
@@ -0,0 +1,185 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
from math import isclose
import pytest
from PIL import Image
from ocrmypdf.exec import ghostscript
from ocrmypdf.leptonica import Pix
from ocrmypdf.pdfinfo import PdfInfo
# pytest.helpers is dynamic
# pylint: disable=no-member,redefined-outer-name
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
spoof = pytest.helpers.spoof
RENDERERS = ['hocr', 'sandwich']
def test_deskew(spoof_tesseract_noop, resources, outdir):
# Run with deskew
deskewed_pdf = check_ocrmypdf(
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
log = logging.getLogger()
deskewed_png = outdir / 'deskewed.png'
ghostscript.rasterize_pdf(
deskewed_pdf,
deskewed_png,
xres=150,
yres=150,
raster_device='pngmono',
log=log,
pageno=1,
)
pix = Pix.open(deskewed_png)
skew_angle, _skew_confidence = pix.find_skew()
print(skew_angle)
assert -0.5 < skew_angle < 0.5, "Deskewing failed"
def test_remove_background(spoof_tesseract_noop, resources, outdir):
# Ensure the input image does not contain pure white/black
with Image.open(resources / 'congress.jpg') as im:
assert im.getextrema() != ((0, 255), (0, 255), (0, 255))
output_pdf = check_ocrmypdf(
resources / 'congress.jpg',
outdir / 'test_remove_bg.pdf',
'--remove-background',
'--image-dpi',
'150',
env=spoof_tesseract_noop,
)
log = logging.getLogger()
output_png = outdir / 'remove_bg.png'
ghostscript.rasterize_pdf(
output_pdf,
output_png,
xres=100,
yres=100,
raster_device='png16m',
log=log,
pageno=1,
)
# The output image should contain pure white and black
with Image.open(output_png) as im:
assert im.getextrema() == ((0, 255), (0, 255), (0, 255))
# This will run 5 * 2 * 2 = 20 test cases
@pytest.mark.parametrize(
"pdf", ['palette.pdf', 'cmyk.pdf', 'ccitt.pdf', 'jbig2.pdf', 'lichtenstein.pdf']
)
@pytest.mark.parametrize("renderer", ['sandwich', 'hocr'])
@pytest.mark.parametrize("output_type", ['pdf', 'pdfa'])
def test_exotic_image(
spoof_tesseract_cache, pdf, renderer, output_type, resources, outdir
):
outfile = outdir / f'test_{pdf}_{renderer}.pdf'
check_ocrmypdf(
resources / pdf,
outfile,
'-dc' if pytest.helpers.have_unpaper() else '-d',
'-v',
'1',
'--output-type',
output_type,
'--sidecar',
'--skip-text',
'--pdf-renderer',
renderer,
env=spoof_tesseract_cache,
)
assert outfile.with_suffix('.pdf.txt').exists()
@pytest.mark.parametrize('renderer', RENDERERS)
def test_non_square_resolution(renderer, spoof_tesseract_cache, resources, outpdf):
# Confirm input image is non-square resolution
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 = 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
@pytest.mark.parametrize('renderer', RENDERERS)
def test_convert_to_square_resolution(
renderer, spoof_tesseract_cache, resources, outpdf
):
# Confirm input image is non-square resolution
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(
resources / 'aspect.pdf',
outpdf,
'--force-ocr',
'--pdf-renderer',
renderer,
env=spoof_tesseract_cache,
)
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
# 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)
# 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].xres
out_im_h = out_p0.images[0].height / out_p0.images[0].yres
assert isclose(out_p0.width_inches, out_im_w)
assert isclose(out_p0.height_inches, out_im_h)
+6 -6
View File
@@ -21,10 +21,10 @@ from os import fspath
from unittest.mock import Mock
import img2pdf
import pikepdf
import pytest
from PIL import Image
import pikepdf
from ocrmypdf import leptonica
from ocrmypdf.exec import ghostscript, tesseract
from ocrmypdf.pdfinfo import PdfInfo
@@ -224,12 +224,12 @@ def test_rotate_deskew_timeout(resources, outdir):
@pytest.mark.parametrize('image_angle', (0, 90, 180, 270))
def test_rotate_page_level(image_angle, page_angle, resources, outdir):
def make_rotate_test(prefix, image_angle, page_angle):
im = Image.open(fspath(resources / 'typewriter.png'))
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image, f'ROTATE_{ccw_angle}'))
memimg = BytesIO()
im.save(memimg, format='PNG')
with Image.open(fspath(resources / 'typewriter.png')) as im:
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image, f'ROTATE_{ccw_angle}'))
im.save(memimg, format='PNG')
memimg.seek(0)
mempdf = BytesIO()
img2pdf.convert(
+128
View File
@@ -0,0 +1,128 @@
# © 2019 James R. Barlow: github.com/jbarlow83
#
# This file is part of OCRmyPDF.
#
# OCRmyPDF is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OCRmyPDF is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from pathlib import Path
from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run
import pytest
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec import qpdf
# pytest.helpers is dynamic
# pylint: disable=no-member,redefined-outer-name
run_ocrmypdf = pytest.helpers.run_ocrmypdf
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof
@pytest.fixture(scope='session')
def spoof_tess_bad_utf8(tmp_path_factory):
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
# Runs: ocrmypdf - output.pdf < testfile.pdf
with open(input_file, 'rb') as input_stream:
p_args = ocrmypdf_exec + ['-', output_file]
p = run(
p_args,
stdout=PIPE,
stderr=PIPE,
stdin=input_stream,
env=spoof_tesseract_noop,
)
assert p.returncode == ExitCode.ok
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
# Runs: ocrmypdf francais.pdf - > test_stdout.pdf
with open(output_file, 'wb') as output_stream:
p_args = ocrmypdf_exec + [input_file, '-']
p = run(
p_args,
stdout=output_stream,
stderr=PIPE,
stdin=DEVNULL,
env=spoof_tesseract_noop,
)
assert p.returncode == ExitCode.ok
assert qpdf.check(output_file, log=None)
@pytest.mark.skipif(
sys.version_info[0:3] >= (3, 6, 4), reason="issue fixed in Python 3.6.4"
)
@pytest.mark.skipif(os.name == 'nt', reason="POSIX problem")
def test_closed_streams(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
input_file = str(resources / 'francais.pdf')
output_file = str(outpdf)
def evil_closer():
os.close(0)
os.close(1)
p_args = ocrmypdf_exec + [input_file, output_file]
p = Popen( # pylint: disable=subprocess-popen-preexec-fn
p_args,
close_fds=True,
stdout=None,
stderr=PIPE,
stdin=None,
env=spoof_tesseract_noop,
preexec_fn=evil_closer,
)
out, err = p.communicate()
print(err.decode())
assert p.returncode == ExitCode.ok
@pytest.mark.skipif(sys.version_info >= (3, 7, 0), reason='better utf-8')
@pytest.mark.skipif(
Path('/etc/alpine-release').exists(), reason="invalid test on alpine"
)
@pytest.mark.skipif(os.name == 'nt', reason="invalid test on Windows")
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.xfail(
os.name == 'nt' and sys.version_info < (3, 8),
reason="Windows does not like this; not sure how to fix",
)
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"
-24
View File
@@ -37,30 +37,6 @@ def test_tesseract_v4():
assert tesseract.v4()
def test_pagesize_consistency_tess4(resources, outpdf):
from math import isclose
infile = resources / 'linn.pdf'
before_dims = pytest.helpers.first_page_dimensions(infile)
check_ocrmypdf(
infile,
outpdf,
'--pdf-renderer',
'sandwich',
'--clean' if pytest.helpers.have_unpaper() else None,
'--deskew',
'--remove-background',
'--clean-final' if pytest.helpers.have_unpaper() else None,
)
after_dims = pytest.helpers.first_page_dimensions(outpdf)
assert isclose(before_dims[0], after_dims[0])
assert isclose(before_dims[1], after_dims[1])
@pytest.mark.parametrize('basename', ['graph_ocred.pdf', 'cardinal.pdf'])
def test_skip_pages_does_not_replicate(resources, basename, outdir):
infile = resources / basename
+1 -1
View File
@@ -20,8 +20,8 @@ from unittest.mock import patch
import pytest
from ocrmypdf.cli import parser
from ocrmypdf._validation import check_options
from ocrmypdf.cli import parser
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.exec import unpaper
+5 -3
View File
@@ -24,6 +24,7 @@ from ocrmypdf.pdfinfo import PdfInfo
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
spoof = pytest.helpers.spoof
@@ -32,9 +33,10 @@ 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_ghostscript_fails(poster, no_outpdf, caplog):
result = run_ocrmypdf_api(poster, no_outpdf, '--output-type=pdfa')
assert result == ExitCode.input_file
assert 'not supported by Ghostscript' in caplog.text
def test_userunit_qpdf_passes(spoof_tesseract_cache, poster, outpdf):
+72 -9
View File
@@ -15,20 +15,22 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
import ocrmypdf._validation as vd
from ocrmypdf.api import create_options
from ocrmypdf.exceptions import MissingDependencyError, BadArgsError
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
from ocrmypdf.pdfinfo import PdfInfo
def make_opts(input_file='a.pdf', output_file='b.pdf', language='eng', **kwargs):
return create_options(
input_file=input_file, output_file=output_file, language=language, **kwargs
)
if language is not None:
kwargs['language'] = language
return create_options(input_file=input_file, output_file=output_file, **kwargs)
def test_hocr_notlatin_warning(caplog):
@@ -37,15 +39,21 @@ def test_hocr_notlatin_warning(caplog):
def test_old_ghostscript(caplog):
with patch('ocrmypdf.exec.ghostscript.version', return_value='9.19'):
with patch('ocrmypdf.exec.ghostscript.version', return_value='9.19'), patch(
'ocrmypdf.exec.tesseract.has_textonly_pdf', return_value=True
):
vd.check_options_output(make_opts(language='chi_sim', output_type='pdfa'))
assert 'Ghostscript does not work correctly' in caplog.text
with patch('ocrmypdf.exec.ghostscript.version', return_value='9.18'):
with patch('ocrmypdf.exec.ghostscript.version', return_value='9.18'), patch(
'ocrmypdf.exec.tesseract.has_textonly_pdf', return_value=True
):
with pytest.raises(MissingDependencyError):
vd.check_options_output(make_opts(output_type='pdfa-3'))
with patch('ocrmypdf.exec.ghostscript.version', return_value='9.24'):
with patch('ocrmypdf.exec.ghostscript.version', return_value='9.24'), patch(
'ocrmypdf.exec.tesseract.has_textonly_pdf', return_value=True
):
with pytest.raises(MissingDependencyError):
vd.check_dependency_versions(make_opts())
@@ -108,8 +116,63 @@ def test_report_file_size(tmp_path, caplog):
opts = make_opts()
vd.report_output_file_size(opts, in_, out)
assert caplog.text == ''
caplog.clear()
os.truncate(in_, 25001)
os.truncate(out, 50000)
with patch('ocrmypdf._validation.jbig2enc.available', return_value=True), patch(
'ocrmypdf._validation.pngquant.available', return_value=True
):
vd.report_output_file_size(opts, in_, out)
assert 'No reason' in caplog.text
caplog.clear()
with patch('ocrmypdf._validation.jbig2enc.available', return_value=False), patch(
'ocrmypdf._validation.pngquant.available', return_value=True
):
vd.report_output_file_size(opts, in_, out)
assert 'optional dependency' in caplog.text
caplog.clear()
opts = make_opts(in_, out, optimize=0)
vd.report_output_file_size(opts, in_, out)
assert 'No reason' in caplog.text
assert 'disabled' in caplog.text
caplog.clear()
def test_false_action_store_true():
opts = make_opts(keep_temporary_files=True)
assert opts.keep_temporary_files == True
opts = make_opts(keep_temporary_files=False)
assert opts.keep_temporary_files == False
@pytest.mark.parametrize('progress_bar', [True, False])
def test_no_progress_bar(progress_bar, resources):
opts = make_opts(progress_bar=progress_bar, input_file=(resources / 'trivial.pdf'))
with patch('ocrmypdf.pdfinfo.info.tqdm', autospec=True) as tqdmpatch:
vd.check_options(opts)
pdfinfo = PdfInfo(opts.input_file, progbar=opts.progress_bar)
assert pdfinfo is not None
assert tqdmpatch.called
_args, kwargs = tqdmpatch.call_args
assert kwargs['disable'] != progress_bar
def test_language_warning(caplog):
opts = make_opts(language=None)
caplog.set_level(logging.DEBUG)
with patch(
'ocrmypdf._validation.locale.getlocale', return_value=('en_US', 'UTF-8')
):
vd.check_options_languages(opts)
assert opts.language == ['eng']
assert '' in caplog.text
opts = make_opts(language=None)
with patch(
'ocrmypdf._validation.locale.getlocale', return_value=('fr_FR', 'UTF-8')
):
vd.check_options_languages(opts)
assert opts.language == ['eng']
assert 'assuming --language' in caplog.text