Compare commits

...
245 Commits
Author SHA1 Message Date
James R. Barlow 2cff6ad2d1 Fixed blank pages produced when NULs removed from metadata 2019-07-03 02:22:50 -07:00
James R. Barlow 1cc4c45b7e docs: mention WSL works
[ci skip]
2019-07-03 00:49:06 -07:00
James R. Barlow 07d4fff3d4 docs: mention FreeBSD works 2019-05-14 02:13:56 -07:00
James R. Barlow 7e388f59af Move completions to better location/Homebrew compat 2019-05-13 00:22:52 -07:00
James R. Barlow 84ec83151d v8.3.0 notes: clarify 2019-05-12 03:36:41 -07:00
James R. Barlow 0a72c12ff0 weave: add new test for link consistency 2019-05-12 03:36:33 -07:00
James R. Barlow 5adbddd1f9 ghostscript: rendering threads has no effect on pdfwrite, so remove it
See
https://www.ghostscript.com/doc/current/Language.htm
2019-05-12 03:36:07 -07:00
James R. Barlow c2fecffdb4 Require pikepdf 1.3.0 2019-05-12 02:16:05 -07:00
James R. Barlow 7dea0a8362 v8.3.0 release notes in progress 2019-05-11 12:50:44 -07:00
James R. Barlow 482cb788ed Don't use MagicMock() as a dummy logger in pytest 2019-05-11 12:44:17 -07:00
James R. Barlow 15a988b999 weave: use emplacement method, scrap TOC repair
The new emplacement method updates page objects in place without
generating new objgen numbers, meaning we no longer need to update the table
of contents to preserve links.
2019-05-11 12:40:25 -07:00
James R. Barlow 83398e54ea weave: fix corruption of certain high page count files
Corruption occurred when replacements was not incremented for multiple
consecutive pages.
2019-05-11 12:22:21 -07:00
James R. Barlow bcdd196699 ghostscript: remove unnecessary post-render resizing step 2019-05-11 12:10:50 -07:00
James R. Barlow 0cd576e701 Rename bash completions file 2019-05-11 10:52:42 -07:00
James R. Barlow 4d5e0eb749 docs: mention completions 2019-05-06 18:07:41 -07:00
Frankandjbarlow83 7ed0f8f50e Add bash completion (#384)
* Add bash completion

file must be copied to completion folder e.g. /usr/share/bash-completion/completions
2019-05-06 15:18:15 -07:00
James R. Barlow 79c84eefa3 Fix main.txt 2019-04-23 02:21:31 -07:00
James R. Barlow 5398003160 Fix test.txt 2019-04-23 00:42:40 -07:00
James R. Barlow 58b2bed99d v8.2.4 notes 2019-04-23 00:07:12 -07:00
James R. Barlow 58c29ffb5c weave: use explicit pdf.close(), drastically reduce open file handles
With the new pikepdf 1.2.0 we no longer need to hold file handles
open because of the "copy to memory" functionality. We retain
the behavior of closing/reopening the output PDF every 100 pages as
a way to limit memory usage.
2019-04-18 15:12:48 -07:00
James R. Barlow f615b6f0e8 pdfinfo: be more specific about detecting XFA we can't render 2019-04-18 15:07:25 -07:00
James R. Barlow e0c8dadcce Explicitly close most pikepdf.Pdf when done with them 2019-04-18 15:02:12 -07:00
James R. Barlow 9a86f53109 Ignore pip-wheel-metadata folder
https://github.com/pypa/pip/issues/6213
2019-04-18 10:42:10 -07:00
James R. Barlow 91cb092aa0 Remove PyCharm debugger hack 2019-04-18 10:15:02 -07:00
James R. Barlow 922a107b7f Remove safety traversal of PDF table of contents
qpdf fixed the danging reference issue (qpdf #240) in 8.3.0, which is
required by pikepdf 1.1.0. We no
longer need the workaround.
2019-04-13 00:24:03 -07:00
James R. Barlow 6e49bb3588 v8.2.3 notes 2019-04-03 01:19:12 -07:00
James R. Barlow 427afc0616 Fix LeptonicaErrorTrap when a sys.stderr.fileno() is not available
The LeptonicaErrorTrap was problematic for Celery and other
libraries that mess with stderr.

Closes #359
2019-03-17 14:22:36 -07:00
James R. Barlow 9c7ee2bf23 Better help text for --verbose 2019-03-17 13:29:25 -07:00
James R. Barlow c5cfaa950b readme: tweaks 2019-03-16 14:09:19 -07:00
James R. Barlow 4e2a98ead4 leptonica: fix junkpixt harder 2019-03-16 14:08:58 -07:00
James R. Barlow 210f134b5b Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2019-03-08 15:38:31 -08:00
James R. Barlow 696c0721a0 docs: fix broken sphinx ref
[ci skip]
2019-03-08 15:38:26 -08:00
James R. Barlow aabab95418 docs: use images folder 2019-03-08 15:38:01 -08:00
James R. Barlow 7d614dd68b docs: explain Automator workflow 2019-03-08 15:37:42 -08:00
jumbliesandjbarlow83 f57dda7939 Update batch.rst (#362)
Added docker instructions for passing "find" filenames into container.  Obviates prior incorrect flag fix.
2019-03-08 12:46:50 -08:00
James R. Barlow 1b4542aa77 Further fixes to external program version testing 2019-03-07 14:27:16 -08:00
James R. Barlow 6c7fca57ec v8.2.1 notes 2019-03-06 22:22:50 -08:00
James R. Barlow 486dc7e22c Fix some test failures missed in prev commit 2019-03-06 13:28:50 -08:00
James R. Barlow dc616bb507 Fix test suite so --clean is not requested when unpaper is not installed 2019-03-05 22:33:13 -08:00
James R. Barlow 902bda43e3 main: fix version testing unnecessarily throwing exception to itself 2019-03-05 22:32:06 -08:00
James R. Barlow f7da63f68b main: fix redundant argument test 2019-03-05 22:29:29 -08:00
James R. Barlow 5da26e4c9c Convert most uses of subprocess.Popen to subprocess.run in test suite 2019-03-05 22:25:22 -08:00
James R. Barlow c19c852705 Fix exception while attempting to print error message for missing program 2019-03-05 16:32:48 -08:00
James R. Barlow a27ee3ee8c optimize: use Decode to invert 1bpp PNGs for now 2019-03-03 17:50:12 -08:00
James R. Barlow c2f316c2c5 v8.2.0 release notes: optimizer 2019-03-03 15:26:01 -08:00
James R. Barlow 974979b0a0 Merge branch 'feature/optimization-fixes' 2019-03-03 15:00:20 -08:00
James R. Barlow 66586bdaab optimize: Disable jpg->png migration
Needs more testing before release
2019-03-03 14:59:59 -08:00
James R. Barlow 01d2ea309f Fix Predictor name and photometric flip 2019-03-03 14:57:15 -08:00
James R. Barlow e918480351 v8.2.0 release notes 2019-03-03 14:15:20 -08:00
James R. Barlow 52fd84fa95 Remove debug message 2019-03-03 13:31:10 -08:00
James R. Barlow 2c56b0935c docs: minor 2019-03-03 03:28:17 -08:00
James R. Barlow 4f69ace868 optimize: fix all JBIG2 images binned on last page
During some past refactor it appears we now end up treating
all JBIG2 images as if they appeared on the last page in the
file. This bug had no visual side ffects but probably led to
suboptimal JBIG2 encoding.
2019-03-03 03:28:17 -08:00
James R. Barlow 497c531112 optimize: update comments 2019-03-03 03:28:17 -08:00
James R. Barlow b27b92fbf3 optimize: on aggressive settings try JPG to PNG transcoding
If the color count of an image is low such as when black and white
documents are scanned in color, PNG with lossy quantization may
produce a superior encoding to JPEG. This is expensive to test however.
2019-03-03 03:28:17 -08:00
James R. Barlow 2e6ba2df8c optimize: fix recoding of PNGs
Previously we opened pngquant-compressed PNGs with transcoding
because the transcode free function in Leptonica didn't seem to
work. This mean Leptonica may have thrown away the hard of
pngquant if didn't understand the encoding.

This change resolves the issue and allows us to open PNG encoded
data and insert it into a PDF without transcoding. Should improve
encoding quality.
2019-03-03 03:28:17 -08:00
James R. Barlow 67a405c6b7 Move install-time external program checks out of setup.py
We did runtime tests for several of them anyway, and it's better to do
at runtime since config may change after installation.
2019-03-03 03:26:56 -08:00
James R. Barlow 58e6663806 Update test cache for french->german change 2019-03-03 03:23:59 -08:00
James R. Barlow 602570fcf9 Update requirements 2019-03-03 02:27:56 -08:00
James R. Barlow 691f8ce254 Docs: reorganize for new docker-alpine image 2019-03-01 23:15:32 -08:00
James R. Barlow 22812e74b9 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2019-02-26 13:01:59 -08:00
Martin Windandjbarlow83 9d824e723d Add Dockerfile based on alpine:3.9 (#354)
* Do not exclude .git from docker build

* Use multi-stage builds to keep the image size down

* Copy project files to get the test suite.

* Add webservice

* Add tesseract language data for German and Chinese Simplified
2019-02-26 13:01:38 -08:00
James R. Barlow 5dad800d85 Add version to build-system declaration 2019-02-26 12:58:44 -08:00
James R. Barlow 56a56a4dcb docs: avoid importing ocrmypdf 2019-02-26 12:57:50 -08:00
James R. Barlow 3f1d9ef99c Fix tests for move to Alpine dockerfile 2019-02-26 12:30:21 -08:00
James R. Barlow 92c8a5885e Declare build system in pyproject.toml 2019-02-26 12:23:33 -08:00
James R. Barlow 7749d14252 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2019-02-24 01:56:47 -08:00
Julien Maandjbarlow83 9b92af5aed README: install other language packs on macOS (#352)
The default homebrew formula installs only the English language pack.
Another brew formula exists to install all other language packs.
This makes it easier than having to do the whole install manually.
2019-02-19 10:13:36 -08:00
James R. Barlow 0bf26b03ae optimize: Modernize pikepdf usage 2019-02-16 14:03:10 -08:00
James R. Barlow e2847ea4c3 v8.1.0 release notes 2019-02-10 02:10:48 -08:00
James R. Barlow 19e35db2b7 Fix issue when weave handoff occurs with no OCR font present
If using --tesseract-timeout 0 and any image processing on a file with
more than 100 pages, the weave handoff will occur. Ensure this
works correctly even if no Glyphless font is present.

Closes #347
2019-02-10 02:05:59 -08:00
James R. Barlow df688742d5 Fix exception on traversing corrupt ToC entries 2019-02-10 00:50:21 -08:00
James R. Barlow 42c2925f9d Activate black precommit 2019-02-08 14:09:08 -08:00
James R. Barlow 933f0b8f9b docs: more unpaper details 2019-02-08 13:05:09 -08:00
James R. Barlow 03ab5a8ee2 If --tesseract-timeout 0, say nothing when we time out
This is our "don't actually OCR" mode. No need to mention it.
2019-02-08 13:04:48 -08:00
James R. Barlow 4f06920224 Be os.nice()-r 2019-02-07 17:24:47 -08:00
James R. Barlow a733b09623 webservice: add an optional config and larger upload limit 2019-02-07 17:24:17 -08:00
James R. Barlow 5483dacf52 Fuzz 2019-02-07 17:09:47 -08:00
James R. Barlow ae7844ad88 --clean-final implies --clean
It's never made sense to leave it out before; might as well introduce it.
2019-02-07 17:08:08 -08:00
James R. Barlow a6e7485da6 docs: --unpaper-args 2019-02-07 17:06:51 -08:00
James R. Barlow 3bcc6d6121 Merge 'feature/unpaper-args' 2019-02-07 17:06:28 -08:00
James R. Barlow 9fe067bbd9 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2019-02-07 16:31:06 -08:00
James R. Barlow f095e91cb4 unpaper-args: add test case and harden feature 2019-02-07 16:21:02 -08:00
Charles Forceyandjbarlow83 66c8d4b47a Adjust the docker pull command for webservice (#346)
Not completely sure this is correct, but I think `docker pull jbarlow83/ocrmypdf-webservice` might be the correct command for getting the web service version.  It installs as expected:

```
docker pull jbarlow83/ocrmypdf-webservice
Using default tag: latest
latest: Pulling from jbarlow83/ocrmypdf-webservice
38e2e6cd5626: Already exists 
705054bc3f5b: Already exists 
c7051e069564: Already exists 
7308e914506c: Already exists 
3977c3cd82d1: Already exists 
ec01b9573956: Already exists 
b508b5192a3c: Already exists 
ace6e737fffb: Already exists 
0a453ee84e11: Already exists 
f8cb8b66151b: Already exists 
f53c3b27b23f: Already exists 
22df51ea5473: Already exists 
e38d932f9f30: Already exists 
b9d3c1d5b53b: Already exists 
68be2088ada3: Already exists 
8b17945ab41b: Pull complete 
59c4aae491bd: Pull complete 
19dce698a07e: Pull complete 
Digest: sha256:0cc9433d490c9a65389403757bf6081a30bcd248055340a8789c23d9cdf9ac8a
Status: Downloaded newer image for jbarlow83/ocrmypdf-webservice:latest
```
2019-01-25 10:41:42 -08:00
James R. Barlow 721489a06c docs: remove reference to --skip-repair since the argument was removed 2019-01-18 05:44:11 -08:00
James R. Barlow 9a4493f211 Add --unpaper-args
Needs test code and stricter validation
2019-01-18 05:33:28 -08:00
James R. Barlow edb4d6c586 docs: Clarify ArchLinux edition is in AUR 2019-01-18 05:29:37 -08:00
James R. Barlow b8cd3acd9e v8.0.1 notes 2019-01-17 00:57:28 -08:00
James R. Barlow 03779e33da docs: Update some install procedures for v8 changes
[ci skip]
2019-01-12 00:33:36 -08:00
James R. Barlow c466483e82 docs: Explain intermediate files 2019-01-11 14:52:05 -08:00
James R. Barlow e3a58219d1 Ensure XObjects with no subtype don't cause an exception
Closes #325
2019-01-08 16:46:08 -08:00
James R. Barlow 72337094ca v8.0.0 release notes 2019-01-05 23:35:47 -08:00
James R. Barlow f472587d22 Bump pikepdf version, point to release notes 2019-01-05 16:48:13 -08:00
James R. Barlow f34b3015b2 Prevent Ghostscript from generating invalid XMP metadata
If DocumentInfo contains NULs Ghostscript will generate XMP with
NULs which is not allowed. Repair DocumentInfo before Ghostscript sees it.
2019-01-04 13:20:41 -08:00
James R. Barlow 089ece2715 use pikepdf 0.10.2 2019-01-03 12:08:43 -08:00
James R. Barlow 6438465e3f Add fish completions 2019-01-02 17:08:30 -08:00
James R. Barlow 7d330afd81 Delinting 2019-01-02 13:34:45 -08:00
James R. Barlow 68fbd9fcc9 pikepdf: version bump 2018-12-31 15:37:31 -08:00
James R. Barlow c771938907 Convert to f-strings where it makes sense 2018-12-31 15:01:19 -08:00
James R. Barlow c2a947acf4 travis: fix 2018-12-31 01:18:30 -08:00
James R. Barlow 8c0009c5c8 Make pdfminer.six optional
Mainly since the current release of pdfminer.six lacks a sdist, blocking
homebrew packaging. Also in case other distros don't accept pdfminer.six.
2018-12-31 01:08:43 -08:00
James R. Barlow cfc5cdf47d pdfa: remove a pile of deprecated code
It's now handled in pikepdf.
2018-12-31 00:05:13 -08:00
James R. Barlow 05152a8af9 Remove always-false Tess v3 tests 2018-12-30 02:01:05 -08:00
James R. Barlow 0880b16491 Sort imports with isort 2018-12-30 01:28:15 -08:00
James R. Barlow 06308a22ce Reformat with black 2018-12-30 01:27:49 -08:00
James R. Barlow 80bd7de580 Generate test cache 2018-12-30 01:02:37 -08:00
James R. Barlow 8b90c45437 Drop support for Tesseract 3 2018-12-30 00:47:12 -08:00
James R. Barlow 72b920eb16 Drop support for Python 3.5 2018-12-30 00:23:26 -08:00
James R. Barlow b4a51907d6 Detect when metadata is dropped during PDF/A conversion 2018-12-30 00:13:25 -08:00
James R. Barlow 1ca1221432 leptonica.py: Fix exception on certain types of barcode failures
Closes #322
2018-12-19 17:23:23 -08:00
James R. Barlow 40b72b0fa8 v7.4.1 release notes 2018-12-19 16:41:09 -08:00
James R. Barlow 0e55b4ad52 Travis: remove Brewfile 2018-12-19 16:40:48 -08:00
James R. Barlow 7b4f5a8fc4 docs: try to fix readthedocs
[ci skip]
2018-12-19 15:30:07 -08:00
James R. Barlow 9261a38493 Readme: more media 2018-12-19 15:27:54 -08:00
James R. Barlow cc8ff318ed New issue template 2018-12-19 15:27:44 -08:00
James R. Barlow ad15e845f9 docs: Ghostscript PDF/A XMP metadata loss; ocrmypdf-webservice
[ci skip]
2018-12-17 23:20:49 -08:00
James R. Barlow ab632f57cd v7.4.0 release notes 2018-12-15 15:27:23 -08:00
James R. Barlow 13d20bd993 pdfinfo: tolerate PDFs that overflow and underflow the graphics stack 2018-12-15 15:10:29 -08:00
James R. Barlow b973208137 Require pikepdf 0.9.1 2018-12-15 14:23:10 -08:00
James R. Barlow 942abf8074 Fix reqs/main.txt for pikepdf 0.9.0 2018-12-14 23:29:26 -08:00
James R. Barlow ed9bb985e2 Fix pikepdf 0.9.0 2018-12-14 23:21:13 -08:00
James R. Barlow 5a7a8e573b Require pikepdf 0.9.0 2018-12-14 23:06:57 -08:00
James R. Barlow ce878db913 Rename to polyglot.dockerfile 2018-12-14 23:06:29 -08:00
James R. Barlow a3d58683b2 Update webservice.py with separate license 2018-12-14 23:05:54 -08:00
James R. Barlow 039e8ca7e7 Merge branches 'feature/newer-pike' and 'feature/webapp' 2018-12-14 18:08:31 -08:00
James R. Barlow 0ebbd4e21b Don't open encrypted files, even if password is empty 2018-12-13 22:48:00 -08:00
James R. Barlow 2cb75f6076 Refactor pipeline to make PDF/A conversion a separate step 2018-12-13 20:48:48 -08:00
James R. Barlow 857d871364 Fix regression on Ghostscript path 2018-12-13 20:36:41 -08:00
James R. Barlow 632dab2cc0 Replace Ghostscript DOCINFO and fix 9.25 metadata date regression
We no longer use Ghostscript to manage PDF metadata, instead
omitting the DOCINFO segment from the pdfmark file we generate.

Instead all of the relevant metadata code has been migrated to pikepdf,
and we use that API. This should be more consistent and fixes the
Ghostscript version-depedent quirks.

Also removes our python-xmp-toolkit dependency, except for
testing.
2018-12-13 18:13:30 -08:00
James R. Barlow 7647918f2d setup: suppress XMLParser() warning - defusedxml related 2018-12-12 22:13:32 -08:00
James R. Barlow 75c5d8055c pdfinfo: fix FutureWarning 2018-12-12 22:12:14 -08:00
James R. Barlow a938bbea55 Remove more libxmp dependencies 2018-12-12 22:02:35 -08:00
James R. Barlow 414407fbd6 Deprecate encode/decode_pdf_date and remap to pikepdf version 2018-12-12 22:01:21 -08:00
James R. Barlow 076fc717df pdfa: replace PDF/A checking with pikepdf implementation 2018-12-12 21:41:16 -08:00
James R. Barlow 2a04b2d82b Rename webapp to webservice 2018-12-12 21:29:05 -08:00
James R. Barlow 065db414c0 webapp docker: Build from polyglot 2018-12-12 21:24:04 -08:00
James R. Barlow 19a054a78b Add webapp stuff 2018-12-10 20:03:52 -08:00
James R. Barlow 9df24a81b7 Fix comment in layout.py 2018-11-28 15:16:34 -08:00
James R. Barlow 40c0acd3f2 Support using --force-ocr and --threshold or --mask-barcodes together 2018-11-28 15:16:24 -08:00
James R. Barlow 20db7f0a8f leptonica: delete file junkpixt.png if created 2018-11-28 13:47:55 -08:00
James R. Barlow e54f6ee37f v7.3.1 release notes 2018-11-16 02:13:41 -08:00
James R. Barlow 2da556bf79 Fix unsupported operand Decimal, float 2018-11-16 02:13:25 -08:00
James R. Barlow b183ad8167 Fix barcodes error handling 2018-11-16 02:08:16 -08:00
James R. Barlow 9e6b54c7ed Add test case for Type3 fonts with no Unicode mapping 2018-11-15 21:54:26 -08:00
James R. Barlow d3b334c10f Test case: true type font without Unicode mapping 2018-11-15 16:22:53 -08:00
James R. Barlow 622f2c4bab More argument checking 2018-11-15 15:59:38 -08:00
James R. Barlow 07b638a394 pdfminer: detect TrueType fonts with no valid encoding information 2018-11-15 13:44:11 -08:00
James R. Barlow 9bee2405d8 Leptonica: make threshold functions more flexible 2018-11-15 13:43:34 -08:00
James R. Barlow 8f040491bf Fix erasure of undetectable barcodes 2018-11-15 12:03:51 -08:00
James R. Barlow 8a18988706 Fix 'del draw' exception 2018-11-15 12:02:53 -08:00
James R. Barlow 47a954514b Fix name2unicode ignoring certain markers 2018-11-15 12:02:30 -08:00
James R. Barlow e3b65d4288 Fix detailed page analysis enabled at wrong time 2018-11-15 12:02:08 -08:00
James R. Barlow 4704f7ed1d Add ReadTheDocs yml so we can build with Py3.6 2018-11-12 13:43:17 -08:00
James R. Barlow 3a2745445a Fix docs build 2018-11-12 13:26:04 -08:00
James R. Barlow 12e15bab15 v7.3.0 release notes 2018-11-11 02:05:52 -08:00
James R. Barlow 9593aa4fb9 Merge v7.3.0 development 2018-11-11 01:38:42 -08:00
James R. Barlow 817d520e63 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2018-11-11 01:34:00 -08:00
James R. Barlow 700abbb8a5 Documentation for OCR quality features 2018-11-10 15:48:41 -08:00
James R. Barlow 701ef1df3f Add threshold function to work around Tesseract's poor thresholding of bright backgrounds 2018-11-10 15:34:37 -08:00
James R. Barlow 0f5c484b62 Travis: only need to specify chardet because we use pip install --no-deps 2018-11-10 13:57:04 -08:00
James R. Barlow cc7f2a3f02 Fix Python 3.5 pathlib regressions 2018-11-10 02:11:23 -08:00
James R. Barlow 755b5d87e3 Add missing chardet, implied by pdfminer.six? 2018-11-10 01:50:51 -08:00
James R. Barlow e55a4115e1 Travis: pytest 3.10.0 internal error? 2018-11-10 01:44:05 -08:00
James R. Barlow 16a6fd2ea9 Update docs for --redo-ocr and --mask-barcodes 2018-11-10 01:34:33 -08:00
James R. Barlow e3fce112ed main.txt: wrong pdfminer 2018-11-10 01:32:27 -08:00
James R. Barlow eacd26a68b Mention v6.2.5 release 2018-11-10 01:10:45 -08:00
James R. Barlow 0e88b3c38a Update v7.3.0 release notes 2018-11-10 01:09:19 -08:00
James R. Barlow a2170ef8d6 test: test version check code 2018-11-10 00:56:22 -08:00
James R. Barlow eed0424390 Update requirements 2018-11-10 00:56:04 -08:00
James R. Barlow 5ed05e08b1 Fix "no languages" test and misuse of os.environ 2018-11-09 01:57:11 -08:00
James R. Barlow 58b26f6715 Leptonica: learn to despeckle 1bpp images 2018-11-07 01:49:13 -08:00
James R. Barlow 806daf4284 leptonica: reduce boilerplate for PIX (2/2) 2018-11-06 20:33:40 -08:00
James R. Barlow c64bc9329e leptonica: reduce boilerplate for wrapper classes (except PIX) 2018-11-06 20:12:09 -08:00
James R. Barlow dd01745519 Leptonica: add masked threshold fn 2018-11-06 19:31:06 -08:00
James R. Barlow 501ce726e7 Fix two failing tests 2018-11-06 11:16:08 -08:00
James R. Barlow 03076e89ce Leptonica: reduce verbosity, more error trapping, more garbage collection 2018-11-06 11:10:59 -08:00
James R. Barlow 02f37293ee Integrate barcode masking 2018-11-05 13:01:13 -08:00
James R. Barlow 590942ad14 Leptonica: Add barcode API 2018-11-05 01:48:38 -08:00
James R. Barlow 2ac028c759 test: Add a basic redo OCR test 2018-11-04 15:54:41 -08:00
James R. Barlow 2125b5bfab Remove text detection from our parser interpret_contents
It's redundant now
2018-11-04 15:47:55 -08:00
James R. Barlow b96532caa4 Only do detailed page analysis when needed by --redo-ocr 2018-11-04 15:40:49 -08:00
James R. Barlow 995fc58466 Move Ghostscript text analysis into its own module 2018-11-04 14:55:48 -08:00
James R. Barlow c023cae299 Make pdfminer Type3 patch conditional on PScript5.dll
It appears that PDFs created by this software have a bug in their BBox
which will cause us to misjudge the space occupied by the font.

Other programs probably work around this by ignoring BBox and reading
each character procedure.
2018-11-04 01:53:53 -07:00
James R. Barlow 237eaf9130 Exception message not printed in some cases
Closes #310
2018-11-03 17:10:24 -07:00
James R. Barlow 8b9ab25125 coverage: test compile leptonica 2018-11-02 01:55:25 -07:00
James R. Barlow 77e87abe8f coverage: ensure get_orientation is checked 2018-11-02 01:32:20 -07:00
James R. Barlow 3be02e1e8d coverage: improve leptonic; don't create objects with null pointers 2018-11-02 01:10:10 -07:00
James R. Barlow 64c9ede979 leptonica: barcodes, BOXA 2018-11-02 00:42:01 -07:00
James R. Barlow 5b8d197812 coverage: make it more likely timeout is tested 2018-11-02 00:41:15 -07:00
James R. Barlow 2cba62dc4f coverage: ensure rotation is actually tested 2018-11-02 00:40:56 -07:00
James R. Barlow 288e28328f coverage: add qpdf 2018-11-02 00:37:33 -07:00
James R. Barlow b8214b3c49 coverage: exclude unicodefun.py 2018-11-02 00:33:08 -07:00
James R. Barlow 8681693994 Set up code coverage (it works with multiprocessing now!) 2018-11-02 00:31:50 -07:00
James R. Barlow 1364c63b7c Fix failure to pickle file with AcroForm 2018-11-01 20:07:53 -07:00
James R. Barlow 4ba9e8fe25 Add AcroForm detection 2018-10-30 22:28:44 -07:00
James R. Barlow a195713bb4 Throw exception on corrupt text 2018-10-30 16:35:09 -07:00
James R. Barlow 600d31a907 Require pikepdf 0.3.7 2018-10-30 16:22:05 -07:00
James R. Barlow be31cec332 Add corrupt text warning (when using --redo-ocr) 2018-10-30 16:19:58 -07:00
James R. Barlow 22a7cd3421 Add argument checks for --redo-ocr 2018-10-30 16:19:13 -07:00
James R. Barlow 8b61d2d521 pdfminer: If font descent claims to be positive, treat it as negative 2018-10-30 14:40:53 -07:00
James R. Barlow 559e5269d2 Ensure inline image is parsed correctly
Requires pikepdf > 0.3.6
2018-10-29 23:30:53 -07:00
James R. Barlow ebf6acb318 pdfminer patch: Type3 font height calculation is incorrect
Not sure where it goes wrong or why it needs special treatment, but
this does address it.
2018-10-29 22:27:25 -07:00
James R. Barlow 7acd75f013 pipeline: fix bbox coordinates 2018-10-29 22:26:37 -07:00
James R. Barlow 93623b2226 Refactor TextboxInfo 2018-10-29 14:46:40 -07:00
James R. Barlow d71fd089cb layout: allow names beginning with /i0123 for now
Showed up in GGastro2.pdf. Need to check if this pattern has valid
Unicode mappings but allow for now.
2018-10-29 14:45:59 -07:00
James R. Barlow 05aa43c856 Require pdfminer 2018-10-29 12:45:15 -07:00
James R. Barlow de80fb6bc8 Fix some failing tests after --redo-ocr changes 2018-10-29 11:49:38 -07:00
James R. Barlow 8e396f4be2 Document --redo-ocr more accurately 2018-10-29 02:03:58 -07:00
James R. Barlow efec6da377 Fix error on serializing bad character markers
(Since they held a reference to their font, which in turn, had an
open file handle.)
2018-10-29 02:02:00 -07:00
James R. Barlow 00ef53195e Fix corrupt Unicode mapping detection's false positives 2018-10-29 01:30:19 -07:00
James R. Barlow f564aaf485 Remove only_ocr_text 2018-10-28 22:41:18 -07:00
James R. Barlow 5ac2d31d0d Redo OCR can now handle visible and invisible text, so adjust accordingly
Still can't filter out corrupt text
2018-10-28 14:06:25 -07:00
James R. Barlow fda890ab47 pdfinfo: further layout improvements
Rather than grouping visible/invisible in a custom analysis step,
use pdfminer's analysis and iterate.
Make iteration predicate and return more generic.
2018-10-28 14:05:50 -07:00
Stefan Weilandjbarlow83 a873278c2a Fix some recommendations from LGTM (#309)
* Fix unreachable code

This fixes an issue reported by LGTM.

Signed-off-by: Stefan Weil <sw@weilnetz.de>

* Remove unused imports

This fixes several recommendations from LGTM.

Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-10-28 13:59:58 -07:00
James R. Barlow e6d64be890 pdfinfo: formatting 2018-10-27 23:22:44 -07:00
James R. Barlow 0e4d978d20 pdfinfo: all -> not any 2018-10-27 23:22:28 -07:00
James R. Barlow b12c2cfedf Fix handling of Type3 fonts with no ToUnicode mapping 2018-10-27 01:24:48 -07:00
James R. Barlow 58cc70725e Reorganize around getting bboxes for visible/invisible text 2018-10-26 01:07:02 -07:00
James R. Barlow 339afb02aa --redo-ocr now works in the presence of printable text 2018-10-25 16:53:47 -07:00
James R. Barlow 7ba0ff5c36 Fix strip invisible text bug: missing BT operator 2018-10-25 16:52:23 -07:00
James R. Barlow ff41fbf673 Add pdfminer based layout analysis 2018-10-25 12:42:35 -07:00
James R. Barlow 2435cd23ce Move pdfinfo into a package 2018-10-25 00:37:38 -07:00
James R. Barlow a063cff720 Rename/expose strip_invisible_text 2018-10-24 21:53:24 -07:00
James R. Barlow 0d396e1ac0 option check: Remove always-True condition
Both renderers are now lossless reconstruction-capable. (Have
been since 7.0)
2018-10-22 22:13:59 -07:00
James R. Barlow f5807a2053 Require pikepdf 0.3.5 2018-10-21 21:37:15 -07:00
James R. Barlow eb4938a36f Fix KeyError 'has_vector' 2018-10-20 01:20:22 -07:00
James R. Barlow c5ad530bbf pdfinfo: reminder about 'INLINE IMAGE' sentinel 2018-10-20 01:17:08 -07:00
James R. Barlow d11c428407 Redo OCR: disallow in cases that will damage the output PDF 2018-10-20 01:14:33 -07:00
James R. Barlow 6182b1f53e Merge branch 'feature/remove-vectors' into feature/redo-ocr 2018-10-20 01:13:24 -07:00
James R. Barlow 00fc1a12e2 optimize: should remove unreference resources too 2018-10-19 00:03:56 -07:00
James R. Barlow 16af753206 Add functional "redo OCR" feature
Needs argument validation and some other changes. Needs testing
with mixed-content PDFs.

Only really works for pure invisible text at the moment.
2018-10-19 00:02:19 -07:00
James R. Barlow fa48205bb8 Add feature to remove vector graphics objects 2018-10-18 21:46:08 -07:00
James R. Barlow f7dbf94071 pipeline: if vector graphic objects exist, ensure the DPI is reasonable 2018-10-18 01:23:31 -07:00
James R. Barlow b18e66e2ca pdfinfo: learn to detect vector graphic objects 2018-10-18 01:21:51 -07:00
James R. Barlow 7a5504dfa5 pdfinfo: fix terminology (operands, command) -> (operands, operator) 2018-10-18 01:18:30 -07:00
James R. Barlow d1cad7bc68 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2018-10-16 01:28:17 -07:00
Elliott Sales de Andradeandjbarlow83 c58d5c097c Add Fedora install instructions. (#304)
* Add Fedora install instructions.

* Fix path to fedora_rawhide badget
2018-10-14 13:28:50 -07:00
James R. Barlow 46157ca94e docs: some redundancies 2018-10-12 21:29:27 -07:00
jbarlow83andGitHub dd99511bcc Fix broken badges in README 2018-10-12 21:16:08 -07:00
M.Yasoob Ullah Khalid ☺andjbarlow83 5bc2efd3c7 Removed extra word from docs (#303) 2018-10-12 21:02:16 -07:00
James R. Barlow 1b18dbecf5 Fix filename test.txt 2018-10-11 16:03:25 -07:00
James R. Barlow 9f82c0eb6e v7.2.1 release notes 2018-10-11 15:55:01 -07:00
James R. Barlow 68bac1b177 Fix compatibility with pikepdf 0.3.5 API change 2018-10-11 15:51:34 -07:00
James R. Barlow 1495b78330 Remove cruft to support leptonica < 1.72 in test suite 2018-10-11 01:37:32 -07:00
James R. Barlow 6f777d2848 Include Debian copyright file 2018-10-10 23:55:48 -07:00
James R. Barlow 5650eba848 Cleanup MANIFEST.in, reorg requirements/*.txt, fix non-Unicode readme 2018-10-10 23:53:08 -07:00
221 changed files with 15555 additions and 5579 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ RUN . /appenv/bin/activate; \
# Do this now to make the best use of Docker cache.
COPY . /application
RUN . /appenv/bin/activate; \
pip install -r /application/test_requirements.txt
pip install -r /application/requirements/test.txt
# Remove the junk, including the source version of application since it was
# already installed
+84
View File
@@ -0,0 +1,84 @@
FROM alpine:3.9 as base
FROM base as builder
ENV LANG=C.UTF-8
RUN \
echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories \
# Add runtime dependencies
&& apk add --update \
python3-dev \
py3-setuptools \
jbig2enc@testing \
ghostscript \
qpdf \
tesseract-ocr \
unpaper \
pngquant \
libxml2-dev \
libxslt-dev \
zlib-dev \
qpdf-dev \
libffi-dev \
leptonica-dev \
binutils \
# 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
RUN \
echo '@testing http://nl.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories \
# Add runtime dependencies
&& apk add --update \
python3 \
jbig2enc@testing \
ghostscript \
qpdf \
tesseract-ocr \
tesseract-ocr-data-deu \
tesseract-ocr-data-chi_sim \
unpaper \
pngquant \
libxml2 \
libxslt \
zlib \
qpdf \
libffi \
leptonica-dev \
binutils \
&& mkdir /app
WORKDIR /app
# Copy build artifacts (python site-packages9
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/.docker/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
# Copy PKG-INFO from build artifact in app dir to make setuptools-scm happy
RUN cp /usr/lib/python3.6/site-packages/ocrmypdf-*.egg-info/PKG-INFO /app
ENTRYPOINT ["/usr/bin/ocrmypdf"]
+24
View File
@@ -0,0 +1,24 @@
# OCRmyPDF webservice
#
FROM jbarlow83/ocrmypdf-polyglot:latest
USER root
# Update system and install our dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-flask
RUN apt-get autoremove -y && apt-get clean -y
EXPOSE 5000
COPY .docker/webservice.py /application
USER docker
VOLUME ["/config"]
# This config file is optional
ENV OCRMYPDF_WEBSERVICE_SETTINGS "/config/config.py"
ENTRYPOINT ["python3", "/application/webservice.py"]
+125
View File
@@ -0,0 +1,125 @@
# webservice.py wrapper for OCRmyPDF
# Copyright (C) 2019 James R. Barlow: github.com/jbarlow83
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""This is a simple web service/HTTP wrapper for OCRmyPDF
This may be more convenient than the command line tool for some Docker users.
Note that OCRmyPDF uses Ghostscript, which is licensed under AGPLv3+. While
OCRmyPDF is under GPLv3, this file is distributed under the Affero GPLv3+ license,
to emphasize that SaaS deployments should make sure they comply with
Ghostscript's license as well as OCRmyPDF's.
"""
from flask import (
Flask,
Response,
flash,
request,
redirect,
url_for,
abort,
send_from_directory,
)
from subprocess import run, PIPE
from tempfile import TemporaryDirectory
from werkzeug.utils import secure_filename
import os
import shlex
app = Flask(__name__)
app.secret_key = "secret"
app.config['MAX_CONTENT_LENGTH'] = 50_000_000
app.config.from_envvar("OCRMYPDF_WEBSERVICE_SETTINGS", silent=True)
ALLOWED_EXTENSIONS = set(["pdf"])
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def do_ocrmypdf(file):
uploaddir = TemporaryDirectory(prefix="ocrmypdf-upload")
downloaddir = TemporaryDirectory(prefix="ocrmypdf-download")
filename = secure_filename(file.filename)
up_file = os.path.join(uploaddir.name, filename)
file.save(up_file)
down_file = os.path.join(downloaddir.name, filename)
cmd_args = [arg for arg in shlex.split(request.form["params"])]
if "--sidecar" in cmd_args:
return Response("--sidecar not supported", 501, mimetype='text/plain')
ocrmypdf_args = ["ocrmypdf", *cmd_args, up_file, down_file]
proc = run(ocrmypdf_args, stdout=PIPE, stderr=PIPE, encoding="utf-8")
if proc.returncode != 0:
stderr = proc.stderr
return Response(stderr, 400, mimetype='text/plain')
return send_from_directory(downloaddir.name, filename)
@app.route("/", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
if "file" not in request.files:
return Response("No file in POST", 400, mimetype='text/plain')
file = request.files["file"]
if file.filename == "":
return Response("Empty filename", 400, mimetype='text/plain')
if not allowed_file(file.filename):
return Response("Invalid filename", 400, mimetype='text/plain')
if file and allowed_file(file.filename):
return do_ocrmypdf(file)
return Response("Some other problem", 400, mimetype='text/plain')
return """
<!doctype html>
<title>OCRmyPDF webservice</title>
<h1>Upload a PDF (debug UI)</h1>
<form method=post enctype=multipart/form-data>
<label for="args">Command line parameters</label>
<input type=textbox name=params>
<label for="file">File to upload</label>
<input type=file name=file>
<input type=submit value=Upload>
</form>
<h4>Notice</h2>
<div style="font-size: 70%; max-width: 34em;">
<p>This is a webservice wrapper for OCRmyPDF.</p>
<p>Copyright 2019 James R. Barlow</p>
<p>This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
</p>
<p>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.
</p>
<p>
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.
</p>
</div>
"""
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
+1
View File
@@ -5,6 +5,7 @@
*.sublime*
**/*.pyc
.*/
!.git/
!.docker/
.ruffus_history.sqlite
bin/
-5
View File
@@ -1,5 +0,0 @@
Please include the command line and a test file with your issue report.
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
If you wish to encrypt a test file for the OCRmyPDF maintainer only, see the [Wiki](https://github.com/jbarlow83/OCRmyPDF/wiki).
+33
View File
@@ -0,0 +1,33 @@
**Describe the issue**
A clear and concise description of what the issue is.
**To Reproduce**
What command line were you trying to run?
```bash
ocrmypdf ...arguments... input.pdf output.pdf
```
**Example file**
Please include an example *input* PDF (or image). The input file is more helpful.
Please check any or all that apply about the test file:
- [ ] This is the input file
- [ ] The file contains no personal or confidential information
- [ ] I am the copyright holder for this file
- [ ] I permit this file to be included in the OCRmyPDF test suite under the CC-BY-SA 4.0 license
- [ ] I am not the copyright holder, but this file is available under a free software license
Files that are not free for inclusion in this project are quite welcome, but we like to collect free files for our test suite when possible. Please do *not* submit files with confidential information. At your option you may encrypt files for OCRmyPDF's author only.
**Expected behavior**
A clear and concise description of what you expected to happen. Include screenshots if applicable.
**System:**
- OS: [e.g. Linux, macOS]
- OCRmyPDF Version: [e.g. v7.4.0]
**Additional context**
Add any other context about the problem here.
+8 -2
View File
@@ -3,9 +3,10 @@
.pylintrc
.pytest_cache/
.ruffus_history.sqlite
.venv/
.venv*/
*.pyc
*.sublime-*
*.DS_Store
# Package building
.eggs/
@@ -13,6 +14,7 @@
build/
dist/
wheelhouse/
pip-wheel-metadata/
# Automatically generated files
docs/_build/
@@ -22,7 +24,7 @@ docs/Makefile
ocrmypdf/lib/_*.py
# Code coverage
.coverage
.coverage*
htmlcov/
# Testing
@@ -32,9 +34,13 @@ htmlcov/
*.profile
/*.pdf
/*.qdf
/*.png
/scratch.py
IDEAS
log/
tests/output/
tests/resources/private/
tmp/
/debug_tests.py
*.traineddata
/private
+6
View File
@@ -0,0 +1,6 @@
repos:
- repo: https://github.com/ambv/black
rev: stable
hooks:
- id: black
language_version: python3.7
+10
View File
@@ -0,0 +1,10 @@
build:
image: latest
python:
version: 3.6
formats:
- pdf
requirements_file: requirements/main.txt
+40 -12
View File
@@ -9,7 +9,7 @@ matrix:
dist: trusty
sudo: required
language: python
python: "3.5"
python: "3.6"
env:
- DIST=trusty
addons: &trusty_apt
@@ -33,14 +33,6 @@ matrix:
- tesseract-ocr-deu
- tesseract-ocr-eng
- tesseract-ocr-fra
- os: linux
dist: trusty
sudo: required
language: python
python: "3.6"
env:
- DIST=trusty
addons: *trusty_apt
- os: linux
dist: xenial
sudo: required
@@ -68,6 +60,39 @@ matrix:
- os: osx
osx_image: xcode9.2
language: generic
addons:
homebrew:
update: true
packages:
- exempi
- ghostscript
- jbig2enc
- leptonica
- openjpeg
- pngquant
- python
- qpdf
- tesseract
- unpaper
- os: osx
osx_image: xcode9.2
language: generic
env:
- ADD_PDFMINER=1
addons:
homebrew:
update: true
packages:
- exempi
- ghostscript
- jbig2enc
- leptonica
- openjpeg
- pngquant
- python
- qpdf
- tesseract
- unpaper
before_cache:
- rm -f $HOME/.cache/pip/log/debug.log
@@ -83,7 +108,6 @@ before_install: |
sudo dpkg -i packages/unpaper_6.1-1.deb
fi
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
brew update --quiet && brew bundle --file=.travis/Brewfile --quiet
pip3 install --upgrade pip
pip3 install wheel
fi
@@ -91,9 +115,13 @@ before_install: |
install:
- export PATH=$PWD/bin:$PATH
- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251
- pip3 install -r requirements.txt
- pip3 install -r requirements/main.txt
- pip3 install --no-deps .
- pip3 install -r test_requirements.txt
- |
if [[ "$ADD_PDFMINER" == "1" ]]; then
pip3 install --no-deps .[pdfminer]
fi
- pip3 install -r requirements/test.txt
script:
- tesseract --version
-11
View File
@@ -1,11 +0,0 @@
brew 'python'
brew 'exempi'
brew 'ghostscript'
brew 'jbig2enc'
brew 'leptonica'
brew 'openjpeg'
brew 'pngquant'
brew 'qpdf'
brew 'tesseract'
brew 'unpaper'
+6 -12
View File
@@ -1,7 +1,5 @@
# requirements
include requirements.txt
include test_requirements.txt
include dev_requirements.txt
recursive-include requirements *
# git
include .git_archival.txt
@@ -12,21 +10,20 @@ 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-include tests/cache *
recursive-exclude tests/output *
recursive-exclude tests/output_pageinfo *
recursive-exclude tests/resources/private *
# documentation
include LICENSE
include *.rst
recursive-exclude .github *
recursive-exclude .github *
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs *.svg
@@ -42,8 +39,5 @@ exclude .travis*
# code
recursive-include src/ocrmypdf *.py
exclude ocrmypdf/lib/_leptonica.py
exclude src/ocrmypdf/lib/_leptonica.py
exclude scratch.py
+48 -29
View File
@@ -1,11 +1,15 @@
OCRmyPDF
========
![image](https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master%0A%20:target:%20https://travis-ci.org/jbarlow83/OCRmyPDF)
[![Travis build status][travis]](https://travis-ci.org/jbarlow83/OCRmyPDF) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs]
![image](https://img.shields.io/pypi/v/ocrmypdf.svg%0A%20:target:%20https://pypi.org/project/ocrmypdf/)
[travis]: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master "Travis build status"
![image](https://img.shields.io/homebrew/v/ocrmypdf.svg%0A%20:alt:%20homebrew%0A%20:target:%20http://brewformulas.org/Ocrmypdf)
[pypi]: https://img.shields.io/pypi/v/ocrmypdf.svg "PyPI version"
[homebrew]: https://img.shields.io/homebrew/v/ocrmypdf.svg "Homebrew version"
[docs]: https://readthedocs.org/projects/ocrmypdf/badge/?version=latest "RTD"
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.
@@ -21,20 +25,22 @@ ocrmypdf # it's a scriptable command line program
output_searchable.pdf # produces validated PDF output
```
[See the release notes for details on the latest changes](https://ocrmypdf.readthedocs.io/en/latest/release_notes.html).
Main features
-------------
- Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
- Optimizes PDF images, often producing files smaller than the input file
- If requested deskews and/or cleans the image before performing OCR
- Validates input and output files
- Distributes work across all available CPU cores
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine
- Supports more than [100 languages](https://github.com/tesseract-ocr/tessdata) recognized by Tesseract
- Battle-tested on thousands of PDFs, a test suite and continuous integration
- Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
- Optimizes PDF images, often producing files smaller than the input file
- If requested deskews and/or cleans the image before performing OCR
- Validates input and output files
- Distributes work across all available CPU cores
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata)
- Scales properly to handle files with thousands of pages
- Battle-tested on millions of PDFs
For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/).
@@ -43,13 +49,13 @@ 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.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR
- Or they did not produce valid PDF files
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR
- Or they did not produce valid PDF files
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
...so I decided to develop my own tool.
@@ -64,6 +70,12 @@ Users of Debian 9 or later or Ubuntu 16.10 or later may simply
apt-get install ocrmypdf
```
and users of Fedora 29 or later may simply
```bash
dnf install ocrmypdf
```
and macOS users with Homebrew may simply
```bash
@@ -100,11 +112,11 @@ Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/e
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)
- 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)
Requirements
------------
@@ -114,8 +126,15 @@ Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Gho
Press & Media
-------------
- [c't 1-2014, page 59](http://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't
- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](http://heise.de/-2356670)
- [Going paperless with OCRmyPDF](https://medium.com/@ikirichenko/going-paperless-with-ocrmypdf-e2f36143f46a)
- [Converting a scanned document into a compressed searchable PDF with redactions](https://medium.com/@treyharris/converting-a-scanned-document-into-a-compressed-searchable-pdf-with-redactions-63f61c34fe4c)
- [c't 1-2014, page 59](http://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't
- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](http://heise.de/-2356670)
Business enquiries
------------------
OCRmyPDF would not be the software that it is today is without companies and users choosing to provide support for feature development and consulting enquiries. We are happy to discuss all enquiries, whether for extending the existing feature set, or integrating OCRmyPDF into a larger system.
License
-------
@@ -124,7 +143,7 @@ The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered
The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0).
OCRmyPDF versions prior to 6.0 were licensed under the MIT License.
OCRmyPDF versions prior to 6.0 were distributed under the MIT License.
Disclaimer
----------
+1494
View File
File diff suppressed because it is too large Load Diff
+58 -10
View File
@@ -1,6 +1,34 @@
Advanced features
=================
Control of unpaper
------------------
OCRmyPDF uses ``unpaper`` to provide the implementation of the ``--clean`` and ``--clean-final`` arguments. `unpaper <https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md>`_ provides a variety of image processing filters to improve images.
By default, OCRmyPDF uses only ``unpaper`` arguments that were found to be safe to use on almost all files without having to inspect every page of the file afterwards. This is particularly true when only ``--clean`` is used, since that instructs OCRmyPDF to only clean the image before OCR and not the final image.
However, if you wish to use the more aggressive options in ``unpaper``, you may use ``--unpaper-args '...'`` to override the OCRmyPDF's defaults and forward other arguments to unpaper. This option will forward arguments to ``unpaper`` without any knowledge of what that program considers to be valid arguments. The string of arguments must be quoted as shown in the examples below. No filename arguments may be included. OCRmyPDF will assume it can append input and output filename of intermediate images to the ``--unpaper-args`` string.
In this example, we tell ``unpaper`` to expect two pages of text on a sheet (image), such as occurs when two facing pages of a book are scanned. ``unpaper`` uses this information to deskew each independently and clean up the margins of both.
.. code-block:: bash
ocrmypdf --clean --clean-final --unpaper-args '--layout double' input.pdf output.pdf
ocrmypdf --clean --clean-final --unpaper-args '--layout double --no-noisefilter' input.pdf output.pdf
.. warning::
Some ``unpaper`` features will reposition text within the image. ``--clean-final`` is recommended to avoid this issue.
.. warning::
Some ``unpaper`` features cause multiple input or output files to be consumed or produced. OCRmyPDF requires ``unpaper`` to consume one file and produce one file. An deviation from that condition will result in errors.
.. note::
``unpaper`` uses uncompressed PBM/PGM/PPM files for its intermediate files. For large images or documents, it can take a lot of temporary disk space.
Control of OCR options
----------------------
@@ -13,8 +41,9 @@ If a page in a PDF seems to have text, by default OCRmyPDF will exit without mod
If ``--skip-text`` is issued, then no OCR will be performed on pages that already have text. The page will be copied to the output. This may be useful for documents that contain both "born digital" and scanned content, or to use OCRmyPDF to normalize and convert to PDF/A regardless of their contents.
If ``--force-ocr`` is issued, then all pages will be rasterized to images, discarding any hidden OCR text, and rasterizing any printable text. This is useful for redoing OCR, for fixing OCR text with a damaged character map (text is selectable but not searchable), and destroying redacted information.
If ``--redo-ocr`` is issued, then a detailed text analysis is performed. Text is categorized as either visible or invisible. Invisible text (OCR) is stripped out. Then an image of each page is created with visible text masked out. The page image is sent for OCR, and any additional text is inserted as OCR. If a file contains a mix of text and bitmap images that contain text, OCRmyPDF will locate the additional text in images without disrupting the existing text.
If ``--force-ocr`` is issued, then all pages will be rasterized to images, discarding any hidden OCR text, and rasterizing any printable text. This is useful for redoing OCR, for fixing OCR text with a damaged character map (text is selectable but not searchable), and destroying redacted information. Any forms and vector graphics will be rasterized as well.
Time and image size limits
""""""""""""""""""""""""""
@@ -45,17 +74,16 @@ Some relevant environment variables that influence Tesseract's behavior include:
Controls the number of threads Tesseract will use. OCRmyPDF will manage this environment if it is not already set. (Currently, it will set it to 1 because this gives the best results in testing.)
For example, if you are testing tesseract 4.00 and don't wish to use an existing tesseract 3.04 installation, you can launch OCRmyPDF as follows:
For example, if you have a development build of Tesseract don't wish to use the system installation, you can launch OCRmyPDF as follows:
.. code-block:: bash
env \
PATH=/home/user/src/tesseract4/api:$PATH \
TESSDATA_PREFIX=/home/user/src/tesseract4 \
ocrmypdf --tesseract-oem 2 input.pdf output.pdf
In this example ``TESSDATA_PREFIX`` directs Tesseract 4.0 to use LSTM training data. ``--tesseract-oem 1`` requests tesseract 4.0's new LSTM engine. (Tesseract 4.0 only.)
PATH=/home/user/src/tesseract/api:$PATH \
TESSDATA_PREFIX=/home/user/src/tesseract \
ocrmypdf input.pdf output.pdf
In this example ``TESSDATA_PREFIX`` is required to redirect Tesseract to an alternate folder for its "tessdata" files.
Overriding other support programs
"""""""""""""""""""""""""""""""""
@@ -105,7 +133,7 @@ rendering
Creating a new PDF from other data (such as an existing PDF).
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` selects ``sandwich`` for Tesseract 3.05.01 or newer, or ``hocr`` for older versions of Tesseract.
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The renderer may be selected using ``--pdf-renderer``. The default is ``auto`` which lets OCRmyPDF select the renderer to use. Currently, ``auto`` always selects ``sandwich``.
The ``sandwich`` renderer
"""""""""""""""""""""""""
@@ -116,8 +144,6 @@ Currently this is the best renderer for most uses, however it is implemented in
When image preprocessing features like ``--deskew`` are used, the original PDF will be rendered as a full page and the OCR layer will be placed on top.
If a PDF created with this renderer using Tesseract versions older than 3.05.00 is then passed through Ghostscript's pdfwrite feature, the OCR text *may* be corrupted. The ``--output-type=pdfa`` argument will produce a warning in this situation. For this reason, OCRmyPDF automatically selects the ``hocr`` for older Tesseract versions.
The ``hocr`` renderer
"""""""""""""""""""""
@@ -189,3 +215,25 @@ user interface. They may be imported from ``ocrmypdf.exceptions``.
* - 130
- ``ExitCode.ctrl_c``
- The program was interrupted by pressing Ctrl+C.
Debugging the intermediate files
--------------------------------
OCRmyPDF normally saves its intermediate results to a temporary folder and deletes this folder when it exits, whether it succeeded or failed.
If the ``-k`` argument is issued on the command line, OCRmyPDF will keep the temporary folder and print the location, whether it succeeded or failed (provided the Python interpreter did not crash). An example message is:
.. code-block:: none
Temporary working files saved at:
/tmp/com.github.ocrmypdf.u20wpz07
The organization of this folder is an implementation detail and subject to change between releases. However the general organization is that 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 on arguments this may differ from the presentation image
* ``layers.rendered.pdf`` - the composite PDF, before metadata repair and optimization
* ``images/*`` - images extracted during the optimization process; here the prefix indicates a PDF object ID not a page number
+23 -12
View File
@@ -18,16 +18,22 @@ The ``--tag`` argument tells parallel to print the filename as a prefix whenever
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
OCRmyPDF automatically repairs PDFs before parsing and gathering information from them. If you are already repairing PDFs with ``qpdf`` prior to attempting OCR, or you can use ``--skip-repair`` to skip this step. It may improve performance for large files, since repairing PDFs is single-threaded.
OCRmyPDF automatically repairs PDFs before parsing and gathering information from them.
Directory trees
---------------
This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes
This will walk through a directory tree and run OCR on all files in place, printing the output in a way that makes
.. code-block:: bash
find . --printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
Alternatively, with a docker container (mounts a volume to the container 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>/{}' \;
This only runs one ``ocrmypdf`` process at a time. This variation uses ``find`` to create a directory list and ``parallel`` to parallelize runs of ``ocrmypdf``, again updating files in place.
@@ -66,7 +72,7 @@ This user contributed script also provides an example of batch processing.
log_file = script_dir + '/ocr-tree.log'
logging.basicConfig(
level=logging.INFO, format='%(asctime)s %(message)s',
level=logging.INFO, format='%(asctime)s %(message)s',
filename=log_file, filemode='w')
for dir_name, subdirs, file_list in os.walk(start_dir):
@@ -80,9 +86,9 @@ This user contributed script also provides an example of batch processing.
print(full_path)
cmd = ["ocrmypdf", "--deskew", filename, filename]
logging.info(cmd)
proc = subprocess.Popen(
proc = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout.read()
result = proc.stdout
if proc.returncode == 6:
print("Skipped document because it already contained text")
elif proc.returncode == 0:
@@ -144,14 +150,14 @@ This is only possible for x86-based Synology products. Some Synology products us
timestamp_OCR = time.strftime("%Y-%m-%d-%H%M_OCR_")
filename_OCR = timestamp_OCR + file_noext + '.pdf'
docker_mount = dir_name + ':/home/docker'
# create string for pdf processing
# create string for pdf processing
# diskstation needs a user:group docker:docker. find uid:gid of your diskstation docker:docker with id docker.
# use this uid:gid in -u flag
# rw rights for docker:docker at source dir are also necessary
# the script is processed as root user via chron
# the script is processed as root user via chron
cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u=1030:65538', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR]
logging.info(cmd)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout.read()
logging.info(result)
full_path_OCR = dir_name + '/' + filename_OCR
@@ -163,14 +169,11 @@ This is only possible for x86-based Synology products. Some Synology products us
shutil.move(full_path, full_path_archive)
logging.info('Finished.\n')
Huge batch jobs
"""""""""""""""
If you have thousands of files to work with, contact the author. Consulting work related to OCRmyPDF helps fund this open source project and all inquiries are appreciated.
Hot (watched) folders
---------------------
@@ -211,4 +214,12 @@ Alternatives
* `Watchman <https://facebook.github.io/watchman/>`_ is a more powerful alternative to ``watchmedo``.
macOS Automator
---------------
You can use the Automator app with macOS, to create a Workflow or Quick Action. Use a *Run Shell Script* action in your workflow. In the context of Automator, the ``PATH`` may be set differently your Terminal's ``PATH``; you may need to explicitly set the PATH to include ``ocrmypdf``. The following example may serve as a starting point:
.. image:: images/macos-workflow.png
:alt: Example macOS Automator script
You may customize the command sent to ocrmypdf.
+37 -34
View File
@@ -52,7 +52,9 @@ master_doc = 'index'
# General information about the project.
project = 'ocrmypdf'
copyright = '2018, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
copyright = (
'2019, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
)
author = 'James R. Barlow'
# The version info for the project you're documenting, acts as replacement for
@@ -62,6 +64,7 @@ author = 'James R. Barlow'
# The short X.Y version.
import os
on_rtd = os.environ.get('READTHEDOCS') == 'True'
if on_rtd:
@@ -74,17 +77,20 @@ if on_rtd:
def __getattr__(cls, name):
return MagicMock()
MOCK_MODULES = ['pikepdf', 'libxmp', 'libxmp.utils']
MOCK_MODULES = [
'pikepdf',
'pikepdf.models',
'pikepdf.models.metadata',
'ocrmypdf.leptonica',
]
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
from ocrmypdf import __version__ as OCRMYPDF_VERSION
from pkg_resources import get_distribution, DistributionNotFound
_version_parts = OCRMYPDF_VERSION.split('.')
version = '.'.join(_version_parts[0:2])
# The full version, including alpha/beta/rc tags.
release = OCRMYPDF_VERSION
release = get_distribution('ocrmypdf').version
version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -153,9 +159,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'display_version': False
}
html_theme_options = {'display_version': False}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
@@ -268,29 +272,25 @@ htmlhelp_basename = 'ocrmypdfdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation',
'James R. Barlow', 'manual'),
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation', 'James R. Barlow', 'manual')
]
# The name of an image file (relative to this directory) to place at the top of
@@ -330,10 +330,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
[author], 1)
]
man_pages = [(master_doc, 'ocrmypdf', 'ocrmypdf Documentation', [author], 1)]
# If true, show URL addresses after external links.
#
@@ -346,9 +343,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
author, 'ocrmypdf', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
'ocrmypdf',
'ocrmypdf Documentation',
author,
'ocrmypdf',
'One line description of project.',
'Miscellaneous',
)
]
# Documents to append as an appendix to all manuals.
+60 -40
View File
@@ -5,7 +5,7 @@ Basic examples
--------------
Help!
"""""
^^^^^
ocrmypdf has built-in help.
@@ -15,28 +15,28 @@ ocrmypdf has built-in help.
Add an OCR layer and convert to PDF/A
"""""""""""""""""""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
ocrmypdf input.pdf output.pdf
Add an OCR layer and output a standard PDF
""""""""""""""""""""""""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
ocrmypdf --output-type pdf input.pdf output.pdf
Create a PDF/A with all color and grayscale images converted to JPEG
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
Modify a file in place
""""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^^
The file will only be overwritten if OCRmyPDF is successful.
@@ -45,7 +45,7 @@ The file will only be overwritten if OCRmyPDF is successful.
ocrmypdf myfile.pdf myfile.pdf
Correct page rotation
"""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^
OCR will attempt to automatic correct the rotation of each page. This can help fix a scanning job that contains a mix of landscape and portrait pages.
@@ -57,11 +57,10 @@ You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make p
If the page is "just a little off horizontal", like a crooked picture, then you want ``--deskew``. ``--rotate-pages`` is for when the cardinal angle is wrong.
OCR languages other than English
""""""""""""""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default OCRmyPDF assumes the document is English.
OCRmyPDF assumes the document is in English unless told otherwise. OCR quality may be poor if the wrong language is used.
.. code-block:: bash
@@ -70,9 +69,10 @@ By default OCRmyPDF assumes the document is English.
Language packs must be installed for all languages specified. See :ref:`Installing additional language packs <lang-packs>`.
Unfortunately, the Tesseract OCR engine has no ability to detect the language when it is unknown.
Produce PDF and text file containing OCR text
"""""""""""""""""""""""""""""""""""""""""""""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This produces a file named "output.pdf" and a companion text file named "output.txt".
@@ -81,9 +81,12 @@ This produces a file named "output.pdf" and a companion text file named "output.
ocrmypdf --sidecar output.txt input.pdf output.pdf
OCR images, not PDFs
--------------------
^^^^^^^^^^^^^^^^^^^^
If you are starting with images, you can just use Tesseract 3.04 or later directly to convert images to PDFs:
Option: use Tesseract
"""""""""""""""""""""
If you are starting with images, you can just use Tesseract directly to convert images to PDFs:
.. code-block:: bash
@@ -94,9 +97,12 @@ If you are starting with images, you can just use Tesseract 3.04 or later direct
# When there are multiple images
tesseract text-file-containing-list-of-image-filenames.txt output-prefix pdf
Tesseract's PDF output is quite good  OCRmyPDF uses it by internally by default. However, OCRmyPDF has many features not available in Tesseract like like image processing, metadata control, and PDF/A generation.
Tesseract's PDF output is quite good  OCRmyPDF uses it internally, in some cases. However, OCRmyPDF has many features not available in Tesseract like image processing, metadata control, and PDF/A generation.
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the results to run ocrmypdf. The `-` tells ocrmypdf to read standard input.
Option: use img2pdf
"""""""""""""""""""
You can also use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the results to run ocrmypdf. The ``-`` tells ocrmypdf to read standard input.
.. code-block:: bash
@@ -104,6 +110,9 @@ Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to
``img2pdf`` is recommended because it does an excellent job at generating PDFs without transcoding images.
Option: use OCRmyPDF (single images only)
"""""""""""""""""""""""""""""""""""""""""
For convenience, OCRmyPDF can also convert single images to PDFs on its own. If the resolution (dots per inch, DPI) of an image is not set or is incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54 cm, 1 dpi = 0.39 dpcm).
.. code-block:: bash
@@ -112,10 +121,10 @@ For convenience, OCRmyPDF can also convert single images to PDFs on its own. If
If you have multiple images, you must use ``img2pdf`` to convert the images to PDF.
.. note::
ImageMagick ``convert`` can also convert a group of images to PDF, but in the author's experience it takes a long time, transcodes unnecessarily and gives poor results.
Not recommended
"""""""""""""""
We caution against using ImageMagick or Ghostscript to convert images to PDF, since they may transcode images or produce downsampled images, sometimes without warning.
Image processing
----------------
@@ -132,6 +141,8 @@ OCRmyPDF perform some image processing on each page of a PDF, if desired. The s
* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important.
* ``--mask-barcodes`` will suppress any barcodes detected in a page image. Barcodes are known to confuse Tesseract OCR and interfere with the recognition of text on the same baseline as a barcode. The output file will contain the unaltered image of the barcode.
.. note::
In many cases image processing will rasterize PDF pages as images, potentially losing quality.
@@ -140,9 +151,8 @@ OCRmyPDF perform some image processing on each page of a PDF, if desired. The s
``--clean-final`` and ``-remove-background`` may leave undesirable visual artifacts in some images where their algorithms have shortcomings. Files should be visually reviewed after using these options.
OCR and correct document skew (crooked scan)
""""""""""""""""""""""""""""""""""""""""""""
Example: OCR and correct document skew (crooked scan)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Deskew:
@@ -158,7 +168,7 @@ Image processing commands can be combined. The order in which options are given
Don't actually OCR my PDF
"""""""""""""""""""""""""
-------------------------
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image processing without performing OCR, if all you want to is to apply image processing or PDF/A conversion.
@@ -167,28 +177,22 @@ If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image processing wi
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
Redo OCR
""""""""
Redo existing OCR
-----------------
To redo OCR on a file OCRed with other OCR software or a previous version of OCRmyPDF and/or Tesseract, you may use the ``--force-ocr`` argument. Normally, OCRmyPDF does not modify files that already appear to contain OCR text.
To redo OCR on a file OCRed with other OCR software or a previous version of OCRmyPDF and/or Tesseract, you may use the ``--redo-ocr`` argument. (Normally, OCRmyPDF will exit with an error if asked to modify a file with OCR.)
This may be helpful for users who want to take advantage of accuracy improvements in Tesseract 4.0 for files they previously OCRed with an earlier version of Tesseract and OCRmyPDF.
.. code-block:: bash
ocrmypdf --force-ocr input.pdf output.pdf
ocrmypdf --redo-ocr input.pdf output.pdf
Note that the method above will force rasterization of all pages, potentially reducing quality or losing vector content.
This method will replace OCR without rasterizing, reducing quality or removing vector content. If a file contains a mix of pure digital text and OCR, digital text will be ignored and OCR will be replaced. As such this mode is incompatible with image processing options, since they alter the appearance of the file.
To ensure quality is preserved, one could extract all of the images and rebuild the PDF for a lossless transformation. This recipe does not work when PDFs contain multiple images per page, as many do in practice. It will also lose any page rotation information.
.. code-block:: bash
pdfimages -all old-ocr.pdf prefix # extract all images
img2pdf -o temp.pdf prefix* # construct new PDF from the images
# review the new PDF to ensure it visually matches the old one
ocrmypdf --output-type pdf temp.pdf new-ocr.pdf
``--output-type pdf`` is used here to avoid using Ghostscript which will also rasterize images.
In some cases, existing OCR cannot be detected or replaced. Files produced by OCRmyPDF v2.2 or earlier, for example, are internally represented as having visible text with an opaque image drawn on top. This situation cannot be detected.
If ``--redo-ocr`` does not work, you can use ``--force-ocr``, which will force rasterization of all pages, potentially reducing quality or losing vector content.
Improving OCR quality
---------------------
@@ -199,17 +203,33 @@ Rotating pages and deskewing helps to ensure that the page orientation is correc
OCR quality will suffer if the resolution of input images is not correct (since the range of pixel sizes that will be checked for possible fonts will also be incorrect).
PDF optimization
----------------
By default OCRmyPDF will attempt to perform lossless optimizations on the images inside PDFs after OCR is complete. Optimization is performed even if no OCR text is found.
The ``--optimize N`` (short form ``-O``) argument controls optimization, where ``N`` ranges from 0 to 3. ``--optimize 0`` disables optimizations. ``1`` enables lossless optimizations that can be performed safely with no quality loss. ``2`` enables lossy optimizations such as image color quantizations. ``3`` enables more aggressive optimizations and targets a lower JPEG quality.
The ``--optimize N`` (short form ``-O``) argument controls optimization, where ``N`` ranges from 0 to 3 inclusive, analogous to the optimization levels in the GCC compiler.
Optimization is improved when a JBIG2 encoder is available and when ``pngquant`` is installed. If either of these components are missing, then some types of images will not be optimized.
.. list-table::
:widths: auto
:header-rows: 1
Currently optimization attempts to find more efficient encodings for images. The types of optimization available may expand over time. By default, OCRmyPDF compresses data streams inside PDFs, and will change inefficient encodings to more modern versions. A program like ``qpdf`` can be used to change encodings, e.g. to inspect the internals fo a PDF.
* - Level
- Comments
* - ``--optimize 0``
- Disables optimization.
* - ``--optimize 1``
- Enables lossless optimizations, such as transcoding images to more
efficient formats. Also compress other uncompressed objects in the
PDF and enables the more efficient "object streams" within the PDF.
* - ``--optimize 2``
- All of the above, and enables lossy optimizations and color quantization.
* - ``--optimize 3``
- All of the above, and enables more aggressive optimizations and targets lower image quality.
Optimization is improved when a JBIG2 encoder is available and when ``pngquant`` is installed. If either of these components are missing, then some types of images cannot be optimized.
The types of optimization available may expand over time. By default, OCRmyPDF compresses data streams inside PDFs, and will change inefficient compression modes to more modern versions. A program like ``qpdf`` can be used to change encodings, e.g. to inspect the internals fo a PDF.
.. code-block:: bash
+155
View File
@@ -0,0 +1,155 @@
OCRmyPDF Docker image
=====================
OCRmyPDF is also available in a Docker image that packages recent versions of all dependencies.
For users who already have Docker installed this may be an easy and convenient option. However, it is less performant than a system installation and may require Docker engine configuration.
OCRmyPDF needs a generous amount of RAM, CPU cores, and temporary storage space.
.. _docker-install:
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 execute the image:
.. code-block:: bash
docker run hello-world
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and macOS has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
.. code-block:: bash
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
Using the Docker image on the command line
------------------------------------------
**Unlike typical Docker containers**, in this mode we are using the OCRmyPDF Docker container is intended to be emphemeral it runs for one OCR job and then terminates, just like a command line program. We are using Docker as a way of delivering an application, not a server.
To start a Docker container (instance of the image):
.. code-block:: bash
docker tag jbarlow83/ocrmypdf-alpine ocrmypdf
docker run --rm ocrmypdf (... all other arguments here...)
For convenience, create a shell alias to hide the Docker command:
.. code-block:: bash
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
ocrmypdf --version # runs docker version
Or in the wonderful `fish shell <https://fishshell.com/>`_:
.. code-block:: fish
alias ocrmypdf 'docker run --rm ocrmypdf'
funcsave ocrmypdf
.. _docker-lang-packs:
Adding languages to the Docker image
------------------------------------
By default the Docker image includes English, German and Simplified Chinese, the most popular languages for OCRmyPDF users based on feedback. You may add other languages by creating a new Dockerfile based on the public one:
.. code-block:: dockerfile
FROM jbarlow83/ocrmypdf-alpine
# Add French
RUN apk add tesseract-ocr-data-fra
Executing the test suite
------------------------
The OCRmyPDF test suite is installed with image. To run it:
.. code-block:: bash
docker run --entrypoint python3 jbarlow83/ocrmypdf-alpine setup.py test
Using the OCRmyPDF web service wrapper
--------------------------------------
The OCRmyPDF Docker image includes an example, barebones HTTP web service. The webservice may be launched as follows:
.. code-block:: bash
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf-alpine webservice.py
Unlike command line usage this program will open a socket and wait for connections.
.. warning::
The OCRmyPDF web service wrapper is intended for demonstration or development. It provides no security, no authentication, no protection against denial of service attacks, and no load balancing. The default Flask WSGI server is used, which is intended for development only. The server is single-threaded and so can respond to only one client at a time. It cannot respond to clients while busy with OCR.
Clients must keep their open connection while waiting for OCR to complete. This may entail setting a long timeout; this interface is more useful for internal HTTP API calls.
Unlike the rest of OCRmyPDF, this web service is licensed under the Affero GPLv3 (AGPLv3) since Ghostscript, a dependency of OCRmyPDF, is also licensed in this way.
In addition to the above, please read our :ref:`general remarks on using OCRmyPDF as a service <ocr-service>`.
Legacy Ubuntu Docker images
---------------------------
Previously OCRmyPDF was delivered in several Docker images for different purposes, based on Ubuntu.
The Ubuntu-based images will be maintained for some time but should not be used for new deployments. They are as follows:
.. list-table::
:widths: auto
:header-rows: 1
* - Image name
- Download command
- Notes
* - ocrmypdf
- ``docker pull jbarlow83/ocrmypdf``
- Latest ocrmypdf with Tesseract 4.0.0-beta1 on Ubuntu 18.04. Includes English, French, German, Spanish, Portugeuse and Simplified Chinese.
* - ocrmypdf-polyglot
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- As above, with all available language packs.
* - ocrmypdf-webservice
- ``docker pull jbarlow83/ocrmypdf-webservice``
- All language packs, and a simple HTTP wrapper allowing OCRmyPDF to be used as a web service. Note that this component is licensed under AGPLv3.
To execute the Ubuntu-based OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This limitation applies only to the legacy images.
This example command uses the current working directory as the writable volume:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
.. note:: The working directory should be a writable local volume or Docker may not have permission to access it.
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
In some environments the permissions associated with Docker can be complex to configure. The process that executes Docker may end up not having the permissions to write the specified file system. In that case one can stream the file into and out of the Docker process and avoid all permission hassles, using ``-`` as the input and output filename:
.. code-block:: bash
docker run --rm -i ocrmypdf <other arguments to ocrmypdf> - - <input.pdf >output.pdf

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+1
View File
@@ -25,6 +25,7 @@ PDF is the best format for storing and exchanging scanned documents. Unfortunat
:maxdepth: 2
cookbook
docker
advanced
batch
security
+150 -165
View File
@@ -1,14 +1,17 @@
Installation
============
Installing OCRmyPDF
===================
.. |latest| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
:alt: OCRmyPDF latest released version on PyPI
|latest|
The easiest way to install OCRmyPDF to follow the steps for your operating system/platform.
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.
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, your best bet is to install
the most recent version your platform provides, and then upgrade that version by
installing the Python binary wheels.
.. contents:: Platform-specific steps
:depth: 2
@@ -40,7 +43,7 @@ Debian and Ubuntu 16.10 or newer
+-------------------------------------------+
| **OCRmyPDF version** |
| **OCRmyPDF versions in Debian & Ubuntu** |
+-------------------------------------------+
| |latest| |
+-------------------------------------------+
@@ -63,6 +66,45 @@ For full details on version availability for your platform, check the `Debian Pa
OCRmyPDF for Debian and Ubuntu currently omit the JBIG2 encoder. OCRmyPDF works fine without it but will produce larger output files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later will automatically detect it (specifically the ``jbig2`` binary) on the ``PATH``. To add JBIG2 encoding, see :ref:`jbig2`.
Fedora 29 or newer
^^^^^^^^^^^^^^^^^^
.. |fedora-29| image:: https://repology.org/badge/version-for-repo/fedora29/ocrmypdf.svg
:alt: Fedora 29
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
+------------------------------+
| **OCRmyPDF version** |
+------------------------------+
| |latest| |
+------------------------------+
| |fedora-29| |fedora-rawhide| |
+------------------------------+
Users of Fedora 29 later may simply
.. code-block:: bash
dnf install ocrmypdf
For full details on version availability, check the `Fedora Package Tracker
<https://apps.fedoraproject.org/packages/ocrmypdf>`_.
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`_.
.. note::
OCRmyPDF for Fedora currently omits the JBIG2 encoder due to patent issues.
OCRmyPDF works fine without it but will produce larger output files. If you
build jbig2enc from source, ocrmypdf 7.0.0 and later will automatically
detect it on the ``PATH``. To add JBIG2 encoding, see `Installing the JBIG2
encoder <jbig2>`_.
Installing the latest version on Ubuntu 18.04 LTS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -97,22 +139,56 @@ To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 16.04 LTS
^^^^^^^^^^^^^^^^
No package is currently available for Ubuntu 16.04, but you can install the dependencies manually:
No package is available for Ubuntu 16.04. OCRmyPDF 8.0 and newer require Python
3.6. Ubuntu 16.04 ships Python 3.5, but you can install Python 3.6 on it. Or,
you can skip Python 3.6 and install OCRmyPDF 7.x or older - for that procedure,
please see the installation documentation for the version of OCRmyPDF you plan
to use.
**Install system packages for OCRmyPDF**
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
sudo apt-get install -y software-properties-common python-software-properties
sudo add-apt-repository -y \
ppa:jonathonf/python-3.6 \
ppa:alex-p/tesseract-ocr
sudo apt-get update
sudo apt-get install -y \
ghostscript \
libexempi3 \
libffi6 \
pngquant \
python3-cffi \
python3-pip \
python3.6 \
qpdf \
tesseract-ocr \
unpaper
If you wish install OCRmyPDF for the current user, and ensure that the ``PATH``
This will install a Python 3.6 binary at ``/usr/bin/python3.6`` alongside the
system's Python 3.5. Do not remove the system Python. This will also install
Tesseract 4.0 from a PPA, since the version available in Ubuntu 16.04 is too old
for OCRmyPDF.
Now install pip for Python 3.6. This will install the Python 3.6 version of
``pip`` at ``/usr/local/bin/pip``.
.. code-block:: bash
curl https://bootstrap.pypa.io/get-pip.py | sudo python3.6
**Install OCRmyPDF**
OCRmyPDF requires the locale to be set for UTF-8. **On some minimal Ubuntu
installations systems**, it may be necessary to set the locale.
.. code-block:: bash
# Optional: Only need to set these if they are not already set
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
Now install OCRmyPDF for the current user, and ensure that the ``PATH``
environment variable contains ``$HOME/.local/bin``.
.. code-block:: bash
@@ -120,38 +196,20 @@ environment variable contains ``$HOME/.local/bin``.
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
Alternately, you can install ocrmypdf system-wide. (Not recommended.)
.. code-block:: bash
sudo pip3 install ocrmypdf
At your option, you may upgrade Ubuntu 16.04 LTS to Tesseract 4.0 for improved OCR results.
.. code-block:: bash
sudo apt-get install -y software-properties-common python-software-properties
sudo add-apt-repository ppa:alex-p/tesseract-ocr -y
sudo apt-get update
sudo apt-get upgrade tesseract-ocr
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 14.04 LTS
^^^^^^^^^^^^^^^^
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options, because it is older and does not provide ``pip``.
Update apt-get:
.. code-block:: bash
sudo apt-get update
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other
options, because of its age. Several backports are required. For explanations of
some steps of this procedure, see the similar steps for Ubuntu 16.04.
Install system dependencies:
.. code-block:: bash
sudo apt-get update
sudo apt-get install \
software-properties-common python-software-properties \
zlib1g-dev \
@@ -161,9 +219,13 @@ Install system dependencies:
pngquant \
qpdf
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1), Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and Tesseract 3.x on your system. Python 3.6 will be installed alongside the system Python 3.4.
We will need backports of Ghostscript 9.16, libav-11 (for unpaper 6.1),
Tesseract 4.00 (alpha), and Python 3.6. This will replace Ghostscript and
Tesseract 3.x on your system. Python 3.6 will be installed alongside the system
Python 3.4.
If you prefer to not modify your system in this matter, consider using a Docker container.
If you prefer to not modify your system in this matter, consider using a Docker
container.
.. code-block:: bash
@@ -189,8 +251,6 @@ 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
The ``wget`` command will download a program and run it.
These installation instructions omit the optional dependency ``unpaper``, which is only available at version 0.4.2 in Ubuntu 14.04. The author could not find a backport of ``unpaper``, and created a .deb package to do the job of installing unpaper 6.1 (for x86 64-bit only):
.. code-block:: bash
@@ -200,14 +260,14 @@ These installation instructions omit the optional dependency ``unpaper``, which
To add JBIG2 encoding, see :ref:`jbig2`.
ArchLinux
^^^^^^^^^
ArchLinux (AUR)
^^^^^^^^^^^^^^^
.. image:: https://repology.org/badge/version-for-repo/aur/ocrmypdf.svg
:alt: ArchLinux
:target: https://repology.org/metapackage/ocrmypdf
The author is aware of an `ArchLinux User Repository package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. You can use the following command.
There is an `ArchLinux User Repository package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. You can use the following command.
.. code-block:: bash
@@ -238,6 +298,12 @@ OCRmyPDF is now a standard `Homebrew <https://brew.sh>`_ formula. To install on
brew install ocrmypdf
This will include only the English language pack. If you need other languages you can optionally install them all:
.. code-block:: bash
brew install tesseract-lang # Optional: Install all language packs
.. note::
Users who previously installed OCRmyPDF on macOS using ``pip install ocrmypdf`` should remove the pip version (``pip3 uninstall ocrmypdf``) before switching to the Homebrew version.
@@ -298,130 +364,39 @@ The command line program should now be available:
ocrmypdf --help
.. _docker-install:
Installing on FreeBSD
---------------------
FreeBSD 11.2 is known to work. Other versions likely work but have not been tested.
In general it should work to:
#. `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.
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.
If you have `Docker <https://docs.docker.com/>`_ installed on your system, you can install a Docker image of the latest release.
Follow the Docker installation instructions for your platform. If you can run this command successfully, your system is ready to download and execute the image:
.. code-block:: bash
docker run hello-world
OCRmyPDF will use all available CPU cores. By default, the VirtualBox machine instance on Windows and macOS has only a single CPU core enabled. Use the VirtualBox Manager to determine the name of your Docker engine host, and then follow these optional steps to enable multiple CPUs:
.. code-block:: bash
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
Assuming you have a Docker engine running, you can download one of the three available images:
.. list-table::
:widths: auto
:header-rows: 1
* - Image name
- Download command
- Notes
* - ocrmypdf
- ``docker pull jbarlow83/ocrmypdf``
- Latest ocrmypdf with Tesseract 4.0.0-beta1 on Ubuntu 18.04. Includes English, French, German, Spanish, Portugeuse and Simplified Chinese.
* - ocrmypdf-polyglot
- ``docker pull jbarlow83/ocrmypdf-polyglot``
- As above, with all available language packs.
For example:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
Then tag it to give a more convenient name, just ocrmypdf:
.. code-block:: bash
docker tag jbarlow83/ocrmypdf ocrmypdf
.. _docker-polyglot:
The alternative "polyglot" image provides `all available language packs <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
You can then run ocrmypdf using the command:
.. code-block:: bash
docker run --rm ocrmypdf --help
To execute the OCRmyPDF on a local file, you must `provide a writable volume to the Docker image <https://docs.docker.com/userguide/dockervolumes/>`_, and both the input and output file must be inside the writable volume. This example command uses the current working directory as the writable volume:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" <other docker arguments> ocrmypdf <your arguments to ocrmypdf>
In this worked example, the current working directory contains an input file called ``test.pdf`` and the output will go to ``output.pdf``:
.. code-block:: bash
docker run --rm -v "$(pwd):/home/docker" ocrmypdf --skip-text test.pdf output.pdf
.. note:: The working directory should be a writable local volume or Docker may not have permission to access it.
Note that ``ocrmypdf`` has its own separate ``-v VERBOSITYLEVEL`` argument to control debug verbosity. All Docker arguments should before the ``ocrmypdf`` image name and all arguments to ``ocrmypdf`` should be listed after.
In some environments the permissions associated with Docker can be complex to configure. The process that executes Docker may end up not having the permissions to write the specified file system. In that case one can stream the file into and out of the Docker process and avoid all permission hassles, using ``-`` as the input and output filename:
.. code-block:: bash
docker run --rm -i ocrmypdf <other arguments to ocrmypdf> - - <input.pdf >output.pdf
For convenience, a shell alias can hide the docker command:
.. code-block:: bash
alias ocrmypdf='docker run --rm -v "$(pwd):/home/docker" ocrmypdf'
ocrmypdf --version # runs docker version
Or in the wonderful `fish shell <https://fishshell.com/>`_:
.. code-block:: fish
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
funcsave ocrmypdf
.. note::
The ocrmypdf Docker images are designed for application delivery, to enable use of OCRmyPDF without fussing with dependencies. ``docker run --rm`` argument tells Docker to delete the container after it runs, because each container is only good for a single job. The Docker image is not designed for use as a persistent web service or for use on Amazon EC2 Container Service (AWS ECS).
See `OCRmyPDF Docker Image <docker>`_ for more information.
Installing on Windows
---------------------
Direct installation on Windows is not possible. `Install the Docker <docker-install_>`_ container as described above. Ensure that your command prompt can run the docker "hello world" container.
Direct installation on Windows is not possible, because there are a
POSIX dependencies. Your options are:
It would probably not be too difficult to run on Windows. The main reason this has been avoided is the difficulty of packaging and installing the various non-Python dependencies: Tesseract, QPDF, Ghostscript, Leptonica. Pull requests to add or improve Windows support would be quite welcome.
Running on Windows
~~~~~~~~~~~~~~~~~~
The command line syntax to run ocrmypdf from a command prompt will resemble:
.. code-block:: bat
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
where /c/Users/sampleuser is a Unix representation of the Windows path C:\\Users\\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
`Bash on Ubuntu on Windows <https://github.com/Microsoft/BashOnWindows>`_ should also be a viable route for running the OCRmyPDF Docker container.
* Install Ubuntu 18.04 in Windows 10 Subsystem for Linux, then follow
the Ubuntu 18.04 procedure.
* `Install the Docker <docker-install>`__ container. Ensure that
your command prompt can run the docker "hello world" container.
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.
Installing with Python pip
--------------------------
@@ -447,23 +422,21 @@ Since ``pip3 install --user`` does not work correctly on some platforms, notably
pip3 install ocrmypdf
Requirements for pip and HEAD install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OCRmyPDF currently requires these external programs and libraries to be installed, and must be satisfied using the operating system package manager. ``pip`` cannot provide them.
- Python 3.5 or newer
- Python 3.6 or newer
- Ghostscript 9.15 or newer
- libexempi3 2.2.0 or newer
- qpdf 8.1.0 or newer
- Tesseract 3.04 or newer
- Tesseract 4.0.0-alpha or newer
As of ocrmypdf 7.0.0, the following versions are recommended:
As of ocrmypdf 7.2.1, the following versions are recommended:
- Python 3.7
- Ghostscript 9.23
- libexempi3 2.4.5
- qpdf 8.1.0
- Tesseract 4.0.0-beta1
- Ghostscript 9.23 or newer
- qpdf 8.2.1
- Tesseract 4.0.0 or newer
- jbig2enc 0.29 or newer
- pngquant 2.5 or newer
- unpaper 6.1
@@ -478,13 +451,11 @@ jbig2enc, pngquant, and unpaper are optional. If missing certain features are di
These are in addition to the Python packaging dependencies, meaning that unfortunately, the ``pip install`` command cannot satisfy all of them.
Python 3.7 and Tesseract 4.0.0-beta.1 are recommended for best OCR results and best performance.
Installing HEAD revision from sources
-------------------------------------
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing.
If you have ``git`` and Python 3.6 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing.
If you prefer to build every from source, you will need to `build pikepdf from source <https://pikepdf.readthedocs.io/en/latest/installation.html#building-from-source>`_. First ensure you can build and install pikepdf.
@@ -525,7 +496,7 @@ dependencies. Older version than the ones mentioned in the release notes
are likely not to be compatible to OCRmyPDF.
For development
~~~~~~~~~~~~~~~
^^^^^^^^^^^^^^^
To install all of the development and test requirements:
@@ -536,6 +507,20 @@ To install all of the development and test requirements:
source venv/bin/activate
cd OCRmyPDF
pip install -e .
pip install -r dev_requirements.txt -r test_requirements.txt
pip install -r requirements/dev.txt -r requirements/test.txt
To add JBIG2 encoding, see :ref:`jbig2`.
Shell completions
-----------------
Completions for ``bash`` and ``fish`` are available in the project's
``misc/completion`` folder. The ``bash`` completions are likely ``zsh``
compatible but this has not been confirmed. Package maintainers, please install
these at the appropriate locations for your system.
To manually install the ``bash`` completion, copy ``misc/completion/ocrmypdf.bash`` to
``/etc/bash_completion.d/ocrmypdf`` (rename the file).
To manually install the ``fish`` completion, copy ``misc/completion/ocrmypdf.fish`` to
``~/.config/fish/completions/ocrmypdf.fish``.
+12 -8
View File
@@ -16,7 +16,7 @@ About PDFs
PDFs are page description files that attempts to preserve a layout exactly. They contain `vector graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
.. image:: bitmap_vs_svg.svg
.. image:: images/bitmap_vs_svg.svg
A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page.
@@ -64,7 +64,7 @@ In the case of a PDF that is nothing other than a container of images (no rotati
OCRmyPDF uses several strategies depending on input options and the input PDF itself, but generally speaking it rasterizes a page for OCR and then grafts the OCR back onto the original. As such it can handle complex PDFs and still preserve their contents as much as possible.
OCRmyPDF also supports a many, many edge cases that have cropped over several years of development. We support PDF features like images inside of Form XObjects, and pages with UserUnit scaling. We support rare image formats like non-monochrome 1-bit images. Thanks to pikepdf and QPDF, we auto-repair PDFs that are damaged. (Not that you need to know what any of these are! You should be able to throw any PDF at it.)
OCRmyPDF also supports a many, many edge cases that have cropped over several years of development. We support PDF features like images inside of Form XObjects, and pages with UserUnit scaling. We support rare image formats like non-monochrome 1-bit images. We warn about files you may not to OCR. Thanks to pikepdf and QPDF, we auto-repair PDFs that are damaged. (Not that you need to know what any of these are! You should be able to throw any PDF at it.)
Limitations
@@ -76,20 +76,21 @@ OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these l
* It is not capable of recognizing handwriting.
* It may find gibberish and report this as OCR output.
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns, and may try to join text across columns.
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
* It does not expose information about what font family text belongs to.
OCRmyPDF is also limited by the PDF specification:
* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others.
* Because some popular open source PDF viewers have a particularly hard time with spaces betweem words, OCRmyPDF appends a space to each text element as a workaround. While this mixes document structure with graphical information that ideally should be left to the PDF viewer to interpret, it improves compatibility with some viewers and does not cause problems for better ones.
* Because some popular open source PDF viewers have a particularly hard time with spaces betweem words, OCRmyPDF appends a space to each text element as a workaround (when using ``--pdf-renderer hocr``). While this mixes document structure with graphical information that ideally should be left to the PDF viewer to interpret, it improves compatibility with some viewers and does not cause problems for better ones.
Ghostscript also imposes some limitations:
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
* PDFs containing JPEG 2000-encoded content will be converted to JPEG encoding, which may introduce compression artifacts, if Ghostscript PDF/A is enabled.
* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format.
* Ghostscript may transcode grayscale and color images, either lossy to lossless or lossless to lossy, based on an internal algorithm. This behavior can be suppressed by setting ``--pdfa-image-compression`` to ``jpeg`` or ``lossless`` to set all images to one type or the other. Ghostscript has no option to maintain the input image's format. (Ghostscript 9.25+ can copy JPEG images without transcoding them; earlier versions will transcode.)
* Ghostscript's PDF/A conversion removes any XMP metadata that is not one of the standard XMP metadata namespaces for PDFs. In particular, PRISM Metdata is removed.
Regarding OCRmyPDF itself:
@@ -109,7 +110,10 @@ To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly test
Web front-ends
--------------
* `Nextcloud OCR <https://github.com/janis91/ocr>`_ is a free software plugin for the Nextcloud private cloud software
* `OCRmyPDF-web <https://github.com/sseemayer/OCRmyPDF-web>`_, a micro web-frontend for OCRmyPDF (third-party, not actively maintained)
The Docker image ``ocrmypdf-alpine`` 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 security measures to speak of.
Bear in mind that OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online`_).
In addition, the following third-party integrations are available:
* `Nextcloud OCR <https://github.com/janis91/ocr>`_ is a free software plugin for the Nextcloud private cloud software
OCRmyPDF is not designed to be secure against malware-bearing PDFs (see `Using OCRmyPDF online <ocr-service>`_). Users should ensure they comply with OCRmyPDF's licenses and the licenses of all dependencies. In particular, OCRmyPDF requires Ghostscript, which is licensed under AGPLv3.
+1 -1
View File
@@ -32,4 +32,4 @@ JBIG2 lossy mode does achieve higher compression ratios than any other monochrom
To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``. ``--optimize {1,2,3}`` are necessary for the argument to take effect also required. Also, a JBIG2 encoder must be installed as described in the previous section.
*ocrmypdf v7.0 and v7.1 used lossy mode by default.*
*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by default.*
+20 -32
View File
@@ -3,7 +3,7 @@
Installing additional language packs
====================================
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English.
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English.
Tesseract supports `most languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
@@ -17,12 +17,27 @@ Debian and Ubuntu users
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
# Install Chinese Simplified language pack
apt-get install tesseract-ocr-chi-sim
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
languages can be requested using either ``-l eng+fre`` (English and French) or ``-l eng -l fre``.
Fedora users
------------
.. code-block:: bash
# Display a list of all Tesseract language packs
dnf search tesseract
# Install Chinese Simplified language pack
dnf install tesseract-langpack-chi_sim
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to
what languages it should search for. Multiple languages can be requested using
either ``-l eng+fre`` (English and French) or ``-l eng -l fre``.
macOS users
-----------
@@ -31,31 +46,4 @@ You can install additional language packs by :ref:`installing Tesseract using Ho
Docker users
------------
Users of the Docker image may use the alternative :ref:`"polyglot" container <docker-polyglot>` which includes all languages.
Adding individual language packs to a Docker image
""""""""""""""""""""""""""""""""""""""""""""""""""
If you wish to add a single language pack, you could do the following:
* Download the desired ``.trainedata`` file from the `tessdata <https://github.com/tesseract-ocr/tessdata>`_ repository. Let's use Hebrew in this example (``heb.traineddata``)
* Copy the file to ``/home/user/downloads/heb.traineddata``.
* Create a new container based on the ocrmypdf-tess4 image and jump into it with a terminal:
.. code-block:: bash
host$ docker run -v /home/user/downloads:/home/docker -it --entrypoint /bin/bash ocrmypdf-tess4
* Put the file where Tesseract expects it:
.. code-block:: bash
docker$ cp /home/docker/heb.traineddata /usr/share/tesseract-ocr/tessdata
* Note the container id, and save it as a new image (in this example, ``ocrmypdf-tess4-heb``)
.. code-block:: bash
host$ docker commit <container_id> ocrmypdf-tess4-heb
Users of the OCRmyPDF Docker image should install language packs into a derived Docker image as :ref:`described in that section <docker-lang-packs>`.
+192
View File
@@ -13,6 +13,189 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
find: [^`]\#([0-9]{1,3})[^0-9]
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
v8.3.1
------
- Fixed an issue where PDFs with malformed metadata would be rendered as blank pages. `#398 <https://github.com/jbarlow83/OCRmyPDF/issues/398>`_.
v8.3.0
------
- Improved the strategy for updating pages when a new image of the page was produced. We know attempt to preserve more content from the original file, for annotations in particular.
- For PDFs with more than 100 pages and a sequence where one PDF page was replaced and one or more subsequent ones were skipped, an intermediate file would be corrupted while grafting OCR text, causing processing to fail.
- Previously, we resized the images produced by Ghostscript by a small number of pixels to ensure the output image size was an exactly what we wanted. Having discovered a way to get Ghostscript to produce the exact image sizes we require, we eliminated the resizing step.
- Command line completions for ``bash`` are now available, in addition to ``fish``, both in ``misc/completion``. Package maintainers, please install these so users can take advantage.
- Updated requirements.
- pikepdf 1.3.0 is now required.
v8.2.4
------
- Fixed a false positive while checking for a certain type of PDF that only Acrobat can read. We now more accurately detect Acrobat-only PDFs.
- OCRmyPDF holds fewer open file handles and is more prompt about releasing those it no longer needs.
- Minor optimization: we no longer traverse the table of contents to ensure all references in it are resolved, as changes to libqpdf have made this unnecessary.
- pikepdf 1.2.0 is now required.
v8.2.3
------
- Fixed that ``--mask-barcodes`` would occasionally leave a unwanted temporary file named ``junkpixt`` in the current working folder.
- Fixed (hopefully) handling of Leptonica errors in an environment where a non-standard ``sys.stderr`` is present.
- Improved help text for ``--verbose``.
v8.2.2
------
- Fixed a regression from v8.2.0, an exception that occurred while attempting to report that ``unpaper`` or another optional dependency was unavailable.
- In some cases, ``ocrmypdf [-c|--clean]`` failed to exit with an error when ``unpaper`` is not installed.
v8.2.1
------
- This release was canceled.
v8.2.0
------
- A major improvement to our Docker image is now available thanks to hard work contributed by @mawi12345. The new Docker image, ocrmypdf-alpine, is based on Alpine Linux, and includes most of the functionality of three existed images in a smaller package. This image will replace the main Docker image eventually but for now all are being built. `See documentation for details <https://ocrmypdf.readthedocs.io/en/latest/docker.html>`_.
- Documentation reorganized especially around the use of Docker images.
- Fixed a problem with PDF image optimization, where the optimizer would unnecessarily decompress and recompress PNG images, in some cases losing the benefits of the quantization it just had just performed. The optimizer is now capable of embedding PNG images into PDFs without transcoding them.
- Fixed a minor regression with lossy JBIG2 image optimization. All JBIG2 candidates images were incorrectly placed into a single optimization group for the whole file, instead of grouping pages together. This usually makes a larger JBIG2Globals dictionary and results in inferior compression, so it worked less well than designed. However, quality would not be impacted. Lossless JBIG2 was entirely unaffected.
- Updated dependencies, including pikepdf to 1.1.0. This fixes `#358 <https://github.com/jbarlow83/OCRmyPDF/issues/358>`_.
- The install-time version checks for certain external programs have been removed from setup.py. These tests are now performed at run-time.
- The non-standard option to override install-time checks (``setup.py install --force``) is now deprecated and prints a warning. It will be removed in a future release.
v8.1.0
------
- Added a feature, ``--unpaper-args``, which allows passing arbitrary arguments to ``unpaper`` when using ``--clean`` or ``--clean-final``. The default, very conservative unpaper settings are suppressed.
- The argument ``--clean-final`` now implies ``--clean``. It was possible to issue ``--clean-final`` on its before this, but it would have no useful effect.
- Fixed an exception on traversing corrupt table of contents entries (specifically, those with invalid destination objects)
- Fixed an issue when using ``--tesseract-timeout`` and image processing features on a file with more than 100 pages. `#347 <https://github.com/jbarlow83/OCRmyPDF/issues/347>`_
- OCRmyPDF now always calls ``os.nice(5)`` to signal to operating systems that it is a background process.
v8.0.1
------
- Fixed an exception when parsing PDFs that are missing a required field. `#325 <https://github.com/jbarlow83/OCRmyPDF/issues/325>`_
- pikepdf 1.0.5 is now required, to address some other PDF parsing issues.
v8.0.0
------
No major features. The intent of this release is to sever support for older versions of certain dependencies.
**Breaking changes**
- Dropped support for Tesseract 3.x. Tesseract 4.0 or newer is now required.
- Dropped support for Python 3.5.
- Some ``ocrmypdf.pdfa`` APIs that were deprecated in v7.x were removed. This functionality has been moved to pikepdf.
**Other changes**
- Fixed an unhandled exception when attempting to mask barcodes. `#322 <https://github.com/jbarlow83/OCRmyPDF/issues/322>`_
- It is now possible to use ocrmypdf without pdfminer.six, to support distributions that do not have it or cannot currently use it (e.g. Homebrew). Downstream maintainers should include pdfminer.six if possible.
- A warning is now issue when PDF/A conversion removes some XMP metadata from the input PDF. (Only a "whitelist" of certain XMP metadata types are allowed in PDF/A.)
- Fixed several issues that caused PDF/As to be produced with nonconforming XMP metadata (would fail validation with veraPDF).
- Fixed some instances where invalid DocumentInfo from a PDF cause XMP metadata creation to fail.
- Fixed a few documentation problems.
- pikepdf 1.0.2 is now required.
v7.4.0
------
- ``--force-ocr`` may now be used with the new ``--threshold`` and ``--mask-barcodes`` features
- pikepdf >= 0.9.1 is now required.
- Changed metadata handling to pikepdf 0.9.1. As a result, metadata handling of non-ASCII characters in Ghostscript 9.25 or later is fixed.
- chardet >= 3.0.4 is temporarily listed as required. pdfminer.six depends on it, but the most recent release does not specify this requirement. (`#326 <https://github.com/jbarlow83/OCRmyPDF/issues/326>`_)
- python-xmp-toolkit and libexempi are no longer required.
- A new Docker image is now being provided for users who wish to access OCRmyPDF over a simple HTTP interface, instead of the command line.
- Increase tolerance of PDFs that overflow or underflow the PDF graphics stack. (`#325 <https://github.com/jbarlow83/OCRmyPDF/issues/325>`_)
v7.3.1
------
- Fixed performance regression from v7.3.0; fast page analysis was not selected when it should be.
- Fixed a few exceptions related to the new ``--mask-barcodes`` feature and improved argument checking
- Added missing detection of TrueType fonts that lack a Unicode mapping
v7.3.0
------
- Added a new feature ``--redo-ocr`` to detect existing OCR in a file, remove it, and redo the OCR. This may be particularly helpful for anyone who wants to take advantage of OCR quality improvements in Tesseract 4.0. Note that OCR added by OCRmyPDF before version 3.0 cannot be detected since it was not properly marked as invisible text in the earliest versions. OCR that constructs a font from visible text, such as Adobe Acrobat's ClearScan.
- OCRmyPDF's content detection is generally more sophisticated. It learns more about the contents of each PDF and makes better recommendations:
- OCRmyPDF can now detect when a PDF contains text that cannot be mapped to Unicode (meaning it is readable to human eyes but copy-pastes as gibberish). In these cases it recommends ``--force-ocr`` to make the text searchable.
- PDFs containing vector objects are now rendered at more appropriate resolution for OCR.
- We now exit with an error for PDFs that contain Adobe LiveCycle Designer's dynamic XFA forms. Currently the open source community does not have tools to work with these files.
- OCRmyPDF now warns when a PDF that contains Adobe AcroForms, since such files probably do not need OCR. It can work with these files.
- Added three new **experimental** features to improve OCR quality in certain conditions. The name, syntax and behavior of these arguments is subject to change. They may also be incompatible with some other features.
- ``--remove-vectors`` which strips out vector graphics. This can improve OCR quality since OCR will not search artwork for readable text; however, it currently removes "text as curves" as well.
- ``--mask-barcodes`` to detect and suppress barcodes in files. We have observed that barcodes can interfere with OCR because they are "text-like" but not actually textual.
- ``--threshold`` which uses a more sophisticated thresholding algorithm than is currently in use in Tesseract OCR. This works around a `known issue in Tesseract 4.0 <https://github.com/tesseract-ocr/tesseract/issues/1990>`_ with dark text on bright backgrounds.
- Fixed an issue where an error message was not reported when the installed Ghostscript was very old.
- The PDF optimizer now saves files with object streams enabled when the optimization level is ``--optimize 1`` or higher (the default). This makes files a little bit smaller, but requires PDF 1.5. PDF 1.5 was first released in 2003 and is broadly supported by PDF viewers, but some rudimentary PDF parsers such as PyPDF2 do not understand object streams. You can use the command line tool ``qpdf --object-streams=disable`` or `pikepdf <https://github.com/pikepdf/pikepdf>`_ library to remove them.
- New dependency: pdfminer.six 20181108. Note this is a fork of the Python 2-only pdfminer.
- Deprecation notice: At the end of 2018, we will be ending support for Python 3.5 and Tesseract 3.x. OCRmyPDF v7 will continue to work with older versions.
v7.2.1
------
- Fix compatibility with an API change in pikepdf 0.3.5.
- A kludge to support Leptonica versions older than 1.72 in the test suite was dropped. Older versions of Leptonica are likely still compatible. The only impact is that a portion of the test suite will be skipped.
v7.2.0
------
@@ -144,6 +327,15 @@ v7.0.0
+ It may be necessary to separately ``pip install pycparser`` to avoid `another Python 3.7 issue <https://github.com/eliben/pycparser/pull/135>`_.
v6.2.5
------
- Disable a failing test due to Tesseract 4.0rc1 behavior change. Previously, Tesseract would exit with an error message if its configuration was invalid, and OCRmyPDF would intercept this message. Now Tesseract issues a warning, which OCRmyPDF v6.2.5 may relay or ignore. (In v7.x, OCRmyPDF will respond to the warning.)
- This release branch no longer supports using the optional PyMuPDF installation, since it was removed in v7.x.
- This release branch no longer supports macOS. macOS users should upgrade to v7.x.
v6.2.4
------
+4 -2
View File
@@ -25,12 +25,14 @@ Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://gho
Depending on the options specified, OCRmyPDF may graft the OCR layer into the existing PDF or it may essentially reconstruct ("re-fry") a visually identical PDF that may be quite different at the binary level. That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
.. _ocr-service:
Using OCRmyPDF online or as a service
-------------------------------------
OCRmyPDF should not be deployed as a public-facing service, such as a website where a potential attacker could upload a PDF of their choice for OCR. OCRmyPDF is not designed to be secure against PDF malware. Another concern is PDFs specifically designed to be a denial of service attack: PDFs can contain recursive data structures that sometimes send parsers into infinite loops, and issue complex graphics drawing commands.
OCRmyPDF is not designed for use as a public web service where a malicious user could upload a chosen PDF. In particular, it is not necessarily secure against PDF malware or PDFs that cause denial of service. OCRmyPDF relies on Ghostscript, and therefore, if deployed online one should be prepared to comply with Ghostscript's Affero GPL license, OCRmyPDF's GPL license, and any other licenses.
Setting aside these concerns, a side effect of OCRmyPDF is it may incidentally sanitize PDFs that contain malware. It runs ``qpdf`` to repair the PDF, which could correct malformed PDF structures that are part of an attack. When PDF/A output is selected (the default), the input PDF is partially reconstructed by Ghostscript. When ``--force-ocr`` is used, all pages are rasterized and reconverted to PDF, which could remove malware in embedded images. No guarantees.
Setting aside these concerns, a side effect of OCRmyPDF is it may incidentally sanitize PDFs that contain certain types of malware. It runs ``qpdf`` to repair the PDF, which could correct malformed PDF structures that are part of an attack. When PDF/A output is selected (the default), the input PDF is partially reconstructed by Ghostscript. When ``--force-ocr`` is used, all pages are rasterized and reconverted to PDF, which could remove malware in embedded images.
OCRmyPDF should be relatively safe to use in a trusted intranet, with some considerations:
+87
View File
@@ -0,0 +1,87 @@
# ocrmypdf completion -*- shell-script -*-
_ocrmypdf()
{
local cur prev cword words split
_init_completion -s || return
case $prev in
--version|-h|--help)
return
;;
--user-words|--user-patterns|--tesseract-config)
_filedir
return
;;
--output-type)
COMPREPLY=( $( compgen -W 'pdfa pdf pdfa-1 pdfa-2 pdfa-3' -- \
"$cur" ) )
return
;;
--pdf-renderer)
COMPREPLY=( $( compgen -W 'auto hocr sandwich' -- "$cur" ) )
return
;;
--pdfa-image-compression)
COMPREPLY=( $( compgen -W 'auto jpeg lossless' -- "$cur" ) )
return
;;
-O|--optimize|--tesseract-oem)
COMPREPLY=( $( compgen -W '{0..3}' -- "$cur" ) )
return
;;
--jpeg-quality|--png-quality)
COMPREPLY=( $( compgen -W '{0..100}' -- "$cur" ) )
return
;;
-l|--language)
COMPREPLY=$( command tesseract --list-langs 2>/dev/null )
COMPREPLY=( $( compgen -W '${COMPREPLY[@]##*:}' -- "$cur" ) )
return
;;
--image-dpi|--oversample|--skip-big|--max-image-mpixels|\
--tesseract-timeout|--rotate-pages-threshold)
COMPREPLY=( $( compgen -P "$cur" -W '{0..9}' ) )
return
;;
-j|--jobs)
COMPREPLY=( $( compgen -W '{1..'$( _ncpus )'}' -- "$cur" ) )
return
;;
-v|--verbose)
COMPREPLY=( $( compgen -W '{1..9}' -- "$cur" ) ) # max level ?
return
;;
--tesseract-pagesegmode)
COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) )
return
;;
--sidecar|--title|--author|--subject|--keywords|--unpaper-args)
# argument required but no completions available
return
;;
esac
$split && return
if [[ $cur == -* ]]; then
COMPREPLY=( $( compgen -W '--language --image-dpi --output-type
--sidecar --version --jobs --quiet --verbose --title --author
--subject --keywords --rotate-pages --remove-background --deskew
--clean --clean-final --unpaper-args --oversample --remove-vectors
--mask-barcodes --threshold --force-ocr --skip-text --redo-ocr
--skip-big --jpeg-quality --png-quality --jbig2-lossy
--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 --flowchart --output-type' \
-- "$cur" ) )
return
else
_filedir
return
fi
} &&
complete -F _ocrmypdf ocrmypdf
# ex: filetype=sh
+70
View File
@@ -0,0 +1,70 @@
complete -c ocrmypdf -l version
complete -c ocrmypdf -l help
complete -c ocrmypdf -l sidecar -r -d "write OCR to text file"
complete -c ocrmypdf -s q -l quiet
complete -c ocrmypdf -s r -l rotate-pages -d "rotate pages to correct orientation"
complete -c ocrmypdf -s d -l deskew -d "fix small horizontal alignment skew"
complete -c ocrmypdf -s c -l clean -d "clean document images before OCR"
complete -c ocrmypdf -s i -l clean-final -d "clean document images and keep result"
complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
complete -c ocrmypdf -l mask-barcodes -d "mask barcodes from OCR"
complete -c ocrmypdf -l threshold -d "threshold images before OCR"
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
complete -c ocrmypdf -x -s l -l language -d 'language'
complete -c ocrmypdf -x -s l -l language -a '(tesseract --list-langs)'
complete -c ocrmypdf -x -l image-dpi -d "assume this DPI if input image DPI is unknown"
function __fish_ocrmypdf_output_type
echo -e "pdfa\t"(_ "output a PDF/A (default)")
echo -e "pdf\t"(_ "output a standard PDF")
echo -e "pdfa-1\t"(_ "output a PDF/A-1b")
echo -e "pdfa-2\t"(_ "output a PDF/A-2b")
echo -e "pdfa-3\t"(_ "output a PDF/A-3b")
end
complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "select PDF output options"
function __fish_ocrmypdf_pdf_renderer
echo -e "auto\t"(_ "auto select PDF renderer")
echo -e "hocr\t"(_ "use hocr renderer")
echo -e "sandwich\t"(_ "use sandwich renderer")
end
complete -c ocrmypdf -x -l pdf-render -a '(__fish_ocrmypdf_pdf_renderer)' -d "select PDF renderer options"
function __fish_ocrmypdf_optimize
echo -e "0\t"(_ "do not optimize")
echo -e "1\t"(_ "do safe, lossless optimizations (default)")
echo -e "2\t"(_ "do some lossy optimizations")
echo -e "3\t"(_ "do aggressive lossy optimizations (including lossy JBIG2)")
end
complete -c ocrmypdf -x -s O -l optimize -a '(__fish_ocrmypdf_optimize)' -d "select optimization level"
complete -c ocrmypdf -x -s j -l jobs -d "how many worker processes to use"
complete -c ocrmypdf -x -s v -a '(seq 1 9)'
complete -c ocrmypdf -x -l title -d "set metadata"
complete -c ocrmypdf -x -l author -d "set metadata"
complete -c ocrmypdf -x -l subject -d "set metadata"
complete -c ocrmypdf -x -l keywords -d "set metadata"
complete -c ocrmypdf -x -l oversample -d "oversample images to this DPI"
complete -c ocrmypdf -x -l skip-big -d "skip OCR on pages larger than this many MPixels"
complete -c ocrmypdf -x -l jpeg-quality -d "JPEG quality [0..100]"
complete -c ocrmypdf -x -l png-quality -d "PNG quality [0..100]"
complete -c ocrmypdf -x -l jbig2-lossy -d "enable lossy JBIG2 (see docs)"
complete -c ocrmypdf -x -l max-image-mpixels -d "image decompression bomb threshold"
complete -c ocrmypdf -x -l tesseract-config -d "set custom tesseract config file"
complete -c ocrmypdf -x -l tesseract-pagesegmode -d "set tesseract --psm"
complete -c ocrmypdf -x -l tesseract-oem -d "set tesseract --oem"
complete -c ocrmypdf -x -l tesseract-timeout -d "maximum number of seconds to wait for OCR"
complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence"
complete -c ocrmypdf -x -l pdfa-image-compression -a 'auto jpeg lossless' -d "set PDF/A image compression options"
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
+32
View File
@@ -0,0 +1,32 @@
[build-system]
requires = [
"setuptools >= 30.3.0",
"wheel",
"cffi",
"setuptools_scm",
"setuptools_scm_git_archive"
]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 88
py36 = true
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| docs
| misc
| \.egg-info
)/
'''
@@ -1,4 +1,4 @@
check-manifest >= 0.35
twine >= 1.8.1
coverage >= 4.4
coverage >= 4.5
GitPython == 2.1.3
+8 -6
View File
@@ -1,11 +1,13 @@
# 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
cffi == 1.11.5
img2pdf == 0.3.0
pikepdf == 0.3.4
chardet == 3.0.4
cffi == 1.12.2
img2pdf == 0.3.3
pdfminer.six == 20181108
pikepdf == 1.3.0
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.18
pycparser == 2.19
python-xmp-toolkit == 2.0.1
reportlab == 3.4.0
ruffus == 2.7.0
reportlab == 3.5.13
ruffus == 2.8.1
@@ -1,7 +1,7 @@
pytest >= 3.2
pytest-helpers-namespace
pytest-xdist
pytest-cov
pytest >= 4.4.1, < 5
pytest-helpers-namespace >= 2019.1.8
pytest-xdist == 1.28.0
pytest-cov >= 2.6.1
python-xmp-toolkit # requires apt-get install libexempi3
# or brew install exempi
PyPDF2 >= 1.26.0
+10 -1
View File
@@ -11,6 +11,15 @@ ignore =
[tool:pytest]
norecursedirs = lib .pc .git output cache resources
testpaths = tests
filterwarnings =
ignore:.*XMLParser.*:DeprecationWarning
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
[metadata]
license_file = LICENSE
license_file = LICENSE
+40 -199
View File
@@ -20,194 +20,43 @@
from __future__ import print_function, unicode_literals
import sys
if sys.version_info < (3, 5):
print("Python 3.5 or newer is required", file=sys.stderr)
if sys.version_info < (3, 6):
print("Python 3.6 or newer is required", file=sys.stderr)
sys.exit(1)
from setuptools import setup, find_packages # nopep8
from subprocess import STDOUT, check_output, CalledProcessError # nopep8
from collections.abc import Mapping # nopep8
import re # nopep8
import os # nopep8
from setuptools import setup, find_packages
from subprocess import STDOUT, check_output, CalledProcessError
from collections.abc import Mapping
import re
missing_program = '''
The program '{program}' could not be executed or was not found on your
system PATH.
'''
unknown_version = '''
OCRmyPDF requires '{program}' {need_version} or higher. Your system has
'{program}' but we cannot tell what version is installed. Contact the
package maintainer.
'''
old_version = '''
OCRmyPDF requires '{program}' {need_version} or higher. Your system appears
to have {found_version}. Please update this program.
'''
okay_its_optional = '''
This program is OPTIONAL, so installation of OCRmyPDF can proceed, but
some functionality may be missing.
'''
not_okay_its_required = '''
This program is REQUIRED for OCRmyPDF to work. Installation will abort.
'''
osx_install_advice = '''
If you have homebrew installed, try these command to install the missing
packages:
brew update
brew upgrade
brew install {package}
'''
linux_install_advice = '''
On systems with the aptitude package manager (Debian, Ubuntu), try these
commands:
sudo apt-get update
sudo apt-get install {package}
On RPM-based systems (Red Hat, Fedora), search for instructions on
installing the RPM for {program}.
'''
def get_platform():
if sys.platform.startswith('freebsd'):
return 'freebsd'
elif sys.platform.startswith('linux'):
return 'linux'
return sys.platform
def _error_trailer(program, package, optional, **kwargs):
if optional:
print(okay_its_optional.format(**locals()), file=sys.stderr)
else:
print(not_okay_its_required.format(**locals()), file=sys.stderr)
if isinstance(package, Mapping):
package = package[get_platform()]
if get_platform() == 'darwin':
print(osx_install_advice.format(**locals()), file=sys.stderr)
elif get_platform() == 'linux':
print(linux_install_advice.format(**locals()), file=sys.stderr)
def error_missing_program(
program,
package,
optional
):
print(missing_program.format(**locals()), file=sys.stderr)
_error_trailer(**locals())
def error_unknown_version(
program,
package,
optional,
need_version
):
print(unknown_version.format(**locals()), file=sys.stderr)
_error_trailer(**locals())
def error_old_version(
program,
package,
optional,
need_version,
found_version
):
print(old_version.format(**locals()), file=sys.stderr)
_error_trailer(**locals())
def check_external_program(
program,
need_version,
package,
version_check_args=['--version'],
version_scrape_regex=re.compile(r'(\d+\.\d+(?:\.\d+)?)'),
optional=False):
print('Checking for {program} >= {need_version}...'.format(
program=program, need_version=need_version))
try:
result = check_output(
[program] + version_check_args,
universal_newlines=True, stderr=STDOUT)
except (CalledProcessError, FileNotFoundError):
error_missing_program(program, package, optional)
if not optional:
sys.exit(1)
print('Continuing install without {program}'.format(program=program))
return
try:
found_version = version_scrape_regex.search(result).group(1)
except AttributeError:
error_unknown_version(program, package, optional, need_version)
sys.exit(1)
if found_version < need_version:
error_old_version(program, package, optional, need_version,
found_version)
print('Found {program} {found_version}'.format(
program=program, found_version=found_version))
# pylint: disable=w0613
command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
forced = ('--force' in sys.argv)
if command.startswith('install') or command in [
'check',
'test',
'nosetests',
'easy_install',
]:
forced = '--force' in sys.argv
if forced:
print("The argument --force is deprecated. Please discontinue use.")
if not forced and command.startswith('install') or \
command in ['check', 'test', 'nosetests', 'easy_install']:
check_external_program(
program='tesseract',
need_version='3.04', # using backport for Travis CI
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
)
check_external_program(
program='gs',
need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports
package='ghostscript'
)
check_external_program(
program='unpaper',
need_version='6.1', # latest sane version
package='unpaper',
optional=True
)
check_external_program(
program='qpdf',
need_version='8.0.2', # test suite known to fail on 5.1.1
package='qpdf'
)
check_external_program(
program='pngquant',
need_version='2.0.0',
package='pngquant',
optional=True
)
if 'upload' in sys.argv[1:]:
print('Use twine to upload the package - setup.py upload is insecure')
sys.exit(1)
tests_require = open('test_requirements.txt').read().splitlines()
tests_require = open('requirements/test.txt', encoding='utf-8').read().splitlines()
def readme():
with open('README.md') as f:
with open('README.md', encoding='utf-8') as f:
return f.read()
setup(
name='ocrmypdf',
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
@@ -220,7 +69,6 @@ setup(
package_dir={'': 'src'},
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
classifiers=[
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Development Status :: 5 - Production/Stable",
@@ -236,44 +84,37 @@ setup(
"Topic :: Scientific/Engineering :: Image Recognition",
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
],
python_requires=' >= 3.5',
setup_requires=[
'cffi >= 1.9.1', # to build the leptonica module
'pytest-runner', # to enable python setup.py test
'setuptools_scm', # so that version will work
'setuptools_scm_git_archive' # enable version from github tarballs
],
python_requires=' >= 3.6',
setup_requires=[ # can be removed whenever we can drop pip 9 support
'cffi >= 1.9.1', # to build the leptonica module
'pytest-runner', # to enable python setup.py test
'setuptools_scm', # so that version will work
'setuptools_scm_git_archive', # enable version from github tarballs
],
use_scm_version={'version_scheme': 'post-release'},
cffi_modules=[
'src/ocrmypdf/lib/compile_leptonica.py:ffibuilder'
],
cffi_modules=['src/ocrmypdf/lib/compile_leptonica.py:ffibuilder'],
install_requires=[
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.2.4, < 0.4', # pure Python, so track HEAD closely
'pikepdf >= 0.3.3, < 0.4',
'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 ; sys_platform != "darwin"',
'pikepdf >= 1.3.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
'python-xmp-toolkit >= 2, < 3',
'reportlab >= 3.3.0', # oldest released version with sane image handling
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
'reportlab >= 3.3.0', # oldest released version with sane image handling
'ruffus >= 2.7.0',
],
extras_require={
'fitz': [], # Backward compatibility
},
extras_require={'pdfminer': ['pdfminer.six == 20181108']},
tests_require=tests_require,
entry_points={
'console_scripts': [
'ocrmypdf = ocrmypdf.__main__:run_pipeline'
],
},
entry_points={'console_scripts': ['ocrmypdf = ocrmypdf.__main__:run_pipeline']},
package_data={'ocrmypdf': ['data/sRGB.icc']},
include_package_data=True,
zip_safe=False,
project_urls={
'Documentation': 'https://ocrmypdf.readthedocs.io/',
'Source': 'https://github.com/jbarlow83/ocrmypdf',
'Tracker': 'https://github.com/jbarlow83/ocrmypdf/issues'
}
'Tracker': 'https://github.com/jbarlow83/ocrmypdf/issues',
},
)
+12 -4
View File
@@ -25,10 +25,18 @@ __version__ = pkg_resources.get_distribution('ocrmypdf').version
VERSION = __version__
from .exceptions import (
ExitCode, BadArgsError, PdfMergeFailedError, MissingDependencyError,
UnsupportedImageFormatError, DpiError, OutputFileAccessError,
PriorOcrFoundError, InputFileError, SubprocessOutputError,
EncryptedPdfError, TesseractConfigError
ExitCode,
BadArgsError,
PdfMergeFailedError,
MissingDependencyError,
UnsupportedImageFormatError,
DpiError,
OutputFileAccessError,
PriorOcrFoundError,
InputFileError,
SubprocessOutputError,
EncryptedPdfError,
TesseractConfigError,
)
from . import helpers
+485 -329
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -15,10 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import shutil
import sys
from contextlib import suppress
from multiprocessing.managers import SyncManager
import sys
import shutil
from .pdfinfo import PdfInfo
@@ -77,8 +77,7 @@ class JobContextManager(SyncManager):
def cleanup_working_files(work_folder, options):
if options.keep_temporary_files:
print("Temporary working files saved at:\n{0}".format(work_folder),
file=sys.stderr)
print(f"Temporary working files saved at:\n{work_folder}", file=sys.stderr)
else:
with suppress(FileNotFoundError):
shutil.rmtree(work_folder)
+403 -329
View File
File diff suppressed because it is too large Load Diff
+12 -7
View File
@@ -33,12 +33,12 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import codecs
import os
import sys
import codecs
def verify_python3_env():
def verify_python3_env(): # pragma: no cover
"""Ensures that the environment is good for unicode on Python 3."""
# PEP 538 changes in Python 3.7 should make this wrangling unnecessary
@@ -47,6 +47,7 @@ def verify_python3_env():
try:
import locale
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
except Exception:
fs_enc = 'ascii'
@@ -56,8 +57,10 @@ def verify_python3_env():
extra = ''
if os.name == 'posix':
import subprocess
rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()[0]
rv = subprocess.run(
['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
).stdout
good_locales = set()
has_c_utf8 = False
@@ -108,6 +111,8 @@ def verify_python3_env():
'is not supported'
) % bad_locale
raise RuntimeError('ocrmypdf will abort further execution because Python 3 '
'was configured to use ASCII as encoding for the '
'environment.' + extra)
raise RuntimeError(
'ocrmypdf will abort further execution because Python 3 '
'was configured to use ASCII as encoding for the '
'environment.' + extra
)
+144 -180
View File
@@ -15,17 +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/>.
from pathlib import Path
from contextlib import suppress
from itertools import groupby
from pathlib import Path
import os
import pikepdf
from .helpers import flatten_groups, page_number
from .exec import tesseract
from .helpers import flatten_groups, page_number
MAX_REPLACE_PAGES = int(os.environ.get('_OCRMYPDF_MAX_REPLACE_PAGES', 100))
def _update_page_resources(*, page, font, font_key, procset):
"Update this page's fonts with a reference to the Glyphless font"
"""Update this page's fonts with a reference to the Glyphless font"""
if '/Resources' not in page:
page['/Resources'] = pikepdf.Dictionary({})
@@ -34,7 +39,7 @@ def _update_page_resources(*, page, font, font_key, procset):
fonts = resources['/Font']
except KeyError:
fonts = pikepdf.Dictionary({})
if font_key not in fonts:
if font_key is not None and font_key not in fonts:
fonts[font_key] = font
resources['/Font'] = fonts
@@ -43,8 +48,54 @@ def _update_page_resources(*, page, font, font_key, procset):
resources['/ProcSet'] = procset
def strip_invisible_text(pdf, page, log):
stream = []
in_text_obj = False
render_mode = 0
text_objects = []
page.page_contents_coalesce()
for operands, operator in pikepdf.parse_content_stream(page, ''):
if not in_text_obj:
if operator == pikepdf.Operator('BT'):
in_text_obj = True
render_mode = 0
text_objects.append((operands, operator))
else:
stream.append((operands, operator))
else:
if operator == pikepdf.Operator('Tr'):
render_mode = operands[0]
text_objects.append((operands, operator))
if operator == pikepdf.Operator('ET'):
in_text_obj = False
if render_mode != 3:
stream.extend(text_objects)
text_objects.clear()
def convert(op):
try:
return op.unparse()
except AttributeError:
return str(op).encode('ascii')
lines = []
for operands, operator in stream:
if operator == pikepdf.Operator('INLINE IMAGE'):
iim = operands[0]
line = iim.unparse()
else:
line = b' '.join(convert(op) for op in operands) + b' ' + operator.unparse()
lines.append(line)
content_stream = b'\n'.join(lines)
page.Contents = pikepdf.Stream(pdf, content_stream)
def _weave_layers_graft(
*, pdf_base, page_num, text, font, font_key, procset, rotation, log):
*, pdf_base, page_num, text, font, font_key, procset, rotation, strip_old_text, log
):
"""Insert the text layer from text page 0 on to pdf_base at page_num"""
log.debug("Grafting")
@@ -62,7 +113,7 @@ def _weave_layers_graft(
stream = bytearray(pdf_text_contents)
pattern = b'/Im1 Do'
idx = stream.find(pattern)
stream[idx:(idx + len(pattern))] = b' ' * len(pattern)
stream[idx : (idx + len(pattern))] = b' ' * len(pattern)
pdf_text_contents = bytes(stream)
base_page = pdf_base.pages.p(page_num)
@@ -71,12 +122,10 @@ def _weave_layers_graft(
# content may have a rotation applied. Wrap the text stream with a rotation
# so it will be oriented the same way as the rest of the page content.
# (Previous versions OCRmyPDF rotated the content layer to match the text.)
mediabox = [float(pdf_text.pages[0].MediaBox[v])
for v in range(4)]
mediabox = [float(pdf_text.pages[0].MediaBox[v]) for v in range(4)]
wt, ht = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
mediabox = [float(base_page.MediaBox[v])
for v in range(4)]
mediabox = [float(base_page.MediaBox[v]) for v in range(4)]
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
translate = pikepdf.PdfMatrix().translated(-wt / 2, -ht / 2)
@@ -101,139 +150,46 @@ def _weave_layers_graft(
# for a size different between initial and text PDF, then untranslate
ctm = translate @ rotate @ scale @ untranslate
pdf_text_contents = (
b'q %s cm\n' % ctm.encode() +
pdf_text_contents +
b'\nQ\n'
)
pdf_text_contents = b'q %s cm\n' % ctm.encode() + pdf_text_contents + b'\nQ\n'
new_text_layer = pikepdf.Stream(pdf_base, pdf_text_contents)
if strip_old_text:
strip_invisible_text(pdf_base, base_page, log)
base_page.page_contents_add(new_text_layer, prepend=True)
_update_page_resources(
page=base_page, font=font, font_key=font_key, procset=procset
)
pdf_text.close()
def _find_font(text, pdf_base):
"Copy a font from the filename text into pdf_base"
"""Copy a font from the filename text into pdf_base"""
font, font_key = None, None
possible_font_names = ('/f-0-0', '/F1')
try:
pdf_text = pikepdf.open(text)
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
except Exception:
with pikepdf.open(text) as pdf_text:
try:
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
except (AttributeError, IndexError, KeyError):
return None, None
for f in possible_font_names:
pdf_text_font = pdf_text_fonts.get(f, None)
if pdf_text_font is not None:
font_key = f
break
if pdf_text_font:
font = pdf_base.copy_foreign(pdf_text_font)
return font, font_key
except (FileNotFoundError, pikepdf.PdfError):
# PdfError occurs if a 0-length file is written e.g. due to OCR timeout
return None, None
for f in possible_font_names:
pdf_text_font = pdf_text_fonts.get(f, None)
if pdf_text_font is not None:
font_key = f
break
if pdf_text_font:
font = pdf_base.copy_foreign(pdf_text_font)
return font, font_key
def _traverse_toc(pdf_base, visitor_fn, log):
"""
Walk the table of contents, calling visitor_fn() at each node
The /Outlines data structure is a messy data structure, but rather than
navigating hierarchically we just track unique nodes. Enqueue nodes when
we find them, and never visit them again. set() is awesome. We look for
the two types of object in the table of contents that can be page bookmarks
and update the page entry.
"""
visited = set()
queue = set()
link_keys = ('/Parent', '/First', '/Last', '/Prev', '/Next')
if not '/Outlines' in pdf_base.root:
return
queue.add(pdf_base.root.Outlines.objgen)
while queue:
objgen = queue.pop()
visited.add(objgen)
node = pdf_base.get_object(objgen)
log.debug('fix toc: exploring outline entries at %r', objgen)
# Enumerate other nodes we could visit from here
for key in link_keys:
if key not in node:
continue
item = node[key]
if not item.is_indirect:
# Direct references are not allowed here, but it's not clear
# what we should do if we find any. Removing them is an option:
# node[key] = pdf_base.make_indirect(None)
continue
objgen = item.objgen
if objgen not in visited:
queue.add(objgen)
if visitor_fn:
visitor_fn(pdf_base, node, log)
def _fix_toc(pdf_base, pageref_remap, log):
"""Repair the table of contents
Whenever we replace a page wholesale, it gets assigned a new objgen number
and other references to it within the PDF become invalid, most notably in
the table of contents (/Outlines in PDF-speak). In weave_layers we collect
pageref_remap, a mapping that describes the new objgen number given an old
one. (objgen is a tuple, and the gen is almost always zero.)
It may ultimately be better to find a way to rebuild a page in place.
"""
if not pageref_remap:
return
def remap_dest(dest_node):
"""
Inner helper function: change the objgen for any page from the old we
invalidated to its new one.
"""
if not isinstance(dest_node, pikepdf.Array):
return
pageref = dest_node[0]
if pageref['/Type'] == '/Page' and \
pageref.objgen in pageref_remap:
new_objgen = pageref_remap[pageref.objgen]
dest_node[0] = pdf_base.get_object(new_objgen)
def visit_remap_dest(pdf_base, node, log):
"""
Visitor function to fix ToC entries
Test for the two types of references to pages that can occur in ToCs.
Both types have the same final format (an indirect reference to the
target page).
"""
if '/Dest' in node:
# /Dest reference to another page (old method)
remap_dest(node['/Dest'])
elif '/A' in node:
# /A (action) command set to "GoTo" (newer method)
if '/S' in node['/A'] and node['/A']['/S'] == '/GoTo':
remap_dest(node['/A']['/D'])
_traverse_toc(pdf_base, visit_remap_dest, log)
def weave_layers(
infiles,
output_file,
log,
context):
def weave_layers(infiles, output_file, log, context):
"""Apply text layer and/or image layer changes to baseline file
This is where the magic happens. infiles will be the main PDF to modify,
@@ -264,6 +220,7 @@ def weave_layers(
return page_number(key)
except ValueError:
return -1
flat_inputs = sorted(flatten_groups(infiles), key=input_sorter)
groups = groupby(flat_inputs, key=input_sorter)
@@ -272,19 +229,15 @@ def weave_layers(
base = list(basegroup)[0]
path_base = Path(base).resolve()
pdf_base = pikepdf.open(path_base)
keep_open = []
font, font_key, procset = None, None, None
pdfinfo = context.get_pdfinfo()
pagerefs = {}
# Walk the table of contents first, to trigger pikepdf/qpdf to resolve all
# page references in the table of contents. Some PDF generators put invalid
# references in the ToC, so we want to resolve them to null before we
# create any references, or the ToC will be corrupted
_traverse_toc(pdf_base, None, log)
procset = pdf_base.make_indirect(
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]'))
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
)
emplacements = 1
interim_count = 0
# Iterate rest
for page_num, layers in groups:
@@ -292,77 +245,88 @@ def weave_layers(
log.debug(page_num)
log.debug(layers)
text = next(
(ii for ii in layers if ii.endswith('.text.pdf')), None
)
image = next(
(ii for ii in layers if ii.endswith('.image-layer.pdf')), None
)
text = next((ii for ii in layers if ii.endswith('.text.pdf')), None)
image = next((ii for ii in layers if ii.endswith('.image-layer.pdf')), None)
if text and not font:
font, font_key = _find_font(text, pdf_base)
replacing = False
emplaced_page = False
content_rotation = pdfinfo[page_num - 1].rotation
path_image = Path(image).resolve() if image else None
if path_image is not None and path_image != path_base:
# We are replacing the old page with a rasterized PDF of the new
# page
log.debug("Replace")
old_objgen = pdf_base.pages[page_num - 1].objgen
pdf_image = pikepdf.open(image)
keep_open.append(pdf_image)
image_page = pdf_image.pages[0]
pdf_base.pages[page_num - 1] = image_page
# We're adding a new page, which will get a new objgen number pair,
# so we need to update any references to it. qpdf did not like
# my attempt to update the old object in place, but that is an
# option to consider
pagerefs[old_objgen] = pdf_base.pages[page_num - 1].objgen
replacing = True
# We are updating the old page with a rasterized PDF of the new
# page (without changing objgen, to preserve references)
log.debug("Emplacement update")
with pikepdf.open(image) as pdf_image:
emplacements += 1
foreign_image_page = pdf_image.pages[0]
pdf_base.pages.append(foreign_image_page)
local_image_page = pdf_base.pages[-1]
pdf_base.pages[page_num - 1].emplace(local_image_page)
del pdf_base.pages[-1]
emplaced_page = True
autorotate_correction = context.get_rotation(page_num - 1)
if replacing:
if emplaced_page:
content_rotation = autorotate_correction
text_rotation = autorotate_correction
text_misaligned = (text_rotation - content_rotation) % 360
log.debug('%r', [
text_rotation, autorotate_correction, text_misaligned,
content_rotation]
log.debug(
'%r',
[text_rotation, autorotate_correction, text_misaligned, content_rotation],
)
if text and font:
# Graft the text layer onto this page, whether new or old
strip_old = context.get_options().redo_ocr
_weave_layers_graft(
pdf_base=pdf_base, page_num=page_num, text=text, font=font,
font_key=font_key, rotation=text_misaligned, procset=procset,
log=log
pdf_base=pdf_base,
page_num=page_num,
text=text,
font=font,
font_key=font_key,
rotation=text_misaligned,
procset=procset,
strip_old_text=strip_old,
log=log,
)
# Correct the rotation if applicable
pdf_base.pages[page_num - 1].Rotate = \
(content_rotation - autorotate_correction) % 360
pdf_base.pages[page_num - 1].Rotate = (
content_rotation - autorotate_correction
) % 360
if len(keep_open) > 100:
# qpdf limitations require us to keep files open when we intend
# to copy content from them before saving. However, we want to keep
# a lid on file handles and memory usage, so for big files we're
# going to stop and save periodically. Attach the font to page 1
# even if page 1 doesn't use it, so we have a way to get it back.
if emplacements % MAX_REPLACE_PAGES == 0:
# Periodically save and reload the Pdf object. This will keep a
# lid on our memory usage for very large files. Attach the font to
# page 1 even if page 1 doesn't use it, so we have a way to get it
# back.
# TODO refactor this to outside the loop
page0 = pdf_base.pages[0]
_update_page_resources(
page=page0, font=font, font_key=font_key, procset=procset)
interim = output_file + '_working{}.pdf'.format(page_num)
pdf_base.save(interim)
del pdf_base
keep_open = []
page=page0, font=font, font_key=font_key, procset=procset
)
pdf_base = pikepdf.open(interim)
# We cannot read and write the same file, that will corrupt it
# but we don't to keep more copies than we need to. Delete intermediates.
# {interim_count} is the opened file we were updateing
# {interim_count - 1} can be deleted
# {interim_count + 1} is the new file will produce and open
old_file = output_file + f'_working{interim_count - 1}.pdf'
if not context.get_options().keep_temporary_files:
with suppress(FileNotFoundError):
os.unlink(old_file)
next_file = output_file + f'_working{interim_count + 1}.pdf'
pdf_base.save(next_file)
pdf_base.close()
pdf_base = pikepdf.open(next_file)
procset = pdf_base.pages[0].Resources.ProcSet
font = pdf_base.pages[0].Resources.Font.get(font_key)
font, font_key = None, None # Ensure we reacquire this information
interim_count += 1
_fix_toc(pdf_base, pagerefs, log)
pdf_base.save(output_file)
pdf_base.close()
+10 -4
View File
@@ -19,6 +19,7 @@
from enum import IntEnum
from textwrap import dedent
class ExitCode(IntEnum):
ok = 0
bad_args = 1
@@ -52,7 +53,8 @@ class BadArgsError(ExitCodeException):
class PdfMergeFailedError(ExitCodeException):
exit_code = ExitCode.input_file
message = dedent('''\
message = dedent(
'''\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is malformed and
@@ -60,7 +62,9 @@ class PdfMergeFailedError(ExitCodeException):
Try using
ocrmypdf --pdf-renderer sandwich [..other args..]
''')
'''
)
class MissingDependencyError(ExitCodeException):
exit_code = ExitCode.missing_dependency
@@ -92,7 +96,8 @@ class SubprocessOutputError(ExitCodeException):
class EncryptedPdfError(ExitCodeException):
exit_code = ExitCode.encrypted_pdf
message = dedent('''\
message = dedent(
'''\
Input PDF is encrypted. The encryption must be removed to
perform OCR.
@@ -101,7 +106,8 @@ class EncryptedPdfError(ExitCodeException):
You can remove the encryption using
qpdf --decrypt [--password=[password]] infilename
''')
'''
)
class TesseractConfigError(ExitCodeException):
+132 -18
View File
@@ -21,39 +21,153 @@ import os
import re
import sys
from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
from ..exceptions import MissingDependencyError, ExitCode
from collections.abc import Mapping
def get_version(program, *,
version_arg='--version', regex=r'(\d+(\.\d+)*)'):
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'):
"Get the version of the specified program"
args_prog = [
program,
version_arg
]
args_prog = [program, version_arg]
try:
proc = run(
args_prog, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=STDOUT, check=True)
args_prog,
close_fds=True,
universal_newlines=True,
stdout=PIPE,
stderr=STDOUT,
check=True,
)
output = proc.stdout
except FileNotFoundError as e:
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
f"Could not find program '{program}' on the PATH"
) from e
except CalledProcessError as e:
if e.returncode < 0:
if e.returncode != 0:
raise MissingDependencyError(
"Ran program '{}' but it exited with an error:\n{}".format(
program, e.output)) from e
f"Ran program '{program}' but it exited with an error:\n{e.output}"
) from e
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
f"Could not find program '{program}' on the PATH"
) from e
try:
version = re.match(regex, output.strip()).group(1)
except AttributeError as e:
raise MissingDependencyError(
("The program '{}' did not report its version. "
"Message was:\n{}").format(program, output)
f"The program '{program}' did not report its version. "
f"Message was:\n{output}"
)
return version
missing_program = '''
The program '{program}' could not be executed or was not found on your
system PATH.
'''
missing_optional_program = '''
The program '{program}' could not be executed or was not found on your
system PATH. This program is required when you use the
{required_for} arguments. You could try omitting these arguments, or install
the package.
'''
missing_recommend_program = '''
The program '{program}' could not be executed or was not found on your
system PATH. This program is recommended when using the {required_for} arguments,
but not required, so we will proceed. For best results, install the program.
'''
old_version = '''
OCRmyPDF requires '{program}' {need_version} or higher. Your system appears
to have {found_version}. Please update this program.
'''
old_version_required_for = '''
OCRmyPDF requires '{program}' {need_version} or higher when run with the
{required_for} arguments. If you omit these arguments, OCRmyPDF may be able to
proceed. For best results, install the program.
'''
osx_install_advice = '''
If you have homebrew installed, try these command to install the missing
package:
brew install {package}
'''
linux_install_advice = '''
On systems with the aptitude package manager (Debian, Ubuntu), try these
commands:
sudo apt-get update
sudo apt-get install {package}
On RPM-based systems (Red Hat, Fedora), search for instructions on
installing the RPM for {program}.
'''
def _get_platform():
if sys.platform.startswith('freebsd'):
return 'freebsd'
elif sys.platform.startswith('linux'):
return 'linux'
return sys.platform
def _error_trailer(log, program, package, **kwargs):
if isinstance(package, Mapping):
package = package[_get_platform()]
if _get_platform() == 'darwin':
log.info(osx_install_advice.format(**locals()))
elif _get_platform() == 'linux':
log.info(linux_install_advice.format(**locals()))
def _error_missing_program(log, program, package, required_for, recommended):
if required_for:
log.error(missing_optional_program.format(**locals()))
elif recommended:
log.info(missing_recommend_program.format(**locals()))
else:
log.error(missing_program.format(**locals()))
_error_trailer(**locals())
def _error_old_version(
log, program, package, need_version, found_version, required_for
):
if required_for:
log.error(old_version_required_for.format(**locals()))
else:
log.error(old_version.format(**locals()))
_error_trailer(**locals())
def check_external_program(
*,
log,
program,
package,
version_checker,
need_version,
required_for=None,
recommended=False,
):
try:
found_version = version_checker()
except (CalledProcessError, FileNotFoundError, MissingDependencyError):
_error_missing_program(log, program, package, required_for, recommended)
if not recommended:
sys.exit(ExitCode.missing_dependency)
return
if found_version < need_version:
_error_old_version(
log, program, package, need_version, found_version, required_for
)
if not recommended:
sys.exit(ExitCode.missing_dependency)
log.debug(f'Found {program} {found_version}')
+90 -79
View File
@@ -15,15 +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/>.
from tempfile import NamedTemporaryFile
from subprocess import run, PIPE, STDOUT
from shutil import copy
from functools import lru_cache
import re
from functools import lru_cache
from os import fspath
from shutil import copy
from subprocess import PIPE, STDOUT, run
from tempfile import NamedTemporaryFile
from PIL import Image
from . import get_version
from ..exceptions import SubprocessOutputError
from ..helpers import fspath
@lru_cache(maxsize=1)
@@ -69,39 +71,46 @@ def extract_text(input_file, pageno=1):
"""
if pageno is not None:
pages = [
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno
]
pages = ['-dFirstPage=%i' % pageno, '-dLastPage=%i' % pageno]
else:
pages = []
args_gs = [
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=txtwrite',
'-dTextFormat=0',
] + pages + [
'-o', '-',
fspath(input_file)
]
args_gs = (
[
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=txtwrite',
'-dTextFormat=0',
]
+ pages
+ ['-o', '-', fspath(input_file)]
)
p = run(args_gs, stdout=PIPE, stderr=PIPE)
if p.returncode != 0:
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\n%s'
% (input_file, p.stdout.decode(), p.stderr.decode())
)
return p.stdout
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
pageno=1, page_dpi=None, rotation=None):
def rasterize_pdf(
input_file,
output_file,
xres,
yres,
raster_device,
log,
pageno=1,
page_dpi=None,
rotation=None,
filter_vector=False,
):
"""Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
The image is sized to match the integer pixels dimensions implied by
@@ -116,33 +125,39 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
:param log:
:param pageno: page number to rasterize (beginning at page 1)
:param page_dpi: resolution tuple (x, y) overriding output image DPI
:param rotation: 0, 90, 180, 270: clockwise angle to rotate page
:param filter_vector: if True, remove vector graphics objects
:return:
"""
res = xres, yres
int_res = round(xres), round(yres)
res = round(xres, 6), round(yres, 6)
if not page_dpi:
page_dpi = res
with NamedTemporaryFile(delete=True) as tmp:
args_gs = [
'gs',
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=%s' % raster_device,
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
'-r{0}x{1}'.format(str(int_res[0]), str(int_res[1])),
'-o', tmp.name,
'-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',
tmp.name,
'-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)
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
else:
@@ -152,21 +167,8 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
log.error('Ghostscript rasterizing failed')
raise SubprocessOutputError()
# Ghostscript only accepts integers for output resolution
# if the resolution happens to be fractional, then the discrepancy
# would change the size of the output page, especially if the DPI
# is quite low. Resize the image to the expected size
tmp.seek(0)
with Image.open(tmp) as im:
expected_size = round(im.size[0] / int_res[0] * res[0]), \
round(im.size[1] / int_res[1] * res[1])
if expected_size != im.size or page_dpi != (xres, yres):
log.debug(
"Ghostscript: resize output image {} -> {}".format(
im.size, expected_size))
im = im.resize(expected_size)
if rotation is not None:
log.debug("Rotating output by %i", rotation)
# rotation is a clockwise angle and Image.ROTATE_* is
@@ -182,8 +184,15 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
im.save(fspath(output_file), dpi=page_dpi)
def generate_pdfa(pdf_pages, output_file, compression, log,
threads=1, pdf_version='1.5', pdfa_part='2'):
def generate_pdfa(
pdf_pages,
output_file,
compression,
log,
threads=1,
pdf_version='1.5',
pdfa_part='2',
):
"""Generate a PDF/A.
The pdf_pages, a list files, will be merged into output_file. One or more
@@ -237,26 +246,28 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
# 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),
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy
] + compression_args + [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
"-dPDFACompatibilityPolicy=1",
"-sOutputFile=" + gs_pdf.name,
]
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)
p = run(args_gs, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
if _gs_error_reported(p.stdout):
log.error(p.stdout)
@@ -267,7 +278,7 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
log.debug(
"Ghostscript had to remove PDF 'overprinting' from the "
"input file to complete PDF/A conversion. "
)
)
else:
log.debug(p.stdout)
+4 -11
View File
@@ -15,14 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import CalledProcessError, run, PIPE
from functools import lru_cache
import sys
import os
import shutil
from subprocess import PIPE, run
from . import get_version
from ..exceptions import ExitCode, MissingDependencyError
from ..exceptions import MissingDependencyError
@lru_cache(maxsize=1)
@@ -45,7 +42,7 @@ def convert_group(*, cwd, infiles, out_prefix):
out_prefix,
'-s', # symbol mode (lossy)
# '-r', # refinement mode (lossless symbol mode, currently disabled in
# jbig2)
# jbig2)
'-p',
]
args.extend(infiles)
@@ -55,11 +52,7 @@ def convert_group(*, cwd, infiles, out_prefix):
def convert_single(*, cwd, infile, outfile):
args = [
'jbig2',
'-p',
infile
]
args = ['jbig2', '-p', infile]
with open(outfile, 'wb') as fstdout:
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
proc.check_returncode()
+34 -17
View File
@@ -15,15 +15,14 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import CalledProcessError, run
from tempfile import TemporaryFile
from functools import lru_cache
import sys
import os
import shutil
from subprocess import run
from tempfile import NamedTemporaryFile
from PIL import Image
from . import get_version
from ..exceptions import ExitCode, MissingDependencyError
from ..exceptions import MissingDependencyError
@lru_cache(maxsize=1)
@@ -40,14 +39,32 @@ def available():
def quantize(input_file, output_file, quality_min, quality_max):
args = [
'pngquant',
'--force',
'--skip-if-larger',
'--output', output_file,
'--quality', '{}-{}'.format(quality_min, quality_max),
'--',
input_file
]
proc = run(args)
proc.check_returncode()
if input_file.endswith('.jpg'):
im = Image.open(input_file)
with NamedTemporaryFile(suffix='.png') as tmp:
im.save(tmp)
args = [
'pngquant',
'--force',
'--skip-if-larger',
'--output',
output_file,
'--quality',
f'{quality_min}-{quality_max}',
'--',
tmp.name,
]
run(args)
else:
args = [
'pngquant',
'--force',
'--skip-if-larger',
'--output',
output_file,
'--quality',
f'{quality_min}-{quality_max}',
'--',
input_file,
]
run(args)
+6 -13
View File
@@ -15,12 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from subprocess import CalledProcessError, STDOUT, PIPE, run
from functools import lru_cache
from os import fspath
from subprocess import PIPE, STDOUT, CalledProcessError, run
from ..exceptions import InputFileError, SubprocessOutputError, \
EncryptedPdfError
from . import get_version
from . import get_version
@lru_cache(maxsize=1)
@@ -29,22 +28,16 @@ def version():
def check(input_file, log=None):
args_qpdf = [
'qpdf',
'--check',
input_file
]
args_qpdf = ['qpdf', '--check', fspath(input_file)]
if log is None:
import logging as log
try:
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True,
check=True)
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("%s: not a valid PDF, and could not repair it.", input_file)
log.error("Details:")
log.error(e.output)
elif e.returncode == 3:
+100 -99
View File
@@ -15,22 +15,28 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import shutil
from functools import lru_cache
import sys
from collections import namedtuple
from textwrap import dedent
from subprocess import CalledProcessError, TimeoutExpired, check_output, STDOUT
from contextlib import suppress
from functools import lru_cache
from os import fspath
from subprocess import (
PIPE,
STDOUT,
CalledProcessError,
TimeoutExpired,
check_output,
run,
)
from textwrap import dedent
from . import get_version
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number
from . import get_version
OrientationConfidence = namedtuple(
'OrientationConfidence',
('angle', 'confidence'))
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
@@ -39,8 +45,8 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.05.00' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "_blank.png"; bbox 0 0 {0} {1}; ppageno 0'>
@@ -64,56 +70,52 @@ def v4():
def has_textonly_pdf():
"""Does Tesseract have textonly_pdf capability?
Available in 3.05.01, and v4.00.00alpha since January 2017. Best to
Available in v4.00.00alpha since January 2017. Best to
parse the parameter list
"""
args_tess = [
'tesseract',
'--print-parameters',
'pdf'
]
args_tess = ['tesseract', '--print-parameters', 'pdf']
params = ''
try:
params = check_output(
args_tess, universal_newlines=True, stderr=STDOUT)
params = check_output(args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
print("Could not --print-parameters from tesseract",
file=sys.stderr)
print("Could not --print-parameters from tesseract", file=sys.stderr)
raise MissingDependencyError from e
if 'textonly_pdf' in params:
return True
return False
def psm():
"If Tesseract 4.0, use argument --psm instead of -psm"
return '--psm' if v4() else '-psm'
@lru_cache(maxsize=1)
def languages():
args_tess = [
'tesseract',
'--list-langs'
]
try:
langs = check_output(
args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
msg = dedent("""Tesseract failed to report available languages.
def lang_error(output):
msg = dedent(
"""Tesseract failed to report available languages.
Output from Tesseract:
-----------
""")
msg += e.output
"""
)
msg += output
print(msg, file=sys.stderr)
args_tess = ['tesseract', '--list-langs']
try:
proc = run(
args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT, check=True
)
output = proc.stdout
except CalledProcessError as e:
lang_error(e.output)
raise MissingDependencyError from e
return set(lang.strip() for lang in langs.splitlines()[1:])
header, *rest = output.splitlines()
if not header.startswith('List of available languages'):
lang_error(output)
raise MissingDependencyError
return set(lang.strip() for lang in rest)
def tess_base_args(langs, engine_mode):
args = [
'tesseract',
]
args = ['tesseract']
if langs:
args.extend(['-l', '+'.join(langs)])
if engine_mode is not None and v4():
@@ -123,20 +125,22 @@ def tess_base_args(langs, engine_mode):
def get_orientation(input_file, engine_mode, timeout: float, log):
args_tesseract = tess_base_args(['osd'], engine_mode) + [
psm(), '0',
input_file,
'stdout'
'--psm',
'0',
fspath(input_file),
'stdout',
]
try:
stdout = check_output(
args_tesseract, stderr=STDOUT, timeout=timeout)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
except TimeoutExpired:
return OrientationConfidence(angle=0, confidence=0.0)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if (b'Too few characters. Skipping this page' in e.output or
b'Image too large' in e.output):
if (
b'Too few characters. Skipping this page' in e.output
or b'Image too large' in e.output
):
return OrientationConfidence(0, 0)
raise e from e
else:
@@ -148,32 +152,24 @@ def get_orientation(input_file, engine_mode, timeout: float, log):
osd[parts[0].strip()] = parts[1].strip()
angle = int(osd.get('Orientation in degrees', 0))
if 'Orientation' in osd:
# Tesseract < 3.04.01
# reports "Orientation in degrees" as a counterclockwise angle
# We keep it clockwise
assert 'Rotate' not in osd
angle = -angle % 360
else:
# Tesseract >= 3.04.01
# reports "Orientation in degrees" as a clockwise angle
assert 'Rotate' in osd
oc = OrientationConfidence(
angle=angle,
confidence=float(osd.get('Orientation confidence', 0)))
angle=angle, confidence=float(osd.get('Orientation confidence', 0))
)
return oc
def tesseract_log_output(log, stdout, input_file):
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
prefix = f"{(page_number(input_file)):4d}: [tesseract] "
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.")
log.error(
prefix
+ "command line output was not utf-8. "
+ "This usually means Tesseract's language packs do not match "
"the installed version of Tesseract."
)
text = stdout.decode('utf-8', 'backslashreplace')
lines = text.splitlines()
@@ -204,8 +200,10 @@ def tesseract_log_output(log, stdout, input_file):
log.info(prefix + line.strip())
def page_timedout(log, input_file):
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
def page_timedout(log, input_file, timeout):
if timeout == 0:
return
prefix = f"{(page_number(input_file)):4d}: [tesseract] "
log.warning(prefix + " took too long to OCR - skipping")
@@ -223,10 +221,18 @@ def _generate_null_hocr(output_hocr, output_sidecar, image):
f.write('[skipped page]')
def generate_hocr(input_file, output_files, language: list, engine_mode,
tessconfig: list,
timeout: float, pagesegmode: int, user_words, user_patterns,
log):
def generate_hocr(
input_file,
output_files,
language: list,
engine_mode,
tessconfig: list,
timeout: float,
pagesegmode: int,
user_words,
user_patterns,
log,
):
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
@@ -235,7 +241,7 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
args_tesseract = tess_base_args(language, engine_mode)
if pagesegmode is not None:
args_tesseract.extend([psm(), str(pagesegmode)])
args_tesseract.extend(['--psm', str(pagesegmode)])
if user_words:
args_tesseract.extend(['--user-words', user_words])
@@ -245,25 +251,15 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
# Reminder: test suite tesseract spoofers will break after any changes
# to the number of order parameters here
# Tesseract 3.04 requires the order here to be "hocr txt" and will fail
# on "txt hocr"
args_tesseract.extend([
input_file,
prefix,
'hocr',
'txt'
] + tessconfig)
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, stderr=STDOUT,
timeout=timeout)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
except TimeoutExpired:
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
# it does not have a valid hOCR file.
page_timedout(log, input_file)
page_timedout(log, input_file, timeout)
_generate_null_hocr(output_hocr, output_sidecar, input_file)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
@@ -296,10 +292,22 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
out.write(b'')
def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
language: list, engine_mode, text_only: bool,
tessconfig: list, timeout: float, pagesegmode: int,
user_words, user_patterns, log):
def generate_pdf(
*,
input_image,
skip_pdf=None,
output_pdf,
output_text,
language: list,
engine_mode,
text_only: bool,
tessconfig: list,
timeout: float,
pagesegmode: int,
user_words,
user_patterns,
log,
):
'''Use Tesseract to render a PDF.
input_image -- image to analyze
@@ -317,7 +325,7 @@ def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
args_tesseract = tess_base_args(language, engine_mode)
if pagesegmode is not None:
args_tesseract.extend([psm(), str(pagesegmode)])
args_tesseract.extend(['--psm', str(pagesegmode)])
if text_only and has_textonly_pdf():
args_tesseract.extend(['-c', 'textonly_pdf=1'])
@@ -333,22 +341,15 @@ def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
# Reminder: test suite tesseract spoofers might break after any changes
# to the number of order parameters here
args_tesseract.extend([
input_image,
prefix,
'pdf',
'txt'
] + tessconfig)
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, stderr=STDOUT,
timeout=timeout)
stdout = check_output(args_tesseract, stderr=STDOUT, timeout=timeout)
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_text)
except TimeoutExpired:
page_timedout(log, input_image)
page_timedout(log, input_image, timeout)
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image)
+58 -31
View File
@@ -18,14 +18,16 @@
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
from subprocess import CalledProcessError, STDOUT, check_output
from tempfile import NamedTemporaryFile
import sys
import os
import shlex
import subprocess
import sys
from functools import lru_cache
from ..exceptions import MissingDependencyError
from . import get_version
from subprocess import PIPE, STDOUT, CalledProcessError
from tempfile import TemporaryDirectory
from . import get_version
from ..exceptions import MissingDependencyError, SubprocessOutputError
try:
from PIL import Image
@@ -40,11 +42,7 @@ def version():
def run(input_file, output_file, dpi, log, mode_args):
args_unpaper = [
'unpaper',
'-v',
'--dpi', str(dpi)
] + mode_args
args_unpaper = ['unpaper', '-v', '--dpi', str(dpi)] + mode_args
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
@@ -68,35 +66,64 @@ def run(input_file, output_file, dpi, log, mode_args):
im.close()
raise MissingDependencyError() from e
with NamedTemporaryFile(suffix=suffix) as input_pnm, \
NamedTemporaryFile(suffix=suffix, mode="r+b") as output_pnm:
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()
os.unlink(output_pnm.name)
args_unpaper.extend([input_pnm.name, output_pnm.name])
# To prevent any shenanigans from accepting arbitrary parameters in
# --unpaper-args, we:
# 1) run with cwd set to a tmpdir with only unpaper's files
# 2) forbid the use of '/' in arguments, to prevent changing paths
# 3) append absolute paths for the input and output file
# This should ensure that a user cannot clobber some other file with
# their unpaper arguments (whether intentionally or otherwise)
args_unpaper.extend([input_pnm, output_pnm])
try:
stdout = check_output(
args_unpaper, close_fds=True,
universal_newlines=True, stderr=STDOUT,
)
proc = subprocess.run(
args_unpaper,
check=True,
close_fds=True,
universal_newlines=True,
stderr=STDOUT,
cwd=tmpdir,
stdout=PIPE,
)
except CalledProcessError as e:
log.debug(e.output)
raise e from e
else:
log.debug(stdout)
# unpaper sets dpi to 72
Image.open(output_pnm.name).save(output_file, dpi=(dpi, dpi))
log.debug(proc.stdout)
# unpaper sets dpi to 72; fix this
try:
Image.open(output_pnm).save(output_file, dpi=(dpi, dpi))
except (FileNotFoundError, OSError):
raise SubprocessOutputError(
"unpaper: failed to produce the expected output file. Called with: "
+ str(args_unpaper)
) from None
def clean(input_file, output_file, dpi, log):
run(input_file, output_file, dpi, log, [
'--layout', 'none',
'--mask-scan-size', '100', # don't blank out narrow columns
def validate_custom_args(args: str):
unpaper_args = shlex.split(args)
if any('/' in arg for arg in unpaper_args):
raise ValueError('No filenames allowed in --unpaper-args')
return unpaper_args
def clean(input_file, output_file, dpi, log, unpaper_args=None):
default_args = [
'--layout',
'none',
'--mask-scan-size',
'100', # don't blank out narrow columns
'--no-border-align', # don't align visible content to borders
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-deskew', # don't deskew
])
'--no-mask-center', # don't center visible content within page
'--no-grayfilter', # don't remove light gray areas
'--no-blackfilter', # don't remove solid black areas
'--no-deskew', # don't deskew
]
if not unpaper_args:
unpaper_args = default_args
run(input_file, output_file, dpi, log, unpaper_args)
+40 -63
View File
@@ -15,22 +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/>.
from functools import partial
import multiprocessing
import os
import sys
import warnings
from collections.abc import Iterable
from contextlib import suppress
from functools import partial, wraps
from pathlib import Path
import sys
import os
import multiprocessing
import warnings
def re_symlink(input_file, soft_link_name, log=None):
"""
Helper function: relinks soft symbolic link if necessary
"""
input_file = fspath(input_file) # For Py3.5
soft_link_name = fspath(soft_link_name)
input_file = os.fspath(input_file)
soft_link_name = os.fspath(soft_link_name)
if log is None:
prdebug = partial(print, file=sys.stderr)
else:
@@ -38,32 +38,29 @@ def re_symlink(input_file, soft_link_name, log=None):
# Guard against soft linking to oneself
if input_file == soft_link_name:
prdebug("Warning: No symbolic link made. You are using " +
"the original data directory as the working directory.")
prdebug(
"Warning: No symbolic link made. You are using "
+ "the original data directory as the working directory."
)
return
# Soft link already exists: delete for relink?
if os.path.lexists(soft_link_name):
# do not delete or overwrite real (non-soft link) file
if not os.path.islink(soft_link_name):
raise FileExistsError(
"%s exists and is not a link" % soft_link_name)
raise FileExistsError("%s exists and is not a link" % soft_link_name)
try:
os.unlink(soft_link_name)
except OSError:
prdebug("Can't unlink %s" % (soft_link_name))
if not os.path.exists(input_file):
raise FileNotFoundError(
"trying to create a broken symlink to %s" % input_file)
raise FileNotFoundError("trying to create a broken symlink to %s" % input_file)
prdebug("os.symlink(%s, %s)" % (input_file, soft_link_name))
# Create symbolic link using absolute path
os.symlink(
os.path.abspath(input_file),
soft_link_name
)
os.symlink(os.path.abspath(input_file), soft_link_name)
def is_iterable_notstr(thing):
@@ -72,7 +69,7 @@ def is_iterable_notstr(thing):
def page_number(input_file):
"""Get one-based page number implied by filename (000002.pdf -> 2)"""
return int(os.path.basename(fspath(input_file))[0:6])
return int(os.path.basename(os.fspath(input_file))[0:6])
def available_cpu_count():
@@ -83,13 +80,14 @@ def available_cpu_count():
try:
import psutil
return psutil.cpu_count()
except (ImportError, AttributeError):
pass
warnings.warn(
"Could not get CPU count. Assuming one (1) CPU."
"Use -j N to set manually.")
"Could not get CPU count. Assuming one (1) CPU." "Use -j N to set manually."
)
return 1
@@ -103,20 +101,15 @@ def is_file_writable(test_file):
p = Path(test_file)
if p.is_symlink():
# Python 3.5 does not accept parameters for Path.resolve() and behaves
# as if strict=True (throws an exception on failure). Python 3.6
# defaults to strict=False. This implements strict=False like behavior
# for Python 3.5.
if sys.version_info[0:2] <= (3, 5):
p = Path(os.path.realpath(fspath(p)))
else:
p = p.resolve(strict=False)
p = p.resolve(strict=False)
# p.is_file() throws an exception in some cases
if p.exists() and p.is_file():
return os.access(
fspath(p), os.W_OK,
effective_ids=(os.access in os.supports_effective_ids))
os.fspath(p),
os.W_OK,
effective_ids=(os.access in os.supports_effective_ids),
)
else:
try:
fp = p.open('wb')
@@ -129,42 +122,26 @@ def is_file_writable(test_file):
return True
if sys.version_info[0:2] <= (3, 5):
def fspath(path):
"""https://www.python.org/dev/peps/pep-0519/#os"""
import pathlib
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path = path_type.__fspath__(path)
except AttributeError:
# Added for Python 3.5 support.
if isinstance(path, pathlib.Path):
return str(path)
elif hasattr(path_type, '__fspath__'):
raise
else:
if isinstance(path, (str, bytes)):
return path
else:
raise TypeError("expected __fspath__() to return str or bytes, "
"not " + type(path).__name__)
raise TypeError(
"expected str, bytes, pathlib.Path or os.PathLike object, not "
+ path_type.__name__)
else:
fspath = os.fspath
def flatten_groups(groups):
for obj in groups:
if is_iterable_notstr(obj):
yield from obj
else:
yield obj
def deprecated(func):
"""Warn that function is deprecated"""
@wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn(
"Call to deprecated function {}.".format(func.__name__),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)
return new_func
+113 -73
View File
@@ -28,14 +28,14 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch
from xml.etree import ElementTree
from collections import namedtuple
from math import atan, sin, cos
import re
import argparse
import re
from collections import namedtuple
from math import atan, cos, sin
from xml.etree import ElementTree
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
@@ -44,7 +44,7 @@ class HocrTransformError(Exception):
pass
class HocrTransform():
class HocrTransform:
"""
A class for converting documents from the hOCR format.
@@ -53,17 +53,16 @@ class HocrTransform():
"""
box_pattern = re.compile(r'bbox((\s+\d+){4})')
baseline_pattern = re.compile(r'''
baseline_pattern = re.compile(
r'''
baseline \s+
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
([\-\+]?\d+) # +/- int''', re.VERBOSE)
ligatures = str.maketrans({
'': 'ff',
'': 'ffi',
'': 'ffl',
'': 'fi',
'': 'fl',
})
([\-\+]?\d+) # +/- int''',
re.VERBOSE,
)
ligatures = str.maketrans(
{'': 'ff', '': 'ffi', '': 'ffl', '': 'fi', '': 'fl'}
)
def __init__(self, hocrFileName, dpi):
self.dpi = dpi
@@ -78,8 +77,7 @@ class HocrTransform():
# get dimension in pt (not pixel!!!!) of the OCRed image
self.width, self.height = None, None
for div in self.hocr.findall(
".//%sdiv[@class='ocr_page']" % (self.xmlns)):
for div in self.hocr.findall(".//%sdiv[@class='ocr_page']" % (self.xmlns)):
coords = self.element_coordinates(div)
pt_coords = self.pt_from_pixel(coords)
self.width = pt_coords.x2 - pt_coords.x1
@@ -144,8 +142,7 @@ class HocrTransform():
"""
Returns the quantity in PDF units (pt) given quantity in pixels
"""
return Rect._make(
(c / self.dpi * inch) for c in pxl)
return Rect._make((c / self.dpi * inch) for c in pxl)
@classmethod
def replace_unsupported_chars(cls, s):
@@ -156,8 +153,15 @@ class HocrTransform():
"""
return s.translate(cls.ligatures)
def to_pdf(self, outFileName, imageFileName=None, showBoundingboxes=False,
fontname="Helvetica", invisibleText=False, interwordSpaces=False):
def to_pdf(
self,
outFileName,
imageFileName=None,
showBoundingboxes=False,
fontname="Helvetica",
invisibleText=False,
interwordSpaces=False,
):
"""
Creates a PDF file with an image superimposed on top of the text.
Text is positioned according to the bounding box of the lines in
@@ -168,17 +172,15 @@ class HocrTransform():
"""
# create the PDF file
# page size in points (1/72 in.)
pdf = Canvas(
outFileName, pagesize=(self.width, self.height), pageCompression=1)
pdf = Canvas(outFileName, pagesize=(self.width, self.height), pageCompression=1)
# draw bounding box for each paragraph
# light blue for bounding box of paragraph
pdf.setStrokeColorRGB(0, 1, 1)
# light blue for bounding box of paragraph
pdf.setFillColorRGB(0, 1, 1)
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.findall(
".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.findall(".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
elemtxt = self._get_element_text(elem).rstrip()
if len(elemtxt) == 0:
@@ -190,38 +192,58 @@ class HocrTransform():
# draw the bbox border
if showBoundingboxes:
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
fill=1)
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
)
found_lines = False
for line in self.hocr.findall(
".//%sspan[@class='%s']" % (self.xmlns, "ocr_line")):
".//%sspan[@class='%s']" % (self.xmlns, "ocr_line")
):
found_lines = True
self._do_line(pdf, line, "ocrx_word", fontname, invisibleText,
interwordSpaces, showBoundingboxes)
self._do_line(
pdf,
line,
"ocrx_word",
fontname,
invisibleText,
interwordSpaces,
showBoundingboxes,
)
if not found_lines:
# Tesseract did not report any lines (just words)
root = self.hocr.find(".//%sdiv[@class='%s']" % (self.xmlns, "ocr_page"))
self._do_line(pdf, root, "ocrx_word", fontname, invisibleText,
interwordSpaces, showBoundingboxes)
self._do_line(
pdf,
root,
"ocrx_word",
fontname,
invisibleText,
interwordSpaces,
showBoundingboxes,
)
# put the image on the page, scaled to fill the page
if imageFileName is not None:
pdf.drawImage(imageFileName, 0, 0,
width=self.width, height=self.height)
pdf.drawImage(imageFileName, 0, 0, width=self.width, height=self.height)
# finish up the page and save it
pdf.showPage()
pdf.save()
@classmethod
def polyval(cls, poly, x):
return x * poly[0] + poly[1]
def _do_line(self, pdf, line, elemclass, fontname, invisibleText,
interwordSpaces, showBoundingboxes):
def _do_line(
self,
pdf,
line,
elemclass,
fontname,
invisibleText,
interwordSpaces,
showBoundingboxes,
):
pxl_line_coords = self.element_coordinates(line)
line_box = self.pt_from_pixel(pxl_line_coords)
line_height = line_box.y2 - line_box.y1
@@ -254,23 +276,20 @@ class HocrTransform():
pdf.setLineWidth(0.5)
# negate slope because it is defined as a rise/run in pixel
# coordinates and page coordinates have the y axis flipped
pdf.line(line_box.x1,
baseline_y2,
line_box.x2,
self.polyval((-slope, baseline_y2),
line_box.x2 - line_box.x1))
pdf.line(
line_box.x1,
baseline_y2,
line_box.x2,
self.polyval((-slope, baseline_y2), line_box.x2 - line_box.x1),
)
# light green for bounding box of word/line
pdf.setDash(6, 3)
pdf.setStrokeColorRGB(1, 0, 0)
text.setTextTransform(
cos_a, -sin_a, sin_a, cos_a,
line_box.x1, baseline_y2
)
text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2)
pdf.setFillColorRGB(0, 0, 0) # text in black
elements = line.findall(
".//%sspan[@class='%s']" % (self.xmlns, elemclass))
elements = line.findall(".//%sspan[@class='%s']" % (self.xmlns, elemclass))
for elem in elements:
elemtxt = self._get_element_text(elem).strip()
elemtxt = self.replace_unsupported_chars(elemtxt)
@@ -287,22 +306,22 @@ class HocrTransform():
# though it would look better, because it will interfere with
# naive text extraction. \n does not work either.
elemtxt += ' '
box = Rect._make((
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
line_box.y2))
box = Rect._make(
(
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
line_box.y2,
)
)
box_width = box.x2 - box.x1
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
# draw the bbox border
if showBoundingboxes:
pdf.rect(
box.x1,
self.height - line_box.y2,
box_width,
line_height,
fill=0)
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
)
# Adjust relative position of cursor
# This is equivalent to:
@@ -331,19 +350,40 @@ class HocrTransform():
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
parser.add_argument('-b', '--boundingboxes', action="store_true",
default=False, help='Show bounding boxes borders')
parser.add_argument('-r', '--resolution', type=int,
default=300,
help='Resolution of the image that was OCRed')
parser.add_argument('-i', '--image', default=None,
help='Path to the image to be placed above the text')
parser.add_argument('--interword-spaces', action='store_true',
default=False, help='Add spaces between words')
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument(
'outputfile', help='Path to the PDF file to be generated')
'-b',
'--boundingboxes',
action="store_true",
default=False,
help='Show bounding boxes borders',
)
parser.add_argument(
'-r',
'--resolution',
type=int,
default=300,
help='Resolution of the image that was OCRed',
)
parser.add_argument(
'-i',
'--image',
default=None,
help='Path to the image to be placed above the text',
)
parser.add_argument(
'--interword-spaces',
action='store_true',
default=False,
help='Add spaces between words',
)
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(args.hocrfile, args.resolution)
hocr.to_pdf(args.outputfile, args.image, args.boundingboxes, interwordSpaces=args.interword_spaces)
hocr.to_pdf(
args.outputfile,
args.image,
args.boundingboxes,
interwordSpaces=args.interword_spaces,
)
+446 -197
View File
@@ -21,27 +21,26 @@
# Python FFI wrapper for Leptonica library
import argparse
import sys
import os
import logging
import os
import sys
import warnings
from tempfile import TemporaryFile
from collections.abc import Sequence
from contextlib import suppress
from ctypes.util import find_library
from functools import lru_cache
from io import BytesIO
from os import fspath
from tempfile import TemporaryFile
from .lib._leptonica import ffi
from .helpers import fspath
# pylint: disable=protected-access
lept = ffi.dlopen(find_library('lept'))
logger = logging.getLogger(__name__)
def stderr(*objs):
"""Shorthand print to stderr."""
print("leptonica.py:", *objs, file=sys.stderr)
lept = ffi.dlopen(find_library('lept'))
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
class _LeptonicaErrorTrap:
@@ -62,40 +61,48 @@ class _LeptonicaErrorTrap:
def __init__(self):
self.tmpfile = None
self.copy_of_stderr = -1
self.no_stderr = False
def __enter__(self):
from io import UnsupportedOperation
self.tmpfile = TemporaryFile()
# Save the old stderr, and redirect stderr to temporary file
sys.stderr.flush()
with suppress(AttributeError):
sys.stderr.flush()
try:
self.copy_of_stderr = os.dup(sys.stderr.fileno())
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(),
inheritable=False)
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(), inheritable=False)
except AttributeError:
# We are in some unusual context where our Python process does not
# have a sys.stderr. Leptonica still expects to write to file
# descriptor 2, so we are going to ensure it is redirected.
self.copy_of_stderr = None
self.no_stderr = True
os.dup2(self.tmpfile.fileno(), 2, inheritable=False)
except UnsupportedOperation:
self.copy_of_stderr = None
return
def __exit__(self, exc_type, exc_value, traceback):
# Restore old stderr
sys.stderr.flush()
with suppress(AttributeError):
sys.stderr.flush()
if self.copy_of_stderr is not None:
os.dup2(self.copy_of_stderr, sys.stderr.fileno())
os.close(self.copy_of_stderr)
if self.no_stderr:
os.close(2)
# Get data from tmpfile (in with block to ensure it is closed)
with self.tmpfile as tmpfile:
tmpfile.seek(0) # Cursor will be at end, so move back to beginning
leptonica_output = tmpfile.read().decode(errors='replace')
assert self.tmpfile.closed
assert not sys.stderr.closed
# If there are Python errors, let them bubble up
# Get data from tmpfile
self.tmpfile.seek(0) # Cursor will be at end, so move back to beginning
leptonica_output = self.tmpfile.read().decode(errors='replace')
self.tmpfile.close()
# If there are Python errors, record them
if exc_type:
logger.warning(leptonica_output)
return False
# If there are Leptonica errors, wrap them in Python excpetions
if 'Error' in leptonica_output:
@@ -103,6 +110,8 @@ class _LeptonicaErrorTrap:
raise FileNotFoundError()
if 'pixWrite: stream not opened' in leptonica_output:
raise LeptonicaIOError()
if 'index not valid' in leptonica_output:
raise IndexError()
raise LeptonicaError(leptonica_output)
return False
@@ -116,7 +125,43 @@ class LeptonicaIOError(LeptonicaError):
pass
class Pix:
class LeptonicaObject:
"""General wrapper for Leptonica objects
When Leptonica returns an object, we bundled it in a wrapper class, which
manages its memory. The wrapper class assumes that it will be calling some
sort of lept.thingDestroy() function when the instance is deleted. Most
Leptonica objects are reference counted, and destroy decrements the
refcount.
Most of the time, when Leptonica returns something, we wrap and it the job
is done. When wrapping objects that came from a Leptonica container, like
a PIXA returning PIX, the subclass must clone the object before passing it
here, to maintain the reference count.
CFFI ensures that the destroy function is called at garbage collection time
so we do not need to mess with __del__.
"""
cdata_destroy = lambda cdata: None
LEPTONICA_TYPENAME = ''
def __init__(self, cdata):
if not cdata:
raise ValueError('Tried to wrap a NULL ' + self.LEPTONICA_TYPENAME)
self._cdata = ffi.gc(cdata, self._destroy)
@classmethod
def _destroy(cls, cdata):
"""Destroy some cdata"""
# Leptonica API uses double-pointers for its destroy APIs to prevent
# dangling pointers. This means we need to put our single pointer,
# cdata, in a temporary CDATA**.
pp = ffi.new('{} **'.format(cls.LEPTONICA_TYPENAME), cdata)
cls.cdata_destroy(pp)
class Pix(LeptonicaObject):
"""
Wrapper around leptonica's PIX object.
@@ -135,18 +180,21 @@ class Pix:
modified objects. This allows convenient chaining:
>>> Pix.open('filename.jpg').scale((0.5, 0.5)).deskew().show()
"""
def __init__(self, pix):
self._pix = ffi.gc(pix, Pix._pix_destroy)
LEPTONICA_TYPENAME = "PIX"
cdata_destroy = lept.pixDestroy
def __repr__(self):
if self._pix:
if self._cdata:
s = "<leptonica.Pix image size={0}x{1} depth={2}{4} at 0x{3:x}>"
return s.format(self._pix.w, self._pix.h, self._pix.d,
int(ffi.cast('intptr_t', self._pix)),
'(colormapped)' if self._pix.colormap else '')
return s.format(
self._cdata.w,
self._cdata.h,
self._cdata.d,
int(ffi.cast('intptr_t', self._cdata)),
'(colormapped)' if self._cdata.colormap else '',
)
else:
return "<leptonica.Pix image NULL>"
@@ -159,7 +207,7 @@ class Pix:
data = ffi.new('l_uint8 **')
size = ffi.new('size_t *')
err = lept.pixWriteMemPng(data, size, self._pix, 0)
err = lept.pixWriteMemPng(data, size, self._cdata, 0)
if err != 0:
raise LeptonicaIOError("pixWriteMemPng")
@@ -170,7 +218,7 @@ class Pix:
data = ffi.new('l_uint32 **')
size = ffi.new('size_t *')
err = lept.pixSerializeToMemory(self._pix, data, size)
err = lept.pixSerializeToMemory(self._cdata, data, size)
if err != 0:
raise LeptonicaIOError("pixSerializeToMemory")
@@ -187,32 +235,38 @@ class Pix:
cdata_bytes = ffi.new('char[]', state['data'])
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
pix = lept.pixDeserializeFromMemory(
cdata_uint32, len(state['data']))
pix = lept.pixDeserializeFromMemory(cdata_uint32, len(state['data']))
Pix.__init__(self, pix)
def __eq__(self, other):
return self.__getstate__() == other.__getstate__()
if not isinstance(other, Pix):
return NotImplemented
same = ffi.new('l_int32 *', 0)
with _LeptonicaErrorTrap():
err = lept.pixEqual(self._cdata, other._cdata, same)
if err:
raise TypeError()
return bool(same[0])
@property
def width(self):
return self._pix.w
return self._cdata.w
@property
def height(self):
return self._pix.h
return self._cdata.h
@property
def depth(self):
return self._pix.d
return self._cdata.d
@property
def size(self):
return (self._pix.w, self._pix.h)
return (self._cdata.w, self._cdata.h)
@property
def info(self):
return {'dpi': (self._pix.xres, self._pix.yres)}
return {'dpi': (self._cdata.xres, self._cdata.yres)}
@property
def mode(self):
@@ -221,7 +275,7 @@ class Pix:
return '1'
elif self.depth >= 16:
return 'RGB'
elif not self._pix.colormap:
elif not self._cdata.colormap:
return 'L'
else:
return 'P'
@@ -242,8 +296,7 @@ class Pix:
with _LeptonicaErrorTrap():
return cls(lept.pixRead(os.fsencode(filename)))
def write_implied_format(
self, path, jpeg_quality=0, jpeg_progressive=0):
def write_implied_format(self, path, jpeg_quality=0, jpeg_progressive=0):
"""Write pix to the filename, with the extension indicating format.
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
@@ -252,11 +305,22 @@ class Pix:
filename = fspath(path)
with _LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
os.fsencode(filename),
self._pix, jpeg_quality, jpeg_progressive)
os.fsencode(filename), self._cdata, jpeg_quality, jpeg_progressive
)
@classmethod
def frompil(self, pillow_image):
"""Create a copy of a PIL.Image from this Pix"""
bio = BytesIO()
pillow_image.save(bio, format='png', compress_level=1)
py_buffer = bio.getbuffer()
c_buffer = ffi.from_buffer(py_buffer)
with _LeptonicaErrorTrap():
pix = Pix(lept.pixReadMem(c_buffer, len(c_buffer)))
return pix
def topil(self):
"Returns a PIL.Image version of this Pix"
"""Returns a PIL.Image version of this Pix"""
from PIL import Image
# Leptonica manages data in words, so it implicitly does an endian
@@ -269,17 +333,17 @@ class Pix:
raw_mode = 'ABGR'
elif self.mode == '1':
raw_mode = '1;I'
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
pix = Pix(lept.pixEndianByteSwapNew(pix._cdata))
else:
raw_mode = self.mode
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
pix = Pix(lept.pixEndianByteSwapNew(pix._cdata))
else:
raw_mode = self.mode # no endian swap needed
size = (pix._pix.w, pix._pix.h)
bytecount = pix._pix.wpl * 4 * pix._pix.h
buf = ffi.buffer(pix._pix.data, bytecount)
stride = pix._pix.wpl * 4
size = (pix._cdata.w, pix._cdata.h)
bytecount = pix._cdata.wpl * 4 * pix._cdata.h
buf = ffi.buffer(pix._cdata.data, bytecount)
stride = pix._cdata.wpl * 4
im = Image.frombytes(self.mode, size, buf, 'raw', raw_mode, stride)
@@ -298,21 +362,21 @@ class Pix:
for skew angle
"""
with _LeptonicaErrorTrap():
return Pix(lept.pixDeskew(self._pix, reduction_factor))
return Pix(lept.pixDeskew(self._cdata, reduction_factor))
def scale(self, scale_xy):
"Returns the pix object rescaled according to the proportions given."
with _LeptonicaErrorTrap():
return Pix(lept.pixScale(self._pix, scale_xy[0], scale_xy[1]))
return Pix(lept.pixScale(self._cdata, scale_xy[0], scale_xy[1]))
def rotate180(self):
with _LeptonicaErrorTrap():
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
return Pix(lept.pixRotate180(ffi.NULL, self._cdata))
def rotate_orth(self, quads):
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
with _LeptonicaErrorTrap():
return Pix(lept.pixRotateOrth(self._pix, quads))
return Pix(lept.pixRotateOrth(self._cdata, quads))
def find_skew(self):
"""Returns a tuple (deskew angle in degrees, confidence value).
@@ -322,7 +386,7 @@ class Pix:
with _LeptonicaErrorTrap():
angle = ffi.new('float *', 0.0)
confidence = ffi.new('float *', 0.0)
result = lept.pixFindSkew(self._pix, angle, confidence)
result = lept.pixFindSkew(self._cdata, angle, confidence)
if result == 0:
return (angle[0], confidence[0])
else:
@@ -330,7 +394,7 @@ class Pix:
def convert_rgb_to_luminance(self):
with _LeptonicaErrorTrap():
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
gray_pix = lept.pixConvertRGBToLuminance(self._cdata)
if gray_pix:
return Pix(gray_pix)
return None
@@ -343,114 +407,165 @@ class Pix:
"""
with _LeptonicaErrorTrap():
return Pix(lept.pixRemoveColormapGeneral(
self._pix, removal_type, lept.L_COPY))
return Pix(
lept.pixRemoveColormapGeneral(self._cdata, removal_type, lept.L_COPY)
)
def otsu_adaptive_threshold(
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1
):
with _LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
p_pix = ffi.new('PIX **')
pix = Pix(lept.pixConvertTo8(self._cdata, 0))
result = lept.pixOtsuAdaptiveThreshold(
self._pix,
sx, sy,
smoothx, smoothy,
scorefract,
ffi.NULL,
p_pix)
pix._cdata, sx, sy, smoothx, smoothy, scorefract, ffi.NULL, p_pix
)
if result == 0:
return Pix(p_pix[0])
else:
return None
def otsu_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
bgval=255, kernel_size=(2, 2), scorefract=0.1):
self,
mask=None,
tile_size=(10, 15),
thresh=100,
mincount=50,
bgval=255,
kernel_size=(2, 2),
scorefract=0.1,
):
with _LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
if mask is None:
mask = ffi.NULL
mask = ffi.NULL
if isinstance(mask, Pix):
mask = mask._pix
mask = mask._cdata
pix = Pix(lept.pixConvertTo8(self._cdata, 0))
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
self._pix,
pix._cdata,
mask,
sx, sy,
thresh, mincount, bgval,
smoothx, smoothy,
sx,
sy,
thresh,
mincount,
bgval,
smoothx,
smoothy,
scorefract,
ffi.NULL
)
if thresh_pix == ffi.NULL:
return None
ffi.NULL,
)
return Pix(thresh_pix)
def masked_threshold_on_background_norm(
self,
mask=None,
tile_size=(10, 15),
thresh=100,
mincount=50,
kernel_size=(2, 2),
scorefract=0.1,
):
with _LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
mask = ffi.NULL
if isinstance(mask, Pix):
mask = mask._cdata
pix = Pix(lept.pixConvertTo8(self._cdata, 0))
thresh_pix = lept.pixMaskedThreshOnBackgroundNorm(
pix._cdata,
mask,
sx,
sy,
thresh,
mincount,
smoothx,
smoothy,
scorefract,
ffi.NULL,
)
return Pix(thresh_pix)
def crop_to_foreground(
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
showmorph=0, display=0, pdfdir=ffi.NULL):
self,
threshold=128,
mindist=70,
erasedist=30,
pagenum=0,
showmorph=0,
display=0,
pdfdir=ffi.NULL,
):
with _LeptonicaErrorTrap():
cropbox = Box(lept.pixFindPageForeground(
self._pix,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir))
cropbox = Box(
lept.pixFindPageForeground(
self._cdata,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir,
)
)
print(repr(cropbox))
cropped_pix = lept.pixClipRectangle(
self._pix,
cropbox._box,
ffi.NULL)
cropped_pix = lept.pixClipRectangle(self._cdata, cropbox._cdata, ffi.NULL)
return Pix(cropped_pix)
def clean_background_to_white(
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255
):
with _LeptonicaErrorTrap():
return Pix(lept.pixCleanBackgroundToWhite(
self._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white))
return Pix(
lept.pixCleanBackgroundToWhite(
self._cdata,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white,
)
)
def gamma_trc(self, gamma=1.0, minval=0, maxval=255):
with _LeptonicaErrorTrap():
return Pix(lept.pixGammaTRC(
ffi.NULL,
self._pix,
gamma,
minval,
maxval
))
return Pix(lept.pixGammaTRC(ffi.NULL, self._cdata, gamma, minval, maxval))
def background_norm(
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
self,
mask=None,
grayscale=None,
tile_size=(10, 15),
fg_threshold=60,
min_count=40,
bg_val=200,
smooth_kernel=(2, 1),
):
# Background norm doesn't work on color mapped Pix, so remove colormap
target_pix = self.remove_colormap(lept.REMOVE_CMAP_BASED_ON_SRC)
with _LeptonicaErrorTrap():
return Pix(lept.pixBackgroundNorm(
target_pix._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
tile_size[0],
tile_size[1],
fg_threshold,
min_count,
bg_val,
smooth_kernel[0],
smooth_kernel[1]
))
return Pix(
lept.pixBackgroundNorm(
target_pix._cdata,
mask or ffi.NULL,
grayscale or ffi.NULL,
tile_size[0],
tile_size[1],
fg_threshold,
min_count,
bg_val,
smooth_kernel[0],
smooth_kernel[1],
)
)
@staticmethod
@lru_cache(maxsize=1)
@@ -464,53 +579,93 @@ class Pix:
# implementation of pixCorrelationBinary that overflows on larger
# images. Ubuntu 14.04/trusty has 1.70. Ubuntu PPA
# ppa:alex-p/tesseract-ocr has leptonlib 1.75.
pix1_count = ffi.new('l_int32 *')
pix2_count = ffi.new('l_int32 *')
pixn_count = ffi.new('l_int32 *')
tab8 = Pix.make_pixel_sum_tab8()
raise LeptonicaError("Leptonica version is too old")
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
# Python converts these int32s to larger units as needed
# to avoid overflow. Overflow happens easily here.
correlation = (
(pixn_count[0] * pixn_count[0]) /
(pix1_count[0] * pix2_count[0])
)
return correlation
else:
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
return correlation[0]
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1._cdata, pix2._cdata, correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
return correlation[0]
def generate_pdf_ci_data(self, type_, quality):
"Convert to PDF data, with transcoding"
p_compdata = ffi.new('L_COMP_DATA **')
result = lept.pixGenerateCIData(self._pix, type_, quality, 0,
p_compdata)
result = lept.pixGenerateCIData(self._cdata, type_, quality, 0, p_compdata)
if result != 0:
raise LeptonicaError("Generate PDF data failed")
return CompressedData(p_compdata[0])
def invert(self):
return Pix(lept.pixInvert(ffi.NULL, self._pix))
return Pix(lept.pixInvert(ffi.NULL, self._cdata))
@staticmethod
def _pix_destroy(pix):
p_pix = ffi.new('PIX **', pix)
lept.pixDestroy(p_pix)
# print('pix destroy ' + repr(pix))
def locate_barcodes(self):
try:
with _LeptonicaErrorTrap():
pix = Pix(lept.pixConvertTo8(self._cdata, 0))
pixa_candidates = PixArray(lept.pixExtractBarcodes(pix._cdata, 0))
if not pixa_candidates:
return
sarray = StringArray(
lept.pixReadBarcodes(
pixa_candidates._cdata,
lept.L_BF_ANY,
lept.L_USE_WIDTHS,
ffi.NULL,
0,
)
)
except (LeptonicaError, ValueError, IndexError):
return
finally:
leptonica_junk = ('junkpixt.png', 'junkpixt')
for junk in leptonica_junk:
with suppress(FileNotFoundError):
os.unlink(junk) # leptonica may produce this
for n, s in enumerate(sarray):
decoded = s.decode()
if decoded.strip() == '':
continue
box = pixa_candidates.get_box(n)
left, top = box.x, box.y
right, bottom = box.x + box.w, box.y + box.h
yield (decoded, (left, top, right, bottom))
def despeckle(self, size):
if size == 2:
speckle2 = """
oooo
oC o
o o
oooo
"""
sel1 = Sel.from_selstr(speckle2, 'speckle2')
sel2 = Sel.create_brick(2, 2, 0, 0, lept.SEL_HIT)
elif size == 3:
speckle3 = """
ooooo
oC o
o o
o o
ooooo
"""
sel1 = Sel.from_selstr(speckle3, 'speckle3')
sel2 = Sel.create_brick(3, 3, 0, 0, lept.SEL_HIT)
else:
raise ValueError(size)
pixhmt = Pix(lept.pixHMT(ffi.NULL, self._cdata, sel1._cdata))
pixdilated = Pix(lept.pixDilate(ffi.NULL, pixhmt._cdata, sel2._cdata))
pixsub = Pix(lept.pixSubtract(ffi.NULL, self._cdata, pixdilated._cdata))
return pixsub
class CompressedData:
def __init__(self, compdata):
self._compdata = ffi.gc(compdata, CompressedData._destroy)
class CompressedData(LeptonicaObject):
"""Wrapper for L_COMP_DATA - abstract compressed image data"""
LEPTONICA_TYPENAME = 'L_COMP_DATA'
cdata_destroy = lept.l_CIDataDestroy
@classmethod
def open(cls, path, jpeg_quality=75):
@@ -519,70 +674,150 @@ class CompressedData:
p_compdata = ffi.new('L_COMP_DATA **')
result = lept.l_generateCIDataForPdf(
os.fsencode(filename), ffi.NULL, jpeg_quality, p_compdata)
os.fsencode(filename), ffi.NULL, jpeg_quality, p_compdata
)
if result != 0:
raise LeptonicaError("CompressedData.open")
return CompressedData(p_compdata[0])
def __len__(self):
return self._compdata.nbytescomp
return self._cdata.nbytescomp
def read(self):
buf = ffi.buffer(self._compdata.datacomp, self._compdata.nbytescomp)
buf = ffi.buffer(self._cdata.datacomp, self._cdata.nbytescomp)
return bytes(buf)
def __getattr__(self, name):
if hasattr(self._compdata, name):
return getattr(self._compdata, name)
if hasattr(self._cdata, name):
return getattr(self._cdata, name)
raise AttributeError(name)
def get_palette_pdf_string(self):
"Returns palette pre-formatted for use in PDF"
buflen = len('< ') + len(' rrggbb') * self._compdata.ncolors + len('>')
buf = ffi.buffer(self._compdata.cmapdatahex, buflen)
buflen = len('< ') + len(' rrggbb') * self._cdata.ncolors + len('>')
buf = ffi.buffer(self._cdata.cmapdatahex, buflen)
return bytes(buf)
@staticmethod
def _destroy(compdata):
pp = ffi.new('L_COMP_DATA **', compdata)
lept.l_CIDataDestroy(pp)
class PixArray(LeptonicaObject, Sequence):
"""Wrapper around PIXA (array of PIX)"""
LEPTONICA_TYPENAME = 'PIXA'
cdata_destroy = lept.pixaDestroy
def __len__(self):
return self._cdata[0].n
def __getitem__(self, n):
with _LeptonicaErrorTrap():
return Pix(lept.pixaGetPix(self._cdata, n, lept.L_CLONE))
def get_box(self, n):
with _LeptonicaErrorTrap():
return Box(lept.pixaGetBox(self._cdata, n, lept.L_CLONE))
class Box:
"""Wrapper around Leptonica's BOX objects.
class Box(LeptonicaObject):
"""Wrapper around Leptonica's BOX objects (a pixel rectangle)
See class Pix for notes about reference counting.
Uses x, y, w, h coordinates.
"""
def __init__(self, box):
self._box = ffi.gc(box, Box._box_destroy)
LEPTONICA_TYPENAME = 'BOX'
cdata_destroy = lept.boxDestroy
def __repr__(self):
if self._box:
if self._cdata:
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
self.x, self.y, self.w, self.h)
self.x, self.y, self.w, self.h
)
return '<leptonica.Box NULL>'
@property
def x(self):
return self._box.x
return self._cdata.x
@property
def y(self):
return self._box.y
return self._cdata.y
@property
def w(self):
return self._box.w
return self._cdata.w
@property
def h(self):
return self._box.h
return self._cdata.h
@staticmethod
def _box_destroy(box):
p_box = ffi.new('BOX **', box)
lept.boxDestroy(p_box)
class BoxArray(LeptonicaObject, Sequence):
"""Wrapper around Leptonica's BOXA (Array of BOX) objects."""
LEPTONICA_TYPENAME = 'BOXA'
cdata_destroy = lept.boxaDestroy
def __repr__(self):
if not self._cdata:
return '<BoxArray>'
boxes = (repr(box) for box in self)
return '<BoxArray [' + ', '.join(boxes) + ']>'
def __len__(self):
return self._cdata.n
def __getitem__(self, n):
if not isinstance(n, int):
raise TypeError('list indices must be integers')
if 0 <= n < len(self):
return Box(lept.boxaGetBox(self._cdata, n, lept.L_CLONE))
raise IndexError(n)
class StringArray(LeptonicaObject, Sequence):
"""Leptonica SARRAY/string array"""
LEPTONICA_TYPENAME = 'SARRAY'
cdata_destroy = lept.sarrayDestroy
def __len__(self):
return self._cdata.n
def __getitem__(self, n):
if 0 <= n < len(self):
return ffi.string(self._cdata.array[n])
raise IndexError(n)
class Sel(LeptonicaObject):
"""Leptonica 'sel'/selection element for hit-miss transform"""
LEPTONICA_TYPENAME = 'SEL'
cdata_destroy = lept.selDestroy
@classmethod
def from_selstr(cls, selstr, name):
# TODO this will strip a horizontal line of don't care's
lines = [line.strip() for line in selstr.split('\n') if line.strip()]
h = len(lines)
w = len(lines[0])
lengths = set(len(line) for line in lines)
if len(lengths) != 1:
raise ValueError("All lines in selstr must be same length")
repacked = ''.join(line.strip() for line in lines)
buf_selstr = ffi.from_buffer(repacked.encode('ascii'))
buf_name = ffi.from_buffer(name.encode('ascii'))
sel = lept.selCreateFromString(buf_selstr, h, w, buf_name)
return cls(sel)
@classmethod
def create_brick(cls, h, w, cy, cx, type_):
sel = lept.selCreateBrick(h, w, cy, cx, type_)
return cls(sel)
def __repr__(self):
selstr = ffi.gc(lept.selPrintToString(self._cdata), lept.lept_free)
return '<Sel \n' + ffi.string(selstr).decode('ascii') + '\n>'
@lru_cache(maxsize=1)
@@ -614,15 +849,22 @@ def deskew(infile, outfile, dpi):
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
black_threshold=70, white_threshold=190):
def remove_background(
infile,
outfile,
tile_size=(40, 60),
gamma=1.0,
black_threshold=70,
white_threshold=190,
):
try:
pix = Pix.open(infile)
except LeptonicaIOError:
raise LeptonicaIOError("Failed to open file: %s" % infile)
pix = pix.background_norm(tile_size=tile_size).gamma_trc(
gamma, black_threshold, white_threshold)
gamma, black_threshold, white_threshold
)
try:
pix.write_implied_format(outfile)
@@ -631,15 +873,22 @@ def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Python wrapper to access Leptonica")
parser = argparse.ArgumentParser(description="Python wrapper to access Leptonica")
subparsers = parser.add_subparsers(title='commands',
description='supported operations')
subparsers = parser.add_subparsers(
title='commands', description='supported operations'
)
parser_deskew = subparsers.add_parser('deskew')
parser_deskew.add_argument('-r', '--dpi', dest='dpi', action='store',
type=int, default=300, help='input resolution')
parser_deskew.add_argument(
'-r',
'--dpi',
dest='dpi',
action='store',
type=int,
default=300,
help='input resolution',
)
parser_deskew.add_argument('infile', help='image to deskew')
parser_deskew.add_argument('outfile', help='deskewed output image')
parser_deskew.set_defaults(func=deskew)
File diff suppressed because one or more lines are too long
+196 -13
View File
@@ -19,8 +19,8 @@
from cffi import FFI
ffibuilder = FFI()
ffibuilder.set_source("ocrmypdf.lib._leptonica", None)
ffibuilder.cdef("""
ffibuilder.cdef(
"""
typedef signed char l_int8;
typedef unsigned char l_uint8;
typedef short l_int16;
@@ -32,6 +32,8 @@ typedef double l_float64;
typedef long long l_int64;
typedef unsigned long long l_uint64;
typedef int l_ok; /*!< return type 0 if OK, 1 on error */
struct Pix
{
l_uint32 w; /* width in pixels */
@@ -61,6 +63,17 @@ struct PixColormap
};
typedef struct PixColormap PIXCMAP;
/*! Array of pix */
struct Pixa
{
l_int32 n; /*!< number of Pix in ptr array */
l_int32 nalloc; /*!< number of Pix ptrs allocated */
l_uint32 refcount; /*!< reference count (1 if no clones) */
struct Pix **pix; /*!< the array of ptrs to pix */
struct Boxa *boxa; /*!< array of boxes */
};
typedef struct Pixa PIXA;
struct Box
{
l_int32 x;
@@ -72,6 +85,25 @@ struct Box
};
typedef struct Box BOX;
/*! Array of Box */
struct Boxa
{
l_int32 n; /*!< number of box in ptr array */
l_int32 nalloc; /*!< number of box ptrs allocated */
l_uint32 refcount; /*!< reference count (1 if no clones) */
struct Box **box; /*!< box ptr array */
};
typedef struct Boxa BOXA;
/*! String array: an array of C strings */
struct Sarray
{
l_int32 nalloc; /*!< size of allocated ptr array */
l_int32 n; /*!< number of strings allocated */
l_int32 refcount; /*!< reference count (1 if no clones) */
char **array; /*!< string array */
};
typedef struct Sarray SARRAY;
/*! Pdf formatted encoding types */
enum {
@@ -104,6 +136,18 @@ struct L_Compressed_Data
};
typedef struct L_Compressed_Data L_COMP_DATA;
/*! Selection */
struct Sel
{
l_int32 sy; /*!< sel height */
l_int32 sx; /*!< sel width */
l_int32 cy; /*!< y location of sel origin */
l_int32 cx; /*!< x location of sel origin */
l_int32 **data; /*!< {0,1,2}; data[i][j] in [row][col] order */
char *name; /*!< used to find sel by name */
};
typedef struct Sel SEL;
enum {
REMOVE_CMAP_TO_BINARY = 0, /*!< remove colormap for conv to 1 bpp */
REMOVE_CMAP_TO_GRAYSCALE = 1, /*!< remove colormap for conv to 8 bpp */
@@ -122,10 +166,50 @@ enum {
/*!< the array with clones (e.g., pix) */
};
""")
/*! Flags for method of extracting barcode widths */
enum {
L_USE_WIDTHS = 1, /*!< use histogram of barcode widths */
L_USE_WINDOWS = 2 /*!< find best window for decoding transitions */
};
ffibuilder.cdef("""
/*! Flags for barcode formats */
enum {
L_BF_UNKNOWN = 0, /*!< unknown format */
L_BF_ANY = 1, /*!< try decoding with all known formats */
L_BF_CODE128 = 2, /*!< decode with Code128 format */
L_BF_EAN8 = 3, /*!< decode with EAN8 format */
L_BF_EAN13 = 4, /*!< decode with EAN13 format */
L_BF_CODE2OF5 = 5, /*!< decode with Code 2 of 5 format */
L_BF_CODEI2OF5 = 6, /*!< decode with Interleaved 2 of 5 format */
L_BF_CODE39 = 7, /*!< decode with Code39 format */
L_BF_CODE93 = 8, /*!< decode with Code93 format */
L_BF_CODABAR = 9, /*!< decode with Code93 format */
L_BF_UPCA = 10 /*!< decode with UPC A format */
};
enum {
L_SEVERITY_EXTERNAL = 0, /* Get the severity from the environment */
L_SEVERITY_ALL = 1, /* Lowest severity: print all messages */
L_SEVERITY_DEBUG = 2, /* Print debugging and higher messages */
L_SEVERITY_INFO = 3, /* Print informational and higher messages */
L_SEVERITY_WARNING = 4, /* Print warning and higher messages */
L_SEVERITY_ERROR = 5, /* Print error and higher messages */
L_SEVERITY_NONE = 6 /* Highest severity: print no messages */
};
enum {
SEL_DONT_CARE = 0,
SEL_HIT = 1,
SEL_MISS = 2
};
"""
)
ffibuilder.cdef(
"""
PIX * pixRead ( const char *filename );
PIX * pixReadMem ( const l_uint8 *data, size_t size );
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 );
@@ -137,6 +221,11 @@ pixWriteMemPng(l_uint8 **pdata,
void pixDestroy ( PIX **ppix );
l_ok
pixEqual(PIX *pix1,
PIX *pix2,
l_int32 *psame);
PIX *
pixEndianByteSwapNew(PIX *pixs);
@@ -157,6 +246,8 @@ l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
PIX * pixConvertRGBToLuminance(PIX *pixs);
PIX * pixConvertTo8(PIX *pixs, l_int32 cmapflag);
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
l_int32
@@ -182,6 +273,18 @@ pixOtsuThreshOnBackgroundNorm(PIX *pixs,
l_float32 scorefract,
l_int32 *pthresh);
PIX *
pixMaskedThreshOnBackgroundNorm(PIX *pixs,
PIX *pixim,
l_int32 sx,
l_int32 sy,
l_int32 thresh,
l_int32 mincount,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
l_int32 *pthresh);
PIX *
pixCleanBackgroundToWhite(PIX *pixs,
PIX *pixim,
@@ -265,13 +368,13 @@ pixGetAverageMaskedRGB(PIX *pixs,
l_float32 *pgval,
l_float32 *pbval);
PIX *
PIX *
pixGlobalNormRGB(PIX * pixd,
PIX * pixs,
l_int32 rval,
l_int32 gval,
l_int32 bval,
l_int32 mapval);
l_int32 mapval);
PIX *
pixInvert(PIX * pixd,
@@ -289,22 +392,102 @@ pixGenerateCIData(PIX *pixs,
l_int32 ascii85,
L_COMP_DATA **pcid);
l_int32
l_generateCIDataForPdf(const char *fname,
PIX *pix,
l_int32 quality,
SARRAY *
pixProcessBarcodes(PIX *pixs,
l_int32 format,
l_int32 method,
SARRAY **psaw,
l_int32 debugflag);
PIX *
pixaGetPix(PIXA *pixa,
l_int32 index,
l_int32 accesstype);
BOX*
pixaGetBox (PIXA * pixa,
l_int32 index,
l_int32 accesstype );
PIXA *
pixExtractBarcodes(PIX *pixs,
l_int32 debugflag);
BOXA *
pixLocateBarcodes ( PIX *pixs, l_int32 thresh, PIX **ppixb, PIX **ppixm );
SARRAY *
pixReadBarcodes(PIXA *pixa,
l_int32 format,
l_int32 method,
SARRAY **psaw,
l_int32 debugflag);
l_int32
l_generateCIDataForPdf(const char *fname,
PIX *pix,
l_int32 quality,
L_COMP_DATA **pcid);
void
BOX *
boxClone ( BOX *box );
BOX *
boxaGetBox ( BOXA *boxa, l_int32 index, l_int32 accessflag );
SEL *
selCreateFromString ( const char *text, l_int32 h, l_int32 w, const char *name );
SEL *
selCreateBrick ( l_int32 h, l_int32 w, l_int32 cy, l_int32 cx, l_int32 type );
char *
selPrintToString(SEL *sel);
PIX *
pixDilate ( PIX *pixd, PIX *pixs, SEL *sel );
PIX *
pixErode ( PIX *pixd, PIX *pixs, SEL *sel );
PIX *
pixHMT ( PIX *pixd, PIX *pixs, SEL *sel );
PIX *
pixSubtract ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
void
boxDestroy(BOX **pbox);
void
boxaDestroy ( BOXA **pboxa );
void
pixaDestroy(PIXA **ppixa);
l_ok
pixRenderBoxa ( PIX *pix, BOXA *boxa, l_int32 width, l_int32 op );
void
l_CIDataDestroy(L_COMP_DATA **pcid);
void
lept_free(void *ptr);
""")
sarrayDestroy(SARRAY **psa);
void
lept_free(void *ptr);
void selDestroy ( SEL **psel );
l_int32
setMsgSeverity(l_int32 newsev);
"""
)
ffibuilder.set_source("ocrmypdf.lib._leptonica", None)
if __name__ == '__main__':
ffibuilder.compile(verbose=True)
+143 -113
View File
@@ -15,27 +15,29 @@
# 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
import concurrent.futures
from collections import defaultdict, namedtuple
import logging
import sys
from collections import defaultdict
from os import fspath
from pathlib import Path
from PIL import Image
import pikepdf
from pikepdf import Name, Dictionary, Array
from ._jobcontext import JobContext
from . import leptonica
from .helpers import re_symlink, fspath
from .exec import pngquant, jbig2enc
from ._jobcontext import JobContext
from .exec import jbig2enc, pngquant
from .helpers import re_symlink
DEFAULT_JPEG_QUALITY = 75
DEFAULT_PNG_QUALITY = 70
def img_name(root, xref, ext):
return fspath(root / '{:08d}{}'.format(xref, ext))
return fspath(root / f'{xref:08d}{ext}')
def png_name(root, xref):
@@ -51,7 +53,7 @@ def tif_name(root, xref):
def extract_image_filter(pike, root, log, image, xref):
if image.Subtype != '/Image':
if image.Subtype != Name.Image:
return None
if image.Length < 100:
log.debug("Skipping small image, xref %s", xref)
@@ -67,7 +69,7 @@ def extract_image_filter(pike, root, log, image, xref):
if pim.bits_per_component > 8:
return None # Don't mess with wide gamut images
if filtdp[0] == '/JPXDecode':
if filtdp[0] == Name.JPXDecode:
return None # Don't do JPEG2000
return pim, filtdp
@@ -79,11 +81,13 @@ def extract_image_jbig2(*, pike, root, log, image, xref, options):
return None
pim, filtdp = result
if pim.bits_per_component == 1 \
and filtdp != '/JBIG2Decode' \
and jbig2enc.available():
if (
pim.bits_per_component == 1
and filtdp != Name.JBIG2Decode
and jbig2enc.available()
):
try:
imgname = Path(root / '{:08d}'.format(xref))
imgname = Path(root / f'{xref:08d}')
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
@@ -99,8 +103,7 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
return None
pim, filtdp = result
if filtdp[0] == '/DCTDecode' \
and options.optimize >= 2:
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,
# and possibly recompressible, or not. The number itself doesn't mean
@@ -118,16 +121,18 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
# with Image.open(stream) as im:
# im.save(jpg_name(root, xref), icc_profile=iccbytes)
try:
imgname = Path(root / '{:08d}'.format(xref))
imgname = Path(root / f'{xref:08d}')
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except pikepdf.UnsupportedImageTypeError:
return None
return xref, ext
elif pim.indexed \
and pim.colorspace in pim.SIMPLE_COLORSPACES \
and options.optimize >= 3:
elif (
pim.indexed
and pim.colorspace in pim.SIMPLE_COLORSPACES
and options.optimize >= 3
):
# Try to improve on indexed images - these are far from low hanging
# fruit in most cases
pim.as_pil_image().save(png_name(root, xref))
@@ -137,20 +142,29 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
# generating a PNG from compressed data
pim.as_pil_image().save(png_name(root, xref))
return xref, '.png'
else:
return None
return True
return None
def extract_images(pike, root, log, options, extract_fn):
"""Extract image using extract_fn
extract_fn decides where the image is interesting in this case
Enumerate images on each page, lookup their xref/ID number in the PDF.
Exclude images that are soft masks (i.e. alpha transparency related).
Record the page number on which an image is first used, since images may be
used on multiple pages (or multiple times on the same page).
Current we do not check Form XObjects or other objects that may contain
images, and we don't evaluate alternate images or thumbnails.
extract_fn must decide if wants to extract the image in this context. If
it does a tuple should be returned: (xref, ext) where .ext is the file
extension. extract_fn must also extract the file it finds interesting.
"""
include_xrefs = set()
exclude_xrefs = set()
pageno_for_xref = {}
errors = 0
for pageno, page in enumerate(pike.pages):
try:
@@ -166,23 +180,23 @@ def extract_images(pike, root, log, options, extract_fn):
smask_xref = image.SMask.objgen[0]
exclude_xrefs.add(smask_xref)
include_xrefs.add(xref)
if xref not in pageno_for_xref:
pageno_for_xref[xref] = pageno
working_xrefs = include_xrefs - exclude_xrefs
for xref in working_xrefs:
image = pike.get_object((xref, 0))
try:
result = extract_fn(
pike=pike, root=root, log=log, image=image,
xref=xref, options=options
pike=pike, root=root, log=log, image=image, xref=xref, options=options
)
except Exception as e:
log.debug("Image xref %s", xref)
log.debug(repr(e))
log.debug("Image xref %s, error %s", xref, repr(e))
errors += 1
else:
if result:
_, ext = result
yield pageno, xref, ext
yield pageno_for_xref[xref], xref, ext
def extract_images_generic(pike, root, log, options):
@@ -190,17 +204,13 @@ def extract_images_generic(pike, root, log, options):
jpegs = []
pngs = []
for _, xref, ext in extract_images(
pike, root, log, options, extract_image_generic):
for _, xref, ext in extract_images(pike, root, log, options, extract_image_generic):
log.debug('xref = %s ext = %s', xref, ext)
if ext == '.png':
pngs.append(xref)
elif ext == '.jpg':
jpegs.append(xref)
log.debug(
"Optimizable images: "
"JPEGs: %s PNGs: %s", len(jpegs), len(pngs)
)
log.debug("Optimizable images: JPEGs: %s PNGs: %s", len(jpegs), len(pngs))
return jpegs, pngs
@@ -209,17 +219,16 @@ def extract_images_jbig2(pike, root, log, options):
jbig2_groups = defaultdict(list)
for pageno, xref, ext in extract_images(
pike, root, log, options, extract_image_jbig2):
pike, root, log, options, extract_image_jbig2
):
group = pageno // options.jbig2_page_group_size
jbig2_groups[group].append((xref, ext))
# Elide empty groups
jbig2_groups = {group: xrefs for group, xrefs in jbig2_groups.items()
if len(xrefs) > 0}
log.debug(
"Optimizable images: "
"JBIG2 groups: %s", (len(jbig2_groups),)
)
jbig2_groups = {
group: xrefs for group, xrefs in jbig2_groups.items() if len(xrefs) > 0
}
log.debug("Optimizable images: JBIG2 groups: %s", (len(jbig2_groups),))
return jbig2_groups
@@ -228,18 +237,18 @@ def _produce_jbig2_images(jbig2_groups, root, log, options):
def jbig2_group_futures(executor, root, groups):
for group, xref_exts in groups.items():
prefix = 'group{:08d}'.format(group)
prefix = f'group{group:08d}'
future = executor.submit(
jbig2enc.convert_group,
cwd=fspath(root),
infiles=(img_name(root, xref, ext) for xref, ext in xref_exts),
out_prefix=prefix
out_prefix=prefix,
)
yield future
def jbig2_single_futures(executor, root, groups):
for group, xref_exts in groups.items():
prefix = 'group{:08d}'.format(group)
prefix = f'group{group:08d}'
# Second loop is to ensure multiple images per page are unpacked
for n, xref_ext in enumerate(xref_exts):
xref, ext = xref_ext
@@ -247,7 +256,7 @@ def _produce_jbig2_images(jbig2_groups, root, log, options):
jbig2enc.convert_single,
cwd=fspath(root),
infile=img_name(root, xref, ext),
outfile=root / ('{}.{:04d}'.format(prefix, n))
outfile=root / f'{prefix}.{n:04d}',
)
yield future
@@ -256,8 +265,7 @@ def _produce_jbig2_images(jbig2_groups, root, log, options):
else:
jbig2_futures = jbig2_single_futures
with concurrent.futures.ThreadPoolExecutor(
max_workers=options.jobs) as executor:
with concurrent.futures.ThreadPoolExecutor(max_workers=options.jobs) as executor:
futures = jbig2_futures(executor, root, jbig2_groups)
for future in concurrent.futures.as_completed(futures):
proc = future.result()
@@ -275,20 +283,18 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options):
must be lossy encoding since jbig2enc does not support refinement coding.
When the JBIG2 symbolic coder is not used, each JBIG2 stands on its own
and needs no dictionary. Currently this is must be lossless JBIG2.
and needs no dictionary. Currently this must be lossless JBIG2.
"""
_produce_jbig2_images(jbig2_groups, root, log, options)
for group, xref_exts in jbig2_groups.items():
prefix = 'group{:08d}'.format(group)
prefix = f'group{group:08d}'
jbig2_symfile = root / (prefix + '.sym')
if jbig2_symfile.exists():
jbig2_globals_data = jbig2_symfile.read_bytes()
jbig2_globals = pikepdf.Stream(pike, jbig2_globals_data)
jbig2_globals_dict = pikepdf.Dictionary({
'/JBIG2Globals': jbig2_globals
})
jbig2_globals_dict = Dictionary(JBIG2Globals=jbig2_globals)
elif options.jbig2_page_group_size == 1:
jbig2_globals_dict = None
else:
@@ -296,12 +302,11 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options):
for n, xref_ext in enumerate(xref_exts):
xref, _ = xref_ext
jbig2_im_file = root / (prefix + '.{:04d}'.format(n))
jbig2_im_file = root / (prefix + f'.{n:04d}')
jbig2_im_data = jbig2_im_file.read_bytes()
im_obj = pike.get_object(xref, 0)
im_obj.write(
jbig2_im_data, pikepdf.Name('/JBIG2Decode'),
jbig2_globals_dict
jbig2_im_data, filter=Name.JBIG2Decode, decode_parms=jbig2_globals_dict
)
@@ -315,91 +320,114 @@ def transcode_jpegs(pike, jpegs, root, log, options):
# 'close'. Seems to be mostly harmless
# https://github.com/python-pillow/Pillow/issues/1144
with Image.open(fspath(in_jpg)) as im:
im.save(fspath(opt_jpg),
optimize=True,
quality=options.jpeg_quality)
# pylint: disable=no-member
im.save(fspath(opt_jpg), optimize=True, quality=options.jpeg_quality)
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
log.debug("xref %s, jpeg, made larger - skip", xref)
continue
compdata = leptonica.CompressedData.open(opt_jpg)
im_obj = pike.get_object(xref, 0)
im_obj.write(
compdata.read(), filter=pikepdf.Name('/DCTDecode')
)
im_obj.write(compdata.read(), filter=Name.DCTDecode)
def transcode_pngs(pike, pngs, root, log, options):
def transcode_pngs(pike, images, image_name_fn, root, log, options):
if options.optimize >= 2:
png_quality = (
max(10, options.png_quality - 10),
min(100, options.png_quality + 10)
min(100, options.png_quality + 10),
)
with concurrent.futures.ThreadPoolExecutor(
max_workers=options.jobs) as executor:
for xref in pngs:
max_workers=options.jobs
) as executor:
for xref in images:
log.debug(image_name_fn(root, xref))
executor.submit(
pngquant.quantize,
png_name(root, xref), png_name(root, xref),
png_quality[0], png_quality[1])
image_name_fn(root, xref),
png_name(root, xref),
png_quality[0],
png_quality[1],
)
for xref in pngs:
for xref in images:
im_obj = pike.get_object(xref, 0)
# Open, transcode (!), package for PDF
try:
pix = leptonica.Pix.open(png_name(root, xref))
if pix.depth == 1:
pix = pix.invert() # PDF assumes 1 is black for monochrome
compdata = pix.generate_pdf_ci_data(
leptonica.lept.L_FLATE_ENCODE, 0
)
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
log.error(e)
continue
# This is what we should be doing: open the compressed data without
# transcoding. However this shifts each pixel row by one for some
# reason.
#compdata = leptonica.CompressedData.open(png_name(root, xref))
# If re-coded image is larger don't use it - we test here because
# pngquant knows the size of the temporary output file but not the actual
# object in the PDF
if len(compdata) > int(im_obj.stream_dict.Length):
continue # If we produced a larger image, don't use
log.debug(
f"pngquant: pngquant did not improve over original image "
f"{len(compdata)} > {int(im_obj.stream_dict.Length)}"
)
continue
predictor = None
if compdata.predictor > 0:
predictor = pikepdf.Dictionary({'/Predictor': compdata.predictor})
# 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
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 = [pikepdf.Name('/Indexed'), pikepdf.Name('/DeviceRGB'),
compdata.ncolors - 1, palette_stream]
palette = [
Name.Indexed,
Name.DeviceRGB,
compdata.ncolors - 1,
palette_stream,
]
cs = palette
else:
if compdata.spp == 1:
cs = pikepdf.Name('/DeviceGray')
# 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 = pikepdf.Name('/DeviceRGB')
cs = Name.DeviceRGB
elif compdata.spp == 4:
cs = pikepdf.Name('/DeviceCMYK')
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=pikepdf.Name('/FlateDecode'), decode_parms=predictor
)
im_obj.write(compdata.read(), filter=Name.FlateDecode, decode_parms=dparms)
def optimize(
input_file,
output_file,
log,
context):
def optimize(input_file, output_file, log, context):
options = context.get_options()
if options.optimize == 0:
@@ -407,37 +435,40 @@ def optimize(
return
if options.jpeg_quality == 0:
options.jpeg_quality = \
DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
options.jpeg_quality = DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
if options.png_quality == 0:
options.png_quality = \
DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
options.png_quality = DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
if options.jbig2_page_group_size == 0:
options.jbig2_page_group_size = \
10 if options.jbig2_lossy else 1
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
pike = pikepdf.Pdf.open(input_file)
root = Path(output_file).parent / 'images'
root.mkdir(exist_ok=True) # pylint: disable=no-member
root.mkdir(exist_ok=True)
jpegs, pngs = extract_images_generic(pike, root, log, options)
transcode_jpegs(pike, jpegs, root, log, options)
transcode_pngs(pike, pngs, root, log, options)
# if options.optimize >= 2:
# Try pngifying the jpegs
# transcode_pngs(pike, jpegs, jpg_name, root, log, options)
transcode_pngs(pike, pngs, png_name, root, log, options)
jbig2_groups = extract_images_jbig2(pike, root, log, options)
convert_to_jbig2(pike, jbig2_groups, root, log, options)
target_file = Path(output_file).with_suffix('.opt.pdf')
pike.save(target_file, preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate)
pike.remove_unreferenced_resources()
pike.save(
target_file,
preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
)
input_size = Path(input_file).stat().st_size
output_size = Path(target_file).stat().st_size
ratio = input_size / output_size
savings = 1 - output_size / input_size
log.info("Optimize ratio: {:.2f} savings: {:.1f}%".format(
ratio, 100 * savings))
log.info(f"Optimize ratio: {ratio:.2f} savings: {(100 * savings):.1f}%")
if savings < 0:
log.info("Optimize did not improve the file - discarded")
@@ -453,8 +484,7 @@ def main(infile, outfile, level, jobs=1):
class OptimizeOptions:
"""Emulate ocrmypdf's options"""
def __init__(
self, jobs, optimize, jpeg_quality, png_quality, jb2lossy):
def __init__(self, jobs, optimize, jpeg_quality, png_quality, jb2lossy):
self.jobs = jobs
self.optimize = optimize
self.jpeg_quality = jpeg_quality
@@ -471,7 +501,7 @@ def main(infile, outfile, level, jobs=1):
optimize=int(level),
jpeg_quality=0, # Use default
png_quality=0,
jb2lossy=False
jb2lossy=False,
)
ctx.set_options(options)
+37 -223
View File
@@ -31,23 +31,18 @@ Ghostscript's handling of pdfmark.
"""
from binascii import hexlify
from datetime import datetime
from pathlib import Path
from shutil import copyfile
from string import Template
import pkg_resources
import os
from binascii import hexlify
from pathlib import Path
from string import Template
from libxmp.utils import file_to_dict
from libxmp import consts
import pkg_resources
from ocrmypdf.helpers import fspath
import pikepdf
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
'ocrmypdf', ICC_PROFILE_RELPATH)
SRGB_ICC_PROFILE = pkg_resources.resource_filename('ocrmypdf', ICC_PROFILE_RELPATH)
# This is a template written in PostScript which is needed to create PDF/A
@@ -58,9 +53,6 @@ pdfa_def_template = u"""%!
/ICCProfile $icc_profile
def
[$docinfo
/DOCINFO pdfmark
% Define an ICC profile :
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
@@ -91,190 +83,29 @@ def
"""
def encode_text_string(s: str) -> str:
"""
Encode text string to hex string for use in a PDF
def generate_pdfa_ps(target_filename, icc='sRGB'):
"""Create a Postscript pdfmark file for Ghostscript PDF/A conversion
From PDF 32000-1:2008 a string object may be included in hexademical form
if it is enclosed in angle brackets. For general Unicode the string should
be UTF-16 (big endian) with byte order marks. Many strings including all
ASCII strings could be encoded as PdfDocEncoding literals provided
that certain Postscript sequences are escaped. But it's far simpler to
encode everything as UTF-16.
"""
A pdfmark file is a small Postscript program that provides some information
Ghostscript needs to perform PDF/A conversion. The only information we put
in specifies that we want the file to be a PDF/A, and we want to Ghostscript
to convert objects to the sRGB colorspace if it runs into any object that
it decides must be converted.
# Sometimes lazy C programmers leave their NULs at the end of strings they
# insert into PDFs
# tests/resources/aspect.pdf is one example (created by ImageMagick)
s = s.replace('\x00', '')
See the Adobe pdfmark Reference for details:
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
if s == '':
return ''
utf16_bytes = s.encode('utf-16be')
ascii_hex_bytes = hexlify(b'\xfe\xff' + utf16_bytes)
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
return ascii_hex_str
def _encode_ascii(s: str) -> str:
"""
Aggressively strip non-ASCII and PDF escape sequences
Ghostscript 9.24+ lost support for UTF-16BE in pdfmark files for reasons
given in GhostPDL commit e997c683. Our temporary workaround is use ASCII
and drop all non-ASCII characters. A slightly improved alternative would
be to implement PdfDocEncoding in pikepdf and encode to that, or handle
metadata there.
"""
trans = str.maketrans({
'(': '',
')': '',
'\\': '',
'\0': ''
})
return s.translate(trans).encode('ascii', errors='replace').decode()
def encode_pdf_date(d: datetime) -> str:
"""
Encode Python datetime object as PDF date string
From Adobe pdfmark manual:
(D:YYYYMMDDHHmmSSOHH'mm')
D: is an optional prefix. YYYY is the year. All fields after the year are
optional. MM is the month (01-12), DD is the day (01-31), HH is the
hour (00-23), mm are the minutes (00-59), and SS are the seconds
(00-59). The remainder of the string defines the relation of local
time to GMT. O is either + for a positive difference (local time is
later than GMT) or - (minus) for a negative difference. HH' is the
absolute value of the offset from GMT in hours, and mm' is the
absolute value of the offset in minutes. If no GMT information is
specified, the relation between the specified time and GMT is
considered unknown. Regardless of whether or not GMT
information is specified, the remainder of the string should specify
the local time.
"""
pdfmark_date_fmt = r'%Y%m%d%H%M%S'
s = d.strftime(pdfmark_date_fmt)
tz = d.strftime('%z')
if tz == 'Z' or tz == '':
# Ghostscript <= 9.23 handles missing timezones incorrectly, so if
# timezone is missing, move it into GMT.
# https://bugs.ghostscript.com/show_bug.cgi?id=699182
s += "+00'00'"
else:
sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
return s
def decode_pdf_date(s: str) -> datetime:
"""
Decode a pdfmark date to a Python datetime object
A pdfmark date is a string in a paritcular format. See the pdfmark
Reference for the specification.
"""
if s.startswith('D:'):
s = s[2:]
# Literal Z00'00', is incorrect but found in the wild,
# probably made by OS X Quartz -- standardize
if s.endswith("Z00'00'"):
s = s.replace("Z00'00'", '+0000')
elif s.endswith('Z'):
s = s.replace('Z', '+0000')
s = s.replace("'", "") # Remove apos from PDF time strings
return datetime.strptime(s, r'%Y%m%d%H%M%S%z')
def _get_pdfmark_dates(pdfmark):
"""
Encode dates in the expected format for pdfmark Postscript
The best way to deal with amissing date entry is set it to null, because if
the key is omitted Ghostscript will set it to now - we do not want to erase
the fact that the value was unknown. Setting to an empty string breaks
Ghostscript 9.22 as reported here:
https://bugs.ghostscript.com/show_bug.cgi?id=699182
"""
for key in ('/CreationDate', '/ModDate'):
if key not in pdfmark:
continue
if pdfmark[key].strip() == '':
yield ' {} null'.format(key)
continue
date_str = pdfmark[key]
if date_str.startswith('D:'):
date_str = date_str[2:]
try:
yield ' {} (D:{})'.format(
key,
encode_pdf_date(decode_pdf_date(date_str)))
except ValueError:
yield ' {} null'.format(key)
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
"""
Create a Postscript pdfmark file for Ghostscript.
pdfmark contains the various objects as strings; these must be encoded in
ASCII, and dates have a special format.
:param icc_profile: filename of the ICC profile to include in pdfmark
:param icc_identifier: ICC identifier such as 'sRGB'
:param pdfmark: a dictionary containing keys to include the pdfmark
:param ascii_docinfo: if True, the docinfo block must be encoded in pure
ASCII and may not contain UTF-16BE-BOM-hex encoded strings, as
required for Ghostscript 9.24+
:param target_filename: filename to save
:param icc: ICC identifier such as 'sRGB'
:returns: a string containing the entire pdfmark
"""
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
# ERROR: VMerror (-25) on closing pdfwrite device.
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
# Work around this by only adding keys that have a nontrivial value
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
def docinfo_gen():
if not ascii_docinfo:
docinfo_line_template = ' {key} <{value}>'
encode = encode_text_string
else:
docinfo_line_template = ' {key} ({value})'
encode = _encode_ascii
yield from _get_pdfmark_dates(pdfmark)
for key in docinfo_keys:
if key in pdfmark and pdfmark[key].strip() != '':
line = docinfo_line_template.format(
key=key, value=encode(pdfmark[key]))
yield line
docinfo = '\n'.join(docinfo_gen())
t = Template(pdfa_def_template)
result = t.substitute(icc_profile=icc_profile,
icc_identifier=icc_identifier,
docinfo=docinfo)
return result
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
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
# 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
@@ -285,7 +116,8 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
hex_icc_profile = hexlify(bytes_icc_profile)
icc_profile = '<' + hex_icc_profile.decode('ascii') + '>'
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
t = Template(pdfa_def_template)
ps = t.substitute(icc_profile=icc_profile, icc_identifier=icc)
# We should have encoded everything to pure ASCII by this point, and
# to be safe, only allow ASCII in PostScript
@@ -295,41 +127,23 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
def file_claims_pdfa(filename):
"""Determines if the file claims to be PDF/A compliant
Checking if a file is a truly compliant PDF/A is a massive undertaking
that no open source tool does properly. Some commercial tools are
generally reliable (Acrobat).
This checks if the XMP metadata contains a PDF/A marker.
This only checks if the XMP metadata contains a PDF/A marker. It does not
do full PDF/A validation.
"""
xmp = file_to_dict(filename)
if not xmp:
return {'pass': False, 'output': 'pdf',
'conformance': 'No XMP metadata'}
if not consts.XMP_NS_PDFA_ID in xmp:
return {'pass': False, 'output': 'pdf',
'conformance': 'No PDF/A metadata in XMP'}
pdfa_node = xmp[consts.XMP_NS_PDFA_ID]
def read_node(node, key):
return next(
(v for k, v, meta in node if k == key), ''
)
part = read_node(pdfa_node, 'pdfaid:part')
conformance = read_node(pdfa_node, 'pdfaid:conformance')
part_conformance = part + conformance
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
conformance = 'PDF/A-{}'.format(
part_conformance)
pdfa_dict = {}
if part_conformance in valid_part_conforms:
pdfa_dict['pass'] = True
pdfa_dict['output'] = 'pdfa'
pdfa_dict['conformance'] = conformance
with pikepdf.open(filename) as pdf:
pdfmeta = pdf.open_metadata()
if not pdfmeta.pdfa_status:
return {
'pass': False,
'output': 'pdf',
'conformance': 'No PDF/A metadata in XMP',
}
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
pdfa_dict = {}
if pdfmeta.pdfa_status in valid_part_conforms:
pdfa_dict['pass'] = True
pdfa_dict['output'] = 'pdfa'
pdfa_dict['conformance'] = conformance
return pdfa_dict
@@ -20,34 +20,25 @@ from collections import namedtuple
from decimal import Decimal
from enum import Enum
from math import hypot, isclose
from os import fspath
from pathlib import Path
from unittest.mock import Mock
from warnings import warn
import re
import xml.etree.ElementTree as ET
from .exec import ghostscript
from .helpers import fspath
from pikepdf import PdfMatrix
import pikepdf
Colorspace = Enum('Colorspace',
'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
from . import ghosttext
Encoding = Enum('Encoding',
'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + \
'runlength')
from ..exceptions import EncryptedPdfError, MissingDependencyError
# Forgive me for I have sinned
# I am using regular expressions to parse XML. However the XML in this case,
# generated by Ghostscript, is self-consistent enough to be parseable.
regex_remove_char_tags = re.compile(br"""
<char\b
(?: [^>] # anything single character but >
| \">\" # special case: trap ">"
)*
/> # terminate with '/>'
""", re.VERBOSE)
Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
Encoding = Enum(
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + 'runlength'
)
FRIENDLY_COLORSPACE = {
'/DeviceGray': Colorspace.gray,
@@ -78,7 +69,7 @@ FRIENDLY_ENCODING = {
'/A85': Encoding.ascii85,
'/LZW': Encoding.lzw,
'/Fl': Encoding.flate,
'/RL': Encoding.runlength
'/RL': Encoding.runlength,
}
FRIENDLY_COMP = {
@@ -86,41 +77,48 @@ FRIENDLY_COMP = {
Colorspace.rgb: 3,
Colorspace.cmyk: 4,
Colorspace.lab: 3,
Colorspace.index: 1
Colorspace.index: 1,
}
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
def _is_unit_square(shorthand):
values = map(float, shorthand)
pairwise = zip(values, UNIT_SQUARE)
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
XobjectSettings = namedtuple('XobjectSettings',
['name', 'shorthand', 'stack_depth'])
InlineSettings = namedtuple('InlineSettings',
['iimage', 'shorthand', 'stack_depth'])
XobjectSettings = namedtuple('XobjectSettings', ['name', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple('ContentsInfo',
['xobject_settings', 'inline_images', 'found_text'])
InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth'])
ContentsInfo = namedtuple(
'ContentsInfo', ['xobject_settings', 'inline_images', 'found_vector']
)
TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt'])
class VectorInfo:
def __init__(self):
pass
def _normalize_stack(operations):
"""Convert runs of qQ's in the stack into single operations"""
for operands, command in operations:
command = str(command)
if re.match(r'Q*q+$', command): # Zero or more Q, one or more q
for char in command: # Split into individual
yield ([], char) # Yield individual
def _normalize_stack(graphobjs):
"""Convert runs of qQ's in the stack into single graphobjs"""
for operands, operator in graphobjs:
operator = str(operator)
if re.match(r'Q*q+$', operator): # Zero or more Q, one or more q
for char in operator: # Split into individual
yield ([], char) # Yield individual
else:
yield (operands, command)
yield (operands, operator)
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
"""Interpret the PDF content stream
"""Interpret the PDF content stream.
The stack represents the state of the PDF graphics stack. We are only
interested in the current transformation matrix (CTM) so we only track
@@ -138,53 +136,66 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
PDF units suit our needs so we initialize ctm to the identity matrix.
According to the PDF specification, the maximum stack depth is 32. Other
viewers tolerate some amount beyond this. We issue a warning if the
stack depth exceeds the spec limit and set a hard limit beyond this to
bound our memory requirements. If the stack underflows behavior is
undefined in the spec, but we just pretend nothing happened and leave the
CTM unchanged.
"""
stack = []
ctm = PdfMatrix(initial_shorthand)
xobject_settings = []
inline_images = []
found_text = False
text_operators = set(['Tj', 'TJ', '"', "'"])
operator_whitelist = """q Q Do cm TJ Tj " ' BI ID EI"""
found_vector = False
vector_ops = set('S s f F f* B B* b b*'.split())
image_ops = set('BI ID EI q Q Do cm'.split())
operator_whitelist = ' '.join(vector_ops | image_ops)
for n, op in enumerate(_normalize_stack(
pikepdf.parse_content_stream(contentstream, operator_whitelist))):
operands, command = op
if command == 'q':
for n, graphobj in enumerate(
_normalize_stack(
pikepdf.parse_content_stream(contentstream, operator_whitelist)
)
):
operands, operator = graphobj
if operator == 'q':
stack.append(ctm)
if len(stack) > 32:
raise RuntimeError(
"PDF graphics stack overflow, command %i" % n)
elif command == 'Q':
if len(stack) > 32: # See docstring
if len(stack) > 128:
raise RuntimeError(
"PDF graphics stack overflowed hard limit, operator %i" % n
)
warn("PDF graphics stack overflowed spec limit")
elif operator == 'Q':
try:
ctm = stack.pop()
except IndexError:
raise RuntimeError(
"PDF graphics stack underflow, command %i" % n)
elif command == 'cm':
# Keeping the ctm the same seems to be the only sensible thing
# to do. Just pretend nothing happened, keep calm and carry on.
warn("PDF graphics stack underflowed - PDF may be malformed")
elif operator == 'cm':
ctm = PdfMatrix(operands) @ ctm
elif command == 'Do':
elif operator == 'Do':
image_name = operands[0]
settings = XobjectSettings(
name=image_name, shorthand=ctm.shorthand,
stack_depth=len(stack))
name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack)
)
xobject_settings.append(settings)
elif command == 'INLINE IMAGE':
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
iimage = operands[0]
inline = InlineSettings(
iimage=iimage, shorthand=ctm.shorthand,
stack_depth=len(stack))
iimage=iimage, shorthand=ctm.shorthand, stack_depth=len(stack)
)
inline_images.append(inline)
elif command in text_operators:
found_text = True
elif operator in vector_ops:
found_vector = True
return ContentsInfo(
xobject_settings=xobject_settings,
inline_images=inline_images,
found_text=found_text)
found_vector=found_vector,
)
def _get_dpi(ctm_shorthand, image_size):
@@ -254,8 +265,7 @@ def _get_dpi(ctm_shorthand, image_size):
class ImageInfo:
DPI_PREC = Decimal('1.000')
def __init__(self, *, name='', pdfimage=None, inline=None,
shorthand=None):
def __init__(self, *, name='', pdfimage=None, inline=None, shorthand=None):
self._name = str(name)
self._shorthand = shorthand
@@ -340,19 +350,24 @@ class ImageInfo:
return _get_dpi(self._shorthand, (self._width, self._height))[1]
def __repr__(self):
class_locals = {attr: getattr(self, attr, None) for attr in dir(self)
if not attr.startswith('_')}
class_locals = {
attr: getattr(self, attr, None)
for attr in dir(self)
if not attr.startswith('_')
}
return (
"<ImageInfo '{name}' {type_} {width}x{height} {color} "
"{comp} {bpc} {enc} {xres}x{yres}>").format(**class_locals)
"{comp} {bpc} {enc} {xres}x{yres}>"
).format(**class_locals)
def _find_inline_images(contentsinfo):
"Find inline images in the contentstream"
for n, inline in enumerate(contentsinfo.inline_images):
yield ImageInfo(name='inline-%02d' % n, shorthand=inline.shorthand,
inline=inline)
yield ImageInfo(
name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline
)
def _image_xobjects(container):
@@ -376,6 +391,8 @@ def _image_xobjects(container):
xobjs = resources['/XObject'].as_dict()
for xobj in xobjs:
candidate = xobjs[xobj]
if not '/Subtype' in candidate:
continue
if candidate['/Subtype'] == '/Image':
pdfimage = candidate
yield (pdfimage, xobj)
@@ -388,7 +405,6 @@ def _find_regular_images(container, contentsinfo):
that contains images.
Generates images with their DPI at time of drawing.
"""
for pdfimage, xobj in _image_xobjects(container):
@@ -407,8 +423,7 @@ def _find_regular_images(container, contentsinfo):
# these from our DPI calculation for the page.
continue
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=
draw.shorthand)
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=draw.shorthand)
def _find_form_xobject_images(pdf, container, contentsinfo):
@@ -438,11 +453,12 @@ def _find_form_xobject_images(pdf, container, contentsinfo):
# but in practice both Form XObjects and multiple drawing of the
# same object are both very rare.
ctm_shorthand = settings.shorthand
yield from _find_images(
pdf=pdf, container=form_xobject, shorthand=ctm_shorthand)
yield from _process_content_streams(
pdf=pdf, container=form_xobject, shorthand=ctm_shorthand
)
def _find_images(*, pdf, container, shorthand=None):
def _process_content_streams(*, pdf, container, shorthand=None):
"""Find all individual instances of images drawn in the container
Usually the container is a page, but it may also be a Form XObject.
@@ -451,7 +467,7 @@ def _find_images(*, pdf, container, shorthand=None):
in an XObject.
Form XObjects may include inline images, XObject images,
and recursively, other Form XObjects; and also vector drawing commands.
and recursively, other Form XObjects; and also vector graphic objects.
Every instance of an image being drawn somewhere is flattened and
treated as a unique image, since if the same image is drawn multiple times
@@ -463,8 +479,7 @@ def _find_images(*, pdf, container, shorthand=None):
if container.get('/Type') == '/Page' and '/Contents' in container:
initial_shorthand = shorthand or UNIT_SQUARE
elif container.get('/Type') == '/XObject' and \
container['/Subtype'] == '/Form':
elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form':
# Set the CTM to the state it was when the "Do" operator was
# encountered that is drawing this instance of the Form XObject
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
@@ -483,48 +498,13 @@ def _find_images(*, pdf, container, shorthand=None):
contentsinfo = _interpret_contents(container, initial_shorthand)
if contentsinfo.found_vector:
yield VectorInfo()
yield from _find_inline_images(contentsinfo)
yield from _find_regular_images(container, contentsinfo)
yield from _find_form_xobject_images(pdf, container, contentsinfo)
def _page_get_textblocks(infile, pageno, xmltext):
"""Smarter text detection"""
root = xmltext
if not hasattr(xmltext, 'findall'):
return []
def blocks():
for span in root.findall('.//span'):
bbox_str = span.attrib['bbox']
font_size = span.attrib['size']
pts = [int(pt) for pt in bbox_str.split()]
pts[1] = pts[1] - int(float(font_size) + 0.5)
bbox = tuple(pts)
yield bbox
def joined_blocks():
prev = None
for bbox in blocks():
if prev is None:
prev = bbox
if bbox[1] == prev[1] and bbox[3] == prev[3]:
gap = prev[2] - bbox[0]
height = bbox[3] - bbox[1]
if gap < height:
# Join boxes
prev = (prev[0], prev[1], bbox[2], bbox[3])
continue
# yield previously joined bboxes and start anew
yield prev
prev = bbox
if prev is not None:
yield prev
return [block for block in joined_blocks()]
def _page_has_text(text_blocks, page_width, page_height):
"""Smarter text detection that ignores text in margins"""
@@ -532,18 +512,19 @@ def _page_has_text(text_blocks, page_width, page_height):
margin_ratio = 0.125
interior_bbox = (
margin_ratio * pw, margin_ratio * ph,
(1 - margin_ratio) * pw, (1 - margin_ratio) * ph
margin_ratio * pw, # left
(1 - margin_ratio) * ph, # top
(1 - margin_ratio) * pw, # right
margin_ratio * ph, # bottom (first quadrant: bottom < top)
)
def rects_intersect(a, b):
"""
Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3)
https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
Negative signs to account for our coordinates being in the fourth quadrant
and the formula assuming the first
Formula assumes all boxes are in first quadrant
"""
return a[0] < b[2] and a[2] > b[0] and -a[1] > -b[3] and -a[3] < -b[1]
return a[0] < b[2] and a[2] > b[0] and a[1] > b[3] and a[3] < b[1]
has_text = False
for bbox in text_blocks:
@@ -553,22 +534,50 @@ def _page_has_text(text_blocks, page_width, page_height):
return has_text
def simplify_textboxes(miner, textbox_getter):
"""Extract only limited content from text boxes
We do this to save memory and ensure that our objects are pickleable.
"""
for box in textbox_getter(miner):
first_line = box._objs[0]
first_char = first_line._objs[0]
visible = first_char.rendermode != 3
corrupt = first_char.get_text() == '\ufffd'
yield TextboxInfo(box.bbox, visible, corrupt)
def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
pageinfo = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
page = pdf.pages[pageno]
pageinfo['textinfo'] = _page_get_textblocks(
fspath(infile), pageno, xmltext=xmltext)
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
pageinfo['has_text'] = _page_has_text(
pageinfo['textinfo'], width_pt, height_pt)
if xmltext is not None:
bboxes = ghosttext.page_get_textblocks(
fspath(infile), pageno, xmltext=xmltext, height=height_pt
)
pageinfo['bboxes'] = bboxes
else:
# pdfminer required for this section
try:
from .layout import get_page_analysis, get_text_boxes
except ImportError:
raise MissingDependencyError(
"pdfminer is required for this feature. Your distribution "
"may not have installed it."
)
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
miner = get_page_analysis(infile, pageno, pscript5_mode)
pageinfo['textboxes'] = list(simplify_textboxes(miner, get_text_boxes))
bboxes = (box.bbox for box in pageinfo['textboxes'])
pageinfo['has_text'] = _page_has_text(bboxes, width_pt, height_pt)
userunit = page.get('/UserUnit', Decimal(1.0))
if not isinstance(userunit, Decimal):
@@ -583,62 +592,56 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
pageinfo['rotate'] = 0
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
pageinfo['images'] = [im for im in
_find_images(pdf=pdf, container=page,
shorthand=userunit_shorthand)]
contentsinfo = [
ci
for ci in _process_content_streams(
pdf=pdf, container=page, shorthand=userunit_shorthand
)
]
pageinfo['has_vector'] = False
if any(isinstance(ci, VectorInfo) for ci in contentsinfo):
pageinfo['has_vector'] = True
pageinfo['images'] = [im for im in contentsinfo if isinstance(im, ImageInfo)]
if pageinfo['images']:
xres = Decimal(max(image.xres for image in pageinfo['images']))
yres = Decimal(max(image.yres for image in pageinfo['images']))
pageinfo['xres'], pageinfo['yres'] = xres, yres
pageinfo['width_pixels'] = \
int(round(xres * pageinfo['width_inches']))
pageinfo['height_pixels'] = \
int(round(yres * pageinfo['height_inches']))
pageinfo['width_pixels'] = int(round(xres * pageinfo['width_inches']))
pageinfo['height_pixels'] = int(round(yres * pageinfo['height_inches']))
return pageinfo
def _pdf_get_all_pageinfo(infile, log=None):
def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None):
if not log:
log = Mock()
pdf = pikepdf.open(infile)
existing_text = ghostscript.extract_text(infile, pageno=None)
existing_text = regex_remove_char_tags.sub(b' ', existing_text)
try:
root = ET.fromstringlist([
b'<document>\n', existing_text, b'</document>\n'
])
page_xml = root.findall('page')
except ET.ParseError as e:
log.error(
"An error occurred while attempting to retrieve existing text in "
"the input file. Will attempt to continue assuming that there is "
"no existing text in the file. The error was:")
log.error(e)
page_xml = [None] * len(pdf.pages)
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.")
if page_count_difference > 0:
page_xml.extend([None] * page_count_difference)
pdf = pikepdf.open(infile) # Do not close in this function
if pdf.is_encrypted:
pdf.close()
raise EncryptedPdfError() # Triggered by encryption with empty passwd
if detailed_analysis:
pages_xml = None
else:
pages_xml = ghosttext.extract_text_xml(infile, pdf, pageno=None, log=log)
pages = []
for n in range(len(pdf.pages)):
page = PageInfo(pdf, n, infile, page_xml[n])
page_xml = pages_xml[n] if pages_xml else None
page = PageInfo(pdf, n, infile, page_xml, detailed_analysis)
pages.append(page)
return pages, pdf
class PageInfo:
def __init__(self, pdf, pageno, infile, xmltext):
def __init__(self, pdf, pageno, infile, xmltext, detailed_analysis=False):
self._pageno = pageno
self._infile = infile
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile, xmltext)
self._detailed_analysis = detailed_analysis
@property
def pageno(self):
@@ -648,6 +651,16 @@ class PageInfo:
def has_text(self):
return self._pageinfo['has_text']
@property
def has_corrupt_text(self):
if not self._detailed_analysis:
raise NotImplementedError('Did not do detailed analysis')
return any(tbox.is_corrupt for tbox in self._pageinfo['textboxes'])
@property
def has_vector(self):
return self._pageinfo['has_vector']
@property
def width_inches(self):
return self._pageinfo['width_inches']
@@ -679,8 +692,27 @@ class PageInfo:
def images(self):
return self._pageinfo['images']
def get_textareas(self):
yield from self._pageinfo['textinfo']
def get_textareas(self, visible=None, corrupt=None):
def predicate(obj, want_visible, want_corrupt):
result = True
if want_visible is not None:
if obj.is_visible != want_visible:
result = False
if want_corrupt is not None:
if obj.is_corrupt != want_corrupt:
result = False
return result
if 'textboxes' not in self._pageinfo:
if visible is not None and corrupt is not None:
raise NotImplementedError('Ghostscript textboxes cannot be classified')
return self._pageinfo['bboxes']
return (
obj.bbox
for obj in self._pageinfo['textboxes']
if predicate(obj, visible, corrupt)
)
@property
def xres(self):
@@ -703,22 +735,34 @@ class PageInfo:
def __repr__(self):
return (
'<PageInfo '
'pageno={} {}"x{}" rotation={} res={}x{} has_text={}>').format(
self.pageno, self.width_inches, self.height_inches,
'<PageInfo ' 'pageno={} {}"x{}" rotation={} res={}x{} has_text={}>'
).format(
self.pageno,
self.width_inches,
self.height_inches,
self.rotation,
self.xres, self.yres, self.has_text
self.xres,
self.yres,
self.has_text,
)
class PdfInfo:
"""Get summary information about a PDF
"""Get summary information about a PDF"""
"""
def __init__(self, infile, log=None):
def __init__(self, infile, detailed_page_analysis=False, log=None):
self._infile = infile
self._pages, pdf = _pdf_get_all_pageinfo(infile, log=log)
self._pages, pdf = _pdf_get_all_pageinfo(
infile, detailed_page_analysis, log=log
)
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
self._has_acroform = False
if '/AcroForm' in pdf.root:
if len(pdf.root.AcroForm.get('/Fields', [])) > 0:
self._has_acroform = True
elif '/XFA' in pdf.root.AcroForm:
self._has_acroform = True
pdf.close()
@property
def pages(self):
@@ -733,6 +777,10 @@ class PdfInfo:
def has_userunit(self):
return any(page.userunit != 1.0 for page in self.pages)
@property
def has_acroform(self):
return self._has_acroform
@property
def filename(self):
if not isinstance(self._infile, (str, Path)):
@@ -750,15 +798,18 @@ class PdfInfo:
return len(self._pages)
def __repr__(self):
return "<PdfInfo('...'), page count={}>".format(len(self))
return f"<PdfInfo('...'), page count={len(self)}>"
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile')
args = parser.parse_args()
info = _pdf_get_all_pageinfo(args.infile)
from pprint import pprint
pprint(info)
+98
View File
@@ -0,0 +1,98 @@
# © 2018 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 re
import xml.etree.ElementTree as ET
from ..exec import ghostscript
# Forgive me for I have sinned
# I am using regular expressions to parse XML. However the XML in this case,
# generated by Ghostscript, is self-consistent enough to be parseable.
regex_remove_char_tags = re.compile(
br"""
<char\b
(?: [^>] # anything single character but >
| \">\" # special case: trap ">"
)*
/> # terminate with '/>'
""",
re.VERBOSE,
)
def page_get_textblocks(infile, pageno, xmltext, height):
"""Get text boxes out of Ghostscript txtwrite xml"""
root = xmltext
if not hasattr(xmltext, 'findall'):
return []
def blocks():
for span in root.findall('.//span'):
bbox_str = span.attrib['bbox']
font_size = span.attrib['size']
pts = [int(pt) for pt in bbox_str.split()]
pts[1] = pts[1] - int(float(font_size) + 0.5)
bbox_topdown = tuple(pts)
bb = bbox_topdown
bbox_bottomup = (bb[0], height - bb[3], bb[2], height - bb[1])
yield bbox_bottomup
def joined_blocks():
prev = None
for bbox in blocks():
if prev is None:
prev = bbox
if bbox[1] == prev[1] and bbox[3] == prev[3]:
gap = prev[2] - bbox[0]
height = abs(bbox[3] - bbox[1])
if gap < height:
# Join boxes
prev = (prev[0], prev[1], bbox[2], bbox[3])
continue
# yield previously joined bboxes and start anew
yield prev
prev = bbox
if prev is not None:
yield prev
return [block for block in joined_blocks()]
def extract_text_xml(infile, pdf, pageno=None, log=None):
existing_text = ghostscript.extract_text(infile, pageno=None)
existing_text = regex_remove_char_tags.sub(b' ', existing_text)
try:
root = ET.fromstringlist([b'<document>\n', existing_text, b'</document>\n'])
page_xml = root.findall('page')
except ET.ParseError as e:
log.error(
"An error occurred while attempting to retrieve existing text in "
"the input file. Will attempt to continue assuming that there is "
"no existing text in the file. The error was:"
)
log.error(e)
page_xml = [None] * len(pdf.pages)
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.")
if page_count_difference > 0:
page_xml.extend([None] * page_count_difference)
return page_xml
+299
View File
@@ -0,0 +1,299 @@
# © 2018 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 re
from math import copysign
from pathlib import Path
from unittest.mock import patch
import pdfminer.encodingdb
import pdfminer.pdfdevice
import pdfminer.pdfinterp
from pdfminer.converter import PDFLayoutAnalyzer
from pdfminer.glyphlist import glyphname2unicode
from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox
from pdfminer.pdfdocument import PDFTextExtractionNotAllowed
from pdfminer.pdffont import PDFFont, PDFSimpleFont, PDFUnicodeNotDefined
from pdfminer.pdfpage import PDFPage
from pdfminer.utils import bbox2str, matrix2str
from ..exceptions import EncryptedPdfError
STRIP_NAME = re.compile(r'[0-9]+')
#
# Unconditional pdfminer patches
#
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
raise KeyError(name)
m = STRIP_NAME.search(name)
if not m:
raise KeyError(name)
return chr(int(m.group(0)))
pdfminer.encodingdb.name2unicode = name2unicode
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__
original_PDFSimpleFont_init = PDFSimpleFont.__init__
def PDFSimpleFont__init__(self, descriptor, widths, spec):
# Font encoding is specified either by a name of
# built-in encoding or a dictionary that describes
# the differences.
original_PDFSimpleFont_init(self, descriptor, widths, spec)
# pdfminer is incorrect. If there is no ToUnicode and no Encoding, do not
# assume Unicode conversion is possible. RM 9.10.2
if not self.unicode_map and 'Encoding' not in spec:
self.cid2unicode = {}
return
PDFSimpleFont.__init__ = PDFSimpleFont__init__
#
# pdfminer patches when creator is PScript5.dll
#
def PDFType3Font__PScript5_get_height(self):
h = self.bbox[3] - self.bbox[1]
if h == 0:
h = self.ascent - self.descent
return h * copysign(1.0, self.vscale)
def PDFType3Font__PScript5_get_descent(self):
return self.descent * copysign(1.0, self.vscale)
def PDFType3Font__PScript5_get_ascent(self):
return self.ascent * copysign(1.0, self.vscale)
class LTStateAwareChar(LTChar):
"""A subclass of LTChar that tracks text render mode at time of drawing"""
__slots__ = (
'rendermode',
'_text',
'matrix',
'fontname',
'adv',
'upright',
'size',
'width',
'height',
'bbox',
'x0',
'x1',
'y0',
'y1',
)
def __init__(
self,
matrix,
font,
fontsize,
scaling,
rise,
text,
textwidth,
textdisp,
ncs,
graphicstate,
textstate,
):
super().__init__(
matrix,
font,
fontsize,
scaling,
rise,
text,
textwidth,
textdisp,
ncs,
graphicstate,
)
self.rendermode = textstate.render
def is_compatible(self, obj):
"""Check if characters can be combined into a textline
We consider characters compatible if:
- the Unicode mapping is known, and both have the same render mode
- the Unicode mapping is unknown but both are part of the same font
"""
both_unicode_mapped = isinstance(self._text, str) and isinstance(obj._text, str)
try:
if both_unicode_mapped:
return self.rendermode == obj.rendermode
font0, _ = self._text
font1, _ = obj._text
return font0 == font1 and self.rendermode == obj.rendermode
except (ValueError, AttributeError):
return False
def get_text(self):
if isinstance(self._text, tuple):
return ''
return self._text
def __repr__(self):
return '<%s %s matrix=%s rendermode=%r font=%r adv=%s text=%r>' % (
self.__class__.__name__,
bbox2str(self.bbox),
matrix2str(self.matrix),
self.rendermode,
self.fontname,
self.adv,
self.get_text(),
)
class TextPositionTracker(PDFLayoutAnalyzer):
"""A page layout analyzer that pays attention to text visibility"""
def __init__(self, rsrcmgr, pageno=1, laparams=None):
super().__init__(rsrcmgr, pageno, laparams)
self.textstate = None
self.result = None
def begin_page(self, page, ctm):
super().begin_page(page, ctm)
self.cur_item = LTPage(self.pageno, page.mediabox)
def end_page(self, page):
assert not self._stack, str(len(self._stack))
assert isinstance(self.cur_item, LTPage), str(type(self.cur_item))
if self.laparams is not None:
self.cur_item.analyze(self.laparams)
self.pageno += 1
self.receive_layout(self.cur_item)
def render_string(self, textstate, seq, ncs, graphicstate):
self.textstate = textstate.copy()
super().render_string(self.textstate, seq, ncs, graphicstate)
def render_char(
self, matrix, font, fontsize, scaling, rise, cid, ncs, graphicstate
):
try:
text = font.to_unichr(cid)
assert isinstance(text, str), str(type(text))
except PDFUnicodeNotDefined:
text = self.handle_undefined_char(font, cid)
textwidth = font.char_width(cid)
textdisp = font.char_disp(cid)
item = LTStateAwareChar(
matrix,
font,
fontsize,
scaling,
rise,
text,
textwidth,
textdisp,
ncs,
graphicstate,
self.textstate,
)
self.cur_item.add(item)
return item.adv
def handle_undefined_char(self, font, cid):
# log.info('undefined: %r, %r', font, cid)
return (font.fontname, cid)
def receive_layout(self, ltpage):
self.result = ltpage
def get_result(self):
return self.result
def get_page_analysis(infile, pageno, pscript5_mode):
rman = pdfminer.pdfinterp.PDFResourceManager(caching=True)
dev = TextPositionTracker(rman, laparams=LAParams())
interp = pdfminer.pdfinterp.PDFPageInterpreter(rman, dev)
if pscript5_mode:
patcher = patch.multiple(
'pdfminer.pdffont.PDFType3Font',
spec=True,
get_ascent=PDFType3Font__PScript5_get_ascent,
get_descent=PDFType3Font__PScript5_get_descent,
get_height=PDFType3Font__PScript5_get_height,
)
patcher.start()
try:
with Path(infile).open('rb') as f:
page = PDFPage.get_pages(f, pagenos=[pageno], maxpages=0)
interp.process_page(next(page))
except PDFTextExtractionNotAllowed:
raise EncryptedPdfError()
finally:
if pscript5_mode:
patcher.stop()
return dev.get_result()
def get_text_boxes(obj):
for child in obj:
if isinstance(child, (LTTextBox)):
yield child
else:
try:
yield from get_text_boxes(child)
except TypeError:
continue
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,25 +1,25 @@
é la Waterman
a la Waterman
h ens linzen
4 ons linzen
3 liter water
3 mien
3 uien
bloem} hater
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, gout
Be linzen wassen en in¢l liter kokend wa-
ter 1 dag laten weken. 2 liter water bi'
de linzen voegen, gender hat water wagrin
ze geweekt zijn af te gieten. De helft Van
de mien bakken met laurier en kruiénagal.
Alla mien, kerrie en zout bij de linzen
voegen. A1135 aan de Rook brengem. Van de
bloem mat boter-en melk een papje maken en
verder afmaken met de soap. A13 de linzen
gaar zijn is de seep klaar.
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.
@@ -1,3 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,4 +1,4 @@
This shnuld be a perfect circle-
This should be a perfect circle:
@@ -5,20 +5,27 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 3.05.01' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.7vjmeixk/000001.ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/37/78_114p552q16vv6vmgm5kr00000gn/T/com.github.ocrmypdf.jz4b2s0s/000001.ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 296 96 704 504">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 296 96 704 504">
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336"><span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'><strong> </strong></span>
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336">
<span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'><strong><em> </em></strong></span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 150 592 841 622">
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8"><span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 85'><strong>This</strong></span> <span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 81'>shnuld</span> <span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 81'><strong>be</strong></span> <span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 85'><em>a</em></span> <span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 76'>perfect</span> <span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 76'><strong>circle-</strong></span>
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 96'><strong><em>This</em></strong></span>
<span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 95'><strong><em>should</em></strong></span>
<span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 95'><strong><em>be</em></strong></span>
<span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 95'><strong><em>a</em></strong></span>
<span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 95'><strong><em>perfect</em></strong></span>
<span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 55'><strong><em>circle:</em></strong></span>
</span>
</p>
</div>
@@ -1,3 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Too few characters. Skipping this page
OSD: Weak margin (0.00) for 24 blob text block, but using orientation anyway: 0
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,4 +1,4 @@
This shnuld be a perfect circle-
This should be a perfect circle:
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1 +1 @@
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Tesseract Open Source OCR Engine v4.0.0 with Leptonica
@@ -1,51 +1,51 @@
The LinnSequencer
32 Track MIDI Sequence Recorder
The LinnSequencer is a stateofthe-art composition and performance tool for the professional musician. It is
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
0 Operation is similar to multitrack tape recorder with PLAY, STOP, RECORD, FAST
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
0 Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
e Each of the 100 sequences contains 32 simultaneous, polyphonic tracks. Each track may
be assigned to one of 16 MIDI channels. Simultaneously plays up to 16 polyphonic
synthesizers !
synthesizers!
0 Ultrafast 3 1/2 ” disk drive stores complex songs in seconds and holds over 110,000 notes
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
per disk!
0 One or all tracks may be TRANSPOSED at the touch of a key.
0 Exclusive realtime ERASE function makes editing FAST.
0 Exclusive REPEAT function automatically repeats any held notes at a pre-selected
¢ One or all tracks may be TRANSPOSED at the touch of a key.
e Exclusive real-time ERASE function makes editing FAST.
* Exclusive REPEAT function automatically repeats any held notes at a pre-selected
rhythmic value.
0 TIMING CORRECTION works during playback and operates without chopping notes.
¢ TIMING CORRECTION works during playback and operates without chopping notes.
0 Optional SMPTE time code synchronization.
¢ Optional SMPTE time code synchronization.
0 Optional remote control.
© Optional remote control.
Recording 3 Sequence
Recording a Sequence
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
youll hear what you played—only all timing errors will be
you ll hear what you played—only all timing errors will be
corrected! (Timing correction may be adjusted or defeated).
Any additional notes played will be added into the track
—existing notes are not erased while recording!
existing notes are not erased while recording!
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spotrecording. To overdub a new part,
select a different track and start recording~while you
record, the rsttrack will play in perfect sync (unless you
your sequence for spot-recording. To overdub a new part,
select a different track and start recordingwhile you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
@@ -58,7 +58,7 @@ the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specic points within a sequence,
tion. To overdub notes at specific points within a sequence,
Additional Features
@@ -70,7 +70,7 @@ from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections.
unwanted sections,
Creating a Song
@@ -90,34 +90,34 @@ the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
0 Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
HELP button displays additional explanations.
0 Non-destructive recording—existing notes are not erased while recording.
0 Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
* Non-destructive recording—existing notes are not erased while recording.
¢ Two FOOTSWITCH INPUTS may be assigned to remotely control many of the commonly used functions, including
ERASE, REPEAT, PLAY/STOP, or LOCATE.
0 Two TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
0 Will sync to standard LinnDrum or Linn 9000 sync tone.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
' Utilizes ultra highspeed, 8 MHz 80186 16 bit computer internally for FAST operation.
0 TEMPO may be specified in BEATS-PERMINUTE or FRAMES-PERBEAT at 24, 25, or 30 frames per second,
© Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
* TEMPO may be specified in BEATS-PER-MINUTE or FRAMES-PER-BEAT at 24, 25, or 30 frames per second,
(even drop frame!)
0 TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
on the TAP TEMPO button.
0 TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
° Any TIME SIGNATURE may be used, and may be changed within a song.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
E1111
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 7088131 T ELEX #298949 LINN UR
(818) 708-8131 TELEX #298949 LINN UR
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 0
Rotate: 0
Orientation confidence: 18.91
Orientation confidence: 34.40
Script: Latin
Script confidence: 0.97
Script confidence: 2.90
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 90
Rotate: 270
Orientation confidence: 17.21
Orientation confidence: 34.53
Script: Latin
Script confidence: 1.06
Script confidence: 3.83
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 180
Rotate: 180
Orientation confidence: 14.79
Orientation confidence: 31.79
Script: Latin
Script confidence: 2.87
Script confidence: 3.18
@@ -1,6 +1,6 @@
Page number: 0
Orientation in degrees: 270
Rotate: 90
Orientation confidence: 19.19
Orientation confidence: 32.59
Script: Latin
Script confidence: 0.58
Script confidence: 2.85

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