Compare commits

...
237 Commits
Author SHA1 Message Date
James R. Barlow 5112e9e857 Redo dpi calc to avoid 'math.ulp' 2021-04-16 00:54:41 -07:00
James R. Barlow 757b72b0af Revert "Remove apparently unused portion of a test"
This reverts commit d89a633ba7.
2021-04-16 00:21:11 -07:00
James R. Barlow 75c5b92cb9 v12.0.0 update release notes 2021-04-15 23:49:58 -07:00
James R. Barlow d673126994 Fix ZeroDivisionError on files containing images drawn at scale 0
Fixes #761
2021-04-15 23:26:14 -07:00
James R. Barlow d89a633ba7 Remove apparently unused portion of a test 2021-04-15 23:25:18 -07:00
James R. Barlow 710d797299 Fix comment typos 2021-04-14 00:36:58 -07:00
James R. Barlow fc75254c60 Fix awslambda progressbar.update() error
Fixes #759
2021-04-13 14:37:30 -07:00
James R. Barlow f453e94f14 awslambda: better documentation 2021-04-13 13:16:35 -07:00
James R. Barlow 8f8aaa93ed Refactor removing log handlers 2021-04-13 13:16:21 -07:00
James R. Barlow a90b9e669f Add reminder to not mess with pool/listener order 2021-04-09 13:22:48 -07:00
James R. Barlow e4f69cc1d6 Maybe fix a deadlock on attempting sys.stderr.flush()
Closes #758
Closes #733
2021-04-09 01:59:30 -07:00
James R. Barlow a5852ba199 Remove parent process's log handlers properly 2021-04-08 21:39:18 -07:00
James R. Barlow 051b9da991 Remove undocumented/unused debug environment variables 2021-04-08 21:08:12 -07:00
James R. Barlow 139d9f9841 Shut up pikepdf mmap disabled message 2021-04-08 20:58:46 -07:00
James R. Barlow 532d65a355 ci: push on tag 2021-04-08 14:19:59 -07:00
James R. Barlow 9de38afb13 Fix Tesseract version for ubuntu 18.04 2021-04-08 13:07:13 -07:00
James R. Barlow 913c939dc9 docs: update wrt AWS 2021-04-08 01:51:44 -07:00
James R. Barlow 9db9a3d6ec helpers: improve test coverage of Resolution 2021-04-07 23:26:37 -07:00
James R. Barlow 8423bd549b helpers: don't trap exception on failure to unlink
If we can't unlink a file we expect to unlink, logging and moving on is
probably the wrong action.

Coverage never hits this line.
2021-04-07 23:16:10 -07:00
James R. Barlow 336d274a54 Drop remnants of support for Tesseract without has_textonly_pdf
Also improve Tesseract version checking so it can compare all of their
weird conventions.
2021-04-07 23:05:21 -07:00
James R. Barlow 906d77b389 tests: remove obsolete running_in_travis() 2021-04-07 02:25:10 -07:00
James R. Barlow 9416e850ff Remove another instance of helpers_namespace 2021-04-07 02:23:04 -07:00
James R. Barlow fd3248869c Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2021-04-07 02:18:14 -07:00
James R. Barlow 2a09a668f6 Delinting: unused args 2021-04-07 02:18:08 -07:00
James R. Barlow e788dde607 tests: eliminate unnecessary mmap 2021-04-07 02:11:31 -07:00
James R. Barlow 173a80864d Delinting 2021-04-07 02:09:45 -07:00
James R. Barlow aa115a8be3 Remove pytest_helpers_namespace 2021-04-07 01:56:51 -07:00
e0441c4aa1 Explicitly require setuptools, since pdfa.py imports pkg_resources (#755)
Co-authored-by: jbarlow83 <jbarlow83@users.noreply.github.com>
2021-04-06 02:23:44 -07:00
James R. Barlow a861c58da2 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2021-04-06 02:07:16 -07:00
James R. Barlow b1306bd7a8 tests: skip test_bash on Windows 2021-04-06 01:15:00 -07:00
James R. Barlow 3a6eb383dc Run all tests 2021-04-06 00:28:06 -07:00
James R. Barlow 6c942ecefd Update release notes 2021-04-06 00:17:36 -07:00
James R. Barlow a25f8ecc62 Remove Azure Pipelines 2021-04-06 00:17:29 -07:00
andkrauseandJames R. Barlow e1f4813d94 Dockerfile: support arm64
Thanks to @andkrause for submitting the initial PR that made this change
possible, @0x326 for review comments on the same PR, and both for their
patience in waiting for the rest of OCRmyPDF to catch up.

Co-authored-by: James R. Barlow

Closes #564
2021-04-06 00:09:04 -07:00
James R. Barlow af526f078d Activate codecov 2021-04-06 00:08:58 -07:00
James R. Barlow 16438c1312 Begin GitHub Actions migration 2021-04-06 00:08:13 -07:00
James R. Barlow dd6cb7ce20 Preparing release notes 2021-04-02 01:10:53 -07:00
James R. Barlow 653e2e23df Update deps for next release 2021-04-02 01:10:39 -07:00
James R. Barlow a2033698fa graft: use newer pikepdf style 2021-04-02 00:11:13 -07:00
James R. Barlow ec1d585d40 Merge branch 'feature/misc-breaking' 2021-04-01 16:51:04 -07:00
James R. Barlow a4e1f8e1f3 Merge branch 'feature/lambda' 2021-04-01 16:36:22 -07:00
James R. Barlow 2e155c31bf Ensure builtin module registration is deterministic 2021-04-01 16:30:42 -07:00
James R. Barlow e09ae9c68a Fix test suite failure if filter_pdf_page is missing 2021-04-01 16:25:40 -07:00
James R. Barlow c5bf1dd90d reqs: bump reqs to avoid reportlab issue 2021-04-01 16:01:23 -07:00
e96770c5e4 Updated documentation for windows and additional languages (#753)
Changed the documentation for how to add new languages on Windows.

Co-authored-by: jbarlow83 <jbarlow83@users.noreply.github.com>
2021-03-26 16:13:28 -07:00
James R. Barlow f4f0f3c022 v11.7.3 release notes 2021-03-20 23:30:42 -07:00
James R. Barlow 0a42934c08 Exclude Group 3 images from optimization 2021-03-20 23:28:21 -07:00
James R. Barlow d8f47768f9 v11.7.2 release notes 2021-03-19 00:31:38 -07:00
James R. Barlow c9594a4a5f Update pinned versions to avoid Pillow vulnerabilties
See https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst
2021-03-19 00:31:27 -07:00
James R. Barlow 873f915212 docs: mention problems with Debian/Ubuntu and other tidying 2021-03-09 23:24:10 -08:00
James R. Barlow 079c162a96 Ensure sidecar is not input or output file 2021-03-05 00:29:42 -08:00
James R. Barlow 25c8c4656f Fix error message change 2021-03-03 01:02:59 -08:00
James R. Barlow ffcae9a1a0 v11.7.1 release notes 2021-03-03 00:46:35 -08:00
James R. Barlow 6e71fe1186 Clarify --unpaper-args errors 2021-03-03 00:44:21 -08:00
James R. Barlow 0885799010 Update docs for conda
Closes #743
2021-03-03 00:43:45 -08:00
James R. Barlow 8ffc99f648 optimize: log errors more loudly 2021-03-03 00:43:40 -08:00
James R. Barlow 2261c51eff Reactivate pngquant on windows 2021-02-26 01:19:03 -08:00
James R. Barlow 5c470778a3 v11.7.0 release notes 2021-02-26 00:29:52 -08:00
James R. Barlow 4124889f36 Don't generate PDF/A-1b with object streams
Acrobat insists that PDF/A-1b should not have object streams.
Other programs like veraPDF disagree with this restriction, but
we can accommodate Acrobat so we will.

Also add more tests around this.
2021-02-26 00:23:57 -08:00
James R. Barlow a23c22b0e8 helpers: tidy check_pdf 2021-02-25 22:51:53 -08:00
James R. Barlow dd1f5f7215 pyproject: black doesn't like py39 yet 2021-02-25 16:10:20 -08:00
Dima KuznetsovandGitHub 5e2206bae7 Allow --sidecar along --pages (#735) 2021-02-19 16:55:35 -08:00
James R. Barlow 079ee86d43 pyproject: also target py39 2021-02-18 01:48:56 -08:00
James R. Barlow 3692868004 v11.6.2 release notes 2021-02-15 01:48:14 -08:00
James R. Barlow 064f935699 Fix page rotation regression
Page size fixes in commit b26749 did accounted for a "kept" rotation,
but not a corrected rotation.

Fixes #730.
2021-02-15 01:47:09 -08:00
James R. Barlow 8770fff968 tests: remove unreliable/incomplete test 2021-02-15 01:05:08 -08:00
James R. Barlow 82de78b6b0 v11.6.1 release notes 2021-02-14 01:51:26 -08:00
James R. Barlow 2a52c6dec2 optimize: skip images with unusually small dimensions
They're unlikely to be handled well by our recompressors. It seems
that JBIG2 cannot handle very small widths.

Fixes #732
2021-02-14 01:43:25 -08:00
James R. Barlow 2898879be7 docker-compose: fix typo 2021-02-14 01:43:06 -08:00
James R. Barlow 18e613657c docker-compose: fix typo 2021-02-14 01:23:01 -08:00
James R. Barlow a48ca556c7 Add filter_pdf_page hook 2021-02-14 01:22:33 -08:00
James R. Barlow 9cba738b48 Remove deprecated code 2021-01-31 19:27:59 -08:00
James R. Barlow bccf2f423f Stricter parameter checking for many public functions 2021-01-31 19:27:25 -08:00
James R. Barlow 390fdf8c05 Package OCR in Form XObject
Should improve results in some situations where the initial content
stream is messy or not well-formed.
2021-01-31 19:27:25 -08:00
James R. Barlow 166de3086b Merge branch 'feature/colorstrategy' 2021-01-31 19:26:59 -08:00
James R. Barlow 206c675df6 docs: api 2021-01-31 19:26:35 -08:00
James R. Barlow 6c8f9223e9 Update awslambda to new pluginspec 2021-01-31 03:00:05 -08:00
James R. Barlow 85c6a974ca Fix calls to hook.get_executor 2021-01-31 02:46:44 -08:00
James R. Barlow dccdcfaa91 leptonica: tidy 2021-01-31 02:46:09 -08:00
James R. Barlow b1da09f141 Add plugin for setting logging console
So that we are not tied to tqdm.
2021-01-31 02:29:11 -08:00
James R. Barlow 42c84531e4 optimize: rewrite JPEG optimize to avoid use of tqdm and parallelize
For some reason JPEG optimization was not done in parallel, and was
perhaps never done in parallel. Strange oversight.
2021-01-31 02:18:46 -08:00
James R. Barlow a9ad805347 optimize: Remove shim for unsupported pikepdf version 2021-01-31 00:08:20 -08:00
James R. Barlow 16bda74974 Refactor - decouple progressbar from executor 2021-01-30 20:42:00 -08:00
James R. Barlow d274d88929 Refactor to eliminate global state in _concurrent 2021-01-30 17:36:30 -08:00
James R. Barlow 327df5cbbc Use ColorConversionStrategy "LeaveColorUnchanged"
Faster, still produces PDF/A
2021-01-26 15:44:41 -08:00
James R. Barlow 46d0632fe2 v11.6.0 release notes 2021-01-26 01:47:49 -08:00
James R. Barlow ef1e7a814e Delinting 2021-01-26 01:45:04 -08:00
James R. Barlow 1084724937 docs: improve API docs 2021-01-26 01:40:40 -08:00
James R. Barlow ecb0109d79 docs: fix rst formatting error 2021-01-26 01:29:45 -08:00
James R. Barlow 386cabff00 Make progress pool common rather than plugin-specific 2021-01-24 23:56:09 -08:00
James R. Barlow 3bd5054634 lambda: move to extra_plugins folder 2021-01-24 23:47:26 -08:00
James R. Barlow 6a8dd65aa2 lambda: more issues related to new executor semantics
Now all tests pass, except for:
-tests that check the progress bar
-tests where xdist may or may not load a _lambda_plugin by running
some other test first before a test in optimize
2021-01-24 23:46:40 -08:00
James R. Barlow 6083b4f0a7 lambda: don't overrun number of workers needed 2021-01-24 23:46:40 -08:00
James R. Barlow 1a3ce59476 lambda: Don't be paranoid about exception marshalling
It works
2021-01-24 23:46:40 -08:00
James R. Barlow c395436ba3 lambda: tidying, special casing use_threads 2021-01-24 23:46:40 -08:00
James R. Barlow 8d23d0b441 Operational lambda executor 2021-01-24 23:46:40 -08:00
James R. Barlow c6a2716cdb Temporary move into package 2021-01-24 23:46:40 -08:00
James R. Barlow 5545bae76f lambda_plugin.py: doesn't work since entry point needs to be in package 2021-01-24 23:46:33 -08:00
James R. Barlow 7bccb8c748 tests: fix concurrency 2021-01-24 23:46:33 -08:00
James R. Barlow 173c0d1274 concurrency: lock progress pool
For API sanity and to communicate expectations. One progress pool at
a time is plenty of complexity.
2021-01-24 23:46:33 -08:00
James R. Barlow 6953f32465 pdfinfo: remove some messy concurrency handling
We can cut down on the use of global variables and save opening
an extra copy of the Pdf when threaded.
2021-01-24 23:46:33 -08:00
James R. Barlow 26b4d9bb4b Refactor concurrency so that it is pluggable
However, this may not be the best idea because it involves global
state that could be overridden by a parallel call to ocrmypdf.ocr.
2021-01-24 23:46:29 -08:00
James R. Barlow 34e564cd7d Use queue.Queue instead of multiprocessing.Queue in threaded mode 2021-01-24 23:45:26 -08:00
James R. Barlow 504d5776d2 Refactor plugin manager to eliminate callback 2021-01-24 23:42:40 -08:00
James R. Barlow ee23976858 Re-sequence plugin installation 2021-01-24 23:42:28 -08:00
James R. Barlow f559316881 Insert setuptools plugins with ocrmypdf prefix 2021-01-24 23:42:09 -08:00
James R. Barlow 084610c242 Automate insertion of builtin modules 2021-01-24 23:41:50 -08:00
James R. Barlow 9ff627472b Update pre-commit 2021-01-24 23:40:59 -08:00
James R. Barlow 956310d1ec Import PageContext, PdfContext since they are referenced in pluginspec 2021-01-24 02:04:47 -08:00
James R. Barlow 1a982da442 tests: confirm that we produce pdf when optimization is off 2021-01-24 01:54:25 -08:00
James R. Barlow 4879a1f0de docs: no MS Store Python 2021-01-18 13:27:31 -08:00
James R. Barlow ce66bcc9c8 github: Ask how ocrmypdf was installed 2021-01-10 02:35:24 -08:00
James R. Barlow 1ebf3144af v11.5.0 release notes 2021-01-09 16:48:40 -08:00
James R. Barlow 7a1cccbc4e Fallback to LeptonicaErrorTrap_Redirect if ffi.callback fails
Might fix issue #709, Apple silicon support.
2021-01-09 16:47:11 -08:00
James R. Barlow ebacff1b39 tests: Fix debug logging test 2021-01-09 16:41:57 -08:00
James R. Barlow c7c447be66 Add test for configure_debug_logging
Since we can't directly test it
2021-01-09 16:02:12 -08:00
James R. Barlow 91aa175602 Consider text when determining page raster DPI
Previously if we found vectors of any sort on a page, we would bump
the DPI up to 400. We did nothing
about pages with text. As a result,
pages with a low image resolution
and printable text would have the text downgraded to image
resolution when --force-ocr was used.

We don't try to determine if the text is visible or invisible OCR text, since
that is a slower test. --redo-ocr would improve such cases anyway.
2021-01-09 16:01:49 -08:00
James R. Barlow b267494e4a Create raster PDF pages to match input page size
Previously we produced a raster image, then multiplied image width
by DPI to get the page size. However if there is rounding the
page size may not match exactly. In this modified approach we
constrain the page size to match.
2021-01-08 15:10:43 -08:00
James R. Barlow f687180ecc tests: tidy pdfinfo 2021-01-08 15:04:52 -08:00
James R. Barlow 6f4b38b103 ghostscript: tidy comments 2021-01-08 00:41:03 -08:00
James R. Barlow d32324859c v11.4.5 release notes 2021-01-06 11:42:28 -08:00
James R. Barlow 48222b87b5 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2021-01-06 03:59:40 -08:00
62e5edc72b fix unclosed file warnings. (#710)
Co-authored-by: Jonas Winkler <jonas.winkler@jpwinkler.de>
2021-01-06 03:59:28 -08:00
James R. Barlow 2846d46bb8 Remove .coveragerc and fold into setup.cfg 2021-01-06 03:58:18 -08:00
James R. Barlow 47ef1914d4 v11.4.4 release notes 2021-01-01 01:39:24 -08:00
James R. Barlow df157552f3 Make ocrmypdf.ocr take a threading lock 2021-01-01 01:37:09 -08:00
James R. Barlow 0b3a526049 Partial fix crash on 'userunit' None (#700)
Our method of getting data from pdfminer would silently consume a StopIteration
if pdfminer returned no processed pages, leading to odd error message.

We improve an error from pdfminer properly, and returning a more
descriptive error of our own.

It would be possible for ocrmypdf to repair the file before sending it to
pdfminer, but this seems to be rare enough that we won't do that yet.
2021-01-01 01:11:32 -08:00
James R. Barlow 1e80d412fa tesseract: fix typing of some optional arguments 2021-01-01 00:46:00 -08:00
James R. Barlow df6e106203 concurrent: simplify results loop 2021-01-01 00:44:46 -08:00
James R. Barlow bd0f005861 tests: tag tests that need pngquant, jbig2enc 2020-12-30 01:58:57 -08:00
James R. Barlow 6ba4b7b3f3 ci: temporarily disable pngquant on Windows
Looks like a packaging error, choco complains of bad hashes.
2020-12-30 01:40:56 -08:00
James R. Barlow 2c11349ee8 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-12-29 21:40:46 -08:00
James R. Barlow b0afef09ef v11.4.3 release notes 2020-12-29 21:40:35 -08:00
James R. Barlow 72fa347c38 tests: skip metadata test for two pikepdf versions that warn incorrectly 2020-12-29 01:47:52 -08:00
James R. Barlow 96d68c2413 pipeline: refactor metadata_fixup 2020-12-29 01:47:32 -08:00
James R. Barlow babc76fa74 tests: assert that most patched functions are called
We were not actually checking if functions we patched we called when
expected.
2020-12-28 23:58:33 -08:00
Tim GatesandGitHub dc06990e5d docs: fix simple typo, instsalled -> installed (#704)
There is a small typo in docs/installation.rst.

Should read `installed` rather than `instsalled`.
2020-12-28 15:28:34 -08:00
James R. Barlow 0ff0d2f8d1 Remove PDF/A overprint debug message
Since we currently log all of a process's output at debug it's
redundant to log this separate message.
2020-12-27 16:19:05 -08:00
James R. Barlow 81602cf420 Fix test not patching properly after Ghostscript polling change 2020-12-27 16:01:50 -08:00
James R. Barlow 607e2d7e81 v11.4.2 release notes 2020-12-27 03:29:35 -08:00
James R. Barlow b01d9e07e8 Deal with missing pthread_sigmask on Cygwin
Closes #701
2020-12-27 02:24:00 -08:00
James R. Barlow 91db94cf2e watcher: fix OCR_LOGLEVEL env var not processed
Closes #702
2020-12-27 02:02:44 -08:00
James R. Barlow 416df803d4 pdfinfo: stricter typing 2020-12-24 22:39:00 -08:00
James R. Barlow 037b96ca16 pdfinfo: refactor to eliminate RawPageInfo 2020-12-24 02:57:44 -08:00
James R. Barlow bb258fc99c pdfinfo: Refactor pageinfo dictionary into a class 2020-12-24 01:47:53 -08:00
James R. Barlow 4b8ccbe8cb v11.4.1 release notes 2020-12-22 01:41:15 -08:00
James R. Barlow ab1ff3331b misc: synology fix
Accept user-contributed fix. Not testable.

Close #690.
2020-12-22 01:38:41 -08:00
James R. Barlow 3675ae918c Fix certain invalid page ranges causing exception
Closes #686
2020-12-22 01:22:14 -08:00
James R. Barlow 0ba32b96b7 Revert "v11.4.0 release notes - remove change not actually implemented"
This reverts commit ad202693b3.
Temporary folder prefix was actually changed in commit f11bb53e.
2020-12-22 00:47:25 -08:00
James R. Barlow add64e4fa2 docs: com.github.ocrmypdf -> ocrmypdf.io 2020-12-22 00:46:42 -08:00
James R. Barlow 7fe2954ede Change wheel tag to py36, update package_data to include py.typed 2020-12-12 16:49:04 -08:00
James R. Barlow ad202693b3 v11.4.0 release notes - remove change not actually implemented
Remove a change that was pushed back to a future release.
2020-12-12 16:27:38 -08:00
James R. Barlow 594ef83551 v11.4.0 release notes 2020-12-11 15:09:49 -08:00
James R. Barlow 78b71618c1 Fix BufferedReader TypeError 2020-12-11 14:19:20 -08:00
James R. Barlow b8aa89e1ec Fix log message queue flooding on certain files
Fixes #692
2020-12-11 14:14:21 -08:00
James R. Barlow b4c1f66bc1 typing: tidy up 2020-12-11 14:14:21 -08:00
James R. Barlow 5172dbde8d subprocess: use more mypy-friendly syntax 2020-12-11 14:14:21 -08:00
James R. Barlow d2908640c6 pdfa: help mypy figure out a type 2020-12-11 14:14:21 -08:00
James R. Barlow 997bf7578d hocrtransform: fix exception if no div ocr_page object 2020-12-11 14:14:21 -08:00
James R. Barlow 043258242c hocrtransform: trivial typing 2020-12-11 14:14:21 -08:00
James R. Barlow 156d5d9a9c cli: typing 2020-12-11 14:14:21 -08:00
James R. Barlow 0b7e52fb5e api: parse cmdline in more type friendly way 2020-12-11 14:14:21 -08:00
James R. Barlow a5feef07d0 Declare ocrmypdf as typed 2020-12-11 14:14:21 -08:00
James R. Barlow f11bb53e61 Change prefix of temporary folders
Shouldn't really use a name that suggests a connection to GitHub.
2020-12-07 21:51:46 -08:00
James R. Barlow 68a57a7839 Add feature to generate hocr-pdf with visible debug text 2020-12-04 17:38:48 -08:00
James R. Barlow 4194430dc1 Begin next release notes 2020-12-04 13:28:04 -08:00
James R. Barlow a707c56fae docs: improve windows instructions 2020-12-04 13:21:54 -08:00
James R. Barlow 3cba50bfbd windows: look in registry for Tesseract and Ghostscript 2020-12-04 13:21:54 -08:00
James R. Barlow ed5e17d0a4 completions: consider *.PDF and some images too 2020-12-04 13:20:35 -08:00
James R. Barlow ce0e0ecd4d Decouple tqdm from progressbar setup 2020-12-04 13:20:28 -08:00
James R. Barlow 7e1223c12c ghostscript: add output tracing 2020-11-29 14:53:35 -08:00
James R. Barlow b83d7f6d1a subprocess: refactor and add run_polling_stderr 2020-11-29 14:36:03 -08:00
James R. Barlow 80e957908a tesseract: fix run call with logs_errors_to_stdout 2020-11-29 14:25:46 -08:00
James R. Barlow f0e7bea8ba docs: remove redundant statement 2020-11-27 13:54:36 -08:00
James R. Barlow 0cdb9bd04a docs: remove description of how OMP_THREAD_LIMIT is managed 2020-11-23 12:36:04 -08:00
James R. Barlow 8224d89bc6 v11.3.4 release notes 2020-11-18 11:57:28 -08:00
James R. Barlow a2bbbe2a26 v11.3.4 release notes 2020-11-18 11:56:29 -08:00
James R. Barlow 43f41863fa check_pdf: document how we handle linearization 2020-11-18 11:54:07 -08:00
James R. Barlow d71e50e83d Fix "readLinearizationData for file that is not linearized"
pikepdf 2.1.0 throws wrong type of exception in this case, so special-case it.

Closes #680
Closes #681
2020-11-18 11:52:17 -08:00
James R. Barlow 1f598da3c1 ghostscript: better docs and comments 2020-11-18 11:34:17 -08:00
James R. Barlow d0cdbd5e1c watcher: include uppercase .PDF too 2020-11-12 02:29:47 -08:00
James R. Barlow 5c56f61209 unpaper: type hints 2020-11-11 02:59:37 -08:00
James R. Barlow 9bec85470a Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-11-10 04:08:05 -08:00
James R. Barlow a03863a17d docs: fix link to docker image 2020-11-10 04:08:01 -08:00
James R. Barlow 22cd9b2364 docs: fix csv-table errors 2020-11-10 04:07:49 -08:00
pretentious7andGitHub 4fc7d6d93e fix typo "charcter" -> "character" (#673) 2020-11-09 16:53:02 -08:00
James R. Barlow 71f0e7f545 v11.3.3 release notes 2020-11-07 00:53:33 -08:00
James R. Barlow 895fddd85e Replace most uses of universal_newlines with text
The parameters are equivalent but the latter is better named. Since
Python 3.6 doesn't support text= we use our wrapper to add it in that
place.

This is for subprocess.run.
2020-11-07 00:48:08 -08:00
James R. Barlow 5a59e4d543 unpaper: don't use universal_newlines=True
There's no specific reason to do this. We can log binary output equally
 well.
2020-11-07 00:18:27 -08:00
James R. Barlow b51abf2249 azure: Fix indentation mistake 2020-11-04 12:19:35 -08:00
James R. Barlow 6d3f9ff15a api: rework ocr() slightly to simplify variable handling 2020-11-03 17:10:52 -08:00
James R. Barlow 5d1d1a712b docs: more details about macOS API changes
Due to fork->spawn
2020-11-03 17:09:58 -08:00
James R. Barlow 6d5f8133e0 docs: show ifmain guard in example 2020-11-03 15:28:33 -08:00
James R. Barlow 13018d3d5c ci: Extend test matrix to Python 3.9 2020-11-03 04:15:14 -08:00
James R. Barlow 14a85f9473 Fix pinned dependencies 2020-11-03 04:12:47 -08:00
James R. Barlow d22a1b3367 v11.3.2 release notes (2)
Since we never tagged it, fix other things.
2020-11-03 02:03:25 -08:00
James R. Barlow b913e5dfef ghostscript: don't repeat log in debug
Subprocess already does this for us.
2020-11-03 01:45:06 -08:00
James R. Barlow dd8a5a4c72 Fix log domain names
ocrmypdf.subprocess.subprocess.ghostscript -> ocrmypdf.subprocess.ghostscript
2020-11-03 01:44:35 -08:00
James R. Barlow 36e9a54f02 Remove extraneous page rotation
This was added in commit b5ccbfd but seems to have been ill-advised.
2020-11-03 01:34:28 -08:00
James R. Barlow 3707af3b74 Change pdf.root to pdf.Root 2020-11-03 01:30:31 -08:00
James R. Barlow ced7ad9164 unpaper: round off DPI 2020-11-03 01:14:57 -08:00
James R. Barlow 54bbbfdeb3 Fix UnboundLocalError when considering ImageMasks for optimization
Uncovered by test file in issue 667, although unrelated to that issue.
2020-11-03 01:08:14 -08:00
James R. Barlow 7f73a6ed1e Some Python 3.9 fixes 2020-11-03 00:45:47 -08:00
James R. Barlow dce206d3dc Fix pre-commit for Py3.9 2020-11-03 00:20:25 -08:00
James R. Barlow 9304c856cf Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-11-02 02:47:36 -08:00
James R. Barlow e5df98cbdf v11.3.2 release notes 2020-11-02 02:43:32 -08:00
James R. Barlow 19bf3aeb00 api: improve typing 2020-11-02 02:33:34 -08:00
James R. Barlow e86be0031c unpaper: fix process output handling
With the ocrmypdf.subprocess wrapper, logging the output here
is redundant and loses the page number context.
2020-11-02 01:07:41 -08:00
James R. Barlow 6425977998 unpaper: use pnm instead of png
Some users reported problems with PNG recently; try PNM.

Fixes #665
Fixes #667
2020-11-02 01:05:56 -08:00
James R. Barlow d57df2d980 subprocess: support programs that write their messages to stdout 2020-11-02 01:00:59 -08:00
James R. Barlow 664d0c7969 Document configure_debug_logging 2020-11-02 00:59:00 -08:00
James R. Barlow a354663ee1 Fix typo in API documentation 2020-11-02 00:58:28 -08:00
Graham MilnandGitHub b21b048ec4 Add macOS brew language support (#615)
Note `brew` command for installing additional languages on macOS.
2020-10-30 01:09:06 -07:00
James R. Barlow 709c65b41a v11.3.1 release notes 2020-10-27 23:11:11 -07:00
James R. Barlow 67f99c5bb7 Endorse pdfminer.six 20201018 2020-10-27 23:09:45 -07:00
James R. Barlow d55e673d9c Fix warning about --pdfa-image-compression argument at wrong times
Closes #663
2020-10-27 23:09:45 -07:00
James R. Barlow 21b90d2d14 Endorse pikepdf 2.x 2020-10-27 23:09:45 -07:00
Edward BettsandGitHub 2def7e3392 Use % for percentage in string format (#643) 2020-10-27 23:09:14 -07:00
James R. Barlow b0dcaa7512 v11.3.0 release notes 2020-10-24 03:19:32 -07:00
James R. Barlow e8285b1d10 Add test to confirm rasterize_pdf_page rotates correct 2020-10-24 03:10:59 -07:00
James R. Barlow 5ba56adb53 Fix page rotation issue (again)
Commit 1327ab3 introduced a fix for a regression, which was reported
in #581, #634. It appears that the actual cause of this issue was
default parameters to rasterize_pdf_page in pluggy not working as
expected, causing a default rotation=0 even when a rotation was needed.
As such the OCR image was generated with the wrong orientation,
causing the initial regression and fix in commit 1327ab3.

Now that the real problem is identified, it's apparent that the logic
prior to 1327ab3 was found and we can revert to 1327ab3 since it fixes
all known cases including #658.

This reverts 1327ab3 except for retaining improves to rotation output.
2020-10-24 02:45:21 -07:00
James R. Barlow ca735278e0 setup: Version pluggy better 2020-10-24 02:35:41 -07:00
James R. Barlow b5ccbfdf25 Fix hookspec of rasterize_pdf_page to remove default parameters 2020-10-24 02:35:18 -07:00
James R. Barlow 8c35d6e6e4 Fix debug log messages being suppressed from child processes 2020-10-22 02:20:06 -07:00
James R. Barlow d1e0c81eda Ensure worker_pdf is closed after gathering info in a thread
This is hacky, uses global state, but it does improve the situation for now.
2020-10-22 00:38:24 -07:00
James R. Barlow 10c8e4f8b4 Only create debug.log when running from command line
When used as a library ocrmypdf shouldn't make policy decisions, like where to
put a log file. Unsurprisingly, creating it causes problems for library users
because we deleted the temporary folder which held the log file and made no
effort to move it to a new location.

Also update the documentation to better described how an application should
handle this.

Closes #657
2020-10-20 01:29:36 -07:00
James R. Barlow 6be2242c21 Describe "OCR" step as "Image processing" when --tesseract-timeout=0
Fixes #647
2020-10-08 01:03:42 -07:00
James R. Barlow 204c9d6ae1 Fix inverted colors during JBIG2 optimization on paletted images
Fixes #640
2020-10-07 04:08:50 -07:00
James R. Barlow 6eb393590b v11.2.0 release notes
Change v11.1.3 to v11.2.0 since it contains functional changes.
2020-10-06 03:24:31 -07:00
James R. Barlow 07c6654057 v11.1.3 release notes 2020-10-06 03:22:48 -07:00
James R. Barlow 4e15eb8d14 Fix image optimization discarding image masks and soft masks associated with PNGs
Fixes #648
2020-10-06 03:20:54 -07:00
James R. Barlow 8b01ab8ad2 Better type checking on ocrmypdf.ocr(plugins=...) 2020-10-05 15:02:34 -07:00
James R. Barlow e0a522ad50 Document the example plugin 2020-10-05 15:01:44 -07:00
James R. Barlow a1a8788c5a Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2020-09-29 02:46:27 -07:00
James R. Barlow cccdc178c3 v11.1.2 release notes 2020-09-29 02:46:18 -07:00
James R. Barlow 4eacb3454f hOCR: write text in correct order
Fixes #642
2020-09-29 02:45:11 -07:00
Jimit DholakiaandGitHub 82b8b41e80 docs: Add 'unpaper' optional dependency for Ubuntu 18.04 (#639) 2020-09-25 11:54:31 -07:00
143 changed files with 7036 additions and 5121 deletions
-24
View File
@@ -1,24 +0,0 @@
[paths]
source =
src
*/site-packages
[run]
branch = true
parallel = true
concurrency =
thread
multiprocessing
source =
src/ocrmypdf
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if 0:
if False:
if __name__ == .__main__.:
if TYPE_CHECKING:
+5 -2
View File
@@ -10,8 +10,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool \
libleptonica-dev \
zlib1g-dev \
python3 \
python3-dev \
python3-distutils \
libffi-dev \
libqpdf-dev \
ca-certificates \
curl \
git
@@ -62,7 +64,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-fra \
tesseract-ocr-por \
tesseract-ocr-spa \
unpaper
unpaper \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
+5 -1
View File
@@ -21,8 +21,12 @@ If applicable, add screenshots to help explain your problem.
**System (please complete the following information):**
- OS:
- Python version:
- Python version:
- OCRmyPDF version:
**Installation**
How did you install OCRmyPDF? Did you install it from your operating system's
package manager, or using pip?
**Additional context**
Add any other context about the problem here.
+275
View File
@@ -0,0 +1,275 @@
name: Test and deploy
on:
push:
branches:
- master
- ci
- release/*
tags:
- v*
paths-ignore:
- README*
pull_request:
jobs:
test_linux:
name: Test ${{ matrix.os }} with Python ${{ matrix.python }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04] #, ubuntu-20.04]
python: ["3.6"] #, "3.7", "3.8", "3.9"]
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v2
name: Install Python
with:
python-version: ${{ matrix.python }}
- name: Install common packages
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
curl \
ghostscript \
img2pdf \
libffi-dev \
liblept5 \
libsm6 libxext6 libxrender-dev \
pngquant \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
unpaper \
zlib1g
- name: Install Ubuntu 18.04 packages
if: matrix.os == 'ubuntu-18.04'
run: |
sudo apt-get install -y --no-install-recommends \
libexempi3
- name: Install Ubuntu 20.04 packages
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt-get install -y --no-install-recommends \
libexempi8
- name: Install Python packages
run: |
python -m pip install -r requirements/main.txt -r requirements/test.txt .
- name: Report versions
run: |
tesseract --version
gs --version
pngquant --version
unpaper --version
img2pdf --version
- name: Test
run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
files: ./coverage.xml
env_vars: OS,PYTHON
test_macos:
name: Test macOS
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
python: ["3.9"]
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v2
name: Install Python
with:
python-version: ${{ matrix.python }}
- name: Install Homebrew deps
run: |
brew update
brew install \
exempi \
ghostscript \
jbig2enc \
leptonica \
openjpeg \
pngquant \
tesseract
- name: Install Python packages
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements/main.txt -r requirements/test.txt .
- name: Report versions
run: |
tesseract --version
gs --version
pngquant --version
img2pdf --version
- name: Test
run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
files: ./coverage.xml
env_vars: OS,PYTHON
test_windows:
name: Test Windows
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
python: ["3.9"]
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v2
name: Install Python
with:
python-version: ${{ matrix.python }}
- name: Install system packages
run: |
choco install --yes --no-progress --pre tesseract
choco install --yes --no-progress ghostscript
choco install --yes --no-progress pngquant
- name: Install Python packages
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements/main.txt -r requirements/test.txt .
- name: Test
run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
files: ./coverage.xml
env_vars: OS,PYTHON
wheel_sdist_linux:
name: Build sdist and wheels
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v2
name: Install Python
with:
python-version: "3.6"
- name: Make wheels and sdist
run: |
python -m pip install --upgrade pip wheel
python setup.py sdist
python setup.py bdist_wheel
- uses: actions/upload-artifact@v2
with:
path: |
./dist/*.whl
./dist/*.tar.gz
upload_pypi:
name: Deploy artifacts to PyPI
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v2
with:
name: artifact
path: dist
- uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.TOKEN_PYPI }}
# repository_url: https://test.pypi.org/legacy/
docker:
name: Build Docker images
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
runs-on: ubuntu-latest
steps:
- name: Set image tag to release or branch
run: echo "DOCKER_IMAGE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: If master, set to latest
run: echo 'DOCKER_IMAGE_TAG=latest' >> $GITHUB_ENV
if: env.DOCKER_IMAGE_TAG == 'master'
- name: Set Docker Hub repository to username
run: echo "DOCKER_REPOSITORY=jbarlow83" >> $GITHUB_ENV
- name: Set image name
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
- uses: actions/checkout@v2
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: jbarlow83
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Print image tag
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
- name: Build
run: |
docker buildx build \
--push \
--platform linux/arm64/v8,linux/amd64 \
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
--file .docker/Dockerfile .
+1
View File
@@ -24,6 +24,7 @@ venv*/
/debug_tests.py
*.traineddata
/private
/coverage.xml
# Package building
*.egg-info/
+4 -4
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
rev: v3.4.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
@@ -12,12 +12,12 @@ repos:
hooks:
- id: seed-isort-config
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.0.5 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
rev: v5.7.0 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 19.10b0
rev: 20.8b1
hooks:
- id: black
language_version: python3.8
language_version: python
exclude: ^src/ocrmypdf/lib/_leptonica.py
+13 -18
View File
@@ -1,6 +1,6 @@
<img src="docs/images/logo.svg" width="240" alt="OCRmyPDF">
[![Build Status][azure]](https://dev.azure.com/jim0585/ocrmypdf/_build/latest?definitionId=2&branchName=master) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
[![Build Status](https://github.com/jbarlow83/OCRmyPDF/actions/workflows/build.yml/badge.svg)](https://github.com/jbarlow83/OCRmyPDF/actions/workflows/build.yml) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
[azure]: https://dev.azure.com/jim0585/ocrmypdf/_apis/build/status/jbarlow83.OCRmyPDF?branchName=master
[travis]: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master "Travis build status"
@@ -59,23 +59,15 @@ I searched the web for a free command line tool to OCR PDF files: I found many,
Linux, Windows, macOS and FreeBSD are supported. Docker images are also available.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
```bash
apt-get install ocrmypdf
```
and users of Fedora 29 or later may simply
```bash
dnf install ocrmypdf
```
and Homebrew users (macOS, Linux, Windows Subsystem for Linux) may simply
```bash
brew install ocrmypdf
```
| Operating system | Install command |
| ----------------------------- | ------------------------------|
| Debian, Ubuntu | ``apt install ocrmypdf`` |
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
| Fedora | ``dnf install ocrmypdf`` |
| macOS | ``brew install ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py37-ocrmypdf`` |
| Conda | ``conda install ocrmypdf`` |
For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps.
@@ -92,6 +84,9 @@ apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified lan
# Arch Linux users
pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs
# brew macOS users
brew install tesseract-lang
```
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.
-249
View File
@@ -1,249 +0,0 @@
trigger:
tags:
include:
- v*
branches:
include:
- "*"
exclude:
- "travis"
stages:
- stage: "Test"
jobs:
- job: Windows
pool:
vmImage: "vs2017-win2016"
strategy:
matrix:
Python36:
python.version: "3.6"
Python37:
python.version: "3.7"
Python38:
python.version: "3.8"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- pwsh: |
choco install --yes --no-progress --pre tesseract
choco install --yes --no-progress python3
choco install --yes --no-progress ghostscript
choco install --yes --no-progress pngquant
displayName: "Install system packages"
- pwsh: |
refreshenv
python -m pip install --upgrade pip wheel
python -m pip install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- pwsh: |
refreshenv
$env:pathext += ';.py'
# -n auto helps Windows
python -m pytest -n auto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- job: "Ubuntu_1804"
pool:
vmImage: "ubuntu-18.04"
strategy:
matrix:
Python36:
python.version: "3.6"
Python37:
python.version: "3.7"
Python38:
python.version: "3.8"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- bash: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
python3-software-properties \
curl \
ghostscript \
img2pdf \
libexempi3 \
libffi-dev \
liblept5 \
libsm6 libxext6 libxrender-dev \
pngquant \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
unpaper \
zlib1g
displayName: "Install system packages"
- bash: |
curl https://bootstrap.pypa.io/get-pip.py | python3
pip3 install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- bash: |
tesseract --version
displayName: "Record versions"
- bash: |
# -n auto is slower on Linux and breaks on Python 3.8
pytest -n0 --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- job: "Ubuntu_1604"
pool:
vmImage: "ubuntu-16.04"
strategy:
matrix:
Python36:
python.version: "3.6"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
- bash: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
software-properties-common
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ghostscript \
img2pdf \
libexempi3 \
libffi-dev \
liblept5 \
libsm6 libxext6 libxrender-dev \
pngquant \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
unpaper \
zlib1g
displayName: "Install system packages"
- bash: |
curl https://bootstrap.pypa.io/get-pip.py | python3
pip3 install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- bash: |
tesseract --version
displayName: "Record versions"
- bash: |
# -n auto is slower on Linux and breaks on Python 3.8
pytest -n0 --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- job: "macOS_Mojave"
pool:
vmImage: "macos-10.14"
steps:
# https://github.com/actions/virtual-environments/issues/664
# - task: UsePythonVersion@0
# inputs:
# versionSpec: "$(python.version)"
- bash: |
brew update
brew unlink python@2
brew upgrade python
echo "Using Python `python3 --version`"
displayName: "Update brew and Python"
- bash: |
brew install \
exempi \
ghostscript \
jbig2enc \
leptonica \
openjpeg \
pngquant \
tesseract
displayName: "Install system packages"
- bash: |
pip3 install --upgrade pip
pip3 install -r requirements/main.txt -r requirements/test.txt .
displayName: "Install Python packages"
- bash: |
tesseract --version
displayName: "Record versions"
- bash: pytest -nauto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
displayName: "Test"
- task: PublishTestResults@2
inputs:
testResultsFiles: "test.xml"
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
condition: succeededOrFailed()
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
- stage: "Artifacts"
jobs:
- job: "sdist_wheel"
pool:
vmImage: "ubuntu-18.04"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "3.7"
- bash: |
python -m pip install --upgrade pip wheel
python setup.py sdist bdist_wheel
- publish: dist
artifact: sdist_wheel
- stage: "Deploy"
jobs:
- deployment: "PyPI"
pool:
vmImage: "ubuntu-18.04"
environment: "deploy"
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: sdist_wheel
- script: |
mkdir -p dist
mv $(Pipeline.Workspace)/sdist_wheel/* dist
displayName: "Move dist files"
- task: UsePythonVersion@0
inputs:
versionSpec: "3.8"
architecture: x64
- script: |
pip install --upgrade twine
displayName: "Generate artifacts"
- script: |
cat <<FILE >.pypirc
[distutils]
index-servers =
pypi
[pypi]
username: __token__
password: $(TOKEN_PYPI)
FILE
displayName: "Generate PyPI auth file"
- script: |
python -m twine upload --config-file .pypirc dist/*
displayName: "Upload to PyPI"
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))
- script: |
curl -X POST -d "token=$(TOKEN_RTD)" https://readthedocs.org/api/v2/webhook/pikepdf/39557/
displayName: "Trigger ReadTheDocs"
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/tags/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')))
+2 -3
View File
@@ -125,8 +125,7 @@ include:
.. envvar:: OMP_THREAD_LIMIT
Controls the number of threads Tesseract will use. OCRmyPDF will
manage this environment if it is not already set. (Currently, it will
set it to 1 because this gives the best results in testing.)
manage this environment variable if it is not already set.
For example, if you have a development build of Tesseract don't wish to
use the system installation, you can launch OCRmyPDF as follows:
@@ -315,7 +314,7 @@ message is:
.. code-block:: none
Temporary working files retained at:
/tmp/com.github.ocrmypdf.u20wpz07
/tmp/ocrmypdf.io.u20wpz07
The organization of this folder is an implementation detail and subject
to change between releases. However the general organization is that
+17 -12
View File
@@ -20,7 +20,8 @@ and largely have the same functions.
import ocrmypdf
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True)
if __name__ == '__main__': # To ensure correct behavior on Windows and macOS
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True)
With a few exceptions, all of the command line arguments are available
and may be passed as equivalent keywords.
@@ -35,33 +36,37 @@ The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line
execution. To do this, it will:
- create a monitoring thread
- create worker processes (forking itself)
- manage the signal flags of worker processes
- create worker processes (on Linux, forking itself; on Windows and macOS, by
spawning)
- manage the signal flags of its worker processes
- execute other subprocesses (forking and executing other programs)
The Python process that calls ``ocrmypdf.ocr()`` must be sufficiently
privileged to perform these actions. If it is not, ``ocrmypdf()`` will
fail.
privileged to perform these actions.
There is no currently no option to manage how jobs are scheduled other
than the argument ``jobs=`` which will limit the number of worker
processes.
Forking a child process to call ``ocrmypdf.ocr()`` is suggested. That
Creating a child process to call ``ocrmypdf.ocr()`` is suggested. That
way your application will survive and remain interactive even if
OCRmyPDF does not.
OCRmyPDF fails for any reason.
Programs that call ``ocrmypdf.ocr()`` should also install a SIGBUS signal
handler (except on Windows), to raise an exception if access to a memory
mapped file fails. OCRmyPDF may use memory mapping.
``ocrmypdf.ocr()`` will take a threading lock to prevent multiple runs of itself
in the same Python interpreter process. This is not thread-safe, because of how
OCRmyPDF's plugins and Python's library import system work. If you need to parallelize
OCRmyPDF, use processes.
.. warning::
On Windows, the script that calls ``ocrmypdf.ocr()`` must be protected
by an "ifmain" guard (``if __name__ == '__main__'``) or you must use
``ocrmypdf.ocr(...use_threads=True)``. If you do not take at least one
of these steps, Windows process semantics will prevent OCRmyPDF from working
correctly.
On Windows and macOS, the script that calls ``ocrmypdf.ocr()`` must be
protected by an "ifmain" guard (``if __name__ == '__main__'``). If you do
not take at least one of these steps, process semantics will prevent
OCRmyPDF from working correctly.
Logging
-------
+12
View File
@@ -5,6 +5,15 @@ API Reference
This page summarizes the rest of the public API. Generally speaking this
should mainly of interest to plugin developers.
ocrmypdf
========
.. autoclass:: ocrmypdf.PageContext
:members:
.. autoclass:: ocrmypdf.PdfContext
:members:
ocrmypdf.exceptions
===================
@@ -17,6 +26,9 @@ ocrmypdf.helpers
.. automodule:: ocrmypdf.helpers
:members:
:noindex: deprecated
.. autodecorator:: deprecated
ocrmypdf.hocrtransform
======================
+1 -8
View File
@@ -127,7 +127,7 @@ Users may need to customize the script to meet their requirements.
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``"
"OCR_DESKEW", "Apply deskew to crooked input PDFs"
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={"rotate_pages": true}'``.
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true}'``."
"OCR_POLL_NEW_FILE_SECONDS", "Polling interval"
"OCR_LOGLEVEL", "Level of log messages to report"
@@ -202,13 +202,6 @@ Alternatives
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
powerful alternative to ``watchmedo``.
AWS Lambda is not viable
------------------------
AWS Lambda and its equivalents have low limits on execution time and payload
size, relative to OCRmyPDF's needs. As of this writing, the request/response
payload for AWS Lambda was 6 MB, which means many PDFs will not fit.
macOS Automator
===============
+2
View File
@@ -1,3 +1,5 @@
.. _docker:
=====================
OCRmyPDF Docker image
=====================
+1 -1
View File
@@ -1,7 +1,7 @@
OCRmyPDF documentation
======================
OCRmyPDF adds an optical charcter recognition (OCR) text layer to scanned PDF
OCRmyPDF adds an optical character recognition (OCR) text layer to scanned PDF
files, allowing them to be searched.
PDF is the best format for storing and exchanging scanned documents.
+79 -47
View File
@@ -12,19 +12,21 @@ system/platform. This version may be out of date, however.
These platforms have one-liner installs:
+-----------------------------+-------------------------------+
| Debian, Ubuntu | ``apt install ocrmypdf`` |
+-----------------------------+-------------------------------+
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
+-----------------------------+-------------------------------+
| Fedora | ``dnf install ocrmypdf`` |
+-----------------------------+-------------------------------+
| macOS | ``brew install ocrmypdf`` |
+-----------------------------+-------------------------------+
| LinuxBrew | ``brew install ocrmypdf`` |
+-----------------------------+-------------------------------+
| FreeBSD | ``pkg install py37-ocrmypdf`` |
+-----------------------------+-------------------------------+
+-------------------------------+-------------------------------+
| Debian, Ubuntu | ``apt install ocrmypdf`` |
+-------------------------------+-------------------------------+
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
+-------------------------------+-------------------------------+
| Fedora | ``dnf install ocrmypdf`` |
+-------------------------------+-------------------------------+
| macOS | ``brew install ocrmypdf`` |
+-------------------------------+-------------------------------+
| LinuxBrew | ``brew install ocrmypdf`` |
+-------------------------------+-------------------------------+
| FreeBSD | ``pkg install py37-ocrmypdf`` |
+-------------------------------+-------------------------------+
| Conda (WSL, macOS, Linux) | ``conda install ocrmypdf`` |
+-------------------------------+-------------------------------+
More detailed procedures are outlined below. If you want to do a manual
install, or install a more recent version than your platform provides, read on.
@@ -54,6 +56,9 @@ Debian and Ubuntu 18.04 or newer
.. |ubu-2004| image:: https://repology.org/badge/version-for-repo/ubuntu_20_04/ocrmypdf.svg
:alt: Ubuntu 20.04 LTS
.. |ubu-2010| image:: https://repology.org/badge/version-for-repo/ubuntu_20_10/ocrmypdf.svg
:alt: Ubuntu 20.10
+-----------------------------------------------+
| **OCRmyPDF versions in Debian & Ubuntu** |
+-----------------------------------------------+
@@ -61,7 +66,7 @@ Debian and Ubuntu 18.04 or newer
+-----------------------------------------------+
| |deb-stable| |deb-testing| |deb-unstable| |
+-----------------------------------------------+
| |ubu-1804| |ubu-2004| |
| |ubu-1804| |ubu-2004| |ubu-2010| |
+-----------------------------------------------+
Users of Debian 9 ("stretch") or later, or Ubuntu 18.04 or later, including users
@@ -90,15 +95,15 @@ For full details on version availability for your platform, check the
automatically detect it (specifically the ``jbig2`` binary) on the
``PATH``. To add JBIG2 encoding, see :ref:`jbig2`.
Fedora 29 or newer
------------------
.. |fedora-31| image:: https://repology.org/badge/version-for-repo/fedora_31/ocrmypdf.svg
:alt: Fedora 31
Fedora
------
.. |fedora-32| image:: https://repology.org/badge/version-for-repo/fedora_32/ocrmypdf.svg
:alt: Fedora 32
.. |fedora-33| image:: https://repology.org/badge/version-for-repo/fedora_33/ocrmypdf.svg
:alt: Fedora 33
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
@@ -107,7 +112,7 @@ Fedora 29 or newer
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |fedora-31| |fedora-32| |fedora-rawhide| |
| |fedora-32| |fedora-33| |fedora-rawhide| |
+-----------------------------------------------+
Users of Fedora 29 or later may simply
@@ -158,7 +163,7 @@ To install ocrmypdf for the system:
.. code-block:: bash
sudo pip3 install ocrmypdf
pip3 install ocrmypdf
To install for the current user only:
@@ -190,7 +195,8 @@ of ocrmypdf, and install the following dependencies:
python3-reportlab \
qpdf \
tesseract-ocr \
zlib1g
zlib1g \
unpaper
We will need a newer version of ``pip`` then was available for Ubuntu 18.04:
@@ -354,7 +360,8 @@ To install OCRmyPDF for Alpine Linux:
Mageia 7
--------
Install the following dependencies:
There is no OS-level packaging available for Mageia, so you must install the
dependencies:
.. code-block:: bash
@@ -379,12 +386,16 @@ Install the following dependencies:
To install ocrmypdf for the system:
.. code-block:: bash
# As root user
pip3 install ocrmypdf
ldconfig
Or, to install for the current user only:
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
@@ -453,7 +464,7 @@ Update Homebrew:
Install or upgrade the required Homebrew packages, if any are missing.
To do this, use ``brew edit ocrmypdf`` to obtain a recent list of Homebrew
dependencies. You could also check the ``azure-pipelines.yml``.
dependencies. You could also check the ``.workflows/build.yml``.
This will include the English, French, German and Spanish language
packs. If you need other languages you can optionally install them all:
@@ -494,10 +505,6 @@ Installing on Windows
Native Windows
--------------
.. note::
It is easier to install OCRmyPDF on Windows Subsystem for Linux.
.. note::
Administrator privileges will be required for some of these steps.
@@ -508,31 +515,41 @@ You must install the following for Windows:
* Tesseract 4.0 or later
* Ghostscript 9.50 or later
You can install these with the Chocolatey package manager:
Using the `Chocolatey <https://chocolatey.org/>`_ package manager, install the
following when running in an Administrator command prompt:
* ``choco install python3``
* ``choco install --pre tesseract``
* ``choco install ghostscript``
* ``choco install pngquant`` (optional)
Also consider adding:
The commands above will install Python 3.x (latest version), Tesseract, Ghostscript
and pngquant. Chocolatey may also need to install the Windows Visual C++ Runtime
DLLs or other Windows patches, and may require a reboot.
* ``choco install pngquant``
Windows 10 64-bit and 64-bit versions of applications are recommended. Earlier
versions of Windows and 32-bit versions of these programs are not tested, and not
supported at this time.
OCRmyPDF will check for Tesseract-OCR and Ghostscript in your Program Files folder.
If they are in some other location, you may need to modify the ``PATH``
environment variable so Tesseract, Ghostscript, and other any optional executables can
be found. You can enter it in the command line or
`follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
to make the change persistent and system-wide.
You may then use pip to install ocrmypdf:
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
Administrator.):
* ``pip install ocrmypdf``
Chocolatey automatically selects appropriate versions of these applications. If you
are installing them manually, please install 64-bit versions of all applications for
64-bit Windows, or 32-bit versions of all applications for 32-bit Windows. Mixing
the "bitness" of these programs will lead to errors.
OCRmyPDF will check the Windows Registry and standard locations in your Program Files
for third party software it needs (specifically, Tesseract and Ghostscript). To
override the versions OCRmyPDF selects, you can modify the ``PATH`` environment
variable. `Follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
to change the PATH.
.. warning::
As of early 2021, users have reported problems with the Microsoft Store version of
Python and OCRmyPDF. These issues affect many other third party Python packages.
Please download Python from Python.org or Chocolatey instead, and do not use the
Microsoft Store version.
Windows Subsystem for Linux
---------------------------
@@ -603,7 +620,7 @@ However, the OCR-to-text-layer functionality is available.
Docker
------
You can also :ref:`Install the Docker <docker-install>` container on Windows. Ensure that
You can also :ref:`Install the Docker <docker>` container on Windows. Ensure that
your command prompt can run the docker "hello world" container.
Installing on FreeBSD
@@ -629,7 +646,7 @@ Installing the Docker image
For some users, installing the Docker image will be easier than
installing all of OCRmyPDF's dependencies.
See `OCRmyPDF Docker Image <docker>`__ for more information.
See :ref:`docker` for more information.
Installing with Python pip
==========================
@@ -637,7 +654,22 @@ Installing with Python pip
OCRmyPDF is delivered by PyPI because it is a convenient way to install
the latest version. However, PyPI and ``pip`` cannot address the fact
that ``ocrmypdf`` depends on certain non-Python system libraries and
programs being instsalled.
programs being installed.
.. warning::
Debian and Ubuntu users: unfortunately, Debian and Ubuntu customize
Python in non-standard ways, and the nature of these customizations
varies from release to release. This can make for a frustrating
user experience. The instructions below work on almost all platforms that
have Python installed, except for Debian and Ubuntu, where you may need
to take additional steps. For best results on Debian and Ubuntu, use the
``apt`` packages; or if these are too old, run
``apt install python3-pip python3-venv``, create a virtual environment,
and install OCRmyPDF in that environment.
`See here for more inforation on Debian-Python issues
<https://gist.github.com/tiran/2dec9e03c6f901814f6d1e8dad09528e>`__.
For best results, first install `your platform's
version <https://repology.org/metapackage/ocrmypdf/versions>`__ of
+4 -1
View File
@@ -70,4 +70,7 @@ derived Docker image as
Windows users
=============
The Tesseract installer provided by Chocolatey already includes 100 languages.
The Tesseract installer provided by Chocolatey currently includes only English language.
To install other languages, download the respective language pack (``.traineddata`` file)
from https://github.com/tesseract-ocr/tessdata/ and place it in
``C:\\Program Files\\Tesseract-OCR\\tessdata`` (or wherever Tesseract OCR is installed).
+35
View File
@@ -65,6 +65,27 @@ similar to ``pytest`` packages such as ``pytest-cov`` (the package) and
``ocrmypdf-`` (for the package name on PyPI) and ``ocrmypdf_`` (for the
module), just like pytest plugins.
Setuptools plugins
==================
You can also create a plugin that OCRmyPDF will always automatically load if both are
installed in the same virtual environment, using a setuptools entrypoint.
Your package's ``setup.py`` would need to contain the following, for a plugin
named ``ocrmypdf-exampleplugin``:
.. code-block:: python
# sample ./setup.py file
from setuptools import setup
setup(
name="ocrmypdf-exampleplugin",
packages=["exampleplugin"],
# the following makes a plugin available to pytest
entry_points={"ocrmypdf": ["exampleplugin = exampleplugin.pluginmodule"]},
)
Plugin requirements
===================
@@ -130,6 +151,18 @@ Custom command line arguments
.. autofunction:: ocrmypdf.pluginspec.check_options
Execution and progress reporting
--------------------------------
.. autoclass: ocrmypdf.pluginspec.Executor
:members:
.. autofunction:: ocrmypdf.pluginspec.get_logging_console
.. autofunction:: ocrmypdf.pluginspec.get_executor
.. autofunction:: ocrmypdf.pluginspec.get_progressbar_class
Applying special behavior before processing
-------------------------------------------
@@ -147,6 +180,8 @@ Modifying intermediate images
.. autofunction:: ocrmypdf.pluginspec.filter_page_image
.. autofunction:: ocrmypdf.pluginspec.filter_pdf_page
OCR engine
----------
+263
View File
@@ -12,6 +12,269 @@ may be unreliable. Use the API to depend on precise behavior.
The public API may be useful in scripts that launch OCRmyPDF processes or that
wish to use some of its features for working with PDFs.
v12.0.0
=======
**Breaking changes**
- Due to recent security issues in pikepdf, Pillow and reportlab, we now require
newer versions of these libraries and some of their dependencies. (If necessary,
package maintainers may override these versions at their discretion; lower
versions will often work.)
- We now use the "LeaveColorUnchanged" color conversion strategy when directing
Ghostscript to create a PDF/A. Generally this is faster than performing a
color conversion, which is not always necessary.
- OCR text is now packaged in a Form XObject. This makes it easier to isolate
OCR from other document content. However, some poor implemented PDF text
extraction algorithms may fail to find the text.
- Many API functions have stricter parameter checking or expect keyword arguments
were they previously did not.
- Some deprecated functions in ``ocrmypdf.optimize`` were removed.
- The ``ocrmypdf.leptonica`` module is now deprecated.
- Continuous integration moved to GitHub Actions.
- We no longer depend on ``pytest_helpers_namespace`` for testing.
**New features**
- New plugin hook: ``get_progressbar_class``, for progress reporting,
allowing developers to replace the standard console progress bar with some
other mechanism, such as updating a GUI progress bar.
- New plugin hook: ``get_executor``, for replacing the concurrency model.
This is primarily to support execution on AWS Lambda, which does not support
standard Python ``multiprocessing`` due to its lack of shared memory.
- New plugin hook: ``get_logging_console``, for replacing the standard
way OCRmyPDF outputs its messages.
- New plugin hook: ``filter_pdf_page``, for modifying individual PDF
pages produced by OCRmyPDF.
- Using the new plugin hooks, it is now possible to run OCRmyPDF on alternative
execution environments that do not have interprocess semaphores, such as
AWS Lambda and Android Termux.
- Continuous integration moved to GitHub Actions.
- We now generate an ARM64-compatible Docker image alongside the x64 image.
Thanks to @andkrause for contributing the change and @0x326 for review comments.
**Fixes**
- Fixed a possible deadlock on attempting to flush ``sys.stderr`` when older
versions of Leptonica are in use.
- Some worker processes inherited resources from their parents such as log
handlers that may have also lead to deadlocks. These resources are now released.
- Improvements to test coverage.
- Removed vestiges of support for Tesseract versions older than 4.0.0-beta1 (
which ships with Ubuntu 18.04).
- OCRmyPDF can now parse all of Tesseract version numbers, since several
schemes have been in use.
- Fixed an issue with parsing PDFs that contain images drawn at a scale of 0. (#761)
v11.7.3
=======
- Exclude CCITT Group 3 images from being optimized. Some libraries
OCRmyPDF uses do not seem to handle this obscure compression format properly.
You may get errors or possible corrupted output images without this fix.
v11.7.2
=======
- Updated pinned versions in main.txt, primarily to upgrade Pillow to 8.1.2, due
to recently disclosed security vulnerabilities in that software.
- The ``--sidecar`` parameter now causes an exception if set to the same file as
the input or output PDF.
v11.7.1
=======
- Some exceptions while attempting image optimization were only logged at the debug
level, causing them to be suppressed. These errors are now logged appropriately.
- Improved the error message related to ``--unpaper-args``.
- Updated documentation to mention the new conda distribution.
v11.7.0
=======
- We now support using ``--sidecar`` in conjunction with ``--pages``; these arguments
used to be mutually exclusive. (#735)
- Fixed a possible issue with PDF/A-1b generation. Acrobat complained that our PDFs use
object streams. More robust PDF/A validators like veraPDF don't consider this a
problem, but we'll honor Acrobat's objection from here on. This may increase file
size of PDF/A-1b files. PDF/A-2b files will not be affected.
v11.6.2
=======
- Fixed a regression where the wrong page orientation would be produced when using
arguments such as ``--deskew --rotate-pages`` (#730).
v11.6.1
=======
- Fixed an issue with attempting optimize unusually narrow-width images by excluding
these images from optimization (#732).
- Remove an obsolete compatibility shim for a version of pikepdf that is no longer
supported.
v11.6.0
=======
- OCRmyPDF will now automatically register plugins from the same virtual environment
with an appropriate setuptools entrypoint.
- Refactor the plugin manager to remove unnecessary complications and make plugin
registration more automatic.
- ``PageContext`` and ``PdfContext`` are now formally part of the API, as they
should have been, since they were part of ``ocrmypdf.pluginspec``.
v11.5.0
=======
- Fixed an issue where the output page size might differ by a fractional amount
due to rounding, when ``--force-ocr`` was used and the page contained objects
with multiple resolutions.
- When determining the resolution at which to rasterize a page, we now consider
printed text on the page as requiring a higher resolution. This fixes issues
with certain pages being rendered with unacceptably low resolution text, but
may increase output file sizes in some workflows where low resolution text
is acceptable.
- Added a workaround to fix an exception that occurs when trying to
``import ocrmypdf.leptonica`` on Apple ARM silicon (or potentially, other
platforms that do not permit write+executable memory).
v11.4.5
=======
- Fixed an issue where files may not be closed when the API is used.
- Improved ``setup.cfg`` with better settings for test coverage.
v11.4.4
=======
- Fixed ``AttributeError: 'NoneType' object has no attribute 'userunit'``, issue #700,
related to OCRmyPDF not properly forwarded an error message from pdfminer.six.
- Adjusted typing of some arguments.
- ``ocrmypdf.ocr`` now takes a ``threading.Lock`` for reasons outlined in the
documentation.
v11.4.3
=======
- Removed a redundant debug message.
- Test suite now asserts that most patched functions are called when they should be.
- Test suite now skips a test that fails on two particular versions of piekpdf.
v11.4.2
=======
- Fixed support for Cygwin, hopefully.
- watcher.py: Fixed an issue with the OCR_LOGLEVEL not being interpreted.
v11.4.1
=======
- Fixed an issue where invalid pages ranges passed using the ``pages`` argument,
such as "1-0" would cause unhandled exceptions.
- Accepted a user-contributed to the Synology demo script in misc/synology.py.
- Clarified documentation about change of temporary file location ``ocrmypdf.io``.
- Fixed Python wheel tag which was incorrectly set to py35 even though we long
since dropped support for Python 3.5.
v11.4.0
=======
- When looking for Tesseract and Ghostscript, we now check the Windows Registry to
see if their installers registered the location of their executables. This should
help Windows users who have installed these programs to non-standard
locations.
- We now report on the progress of PDF/A conversion, since this operation is
sometimes slow.
- Improved command line completions.
- The prefix of the temporary folder OCRmyPDF creates has been changed from
``com.github.ocrmypdf`` to ``ocrmypdf.io``. Scripts that chose to depend on this
prefix may need to be adjusted. (This has always been an implementation detail so is
not considered part of the semantic versioning "contract".)
- Fixed issue #692, where a particular file with malformed fonts would flood an
internal message cue by generating so many debug messages.
- Fixed an exception on processing hOCR files with no page record. Tesseract
is not known to generate such files.
v11.3.4
=======
- Fixed an error message 'called readLinearizationData for file that is not
linearized' that may occur when pikepdf 2.1.0 is used. (Upgrading to pikepdf
2.1.1 also fixes the issue.)
- File watcher now automatically includes ``.PDF`` in addition to ``.pdf`` to
better support case sensitive file systems.
- Some documentation and comment improvements.
v11.3.3
=======
- If unpaper outputs non-UTF-8 data, quietly fix this rather than choke on the
conversion. (Possibly addresses #671.)
v11.3.2
=======
- Explicitly require pikepdf 2.0.0 or newer when running on Python 3.9. (There are
concerns about the stability of pybind11 2.5.x with Python 3.9, which is used in
pikepdf 1.x.)
- Fixed another issue related to page rotation.
- Fixed an issue where image marked as image masks were not properly considered
as optimization candidates.
- On some systems, unpaper seems to be unable to process the PNGs we offer it
as input. We now convert the input to PNM format, which unpaper always accepts.
Fixes #665 and #667.
- DPI sent to unpaper is now rounded to a more reasonable number of decimal digits.
- Debug and error messages from unpaper were being suppressed.
- Some documentation tweaks.
v11.3.1
=======
- Declare support for new versions: pdfminer.six 20201018 and pikepdf 2.x
- Fix warning related to ``--pdfa-image-compression`` that appears at the wrong
time.
v11.3.0
=======
- The "OCR" step is describing as "Image processing" in the output messages when
OCR is disabled, to better explain the application's behavior.
- Debug logs are now only created when run as a command line, and not when OCR
is performed for an API call. It is the calling application's responsibility
to set up logging.
- For PDFs with a low number of pages, we gathered information about the input PDF
in a thread rather than process (when there are more pages). When run as a
thread, we did not close the file handle to the working PDF, leaking one file
handle per call of ``ocrmypdf.ocr``.
- Fixed an issue where debug messages send by child worker processes did not match
the log settings of parent process, causing messages to be dropped. This affected
macOS and Windows only where the parent process is not forked.
- Fixed the hookspec of rasterize_pdf_page to remove default parameters that
were not handled in an expected way by pluggy.
- Fixed another issue with automatic page rotation (#658) due to the issue above.
v11.2.1
=======
- Fixed an issue where optimization of a 1-bit image with a color palette or
associated ICC that was optimized to JBIG2 could have its colors inverted.
v11.2.0
=======
- Fixed an issue with optimizing PNG-type images that had soft masks or image masks.
This is a regression introduced in (or about) v11.1.0.
- Improved type checking of the ``plugins`` parameter for the ``ocrmypdf.ocr``
API call.
v11.1.2
=======
- Fixed hOCR renderer writing the text in roughly reverse order. This should not
affect reasonably smart PDF readers that properly locate the position of all
text, but may confuse those that rely on the order of objects in the content
stream. (#642)
v11.1.1
=======
+3 -2
View File
@@ -59,7 +59,8 @@ complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "se
function __fish_ocrmypdf_pdf_renderer
echo -e "auto\t"(_ "auto select PDF renderer")
echo -e "hocr\t"(_ "use hocr renderer")
echo -e "hocr\t"(_ "use hOCR renderer")
echo -e "hocrdebug\t"(_ "uses hOCR renderer in debug mode, showing recognized text")
echo -e "sandwich\t"(_ "use sandwich renderer")
end
complete -c ocrmypdf -x -l pdf-renderer -a '(__fish_ocrmypdf_pdf_renderer)' -d "select PDF renderer options"
@@ -135,4 +136,4 @@ complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf; __fish_complete_suffix .PDF; __fish_complete_suffix .jpg; __fish_complete_suffix .png)"
+1 -1
View File
@@ -9,7 +9,7 @@ services:
- "/media/scan:/input"
- "/mnt/scan:/output"
environment:
- OCR_OUTPUT_DIRECTORY_YEAR_MONT=0
- OCR_OUTPUT_DIRECTORY_YEAR_MONTH=0
user: "<SET TO YOUR USER ID>:<SET TO YOUR GROUP ID>"
entrypoint: python3
command: watcher.py
+19
View File
@@ -18,6 +18,25 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
An example of an OCRmyPDF plugin.
This plugin adds two new command line arguments
--grayscale-ocr: converts the image to grayscale before performing OCR on it
(This is occasionally useful for images whose color confounds OCR. It only
affects the image shown to OCR. The image is not saved.)
--mono-page: converts pages all pages in the output file to black and white
To use this from the command line:
ocrmypdf --plugin path/to/example_plugin.py --mono-page input.pdf output.pdf
To use this as an API:
import ocrmypdf
ocrmypdf.ocr('input.pdf', 'output.pdf',
plugins=['path/to/example_plugin.py'], mono_page=True
)
"""
import logging
from PIL import Image
+3 -1
View File
@@ -79,8 +79,10 @@ for dir_name, subdirs, file_list in os.walk(start_dir):
stdout=output_file,
stderr=subprocess.PIPE,
check=False,
text=True,
errors='ignore',
)
logging.info(proc.stderr.read())
logging.info(proc.stderr)
os.chmod(full_path_ocr, 0o664)
os.chmod(full_path, 0o664)
full_path_ocr_archive = sys.argv[2]
+9 -4
View File
@@ -44,8 +44,8 @@ DESKEW = bool(os.getenv('OCR_DESKEW', ''))
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
USE_POLLING = bool(os.getenv('OCR_USE_POLLING', ''))
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO').upper()
PATTERNS = ['*.pdf']
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO')
PATTERNS = ['*.pdf', '*.PDF']
log = logging.getLogger('ocrmypdf-watcher')
@@ -117,7 +117,12 @@ class HandleObserverEvent(PatternMatchingEventHandler):
def main():
ocrmypdf.configure_logging(
verbosity=ocrmypdf.Verbosity.default, manage_root_logger=True
verbosity=(
ocrmypdf.Verbosity.default
if LOGLEVEL != 'DEBUG'
else ocrmypdf.Verbosity.debug
),
manage_root_logger=True,
)
log.setLevel(LOGLEVEL)
log.info(
@@ -135,7 +140,7 @@ def main():
f"ARGS: {OCR_JSON_SETTINGS}\n"
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
f"USE_POLLING: {USE_POLLING}\n"
f"LOGLEVEL: {LOGLEVEL}\n"
f"LOGLEVEL: {LOGLEVEL}"
)
if 'input_file' in OCR_JSON_SETTINGS or 'output_file' in OCR_JSON_SETTINGS:
+8 -8
View File
@@ -1,12 +1,12 @@
# 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.14.0
coloredlogs == 14.0 # technically optional
img2pdf == 0.3.6
pdfminer.six == 20200517
pikepdf == 1.16.1
cffi == 1.14.5
coloredlogs == 15.0 # technically optional
img2pdf == 0.4.0
pdfminer.six == 20201018
pikepdf == 2.10.0
pluggy == 0.13.1
Pillow == 7.1.2
reportlab == 3.5.42
tqdm == 4.46.1
Pillow == 8.1.2
reportlab == 3.5.66
tqdm == 4.59.0
+3 -4
View File
@@ -1,7 +1,6 @@
pytest >= 5.0.0
pytest-helpers-namespace >= 2019.1.8
pytest-xdist >= 1.31.0
pytest-cov >= 2.10.0
pytest >= 6.0.0
pytest-xdist >= 2.2.0
pytest-cov >= 2.11.1
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
# or brew install exempi
#PyMuPDF == 1.13.4 # optional
+1 -1
View File
@@ -1 +1 @@
watchdog == 0.10.2
watchdog == 1.0.2
+28 -1
View File
@@ -1,5 +1,5 @@
[bdist_wheel]
python-tag = py35
python-tag = py36
[aliases]
test=pytest
@@ -15,6 +15,8 @@ filterwarnings =
ignore:.*XMLParser.*:DeprecationWarning
markers =
slow
addopts =
-n auto
[isort]
multi_line_output=3
@@ -27,3 +29,28 @@ known_third_party = PIL,_cffi_backend,cffi,flask,img2pdf,pdfminer,pikepdf,pkg_re
[metadata]
license_file = LICENSE
[coverage:paths]
source =
src/ocrmypdf
[coverage:run]
branch = true
parallel = true
concurrency = multiprocessing
[coverage:report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain if non-runnable code isn't run:
if 0:
if False:
if __name__ == .__main__.:
if TYPE_CHECKING:
+7 -11
View File
@@ -17,10 +17,6 @@ if sys.version_info < (3, 6):
print("Python 3.6 or newer is required", file=sys.stderr)
sys.exit(1)
if 'upload' in sys.argv[1:]:
print('Use twine to upload the package - setup.py upload is insecure')
sys.exit(1)
tests_require = open('requirements/test.txt', encoding='utf-8').read().splitlines()
@@ -63,7 +59,6 @@ setup(
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
],
@@ -73,16 +68,17 @@ setup(
'cffi >= 1.9.1', # must be a setup and install requirement
'coloredlogs >= 14.0', # strictly optional
'img2pdf >= 0.3.0, < 0.5', # pure Python, so track HEAD closely
'pdfminer.six >= 20191110, != 20200720, <= 20200726',
'pikepdf >= 1.14.0, < 2',
'Pillow >= 7.0.0',
'pluggy >= 0.13.0',
'reportlab >= 3.3.0', # oldest released version with sane image handling
'pdfminer.six >= 20191110, != 20200720, <= 20201018',
"pikepdf >= 2.10.0",
'Pillow >= 8.1.2',
'pluggy >= 0.13.0, < 1.0',
'reportlab >= 3.5.66',
'setuptools',
'tqdm >= 4',
],
tests_require=tests_require,
entry_points={'console_scripts': ['ocrmypdf = ocrmypdf.__main__:run']},
package_data={'ocrmypdf': ['data/sRGB.icc']},
package_data={'ocrmypdf': ['data/sRGB.icc', 'py.typed']},
include_package_data=True,
zip_safe=False,
project_urls={
+35
View File
@@ -0,0 +1,35 @@
# Release checklist
## Patch release
- Check `pytest`
- Update release notes
## Minor release
## Major release
- Run `pre-commit autoupdate`
- Check README.md
- Check setup.py
- Are classifiers up to date?
- Is `python_requires` correct?
- Python 3.6 is EOL on December 2021-12. Could drop support then.
- Can we tighten any `install_requires` dependencies?
- Search for old version shims we can remove
- "shim"
- ` pikepdf.__version__`
- Search for deprecation: search all files for deprec*, etc.
- Check requirements/*
- Delete `tests/cache`, do `pytest --runslow`, and update cache.
- Do `pytest --cov-report html`
+2
View File
@@ -8,6 +8,8 @@
from pluggy import HookimplMarker as _HookimplMarker
from ocrmypdf import helpers, hocrtransform, leptonica, pdfa, pdfinfo
from ocrmypdf._concurrent import Executor
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._version import PROGRAM_NAME, __version__
from ocrmypdf.api import Verbosity, configure_logging, ocr
from ocrmypdf.exceptions import (
+4 -1
View File
@@ -47,7 +47,10 @@ def run(args=None):
verbosity = Verbosity.quiet
options.progress_bar = False
configure_logging(
verbosity, progress_bar_friendly=options.progress_bar, manage_root_logger=True
verbosity,
progress_bar_friendly=options.progress_bar,
manage_root_logger=True,
plugin_manager=plugin_manager,
)
log.debug('ocrmypdf %s', __version__)
try:
+117 -122
View File
@@ -4,135 +4,130 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import logging
import logging.handlers
import multiprocessing
import os
import signal
import sys
import threading
from multiprocessing import Pool as ProcessPool
from multiprocessing.dummy import Pool as ThreadPool
from abc import ABC, abstractmethod
from typing import Callable, Iterable, Optional
from tqdm import tqdm
from ocrmypdf.exceptions import InputFileError
def _task_noop(*_args, **_kwargs):
return
def log_listener(queue):
"""Listen to the worker processes and forward the messages to logging
class NullProgressBar:
def __init__(self, **kwargs):
pass
For simplicity this is a thread rather than a process. Only one process
should actually write to sys.stderr or whatever we're using, so if this is
made into a process the main application needs to be directed to it.
def __enter__(self):
return self
See https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
def __exit__(self, exc_type, exc_value, traceback):
return False
def update(self, _arg=None):
return
class Executor(ABC):
pool_lock = threading.Lock()
pbar_class = NullProgressBar
def __init__(self, *, pbar_class=None):
if pbar_class:
self.pbar_class = pbar_class
def __call__(
self,
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
worker_initializer: Optional[Callable] = None,
task: Optional[Callable] = None,
task_arguments: Optional[Iterable] = None,
task_finished: Optional[Callable] = None,
) -> None:
"""
Set up parallel execution and progress reporting.
Args:
use_threads: If ``False``, the workload is the sort that will benefit from
running in a multiprocessing context (for example, it uses Python
heavily, and parallelizing it with threads is not expected to be
performant).
max_workers: The maximum number of workers that should be run.
tdqm_kwargs: Arguments to set up the progress bar.
worker_initializer: Called when a worker is initialized, in the worker's
execution context. If the child workers are processes, it must be
possible to marshall/pickle the worker initializer.
``functools.partial`` can be used to bind parameters.
task: Called when the worker starts a new task, in the worker's execution
context. Must be possible to marshall to the worker.
task_finished: Called when a worker finishes a task, in the parent's
context.
task_arguments: An iterable that generates a group of parameters for each
task. This runs in the parent's context, but the parameters must be
marshallable to the worker.
"""
if not task_arguments:
return # Nothing to do!
if not worker_initializer:
worker_initializer = _task_noop
if not task_finished:
task_finished = _task_noop
if not task:
task = _task_noop
with self.pool_lock:
self._execute(
use_threads=use_threads,
max_workers=max_workers,
tqdm_kwargs=tqdm_kwargs,
worker_initializer=worker_initializer,
task=task,
task_arguments=task_arguments,
task_finished=task_finished,
)
@abstractmethod
def _execute(
self,
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
worker_initializer: Callable,
task: Callable,
task_arguments: Iterable,
task_finished: Callable,
):
"""Custom executors should override this method."""
def setup_executor(plugin_manager) -> Executor:
pbar_class = plugin_manager.hook.get_progressbar_class()
return plugin_manager.hook.get_executor(progressbar_class=pbar_class)
class SerialExecutor(Executor):
"""Implements a purely sequential executor using the parallel protocol.
The current process/thread will be the worker that executes all tasks
in order. As such, ``worker_initializer`` will never be called.
"""
while True:
try:
record = queue.get()
if record is None:
break
logger = logging.getLogger(record.name)
logger.handle(record)
except Exception: # pylint: disable=broad-except
import traceback # pylint: disable=import-outside-toplevel
print("Logging problem", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
def process_sigbus(*args):
raise InputFileError("A worker process lost access to an input file")
def process_init(queue, user_init):
"""Initialize a process pool worker"""
# Ignore SIGINT (our parent process will kill us gracefully)
signal.signal(signal.SIGINT, signal.SIG_IGN)
# Install SIGBUS handler (so our parent process can abort somewhat gracefully)
if hasattr(signal, 'SIGBUS'):
signal.signal(signal.SIGBUS, process_sigbus)
# Reconfigure the root logger for this process to send all messages to a queue
h = logging.handlers.QueueHandler(queue)
root = logging.getLogger()
root.handlers = []
root.addHandler(h)
if user_init:
user_init()
def thread_init(_queue, user_init):
# As a thread, block SIGBUS so the main thread deals with it...
if hasattr(signal, 'SIGBUS'):
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGBUS})
if user_init:
user_init()
def exec_progress_pool(
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
task_initializer: Optional[Callable] = None,
task: Optional[Callable] = None,
task_arguments: Optional[Iterable] = None,
task_finished: Optional[Callable] = None,
):
log_queue: multiprocessing.Queue = multiprocessing.Queue(-1)
listener = threading.Thread(target=log_listener, args=(log_queue,))
if use_threads:
pool_class = ThreadPool
initializer = thread_init
else:
pool_class = ProcessPool
initializer = process_init
listener.start()
with tqdm(**tqdm_kwargs) as pbar:
pool = pool_class(
processes=max_workers,
initializer=initializer,
initargs=(log_queue, task_initializer),
)
try:
results = pool.imap_unordered(task, task_arguments)
while True:
try:
result = results.next()
if task_finished:
task_finished(result, pbar)
else:
pbar.update()
except StopIteration:
break
except KeyboardInterrupt:
# Terminate pool so we exit instantly
pool.terminate()
# Don't try listener.join() here, will deadlock
raise
except Exception:
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
# Unless inside pytest, exit immediately because no one wants
# to wait for child processes to finalize results that will be
# thrown away. Inside pytest, we want child processes to exit
# cleanly so that they output an error messages or coverage data
# we need from them.
pool.terminate()
raise
finally:
# Terminate log listener
log_queue.put_nowait(None)
pool.close()
pool.join()
listener.join()
def _execute(
self,
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
worker_initializer: Callable,
task: Callable,
task_arguments: Iterable,
task_finished: Callable,
): # pylint: disable=unused-argument
with self.pbar_class(**tqdm_kwargs) as pbar:
for args in task_arguments:
result = task(args)
task_finished(result, pbar)
+70 -38
View File
@@ -15,34 +15,34 @@ from os import fspath
from pathlib import Path
from shutil import which
from subprocess import PIPE, CalledProcessError
from typing import Optional, cast
from typing import Optional
from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.helpers import Resolution
from ocrmypdf.subprocess import get_version, run
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
log = logging.getLogger(__name__)
missing_gs_error = """
---------------------------------------------------------------------
This error normally occurs when ocrmypdf find can't Ghostscript.
Please ensure Ghostscript is installed and its location is added to
the system PATH environment variable.
For details see:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
_gswin = None
if os.name == 'nt':
_gswin = which('gswin64c')
if not _gswin:
_gswin = which('gswin32c')
if not _gswin:
raise MissingDependencyError(
"""
---------------------------------------------------------------------
This error normally occurs when ocrmypdf can't Ghostscript. Please
ensure Ghostscript is installed and its location is added to the
system PATH environment variable.
For details see:
https://ocrmypdf.readthedocs.io/en/latest/installation.html
---------------------------------------------------------------------
"""
)
raise MissingDependencyError(missing_gs_error)
_gswin = Path(_gswin).stem
GS = _gswin if _gswin else 'gs'
@@ -56,16 +56,14 @@ def version():
def jpeg_passthrough_available() -> bool:
"""Returns True if the installed version of Ghostscript supports JPEG passthru
Prior to 9.23, Ghostscript decode and re-encoded JPEGs internally. In 9.23
Prior to 9.23, Ghostscript decoded and re-encoded JPEGs internally. In 9.23
it gained the ability to keep JPEGs unmodified. However, the 9.23
implementation was buggy and would deletes the last two bytes of images in
some cases, as reported here.
https://bugs.ghostscript.com/show_bug.cgi?id=699216
The issue was fixed for 9.24, hence that is the first version we consider
the feature available. (However, we don't use 9.24 at all, so the first
version that allows JPEG passthrough is 9.25.
the feature available. (Ghostscript 9.24 has its own problems is blacklisted.)
"""
return version() >= '9.24'
@@ -81,8 +79,8 @@ def rasterize_pdf(
raster_device: str,
raster_dpi: Resolution,
pageno: int = 1,
page_dpi: Resolution = None,
rotation: int = None,
page_dpi: Optional[Resolution] = None,
rotation: Optional[int] = None,
filter_vector: bool = False,
):
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units."""
@@ -122,8 +120,6 @@ def rasterize_pdf(
stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr):
log.error(stderr)
elif stderr:
log.debug(stderr)
with Image.open(BytesIO(p.stdout)) as im:
if rotation is not None:
@@ -141,13 +137,44 @@ def rasterize_pdf(
im.save(fspath(output_file), dpi=page_dpi)
class GhostscriptFollower:
re_process = re.compile(r"Processing pages \d+ through (\d+).")
re_page = re.compile(r"Page (\d+)")
def __init__(self, progressbar_class):
self.count = 0
self.progressbar_class = progressbar_class
self.progressbar = None
def __call__(self, line):
if not self.progressbar_class:
return
if not self.progressbar:
m = self.re_process.match(line.strip())
if m:
self.count = int(m.group(1))
self.progressbar = self.progressbar_class(
total=self.count, desc="PDF/A conversion", unit='page'
)
return
else:
m = self.re_page.match(line.strip())
if m:
self.progressbar.update()
def generate_pdfa(
pdf_pages,
output_file: os.PathLike,
*,
compression: str,
pdf_version: str = '1.5',
pdfa_part: str = '2',
progressbar_class=None,
):
# Ghostscript's compression is all or nothing. We can either force all images
# to JPEG, force all to Flate/PNG, or let it decide how to encode the images.
# In most case it's best to let it decide.
compression_args = []
if compression == 'jpeg':
compression_args = [
@@ -169,14 +196,16 @@ def generate_pdfa(
"-dAutoFilterGrayImages=true",
]
strategy = 'LeaveColorUnchanged'
# Older versions of Ghostscript expect a leading slash in
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
# git commit fe1c025d.
strategy = 'RGB' if version() >= '9.19' else '/RGB'
strategy = ('/' + strategy) if version() < '9.19' else strategy
if version() == '9.23':
# 9.23: new feature JPEG passthrough is broken in some cases, best to
# disable it always
# 9.23: added JPEG passthrough as a new feature, but with a bug that
# incorrectly formats some images. Fixed as of 9.24. So we disable this
# feature for 9.23.
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
compression_args.append('-dPassThroughJPEGImages=false')
@@ -186,7 +215,6 @@ def generate_pdfa(
args_gs = (
[
GS,
"-dQUIET",
"-dBATCH",
"-dNOPAUSE",
"-dSAFER",
@@ -206,16 +234,28 @@ def generate_pdfa(
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try:
with Path(output_file).open('wb') as output:
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
p = run_polling_stderr(
args_gs,
stdout=output,
stderr=PIPE,
check=True,
text=True,
encoding='utf-8',
errors='replace',
callback=GhostscriptFollower(progressbar_class),
)
except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create
# PDF/A - check PDF/A status elsewhere
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
log.error(e.stderr)
raise SubprocessOutputError('Ghostscript PDF/A rendering failed') from e
else:
stderr = p.stderr.decode('utf-8', errors='replace')
stderr = p.stderr
# If there is an error we log the whole stderr, except for filtering
# duplicates.
if _gs_error_reported(stderr):
last_part = None
repcount = 0
@@ -228,11 +268,3 @@ def generate_pdfa(
else:
repcount += 1
last_part = part
elif 'overprint mode not set' in stderr:
# Unless someone is going to print PDF/A documents on a
# magical sRGB printer I can't see the removal of overprinting
# being a problem....
log.debug(
"Ghostscript had to remove PDF 'overprinting' from the "
"input file to complete PDF/A conversion. "
)
+8
View File
@@ -41,9 +41,17 @@ def convert_group(*, cwd, infiles, out_prefix):
return proc
def convert_group_mp(args):
return convert_group(cwd=args[0], infiles=args[1], out_prefix=args[2])
def convert_single(*, cwd, infile, outfile):
args = ['jbig2', '-p', infile]
with open(outfile, 'wb') as fstdout:
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
proc.check_returncode()
return proc
def convert_single_mp(args):
return convert_single(cwd=args[0], infile=args[1], outfile=args[2])
+4 -2
View File
@@ -9,10 +9,8 @@
from contextlib import contextmanager
from io import BytesIO
from os import fspath
from pathlib import Path
from subprocess import PIPE
from tempfile import NamedTemporaryFile
from PIL import Image
@@ -61,3 +59,7 @@ def quantize(input_file: Path, output_file: Path, quality_min: int, quality_max:
if result.returncode == 0:
# input_file could be the same as output_file, so we defer the write
output_file.write_bytes(result.stdout)
def quantize_mp(args):
return quantize(*args)
+31 -25
View File
@@ -9,12 +9,14 @@
import logging
import os
import re
import shutil
from collections import namedtuple
from distutils.version import StrictVersion
from os import fspath
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
from typing import List
from typing import List, Optional
from PIL import Image
@@ -53,31 +55,29 @@ class TesseractLoggerAdapter(logging.LoggerAdapter):
return '[tesseract] %s' % (msg), kwargs
class TesseractVersion(StrictVersion):
version_re = re.compile(
r'''
^(\d+) \. (\d+) (\. (\d+))? # groups: 1/major, 2/minor, 3/[skip], 4/patch
[-]? # optional hyphen separator
(?:(alpha|beta|rc|dev)[.\-\ ]?(\d+)?)? # 5/prerelease, 6/prerelease_num
$
''',
re.VERBOSE | re.ASCII,
)
def parse(self, vstring):
try:
super().parse(vstring)
except TypeError as e:
if 'int() argument must be a string' in str(e):
super().parse(vstring + '0')
def version():
return get_version('tesseract', regex=r'tesseract\s(.+)')
def has_textonly_pdf(langs=None):
"""Does Tesseract have textonly_pdf capability?
Available in v4.00.00alpha since January 2017. Best to
parse the parameter list.
"""
args_tess = tess_base_args(langs, engine_mode=None) + ['--print-parameters', 'pdf']
params = ''
try:
proc = run(args_tess, check=True, stdout=PIPE, stderr=STDOUT)
params = proc.stdout
except CalledProcessError as e:
raise MissingDependencyError(
"Could not --print-parameters from tesseract. This can happen if the "
"TESSDATA_PREFIX environment is not set to a valid tessdata folder. "
) from e
if b'textonly_pdf' in params:
return True
return False
def has_user_words():
"""Does Tesseract have --user-words capability?
@@ -100,7 +100,12 @@ def get_languages():
args_tess = ['tesseract', '--list-langs']
try:
proc = run(
args_tess, universal_newlines=True, stdout=PIPE, stderr=STDOUT, check=True
args_tess,
text=True,
stdout=PIPE,
stderr=STDOUT,
logs_errors_to_stdout=True,
check=True,
)
output = proc.stdout
except CalledProcessError as e:
@@ -113,7 +118,7 @@ def get_languages():
return set(lang.strip() for lang in rest)
def tess_base_args(langs: List[str], engine_mode: int) -> List[str]:
def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
args = ['tesseract']
if langs:
args.extend(['-l', '+'.join(langs)])
@@ -122,7 +127,7 @@ def tess_base_args(langs: List[str], engine_mode: int) -> List[str]:
return args
def get_orientation(input_file: Path, engine_mode: int, timeout: float):
def get_orientation(input_file: Path, engine_mode: Optional[int], timeout: float):
args_tesseract = tess_base_args(['osd'], engine_mode) + [
'--psm',
'0',
@@ -216,6 +221,7 @@ def _generate_null_hocr(output_hocr, output_text, image):
def generate_hocr(
*,
input_file: Path,
output_hocr: Path,
output_text: Path,
+44 -39
View File
@@ -13,10 +13,11 @@
import logging
import os
import shlex
from decimal import Decimal
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError
from subprocess import PIPE, STDOUT
from tempfile import TemporaryDirectory
from typing import Tuple
from typing import List, Optional, Tuple, Union
from PIL import Image
@@ -24,10 +25,12 @@ from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.subprocess import get_version
from ocrmypdf.subprocess import run as external_run
DecFloat = Union[Decimal, float]
log = logging.getLogger(__name__)
def version():
def version() -> str:
return get_version('unpaper')
@@ -53,23 +56,25 @@ def _setup_unpaper_io(tmpdir: Path, input_file: Path) -> Tuple[Path, Path]:
except KeyError:
raise MissingDependencyError(
"Failed to convert image to a supported format."
) from e
) from None
if im_modified or input_file.suffix != '.png':
input_png = tmpdir / 'input.png'
im.save(input_png, format='PNG', compress_level=1)
if im_modified or input_file.suffix != '.pnm':
input_pnm = tmpdir / 'input.pnm'
im.save(input_pnm, format='PPM')
else:
# No changes, PNG input, just use the file we already have
input_png = input_file
input_pnm = input_file
output_pnm = tmpdir / f'output{suffix}'
return input_png, output_pnm
return input_pnm, output_pnm
def run(input_file, output_file, dpi, mode_args):
args_unpaper = ['unpaper', '-v', '--dpi', str(dpi)] + mode_args
def run(
input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: List[str]
) -> None:
args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args
with TemporaryDirectory() as tmpdir:
input_png, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file)
input_pnm, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file)
# To prevent any shenanigans from accepting arbitrary parameters in
# --unpaper-args, we:
@@ -78,41 +83,41 @@ def run(input_file, output_file, dpi, mode_args):
# 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([os.fspath(input_png), os.fspath(output_pnm)])
args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)])
external_run(
args_unpaper,
close_fds=True,
check=True,
stderr=STDOUT, # unpaper writes logging output to stdout and stderr
stdout=PIPE, # and cannot send file output to stdout
cwd=tmpdir,
logs_errors_to_stdout=True,
)
try:
proc = external_run(
args_unpaper,
check=True,
close_fds=True,
universal_newlines=True,
stderr=STDOUT, # unpaper writes logging output to stdout and stderr
cwd=tmpdir, # and cannot send file output to stdout
stdout=PIPE,
)
except CalledProcessError as e:
log.debug(e.stderr)
raise e from e
else:
log.debug(proc.stderr)
try:
with Image.open(output_pnm) as imout:
imout.save(output_file, dpi=(dpi, dpi))
except (FileNotFoundError, OSError):
raise SubprocessOutputError(
"unpaper: failed to produce the expected output file. "
+ " Called with: "
+ str(args_unpaper)
) from None
with Image.open(output_pnm) as imout:
imout.save(output_file, dpi=(dpi, dpi))
except (FileNotFoundError, OSError):
raise SubprocessOutputError(
"unpaper: failed to produce the expected output file. "
+ " Called with: "
+ str(args_unpaper)
) from None
def validate_custom_args(args: str):
def validate_custom_args(args: str) -> List[str]:
unpaper_args = shlex.split(args)
if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args):
raise ValueError('No filenames allowed in --unpaper-args')
return unpaper_args
def clean(input_file, output_file, dpi, unpaper_args=None):
def clean(
input_file: Path,
output_file: Path,
*,
dpi: DecFloat,
unpaper_args: Optional[List[str]] = None,
):
default_args = [
'--layout',
'none',
@@ -126,4 +131,4 @@ def clean(input_file, output_file, dpi, unpaper_args=None):
]
if not unpaper_args:
unpaper_args = default_args
run(input_file, output_file, dpi, unpaper_args)
run(input_file, output_file, dpi=dpi, mode_args=unpaper_args)
+53 -36
View File
@@ -6,33 +6,39 @@
import logging
import uuid
from contextlib import suppress
from pathlib import Path
from typing import Optional
import pikepdf
from pikepdf.objects import Dictionary, Name
log = logging.getLogger(__name__)
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"""
def _ensure_dictionary(obj, name):
if name not in obj:
obj[name] = Dictionary({})
return obj[name]
if '/Resources' not in page:
page['/Resources'] = pikepdf.Dictionary({})
resources = page['/Resources']
try:
fonts = resources['/Font']
except KeyError:
fonts = pikepdf.Dictionary({})
def _update_resources(*, obj, font, font_key, procset):
"""Update this obj's fonts with a reference to the Glyphless font.
obj can be a page or Form XObject.
"""
resources = _ensure_dictionary(obj, Name.Resources)
fonts = _ensure_dictionary(resources, Name.Font)
if font_key is not None and font_key not in fonts:
fonts[font_key] = font
resources['/Font'] = fonts
# Reassign /ProcSet to one that just lists everything - ProcSet is
# obsolete and doesn't matter but recommended for old viewer support
resources['/ProcSet'] = procset
if procset:
resources['/ProcSet'] = procset
def strip_invisible_text(pdf, page):
@@ -109,6 +115,7 @@ class OcrGrafter:
if textpdf and not self.font:
self.font, self.font_key = self._find_font(textpdf)
emplaced_page = False
content_rotation = self.pdfinfo[pageno].rotation
path_image = Path(image).resolve() if image else None
if path_image is not None and path_image != self.path_base:
@@ -122,24 +129,21 @@ class OcrGrafter:
local_image_page = self.pdf_base.pages[-1]
self.pdf_base.pages[pageno].emplace(local_image_page)
del self.pdf_base.pages[-1]
# The pdf_image_page will always be created with any /Rotate applied
# applied already
content_rotation = 0
emplaced_page = True
if content_rotation != 0:
# Text can be misaligned on a /Rotate'd page.
# That is because we rasterize pages with /Rotate applied,
# so that the OCR image text is upright and comes back upright.
text_misaligned = (autorotate_correction - content_rotation) % 360
log.debug(
f"Text rotation: (autorotate, content) -> text misalignment = "
f"({autorotate_correction}, {content_rotation}) -> {text_misaligned}"
)
else:
text_misaligned = 0
# Calculate if the text is misaligned compared to the content
if emplaced_page:
content_rotation = autorotate_correction
text_rotation = autorotate_correction
text_misaligned = (text_rotation - content_rotation) % 360
log.debug(
f"Text rotation: (text, autorotate, content) -> text misalignment = "
f"({text_rotation}, {autorotate_correction}, {content_rotation}) -> {text_misaligned}"
)
if textpdf and self.font:
# Graft the text layer onto this page, whether new or old
# Graft the text layer onto this page, whether new or old, possibly
# rotating the text layer by the amount is misaligned.
strip_old = self.context.options.redo_ocr
self._graft_text_layer(
page_num=pageno + 1,
@@ -151,14 +155,14 @@ class OcrGrafter:
strip_old_text=strip_old,
)
# Correct the page rotation
# Correct the overall page rotation if needed, now that the text and content
# are aligned
page_rotation = (content_rotation - autorotate_correction) % 360
self.pdf_base.pages[pageno].Rotate = page_rotation
log.debug(
f"Page rotation: (content, auto) -> page = "
f"({content_rotation}, {autorotate_correction}) -> {page_rotation}"
)
if self.emplacements % MAX_REPLACE_PAGES == 0:
self.save_and_reload()
@@ -171,13 +175,13 @@ class OcrGrafter:
"""
page0 = self.pdf_base.pages[0]
_update_page_resources(
page=page0, font=self.font, font_key=self.font_key, procset=self.procset
_update_resources(
obj=page0, font=self.font, font_key=self.font_key, procset=self.procset
)
# 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} is the opened file we were updating
# {interim_count - 1} can be deleted
# {interim_count + 1} is the new file will produce and open
old_file = self.output_file.with_suffix(f'.working{self.interim_count - 1}.pdf')
@@ -212,6 +216,7 @@ class OcrGrafter:
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
except (AttributeError, IndexError, KeyError):
return None, None
pdf_text_font = None
for f in possible_font_names:
pdf_text_font = pdf_text_fonts.get(f, None)
if pdf_text_font is not None:
@@ -281,17 +286,29 @@ class OcrGrafter:
# finally move the lower left corner to match the mediabox
ctm = translate @ rotate @ scale @ untranslate @ corner
pdf_text_contents = (
b'q %s cm\n' % ctm.encode() + pdf_text_contents + b'\nQ\n'
base_resources = _ensure_dictionary(base_page, Name.Resources)
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
text_xobj_name = Name('/' + str(uuid.uuid4()))
xobj = self.pdf_base.make_stream(pdf_text_contents)
base_xobjs[text_xobj_name] = xobj
xobj.Type = Name.XObject
xobj.Subtype = Name.Form
xobj.FormType = 1
xobj.BBox = mediabox
_update_resources(
obj=xobj, font=font, font_key=font_key, procset=[Name.PDF]
)
new_text_layer = pikepdf.Stream(self.pdf_base, pdf_text_contents)
pdf_draw_xobj = (
(b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
)
new_text_layer = pikepdf.Stream(self.pdf_base, pdf_draw_xobj)
if strip_old_text:
strip_invisible_text(self.pdf_base, base_page)
base_page.page_contents_add(new_text_layer, prepend=True)
_update_page_resources(
page=base_page, font=font, font_key=font_key, procset=procset
_update_resources(
obj=base_page, font=font, font_key=font_key, procset=procset
)
+28 -4
View File
@@ -10,15 +10,22 @@ import shutil
import sys
from argparse import Namespace
from copy import copy
from io import IOBase
from pathlib import Path
from typing import Iterator
from pluggy import PluginManager
from ocrmypdf.pdfinfo import PdfInfo
from ocrmypdf.pdfinfo.info import PageInfo
class PdfContext:
"""Holds our context for a particular run of the pipeline"""
"""Holds the context for a particular run of the pipeline."""
options: Namespace #: The specified options for processing this PDF.
origin: Path #: The filename of the original input file.
pdfinfo: PdfInfo #: Detailed data for this PDF.
plugin_manager: PluginManager #: PluginManager for processing the current PDF.
def __init__(
self,
@@ -35,21 +42,33 @@ class PdfContext:
self.plugin_manager = plugin_manager
def get_path(self, name: str) -> Path:
"""Generate a ``Path`` for an intermediate file involved in processing.
The path will be in a temporary folder that is common for all processing
of this particular PDF.
"""
return self.work_folder / name
def get_page_contexts(self) -> Iterator['PageContext']:
"""Get all ``PageContext`` for this PDF."""
npages = len(self.pdfinfo)
for n in range(npages):
yield PageContext(self, n)
class PageContext:
"""Holds our context for a page
"""Holds our context for a page.
Must be pickable, so stores only intrinsic/simple data elements or those
capable of their serializing themselves via __getstate__.
capable of their serializing themselves via ``__getstate__``.
"""
options: Namespace #: The specified options for processing this PDF.
origin: Path #: The filename of the original input file.
pageno: int #: This page number (zero-based).
pageinfo: PageInfo #: Information on this page.
plugin_manager: PluginManager #: PluginManager for processing the current PDF.
def __init__(self, pdf_context: PdfContext, pageno):
self.work_folder = pdf_context.work_folder
self.origin = pdf_context.origin
@@ -59,6 +78,11 @@ class PageContext:
self.plugin_manager = pdf_context.plugin_manager
def get_path(self, name: str) -> Path:
"""Generate a ``Path`` for a file that is part of processing this page.
The path will be based in a common temporary folder and have a prefix based
on the page number.
"""
return self.work_folder / ("%06d_%s" % (self.pageno + 1, name))
def __getstate__(self):
+112 -49
View File
@@ -13,7 +13,7 @@ from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path
from shutil import copyfileobj
from typing import BinaryIO, Dict, Iterable, Optional, Union, cast
from typing import Dict, Iterable, Optional
import img2pdf
import pikepdf
@@ -21,6 +21,7 @@ from pikepdf.models.metadata import encode_pdf_date
from PIL import Image, ImageColor, ImageDraw
from ocrmypdf import leptonica
from ocrmypdf._concurrent import Executor
from ocrmypdf._exec import unpaper
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._version import PROGRAM_NAME
@@ -145,11 +146,13 @@ def triage(original_filename, input_file, output_file, options):
def get_pdfinfo(
input_file,
*,
executor: Executor,
detailed_analysis=False,
progbar=False,
max_workers=None,
check_pages=None,
):
) -> PdfInfo:
try:
return PdfInfo(
input_file,
@@ -157,11 +160,12 @@ def get_pdfinfo(
progbar=progbar,
max_workers=max_workers,
check_pages=check_pages,
executor=executor,
)
except pikepdf.PasswordError:
raise EncryptedPdfError()
except pikepdf.PdfError:
raise InputFileError()
except pikepdf.PasswordError as e:
raise EncryptedPdfError() from e
except pikepdf.PdfError as e:
raise InputFileError() from e
def validate_pdfinfo_options(context: PdfContext):
@@ -205,17 +209,21 @@ def validate_pdfinfo_options(context: PdfContext):
context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options)
def _vector_page_dpi(pageinfo):
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0.0
def get_page_dpi(pageinfo, options):
"Get the DPI when nonsquare DPI is tolerable"
xres = max(
pageinfo.dpi.x or VECTOR_PAGE_DPI,
options.oversample or 0.0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
_vector_page_dpi(pageinfo),
)
yres = max(
pageinfo.dpi.y or VECTOR_PAGE_DPI,
options.oversample or 0,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
_vector_page_dpi(pageinfo),
)
return Resolution(float(xres), float(yres))
@@ -229,7 +237,7 @@ def get_page_square_dpi(pageinfo, options) -> Resolution:
max(
(xres * userunit) or VECTOR_PAGE_DPI,
(yres * userunit) or VECTOR_PAGE_DPI,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
_vector_page_dpi(pageinfo),
options.oversample or 0.0,
)
)
@@ -242,7 +250,7 @@ def get_canvas_square_dpi(pageinfo, options) -> Resolution:
max(
(pageinfo.dpi.x) or VECTOR_PAGE_DPI,
(pageinfo.dpi.y) or VECTOR_PAGE_DPI,
VECTOR_PAGE_DPI if pageinfo.has_vector else 0.0,
_vector_page_dpi(pageinfo),
options.oversample or 0.0,
)
)
@@ -332,8 +340,10 @@ def rasterize_preview(input_file: Path, page_context: PageContext):
output_file=output_file,
raster_device='jpeggray',
raster_dpi=canvas_dpi,
page_dpi=page_dpi,
pageno=page_context.pageinfo.pageno + 1,
page_dpi=page_dpi,
rotation=0,
filter_vector=False,
)
return output_file
@@ -433,7 +443,7 @@ def rasterize(
device = colorspaces[device_idx]
log.debug(f"Rasterize with {device}")
log.debug(f"Rasterize with {device}, rotation {correction}")
# Produce the page image with square resolution or else deskew and OCR
# will not work properly.
@@ -473,7 +483,12 @@ def preprocess_deskew(input_file: Path, page_context: PageContext):
def preprocess_clean(input_file: Path, page_context: PageContext):
output_file = page_context.get_path('pp_clean.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
unpaper.clean(input_file, output_file, dpi.x, page_context.options.unpaper_args)
unpaper.clean(
input_file,
output_file,
dpi=dpi.x,
unpaper_args=page_context.options.unpaper_args,
)
return output_file
@@ -577,7 +592,9 @@ def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
return output_file
def create_pdf_page_from_image(image: Path, page_context: PageContext):
def create_pdf_page_from_image(
image: Path, page_context: PageContext, orientation_correction
):
# We rasterize a square DPI version of each page because most image
# processing tools don't support rectangular DPI. Use the square DPI as it
# accurately describes the image. It would be possible to resample the image
@@ -585,28 +602,42 @@ def create_pdf_page_from_image(image: Path, page_context: PageContext):
# except that the hocr renderer does not understand non-square DPI. The
# sandwich renderer would be fine.
output_file = page_context.get_path('visible.pdf')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
layout_fun = img2pdf.get_fixed_dpi_layout_fun(dpi)
pageinfo = page_context.pageinfo
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
if effective_rotation % 180 == 90:
pagesize = pagesize[1], pagesize[0]
# This create a single page PDF
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
log.debug('convert')
layout_fun = img2pdf.get_layout_fun(pagesize)
img2pdf.convert(
imfile, with_pdfrw=False, layout_fun=layout_fun, outputstream=pdf
)
log.debug('convert done')
output_file = page_context.plugin_manager.hook.filter_pdf_page(
page=page_context, image_filename=image, output_pdf=output_file
)
return output_file
def render_hocr_page(hocr: Path, page_context: PageContext):
options = page_context.options
output_file = page_context.get_path('ocr_hocr.pdf')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
hocrtransform = HocrTransform(hocr, dpi.x) # square
dpi = get_page_square_dpi(page_context.pageinfo, options)
debug_mode = options.pdf_renderer == 'hocrdebug'
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.x) # square
hocrtransform.to_pdf(
output_file,
out_filename=output_file,
image_filename=None,
show_bounding_boxes=False,
invisible_text=True,
show_bounding_boxes=False if not debug_mode else True,
invisible_text=True if not debug_mode else False,
interword_spaces=True,
)
return output_file
@@ -655,11 +686,6 @@ def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]:
pdfmark['/Creator'] = f'{PROGRAM_NAME} {VERSION} / {creator_tag}'
pdfmark['/Producer'] = f'pikepdf {pikepdf.__version__}'
if 'OCRMYPDF_CREATOR' in os.environ:
pdfmark['/Creator'] = os.environ['OCRMYPDF_CREATOR']
if 'OCRMYPDF_PRODUCER' in os.environ:
pdfmark['/Producer'] = os.environ['OCRMYPDF_PRODUCER']
pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc))
return pdfmark
@@ -707,6 +733,11 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext):
output_file=output_file,
compression=options.pdfa_image_compression,
pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3
progressbar_class=(
context.plugin_manager.hook.get_progressbar_class()
if options.progress_bar
else None
),
)
return output_file
@@ -719,6 +750,24 @@ def should_linearize(working_file: Path, context: PdfContext):
return False
def get_pdf_save_settings(output_type: str):
if output_type == 'pdfa-1':
# Trigger recompression to ensure object streams are removed, because
# Acrobat complains about them in PDF/A-1b validation.
return dict(
preserve_pdfa=True,
compress_streams=True,
stream_decode_level=pikepdf.StreamDecodeLevel.generalized,
object_stream_mode=pikepdf.ObjectStreamMode.disable,
)
else:
return dict(
preserve_pdfa=True,
compress_streams=True,
object_stream_mode=(pikepdf.ObjectStreamMode.generate),
)
def metadata_fixup(working_file: Path, context: PdfContext):
output_file = context.get_path('metafix.pdf')
options = context.options
@@ -749,25 +798,21 @@ def metadata_fixup(working_file: Path, context: PdfContext):
if 'xmp:CreateDate' not in meta:
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
# Ghostscript likes to set title to Untitled if omitted from input.
# Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
# and the XMP Spec do not make this recommendation.
if meta.get('dc:title') == 'Untitled':
with original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False
) as original_meta:
if 'dc:title' not in original_meta:
with original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original:
if meta.get('dc:title') == 'Untitled':
# Ghostscript likes to set title to Untitled if omitted from input.
# Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
# and the XMP Spec do not make this recommendation.
if 'dc:title' not in meta_original:
del meta['dc:title']
meta_original = original.open_metadata()
missing = set(meta_original.keys()) - set(meta.keys())
report_on_metadata(missing)
missing = set(meta_original.keys()) - set(meta.keys())
report_on_metadata(missing)
pdf.save(
output_file,
compress_streams=True,
preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
**get_pdf_save_settings(options.output_type),
linearize=( # Don't linearize if optimize() will be linearizing too
should_linearize(working_file, context)
if options.optimize == 0
@@ -778,23 +823,37 @@ def metadata_fixup(working_file: Path, context: PdfContext):
return output_file
def optimize_pdf(input_file: Path, context: PdfContext):
def optimize_pdf(input_file: Path, context: PdfContext, executor: Executor):
output_file = context.get_path('optimize.pdf')
save_settings = dict(
compress_streams=True,
preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
linearize=should_linearize(input_file, context),
**get_pdf_save_settings(context.options.output_type),
)
optimize(input_file, output_file, context, save_settings)
optimize(input_file, output_file, context, save_settings, executor)
return output_file
def enumerate_compress_ranges(iterable):
skipped_from, index = None, None
for index, txt_file in enumerate(iterable):
index += 1
if txt_file:
if skipped_from is not None:
yield (skipped_from, index - 1), None
skipped_from = None
yield (index, index), txt_file
else:
if skipped_from is None:
skipped_from = index
if skipped_from is not None:
yield (skipped_from, index), None
def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
output_file = context.get_path('sidecar.txt')
with open(output_file, 'w', encoding="utf-8") as stream:
for page_num, txt_file in enumerate(txt_files):
if page_num != 0:
for (frm, to), txt_file in enumerate_compress_ranges(txt_files):
if frm != 1:
stream.write('\f') # Form feed between pages
if txt_file:
with open(txt_file, 'r', encoding="utf-8") as in_:
@@ -807,7 +866,11 @@ def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
else:
stream.write(txt)
else:
stream.write(f'[OCR skipped on page {(page_num + 1)}]')
if frm != to:
pages = f'{frm}-{to}'
else:
pages = f'{frm}'
stream.write(f'[OCR skipped on page(s) {pages}]')
return output_file
+50 -40
View File
@@ -8,13 +8,14 @@
import argparse
import importlib
import importlib.util
import pkgutil
import sys
from functools import partial
from pathlib import Path
from typing import Callable, List, Tuple, Union
from typing import List, Tuple, Union
import pluggy
import ocrmypdf.builtin_plugins
from ocrmypdf import pluginspec
from ocrmypdf.cli import get_parser, plugins_only_parser
@@ -30,62 +31,71 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
"""
def __init__(
self, *args, setup_func: Callable[[pluggy.PluginManager], None], **kwargs
self,
*args,
plugins: List[Union[str, Path]],
builtins: bool = True,
**kwargs,
):
self._init_args = args
self._setup_func = setup_func
self._init_kwargs = kwargs
self.__init_args = args
self.__init_kwargs = kwargs
self.__plugins = plugins
self.__builtins = builtins
super().__init__(*args, **kwargs)
setup_func(self)
self.setup_plugins()
def __getstate__(self):
state = dict(
_init_args=self._init_args,
_setup_func=self._setup_func,
_init_kwargs=self._init_kwargs,
init_args=self.__init_args,
plugins=self.__plugins,
builtins=self.__builtins,
init_kwargs=self.__init_kwargs,
)
return state
def __setstate__(self, state):
self.__init__(
*state['_init_args'],
setup_func=state['_setup_func'],
**state['_init_kwargs'],
*state['init_args'],
plugins=state['plugins'],
builtins=state['builtins'],
**state['init_kwargs'],
)
def setup_plugins(self):
self.add_hookspecs(pluginspec)
def _setup_plugins(
pm: pluggy.PluginManager, plugins: List[Union[str, Path]], builtins: bool = True
):
pm.add_hookspecs(pluginspec)
# 1. Register builtins
if self.__builtins:
for module in sorted(
pkgutil.iter_modules(ocrmypdf.builtin_plugins.__path__)
):
name = f'ocrmypdf.builtin_plugins.{module.name}'
module = importlib.import_module(name)
self.register(module)
all_plugins: List[Union[str, Path]] = []
if builtins:
all_plugins.extend(
[
'ocrmypdf.builtin_plugins.ghostscript',
'ocrmypdf.builtin_plugins.tesseract_ocr',
]
)
all_plugins.extend(plugins)
for name in all_plugins:
if isinstance(name, Path) or name.endswith('.py'):
# Import by filename
module_name = Path(name).stem
spec = importlib.util.spec_from_file_location(module_name, name)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
else:
# Import by dotted module name
module = importlib.import_module(name)
pm.register(module)
# 2. Register setuptools plugins
self.load_setuptools_entrypoints('ocrmypdf')
# 3. Register plugins specified on command line
for name in self.__plugins:
if isinstance(name, Path) or name.endswith('.py'):
# Import by filename
module_name = Path(name).stem
spec = importlib.util.spec_from_file_location(module_name, name)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
else:
# Import by dotted module name
module = importlib.import_module(name)
self.register(module)
def get_plugin_manager(plugins: List[str], builtins=True):
def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
pm = OcrmypdfPluginManager(
project_name='ocrmypdf',
setup_func=partial(_setup_plugins, plugins=plugins, builtins=builtins),
plugins=plugins,
builtins=builtins,
)
return pm
+43 -31
View File
@@ -15,10 +15,9 @@ from pathlib import Path
from tempfile import mkdtemp
from typing import List, NamedTuple, Optional, Tuple
import pikepdf
import PIL
from ocrmypdf._concurrent import exec_progress_pool
from ocrmypdf._concurrent import Executor, setup_executor
from ocrmypdf._graft import OcrGrafter
from ocrmypdf._jobcontext import PageContext, PdfContext, cleanup_working_files
from ocrmypdf._logging import PageNumberFilter
@@ -55,6 +54,7 @@ from ocrmypdf._validation import (
)
from ocrmypdf.exceptions import ExitCode, ExitCodeException
from ocrmypdf.helpers import (
NeverRaise,
available_cpu_count,
check_pdf,
pikepdf_enable_mmap,
@@ -65,7 +65,7 @@ from ocrmypdf.pdfa import file_claims_pdfa
log = logging.getLogger(__name__)
class PageResult(NamedTuple):
class PageResult(NamedTuple): # pylint: disable=inherit-non-class
pageno: int
pdf_page_from_image: Optional[Path]
ocr: Optional[Path]
@@ -203,15 +203,16 @@ def exec_page_sync(page_context: PageContext):
if filtered_image:
visible_image_out = filtered_image
pdf_page_from_image_out = create_pdf_page_from_image(
visible_image_out, page_context
visible_image_out, page_context, orientation_correction
)
if options.pdf_renderer == 'hocr':
if options.pdf_renderer.startswith('hocr'):
(hocr_out, text_out) = ocr_engine_hocr(ocr_image_out, page_context)
ocr_out = render_hocr_page(hocr_out, page_context)
if options.pdf_renderer == 'sandwich':
elif options.pdf_renderer == 'sandwich':
(ocr_out, text_out) = ocr_engine_textonly_pdf(ocr_image_out, page_context)
else:
raise NotImplementedError(f"pdf_renderer {options.pdf_renderer}")
return PageResult(
pageno=page_context.pageno,
@@ -222,14 +223,14 @@ def exec_page_sync(page_context: PageContext):
)
def post_process(pdf_file, context: PdfContext):
def post_process(pdf_file, context: PdfContext, executor: Executor):
pdf_out = pdf_file
if context.options.output_type.startswith('pdfa'):
ps_stub_out = generate_postscript_stub(context)
pdf_out = convert_to_pdfa(pdf_out, ps_stub_out, context)
pdf_out = metadata_fixup(pdf_out, context)
return optimize_pdf(pdf_out, context)
return optimize_pdf(pdf_out, context, executor)
def worker_init(max_pixels: int):
@@ -240,11 +241,12 @@ def worker_init(max_pixels: int):
pikepdf_enable_mmap()
def exec_concurrent(context: PdfContext):
def exec_concurrent(context: PdfContext, executor: Executor):
"""Execute the pipeline concurrently"""
# Run exec_page_sync on every page context
max_workers = min(len(context.pdfinfo), context.options.jobs)
options = context.options
max_workers = min(len(context.pdfinfo), options.jobs)
if max_workers > 1:
log.info("Start processing %d pages concurrently", max_workers)
@@ -266,46 +268,47 @@ def exec_concurrent(context: PdfContext):
finally:
tls.pageno = None
exec_progress_pool(
use_threads=context.options.use_threads,
executor(
use_threads=options.use_threads,
max_workers=max_workers,
tqdm_kwargs=dict(
total=(2 * len(context.pdfinfo)),
desc='OCR',
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
unit='page',
unit_scale=0.5,
disable=not context.options.progress_bar,
disable=not options.progress_bar,
),
task_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
task=exec_page_sync,
task_arguments=context.get_page_contexts(),
task_finished=update_page,
)
# Output sidecar text
if context.options.sidecar:
if options.sidecar:
text = merge_sidecars(sidecars, context)
# Copy text file to destination
copy_final(text, context.options.sidecar, context)
copy_final(text, options.sidecar, context)
# Merge layers to one single pdf
pdf = ocrgraft.finalize()
# PDF/A and metadata
log.info("Postprocessing...")
pdf = post_process(pdf, context)
pdf = post_process(pdf, context, executor)
# Copy PDF file to destination
copy_final(pdf, context.options.output_file, context)
copy_final(pdf, options.output_file, context)
class NeverRaise(Exception):
"""An exception that is never raised"""
def configure_debug_logging(log_filename: Path, prefix: str = ''):
"""
Create a debug log file at a specified location.
pass # pylint: disable=unnecessary-pass
def configure_debug_logging(log_filename, prefix=''):
Arguments:
log_filename: Where to the put the log file.
prefix: The logging domain prefix that should be sent to the log.
"""
log_file_handler = logging.FileHandler(log_filename, delay=True)
log_file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
@@ -326,15 +329,23 @@ def run_pipeline(options, *, plugin_manager, api=False):
if not plugin_manager:
plugin_manager = get_plugin_manager(options.plugins)
work_folder = Path(mkdtemp(prefix="com.github.ocrmypdf."))
work_folder = Path(mkdtemp(prefix="ocrmypdf.io."))
debug_log_handler = None
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
'PYTEST_CURRENT_TEST', ''
if (
(options.keep_temporary_files or options.verbose >= 1)
and not os.environ.get('PYTEST_CURRENT_TEST', '')
and not api
):
debug_log_handler = configure_debug_logging(Path(work_folder) / "debug.log")
# Debug log for command line interface only with verbose output
# See https://github.com/pytest-dev/pytest/issues/5502 for why we skip this
# when pytest is running
debug_log_handler = configure_debug_logging(
Path(work_folder) / "debug.log"
) # pragma: no cover
pikepdf_enable_mmap()
executor = setup_executor(plugin_manager)
try:
check_requested_output_file(options)
start_input_file, original_filename = create_input_file(options, work_folder)
@@ -347,6 +358,7 @@ def run_pipeline(options, *, plugin_manager, api=False):
# Gather pdfinfo and create context
pdfinfo = get_pdfinfo(
origin_pdf,
executor=executor,
detailed_analysis=options.redo_ocr,
progbar=options.progress_bar,
max_workers=options.jobs if not options.use_threads else 1, # To help debug
@@ -359,7 +371,7 @@ def run_pipeline(options, *, plugin_manager, api=False):
validate_pdfinfo_options(context)
# Execute the pipeline
exec_concurrent(context)
exec_concurrent(context, executor)
if options.output_file == '-':
log.info("Output sent to stdout")
+23 -12
View File
@@ -13,7 +13,7 @@ import sys
import unicodedata
from pathlib import Path
from shutil import copyfileobj
from typing import Tuple
from typing import List, Set, Tuple, Union
import pikepdf
import PIL
@@ -78,7 +78,7 @@ def check_options_languages(options, ocr_engine_languages):
def check_options_output(options):
is_latin = options.languages.issubset(HOCR_OK_LANGS)
if options.pdf_renderer == 'hocr' and not is_latin:
if options.pdf_renderer.startswith('hocr') and not is_latin:
msg = (
"The 'hocr' PDF renderer is known to cause problems with one "
"or more of the languages in your document. Use "
@@ -112,6 +112,10 @@ def check_options_sidecar(options):
"--sidecar filename must be specified when output file is stdout."
)
options.sidecar = options.output_file + '.txt'
if options.sidecar == options.input_file or options.sidecar == options.output_file:
raise BadArgsError(
"--sidecar file must be different from the input and output files"
)
def check_options_preprocessing(options):
@@ -133,13 +137,13 @@ def check_options_preprocessing(options):
options.unpaper_args
)
except Exception as e:
raise BadArgsError(str(e))
raise BadArgsError("--unpaper-args: " + str(e)) from e
def _pages_from_ranges(ranges):
def _pages_from_ranges(ranges: str) -> Set[int]:
if is_iterable_notstr(ranges):
return set(ranges)
pages = []
pages: List[int] = []
page_groups = ranges.replace(' ', '').split(',')
for g in page_groups:
if not g:
@@ -150,9 +154,18 @@ def _pages_from_ranges(ranges):
pages.append(int(g) - 1)
else:
try:
pages.extend(range(int(start) - 1, int(end)))
new_pages = list(range(int(start) - 1, int(end)))
if not new_pages:
raise BadArgsError(f"invalid page subrange '{start}-{end}'")
pages.extend(new_pages)
except ValueError:
raise BadArgsError("invalid page range")
raise BadArgsError("invalid page range") from None
if not pages:
raise BadArgsError(
f"The string of page ranges '{ranges}' did not contain any recognizable "
f"page ranges."
)
if not monotonic(pages):
log.warning(
@@ -175,8 +188,6 @@ def check_options_ocr_behavior(options):
)
if exclusive_options >= 2:
raise BadArgsError("Choose only one of --force-ocr, --skip-text, --redo-ocr.")
if options.pages and options.sidecar:
raise BadArgsError("--pages and --sidecar are mutually exclusive")
if options.pages:
options.pages = _pages_from_ranges(options.pages)
@@ -213,12 +224,12 @@ def check_options_optimizing(options):
def check_options_advanced(options):
if options.pdfa_image_compression != 'auto' and options.output_type.startswith(
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
'pdfa'
):
log.warning(
"--pdfa-image-compression argument has no effect when "
"--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'"
"--pdfa-image-compression argument only applies when "
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
)
+82 -39
View File
@@ -8,11 +8,17 @@
import logging
import os
import sys
import threading
from enum import IntEnum
from io import IOBase
from pathlib import Path
from typing import BinaryIO, Iterable, Union
from typing import AnyStr, BinaryIO, Iterable, Optional, Union
from warnings import warn
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
from ocrmypdf._logging import ( # pylint: disable=unused-import
PageNumberFilter,
TqdmConsole,
)
from ocrmypdf._plugin_manager import get_plugin_manager
from ocrmypdf._sync import run_pipeline
from ocrmypdf._validation import check_options
@@ -25,7 +31,10 @@ except ModuleNotFoundError:
coloredlogs = None
PathOrIO = Union[BinaryIO, os.PathLike, str, bytes]
StrPath = Union[os.PathLike, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
_api_lock = threading.Lock()
class Verbosity(IntEnum):
@@ -39,29 +48,45 @@ class Verbosity(IntEnum):
def configure_logging(
verbosity: Verbosity,
*,
progress_bar_friendly: bool = True,
manage_root_logger: bool = False,
plugin_manager=None,
):
"""Set up logging.
Library users may wish to use this function if they want their log output to be
similar to ocrmypdf command line interface. If not used, the external application
should configure logging on its own.
Before calling :func:`ocrmypdf.ocr()`, you can use this function to
configure logging if you want ocrmypdf's output to look like the ocrmypdf
command line interface. It will register log handlers, log filters, and
formatters, configure color logging to standard error, and adjust the log
levels of third party libraries. Details of this are fine-tuned and subject
to change. The ``verbosity`` argument is equivalent to the argument
``--verbose`` and applies those settings. If you have a wrapper
script for ocrmypdf and you want it to be very similar to ocrmypdf, use this
function; if you are using ocrmypdf as part of an application that manages
its own logging, you probably do not want this function.
ocrmypdf will perform all of its logging under the ``"ocrmypdf"`` logging namespace.
In addition, ocrmypdf imports pdfminer, which logs under ``"pdfminer"``. A library
user may wish to configure both; note that pdfminer is extremely chatty at the log
level ``logging.INFO``.
If this function is not called, ocrmypdf will not configure logging, and it
is up to the caller of ``ocrmypdf.ocr()`` to set up logging as it wishes using
the Python standard library's logging module. If this function is called,
the caller may of course make further adjustments to logging.
Library users may perform additional configuration afterwards.
Regardless of whether this function is called, ocrmypdf will perform all of
its logging under the ``"ocrmypdf"`` logging namespace. In addition,
ocrmypdf imports pdfminer, which logs under ``"pdfminer"``. A library user
may wish to configure both; note that pdfminer is extremely chatty at the
log level ``logging.INFO``.
This function does not set up the ``debug.log`` log file that the command
line interface does at certain verbosity levels. Applications should configure
their own debug logging.
Args:
verbosity (Verbosity): Verbosity level.
progress_bar_friendly (bool): Install the TqdmConsole log handler, which is
compatible with the tqdm progress bar; without this log messages will
overwrite the progress bar
manage_root_logger (bool): Configure the process's root logger, to ensure
all log output is sent through
verbosity: Verbosity level.
progress_bar_friendly: If True (the default), install a custom log handler
that is compatible with progress bars and colored output.
manage_root_logger: Configure the process's root logger.
plugin_manager: The plugin manager, used for obtaining the custom log handler.
Returns:
The toplevel logger for ocrmypdf (or the root logger, if we are managing it).
@@ -72,9 +97,11 @@ def configure_logging(
log = logging.getLogger(prefix)
log.setLevel(logging.DEBUG)
if progress_bar_friendly:
console = logging.StreamHandler(stream=TqdmConsole(sys.stderr))
else:
console = None
if plugin_manager and progress_bar_friendly:
console = plugin_manager.hook.get_logging_console()
if not console:
console = logging.StreamHandler(stream=sys.stderr)
if verbosity < 0:
@@ -160,14 +187,14 @@ def create_options(
else:
raise TypeError(f"{arg}: {val} ({type(val)})")
try:
cmdline.append(os.fspath(input_file))
except TypeError:
if isinstance(input_file, (BinaryIO, IOBase)):
cmdline.append('stream://input_file')
try:
cmdline.append(os.fspath(output_file))
except TypeError:
else:
cmdline.append(os.fspath(input_file))
if isinstance(output_file, (BinaryIO, IOBase)):
cmdline.append('stream://output_file')
else:
cmdline.append(os.fspath(output_file))
parser._api_mode = True
options = parser.parse_args(cmdline)
@@ -189,7 +216,7 @@ def ocr( # pylint: disable=unused-argument
language: Iterable[str] = None,
image_dpi: int = None,
output_type=None,
sidecar: os.PathLike = None,
sidecar: Optional[StrPath] = None,
jobs: int = None,
use_threads: bool = None,
title: str = None,
@@ -226,7 +253,8 @@ def ocr( # pylint: disable=unused-argument
user_words: os.PathLike = None,
user_patterns: os.PathLike = None,
fast_web_view: float = None,
plugins: Iterable[str] = None,
plugins: Iterable[StrPath] = None,
plugin_manager=None,
keep_temporary_files: bool = None,
progress_bar: bool = None,
**kwargs,
@@ -248,7 +276,7 @@ def ocr( # pylint: disable=unused-argument
read.
output_file: If a :class:`pathlib.Path`, ``str`` or ``bytes``, this is
interpreted as file system path to the output file. If the object
appears to be a writable stream (with methods such as ``.read()`` and
appears to be a writable stream (with methods such as ``.write()`` and
``.seek()``), the output will be written to this stream. If
``output_file`` is ``"-"``, the output will be written to ``sys.stdout``
(provided that standard output does not seem to be a terminal device).
@@ -278,20 +306,35 @@ def ocr( # pylint: disable=unused-argument
Returns:
:class:`ocrmypdf.ExitCode`
"""
if plugins and plugin_manager:
raise ValueError("plugins= and plugin_manager are mutually exclusive")
if not plugins:
plugins = []
elif isinstance(plugins, (str, Path)):
plugins = [plugins]
else:
plugins = list(plugins)
parser = get_parser()
_plugin_manager = get_plugin_manager(plugins)
_plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
create_options_kwargs = {
k: v for k, v in locals().items() if not k.startswith('_') and k != 'kwargs'
}
# No new variable names should be assigned until these two steps are run
create_options_kwargs = {k: v for k, v in locals().items() if k != 'kwargs'}
create_options_kwargs.update(kwargs)
options = create_options(**create_options_kwargs)
check_options(options, _plugin_manager)
return run_pipeline(options=options, plugin_manager=_plugin_manager, api=True)
parser = get_parser()
create_options_kwargs['parser'] = parser
with _api_lock:
# We can't allow multiple ocrmypdf.ocr() threads to run in parallel, because
# they might install different plugins, and generally speaking we have areas
# of code that use global state.
if not plugin_manager:
plugin_manager = get_plugin_manager(plugins)
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
if 'verbose' in kwargs:
warn("ocrmypdf.ocr(verbose=) is ignored. Use ocrmypdf.configure_logging().")
options = create_options(**create_options_kwargs)
check_options(options, plugin_manager)
return run_pipeline(options=options, plugin_manager=plugin_manager, api=True)
+4
View File
@@ -3,3 +3,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This file exists only mark builtin_plugins as a package.
# The plugin manager will not load it, so anything defined here may not be
# processed as a module.
+172
View File
@@ -0,0 +1,172 @@
# © 2020 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# © 2020 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import logging
import logging.handlers
import multiprocessing
import os
import queue
import signal
import sys
import threading
from contextlib import suppress
from multiprocessing import Pool as ProcessPool
from multiprocessing.pool import ThreadPool
from typing import Callable, Iterable, Union
from tqdm import tqdm
from ocrmypdf import Executor, hookimpl
from ocrmypdf._logging import TqdmConsole
from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers
Queue = Union[multiprocessing.Queue, queue.Queue]
def log_listener(q: Queue):
"""Listen to the worker processes and forward the messages to logging
For simplicity this is a thread rather than a process. Only one process
should actually write to sys.stderr or whatever we're using, so if this is
made into a process the main application needs to be directed to it.
See https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
"""
while True:
try:
record = q.get()
if record is None:
break
logger = logging.getLogger(record.name)
logger.handle(record)
except Exception: # pylint: disable=broad-except
import traceback # pylint: disable=import-outside-toplevel
print("Logging problem", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
def process_sigbus(*args):
raise InputFileError("A worker process lost access to an input file")
def process_init(q: Queue, user_init: Callable[[], None], loglevel):
"""Initialize a process pool worker"""
# Ignore SIGINT (our parent process will kill us gracefully)
signal.signal(signal.SIGINT, signal.SIG_IGN)
# Install SIGBUS handler (so our parent process can abort somewhat gracefully)
with suppress(AttributeError): # Windows and Cygwin do not have SIGBUS
# Windows and Cygwin do not have pthread_sigmask or SIGBUS
signal.signal(signal.SIGBUS, process_sigbus)
# Remove any log handlers that belong to the parent process
root = logging.getLogger()
remove_all_log_handlers(root)
# Set up our single log handler to forward messages to the parent
root.setLevel(loglevel)
root.addHandler(logging.handlers.QueueHandler(q))
user_init()
return
def thread_init(_queue: Queue, user_init: Callable[[], None], _loglevel):
# As a thread, block SIGBUS so the main thread deals with it...
with suppress(AttributeError):
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGBUS})
user_init()
return
class StandardExecutor(Executor):
def _execute(
self,
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
worker_initializer: Callable,
task: Callable,
task_arguments: Iterable,
task_finished: Callable,
):
if use_threads:
log_queue = queue.Queue(-1)
pool_class = ThreadPool
initializer = thread_init
else:
log_queue = multiprocessing.Queue(-1)
pool_class = ProcessPool
initializer = process_init
# Regardless of whether we use_threads for worker processes, the log_listener
# must be a thread. Make sure we create the listener after the worker pool,
# so that it does not get forked into the workers.
listener = threading.Thread(target=log_listener, args=(log_queue,))
listener.start()
with self.pbar_class(**tqdm_kwargs) as pbar:
pool = pool_class(
processes=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
)
try:
results = pool.imap_unordered(task, task_arguments)
for result in results:
if task_finished:
task_finished(result, pbar)
else:
pbar.update()
except KeyboardInterrupt:
# Terminate pool so we exit instantly
pool.terminate()
# Don't try listener.join() here, will deadlock
raise
except Exception:
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
# Unless inside pytest, exit immediately because no one wants
# to wait for child processes to finalize results that will be
# thrown away. Inside pytest, we want child processes to exit
# cleanly so that they output an error messages or coverage data
# we need from them.
pool.terminate()
raise
finally:
# Terminate log listener
log_queue.put_nowait(None)
pool.close()
pool.join()
listener.join()
@hookimpl
def get_executor(progressbar_class):
return StandardExecutor(pbar_class=progressbar_class)
@hookimpl
def get_progressbar_class():
return tqdm
@hookimpl
def get_logging_console():
return logging.StreamHandler(stream=TqdmConsole(sys.stderr))
@@ -0,0 +1,14 @@
# © 2021 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ocrmypdf import hookimpl
@hookimpl
def filter_pdf_page(
page, image_filename, output_pdf
): # pylint: disable=unused-argument
return output_pdf
+13 -4
View File
@@ -61,9 +61,9 @@ def rasterize_pdf_page(
raster_device,
raster_dpi,
pageno,
page_dpi=None,
rotation=None,
filter_vector=False,
page_dpi,
rotation,
filter_vector,
):
ghostscript.rasterize_pdf(
input_file,
@@ -79,12 +79,21 @@ def rasterize_pdf_page(
@hookimpl
def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdfa_part):
def generate_pdfa(
pdf_pages,
pdfmark,
output_file,
compression,
pdf_version,
pdfa_part,
progressbar_class,
):
ghostscript.generate_pdfa(
pdf_pages=[*pdf_pages, pdfmark],
output_file=output_file,
compression=compression,
pdf_version=pdf_version,
pdfa_part=pdfa_part,
progressbar_class=progressbar_class,
)
return output_file
+3 -11
View File
@@ -80,20 +80,14 @@ def check_options(options):
program='tesseract',
package={'linux': 'tesseract-ocr'},
version_checker=tesseract.version,
need_version='4.0.0', # using backport for Travis CI
need_version='4.0.0-beta.1', # using backport for Travis CI
version_parser=tesseract.TesseractVersion,
)
# Decide on what renderer to use
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'sandwich'
if options.pdf_renderer == 'sandwich' and not tesseract.has_textonly_pdf(
set(options.languages)
):
raise MissingDependencyError(
"You are using an alpha version of Tesseract 4.0 that does not support "
"the textonly_pdf parameter. We don't support versions this old."
)
if not tesseract.has_user_words() and (options.user_words or options.user_patterns):
log.warning(
"Tesseract 4.0 ignores --user-words and --user-patterns, so these "
@@ -121,9 +115,7 @@ def validate(pdfinfo, options):
os.environ['OMP_THREAD_LIMIT'] = str(tess_threads)
else:
tess_threads = int(os.environ['OMP_THREAD_LIMIT'])
if tess_threads > 1:
log.info("Using Tesseract OpenMP thread limit %d", tess_threads)
log.debug("Using Tesseract OpenMP thread limit %d", tess_threads)
class TesseractOcrEngine(OcrEngine):
+7 -3
View File
@@ -6,12 +6,15 @@
import argparse
from typing import Optional, Type, TypeVar
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION
T = TypeVar('T')
def numeric(basetype, min_=None, max_=None):
def numeric(basetype: Type[T], min_: Optional[T] = None, max_: Optional[T] = None):
"""Validator for numeric params"""
min_ = basetype(min_) if min_ is not None else None
max_ = basetype(max_) if max_ is not None else None
@@ -164,7 +167,8 @@ Online documentation is located at:
metavar='FILE',
help="Generate sidecar text files that contain the same text recognized "
"by Tesseract. This may be useful for building a OCR text database. "
"If FILE is omitted, the sidecar file be named {output_file}.txt "
"If FILE is omitted, the sidecar file be named {output_file}.txt; the next "
"argument must NOT be the name of the input PDF. "
"If FILE is set to '-', the sidecar is written to stdout (a "
"convenient way to preview OCR quality). The output file and sidecar "
"may not both use stdout at the same time.",
@@ -407,7 +411,7 @@ Online documentation is located at:
)
advanced.add_argument(
'--pdf-renderer',
choices=['auto', 'hocr', 'sandwich'],
choices=['auto', 'hocr', 'sandwich', 'hocrdebug'],
default='auto',
help="Choose OCR PDF renderer - the default option is to let OCRmyPDF "
"choose. See documentation for discussion.",
+193
View File
@@ -0,0 +1,193 @@
# © 2021 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""Alternate executor to support OCRmyPDF in AWS Lambda.
AWS Lambda does not support the standard multiprocessing module because it
environment.
This alternate executor avoids that. However, it has drawbacks. Most notably,
it divvies up work among worker processes at the beginning, to avoid coordinating
effort in ways that require shared queues and semaphores. In this implementation,
there is no shared queue, so no possible lock contention between workers. The
main process has a shared pipe with each worker.
If some tasks are larger than others, some workers will may fall far behind
while others have deep queues. The last worker may end up with fewer tasks.
"""
import logging
import logging.handlers
import signal
from contextlib import suppress
from enum import Enum, auto
from itertools import islice, repeat, takewhile, zip_longest
from multiprocessing import Pipe, Process
from multiprocessing.connection import Connection, wait
from typing import Callable, Iterable, Iterator
from unittest.mock import Mock
from ocrmypdf import Executor, hookimpl
from ocrmypdf._concurrent import NullProgressBar
from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers
class MessageType(Enum):
exception = auto()
result = auto()
complete = auto()
def split_every(n: int, iterable: Iterable) -> Iterator:
"""Split iterable into groups of n.
>>> list(split_every(4, range(10)))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
https://stackoverflow.com/a/22919323
"""
iterator = iter(iterable)
return takewhile(bool, (list(islice(iterator, n)) for _ in repeat(None)))
def process_sigbus(*args):
raise InputFileError("A worker process lost access to an input file")
class ConnectionLogHandler(logging.handlers.QueueHandler):
def __init__(self, conn: Connection) -> None:
super().__init__(None)
self.conn = conn
def enqueue(self, record):
self.conn.send(('log', record))
def process_loop(
conn: Connection, user_init: Callable[[], None], loglevel, task, task_args
):
"""Initialize a process pool worker"""
# Install SIGBUS handler (so our parent process can abort somewhat gracefully)
with suppress(AttributeError): # Windows and Cygwin do not have SIGBUS
# Windows and Cygwin do not have pthread_sigmask or SIGBUS
signal.signal(signal.SIGBUS, process_sigbus)
# Reconfigure the root logger for this process to send all messages to a queue
h = ConnectionLogHandler(conn)
root = logging.getLogger()
remove_all_log_handlers(root)
root.setLevel(loglevel)
root.addHandler(h)
user_init()
for args in task_args:
try:
result = task(args)
except Exception as e:
conn.send((MessageType.exception, e))
break
else:
conn.send((MessageType.result, result))
conn.send((MessageType.complete, None))
conn.close()
return
class LambdaExecutor(Executor):
def _execute(
self,
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
worker_initializer: Callable,
task: Callable,
task_arguments: Iterable,
task_finished: Callable,
):
if use_threads and max_workers == 1:
with self.pbar_class(**tqdm_kwargs) as pbar:
for args in task_arguments:
result = task(args)
task_finished(result, pbar)
return
task_arguments = list(task_arguments)
grouped_args = list(
zip_longest(*list(split_every(max_workers, task_arguments)))
)
if not grouped_args:
return
processes = []
connections = []
for chunk in grouped_args:
parent_conn, child_conn = Pipe()
worker_args = [args for args in chunk if args is not None]
process = Process(
target=process_loop,
args=(
child_conn,
worker_initializer,
logging.getLogger("").level,
task,
worker_args,
),
)
process.daemon = True
processes.append(process)
connections.append(parent_conn)
for process in processes:
process.start()
with self.pbar_class(**tqdm_kwargs) as pbar:
while connections:
for r in wait(connections):
try:
msg_type, msg = r.recv()
except EOFError:
connections.remove(r)
continue
if msg_type == MessageType.result:
if task_finished:
task_finished(msg, pbar)
elif msg_type == 'log':
record = msg
logger = logging.getLogger(record.name)
logger.handle(record)
elif msg_type == MessageType.complete:
connections.remove(r)
elif msg_type == MessageType.exception:
for process in processes:
process.terminate()
raise msg
for process in processes:
process.join()
@hookimpl
def get_executor(progressbar_class):
return LambdaExecutor(pbar_class=progressbar_class)
@hookimpl
def get_logging_console():
return logging.StreamHandler()
@hookimpl
def get_progressbar_class():
return NullProgressBar
+66 -43
View File
@@ -15,9 +15,9 @@ from collections.abc import Iterable
from contextlib import suppress
from functools import wraps
from io import StringIO
from math import isclose
from math import isclose, isfinite
from pathlib import Path
from typing import Any, Sequence, TypeVar
from typing import Any, Sequence
import pikepdf
@@ -39,6 +39,10 @@ class Resolution(namedtuple('Resolution', ('x', 'y'))):
def is_square(self) -> bool:
return isclose(self.x, self.y, rel_tol=1e-3)
@property
def is_finite(self) -> bool:
return isfinite(self.x) and isfinite(self.y)
def take_max(self, vals, yvals=None):
if yvals is not None:
return Resolution(max(self.x, *vals), max(self.y, *yvals))
@@ -54,13 +58,22 @@ class Resolution(namedtuple('Resolution', ('x', 'y'))):
def __str__(self):
return f"{self.x:f}x{self.y:f}"
def __repr__(self):
def __repr__(self): # pragma: no cover
return f"Resolution({self.x}x{self.y} dpi)"
class NeverRaise(Exception):
"""An exception that is never raised"""
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
"""
Helper function: relinks soft symbolic link if necessary
"""Create a symbolic link at ``soft_link_name``, which references ``input_file``.
Think of this as copying ``input_file`` to ``soft_link_name`` with less overhead.
Use symlinks safely. Self-linking loops are prevented. On Windows, file copy is
used since symlinks may require administrator privileges. An existing link at the
destination is removed.
"""
input_file = os.fspath(input_file)
soft_link_name = os.fspath(soft_link_name)
@@ -68,8 +81,8 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
# Guard against soft linking to oneself
if input_file == soft_link_name:
log.warning(
"No symbolic link made. You are using "
"the original data directory as the working directory."
"No symbolic link created. You are using the original data directory "
"as the working directory."
)
return
@@ -78,10 +91,7 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
# do not delete or overwrite real (non-soft link) file
if not os.path.islink(soft_link_name):
raise FileExistsError(f"{soft_link_name} exists and is not a link")
try:
os.unlink(soft_link_name)
except OSError:
log.debug("Can't unlink %s", soft_link_name)
os.unlink(soft_link_name)
if not os.path.exists(input_file):
raise FileNotFoundError(f"trying to create a broken symlink to {input_file}")
@@ -110,7 +120,7 @@ def is_iterable_notstr(thing: Any) -> bool:
def monotonic(L: Sequence) -> bool:
"""Does list increase monotonically?"""
"""Does this sequence increase monotonically?"""
return all(b > a for a, b in zip(L, L[1:]))
@@ -169,49 +179,61 @@ def is_file_writable(test_file: os.PathLike) -> bool:
def check_pdf(input_file: Path) -> bool:
"""Check if a PDF complies with the PDF specification.
Checks for proper formatting and proper linearization.
Checks for proper formatting and proper linearization. Uses pikepdf (which in
turn, uses QPDF) to perform the checks.
"""
pdf = None
try:
pdf = pikepdf.open(input_file)
except pikepdf.PdfError as e:
log.error(e)
return False
else:
messages = pdf.check()
for msg in messages:
if 'error' in msg.lower():
log.error(msg)
with pdf:
messages = pdf.check()
for msg in messages:
if 'error' in msg.lower():
log.error(msg)
else:
log.warning(msg)
sio = StringIO()
linearize_msgs = ''
try:
# If linearization is missing entirely, we do not complain. We do
# complain if linearization is present but incorrect.
pdf.check_linearization(sio)
except RuntimeError:
pass
except (
# Workaround for a problematic pikepdf version
# pragma: no cover
getattr(pikepdf, 'ForeignObjectError')
if pikepdf.__version__ == '2.1.0'
else NeverRaise
):
pass
else:
log.warning(msg)
linearize_msgs = sio.getvalue()
if linearize_msgs:
log.warning(linearize_msgs)
sio = StringIO()
linearize = None
try:
pdf.check_linearization(sio)
except RuntimeError:
pass
else:
linearize = sio.getvalue()
if linearize:
log.warning(linearize)
if not messages and not linearize:
return True
return False
finally:
if pdf:
pdf.close()
if not messages and not linearize_msgs:
return True
return False
T = TypeVar('T')
def clamp(n: T, smallest: T, largest: T) -> T:
"""Clamps the value of n to between smallest and largest."""
def clamp(n, smallest, largest): # mypy doesn't understand types for this
"""Clamps the value of ``n`` to between ``smallest`` and ``largest``."""
return max(smallest, min(n, largest))
def remove_all_log_handlers(logger):
"Remove all log handlers, usually used in a child process."
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler.close() # To ensure handlers with opened resources are released
def pikepdf_enable_mmap():
# try:
# if pikepdf._qpdf.set_access_default_mmap(True):
@@ -220,12 +242,13 @@ def pikepdf_enable_mmap():
# log.debug("pikepdf mmap not available")
# We found a race condition probably related to pybind issue #2252 that can
# cause a crash. For now, disable pikepdf mmap to be on the safe side.
log.debug("pikepdf mmap disabled")
# Fix is not in pybind11 2.6.0
# log.debug("pikepdf mmap disabled")
return
def deprecated(func):
"""Warn that function is deprecated"""
"""Warn that function is deprecated."""
@wraps(func)
def new_func(*args, **kwargs):
+28 -14
View File
@@ -31,18 +31,26 @@
import argparse
import os
import re
from collections import namedtuple
from itertools import chain
from math import atan, cos, sin
from pathlib import Path
from typing import Optional, Tuple, Union
from typing import Any, NamedTuple, Optional, Tuple, Union
from xml.etree import ElementTree
from reportlab.lib.colors import black, cyan, magenta, red
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
Element = ElementTree.Element
class Rect(NamedTuple): # pylint: disable=inherit-non-class
"""A rectangle for managing PDF coordinates."""
x1: Any
y1: Any
x2: Any
y2: Any
class HocrTransformError(Exception):
@@ -69,7 +77,7 @@ class HocrTransform:
{'': 'ff', '': 'ffi', '': 'ffl', '': 'fi', '': 'fl'}
)
def __init__(self, hocr_filename: Union[str, Path], dpi: float):
def __init__(self, *, hocr_filename: Union[str, Path], dpi: float):
self.dpi = dpi
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
@@ -105,7 +113,7 @@ class HocrTransform:
else:
return ''
def _get_element_text(self, element):
def _get_element_text(self, element: Element):
"""
Return the textual content of the element and its children
"""
@@ -119,7 +127,7 @@ class HocrTransform:
return text
@classmethod
def element_coordinates(cls, element) -> Rect:
def element_coordinates(cls, element: Element) -> Rect:
"""
Returns a tuple containing the coordinates of the bounding box around
an element
@@ -133,7 +141,7 @@ class HocrTransform:
return out
@classmethod
def baseline(cls, element) -> Tuple[float, float]:
def baseline(cls, element: Element) -> Tuple[float, float]:
"""
Returns a tuple containing the baseline slope and intercept.
"""
@@ -149,7 +157,7 @@ class HocrTransform:
"""
return Rect._make((c / self.dpi * inch) for c in pxl)
def _child_xpath(self, html_tag, html_class=None):
def _child_xpath(self, html_tag: str, html_class: Optional[str] = None) -> str:
xpath = f".//{self.xmlns}{html_tag}"
if html_class:
xpath += f"[@class='{html_class}']"
@@ -167,10 +175,14 @@ class HocrTransform:
def topdown_position(self, element):
pxl_line_coords = self.element_coordinates(element)
line_box = self.pt_from_pixel(pxl_line_coords)
return -line_box.y2
# Coordinates here are still in the hocr coordinate system, so 0 on the y axis
# is the top of the page and increasing values of y will move towards the
# bottom of the page.
return line_box.y2
def to_pdf(
self,
*,
out_filename: Path,
image_filename: Optional[Path] = None,
show_bounding_boxes: bool = False,
@@ -277,13 +289,15 @@ class HocrTransform:
def _do_line(
self,
pdf: Canvas,
line,
line: Optional[Element],
elemclass: str,
fontname: str,
invisible_text: bool,
interword_spaces: bool,
show_bounding_boxes: bool,
):
if not line:
return
pxl_line_coords = self.element_coordinates(line)
line_box = self.pt_from_pixel(pxl_line_coords)
line_height = line_box.y2 - line_box.y1
@@ -420,10 +434,10 @@ if __name__ == "__main__":
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(args.hocrfile, args.resolution)
hocr = HocrTransform(hocr_filename=args.hocrfile, dpi=args.resolution)
hocr.to_pdf(
args.outputfile,
args.image,
args.boundingboxes,
out_filename=args.outputfile,
image_filename=args.image,
show_bounding_boxes=args.boundingboxes,
interword_spaces=args.interword_spaces,
)
+31 -26
View File
@@ -15,7 +15,6 @@ import logging
import os
import sys
import threading
import warnings
from collections import deque
from collections.abc import Sequence
from contextlib import suppress
@@ -24,18 +23,20 @@ from functools import lru_cache
from io import BytesIO, UnsupportedOperation
from os import fspath
from tempfile import TemporaryFile
from warnings import warn
from ocrmypdf.exceptions import MissingDependencyError
from ocrmypdf.lib._leptonica import ffi
from ocrmypdf.subprocess import shim_paths_with_program_files
# pylint: disable=protected-access
logger = logging.getLogger(__name__)
if os.name == 'nt':
from ocrmypdf.subprocess._windows import shim_env_path
libname = 'liblept-5'
os.environ['PATH'] = shim_paths_with_program_files()
os.environ['PATH'] = shim_env_path()
else:
libname = 'lept'
_libpath = find_library(libname)
@@ -58,9 +59,9 @@ if not _libpath:
---------------------------------------------------------------------
"""
)
if os.name == 'nt':
# On Windows, recent versions of libpng require zlib. We have to make sure
# the zlib version being loaded is the same one that libpng was built with.
if os.name == 'nt':
# On Windows, recent versions of libpng require zlib. We have to make sure
# the zlib version being loaded is the same one that libpng was built with.
# This tries to import zlib from Tesseract's installation folder, falling back
# to find_library() if liblept is being loaded from somewhere else.
# Loading zlib from other places could cause a version mismatch
@@ -87,9 +88,9 @@ except ffi.error as e:
class _LeptonicaErrorTrap_Redirect:
"""
Context manager to trap errors reported by Leptonica.
Context manager to trap errors reported by Leptonica < 1.79 or on Apple Silicon.
Leptonica's error return codes don't provide much informatino about what
Leptonica's error return codes don't provide much information about what
went wrong. Leptonica does, however, write more detailed errors to stderr
(provided this is not disabled at compile time). The Leptonica source
code is very consistent in its use of macros to generate errors.
@@ -113,8 +114,10 @@ class _LeptonicaErrorTrap_Redirect:
# Save the old stderr, and redirect stderr to temporary file
self.leptonica_lock.acquire()
try:
with suppress(AttributeError):
sys.stderr.flush()
# It would make sense to do sys.stderr.flush() here, but that can deadlock
# due to https://bugs.python.org/issue6721. So don't flush. Pretend
# there's nothing important in sys.stderr. If the user cared they would
# be using Leptonica 1.79 or later anyway to avoid this mess.
self.copy_of_stderr = os.dup(sys.stderr.fileno())
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(), inheritable=False)
except AttributeError:
@@ -169,20 +172,6 @@ tls = threading.local()
tls.trap = None
@ffi.callback("void(char *)")
def _stderr_handler(cstr):
msg = ffi.string(cstr).decode(errors='replace')
if msg.startswith("Error"):
logger.error(msg)
elif msg.startswith("Warning"):
logger.warning(msg)
else:
logger.debug(msg)
if tls.trap is not None:
tls.trap.append(msg)
return
class _LeptonicaErrorTrap_Queue:
def __init__(self):
self.queue = deque()
@@ -212,9 +201,25 @@ class _LeptonicaErrorTrap_Queue:
try:
@ffi.callback("void(char *)")
def _stderr_handler(cstr):
msg = ffi.string(cstr).decode(errors='replace')
if msg.startswith("Error"):
logger.error(msg)
elif msg.startswith("Warning"):
logger.warning(msg)
else:
logger.debug(msg)
if tls.trap is not None:
tls.trap.append(msg)
return
lept.leptSetStderrHandler(_stderr_handler)
except ffi.error:
except (ffi.error, MemoryError):
# Pre-1.79 Leptonica does not have leptSetStderrHandler
# And some platforms, notably Apple ARM 64, do not allow the write+execute
# memory needed to set up the callback function.
_LeptonicaErrorTrap = _LeptonicaErrorTrap_Redirect
else:
# 1.79 have this new symbol
@@ -386,7 +391,7 @@ class Pix(LeptonicaObject):
@classmethod
def read(cls, path):
warnings.warn('Use Pix.open() instead', DeprecationWarning)
warn('Use Pix.open() instead', DeprecationWarning)
return cls.open(path)
@classmethod
+144 -155
View File
@@ -9,12 +9,9 @@ import logging
import sys
import tempfile
from collections import defaultdict
from functools import partial
from io import BytesIO
from os import fspath
from pathlib import Path
from typing import (
Any,
Callable,
Dict,
Iterator,
@@ -25,21 +22,19 @@ from typing import (
Optional,
Sequence,
Tuple,
Union,
)
import img2pdf
import pikepdf
from pikepdf import Dictionary, Name, Object, Pdf, PdfImage
from PIL import Image
from tqdm import tqdm
from ocrmypdf import leptonica
from ocrmypdf._concurrent import exec_progress_pool
from ocrmypdf._concurrent import Executor, SerialExecutor
from ocrmypdf._exec import jbig2enc, pngquant
from ocrmypdf._jobcontext import PdfContext
from ocrmypdf.exceptions import OutputFileAccessError
from ocrmypdf.helpers import deprecated, safe_symlink
from ocrmypdf.helpers import safe_symlink
log = logging.getLogger(__name__)
@@ -50,7 +45,7 @@ DEFAULT_PNG_QUALITY = 70
Xref = NewType('Xref', int)
class XrefExt(NamedTuple):
class XrefExt(NamedTuple): # pylint: disable=inherit-non-class
xref: Xref
ext: str
@@ -67,33 +62,42 @@ def jpg_name(root: Path, xref: Xref) -> Path:
return img_name(root, xref, '.jpg')
def tif_name(root: Path, xref: Xref) -> Path:
return img_name(root, xref, '.tif')
def extract_image_filter(
pike: Pdf, root: Path, image: Object, xref: Xref
) -> Optional[Tuple[PdfImage, Tuple[Name, Object]]]:
del pike # unused args
del root
if image.Subtype != Name.Image:
return None
if image.Length < 100:
log.debug("Skipping small image, xref %s", xref)
log.debug(f"Skipping small image, xref {xref}")
return None
if image.Width < 8 or image.Height < 8: # Issue 732
log.debug(f"Skipping oddly sized image, xref {xref}")
return None
pim = PdfImage(image)
if len(pim.filter_decodeparms) > 1:
log.debug("Skipping multiply filtered, xref %s", xref)
log.debug(f"Skipping multiply filtered image, xref {xref}")
return None
filtdp = pim.filter_decodeparms[0]
if pim.bits_per_component > 8:
log.debug(f"Skipping wide gamut image, xref {xref}")
return None # Don't mess with wide gamut images
if filtdp[0] == Name.JPXDecode:
log.debug(f"Skipping JPEG2000 iamge, xref {xref}")
return None # Don't do JPEG2000
if filtdp[0] == Name.CCITTFaxDecode and filtdp[1].get('/K', 0) >= 0:
log.debug(f"Skipping CCITT Group 3 image, xref {xref}")
return None # pikepdf doesn't support Group 3 yet
if Name.Decode in image:
log.debug(f"Skipping image with Decode table, xref {xref}")
return None # Don't mess with custom Decode tables
return pim, filtdp
@@ -102,6 +106,8 @@ def extract_image_filter(
def extract_image_jbig2(
*, pike: pikepdf.Pdf, root: Path, image: Object, xref: Xref, options
) -> Optional[XrefExt]:
del options # unused arg
result = extract_image_filter(pike, root, image, xref)
if result is None:
return None
@@ -112,14 +118,29 @@ def extract_image_jbig2(
and filtdp[0] != Name.JBIG2Decode
and jbig2enc.available()
):
try:
imgname = 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 XrefExt(xref, ext)
# Save any colorspace associated with the image, so that we
# will export a pure 1-bit PNG with no palette or ICC profile.
# Showing the palette or ICC to jbig2enc will cause it to perform
# colorspace transform to 1bpp, which will conflict the palette or
# ICC if it exists.
colorspace = pim.obj.get(pikepdf.Name.ColorSpace, None)
if colorspace is not None or pim.image_mask:
try:
# Set to DeviceGray temporarily; we already in 1 bpc.
pim.obj.ColorSpace = pikepdf.Name.DeviceGray
imgname = 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
finally:
# Restore image colorspace after temporarily setting it to DeviceGray
if colorspace is not None:
pim.obj.ColorSpace = colorspace
else:
del pim.obj.ColorSpace
return XrefExt(xref, ext)
return None
@@ -135,11 +156,6 @@ def extract_image_generic(
if pim.bits_per_component == 1:
return None
try:
pim.indexed # pikepdf 1.6.3 can't handle [/Indexed [/Array...]]
except NotImplementedError:
return None
if filtdp[0] == Name.DCTDecode and options.optimize >= 2:
# This is a simple heuristic derived from some training data, that has
# about a 70% chance of guessing whether the JPEG is high quality,
@@ -195,7 +211,10 @@ def extract_image_generic(
def extract_images(
pike: Pdf, root: Path, options, extract_fn: Callable[..., Optional[XrefExt]],
pike: Pdf,
root: Path,
options,
extract_fn: Callable[..., Optional[XrefExt]],
) -> Iterator[Tuple[int, XrefExt]]:
"""Extract image using extract_fn
@@ -229,7 +248,9 @@ def extract_images(
# Ignore soft masks
smask_xref = Xref(image.SMask.objgen[0])
exclude_xrefs.add(smask_xref)
log.debug(f"Skipping image {smask_xref} because it is an SMask")
include_xrefs.add(xref)
log.debug(f"Treating {xref} as an optimization candidate")
if xref not in pageno_for_xref:
pageno_for_xref[xref] = pageno
@@ -240,8 +261,8 @@ def extract_images(
result = extract_fn(
pike=pike, root=root, image=image, xref=xref, options=options
)
except Exception as e: # pylint: disable=broad-except
log.debug("Image xref %s, error %s", xref, repr(e))
except Exception: # pylint: disable=broad-except
log.exception(f"While extracting image xref {xref}, an error occurred")
errors += 1
else:
if result:
@@ -274,26 +295,22 @@ def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefE
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),))
return jbig2_groups
def _produce_jbig2_images(
jbig2_groups: Dict[int, List[XrefExt]], root: Path, options
jbig2_groups: Dict[int, List[XrefExt]], root: Path, options, executor: Executor
) -> None:
"""Produce JBIG2 images from their groups"""
def jbig2_group_args(root: Path, groups: Dict[int, List[XrefExt]]):
for group, xref_exts in groups.items():
prefix = f'group{group:08d}'
yield dict(
cwd=fspath(root),
infiles=(img_name(root, xref, ext) for xref, ext in xref_exts),
out_prefix=prefix,
yield (
fspath(root), # =cwd
(img_name(root, xref, ext) for xref, ext in xref_exts), # =infiles
prefix, # =out_prefix
)
def jbig2_single_args(root, groups: Dict[int, List[XrefExt]]):
@@ -302,23 +319,20 @@ def _produce_jbig2_images(
# Second loop is to ensure multiple images per page are unpacked
for n, xref_ext in enumerate(xref_exts):
xref, ext = xref_ext
yield dict(
cwd=fspath(root),
infile=img_name(root, xref, ext),
outfile=root / f'{prefix}.{n:04d}',
yield (
fspath(root),
img_name(root, xref, ext),
root / f'{prefix}.{n:04d}',
)
def convert_generic(fn, kwargs_dict):
return fn(**kwargs_dict)
if options.jbig2_page_group_size > 1:
jbig2_args = jbig2_group_args
jbig2_convert = partial(convert_generic, jbig2enc.convert_group)
jbig2_convert = jbig2enc.convert_group_mp
else:
jbig2_args = jbig2_single_args
jbig2_convert = partial(convert_generic, jbig2enc.convert_single)
jbig2_convert = jbig2enc.convert_single_mp
exec_progress_pool(
executor(
use_threads=True,
max_workers=options.jobs,
tqdm_kwargs=dict(
@@ -333,7 +347,11 @@ def _produce_jbig2_images(
def convert_to_jbig2(
pike: Pdf, jbig2_groups: Dict[int, List[XrefExt]], root: Path, options
pike: Pdf,
jbig2_groups: Dict[int, List[XrefExt]],
root: Path,
options,
executor: Executor,
) -> None:
"""Convert images to JBIG2 and insert into PDF.
@@ -348,7 +366,7 @@ def convert_to_jbig2(
and needs no dictionary. Currently this must be lossless JBIG2.
"""
_produce_jbig2_images(jbig2_groups, root, options)
_produce_jbig2_images(jbig2_groups, root, options, executor)
for group, xref_exts in jbig2_groups.items():
prefix = f'group{group:08d}'
@@ -372,27 +390,53 @@ def convert_to_jbig2(
)
def transcode_jpegs(pike: Pdf, jpegs: Sequence[Xref], root: Path, options) -> None:
for xref in tqdm(
jpegs, desc="JPEGs", unit='image', disable=not options.progress_bar
):
in_jpg = jpg_name(root, xref)
opt_jpg = in_jpg.with_suffix('.opt.jpg')
def _optimize_jpeg(args):
xref, in_jpg, opt_jpg, jpeg_quality = args
# This produces a debug warning from PIL
# DEBUG:PIL.Image:Error closing: 'NoneType' object has no attribute
# 'close'. Seems to be mostly harmless
# https://github.com/python-pillow/Pillow/issues/1144
with Image.open(in_jpg) as im:
im.save(opt_jpg, optimize=True, quality=options.jpeg_quality)
# This may produce a debug warning from PIL
# DEBUG:PIL.Image:Error closing: 'NoneType' object has no attribute
# 'close'. Seems to be mostly harmless
# https://github.com/python-pillow/Pillow/issues/1144
with Image.open(in_jpg) as im:
im.save(opt_jpg, optimize=True, quality=jpeg_quality)
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
log.debug("xref %s, jpeg, made larger - skip", xref)
continue
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
log.debug("xref %s, jpeg, made larger - skip", xref)
opt_jpg.unlink()
opt_jpg = None
return xref, opt_jpg
compdata = leptonica.CompressedData.open(opt_jpg)
im_obj = pike.get_object(xref, 0)
im_obj.write(compdata.read(), filter=Name.DCTDecode)
def transcode_jpegs(
pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor
) -> None:
def jpeg_args():
for xref in jpegs:
in_jpg = jpg_name(root, xref)
opt_jpg = in_jpg.with_suffix('.opt.jpg')
yield xref, in_jpg, opt_jpg, options.jpeg_quality
def finish_jpeg(result, pbar):
xref, opt_jpg = result
if opt_jpg:
compdata = leptonica.CompressedData.open(opt_jpg)
im_obj = pike.get_object(xref, 0)
im_obj.write(compdata.read(), filter=Name.DCTDecode)
pbar.update()
executor(
use_threads=True, # Processes are significantly slower at this task
max_workers=options.jobs,
tqdm_kwargs=dict(
desc="JPEGs",
total=len(jpegs),
unit='image',
disable=not options.progress_bar,
),
task=_optimize_jpeg,
task_arguments=jpeg_args(),
task_finished=finish_jpeg,
)
def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
@@ -411,9 +455,25 @@ def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
decode_parms=local_image.DecodeParms,
)
# Don't copy keys from the new image...
del_keys = set(im_obj.keys()) - set(local_image.keys())
# ...except for the keep_fields, which are essential to displaying
# the image correctly and preserving its metadata. (/Decode arrays
# and /SMaskInData are implicitly discarded prior to this point.)
keep_fields = {
'/ID',
'/Intent',
'/Interpolate',
'/Mask',
'/Metadata',
'/OC',
'/OPI',
'/SMask',
'/StructParent',
}
del_keys -= keep_fields
for key in local_image.keys():
if key != Name.Length:
if key != Name.Length and str(key) not in keep_fields:
im_obj[key] = local_image[key]
for key in del_keys:
del im_obj[key]
@@ -426,6 +486,7 @@ def transcode_pngs(
image_name_fn: Callable[[Path, Xref], Path],
root: Path,
options,
executor,
) -> None:
modified: MutableSet[Xref] = set()
if options.optimize >= 2:
@@ -445,10 +506,7 @@ def transcode_pngs(
)
modified.add(xref)
def pngquant_fn(args):
pngquant.quantize(*args)
exec_progress_pool(
executor(
use_threads=True,
max_workers=options.jobs,
tqdm_kwargs=dict(
@@ -457,7 +515,7 @@ def transcode_pngs(
unit='image',
disable=not options.progress_bar,
),
task=pngquant_fn,
task=pngquant.quantize_mp,
task_arguments=pngquant_args(),
)
@@ -466,82 +524,13 @@ def transcode_pngs(
_transcode_png(pike, filename, xref)
@deprecated
def rewrite_png_as_g4(pike: Pdf, im_obj: Object, compdata) -> None:
im_obj.BitsPerComponent = 1
im_obj.Width = compdata.w
im_obj.Height = compdata.h
im_obj.write(compdata.read())
log.debug(f"PNG to G4 {im_obj.objgen}")
if Name.Predictor in im_obj:
del im_obj.Predictor
if Name.DecodeParms in im_obj:
del im_obj.DecodeParms
im_obj.DecodeParms = Dictionary(
K=-1, BlackIs1=bool(compdata.minisblack), Columns=compdata.w
)
im_obj.Filter = Name.CCITTFaxDecode
return
@deprecated
def rewrite_png(pike: Pdf, im_obj: Object, compdata) -> None:
# When a PNG is inserted into a PDF, we more or less copy the IDAT section from
# the PDF and transfer the rest of the PNG headers to PDF image metadata.
# One thing we have to do is tell the PDF reader whether a predictor was used
# on the image before Flate encoding. (Typically one is.)
# According to Leptonica source, PDF readers don't actually need us
# to specify the correct predictor, they just need a value of either:
# 1 - no predictor
# 10-14 - there is a predictor
# Leptonica's compdata->predictor only tells TRUE or FALSE
# 10-14 means the actual predictor is specified in the data, so for any
# number >= 10 the PDF reader will use whatever the PNG data specifies.
# In practice Leptonica should use Paeth, 14, but 15 seems to be the
# designated value for "optimal". So we will use 15.
# See:
# - PDF RM 7.4.4.4 Table 10
# - https://github.com/DanBloomberg/leptonica/blob/master/src/pdfio2.c#L757
predictor = 15 if compdata.predictor > 0 else 1
dparms = Dictionary(Predictor=predictor)
if predictor > 1:
dparms.BitsPerComponent = compdata.bps # Yes, this is redundant
dparms.Colors = compdata.spp
dparms.Columns = compdata.w
im_obj.BitsPerComponent = compdata.bps
im_obj.Width = compdata.w
im_obj.Height = compdata.h
log.debug(
f"PNG {im_obj.objgen}: palette={compdata.ncolors} spp={compdata.spp} bps={compdata.bps}"
)
if compdata.ncolors > 0:
# .ncolors is the number of colors in the palette, not the number of
# colors used in a true color image. The palette string is always
# given as RGB tuples even when the image is grayscale; see
# https://github.com/DanBloomberg/leptonica/blob/master/src/colormap.c#L2067
palette_pdf_string = compdata.get_palette_pdf_string()
palette_data = pikepdf.Object.parse(palette_pdf_string)
palette_stream = pikepdf.Stream(pike, bytes(palette_data))
palette = [Name.Indexed, Name.DeviceRGB, compdata.ncolors - 1, palette_stream]
cs = palette
else:
# ncolors == 0 means we are using a colorspace without a palette
if compdata.spp == 1:
cs = Name.DeviceGray
elif compdata.spp == 3:
cs = Name.DeviceRGB
elif compdata.spp == 4:
cs = Name.DeviceCMYK
im_obj.ColorSpace = cs
im_obj.write(compdata.read(), filter=Name.FlateDecode, decode_parms=dparms)
def optimize(input_file: Path, output_file: Path, context, save_settings) -> None:
def optimize(
input_file: Path,
output_file: Path,
context,
save_settings,
executor: Executor = SerialExecutor(),
) -> None:
options = context.options
if options.optimize == 0:
safe_symlink(input_file, output_file)
@@ -559,14 +548,14 @@ def optimize(input_file: Path, output_file: Path, context, save_settings) -> Non
root.mkdir(exist_ok=True)
jpegs, pngs = extract_images_generic(pike, root, options)
transcode_jpegs(pike, jpegs, root, options)
transcode_jpegs(pike, jpegs, root, options, executor)
# if options.optimize >= 2:
# Try pngifying the jpegs
# transcode_pngs(pike, jpegs, jpg_name, root, options)
transcode_pngs(pike, pngs, png_name, root, options)
transcode_pngs(pike, pngs, png_name, root, options, executor)
jbig2_groups = extract_images_jbig2(pike, root, options)
convert_to_jbig2(pike, jbig2_groups, root, options)
convert_to_jbig2(pike, jbig2_groups, root, options, executor)
target_file = output_file.with_suffix('.opt.pdf')
pike.remove_unreferenced_resources()
@@ -581,7 +570,7 @@ def optimize(input_file: Path, output_file: Path, context, save_settings) -> Non
)
ratio = input_size / output_size
savings = 1 - output_size / input_size
log.info(f"Optimize ratio: {ratio:.2f} savings: {(100 * savings):.1f}%")
log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
if savings < 0:
log.info("Image optimization did not improve the file - discarded")
+2 -1
View File
@@ -33,6 +33,7 @@ def _postscript_objdef(
objtype = '/stream' if stream_name else '/dict'
if stream_name:
assert stream_data is not None
a85_data = base64.a85encode(stream_data, adobe=True).decode('ascii')
yield f'{stream_name} ' + a85_data
yield 'def'
@@ -111,7 +112,7 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
def file_claims_pdfa(filename: Path):
"""Determines if the file claims to be PDF/A compliant
"""Determines if the file claims to be PDF/A compliant.
This only checks if the XMP metadata contains a PDF/A marker. It does not
do full PDF/A validation.
+187 -129
View File
@@ -6,23 +6,25 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import atexit
import logging
import re
from collections import defaultdict, namedtuple
from contextlib import ExitStack
from decimal import Decimal
from enum import Enum
from functools import partial
from math import hypot, isclose
from math import hypot, inf, isclose
from os import PathLike
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing import Container, Iterator, Optional, Tuple, Union
from warnings import warn
import pikepdf
from pikepdf import PdfMatrix
from pikepdf import Object, Pdf, PdfMatrix
from ocrmypdf._concurrent import exec_progress_pool
from ocrmypdf.exceptions import EncryptedPdfError
from ocrmypdf._concurrent import Executor, SerialExecutor
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
from ocrmypdf.helpers import Resolution, available_cpu_count, pikepdf_enable_mmap
from ocrmypdf.pdfinfo.layout import get_page_analysis, get_text_boxes
@@ -115,7 +117,7 @@ def _normalize_stack(graphobjs):
yield (operands, operator)
def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
"""Interpret the PDF content stream.
The stack represents the state of the PDF graphics stack. We are only
@@ -204,7 +206,7 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
)
def _get_dpi(ctm_shorthand, image_size):
def _get_dpi(ctm_shorthand, image_size) -> Resolution:
"""Given the transformation matrix and image size, find the image DPI.
PDFs do not include image resolution information within image data.
@@ -254,25 +256,29 @@ def _get_dpi(ctm_shorthand, image_size):
a, b, c, d, _, _ = ctm_shorthand
# Calculate the width and height of the image in PDF units
image_drawn_width = hypot(a, b)
image_drawn_height = hypot(c, d)
image_drawn = hypot(a, b), hypot(c, d)
# The scale of the image is pixels per unit of default user space (1/72")
scale_w = image_size[0] / image_drawn_width
scale_h = image_size[1] / image_drawn_height
# DPI = scale * 72
dpi_w = scale_w * 72.0
dpi_h = scale_h * 72.0
def calc(drawn, pixels, inches_per_pt=72.0):
# The scale of the image is pixels per unit of default user space (1/72")
scale = pixels / drawn if drawn != 0 else inf
dpi = scale * inches_per_pt
return dpi
dpi_w, dpi_h = (calc(image_drawn[n], image_size[n]) for n in range(2))
return Resolution(dpi_w, dpi_h)
class ImageInfo:
DPI_PREC = Decimal('1.000')
def __init__(self, *, name='', pdfimage=None, inline=None, shorthand=None):
def __init__(
self,
*,
name='',
pdfimage: Optional[Object] = None,
inline: Optional[Object] = None,
shorthand=None,
):
self._name = str(name)
self._shorthand = shorthand
@@ -282,6 +288,8 @@ class ImageInfo:
elif pdfimage is not None:
self._origin = 'xobject'
pim = pikepdf.PdfImage(pdfimage)
else:
raise ValueError("Either pdfimage or inline must be set")
self._width = pim.width
self._height = pim.height
@@ -355,6 +363,10 @@ class ImageInfo:
def enc(self):
return self._enc
@property
def renderable(self):
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
@property
def dpi(self):
return _get_dpi(self._shorthand, (self._width, self._height))
@@ -371,7 +383,7 @@ class ImageInfo:
).format(**class_locals)
def _find_inline_images(contentsinfo):
def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]:
"Find inline images in the contentstream"
for n, inline in enumerate(contentsinfo.inline_images):
@@ -380,7 +392,7 @@ def _find_inline_images(contentsinfo):
)
def _image_xobjects(container):
def _image_xobjects(container) -> Iterator[Tuple[Object, str]]:
"""Search for all XObject-based images in the container
Usually the container is a page, but it could also be a Form XObject
@@ -400,7 +412,7 @@ def _image_xobjects(container):
return
xobjs = resources['/XObject'].as_dict()
for xobj in xobjs:
candidate = xobjs[xobj]
candidate: Object = xobjs[xobj]
if not '/Subtype' in candidate:
continue
if candidate['/Subtype'] == '/Image':
@@ -408,7 +420,9 @@ def _image_xobjects(container):
yield (pdfimage, xobj)
def _find_regular_images(container, contentsinfo):
def _find_regular_images(
container: Object, contentsinfo: ContentsInfo
) -> Iterator[ImageInfo]:
"""Find images stored in the container's /Resources /XObject
Usually the container is a page, but it could also be a Form XObject
@@ -432,7 +446,7 @@ def _find_regular_images(container, contentsinfo):
yield ImageInfo(name=draw.name, pdfimage=pdfimage, shorthand=draw.shorthand)
def _find_form_xobject_images(pdf, container, contentsinfo):
def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: ContentsInfo):
"""Find any images that are in Form XObjects in the container
The container may be a page, or a parent Form XObject.
@@ -464,7 +478,9 @@ def _find_form_xobject_images(pdf, container, contentsinfo):
)
def _process_content_streams(*, pdf, container, shorthand=None):
def _process_content_streams(
*, pdf: Pdf, container: Object, shorthand=None
) -> Iterator[Union[VectorMarker, TextMarker, ImageInfo]]:
"""Find all individual instances of images drawn in the container
Usually the container is a page, but it may also be a Form XObject.
@@ -526,7 +542,7 @@ def _page_has_text(text_blocks, page_width, page_height) -> bool:
margin_ratio * ph, # bottom (first quadrant: bottom < top)
)
def rects_intersect(a, b):
def rects_intersect(a, b) -> bool:
"""
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
@@ -542,7 +558,7 @@ def _page_has_text(text_blocks, page_width, page_height) -> bool:
return has_text
def simplify_textboxes(miner, textbox_getter):
def simplify_textboxes(miner, textbox_getter) -> Iterator[TextboxInfo]:
"""Extract only limited content from text boxes
We do this to save memory and ensure that our objects are pickleable.
@@ -556,97 +572,51 @@ def simplify_textboxes(miner, textbox_getter):
yield TextboxInfo(box.bbox, visible, corrupt)
def _pdf_get_pageinfo(
pdf, pageno: int, infile: PathLike, check_pages, detailed_analysis: bool
):
pageinfo: Dict[str, Any] = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
page = pdf.pages[pageno]
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
check_this_page = pageno in check_pages
if check_this_page and detailed_analysis:
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)
else:
pageinfo['textboxes'] = []
pageinfo['has_text'] = None # i.e. "no information"
userunit = page.get('/UserUnit', Decimal(1.0))
if not isinstance(userunit, Decimal):
userunit = Decimal(userunit)
pageinfo['userunit'] = userunit
pageinfo['width_inches'] = width_pt * userunit / Decimal(72.0)
pageinfo['height_inches'] = height_pt * userunit / Decimal(72.0)
try:
pageinfo['rotate'] = int(page['/Rotate'])
except KeyError:
pageinfo['rotate'] = 0
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
if check_this_page:
pageinfo['has_vector'] = False
pageinfo['has_text'] = False
pageinfo['images'] = []
for ci in _process_content_streams(
pdf=pdf, container=page, shorthand=userunit_shorthand
):
if isinstance(ci, VectorMarker):
pageinfo['has_vector'] = True
elif isinstance(ci, TextMarker):
pageinfo['has_text'] = True
elif isinstance(ci, ImageInfo):
pageinfo['images'].append(ci)
else:
raise NotImplementedError()
else:
pageinfo['has_vector'] = None # i.e. "no information"
pageinfo['has_text'] = None
pageinfo['images'] = None
if pageinfo['images']:
dpi = Resolution(0.0, 0.0).take_max(image.dpi for image in pageinfo['images'])
pageinfo['dpi'] = dpi
pageinfo['width_pixels'] = int(round(dpi.x * float(pageinfo['width_inches'])))
pageinfo['height_pixels'] = int(round(dpi.y * float(pageinfo['height_inches'])))
return pageinfo
worker_pdf = None
def _pdf_pageinfo_sync_init(infile):
def _pdf_pageinfo_sync_init(pdf: Pdf, infile: Path, pdfminer_loglevel):
global worker_pdf # pylint: disable=global-statement
pikepdf_enable_mmap()
worker_pdf = pikepdf.open(infile)
logging.getLogger('pdfminer').setLevel(pdfminer_loglevel)
# If the pdf is not opened, open a copy for our worker process to use
if pdf is None:
worker_pdf = pikepdf.open(infile)
def on_process_close():
worker_pdf.close()
# Close when this process exits
atexit.register(on_process_close)
def _pdf_pageinfo_sync(args):
global worker_pdf # pylint: disable=global-statement
pageno, infile, check_pages, detailed_analysis = args
page = PageInfo(worker_pdf, pageno, infile, check_pages, detailed_analysis)
return page
pageno, thread_pdf, infile, check_pages, detailed_analysis = args
pdf = thread_pdf if thread_pdf is not None else worker_pdf
with ExitStack() as stack:
if not pdf: # When called with SerialExecutor
pdf = stack.enter_context(pikepdf.open(infile))
page = PageInfo(pdf, pageno, infile, check_pages, detailed_analysis)
return page
def _pdf_pageinfo_concurrent(
pdf, infile, progbar, max_workers, check_pages, detailed_analysis=False
pdf,
executor: Executor,
infile,
progbar,
max_workers,
check_pages,
detailed_analysis=False,
):
pages = [None] * len(pdf.pages)
def update_pageinfo(result, pbar):
page = result
if not page:
raise InputFileError("Could read a page in the PDF")
pages[page.pageno] = page
pbar.update()
@@ -654,7 +624,6 @@ def _pdf_pageinfo_concurrent(
max_workers = available_cpu_count()
total = len(pdf.pages)
contexts = ((n, infile, check_pages, detailed_analysis) for n in range(total))
use_threads = False # No performance gain if threaded due to GIL
n_workers = min(1 + len(pages) // 4, max_workers)
@@ -663,13 +632,27 @@ def _pdf_pageinfo_concurrent(
# a separate process.
use_threads = True
exec_progress_pool(
# If we use a thread, we can pass the already-open Pdf for them to use
# If we use processes, we pass a None which tells the init function to open its
# own
initial_pdf = pdf if use_threads else None
contexts = (
(n, initial_pdf, infile, check_pages, detailed_analysis) for n in range(total)
)
assert n_workers == 1 if use_threads else n_workers >= 1, "Not multithreadable"
executor(
use_threads=use_threads,
max_workers=n_workers,
tqdm_kwargs=dict(
total=total, desc="Scanning contents", unit='page', disable=not progbar
),
task_initializer=partial(_pdf_pageinfo_sync_init, infile),
worker_initializer=partial(
_pdf_pageinfo_sync_init,
initial_pdf,
infile,
logging.getLogger('pdfminer').level,
),
task=_pdf_pageinfo_sync,
task_arguments=contexts,
task_finished=update_pageinfo,
@@ -678,13 +661,87 @@ def _pdf_pageinfo_concurrent(
class PageInfo:
def __init__(self, pdf, pageno, infile, check_pages, detailed_analysis=False):
def __init__(
self,
pdf: Pdf,
pageno: int,
infile: PathLike,
check_pages: Container[int],
detailed_analysis: bool = False,
):
self._pageno = pageno
self._infile = infile
self._detailed_analysis = detailed_analysis
self._pageinfo = _pdf_get_pageinfo(
pdf, pageno, infile, check_pages, detailed_analysis
)
self._gather_pageinfo(pdf, pageno, infile, check_pages, detailed_analysis)
def _gather_pageinfo(
self,
pdf: Pdf,
pageno: int,
infile: PathLike,
check_pages: Container[int],
detailed_analysis: bool,
):
page = pdf.pages[pageno]
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
check_this_page = pageno in check_pages
if check_this_page and detailed_analysis:
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
miner = get_page_analysis(infile, pageno, pscript5_mode)
self._textboxes = list(simplify_textboxes(miner, get_text_boxes))
bboxes = (box.bbox for box in self._textboxes)
self._has_text = _page_has_text(bboxes, width_pt, height_pt)
else:
self._textboxes = []
self._has_text = None # i.e. "no information"
userunit = page.get('/UserUnit', Decimal(1.0))
if not isinstance(userunit, Decimal):
userunit = Decimal(userunit)
self._userunit = userunit
self._width_inches = width_pt * userunit / Decimal(72.0)
self._height_inches = height_pt * userunit / Decimal(72.0)
try:
self._rotate = int(page['/Rotate'])
except KeyError:
self._rotate = 0
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
if check_this_page:
self._has_vector = False
self._has_text = False
self._images = []
for ci in _process_content_streams(
pdf=pdf, container=page, shorthand=userunit_shorthand
):
if isinstance(ci, VectorMarker):
self._has_vector = True
elif isinstance(ci, TextMarker):
self._has_text = True
elif isinstance(ci, ImageInfo):
self._images.append(ci)
else:
raise NotImplementedError()
else:
self._has_vector = None # i.e. "no information"
self._has_text = None
self._images = None
self._dpi = None
if self._images:
dpi = Resolution(0.0, 0.0).take_max(
image.dpi for image in self._images if image.renderable
)
self._dpi = dpi
self._width_pixels = int(round(dpi.x * float(self._width_inches)))
self._height_pixels = int(round(dpi.y * float(self._height_inches)))
@property
def pageno(self) -> int:
@@ -692,25 +749,25 @@ class PageInfo:
@property
def has_text(self) -> bool:
return self._pageinfo['has_text']
return self._has_text
@property
def has_corrupt_text(self) -> bool:
if not self._detailed_analysis:
raise NotImplementedError('Did not do detailed analysis')
return any(tbox.is_corrupt for tbox in self._pageinfo['textboxes'])
return any(tbox.is_corrupt for tbox in self._textboxes)
@property
def has_vector(self) -> bool:
return self._pageinfo['has_vector']
return self._has_vector
@property
def width_inches(self) -> Decimal:
return self._pageinfo['width_inches']
return self._width_inches
@property
def height_inches(self) -> Decimal:
return self._pageinfo['height_inches']
return self._height_inches
@property
def width_pixels(self) -> int:
@@ -722,18 +779,18 @@ class PageInfo:
@property
def rotation(self) -> int:
return self._pageinfo.get('rotate', None)
return self._rotate
@rotation.setter
def rotation(self, value):
if value in (0, 90, 180, 270, 360, -90, -180, -270):
self._pageinfo['rotate'] = value
self._rotate = value
else:
raise ValueError("rotation must be a cardinal angle")
@property
def images(self):
return self._pageinfo['images']
return self._images
def get_textareas(
self, visible: Optional[bool] = None, corrupt: Optional[bool] = None
@@ -748,24 +805,22 @@ class PageInfo:
result = False
return result
if 'textboxes' not in self._pageinfo:
if not self._textboxes:
if visible is not None and corrupt is not None:
raise NotImplementedError('Incomplete information on textboxes')
return self._pageinfo['bboxes']
return self._textboxes
return (
obj.bbox
for obj in self._pageinfo['textboxes']
if predicate(obj, visible, corrupt)
)
return (obj.bbox for obj in self._textboxes if predicate(obj, visible, corrupt))
@property
def dpi(self) -> Resolution:
return self._pageinfo.get('dpi', Resolution(0.0, 0.0))
if self._dpi is None:
return Resolution(0.0, 0.0)
return self._dpi
@property
def userunit(self) -> Decimal:
return self._pageinfo.get('userunit', None)
return self._userunit
@property
def min_version(self) -> str:
@@ -788,10 +843,12 @@ class PdfInfo:
def __init__(
self,
infile,
*,
detailed_analysis: bool = False,
progbar: bool = False,
max_workers: int = None,
check_pages=None,
executor: Executor = SerialExecutor(),
):
self._infile = infile
if check_pages is None:
@@ -802,18 +859,19 @@ class PdfInfo:
raise EncryptedPdfError() # Triggered by encryption with empty passwd
self._pages = _pdf_pageinfo_concurrent(
pdf,
executor,
infile,
progbar,
max_workers,
check_pages=check_pages,
detailed_analysis=detailed_analysis,
)
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
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:
if '/AcroForm' in pdf.Root:
if len(pdf.Root.AcroForm.get('/Fields', [])) > 0:
self._has_acroform = True
elif '/XFA' in pdf.root.AcroForm:
elif '/XFA' in pdf.Root.AcroForm:
self._has_acroform = True
@property
+12 -6
View File
@@ -21,7 +21,7 @@ from pdfminer.pdffont import PDFSimpleFont, PDFUnicodeNotDefined
from pdfminer.pdfpage import PDFPage
from pdfminer.utils import bbox2str, matrix2str
from ocrmypdf.exceptions import EncryptedPdfError
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
STRIP_NAME = re.compile(r'[0-9]+')
@@ -223,6 +223,7 @@ def get_page_analysis(infile, pageno, pscript5_mode):
)
interp = pdfminer.pdfinterp.PDFPageInterpreter(rman, dev)
patcher = None
if pscript5_mode:
patcher = patch.multiple(
'pdfminer.pdffont.PDFType3Font',
@@ -235,12 +236,17 @@ def get_page_analysis(infile, pageno, pscript5_mode):
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()
page_iter = PDFPage.get_pages(f, pagenos=[pageno], maxpages=0)
page = next(page_iter, None)
if page is None:
raise InputFileError(
f"pdfminer could not process page {pageno} (counting from 0)."
)
interp.process_page(page)
except PDFTextExtractionNotAllowed as e:
raise EncryptedPdfError() from e
finally:
if pscript5_mode:
if patcher is not None:
patcher.stop()
return dev.get_result()
+143 -9
View File
@@ -8,24 +8,41 @@
from abc import ABC, abstractmethod, abstractstaticmethod
from argparse import ArgumentParser, Namespace
from collections import namedtuple
from logging import Handler
from pathlib import Path
from typing import TYPE_CHECKING, AbstractSet, List, Optional
import pluggy
from ocrmypdf._concurrent import Executor
from ocrmypdf.helpers import Resolution
if TYPE_CHECKING:
from PIL import Image
# pylint: disable=ungrouped-imports
from ocrmypdf._jobcontext import PageContext
from ocrmypdf.pdfinfo import PdfInfo
# pylint: enable=ungrouped-imports
hookspec = pluggy.HookspecMarker('ocrmypdf')
# pylint: disable=unused-argument
@hookspec(firstresult=True)
def get_logging_console() -> Handler:
"""Returns a custom logging handler.
Generally this is necessary when both logging output and a progress bar are both
outputting to ``sys.stderr``.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@hookspec
def add_options(parser: ArgumentParser) -> None:
"""Allows the plugin to add its own command line and API arguments.
@@ -59,7 +76,68 @@ def check_options(options: Namespace) -> None:
Note:
This hook will be called from the main process, and may modify global state
before child worker processes are forked.
"""
"""
@hookspec(firstresult=True)
def get_executor(progressbar_class) -> Executor:
"""Called to obtain an object that manages parallel execution.
This may be used to replace OCRmyPDF's default parallel execution system
with a third party alternative. For example, you could make OCRmyPDF run in a
distributed environment.
OCRmyPDF's executors are analogous to the standard Python executors in
``conconcurrent.futures``, but they do not work the same way. Executors may
be reused for different, unrelated batch operations, since all of the context
for a given job are passed to :meth:`Executor.__call__`.
Should be of type :class:`Executor` or otherwise conforming to the protocol
of that call.
Arguments:
progressbar_class: A progress bar class, which will be created when
Note:
This hook will be called from the main process, and may modify global state
before child worker processes are forked.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@hookspec(firstresult=True)
def get_progressbar_class():
"""Called to obtain a class that can be used to monitor progress.
A progress bar is assumed, but this could be used for any type of monitoring.
The class should follow a tqdm-like protocol. Calling the class should return
a new progress bar object, which is activated with ``__enter__`` and terminated
``__exit__``. An update method is called whenever the progress bar is updated.
Progress bar objects will not be reused; a new one will be created for each
group of tasks.
The progress bar is held in the main process/thread and not updated by child
process/threads. When a child notifies the parent of completed work, the
parent updates the progress bar.
The arguments are the same as `tqdm <https://github.com/tqdm/tqdm>`_ accepts.
Progress bars should never write to ``sys.stdout``, or they will corrupt the
output if OCRmyPDF writes a PDF to standard output.
The type of events that OCRmyPDF reports to a progress bar may change in
minor releases.
Here is how OCRmyPDF will use the progress bar:
Example:
pbar_class = pm.hook.get_progressbar_class()
with pbar_class(**tqdm_kwargs) as pbar:
...
pbar.update(1)
"""
@hookspec
@@ -89,9 +167,9 @@ def rasterize_pdf_page(
raster_device: str,
raster_dpi: Resolution,
pageno: int,
page_dpi: Optional[Resolution] = None,
rotation: Optional[int] = None,
filter_vector: bool = False,
page_dpi: Optional[Resolution],
rotation: Optional[int],
filter_vector: bool,
) -> Path:
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
@@ -163,11 +241,11 @@ def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
will be resized and the OCR layer misaligned. OCRmyPDF does not nothing
to enforce these constraints; it is up to the plugin to do sensible things.
OCRmyPDF will create the PDF page based on the image format used. If you
convert the image to a JPEG, the output page will be created as a JPEG, etc.
If you change the colorspace, that change will be kept. Note that the
OCRmyPDF image optimization stage, if enabled, may ultimately chose a
different format.
OCRmyPDF will create the PDF page based on the image format used (unless the
hook is overriden). If you convert the image to a JPEG, the output page will
be created as a JPEG, etc. If you change the colorspace, that change will be
kept. Note that the OCRmyPDF image optimization stage, if enabled, may
ultimately chose a different format.
If the return value is a file that does not exist, ``FileNotFoundError``
will occur. The return value should be a path to a file in the same folder
@@ -185,6 +263,50 @@ def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
"""
@hookspec(firstresult=True)
def filter_pdf_page(
page: 'PageContext', image_filename: Path, output_pdf: Path
) -> Path:
"""Called to convert a filtered whole page image into a PDF.
A whole page image is only produced when preprocessing command line arguments
are issued or when ``--force-ocr`` is issued. If no whole page is image is
produced for a given page, this function will not be called. This is not
the image that will be shown to OCR. The whole page image is filtered in
the hook above, ``filter_page_image``, then this function is called for
PDF conversion.
This function will only be called when OCRmyPDF runs in a mode such as
"force OCR" mode where rasterizing of all content is performed.
Clever things could be done at this stage such as segmenting the page image into
color regions or vector equivalents.
The provider of the hook implementation is responsible for ensuring that the
OCR text layer is aligned with the PDF produced here, or text misalignment
will result.
Currently this function must produce a single page PDF or the pipeline will
fail. If the intent is to remove the PDF, then create a single page empty
PDF.
Args:
page: Context for this page.
image_filename: Filename of the input image used to create output_pdf,
for "reference" if recreating the output_pdf entirely.
output_pdf: The previous created output_pdf.
Returns:
output_pdf
Note:
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
"""Expresses an OCR engine's confidence in page rotation.
@@ -280,6 +402,7 @@ def generate_pdfa(
compression: str,
pdf_version: str,
pdfa_part: str,
progressbar_class,
) -> Path:
"""Generate a PDF/A.
@@ -302,10 +425,21 @@ def generate_pdfa(
At its own discretion, the PDF/A generator may raise the version,
but should not lower it.
pdfa_part: The desired PDF/A compliance level, such as ``'2B'``.
progressbar_class: The class of a progress bar with a tqdm-like API. An
instance of this class will be initialized when PDF/A conversion
begins, using
``instance = progressbar_class(total: int, desc: str, unit:str)``,
defining the number of work units, a user-visible description,
and the name of the work units ("page"). Then ``instance.update()``
will be called when a work unit is completed. If ``None``, no
progress information is reported.
Returns:
Path: If successful, the hook should return ``output_file``.
Note:
This is a :ref:`firstresult hook<firstresult>`.
See also:
https://github.com/tqdm/tqdm
"""
+1
View File
@@ -0,0 +1 @@
# ocrmypdf is typed
@@ -10,29 +10,94 @@
import logging
import os
import re
import shutil
import sys
from collections.abc import Mapping
from contextlib import suppress
from distutils.version import LooseVersion
from distutils.version import LooseVersion, Version
from functools import lru_cache
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
from subprocess import run as subprocess_run
from typing import Callable, Optional, Type, Union
from ocrmypdf.exceptions import MissingDependencyError
# pylint: disable=logging-format-interpolation
log = logging.getLogger(__name__)
def run(args, *, env=None, **kwargs):
def run(args, *, env=None, logs_errors_to_stdout=False, **kwargs):
"""Wrapper around :py:func:`subprocess.run`
The main purpose of this wrapper is to log subprocess output in an orderly
fashion that indentifies the responsible subprocess. An additional
task is that this function goes to greater lengths to find possible Windows
locations of our dependencies when they are not on the system PATH.
Arguments should be identical to ``subprocess.run``, except for following:
Arguments:
logs_errors_to_stdout: If True, indicates that the process writes its error
messages to stdout rather than stderr, so stdout should be logged
if there is an error. If False, stderr is logged. Could be used with
stderr=STDOUT, stdout=PIPE for example.
"""
args, env, process_log, _text = _fix_process_args(args, env, kwargs)
stderr = None
stderr_name = 'stderr' if not logs_errors_to_stdout else 'stdout'
try:
proc = subprocess_run(args, env=env, **kwargs)
except CalledProcessError as e:
stderr = getattr(e, stderr_name, None)
raise
else:
stderr = getattr(proc, stderr_name, None)
finally:
if process_log.isEnabledFor(logging.DEBUG) and stderr:
with suppress(AttributeError, UnicodeDecodeError):
stderr = stderr.decode('utf-8', 'replace')
if logs_errors_to_stdout:
process_log.debug("stdout/stderr = %s", stderr)
else:
process_log.debug("stderr = %s", stderr)
return proc
def run_polling_stderr(args, *, callback, check=False, env=None, **kwargs):
"""Run a process like ``ocrmypdf.subprocess.run``, and poll stderr.
Every line of produced by stderr will be forwarded to the callback function.
The intended use is monitoring progress of subprocesses that output their
own progress indicators. In addition, each line will be logged if debug
logging is enabled.
Requires stderr to be opened in text mode for ease of handling errors. In
addition the expected encoding= and errors= arguments should be set. Note
that if stdout is already set up, it need not be binary.
"""
args, env, process_log, text = _fix_process_args(args, env, kwargs)
assert text, "Must use text=True"
with Popen(args, env=env, **kwargs) as proc:
lines = []
while proc.poll() is None:
for msg in iter(proc.stderr.readline, ''):
if process_log.isEnabledFor(logging.DEBUG):
process_log.debug(msg.strip())
callback(msg)
lines.append(msg)
stderr = ''.join(lines)
if check and proc.returncode != 0:
raise CalledProcessError(proc.returncode, args, output=None, stderr=stderr)
return CompletedProcess(args, proc.returncode, None, stderr=stderr)
def _fix_process_args(args, env, kwargs):
assert 'universal_newlines' not in kwargs, "Use text= instead of universal_newlines"
if not env:
env = os.environ
@@ -40,53 +105,23 @@ def run(args, *, env=None, **kwargs):
program = args[0]
if os.name == 'nt':
args = _fix_windows_args(program, args, env)
from ocrmypdf.subprocess._windows import fix_windows_args
args = fix_windows_args(program, args, env)
log.debug("Running: %s", args)
process_log = log.getChild('subprocess.' + os.path.basename(program))
if sys.version_info < (3, 7) and os.name == 'nt':
# Can't use close_fds=True on Windows with Python 3.6 or older
# https://bugs.python.org/issue19575, etc.
kwargs['close_fds'] = False
stderr = None
try:
proc = subprocess_run(args, env=env, **kwargs)
except CalledProcessError as e:
stderr = getattr(e, 'stderr', None)
raise
else:
stderr = getattr(proc, 'stderr', None)
finally:
if process_log.isEnabledFor(logging.DEBUG) and stderr:
with suppress(AttributeError, UnicodeDecodeError):
stderr = stderr.decode('utf-8', 'replace')
process_log.debug("stderr = %s", stderr)
return proc
def _fix_windows_args(program, args, env):
"""Adjust our desired program and command line arguments for use on Windows"""
if sys.version_info < (3, 8):
# bpo-33617 - Windows needs manual Path -> str conversion
args = [os.fspath(arg) for arg in args]
program = os.fspath(program)
# If we are running a .py on Windows, ensure we call it with this Python
# (to support test suite shims)
if program.lower().endswith('.py'):
args = [sys.executable] + args
paths = os.pathsep.join(os.get_exec_path(env))
if not shutil.which(args[0], path=paths):
# If the program we want is not on the PATH, add some interesting
# locations in %PROGRAMFILES% to the PATH and try again
shimmed_path = shim_paths_with_program_files(env)
new_args0 = shutil.which(args[0], path=shimmed_path)
if new_args0:
args[0] = new_args0
return args
process_log = log.getChild(os.path.basename(program))
text = kwargs.get('text', False)
if sys.version_info < (3, 7):
if os.name == 'nt':
# Can't use close_fds=True on Windows with Python 3.6 or older
# https://bugs.python.org/issue19575, etc.
kwargs['close_fds'] = False
if 'text' in kwargs:
# Convert run(...text=) to run(...universal_newlines=) for Python 3.6
kwargs['universal_newlines'] = kwargs['text']
del kwargs['text']
return args, env, process_log, text
@lru_cache(maxsize=None)
@@ -107,7 +142,7 @@ def get_version(
proc = run(
args_prog,
close_fds=True,
universal_newlines=True,
text=True,
stdout=PIPE,
stderr=STDOUT,
check=True,
@@ -126,44 +161,18 @@ def get_version(
raise MissingDependencyError(
f"Could not find program '{program}' on the PATH"
) from e
try:
version = re.match(regex, output.strip()).group(1)
except AttributeError as e:
match = re.match(regex, output.strip())
if not match:
raise MissingDependencyError(
f"The program '{program}' did not report its version. "
f"Message was:\n{output}"
)
version = match.group(1)
return version
def shim_paths_with_program_files(env=None):
if not env:
env = os.environ
program_files = env.get('PROGRAMFILES', '')
if not program_files:
return env.get('PATH', '')
def path_walker():
for path in Path(program_files).iterdir():
if not path.is_dir():
continue
if path.name.lower() == 'tesseract-ocr':
yield path
elif path.name.lower() == 'gs':
yield from (p for p in path.glob('**/bin') if p.is_dir())
paths = sorted(
(p for p in path_walker()), key=lambda p: (p.name, p.parent.name), reverse=True
)
paths.extend(
Path(str_path)
for str_path in os.get_exec_path(env)
if Path(str_path) not in set(paths)
)
return os.pathsep.join(str(p) for p in paths)
missing_program = '''
The program '{program}' could not be executed or was not found on your
system PATH.
@@ -258,13 +267,30 @@ def _error_old_version(program, package, need_version, found_version, required_f
def check_external_program(
*,
program,
package,
version_checker,
need_version,
required_for=None,
program: str,
package: str,
version_checker: Union[str, Callable],
need_version: str,
required_for: Optional[str] = None,
recommended=False,
version_parser: Type[Version] = LooseVersion,
):
"""Check for required version of external program and raise exception if not.
Args:
program: The name of the program to test.
package: The name of a software package that typically supplies this program.
Usually the same as program.
version_check: A callable without arguments that retrieves the installed
version of program.
need_version: The minimum required version.
required_for: The name of an argument of feature that requires this program.
recommended: If this external program is recommended, instead of raising
an exception, log a warning and allow execution to continue.
version_parser: A class that should be used to parse and compare version
numbers. Used when version numbers do not follow standard conventions.
"""
try:
if callable(version_checker):
found_version = version_checker()
@@ -273,7 +299,7 @@ def check_external_program(
except (CalledProcessError, FileNotFoundError, MissingDependencyError):
_error_missing_program(program, package, required_for, recommended)
if not recommended:
raise MissingDependencyError()
raise MissingDependencyError(program)
return
def remove_leading_v(s):
@@ -284,9 +310,9 @@ def check_external_program(
found_version = remove_leading_v(found_version)
need_version = remove_leading_v(need_version)
if found_version and LooseVersion(found_version) < LooseVersion(need_version):
if found_version and version_parser(found_version) < version_parser(need_version):
_error_old_version(program, package, need_version, found_version, required_for)
if not recommended:
raise MissingDependencyError()
raise MissingDependencyError(program)
log.debug('Found %s %s', program, found_version)
+162
View File
@@ -0,0 +1,162 @@
# © 2020 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import logging
import os
import shutil
import sys
from distutils.version import LooseVersion
from itertools import chain, filterfalse
from pathlib import Path
from typing import Any, Callable, Iterator, Optional, Tuple, TypeVar, cast
try:
import winreg
except ModuleNotFoundError as e:
raise ModuleNotFoundError("This module is for Windows only") from e
log = logging.getLogger(__name__)
T = TypeVar('T')
def registry_enum(
key: winreg.HKEYType, enum_fn: Callable[[winreg.HKEYType, int], T]
) -> Iterator[T]:
LIMIT = 999
n = 0
while n < LIMIT:
try:
yield enum_fn(key, n)
n += 1
except OSError:
break
if n == LIMIT:
raise ValueError(f"Too many registry keys under {key}")
def registry_subkeys(key: winreg.HKEYType) -> Iterator[str]:
return registry_enum(key, winreg.EnumKey)
def registry_values(key: winreg.HKEYType) -> Iterator[Tuple[str, Any, int]]:
return registry_enum(key, winreg.EnumValue)
def registry_path_ghostscript(env=None) -> Iterator[Path]:
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Artifex\GPL Ghostscript"
) as k:
latest_gs = max(registry_subkeys(k), key=LooseVersion)
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
) as k:
_, gs_path, _ = next(registry_values(k))
yield Path(gs_path) / 'bin'
except OSError as e:
log.warning(e)
def registry_path_tesseract(env=None) -> Iterator[Path]:
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Tesseract-OCR") as k:
for subkey, val, _valtype in registry_values(k):
if subkey == 'InstallDir':
tesseract_path = Path(val)
yield tesseract_path
except OSError as e:
log.warning(e)
def program_files_paths(env=None) -> Iterator[Path]:
if not env:
env = os.environ
program_files = env.get('PROGRAMFILES', '')
def path_walker() -> Iterator[Path]:
for path in Path(program_files).iterdir():
if not path.is_dir():
continue
if path.name.lower() == 'tesseract-ocr':
yield path
elif path.name.lower() == 'gs':
yield from (p for p in path.glob('**/bin') if p.is_dir())
return iter(
sorted(
(p for p in path_walker()),
key=lambda p: (p.name, p.parent.name),
reverse=True,
)
)
def paths_from_env(env=None) -> Iterator[Path]:
return (Path(p) for p in os.get_exec_path(env) if p)
def shim_path(new_paths: Callable[[Any], Iterator[Path]], env=None) -> str:
if not env:
env = os.environ
return os.pathsep.join(str(p) for p in new_paths(env) if p)
SHIMS = [
paths_from_env,
registry_path_ghostscript,
registry_path_tesseract,
program_files_paths,
]
def fix_windows_args(program, args, env):
"""Adjust our desired program and command line arguments for use on Windows"""
if sys.version_info < (3, 8):
# bpo-33617 - Windows needs manual Path -> str conversion
args = [os.fspath(arg) for arg in args]
program = os.fspath(program)
# If we are running a .py on Windows, ensure we call it with this Python
# (to support test suite shims)
if program.lower().endswith('.py'):
args = [sys.executable] + args
# If the program we want is not on the PATH, check elsewhere
for shim in SHIMS:
shimmed_path = shim_path(shim, env)
new_args0 = shutil.which(args[0], path=shimmed_path)
if new_args0:
args[0] = new_args0
break
return args
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
key = lambda x: x
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
def shim_env_path(env=None):
if env is None:
env = os.environ
shim_paths = chain.from_iterable(shim(env) for shim in SHIMS)
return os.pathsep.join(
str(p) for p in unique_everseen(shim_paths, key=lambda p: str.casefold(str(p)))
)
+7
View File
@@ -0,0 +1,7 @@
# © 2021 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Empty __init__.py file
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.mrqsewbu/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.h7hsz1g7/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">
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.ces85e5u/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.tgp04npj/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
File diff suppressed because it is too large Load Diff
@@ -103,7 +103,7 @@ ERASE, REPEAT, PLAY/STOP, or LOCATE.
© Will sync to standard LinnDrum or Linn 9000 sync tone.
® Utilizes ultra high-speed, 8 MHz 80186 16 bit computer internally for FAST operation.
© 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!)
@@ -115,9 +115,9 @@ on the TAP TEMPO button.
¢ TEMPO CHANGES may be programmed into a sequence, with smooth transitions if desired.
¢ Any TIME SIGNATURE may be used, and may be changed within a song.
nn
linn
Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1,128 +1,123 @@
2A NNII 6F6867# XATALL IE18-80L (818)
The LinnSequencer
32 Track MIDI Sequence Recorder
9SEI6 VO “BUBZIRY, “J0aNS PIPUXO OZLEI
“Uy soTUOMOI,q UUrT
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
uut]
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
“‘SUOS B UIJIM pasueyo oq ABU pue posn oq AWW AYN IVNOIS AWLL AUV
“parlsop Jr SUOTIISUBI} YIOOUIS YIM “BoueNbas eB OJUI pourtueIZOId 9q ABU SFONWHO OdINAL e
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
uonng OdNAL dV L 9) uO
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
sojou Jayienb Suiddy} Aq 10 syUSTIOIOUI oINUTIAI-J8g-Jesg & JO sys} UL ofquisn(pe ATTeouIAUINU paiajus oq ABU OdINALL e
synthesizers!
(jouer doup u3a9)
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
“puooes Jed souely O€ 10 “SZ “pz 18 [LVAG-MAd-SHN VU 10 ALOANIWAAd-SLVAd U! patyoeds aq kewl OAL «
uoTe1odo [SVx JO} Aj[eusoyUT JoyndUIOd 11g 9] 98108 ZHI 8g poeds-ysry Bann soz] e
per disk!
"9U0} DUAS 0006 UUL] Jo wNIqUUr] prepue}s 0} OUAS [ITAA ©
¢ 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
“ONYBA 9}OU poloapes Aue Je sas—nd jndyno 07 pewureigold 3q ACW SL Ad LNO YADONAL OML
rhythmic value.
"ALVOOT 10 GOLS/AV 1d LWddad “ASV
¢ TIMING CORRECTION works during playback and operates without chopping notes.
SUIpNpoUr suOTIOUN] posn A[UOUILUOS 94] JO AUBUT [O1]UOD AJ9]OWIAI 0} PousIsse oq ACUI ST AdNI HOLIMSLOO OME «
“SUIPIONAI I[IYM P2sesd JOU Iv $3}OU BUTISIXO—ZUIPIOIA SATON.ASOP-UON
¢ Optional SMPTE time code synchronization.
suoneurldxa peuoyippe sdeydsip uowng g1TqH
© Optional remote control.
oy] pepsau JI suoneiodo [ye yYsnosy] NOA sapins ApIespo Avfdsip QO] Joey Z7¢ 9y3—uoeIodo Urea] 0} Ased aus «
Recording a Sequence
jUorel]suowtap & IO} Aepol Jayeap uur] INOA dag dISHUL
INOA 0} UONUS}]¥ PaplAIPUN INOA SUTJOASp ITY ps pue
p1osai asoduod no Jay 0} pausisap st 1s0uenbesuur] oy)
Aum Aposiooid $,Jeu], SS9d0Id SATTBS1D OY} YIM SOIOJIOIUT
yey) xo]dwWI0d Os dq JOA9U P[NoUsS osn NOA AZopOuYdE} oy
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
you ll hear what you played—only all timing errors will be
ISTUMOIAUIO?) NOAA UOHISOdWIO)
corrected! (Timing correction may be adjusted or defeated).
"NOSpr] B Oy AONUTJUT yada 0} seq Maz Se] BY] Jas UdAd
uvd NOA palisap JJ souanbes Mou ¥B OVUT sjied ou] [Te Adoo
ATesrewO Ne WI) [IM ONOS ALVAAO JeyIe80} wey}
,deyd,, 0} UOTOUNJ ONOS ALVA ou] asn usy] saouanbes
JENPIAIpUt UI (“949 snJOYD aS1OA) UOTIDIS JIseq Yes
Pl0da1 OF ST ABM JOuIOUY “(812g 666 01 dn) ysnory) ABM
Any additional notes played will be added into the track
— existing notes are not erased while recording!
dU} [fe YORI] YORs p10991 0} ST SUOS B 9789I9 0} ABM SUG,
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
SUOS & SUTVAID
Editing
*suoT}oes poJUBMUN
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
SAOUIOI 0} ABM SWS dU} SoyeIodo SUV ALATAaG
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
“OBPLIq dy} PUB SNIOY PUOdAS dT]] Ud9MIAQ SIDA ISI
Additional Features
ay) Jo Adoo B JJasuT WYSE NOAA afdwexs 10.f UO JUSIN]JIP
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
B IO aouaNbas sues OY} UI—JOY OUP 0} UOTIEIO] 9UO WOT]
$1Bq JAOUI OF NOA sMOTIe WOTIOUNS AdOO/IMASNI OULL
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections,
SUIPIONAI JIVIS Udy) “OQuINU eq porisop ay} puy
Creating a Song
0} CNIMAY 10 CYVM Od LSWA “AEVOOT esn Apduns
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
sainjeay [PUOHIPPY
Composition Without Compromise
gouanbas & UTYIIM s]UTOd a1y1dads 3¥ $9100 QnPIOAO OL "UOT}
-ouns dALLS ATONIS 24) Suisn pasueyo Jo pasesa pappe
aq osye ABUT S9]ON U0 9q ]IIM 1 “yoeq podeyd uayM
—aouanbas oy] ul skeyd 71 a10J9q Isnf posers oq 0} d]0U ayy
ssaid pue ASvwug ploy Aydunis jou Suomm & aseso OL
The technology you use should never be so complex that
it interferes with the creative process. Thats precisely why
the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
sunipa
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
jsesdueyo ureisoid pue fepod ureysns
yonoplalje AWOOTOA UOTyeTNpow pusg youd Surpnyour
pep10del are $199JJ2 TCTIN [WV iPeqqnpseao aq Aeur syoen
Ze 07 dn Kem sie Uy *(foeI} JOyOUR OJOS 10 ALLAN
NOA ssofum) duAS yOaysod ul Avy [[IM Yow] ISI 93 “prooar
NOA 3[IYM—SUIPIOIA LIBIS PU YORI) TUdIOTJIP B JOaTas
*y1ed MOU B QNPIsA0 OL, “SuIps0daJ-jods 10} aouanbes mno0k
UI UOHBIO] Aue ssad0e ATYOIND 0} owt} Aue ye pasn aq AvUE
SJONUOD FLIVOOT pur ANIMA CYVMaYOd LSVd
{SUIPIOSAI {IY posesa JOU se So]OU SuTsTXO—
yous} 3U} OUT poppe aq JIM poteyd sajou yeuonippe Auy
HELP button displays additional explanations.
*(povesjap 10 poysn{pe oq ABW UOTIIII0D BUTUTT]) j{paqoeLI09
* 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
2q ][IM S1OLIe Sur [fe ATUO—patey]d nod Jey Jedy ]],NOA
ERASE, REPEAT, PLAY/STOP, or LOCATE.
] req 0] punose yoeq sdoo] sduanbas ay] Udy AA “YOu Yor
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
§,sa0uaNbas at} O] SUIT) UI preogday [IW] INO Avy usy3
AV'1d pue (YOON ssoid Ayduus aousnbes & p1o09es OF,
© Will sync to standard LinnDrum or Linn 9000 sync tone.
g0uaNbas & SUIP10I0y]
© 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,
JONWOD s}JouNaI TeuONdGO e
(even drop frame!)
"UOTJEZIUOIYUAS OPOS UIT} FLAWS [euondo e
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
sou .sulddoys, noyyM sayelodo pue yoegdvyd ZuLINp S¥IOM NOLLOANNYOO ONIWILL e
on the TAP TEMPO button.
onqea ory AY
¢ 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.
pojoojes-oid & ye sajou pyoy Aue syeadas ATTeONewWO Ne UOTOUNS [WAdAY OAISNOX e
LSVJ SUnIpS soyeu UOTOUN ASV UA OUlN-[eal SAISNIOXY e
Koy B JO YONO} 941 12 CASOdSNVALL 0g ABU Syde] [Te 10 9UC e
linn
Linn Electronics, Inc.
i ASIP Jed
S9}0U OOOOTT JOA SpfOy puv SpUOdeS UT SBUOS Xa[AUIOD So10}S DALIP YSIP , 74 € ISCJ-CNIN
jSIOZISOUJUAS
stuoydAjod of 0} dn skeyd A[snoourynuls spouueYd [IW 9T JO duo 0} pousisse oq
ABUL YORI] YOR syous) oruoydAjod snoouelnurs 7¢ SuTeJUOS ssouUaNbas QO] OY} JO YORA e
SJONUOS ATWOOT pur GNIMAY GaVM OA
LSVd GYOOde AOLS AV Td YIM Jopsocas ade} Yowsj-N[NU O} eps st UOTLISdO @
LOPNOUT SaINjeoy s[quyIeUlss AUB S.JJ OSN pue UIes] 0} o[duns A[suIzeUe JOA PnJsomod APOUIOITXO
St 1] “UeIOIsNUL feUOIssajoid oY} 10 JOO} soUBULIOJIJAd pue UOTIsOduIOS 11e-dY1-JO-9}e)s B SI IONUANbDaguUT] ay
JOps1odady soUINbIS [GTI YVAL ZE
Jgouanbaguury oy
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
File diff suppressed because it is too large Load Diff
@@ -1,124 +1,123 @@
2A NNII 6F6867# XATALL IE18-80L (818)
The LinnSequencer
32 Track MIDI Sequence Recorder
9SEI6 VO “BUBZIRY, “J0aNS PIPUXO OZLEI
“Uy soTUOMOI,q UUrT
The LinnSequencer is a state-of-the-art composition and performance tool for the professional musician. It is
uu
extremely powerful, yet amazingly simple to learn and use. Its many remarkable features include:
“‘SUOS B UIJIM pasueyo oq ABU pue posn oq AWW AYN IVNOIS AWLL AUV
“parlsop Jr SUOTIISUBI} YIOOUIS YIM “BoueNbas eB OJUI pourtueIZOId 9q ABU SFONWHO OdINAL e
¢ Operation is similar to multi-track tape recorder with PLAY, STOP, RECORD, FAST
FORWARD, REWIND, and LOCATE controls.
uonng OdNAL dV L 9) uO
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
sojou Jayienb Suiddy} Aq 10 syUSTIOIOUI oINUTIAI-J8g-Jesg & JO sys} UL ofquisn(pe ATTeouIAUINU paiajus oq ABU OdINALL e
synthesizers!
(jouer doup u3a9)
¢ Ultra-fast 3%” disk drive stores complex songs in seconds and holds over 110,000 notes
“puooes Jed souely O€ 10 “SZ “pz 18 [LVAG-MAd-SHN VU 10 ALOANIWAAd-SLVAd U! patyoeds aq kewl OAL «
uoTe1odo [SVx JO} Aj[eusoyUT JoyndUIOd 11g 9] 98108 ZHI 8g poeds-ysry Bann soz] e
per disk!
"9U0} DUAS 0006 UUL] Jo wNIqUUr] prepue}s 0} OUAS [ITAA ©
¢ 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
“ONYBA 9}OU poloapes Aue Je sas—nd jndyno 07 pewureigold 3q ACW SL Ad LNO YADONAL OML
rhythmic value.
"ALVOOT 10 GOLS/AV 1d LWddad “ASV
¢ TIMING CORRECTION works during playback and operates without chopping notes.
SUIpNpoUr suOTIOUN] posn A[UOUILUOS 94] JO AUBUT [O1]UOD AJ9]OWIAI 0} PousIsse oq ACUI ST AdNI HOLIMSLOO OME «
“SUIPIONAI I[IYM P2sesd JOU Iv $3}OU BUTISIXO—ZUIPIOIA SATON.ASOP-UON
¢ Optional SMPTE time code synchronization.
suoneurldxa peuoyippe sdeydsip uowng g1TqH
© Optional remote control.
oy] pepsau JI suoneiodo [ye yYsnosy] NOA sapins ApIespo Avfdsip QO] Joey Z7¢ 9y3—uoeIodo Urea] 0} Ased aus «
Recording a Sequence
jUorel]suowtap & IO} Aepol Jayeap uur] INOA dag dISHUL
INOA 0} UONUS}]¥ PaplAIPUN INOA SUTJOASp ITY ps pue
p1osai asoduod no Jay 0} pausisap st 1s0uenbesuur] oy)
Aum Aposiooid $,Jeu], SS9d0Id SATTBS1D OY} YIM SOIOJIOIUT
yey) xo]dwWI0d Os dq JOA9U P[NoUsS osn NOA AZopOuYdE} oy
To record a sequence, simply press RECORD and PLAY,
then play your MIDI keyboard in time to the Sequencers
click track. When the sequence loops back around to bar 1,
you ll hear what you played—only all timing errors will be
ISTUMOIAUIO?) NOAA UOHISOdWIO)
corrected! (Timing correction may be adjusted or defeated).
"NOSpr] B Oy AONUTJUT yada 0} seq Maz Se] BY] Jas UdAd
uvd NOA palisap JJ souanbes Mou ¥B OVUT sjied ou] [Te Adoo
ATesrewO Ne WI) [IM ONOS ALVAAO JeyIe80} wey}
,deyd,, 0} UOTOUNJ ONOS ALVA ou] asn usy] saouanbes
JENPIAIpUt UI (“949 snJOYD aS1OA) UOTIDIS JIseq Yes
Pl0da1 OF ST ABM JOuIOUY “(812g 666 01 dn) ysnory) ABM
Any additional notes played will be added into the track
— existing notes are not erased while recording!
dU} [fe YORI] YORs p10991 0} ST SUOS B 9789I9 0} ABM SUG,
FAST FORWARD, REWIND, and LOCATE controls
may be used at any time to quickly access any location in
your sequence for spot-recording. To overdub a new part,
select a different track and start recording—while you
record, the first track will play in perfect sync (unless you
MUTE it, or SOLO another track). In this way, up to 32
tracks may be overdubbed! All MIDI effects are recorded
including pitch bend, modulation, velocity, aftertouch,
sustain pedal, and program changes!
SUOS & SUTVAID
Editing
*suoT}oes poJUBMUN
To erase a wrong note, simply hold ERASE and press
the note to be erased just before it plays in the sequence—
when played back, it will be gone. Notes may also be
SAOUIOI 0} ABM SWS dU} SoyeIodo SUV ALATAaG
added, erased, or changed using the SINGLE STEP func-
tion. To overdub notes at specific points within a sequence,
“OBPLIq dy} PUB SNIOY PUOdAS dT]] Ud9MIAQ SIDA ISI
Additional Features
ay) Jo Adoo B JJasuT WYSE NOAA afdwexs 10.f UO JUSIN]JIP
simply use LOCATE, FAST FORWARD, or REWIND to
find the desired bar number, then start recording.
B IO aouaNbas sues OY} UI—JOY OUP 0} UOTIEIO] 9UO WOT]
$1Bq JAOUI OF NOA sMOTIe WOTIOUNS AdOO/IMASNI OULL
The INSERT/COPY function allows you to move bars
from one location to another—in the same sequence or a
different one. For example, you might insert a copy of the
first verse between the second chorus and the bridge.
DELETE BARS operates the same way to remove
unwanted sections,
SUIPIONAI JIVIS Udy) “OQuINU eq porisop ay} puy
Creating a Song
0} CNIMAY 10 CYVM Od LSWA “AEVOOT esn Apduns
One way to create a song is to record each track all the
way through (up to 999 bars). Another way is to record
each basic section (verse, chorus, etc.) in individual
sequences, then use the CREATE SONG function to “chain”
them together. CREATE SONG will then automatically
copy all the parts into a new sequence. If desired, you can
even set the last few bars to repeat infinitely, for a fadeout.
sainjeay [PUOHIPPY
Composition Without Compromise
gouanbas & UTYIIM s]UTOd a1y1dads 3¥ $9100 QnPIOAO OL "UOT}
-ouns dALLS ATONIS 24) Suisn pasueyo Jo pasesa pappe
aq osye ABUT S9]ON U0 9q ]IIM 1 “yoeq podeyd uayM
—aouanbas oy] ul skeyd 71 a10J9q Isnf posers oq 0} d]0U ayy
ssaid pue ASvwug ploy Aydunis jou Suomm & aseso OL
The technology you use should never be so complex that
it interferes with the creative process. Thats precisely why
the LinnSequencer is designed to let you compose, record
and edit while devoting your undivided attention to your
music. See your Linn dealer today for a demonstration!
sunipa
* Simple, easy to learn operation—the 32 character LCD display clearly guides you through all operations. If needed, the
jsesdueyo ureisoid pue fepod ureysns
yonoplalje AWOOTOA UOTyeTNpow pusg youd Surpnyour
pep10del are $199JJ2 TCTIN [WV iPeqqnpseao aq Aeur syoen
Ze 07 dn Kem sie Uy *(foeI} JOyOUR OJOS 10 ALLAN
NOA ssofum) duAS yOaysod ul Avy [[IM Yow] ISI 93 “prooar
NOA 3[IYM—SUIPIOIA LIBIS PU YORI) TUdIOTJIP B JOaTas
*y1ed MOU B QNPIsA0 OL, “SuIps0daJ-jods 10} aouanbes mno0k
UI UOHBIO] Aue ssad0e ATYOIND 0} owt} Aue ye pasn aq AvUE
SJONUOD FLIVOOT pur ANIMA CYVMaYOd LSVd
{SUIPIOSAI {IY posesa JOU se So]OU SuTsTXO—
yous} 3U} OUT poppe aq JIM poteyd sajou yeuonippe Auy
*(povesjap 10 poysn{pe oq ABW UOTIIII0D BUTUTT]) j{paqoeLI09
2q ][IM S1OLIe Sur [fe ATUO—patey]d nod Jey Jedy ]],NOA
] req 0] punose yoeq sdoo] sduanbas ay] Udy AA “YOu Yor
§,sa0uaNbas at} O] SUIT) UI preogday [IW] INO Avy usy3
AV'1d pue (YOON ssoid Ayduus aousnbes & p1o09es OF,
HELP button displays additional explanations.
g0uaNbas & SUIP10I0y]
* 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
JONWOD s}JouNaI TeuONdGO e
ERASE, REPEAT, PLAY/STOP, or LOCATE.
"UOTJEZIUOIYUAS OPOS UIT} FLAWS [euondo e
¢ Iwo TRIGGER OUTPUTS may be programmed to output pulses at any selected note value.
sou .sulddoys, noyyM sayelodo pue yoegdvyd ZuLINp S¥IOM NOLLOANNYOO ONIWILL e
© Will sync to standard LinnDrum or Linn 9000 sync tone.
onqea ory AY
© 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,
pojoojes-oid & ye sajou pyoy Aue syeadas ATTeONewWO Ne UOTOUNS [WAdAY OAISNOX e
LSVJ SUnIpS soyeu UOTOUN ASV UA OUlN-[eal SAISNIOXY e
Koy B JO YONO} 941 12 CASOdSNVALL 0g ABU Syde] [Te 10 9UC e
(even drop frame!)
i ASIP Jed
¢ TEMPO may be entered numerically, adjustable in tenths of a Beat-Per-Minute increments, or by tapping quarter notes
S9}0U OOOOTT JOA SpfOy puv SpUOdeS UT SBUOS Xa[AUIOD So10}S DALIP YSIP , 74 € ISCJ-CNIN
on the TAP TEMPO button.
jSIOZISOUJUAS
¢ 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.
stuoydAjod of 0} dn skeyd A[snoourynuls spouueYd [IW 9T JO duo 0} pousisse oq
ABUL YORI] YOR syous) oruoydAjod snoouelnurs 7¢ SuTeJUOS ssouUaNbas QO] OY} JO YORA e
linn
Linn Electronics, Inc.
SJONUOS ATWOOT pur GNIMAY GaVM OA
LSVd GYOOde AOLS AV Td YIM Jopsocas ade} Yowsj-N[NU O} eps st UOTLISdO @
LOPNOUT SaINjeoy s[quyIeUlss AUB S.JJ OSN pue UIes] 0} o[duns A[suIzeUe JOA PnJsomod APOUIOITXO
St 1] “UeIOIsNUL feUOIssajoid oY} 10 JOO} soUBULIOJIJAd pue UOTIsOduIOS 11e-dY1-JO-9}e)s B SI IONUANbDaguUT] ay
JOps1odady soUINbIS [GTI YVAL ZE
Jgouanbaguury oy
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.ucmqv4tt/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.sly7uqzv/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -29,7 +29,7 @@
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 347 380 2188 423">
<span class='ocr_header' id='line_1_3' title="bbox 347 380 2188 423; baseline -0.001 -12; x_size 38; x_descenders 8; x_ascenders 10">
<span class='ocrx_word' id='word_1_8' title='bbox 347 380 412 410; x_wconf 93'>The</span>
<span class='ocrx_word' id='word_1_9' title='bbox 424 380 676 417; x_wconf 92'>LinnSequencer</span>
<span class='ocrx_word' id='word_1_9' title='bbox 424 380 676 417; x_wconf 91'>LinnSequencer</span>
<span class='ocrx_word' id='word_1_10' title='bbox 688 380 712 411; x_wconf 96'>is</span>
<span class='ocrx_word' id='word_1_11' title='bbox 724 390 743 411; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_12' title='bbox 754 381 1005 423; x_wconf 96'>state-of-the-art</span>
@@ -39,7 +39,7 @@
<span class='ocrx_word' id='word_1_16' title='bbox 1536 380 1602 411; x_wconf 96'>tool</span>
<span class='ocrx_word' id='word_1_17' title='bbox 1615 380 1663 411; x_wconf 97'>for</span>
<span class='ocrx_word' id='word_1_18' title='bbox 1674 381 1725 410; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_19' title='bbox 1737 380 1940 417; x_wconf 95'>professional</span>
<span class='ocrx_word' id='word_1_19' title='bbox 1737 380 1940 417; x_wconf 96'>professional</span>
<span class='ocrx_word' id='word_1_20' title='bbox 1952 380 2112 411; x_wconf 96'>musician.</span>
<span class='ocrx_word' id='word_1_21' title='bbox 2127 381 2152 410; x_wconf 96'>It</span>
<span class='ocrx_word' id='word_1_22' title='bbox 2164 380 2188 410; x_wconf 96'>is</span>
@@ -209,7 +209,7 @@
<div class='ocr_carea' id='block_1_8' title="bbox 382 1021 689 1065">
<p class='ocr_par' id='par_1_10' lang='eng' title="bbox 382 1021 689 1065">
<span class='ocr_line' id='line_1_15' title="bbox 382 1021 689 1065; baseline 0.003 -8; x_size 45; x_descenders 8; x_ascenders 12">
<span class='ocrx_word' id='word_1_135' title='bbox 382 1021 564 1065; x_wconf 96'>rhythmic</span>
<span class='ocrx_word' id='word_1_135' title='bbox 382 1021 564 1065; x_wconf 95'>rhythmic</span>
<span class='ocrx_word' id='word_1_136' title='bbox 577 1021 689 1058; x_wconf 96'>value.</span>
</span>
</p>
@@ -246,7 +246,7 @@
<div class='ocr_carea' id='block_1_11' title="bbox 349 1195 874 1240">
<p class='ocr_par' id='par_1_13' lang='eng' title="bbox 349 1195 874 1240">
<span class='ocr_line' id='line_1_18' title="bbox 349 1195 874 1240; baseline 0 -8; x_size 45; x_descenders 8; x_ascenders 12">
<span class='ocrx_word' id='word_1_154' title='bbox 349 1203 367 1222; x_wconf 74'>©</span>
<span class='ocrx_word' id='word_1_154' title='bbox 349 1203 367 1222; x_wconf 70'>©</span>
<span class='ocrx_word' id='word_1_155' title='bbox 382 1195 560 1240; x_wconf 96'>Optional</span>
<span class='ocrx_word' id='word_1_156' title='bbox 573 1201 709 1233; x_wconf 96'>remote</span>
<span class='ocrx_word' id='word_1_157' title='bbox 723 1196 874 1233; x_wconf 95'>control.</span>
@@ -300,8 +300,8 @@
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 74'>1,</span>
</span>
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 14'>you</span>
<span class='ocrx_word' id='word_1_192' title='bbox 406 1453 436 1496; x_wconf 14'>ll</span>
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 15'>you</span>
<span class='ocrx_word' id='word_1_192' title='bbox 406 1453 439 1496; x_wconf 15'>ll</span>
<span class='ocrx_word' id='word_1_193' title='bbox 441 1457 506 1485; x_wconf 96'>hear</span>
<span class='ocrx_word' id='word_1_194' title='bbox 517 1458 590 1485; x_wconf 96'>what</span>
<span class='ocrx_word' id='word_1_195' title='bbox 600 1466 654 1491; x_wconf 93'>you</span>
@@ -361,7 +361,7 @@
<span class='ocrx_word' id='word_1_230' title='bbox 684 1617 844 1648; x_wconf 95'>REWIND,</span>
<span class='ocrx_word' id='word_1_231' title='bbox 857 1616 912 1643; x_wconf 95'>and</span>
<span class='ocrx_word' id='word_1_232' title='bbox 924 1616 1068 1643; x_wconf 96'>LOCATE</span>
<span class='ocrx_word' id='word_1_233' title='bbox 1079 1616 1199 1643; x_wconf 95'>controls</span>
<span class='ocrx_word' id='word_1_233' title='bbox 1079 1616 1199 1643; x_wconf 96'>controls</span>
</span>
<span class='ocr_line' id='line_1_28' title="bbox 346 1655 1202 1689; baseline 0 -7; x_size 34; x_descenders 6; x_ascenders 9">
<span class='ocrx_word' id='word_1_234' title='bbox 346 1663 409 1688; x_wconf 92'>may</span>
@@ -400,9 +400,9 @@
</span>
<span class='ocr_line' id='line_1_31' title="bbox 346 1773 1203 1808; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_263' title='bbox 346 1774 448 1806; x_wconf 96'>record,</span>
<span class='ocrx_word' id='word_1_264' title='bbox 460 1774 506 1801; x_wconf 97'>the</span>
<span class='ocrx_word' id='word_1_265' title='bbox 503 1769 577 1812; x_wconf 96'>first</span>
<span class='ocrx_word' id='word_1_266' title='bbox 581 1774 658 1801; x_wconf 96'>track</span>
<span class='ocrx_word' id='word_1_264' title='bbox 460 1774 506 1801; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_265' title='bbox 517 1773 577 1801; x_wconf 96'>first</span>
<span class='ocrx_word' id='word_1_266' title='bbox 587 1774 662 1801; x_wconf 96'>track</span>
<span class='ocrx_word' id='word_1_267' title='bbox 673 1774 726 1801; x_wconf 96'>will</span>
<span class='ocrx_word' id='word_1_268' title='bbox 736 1774 799 1807; x_wconf 96'>play</span>
<span class='ocrx_word' id='word_1_269' title='bbox 809 1774 836 1801; x_wconf 96'>in</span>
@@ -412,11 +412,11 @@
<span class='ocrx_word' id='word_1_273' title='bbox 1148 1782 1203 1807; x_wconf 96'>you</span>
</span>
<span class='ocr_line' id='line_1_32' title="bbox 346 1813 1191 1847; baseline 0.002 -8; x_size 33; x_descenders 5; x_ascenders 9">
<span class='ocrx_word' id='word_1_274' title='bbox 346 1813 454 1840; x_wconf 95'>MUTE</span>
<span class='ocrx_word' id='word_1_275' title='bbox 464 1813 492 1845; x_wconf 95'>it,</span>
<span class='ocrx_word' id='word_1_274' title='bbox 346 1813 454 1840; x_wconf 94'>MUTE</span>
<span class='ocrx_word' id='word_1_275' title='bbox 464 1813 492 1845; x_wconf 94'>it,</span>
<span class='ocrx_word' id='word_1_276' title='bbox 505 1821 537 1840; x_wconf 95'>or</span>
<span class='ocrx_word' id='word_1_277' title='bbox 547 1813 642 1840; x_wconf 95'>SOLO</span>
<span class='ocrx_word' id='word_1_278' title='bbox 653 1814 769 1841; x_wconf 95'>another</span>
<span class='ocrx_word' id='word_1_278' title='bbox 653 1814 769 1841; x_wconf 96'>another</span>
<span class='ocrx_word' id='word_1_279' title='bbox 779 1814 875 1847; x_wconf 94'>track).</span>
<span class='ocrx_word' id='word_1_280' title='bbox 889 1814 920 1840; x_wconf 96'>In</span>
<span class='ocrx_word' id='word_1_281' title='bbox 930 1813 984 1841; x_wconf 96'>this</span>
@@ -518,13 +518,13 @@
<span class='ocr_line' id='line_1_41' title="bbox 345 2207 1228 2242; baseline 0.002 -8; x_size 35; x_descenders 7; x_ascenders 9">
<span class='ocrx_word' id='word_1_349' title='bbox 345 2207 412 2234; x_wconf 96'>tion.</span>
<span class='ocrx_word' id='word_1_350' title='bbox 424 2208 461 2235; x_wconf 93'>To</span>
<span class='ocrx_word' id='word_1_351' title='bbox 472 2208 592 2235; x_wconf 91'>overdub</span>
<span class='ocrx_word' id='word_1_351' title='bbox 472 2208 592 2235; x_wconf 92'>overdub</span>
<span class='ocrx_word' id='word_1_352' title='bbox 603 2212 680 2235; x_wconf 96'>notes</span>
<span class='ocrx_word' id='word_1_353' title='bbox 691 2212 718 2235; x_wconf 96'>at</span>
<span class='ocrx_word' id='word_1_354' title='bbox 729 2208 841 2242; x_wconf 96'>specific</span>
<span class='ocrx_word' id='word_1_355' title='bbox 851 2209 943 2242; x_wconf 97'>points</span>
<span class='ocrx_word' id='word_1_356' title='bbox 955 2208 1049 2236; x_wconf 96'>within</span>
<span class='ocrx_word' id='word_1_357' title='bbox 1060 2217 1076 2236; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_356' title='bbox 955 2208 1049 2236; x_wconf 97'>within</span>
<span class='ocrx_word' id='word_1_357' title='bbox 1060 2217 1076 2236; x_wconf 97'>a</span>
<span class='ocrx_word' id='word_1_358' title='bbox 1086 2216 1228 2242; x_wconf 96'>sequence,</span>
</span>
</p>
@@ -681,7 +681,7 @@
<span class='ocrx_word' id='word_1_465' title='bbox 1810 1808 1911 1835; x_wconf 96'>SONG</span>
<span class='ocrx_word' id='word_1_466' title='bbox 1923 1808 2050 1836; x_wconf 96'>function</span>
<span class='ocrx_word' id='word_1_467' title='bbox 2060 1812 2089 1835; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_468' title='bbox 2103 1808 2215 1836; x_wconf 93'>“chain”</span>
<span class='ocrx_word' id='word_1_468' title='bbox 2103 1808 2215 1836; x_wconf 91'>“chain”</span>
</span>
<span class='ocr_line' id='line_1_56' title="bbox 1295 1847 2135 1881; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_469' title='bbox 1295 1848 1370 1874; x_wconf 96'>them</span>
@@ -874,10 +874,10 @@
<div class='ocr_carea' id='block_1_25' title="bbox 347 2556 1768 2590">
<p class='ocr_par' id='par_1_33' lang='eng' title="bbox 347 2556 1768 2590">
<span class='ocr_header' id='line_1_70' title="bbox 347 2556 1768 2590; baseline 0.001 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_604' title='bbox 347 2561 361 2575; x_wconf 86'>¢</span>
<span class='ocrx_word' id='word_1_605' title='bbox 372 2556 433 2583; x_wconf 85'>Iwo</span>
<span class='ocrx_word' id='word_1_604' title='bbox 347 2561 361 2575; x_wconf 85'>¢</span>
<span class='ocrx_word' id='word_1_605' title='bbox 372 2556 433 2583; x_wconf 84'>Iwo</span>
<span class='ocrx_word' id='word_1_606' title='bbox 443 2556 612 2583; x_wconf 96'>TRIGGER</span>
<span class='ocrx_word' id='word_1_607' title='bbox 623 2556 797 2584; x_wconf 96'>OUTPUTS</span>
<span class='ocrx_word' id='word_1_607' title='bbox 623 2556 797 2584; x_wconf 95'>OUTPUTS</span>
<span class='ocrx_word' id='word_1_608' title='bbox 808 2565 871 2590; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_609' title='bbox 881 2557 915 2584; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_610' title='bbox 925 2557 1119 2590; x_wconf 96'>programmed</span>
@@ -887,7 +887,7 @@
<span class='ocrx_word' id='word_1_614' title='bbox 1381 2561 1409 2584; x_wconf 96'>at</span>
<span class='ocrx_word' id='word_1_615' title='bbox 1419 2565 1472 2590; x_wconf 96'>any</span>
<span class='ocrx_word' id='word_1_616' title='bbox 1483 2557 1598 2584; x_wconf 96'>selected</span>
<span class='ocrx_word' id='word_1_617' title='bbox 1608 2561 1673 2584; x_wconf 97'>note</span>
<span class='ocrx_word' id='word_1_617' title='bbox 1608 2561 1673 2584; x_wconf 96'>note</span>
<span class='ocrx_word' id='word_1_618' title='bbox 1683 2556 1768 2583; x_wconf 96'>value.</span>
</span>
</p>
@@ -911,15 +911,15 @@
</div>
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>©</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 95'>Utilizes</span>
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 51'>©</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2677; x_wconf 96'>ultra</span>
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
<span class='ocrx_word' id='word_1_634' title='bbox 757 2649 772 2676; x_wconf 95'>8</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 96'>80186</span>
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 95'>16</span>
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
<span class='ocrx_word' id='word_1_639' title='bbox 1054 2653 1197 2682; x_wconf 96'>computer</span>
<span class='ocrx_word' id='word_1_640' title='bbox 1208 2649 1350 2682; x_wconf 96'>internally</span>
@@ -960,18 +960,18 @@
<div class='ocr_carea' id='block_1_29' title="bbox 347 2777 2174 2811">
<p class='ocr_par' id='par_1_37' lang='eng' title="bbox 347 2777 2174 2811">
<span class='ocr_header' id='line_1_75' title="bbox 347 2777 2174 2811; baseline 0.001 -8; x_size 33; x_descenders 5; x_ascenders 9">
<span class='ocrx_word' id='word_1_664' title='bbox 347 2782 360 2796; x_wconf 81'>¢</span>
<span class='ocrx_word' id='word_1_664' title='bbox 347 2782 360 2796; x_wconf 79'>¢</span>
<span class='ocrx_word' id='word_1_665' title='bbox 372 2777 504 2804; x_wconf 94'>TEMPO</span>
<span class='ocrx_word' id='word_1_666' title='bbox 515 2785 578 2810; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_667' title='bbox 588 2777 622 2804; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_667' title='bbox 588 2777 622 2804; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_668' title='bbox 633 2778 741 2804; x_wconf 95'>entered</span>
<span class='ocrx_word' id='word_1_669' title='bbox 751 2777 934 2811; x_wconf 96'>numerically,</span>
<span class='ocrx_word' id='word_1_670' title='bbox 946 2777 1101 2811; x_wconf 95'>adjustable</span>
<span class='ocrx_word' id='word_1_671' title='bbox 1111 2777 1139 2804; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_670' title='bbox 946 2777 1101 2811; x_wconf 96'>adjustable</span>
<span class='ocrx_word' id='word_1_671' title='bbox 1111 2777 1139 2804; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_672' title='bbox 1149 2778 1239 2805; x_wconf 96'>tenths</span>
<span class='ocrx_word' id='word_1_673' title='bbox 1250 2778 1282 2805; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_674' title='bbox 1290 2786 1307 2805; x_wconf 91'>a</span>
<span class='ocrx_word' id='word_1_675' title='bbox 1317 2777 1567 2805; x_wconf 91'>Beat-Per-Minute</span>
<span class='ocrx_word' id='word_1_674' title='bbox 1290 2786 1307 2805; x_wconf 93'>a</span>
<span class='ocrx_word' id='word_1_675' title='bbox 1317 2777 1567 2805; x_wconf 92'>Beat-Per-Minute</span>
<span class='ocrx_word' id='word_1_676' title='bbox 1577 2777 1748 2809; x_wconf 96'>increments,</span>
<span class='ocrx_word' id='word_1_677' title='bbox 1760 2785 1792 2804; x_wconf 96'>or</span>
<span class='ocrx_word' id='word_1_678' title='bbox 1803 2777 1839 2810; x_wconf 96'>by</span>
@@ -9,459 +9,467 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.1ca2h7iv/000001_ocr.png"; bbox 0 0 1000 1520; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 126 101 547 145">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 126 101 547 145">
<span class='ocr_line' id='line_1_1' title="bbox 126 101 547 125; baseline 0.005 -7; x_size 23; x_descenders 6; x_ascenders 4">
<span class='ocrx_word' id='word_1_1' title='bbox 126 101 173 118; x_wconf 37'>41st</span>
<span class='ocrx_word' id='word_1_2' title='bbox 181 101 303 124; x_wconf 16'>CONGRESS;</span>
<span class='ocrx_word' id='word_1_3' title='bbox 329 123 331 125; x_wconf 42'>|</span>
<span class='ocrx_word' id='word_1_4' title='bbox 443 103 547 120; x_wconf 94'>SENATE.</span>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.6nqy9raf/000001_ocr.png"; bbox 0 0 1000 1520; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 122 99 848 146">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 122 99 848 146">
<span class='ocr_line' id='line_1_1' title="bbox 122 99 848 146; baseline 0 -26; x_size 22; x_descenders 5; x_ascenders 4">
<span class='ocrx_word' id='word_1_1' title='bbox 122 104 169 121; x_wconf 29'>4ist</span>
<span class='ocrx_word' id='word_1_2' title='bbox 177 103 291 125; x_wconf 54'>ConGREss,</span>
<span class='ocrx_word' id='word_1_3' title='bbox 300 105 308 146; x_wconf 46'>}</span>
<span class='ocrx_word' id='word_1_4' title='bbox 439 99 543 130; x_wconf 94'>SENATE.</span>
<span class='ocrx_word' id='word_1_5' title='bbox 736 105 745 146; x_wconf 90'>{</span>
<span class='ocrx_word' id='word_1_6' title='bbox 753 103 788 120; x_wconf 94'>Ex.</span>
<span class='ocrx_word' id='word_1_7' title='bbox 798 103 848 121; x_wconf 95'>Doc,</span>
</span>
<span class='ocr_line' id='line_1_2' title="bbox 156 104 312 145; baseline 0.006 -5; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_5' title='bbox 156 124 180 140; x_wconf 89'>3d</span>
<span class='ocrx_word' id='word_1_6' title='bbox 189 124 264 141; x_wconf 91'>Session.</span>
<span class='ocrx_word' id='word_1_7' title='bbox 304 104 312 145; x_wconf 52'>}</span>
<span class='ocr_line' id='line_1_2' title="bbox 152 125 838 143; baseline -0.001 0; x_size 21.943821; x_descenders 4.94382; x_ascenders 5">
<span class='ocrx_word' id='word_1_8' title='bbox 152 126 176 142; x_wconf 93'>3d</span>
<span class='ocrx_word' id='word_1_9' title='bbox 185 126 260 143; x_wconf 88'>Session.</span>
<span class='ocrx_word' id='word_1_10' title='bbox 767 125 802 142; x_wconf 86'>No.</span>
<span class='ocrx_word' id='word_1_11' title='bbox 810 126 838 142; x_wconf 76'>25.</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_2' title="bbox 388 244 597 269">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 388 244 597 269">
<span class='ocr_line' id='line_1_3' title="bbox 388 244 597 269; baseline 0.005 -1; x_size 32.400002; x_descenders 8.1000004; x_ascenders 8.1000004">
<span class='ocrx_word' id='word_1_8' title='bbox 388 244 597 269; x_wconf 96'>MESSAGE</span>
<div class='ocr_carea' id='block_1_2' title="bbox 384 245 594 269">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 384 245 594 269">
<span class='ocr_line' id='line_1_3' title="bbox 384 245 594 269; baseline 0 0; x_size 32.666668; x_descenders 8.166667; x_ascenders 8.166667">
<span class='ocrx_word' id='word_1_12' title='bbox 384 245 594 269; x_wconf 94'>MESSAGE</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_3' title="bbox 462 302 526 313">
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 462 302 526 313">
<span class='ocr_line' id='line_1_4' title="bbox 462 302 526 313; baseline 0 0; x_size 20; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_9' title='bbox 462 302 484 313; x_wconf 87'>OF</span>
<span class='ocrx_word' id='word_1_10' title='bbox 493 302 526 313; x_wconf 96'>THE</span>
<div class='ocr_carea' id='block_1_3' title="bbox 459 302 523 313">
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 459 302 523 313">
<span class='ocr_line' id='line_1_4' title="bbox 459 302 523 313; baseline 0 0; x_size 20; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_13' title='bbox 459 302 481 313; x_wconf 87'>OF</span>
<span class='ocrx_word' id='word_1_14' title='bbox 490 302 523 313; x_wconf 96'>THE</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_4' title="bbox 151 337 842 377">
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 151 337 842 377">
<span class='ocr_line' id='line_1_5' title="bbox 151 337 842 377; baseline 0.009 -8; x_size 43.365078; x_descenders 10.841269; x_ascenders 10.841269">
<span class='ocrx_word' id='word_1_11' title='bbox 151 337 361 370; x_wconf 96'>PRESIDENT</span>
<span class='ocrx_word' id='word_1_12' title='bbox 382 338 425 370; x_wconf 96'>OF</span>
<span class='ocrx_word' id='word_1_13' title='bbox 447 338 521 371; x_wconf 96'>THE</span>
<span class='ocrx_word' id='word_1_14' title='bbox 542 339 676 372; x_wconf 96'>UNITED</span>
<span class='ocrx_word' id='word_1_15' title='bbox 697 340 842 377; x_wconf 95'>STATES,</span>
<div class='ocr_carea' id='block_1_4' title="bbox 148 338 839 375">
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 148 338 839 375">
<span class='ocr_line' id='line_1_5' title="bbox 148 338 839 375; baseline 0.003 -3; x_size 43.490196; x_descenders 10.872549; x_ascenders 10.872549">
<span class='ocrx_word' id='word_1_15' title='bbox 148 339 358 372; x_wconf 96'>PRESIDENT</span>
<span class='ocrx_word' id='word_1_16' title='bbox 379 339 422 371; x_wconf 95'>OF</span>
<span class='ocrx_word' id='word_1_17' title='bbox 444 338 518 371; x_wconf 96'>THE</span>
<span class='ocrx_word' id='word_1_18' title='bbox 539 338 673 371; x_wconf 96'>UNITED</span>
<span class='ocrx_word' id='word_1_19' title='bbox 694 338 839 375; x_wconf 96'>STATES,</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_5' title="bbox 425 406 565 417">
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 425 406 565 417">
<span class='ocr_line' id='line_1_6' title="bbox 425 406 565 417; baseline 0 -1; x_size 20; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_16' title='bbox 425 406 565 417; x_wconf 94'>COMMUNICATING</span>
<div class='ocr_carea' id='block_1_5' title="bbox 423 406 563 417">
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 423 406 563 417">
<span class='ocr_line' id='line_1_6' title="bbox 423 406 563 417; baseline 0 -1; x_size 20; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_20' title='bbox 423 406 563 417; x_wconf 95'>COMMUNICATING</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_6' title="bbox 125 445 861 494">
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 125 445 861 494">
<span class='ocr_line' id='line_1_7' title="bbox 125 445 861 472; baseline 0.008 -11; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_17' title='bbox 125 445 143 462; x_wconf 93'>A</span>
<span class='ocrx_word' id='word_1_18' title='bbox 149 450 191 467; x_wconf 93'>copy</span>
<span class='ocrx_word' id='word_1_19' title='bbox 195 445 217 467; x_wconf 95'>of</span>
<span class='ocrx_word' id='word_1_20' title='bbox 222 446 331 467; x_wconf 96'>regulations</span>
<span class='ocrx_word' id='word_1_21' title='bbox 337 446 371 467; x_wconf 95'>for</span>
<span class='ocrx_word' id='word_1_22' title='bbox 376 446 403 463; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_23' title='bbox 412 447 494 463; x_wconf 95'>consular</span>
<span class='ocrx_word' id='word_1_24' title='bbox 499 449 557 464; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_25' title='bbox 565 448 587 469; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_26' title='bbox 592 448 619 465; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_27' title='bbox 630 448 694 466; x_wconf 90'>United</span>
<span class='ocrx_word' id='word_1_28' title='bbox 702 448 760 466; x_wconf 93'>States</span>
<span class='ocrx_word' id='word_1_29' title='bbox 768 450 787 466; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_30' title='bbox 795 449 861 472; x_wconf 95'>Japan,</span>
<div class='ocr_carea' id='block_1_6' title="bbox 123 447 859 492">
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 123 447 859 492">
<span class='ocr_line' id='line_1_7' title="bbox 123 447 859 470; baseline 0.001 -6; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_21' title='bbox 123 447 141 465; x_wconf 95'>A</span>
<span class='ocrx_word' id='word_1_22' title='bbox 147 452 189 469; x_wconf 95'>copy</span>
<span class='ocrx_word' id='word_1_23' title='bbox 193 447 215 469; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_24' title='bbox 220 447 329 469; x_wconf 96'>regulations</span>
<span class='ocrx_word' id='word_1_25' title='bbox 335 447 369 468; x_wconf 95'>for</span>
<span class='ocrx_word' id='word_1_26' title='bbox 374 447 401 464; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_27' title='bbox 410 447 492 464; x_wconf 96'>consular</span>
<span class='ocrx_word' id='word_1_28' title='bbox 497 449 555 464; x_wconf 79'>courts</span>
<span class='ocrx_word' id='word_1_29' title='bbox 563 447 585 469; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_30' title='bbox 590 447 617 464; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_31' title='bbox 628 447 692 465; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_32' title='bbox 700 447 758 465; x_wconf 93'>States</span>
<span class='ocrx_word' id='word_1_33' title='bbox 766 448 785 464; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_34' title='bbox 793 447 859 470; x_wconf 96'>Japan,</span>
</span>
<span class='ocr_line' id='line_1_8' title="bbox 147 468 834 494; baseline 0.006 -10; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_31' title='bbox 147 468 215 484; x_wconf 96'>decreed</span>
<span class='ocrx_word' id='word_1_32' title='bbox 222 468 258 485; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_33' title='bbox 265 468 323 485; x_wconf 96'>issued</span>
<span class='ocrx_word' id='word_1_34' title='bbox 331 468 353 489; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_35' title='bbox 361 468 388 485; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_36' title='bbox 395 468 476 486; x_wconf 95'>minister</span>
<span class='ocrx_word' id='word_1_37' title='bbox 482 469 504 491; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_38' title='bbox 509 470 535 487; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_39' title='bbox 546 470 610 487; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_40' title='bbox 618 471 676 488; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_41' title='bbox 682 471 702 488; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_42' title='bbox 710 471 748 488; x_wconf 95'>that</span>
<span class='ocrx_word' id='word_1_43' title='bbox 755 474 834 494; x_wconf 77'>country.</span>
<span class='ocr_line' id='line_1_8' title="bbox 145 468 832 492; baseline 0 -6; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_35' title='bbox 145 470 213 486; x_wconf 96'>decreed</span>
<span class='ocrx_word' id='word_1_36' title='bbox 220 470 256 487; x_wconf 95'>and</span>
<span class='ocrx_word' id='word_1_37' title='bbox 263 469 321 487; x_wconf 95'>issued</span>
<span class='ocrx_word' id='word_1_38' title='bbox 329 469 351 490; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_39' title='bbox 359 469 386 486; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_40' title='bbox 393 468 474 486; x_wconf 96'>minister</span>
<span class='ocrx_word' id='word_1_41' title='bbox 480 469 502 491; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_42' title='bbox 507 470 533 487; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_43' title='bbox 544 469 608 487; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_44' title='bbox 616 470 674 487; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_45' title='bbox 680 470 700 487; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_46' title='bbox 708 470 746 487; x_wconf 96'>that</span>
<span class='ocrx_word' id='word_1_47' title='bbox 753 472 832 492; x_wconf 83'>country.</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_7' title="bbox 122 539 861 579">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 122 539 861 579">
<span class='ocr_caption' id='line_1_9' title="bbox 122 539 861 579; baseline 0.008 -24; x_size 18; x_descenders 4; x_ascenders 4">
<span class='ocrx_word' id='word_1_44' title='bbox 122 541 201 556; x_wconf 81'>January</span>
<span class='ocrx_word' id='word_1_45' title='bbox 207 542 230 560; x_wconf 93'>27,</span>
<span class='ocrx_word' id='word_1_46' title='bbox 237 542 343 560; x_wconf 39'>1871,—Read,</span>
<span class='ocrx_word' id='word_1_47' title='bbox 347 542 414 557; x_wconf 96'>referred</span>
<span class='ocrx_word' id='word_1_48' title='bbox 420 544 436 557; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_49' title='bbox 442 543 470 557; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_50' title='bbox 479 539 571 579; x_wconf 66'>Committee</span>
<span class='ocrx_word' id='word_1_51' title='bbox 582 548 602 558; x_wconf 93'>on</span>
<span class='ocrx_word' id='word_1_52' title='bbox 613 544 704 563; x_wconf 96'>Commerce,</span>
<span class='ocrx_word' id='word_1_53' title='bbox 711 545 742 560; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_54' title='bbox 749 546 813 560; x_wconf 96'>ordered</span>
<span class='ocrx_word' id='word_1_55' title='bbox 818 547 835 560; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_56' title='bbox 841 546 861 560; x_wconf 96'>be</span>
<div class='ocr_carea' id='block_1_7' title="bbox 121 539 860 579">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 121 539 860 579">
<span class='ocr_caption' id='line_1_9' title="bbox 121 539 860 579; baseline 0 -21; x_size 19; x_descenders 4; x_ascenders 4">
<span class='ocrx_word' id='word_1_48' title='bbox 121 544 200 559; x_wconf 95'>JANUARY</span>
<span class='ocrx_word' id='word_1_49' title='bbox 206 544 229 562; x_wconf 85'>27,</span>
<span class='ocrx_word' id='word_1_50' title='bbox 236 543 342 561; x_wconf 58'>1871,—Read,</span>
<span class='ocrx_word' id='word_1_51' title='bbox 346 543 413 558; x_wconf 95'>referred</span>
<span class='ocrx_word' id='word_1_52' title='bbox 419 545 435 558; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_53' title='bbox 441 543 469 557; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_54' title='bbox 478 539 570 579; x_wconf 66'>Committee</span>
<span class='ocrx_word' id='word_1_55' title='bbox 581 547 601 557; x_wconf 96'>on</span>
<span class='ocrx_word' id='word_1_56' title='bbox 612 543 703 562; x_wconf 96'>Commerce,</span>
<span class='ocrx_word' id='word_1_57' title='bbox 710 543 741 558; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_58' title='bbox 748 544 812 558; x_wconf 96'>ordered</span>
<span class='ocrx_word' id='word_1_59' title='bbox 817 545 834 558; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_60' title='bbox 840 544 860 558; x_wconf 96'>be</span>
</span>
<span class='ocr_caption' id='line_1_10' title="bbox 459 565 525 579; baseline 0 -4; x_size 21.5; x_descenders 5.5; x_ascenders 5.5">
<span class='ocrx_word' id='word_1_57' title='bbox 459 565 525 579; x_wconf 96'>printed.</span>
<span class='ocr_caption' id='line_1_10' title="bbox 458 565 524 579; baseline 0 -4; x_size 18.782608; x_descenders 4; x_ascenders 4.782609">
<span class='ocrx_word' id='word_1_61' title='bbox 458 565 524 579; x_wconf 93'>printed.</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_8' title="bbox 122 629 858 769">
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 124 629 566 652">
<span class='ocr_line' id='line_1_11' title="bbox 124 629 566 652; baseline 0.005 -6; x_size 22.268028; x_descenders 5.2680273; x_ascenders 5">
<span class='ocrx_word' id='word_1_58' title='bbox 124 629 150 646; x_wconf 92'>To</span>
<span class='ocrx_word' id='word_1_59' title='bbox 158 629 186 646; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_60' title='bbox 194 629 256 646; x_wconf 96'>Senate</span>
<span class='ocrx_word' id='word_1_61' title='bbox 264 630 301 647; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_62' title='bbox 307 630 368 647; x_wconf 96'>House</span>
<span class='ocrx_word' id='word_1_63' title='bbox 376 630 398 652; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_64' title='bbox 405 630 555 652; x_wconf 93'>Representatives</span>
<span class='ocrx_word' id='word_1_65' title='bbox 560 638 566 648; x_wconf 92'>:</span>
<div class='ocr_carea' id='block_1_8' title="bbox 121 631 858 770">
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 123 631 565 653">
<span class='ocr_line' id='line_1_11' title="bbox 123 631 565 653; baseline 0 -5; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_62' title='bbox 123 631 149 649; x_wconf 96'>To</span>
<span class='ocrx_word' id='word_1_63' title='bbox 157 631 185 648; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_64' title='bbox 193 631 255 648; x_wconf 95'>Senate</span>
<span class='ocrx_word' id='word_1_65' title='bbox 263 631 300 649; x_wconf 95'>and</span>
<span class='ocrx_word' id='word_1_66' title='bbox 306 631 367 648; x_wconf 95'>House</span>
<span class='ocrx_word' id='word_1_67' title='bbox 375 631 397 653; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_68' title='bbox 404 631 554 652; x_wconf 74'>Representatives</span>
<span class='ocrx_word' id='word_1_69' title='bbox 559 638 565 648; x_wconf 86'>:</span>
</span>
</p>
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 122 656 858 726">
<span class='ocr_line' id='line_1_12' title="bbox 146 656 858 681; baseline 0.008 -9; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_66' title='bbox 146 656 156 672; x_wconf 80'>I</span>
<span class='ocrx_word' id='word_1_67' title='bbox 163 657 251 673; x_wconf 80'>transmit</span>
<span class='ocrx_word' id='word_1_68' title='bbox 262 656 358 680; x_wconf 96'>herewith,</span>
<span class='ocrx_word' id='word_1_69' title='bbox 366 657 395 674; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_70' title='bbox 402 658 435 674; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_71' title='bbox 442 658 578 676; x_wconf 96'>consideration</span>
<span class='ocrx_word' id='word_1_72' title='bbox 587 659 608 676; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_73' title='bbox 617 659 715 681; x_wconf 96'>Congress,</span>
<span class='ocrx_word' id='word_1_74' title='bbox 724 665 735 677; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_75' title='bbox 743 661 806 681; x_wconf 96'>report</span>
<span class='ocrx_word' id='word_1_76' title='bbox 811 660 858 678; x_wconf 96'>from</span>
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 121 658 858 724">
<span class='ocr_line' id='line_1_12' title="bbox 145 658 857 681; baseline 0.003 -7; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_70' title='bbox 145 658 155 674; x_wconf 92'>I</span>
<span class='ocrx_word' id='word_1_71' title='bbox 162 659 250 675; x_wconf 92'>transmit</span>
<span class='ocrx_word' id='word_1_72' title='bbox 261 658 357 681; x_wconf 94'>herewith,</span>
<span class='ocrx_word' id='word_1_73' title='bbox 365 658 394 675; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_74' title='bbox 401 659 434 675; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_75' title='bbox 441 658 577 675; x_wconf 96'>consideration</span>
<span class='ocrx_word' id='word_1_76' title='bbox 586 658 607 675; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_77' title='bbox 616 658 714 680; x_wconf 95'>Congress,</span>
<span class='ocrx_word' id='word_1_78' title='bbox 723 663 734 675; x_wconf 94'>a</span>
<span class='ocrx_word' id='word_1_79' title='bbox 742 659 805 679; x_wconf 96'>report</span>
<span class='ocrx_word' id='word_1_80' title='bbox 810 658 857 676; x_wconf 96'>from</span>
</span>
<span class='ocr_line' id='line_1_13' title="bbox 122 678 858 704; baseline 0.008 -10; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_77' title='bbox 122 678 155 694; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_78' title='bbox 163 678 260 700; x_wconf 0'>Secretary</span>
<span class='ocrx_word' id='word_1_79' title='bbox 268 679 289 695; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_80' title='bbox 299 679 357 702; x_wconf 96'>State,</span>
<span class='ocrx_word' id='word_1_81' title='bbox 366 680 403 696; x_wconf 95'>and</span>
<span class='ocrx_word' id='word_1_82' title='bbox 411 680 444 697; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_83' title='bbox 457 681 524 701; x_wconf 96'>papers</span>
<span class='ocrx_word' id='word_1_84' title='bbox 532 681 592 698; x_wconf 95'>which</span>
<span class='ocrx_word' id='word_1_85' title='bbox 605 682 736 703; x_wconf 84'>accompanied</span>
<span class='ocrx_word' id='word_1_86' title='bbox 744 682 765 704; x_wconf 93'>it,</span>
<span class='ocrx_word' id='word_1_87' title='bbox 774 687 858 700; x_wconf 92'>concern-</span>
<span class='ocr_line' id='line_1_13' title="bbox 121 680 858 703; baseline 0 -6; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_81' title='bbox 121 680 155 697; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_82' title='bbox 163 680 259 702; x_wconf 94'>Secretary</span>
<span class='ocrx_word' id='word_1_83' title='bbox 268 680 288 697; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_84' title='bbox 299 680 357 703; x_wconf 96'>State,</span>
<span class='ocrx_word' id='word_1_85' title='bbox 366 681 403 697; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_86' title='bbox 410 681 444 698; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_87' title='bbox 456 681 524 701; x_wconf 95'>papers</span>
<span class='ocrx_word' id='word_1_88' title='bbox 531 680 592 697; x_wconf 94'>which</span>
<span class='ocrx_word' id='word_1_89' title='bbox 605 680 736 702; x_wconf 94'>accompanied</span>
<span class='ocrx_word' id='word_1_90' title='bbox 744 680 765 702; x_wconf 93'>it,</span>
<span class='ocrx_word' id='word_1_91' title='bbox 774 685 858 698; x_wconf 92'>concern-</span>
</span>
<span class='ocr_line' id='line_1_14' title="bbox 122 700 835 726; baseline 0.007 -9; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_88' title='bbox 122 700 155 721; x_wconf 96'>ing</span>
<span class='ocrx_word' id='word_1_89' title='bbox 163 700 277 721; x_wconf 96'>regulations</span>
<span class='ocrx_word' id='word_1_90' title='bbox 285 701 314 718; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_91' title='bbox 321 702 354 719; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_92' title='bbox 361 702 446 719; x_wconf 96'>consular</span>
<span class='ocrx_word' id='word_1_93' title='bbox 453 703 516 720; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_94' title='bbox 524 703 546 720; x_wconf 95'>of</span>
<span class='ocrx_word' id='word_1_95' title='bbox 552 704 583 720; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_96' title='bbox 591 704 661 721; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_97' title='bbox 669 704 732 721; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_98' title='bbox 740 704 759 721; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_99' title='bbox 768 705 835 726; x_wconf 94'>Japan.</span>
<span class='ocr_line' id='line_1_14' title="bbox 122 702 835 724; baseline 0.001 -5; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_92' title='bbox 122 703 155 723; x_wconf 96'>ing</span>
<span class='ocrx_word' id='word_1_93' title='bbox 163 702 277 723; x_wconf 96'>regulations</span>
<span class='ocrx_word' id='word_1_94' title='bbox 285 702 314 719; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_95' title='bbox 321 703 354 720; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_96' title='bbox 361 703 446 720; x_wconf 96'>consular</span>
<span class='ocrx_word' id='word_1_97' title='bbox 453 703 516 720; x_wconf 95'>courts</span>
<span class='ocrx_word' id='word_1_98' title='bbox 524 703 546 720; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_99' title='bbox 552 704 583 720; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_100' title='bbox 591 703 661 720; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_101' title='bbox 669 703 732 720; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_102' title='bbox 740 702 759 719; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_103' title='bbox 768 703 835 724; x_wconf 90'>Japan.</span>
</span>
</p>
<p class='ocr_par' id='par_1_10' lang='eng' title="bbox 682 726 835 745">
<span class='ocr_line' id='line_1_15' title="bbox 682 726 835 745; baseline 0.007 -1; x_size 23.528408; x_descenders 5.5; x_ascenders 5.5">
<span class='ocrx_word' id='word_1_100' title='bbox 682 726 705 744; x_wconf 85'>U.</span>
<span class='ocrx_word' id='word_1_101' title='bbox 715 726 734 744; x_wconf 89'>8.</span>
<span class='ocrx_word' id='word_1_102' title='bbox 743 727 835 745; x_wconf 80'>GRANT.</span>
<p class='ocr_par' id='par_1_10' lang='eng' title="bbox 682 724 835 743">
<span class='ocr_line' id='line_1_15' title="bbox 682 724 835 743; baseline 0 -1; x_size 23.573172; x_descenders 5.5365853; x_ascenders 5.5">
<span class='ocrx_word' id='word_1_104' title='bbox 682 725 705 743; x_wconf 85'>U.</span>
<span class='ocrx_word' id='word_1_105' title='bbox 715 724 734 743; x_wconf 89'>8.</span>
<span class='ocrx_word' id='word_1_106' title='bbox 743 725 835 743; x_wconf 83'>GRANT.</span>
</span>
</p>
<p class='ocr_par' id='par_1_11' lang='eng' title="bbox 144 744 482 769">
<span class='ocr_line' id='line_1_16' title="bbox 144 744 482 769; baseline 0.006 -7; x_size 23; x_descenders 5; x_ascenders 4">
<span class='ocrx_word' id='word_1_103' title='bbox 144 744 293 768; x_wconf 80'>WASHINGTON,</span>
<span class='ocrx_word' id='word_1_104' title='bbox 302 745 385 767; x_wconf 96'>January</span>
<span class='ocrx_word' id='word_1_105' title='bbox 393 747 422 769; x_wconf 93'>27,</span>
<span class='ocrx_word' id='word_1_106' title='bbox 431 747 482 764; x_wconf 93'>1871.</span>
<p class='ocr_par' id='par_1_11' lang='eng' title="bbox 144 746 482 770">
<span class='ocr_line' id='line_1_16' title="bbox 144 746 482 770; baseline 0 -6; x_size 24; x_descenders 6; x_ascenders 5">
<span class='ocrx_word' id='word_1_107' title='bbox 144 746 293 769; x_wconf 78'>WASHINGTON,</span>
<span class='ocrx_word' id='word_1_108' title='bbox 302 746 385 768; x_wconf 96'>January</span>
<span class='ocrx_word' id='word_1_109' title='bbox 393 748 422 770; x_wconf 92'>27,</span>
<span class='ocrx_word' id='word_1_110' title='bbox 431 747 482 764; x_wconf 92'>1871.</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_9' title="bbox 118 846 863 1226">
<p class='ocr_par' id='par_1_12' lang='eng' title="bbox 442 846 833 893">
<span class='ocr_line' id='line_1_17' title="bbox 442 846 709 870; baseline 0.007 -8; x_size 23; x_descenders 6; x_ascenders 4">
<span class='ocrx_word' id='word_1_107' title='bbox 442 846 585 863; x_wconf 96'>DEPARTMENT</span>
<span class='ocrx_word' id='word_1_108' title='bbox 598 850 623 864; x_wconf 96'>OF</span>
<span class='ocrx_word' id='word_1_109' title='bbox 635 847 709 870; x_wconf 95'>STATE,</span>
<div class='ocr_carea' id='block_1_9' title="bbox 120 846 865 1225">
<p class='ocr_par' id='par_1_12' lang='eng' title="bbox 443 846 834 891">
<span class='ocr_line' id='line_1_17' title="bbox 443 846 710 869; baseline 0.004 -7; x_size 23; x_descenders 6; x_ascenders 4">
<span class='ocrx_word' id='word_1_111' title='bbox 443 846 586 863; x_wconf 95'>DEPARTMENT</span>
<span class='ocrx_word' id='word_1_112' title='bbox 599 849 624 863; x_wconf 96'>OF</span>
<span class='ocrx_word' id='word_1_113' title='bbox 636 846 710 869; x_wconf 95'>STATE,</span>
</span>
<span class='ocr_line' id='line_1_18' title="bbox 445 868 833 893; baseline 0.008 -8; x_size 24; x_descenders 6; x_ascenders 6">
<span class='ocrx_word' id='word_1_110' title='bbox 445 890 447 892; x_wconf 1'>.</span>
<span class='ocrx_word' id='word_1_111' title='bbox 528 868 648 892; x_wconf 96'>Washington,</span>
<span class='ocrx_word' id='word_1_112' title='bbox 656 869 739 891; x_wconf 96'>January</span>
<span class='ocrx_word' id='word_1_113' title='bbox 747 870 774 893; x_wconf 95'>26,</span>
<span class='ocrx_word' id='word_1_114' title='bbox 783 870 833 888; x_wconf 85'>1870,</span>
<span class='ocr_line' id='line_1_18' title="bbox 529 868 834 891; baseline 0 -5; x_size 23.255997; x_descenders 5.2559972; x_ascenders 6">
<span class='ocrx_word' id='word_1_114' title='bbox 529 868 649 891; x_wconf 95'>Washington,</span>
<span class='ocrx_word' id='word_1_115' title='bbox 657 868 740 889; x_wconf 96'>January</span>
<span class='ocrx_word' id='word_1_116' title='bbox 748 868 775 891; x_wconf 94'>26,</span>
<span class='ocrx_word' id='word_1_117' title='bbox 784 868 834 886; x_wconf 85'>1870,</span>
</span>
</p>
<p class='ocr_par' id='par_1_13' lang='eng' title="bbox 119 893 863 1050">
<span class='ocr_line' id='line_1_19' title="bbox 145 893 856 915; baseline 0.006 -5; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_115' title='bbox 145 893 183 910; x_wconf 96'>The</span>
<span class='ocrx_word' id='word_1_116' title='bbox 192 893 289 915; x_wconf 96'>Secretary</span>
<span class='ocrx_word' id='word_1_117' title='bbox 296 894 318 911; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_118' title='bbox 327 894 380 912; x_wconf 96'>State</span>
<span class='ocrx_word' id='word_1_119' title='bbox 388 895 421 912; x_wconf 96'>has</span>
<span class='ocrx_word' id='word_1_120' title='bbox 429 894 461 912; x_wconf 97'>the</span>
<span class='ocrx_word' id='word_1_121' title='bbox 468 895 527 912; x_wconf 96'>honor</span>
<span class='ocrx_word' id='word_1_122' title='bbox 534 897 554 912; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_123' title='bbox 563 896 632 913; x_wconf 96'>submit</span>
<span class='ocrx_word' id='word_1_124' title='bbox 639 896 729 914; x_wconf 91'>herewith,</span>
<span class='ocrx_word' id='word_1_125' title='bbox 738 897 767 914; x_wconf 91'>for</span>
<span class='ocrx_word' id='word_1_126' title='bbox 775 897 856 914; x_wconf 96'>revision</span>
<p class='ocr_par' id='par_1_13' lang='eng' title="bbox 121 894 865 1051">
<span class='ocr_line' id='line_1_19' title="bbox 146 894 857 917; baseline 0 -5; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_118' title='bbox 146 895 184 912; x_wconf 96'>The</span>
<span class='ocrx_word' id='word_1_119' title='bbox 193 895 290 917; x_wconf 96'>Secretary</span>
<span class='ocrx_word' id='word_1_120' title='bbox 297 895 319 912; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_121' title='bbox 328 895 381 913; x_wconf 96'>State</span>
<span class='ocrx_word' id='word_1_122' title='bbox 389 896 422 913; x_wconf 96'>has</span>
<span class='ocrx_word' id='word_1_123' title='bbox 430 894 462 912; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_124' title='bbox 469 895 528 912; x_wconf 96'>honor</span>
<span class='ocrx_word' id='word_1_125' title='bbox 535 897 555 912; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_126' title='bbox 564 895 633 912; x_wconf 96'>submit</span>
<span class='ocrx_word' id='word_1_127' title='bbox 640 895 730 913; x_wconf 90'>herewith,</span>
<span class='ocrx_word' id='word_1_128' title='bbox 739 895 768 912; x_wconf 90'>for</span>
<span class='ocrx_word' id='word_1_129' title='bbox 776 895 857 912; x_wconf 95'>revision</span>
</span>
<span class='ocr_line' id='line_1_20' title="bbox 121 909 856 939; baseline 0.007 -7; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_127' title='bbox 121 915 147 937; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_128' title='bbox 159 915 257 938; x_wconf 96'>Congress,</span>
<span class='ocrx_word' id='word_1_129' title='bbox 265 916 284 933; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_130' title='bbox 293 917 402 938; x_wconf 96'>conformity</span>
<span class='ocrx_word' id='word_1_131' title='bbox 411 917 455 934; x_wconf 96'>with</span>
<span class='ocrx_word' id='word_1_132' title='bbox 463 917 495 934; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_133' title='bbox 504 918 606 939; x_wconf 96'>provisions</span>
<span class='ocrx_word' id='word_1_134' title='bbox 614 918 635 935; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_135' title='bbox 645 919 714 936; x_wconf 96'>section</span>
<span class='ocrx_word' id='word_1_136' title='bbox 723 909 735 936; x_wconf 91'>6</span>
<span class='ocrx_word' id='word_1_137' title='bbox 748 919 769 936; x_wconf 95'>of</span>
<span class='ocrx_word' id='word_1_138' title='bbox 778 920 811 937; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_139' title='bbox 825 921 856 937; x_wconf 96'>act</span>
<span class='ocr_line' id='line_1_20' title="bbox 122 907 857 940; baseline 0 -5; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_130' title='bbox 122 918 148 939; x_wconf 95'>by</span>
<span class='ocrx_word' id='word_1_131' title='bbox 160 917 258 940; x_wconf 96'>Congress,</span>
<span class='ocrx_word' id='word_1_132' title='bbox 266 918 285 935; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_133' title='bbox 294 918 403 939; x_wconf 96'>conformity</span>
<span class='ocrx_word' id='word_1_134' title='bbox 412 917 456 935; x_wconf 96'>with</span>
<span class='ocrx_word' id='word_1_135' title='bbox 464 917 496 934; x_wconf 97'>the</span>
<span class='ocrx_word' id='word_1_136' title='bbox 505 918 607 939; x_wconf 96'>provisions</span>
<span class='ocrx_word' id='word_1_137' title='bbox 615 917 636 934; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_138' title='bbox 646 918 715 935; x_wconf 97'>section</span>
<span class='ocrx_word' id='word_1_139' title='bbox 724 907 736 934; x_wconf 92'>6</span>
<span class='ocrx_word' id='word_1_140' title='bbox 749 917 770 934; x_wconf 94'>of</span>
<span class='ocrx_word' id='word_1_141' title='bbox 779 918 812 935; x_wconf 94'>the</span>
<span class='ocrx_word' id='word_1_142' title='bbox 826 919 857 935; x_wconf 96'>act</span>
</span>
<span class='ocr_line' id='line_1_21' title="bbox 120 938 856 962; baseline 0.007 -8; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_140' title='bbox 120 938 216 959; x_wconf 95'>approved</span>
<span class='ocrx_word' id='word_1_141' title='bbox 228 938 264 955; x_wconf 95'>22d</span>
<span class='ocrx_word' id='word_1_142' title='bbox 277 938 298 956; x_wconf 95'>of</span>
<span class='ocrx_word' id='word_1_143' title='bbox 312 939 367 961; x_wconf 96'>June,</span>
<span class='ocrx_word' id='word_1_144' title='bbox 382 939 431 962; x_wconf 72'>1860,</span>
<span class='ocrx_word' id='word_1_145' title='bbox 440 944 452 956; x_wconf 72'>a</span>
<span class='ocrx_word' id='word_1_146' title='bbox 465 944 512 961; x_wconf 96'>copy</span>
<span class='ocrx_word' id='word_1_147' title='bbox 524 940 545 957; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_148' title='bbox 554 940 685 962; x_wconf 80'>“regulations</span>
<span class='ocrx_word' id='word_1_149' title='bbox 692 941 721 958; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_150' title='bbox 729 942 761 958; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_151' title='bbox 769 942 856 959; x_wconf 96'>consular</span>
<span class='ocr_line' id='line_1_21' title="bbox 121 939 857 962; baseline -0.001 -5; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_143' title='bbox 121 940 217 961; x_wconf 96'>approved</span>
<span class='ocrx_word' id='word_1_144' title='bbox 229 940 265 957; x_wconf 95'>22d</span>
<span class='ocrx_word' id='word_1_145' title='bbox 278 939 299 957; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_146' title='bbox 313 940 368 962; x_wconf 96'>June,</span>
<span class='ocrx_word' id='word_1_147' title='bbox 383 940 432 962; x_wconf 57'>1860,</span>
<span class='ocrx_word' id='word_1_148' title='bbox 441 944 453 956; x_wconf 57'>a</span>
<span class='ocrx_word' id='word_1_149' title='bbox 466 944 513 961; x_wconf 96'>copy</span>
<span class='ocrx_word' id='word_1_150' title='bbox 525 940 546 957; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_151' title='bbox 555 940 686 961; x_wconf 82'>“regulations</span>
<span class='ocrx_word' id='word_1_152' title='bbox 693 940 722 957; x_wconf 95'>for</span>
<span class='ocrx_word' id='word_1_153' title='bbox 730 940 762 956; x_wconf 97'>the</span>
<span class='ocrx_word' id='word_1_154' title='bbox 770 940 857 957; x_wconf 95'>consular</span>
</span>
<span class='ocr_line' id='line_1_22' title="bbox 120 960 856 985; baseline 0.005 -8; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_152' title='bbox 120 961 183 977; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_153' title='bbox 197 960 218 977; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_154' title='bbox 227 961 260 977; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_155' title='bbox 271 961 341 978; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_156' title='bbox 355 961 418 979; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_157' title='bbox 429 962 449 979; x_wconf 94'>in</span>
<span class='ocrx_word' id='word_1_158' title='bbox 464 962 543 983; x_wconf 96'>Japan,”</span>
<span class='ocrx_word' id='word_1_159' title='bbox 555 963 632 980; x_wconf 96'>decreed</span>
<span class='ocrx_word' id='word_1_160' title='bbox 644 964 682 980; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_161' title='bbox 694 963 758 981; x_wconf 96'>issued</span>
<span class='ocrx_word' id='word_1_162' title='bbox 771 964 796 985; x_wconf 93'>by</span>
<span class='ocrx_word' id='word_1_163' title='bbox 805 964 825 981; x_wconf 88'>C.</span>
<span class='ocrx_word' id='word_1_164' title='bbox 834 964 856 981; x_wconf 70'>E.</span>
<span class='ocr_line' id='line_1_22' title="bbox 121 962 857 983; baseline -0.001 -3; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_155' title='bbox 121 963 184 980; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_156' title='bbox 198 962 219 979; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_157' title='bbox 228 963 261 979; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_158' title='bbox 272 962 342 980; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_159' title='bbox 356 962 419 980; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_160' title='bbox 430 962 450 979; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_161' title='bbox 465 962 544 983; x_wconf 95'>Japan,”</span>
<span class='ocrx_word' id='word_1_162' title='bbox 556 962 633 979; x_wconf 96'>decreed</span>
<span class='ocrx_word' id='word_1_163' title='bbox 645 963 683 979; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_164' title='bbox 695 962 759 980; x_wconf 93'>issued</span>
<span class='ocrx_word' id='word_1_165' title='bbox 772 962 797 983; x_wconf 92'>by</span>
<span class='ocrx_word' id='word_1_166' title='bbox 806 962 826 979; x_wconf 81'>C.</span>
<span class='ocrx_word' id='word_1_167' title='bbox 835 962 857 979; x_wconf 47'>BE.</span>
</span>
<span class='ocr_line' id='line_1_23' title="bbox 120 982 856 1008; baseline 0.007 -9; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_165' title='bbox 120 982 150 999; x_wconf 96'>De</span>
<span class='ocrx_word' id='word_1_166' title='bbox 156 982 215 1005; x_wconf 96'>Long,</span>
<span class='ocrx_word' id='word_1_167' title='bbox 224 983 256 1000; x_wconf 93'>the</span>
<span class='ocrx_word' id='word_1_168' title='bbox 264 984 348 1001; x_wconf 91'>miniater</span>
<span class='ocrx_word' id='word_1_169' title='bbox 359 984 381 1001; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_170' title='bbox 389 984 421 1001; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_171' title='bbox 429 984 499 1001; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_172' title='bbox 507 985 570 1002; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_173' title='bbox 577 985 595 1002; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_174' title='bbox 604 985 646 1003; x_wconf 96'>that</span>
<span class='ocrx_word' id='word_1_175' title='bbox 654 987 738 1008; x_wconf 96'>country,</span>
<span class='ocrx_word' id='word_1_176' title='bbox 747 986 766 1003; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_177' title='bbox 775 986 856 1007; x_wconf 93'>Septem-</span>
<span class='ocr_line' id='line_1_23' title="bbox 122 984 858 1007; baseline 0 -6; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_168' title='bbox 122 985 152 1001; x_wconf 96'>De</span>
<span class='ocrx_word' id='word_1_169' title='bbox 158 984 217 1007; x_wconf 96'>Long,</span>
<span class='ocrx_word' id='word_1_170' title='bbox 226 985 258 1002; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_171' title='bbox 266 985 350 1002; x_wconf 96'>minister</span>
<span class='ocrx_word' id='word_1_172' title='bbox 361 985 383 1002; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_173' title='bbox 391 985 423 1002; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_174' title='bbox 431 984 501 1001; x_wconf 97'>United</span>
<span class='ocrx_word' id='word_1_175' title='bbox 509 985 572 1002; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_176' title='bbox 579 984 597 1001; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_177' title='bbox 606 984 648 1002; x_wconf 96'>that</span>
<span class='ocrx_word' id='word_1_178' title='bbox 656 986 740 1006; x_wconf 96'>country,</span>
<span class='ocrx_word' id='word_1_179' title='bbox 749 984 768 1001; x_wconf 92'>in</span>
<span class='ocrx_word' id='word_1_180' title='bbox 777 984 858 1005; x_wconf 91'>Septem-</span>
</span>
<span class='ocr_line' id='line_1_24' title="bbox 119 1004 863 1040; baseline 0.007 -19; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_178' title='bbox 119 1004 158 1026; x_wconf 93'>ber,</span>
<span class='ocrx_word' id='word_1_179' title='bbox 167 1005 221 1027; x_wconf 88'>1870;</span>
<span class='ocrx_word' id='word_1_180' title='bbox 234 1006 271 1022; x_wconf 87'>and</span>
<span class='ocrx_word' id='word_1_181' title='bbox 285 1006 324 1023; x_wconf 87'>also</span>
<span class='ocrx_word' id='word_1_182' title='bbox 337 1006 369 1023; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_183' title='bbox 381 1011 448 1028; x_wconf 96'>papers</span>
<span class='ocrx_word' id='word_1_184' title='bbox 459 1007 566 1024; x_wconf 96'>mentioned</span>
<span class='ocrx_word' id='word_1_185' title='bbox 574 1007 593 1024; x_wconf 94'>in</span>
<span class='ocrx_word' id='word_1_186' title='bbox 602 1008 633 1025; x_wconf 93'>the</span>
<span class='ocrx_word' id='word_1_187' title='bbox 641 1008 739 1030; x_wconf 92'>subjoined</span>
<span class='ocrx_word' id='word_1_188' title='bbox 746 1008 784 1030; x_wconf 97'>list,</span>
<span class='ocrx_word' id='word_1_189' title='bbox 782 1005 863 1040; x_wconf 86'>which,</span>
<span class='ocr_line' id='line_1_24' title="bbox 121 1006 865 1034; baseline 0 -11; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_181' title='bbox 121 1007 160 1028; x_wconf 91'>ber,</span>
<span class='ocrx_word' id='word_1_182' title='bbox 169 1007 223 1029; x_wconf 95'>1870;</span>
<span class='ocrx_word' id='word_1_183' title='bbox 236 1008 273 1024; x_wconf 84'>and</span>
<span class='ocrx_word' id='word_1_184' title='bbox 287 1007 326 1024; x_wconf 95'>also</span>
<span class='ocrx_word' id='word_1_185' title='bbox 339 1007 371 1024; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_186' title='bbox 383 1011 450 1029; x_wconf 96'>papers</span>
<span class='ocrx_word' id='word_1_187' title='bbox 461 1007 568 1024; x_wconf 96'>mentioned</span>
<span class='ocrx_word' id='word_1_188' title='bbox 576 1006 595 1023; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_189' title='bbox 604 1007 635 1024; x_wconf 93'>the</span>
<span class='ocrx_word' id='word_1_190' title='bbox 643 1007 741 1029; x_wconf 92'>subjoined</span>
<span class='ocrx_word' id='word_1_191' title='bbox 748 1006 786 1028; x_wconf 96'>list,</span>
<span class='ocrx_word' id='word_1_192' title='bbox 794 1007 865 1034; x_wconf 86'>which,</span>
</span>
<span class='ocr_line' id='line_1_25' title="bbox 120 1027 555 1050; baseline 0.007 -7; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_190' title='bbox 120 1027 195 1044; x_wconf 96'>contain</span>
<span class='ocrx_word' id='word_1_191' title='bbox 203 1028 322 1049; x_wconf 96'>suggestions</span>
<span class='ocrx_word' id='word_1_192' title='bbox 330 1033 353 1045; x_wconf 95'>on</span>
<span class='ocrx_word' id='word_1_193' title='bbox 361 1028 392 1045; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_194' title='bbox 400 1029 471 1050; x_wconf 96'>subject</span>
<span class='ocrx_word' id='word_1_195' title='bbox 478 1029 555 1046; x_wconf 86'>thereof.</span>
<span class='ocr_line' id='line_1_25' title="bbox 122 1029 557 1051; baseline 0 -5; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_193' title='bbox 122 1029 197 1046; x_wconf 96'>contain</span>
<span class='ocrx_word' id='word_1_194' title='bbox 205 1029 324 1051; x_wconf 96'>suggestions</span>
<span class='ocrx_word' id='word_1_195' title='bbox 332 1034 355 1046; x_wconf 96'>on</span>
<span class='ocrx_word' id='word_1_196' title='bbox 363 1029 394 1046; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_197' title='bbox 402 1029 473 1050; x_wconf 93'>subject</span>
<span class='ocrx_word' id='word_1_198' title='bbox 480 1029 557 1046; x_wconf 85'>thereof.</span>
</span>
</p>
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 118 1050 854 1185">
<span class='ocr_line' id='line_1_26' title="bbox 142 1050 853 1076; baseline 0.007 -11; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_196' title='bbox 142 1050 162 1065; x_wconf 96'>A</span>
<span class='ocrx_word' id='word_1_197' title='bbox 169 1054 216 1071; x_wconf 96'>copy</span>
<span class='ocrx_word' id='word_1_198' title='bbox 224 1050 245 1066; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_199' title='bbox 251 1050 323 1067; x_wconf 81'>Article</span>
<span class='ocrx_word' id='word_1_200' title='bbox 330 1050 397 1067; x_wconf 77'>XXVI</span>
<span class='ocrx_word' id='word_1_201' title='bbox 405 1051 425 1068; x_wconf 95'>of</span>
<span class='ocrx_word' id='word_1_202' title='bbox 431 1051 462 1068; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_203' title='bbox 470 1052 556 1068; x_wconf 94'>consular</span>
<span class='ocrx_word' id='word_1_204' title='bbox 560 1052 674 1073; x_wconf 96'>regulations</span>
<span class='ocrx_word' id='word_1_205' title='bbox 680 1053 696 1069; x_wconf 94'>is</span>
<span class='ocrx_word' id='word_1_206' title='bbox 701 1053 740 1070; x_wconf 96'>also</span>
<span class='ocrx_word' id='word_1_207' title='bbox 747 1053 853 1076; x_wconf 96'>submitted,</span>
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 120 1051 857 1185">
<span class='ocr_line' id='line_1_26' title="bbox 144 1051 855 1074; baseline -0.001 -6; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_199' title='bbox 144 1052 164 1067; x_wconf 95'>A</span>
<span class='ocrx_word' id='word_1_200' title='bbox 171 1056 218 1073; x_wconf 95'>copy</span>
<span class='ocrx_word' id='word_1_201' title='bbox 226 1052 247 1068; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_202' title='bbox 253 1051 325 1068; x_wconf 93'>Article</span>
<span class='ocrx_word' id='word_1_203' title='bbox 332 1051 399 1068; x_wconf 86'>XXVI</span>
<span class='ocrx_word' id='word_1_204' title='bbox 407 1052 427 1069; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_205' title='bbox 433 1051 464 1068; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_206' title='bbox 472 1052 558 1068; x_wconf 95'>consular</span>
<span class='ocrx_word' id='word_1_207' title='bbox 562 1051 676 1072; x_wconf 95'>regulations</span>
<span class='ocrx_word' id='word_1_208' title='bbox 682 1052 698 1068; x_wconf 96'>is</span>
<span class='ocrx_word' id='word_1_209' title='bbox 703 1052 742 1068; x_wconf 94'>also</span>
<span class='ocrx_word' id='word_1_210' title='bbox 749 1051 855 1074; x_wconf 95'>submitted,</span>
</span>
<span class='ocr_line' id='line_1_27' title="bbox 119 1071 854 1097; baseline 0.007 -9; x_size 21; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_208' title='bbox 119 1071 156 1088; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_209' title='bbox 169 1071 202 1088; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_210' title='bbox 216 1072 313 1094; x_wconf 96'>Secretary</span>
<span class='ocrx_word' id='word_1_211' title='bbox 319 1073 340 1089; x_wconf 90'>of</span>
<span class='ocrx_word' id='word_1_212' title='bbox 349 1073 402 1090; x_wconf 96'>State</span>
<span class='ocrx_word' id='word_1_213' title='bbox 413 1073 532 1095; x_wconf 96'>respectfully</span>
<span class='ocrx_word' id='word_1_214' title='bbox 540 1075 632 1097; x_wconf 96'>suggests,</span>
<span class='ocrx_word' id='word_1_215' title='bbox 641 1075 670 1091; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_216' title='bbox 677 1075 710 1092; x_wconf 97'>the</span>
<span class='ocrx_word' id='word_1_217' title='bbox 718 1074 854 1093; x_wconf 96'>consideration</span>
<span class='ocr_line' id='line_1_27' title="bbox 121 1072 856 1096; baseline 0 -5; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_211' title='bbox 121 1073 158 1091; x_wconf 95'>and</span>
<span class='ocrx_word' id='word_1_212' title='bbox 171 1073 204 1090; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_213' title='bbox 218 1074 315 1095; x_wconf 96'>Secretary</span>
<span class='ocrx_word' id='word_1_214' title='bbox 321 1074 342 1090; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_215' title='bbox 351 1074 404 1091; x_wconf 96'>State</span>
<span class='ocrx_word' id='word_1_216' title='bbox 415 1073 534 1095; x_wconf 96'>respectfully</span>
<span class='ocrx_word' id='word_1_217' title='bbox 542 1074 634 1096; x_wconf 96'>suggests,</span>
<span class='ocrx_word' id='word_1_218' title='bbox 643 1074 672 1090; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_219' title='bbox 679 1074 712 1091; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_220' title='bbox 720 1072 856 1091; x_wconf 96'>consideration</span>
</span>
<span class='ocr_line' id='line_1_28' title="bbox 118 1093 852 1118; baseline 0.005 -8; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_218' title='bbox 118 1093 140 1110; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_219' title='bbox 149 1093 246 1116; x_wconf 94'>Congress,</span>
<span class='ocrx_word' id='word_1_220' title='bbox 255 1095 287 1111; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_221' title='bbox 300 1095 395 1117; x_wconf 96'>propriety</span>
<span class='ocrx_word' id='word_1_222' title='bbox 406 1096 427 1112; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_223' title='bbox 435 1096 516 1117; x_wconf 96'>limiting</span>
<span class='ocrx_word' id='word_1_224' title='bbox 528 1095 560 1113; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_225' title='bbox 571 1101 633 1118; x_wconf 96'>power</span>
<span class='ocrx_word' id='word_1_226' title='bbox 641 1097 662 1114; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_227' title='bbox 671 1097 764 1114; x_wconf 96'>ministers</span>
<span class='ocrx_word' id='word_1_228' title='bbox 772 1089 799 1122; x_wconf 95'>to</span>
<span class='ocrx_word' id='word_1_229' title='bbox 798 1097 851 1115; x_wconf 95'>make</span>
<span class='ocr_line' id='line_1_28' title="bbox 120 1095 854 1118; baseline -0.001 -5; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_221' title='bbox 120 1095 142 1113; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_222' title='bbox 151 1095 248 1118; x_wconf 96'>Congress,</span>
<span class='ocrx_word' id='word_1_223' title='bbox 257 1097 289 1113; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_224' title='bbox 302 1096 397 1118; x_wconf 96'>propriety</span>
<span class='ocrx_word' id='word_1_225' title='bbox 408 1096 429 1113; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_226' title='bbox 437 1096 518 1117; x_wconf 96'>limiting</span>
<span class='ocrx_word' id='word_1_227' title='bbox 530 1095 562 1113; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_228' title='bbox 573 1100 635 1117; x_wconf 96'>power</span>
<span class='ocrx_word' id='word_1_229' title='bbox 643 1096 664 1113; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_230' title='bbox 673 1096 766 1113; x_wconf 96'>ministers</span>
<span class='ocrx_word' id='word_1_231' title='bbox 774 1091 801 1122; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_232' title='bbox 800 1095 854 1113; x_wconf 96'>make</span>
</span>
<span class='ocr_line' id='line_1_29' title="bbox 119 1115 853 1141; baseline 0.007 -9; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_230' title='bbox 119 1115 194 1133; x_wconf 88'>decrees</span>
<span class='ocrx_word' id='word_1_231' title='bbox 206 1116 243 1133; x_wconf 88'>and</span>
<span class='ocrx_word' id='word_1_232' title='bbox 248 1116 358 1139; x_wconf 96'>regulation,</span>
<span class='ocrx_word' id='word_1_233' title='bbox 367 1117 385 1134; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_234' title='bbox 393 1118 425 1135; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_235' title='bbox 433 1122 486 1135; x_wconf 96'>sense</span>
<span class='ocrx_word' id='word_1_236' title='bbox 493 1118 513 1135; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_237' title='bbox 521 1118 581 1136; x_wconf 96'>which</span>
<span class='ocrx_word' id='word_1_238' title='bbox 589 1119 605 1136; x_wconf 96'>it</span>
<span class='ocrx_word' id='word_1_239' title='bbox 612 1119 629 1136; x_wconf 96'>is</span>
<span class='ocrx_word' id='word_1_240' title='bbox 636 1118 708 1136; x_wconf 95'>limited</span>
<span class='ocrx_word' id='word_1_241' title='bbox 716 1120 741 1141; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_242' title='bbox 747 1120 853 1141; x_wconf 96'>paragraph</span>
<span class='ocr_line' id='line_1_29' title="bbox 121 1117 856 1140; baseline 0 -6; x_size 22; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_233' title='bbox 121 1118 197 1135; x_wconf 95'>decrees</span>
<span class='ocrx_word' id='word_1_234' title='bbox 208 1118 246 1135; x_wconf 94'>and</span>
<span class='ocrx_word' id='word_1_235' title='bbox 250 1117 361 1140; x_wconf 94'>regulation,</span>
<span class='ocrx_word' id='word_1_236' title='bbox 369 1118 388 1135; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_237' title='bbox 395 1119 428 1136; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_238' title='bbox 435 1122 489 1135; x_wconf 96'>sense</span>
<span class='ocrx_word' id='word_1_239' title='bbox 495 1118 516 1135; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_240' title='bbox 523 1118 584 1136; x_wconf 96'>which</span>
<span class='ocrx_word' id='word_1_241' title='bbox 591 1118 608 1135; x_wconf 96'>it</span>
<span class='ocrx_word' id='word_1_242' title='bbox 614 1118 632 1135; x_wconf 96'>is</span>
<span class='ocrx_word' id='word_1_243' title='bbox 638 1117 711 1135; x_wconf 95'>limited</span>
<span class='ocrx_word' id='word_1_244' title='bbox 718 1118 743 1139; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_245' title='bbox 750 1118 856 1139; x_wconf 96'>paragraph</span>
</span>
<span class='ocr_line' id='line_1_30' title="bbox 119 1138 854 1164; baseline 0.007 -10; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_243' title='bbox 119 1138 153 1154; x_wconf 94'>431</span>
<span class='ocrx_word' id='word_1_244' title='bbox 161 1138 183 1155; x_wconf 95'>of</span>
<span class='ocrx_word' id='word_1_245' title='bbox 192 1139 224 1155; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_246' title='bbox 232 1139 297 1156; x_wconf 96'>article</span>
<span class='ocrx_word' id='word_1_247' title='bbox 304 1139 366 1157; x_wconf 93'>before</span>
<span class='ocrx_word' id='word_1_248' title='bbox 374 1140 505 1157; x_wconf 91'>named—that</span>
<span class='ocrx_word' id='word_1_249' title='bbox 513 1141 534 1161; x_wconf 96'>is,</span>
<span class='ocrx_word' id='word_1_250' title='bbox 544 1141 579 1158; x_wconf 87'>“to</span>
<span class='ocrx_word' id='word_1_251' title='bbox 588 1142 628 1158; x_wconf 96'>acts</span>
<span class='ocrx_word' id='word_1_252' title='bbox 635 1143 757 1164; x_wconf 95'>necessary</span>
<span class='ocrx_word' id='word_1_253' title='bbox 732 1134 761 1168; x_wconf 95'>to</span>
<span class='ocrx_word' id='word_1_254' title='bbox 767 1142 854 1164; x_wconf 96'>organize</span>
<span class='ocr_line' id='line_1_30' title="bbox 122 1140 857 1162; baseline 0 -5; x_size 22; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_246' title='bbox 122 1140 156 1157; x_wconf 96'>431</span>
<span class='ocrx_word' id='word_1_247' title='bbox 164 1140 186 1157; x_wconf 97'>of</span>
<span class='ocrx_word' id='word_1_248' title='bbox 195 1141 227 1157; x_wconf 95'>the</span>
<span class='ocrx_word' id='word_1_249' title='bbox 235 1140 300 1158; x_wconf 95'>article</span>
<span class='ocrx_word' id='word_1_250' title='bbox 307 1140 369 1158; x_wconf 93'>before</span>
<span class='ocrx_word' id='word_1_251' title='bbox 377 1140 508 1158; x_wconf 92'>named—that</span>
<span class='ocrx_word' id='word_1_252' title='bbox 516 1141 537 1161; x_wconf 96'>is,</span>
<span class='ocrx_word' id='word_1_253' title='bbox 547 1141 582 1158; x_wconf 85'>“to</span>
<span class='ocrx_word' id='word_1_254' title='bbox 591 1141 631 1157; x_wconf 96'>acts</span>
<span class='ocrx_word' id='word_1_255' title='bbox 638 1141 760 1162; x_wconf 95'>necessary</span>
<span class='ocrx_word' id='word_1_256' title='bbox 738 1136 762 1167; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_257' title='bbox 770 1140 857 1162; x_wconf 96'>organize</span>
</span>
<span class='ocr_line' id='line_1_31' title="bbox 118 1160 659 1185; baseline 0.007 -8; x_size 23; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_255' title='bbox 118 1160 156 1177; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_256' title='bbox 165 1160 207 1182; x_wconf 96'>give</span>
<span class='ocrx_word' id='word_1_257' title='bbox 215 1161 309 1183; x_wconf 95'>efficiency</span>
<span class='ocrx_word' id='word_1_258' title='bbox 317 1163 337 1179; x_wconf 95'>to</span>
<span class='ocrx_word' id='word_1_259' title='bbox 345 1162 377 1179; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_260' title='bbox 385 1164 446 1180; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_261' title='bbox 455 1163 530 1180; x_wconf 96'>created</span>
<span class='ocrx_word' id='word_1_262' title='bbox 536 1163 561 1185; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_263' title='bbox 571 1163 604 1181; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_264' title='bbox 612 1164 659 1181; x_wconf 94'>act.”</span>
<span class='ocr_line' id='line_1_31' title="bbox 121 1162 662 1185; baseline 0.002 -6; x_size 22; x_descenders 4; x_ascenders 5">
<span class='ocrx_word' id='word_1_258' title='bbox 121 1162 159 1180; x_wconf 96'>and</span>
<span class='ocrx_word' id='word_1_259' title='bbox 168 1162 210 1184; x_wconf 97'>give</span>
<span class='ocrx_word' id='word_1_260' title='bbox 218 1163 312 1184; x_wconf 96'>efficiency</span>
<span class='ocrx_word' id='word_1_261' title='bbox 320 1164 340 1180; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_262' title='bbox 348 1163 380 1180; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_263' title='bbox 388 1164 449 1181; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_264' title='bbox 458 1163 533 1180; x_wconf 96'>created</span>
<span class='ocrx_word' id='word_1_265' title='bbox 539 1163 564 1185; x_wconf 96'>by</span>
<span class='ocrx_word' id='word_1_266' title='bbox 574 1162 607 1180; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_267' title='bbox 615 1163 662 1180; x_wconf 96'>act.”</span>
</span>
</p>
<p class='ocr_par' id='par_1_15' lang='eng' title="bbox 141 1183 555 1205">
<span class='ocr_line' id='line_1_32' title="bbox 141 1183 555 1205; baseline 0.007 -6; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_265' title='bbox 141 1183 268 1205; x_wconf 34'>Respectfully</span>
<span class='ocrx_word' id='word_1_266' title='bbox 276 1183 383 1201; x_wconf 93'>submitted.</span>
<span class='ocrx_word' id='word_1_267' title='bbox 553 1194 555 1196; x_wconf 0'>.</span>
<p class='ocr_par' id='par_1_15' lang='eng' title="bbox 144 1184 386 1207">
<span class='ocr_line' id='line_1_32' title="bbox 144 1184 386 1207; baseline 0 -5; x_size 23; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_268' title='bbox 144 1185 271 1207; x_wconf 86'>Respectfully</span>
<span class='ocrx_word' id='word_1_269' title='bbox 279 1184 386 1202; x_wconf 93'>submitted.</span>
</span>
</p>
<p class='ocr_par' id='par_1_16' lang='eng' title="bbox 622 1208 831 1226">
<span class='ocr_line' id='line_1_33' title="bbox 622 1208 831 1226; baseline 0.005 -1; x_size 23.623137; x_descenders 5.52807; x_ascenders 5.5594797">
<span class='ocrx_word' id='word_1_268' title='bbox 622 1208 757 1226; x_wconf 96'>HAMILTON</span>
<span class='ocrx_word' id='word_1_269' title='bbox 765 1209 831 1226; x_wconf 96'>FISH.</span>
<p class='ocr_par' id='par_1_16' lang='eng' title="bbox 625 1206 834 1225">
<span class='ocr_line' id='line_1_33' title="bbox 625 1206 834 1225; baseline -0.005 0; x_size 23.69949; x_descenders 5.5158105; x_ascenders 5.590498">
<span class='ocrx_word' id='word_1_270' title='bbox 625 1206 760 1225; x_wconf 96'>HAMILTON</span>
<span class='ocrx_word' id='word_1_271' title='bbox 768 1207 834 1224; x_wconf 95'>FISH.</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_10' title="bbox 140 1227 314 1246">
<p class='ocr_par' id='par_1_17' lang='eng' title="bbox 140 1227 314 1246">
<span class='ocr_line' id='line_1_34' title="bbox 140 1227 314 1246; baseline 0.011 -2; x_size 22.296295; x_descenders 5.2962961; x_ascenders 4">
<span class='ocrx_word' id='word_1_270' title='bbox 140 1227 180 1244; x_wconf 96'>The</span>
<span class='ocrx_word' id='word_1_271' title='bbox 188 1228 314 1246; x_wconf 81'>PRESIDENT,</span>
<div class='ocr_carea' id='block_1_10' title="bbox 143 1229 317 1247">
<p class='ocr_par' id='par_1_17' lang='eng' title="bbox 143 1229 317 1247">
<span class='ocr_line' id='line_1_34' title="bbox 143 1229 317 1247; baseline 0.006 -1; x_size 22.296295; x_descenders 5.2962961; x_ascenders 4">
<span class='ocrx_word' id='word_1_272' title='bbox 143 1229 183 1246; x_wconf 96'>The</span>
<span class='ocrx_word' id='word_1_273' title='bbox 191 1230 317 1247; x_wconf 74'>PRESIDENT,</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_11' title="bbox 341 1307 628 1331">
<p class='ocr_par' id='par_1_18' lang='eng' title="bbox 341 1307 628 1331">
<span class='ocr_line' id='line_1_35' title="bbox 341 1307 628 1331; baseline 0.007 -7; x_size 23; x_descenders 5; x_ascenders 5">
<span class='ocrx_word' id='word_1_272' title='bbox 341 1307 383 1324; x_wconf 95'>List</span>
<span class='ocrx_word' id='word_1_273' title='bbox 389 1307 411 1330; x_wconf 92'>of</span>
<span class='ocrx_word' id='word_1_274' title='bbox 419 1308 553 1330; x_wconf 90'>accompanying</span>
<span class='ocrx_word' id='word_1_275' title='bbox 557 1313 628 1331; x_wconf 89'>papers.</span>
<div class='ocr_carea' id='block_1_11' title="bbox 345 1308 632 1331">
<p class='ocr_par' id='par_1_18' lang='eng' title="bbox 345 1308 632 1331">
<span class='ocr_line' id='line_1_35' title="bbox 345 1308 632 1331; baseline 0 -6; x_size 22; x_descenders 5; x_ascenders 4">
<span class='ocrx_word' id='word_1_274' title='bbox 345 1308 387 1325; x_wconf 91'>List</span>
<span class='ocrx_word' id='word_1_275' title='bbox 393 1308 415 1331; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_276' title='bbox 423 1308 557 1330; x_wconf 96'>accompanying</span>
<span class='ocrx_word' id='word_1_277' title='bbox 561 1312 632 1331; x_wconf 87'>papers.</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_12' title="bbox 138 1337 853 1395">
<p class='ocr_par' id='par_1_19' lang='eng' title="bbox 138 1337 853 1395">
<span class='ocr_line' id='line_1_36' title="bbox 138 1337 853 1372; baseline 0.007 -10; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_276' title='bbox 138 1346 155 1363; x_wconf 96'>1,</span>
<span class='ocrx_word' id='word_1_277' title='bbox 166 1346 289 1368; x_wconf 96'>Regulations</span>
<span class='ocrx_word' id='word_1_278' title='bbox 296 1337 325 1364; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_279' title='bbox 334 1339 366 1365; x_wconf 97'>the</span>
<span class='ocrx_word' id='word_1_280' title='bbox 374 1348 459 1366; x_wconf 96'>consular</span>
<span class='ocrx_word' id='word_1_281' title='bbox 466 1350 529 1366; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_282' title='bbox 537 1349 558 1366; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_283' title='bbox 566 1349 599 1366; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_284' title='bbox 607 1349 678 1367; x_wconf 96'>United</span>
<span class='ocrx_word' id='word_1_285' title='bbox 686 1349 749 1367; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_286' title='bbox 757 1349 777 1367; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_287' title='bbox 784 1350 853 1372; x_wconf 96'>Japan.</span>
<div class='ocr_carea' id='block_1_12' title="bbox 142 1340 857 1393">
<p class='ocr_par' id='par_1_19' lang='eng' title="bbox 142 1340 857 1393">
<span class='ocr_line' id='line_1_36' title="bbox 142 1340 857 1370; baseline 0 -5; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_278' title='bbox 142 1348 159 1365; x_wconf 96'>1,</span>
<span class='ocrx_word' id='word_1_279' title='bbox 170 1348 293 1370; x_wconf 96'>Regulations</span>
<span class='ocrx_word' id='word_1_280' title='bbox 300 1348 329 1365; x_wconf 96'>for</span>
<span class='ocrx_word' id='word_1_281' title='bbox 338 1340 370 1366; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_282' title='bbox 378 1348 463 1366; x_wconf 96'>consular</span>
<span class='ocrx_word' id='word_1_283' title='bbox 470 1350 533 1366; x_wconf 96'>courts</span>
<span class='ocrx_word' id='word_1_284' title='bbox 541 1349 562 1366; x_wconf 96'>of</span>
<span class='ocrx_word' id='word_1_285' title='bbox 570 1348 603 1365; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_286' title='bbox 611 1348 682 1366; x_wconf 95'>United</span>
<span class='ocrx_word' id='word_1_287' title='bbox 690 1348 753 1366; x_wconf 96'>States</span>
<span class='ocrx_word' id='word_1_288' title='bbox 761 1347 781 1365; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_289' title='bbox 788 1348 857 1370; x_wconf 93'>Japan.</span>
</span>
<span class='ocr_line' id='line_1_37' title="bbox 138 1369 812 1395; baseline 0.007 -10; x_size 23; x_descenders 5; x_ascenders 6">
<span class='ocrx_word' id='word_1_288' title='bbox 138 1369 155 1385; x_wconf 90'>2.</span>
<span class='ocrx_word' id='word_1_289' title='bbox 166 1369 201 1385; x_wconf 95'>Mr.</span>
<span class='ocrx_word' id='word_1_290' title='bbox 210 1369 256 1386; x_wconf 96'>Fish</span>
<span class='ocrx_word' id='word_1_291' title='bbox 264 1370 284 1387; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_292' title='bbox 291 1370 326 1387; x_wconf 86'>Mr.</span>
<span class='ocrx_word' id='word_1_293' title='bbox 334 1370 364 1387; x_wconf 96'>De</span>
<span class='ocrx_word' id='word_1_294' title='bbox 371 1370 429 1393; x_wconf 96'>Long,</span>
<span class='ocrx_word' id='word_1_295' title='bbox 438 1371 546 1392; x_wconf 96'>September</span>
<span class='ocrx_word' id='word_1_296' title='bbox 553 1372 581 1394; x_wconf 96'>10,</span>
<span class='ocrx_word' id='word_1_297' title='bbox 590 1372 640 1389; x_wconf 96'>1870.</span>
<span class='ocrx_word' id='word_1_298' title='bbox 810 1393 812 1395; x_wconf 0'>.</span>
<span class='ocr_line' id='line_1_37' title="bbox 142 1371 644 1393; baseline 0.004 -7; x_size 30.84; x_descenders 5; x_ascenders 8.8400002">
<span class='ocrx_word' id='word_1_290' title='bbox 142 1371 159 1387; x_wconf 90'>2,</span>
<span class='ocrx_word' id='word_1_291' title='bbox 170 1371 205 1387; x_wconf 96'>Mr.</span>
<span class='ocrx_word' id='word_1_292' title='bbox 214 1371 260 1388; x_wconf 95'>Fish</span>
<span class='ocrx_word' id='word_1_293' title='bbox 268 1372 288 1389; x_wconf 95'>to</span>
<span class='ocrx_word' id='word_1_294' title='bbox 295 1371 330 1388; x_wconf 95'>Mr.</span>
<span class='ocrx_word' id='word_1_295' title='bbox 338 1371 368 1388; x_wconf 96'>De</span>
<span class='ocrx_word' id='word_1_296' title='bbox 375 1371 433 1393; x_wconf 96'>Long,</span>
<span class='ocrx_word' id='word_1_297' title='bbox 442 1371 550 1392; x_wconf 95'>September</span>
<span class='ocrx_word' id='word_1_298' title='bbox 557 1371 585 1393; x_wconf 95'>10,</span>
<span class='ocrx_word' id='word_1_299' title='bbox 594 1371 644 1388; x_wconf 79'>1870,</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_13' title="bbox 0 0 1000 1520">
<p class='ocr_par' id='par_1_20' lang='eng' title="bbox 0 0 1000 1520">
<span class='ocr_line' id='line_1_38' title="bbox 0 0 1000 1520; baseline 0 9; x_size 770; x_descenders -385; x_ascenders 385">
<span class='ocrx_word' id='word_1_299' title='bbox 0 0 1000 1520; x_wconf 95'> </span>
<span class='ocr_line' id='line_1_38' title="bbox 0 0 1000 1520; baseline 0 2; x_size 763; x_descenders -381.5; x_ascenders 381.5">
<span class='ocrx_word' id='word_1_300' title='bbox 0 0 1000 1520; x_wconf 95'> </span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_14' title="bbox 19 1496 978 1520">
<p class='ocr_par' id='par_1_21' lang='eng' title="bbox 19 1496 978 1520">
<span class='ocr_line' id='line_1_39' title="bbox 19 1496 978 1520; baseline 0 7; x_size 16; x_descenders -8; x_ascenders 8">
<span class='ocrx_word' id='word_1_300' title='bbox 19 1496 978 1520; x_wconf 95'> </span>
<div class='ocr_carea' id='block_1_14' title="bbox 24 1504 845 1520">
<p class='ocr_par' id='par_1_21' lang='eng' title="bbox 24 1504 845 1520">
<span class='ocr_line' id='line_1_39' title="bbox 24 1504 845 1520; baseline 0 0; x_size 8; x_descenders -4; x_ascenders 4">
<span class='ocrx_word' id='word_1_301' title='bbox 24 1504 845 1520; x_wconf 95'> </span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_15' title="bbox 982 987 1000 1500">
<p class='ocr_par' id='par_1_22' lang='eng' title="bbox 982 987 1000 1500">
<span class='ocr_line' id='line_1_40' title="bbox 982 987 1000 1500; baseline 0 0; x_size 256.5; x_descenders -128.25; x_ascenders 128.25">
<span class='ocrx_word' id='word_1_302' title='bbox 982 987 1000 1500; x_wconf 95'> </span>
</span>
</p>
</div>
@@ -1,5 +1,5 @@
41st CONGRESS; | SENATE.
3d Session. }
4ist ConGREss, } SENATE. { Ex. Doc,
3d Session. No. 25.
MESSAGE
@@ -12,7 +12,7 @@ COMMUNICATING
A copy of regulations for the consular courts of the United States in Japan,
decreed and issued by the minister of the United States in that country.
January 27, 1871,—Read, referred to the Committee on Commerce, and ordered to be
JANUARY 27, 1871,—Read, referred to the Committee on Commerce, and ordered to be
printed.
To the Senate and House of Representatives :
@@ -26,13 +26,13 @@ U. 8. GRANT.
WASHINGTON, January 27, 1871.
DEPARTMENT OF STATE,
. Washington, January 26, 1870,
Washington, January 26, 1870,
The Secretary of State has the honor to submit herewith, for revision
by Congress, in conformity with the provisions of section 6 of the act
approved 22d of June, 1860, a copy of “regulations for the consular
courts of the United States in Japan,” decreed and issued by C. E.
De Long, the miniater of the United States in that country, in Septem-
courts of the United States in Japan,” decreed and issued by C. BE.
De Long, the minister of the United States in that country, in Septem-
ber, 1870; and also the papers mentioned in the subjoined list, which,
contain suggestions on the subject thereof.
@@ -43,7 +43,7 @@ decrees and regulation, in the sense in which it is limited by paragraph
431 of the article before named—that is, “to acts necessary to organize
and give efficiency to the courts created by the act.”
Respectfully submitted. .
Respectfully submitted.
HAMILTON FISH.
@@ -52,7 +52,9 @@ The PRESIDENT,
List of accompanying papers.
1, Regulations for the consular courts of the United States in Japan.
2. Mr. Fish to Mr. De Long, September 10, 1870. .
2, Mr. Fish to Mr. De Long, September 10, 1870,
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.wyikj5nw/000001_ocr.png"; bbox 0 0 512 512; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.tyuf6jbx/000001_ocr.png"; bbox 0 0 512 512; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 0 0 477 512">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 0 0 477 512">
<span class='ocr_line' id='line_1_1' title="bbox 0 0 477 512; baseline 0 0; x_size 684; x_descenders 171; x_ascenders 171">
+44 -64
View File
@@ -1,64 +1,44 @@
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/2400dpi.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000003_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000004_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000002_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000004_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000003_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000002_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/poster.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "Darwin-18.7.0-x86_64-i386-64bit", "python": "3.7.7", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__--psm__7__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__--psm__7__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "--psm", "7", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_hocr__hocr__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/2400dpi.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/aspect.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/ccitt.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/graph_ocred.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/jbig2.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/lichtenstein.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/palette.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/poster.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/skew.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000002_ocr.png__000002_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000002_ocr.png", "$TMPDIR/000002_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/3small.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000003_ocr.png__000003_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000003_ocr.png", "$TMPDIR/000003_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000004_ocr.png__000004_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000004_ocr.png", "$TMPDIR/000004_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000005_ocr.png__000005_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000005_ocr.png", "$TMPDIR/000005_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_hocr__hocr__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_hocr", "hocr", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__eng__000006_ocr.png__000006_ocr_tess__pdf__txt", "sourcefile": "resources/multipage.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "$TMPDIR/000006_ocr.png", "$TMPDIR/000006_ocr_tess", "pdf", "txt"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000001_rasterize_preview.jpg__stdout", "sourcefile": "resources/poster.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000001_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000002_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000002_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000003_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000003_rasterize_preview.jpg", "stdout"]}
{"tesseract_version": "4.1.1", "platform": "macOS-10.14.6-x86_64-i386-64bit", "python": "3.9.0", "argv_slug": "__-l__osd__--psm__0__000004_rasterize_preview.jpg__stdout", "sourcefile": "resources/cardinal.pdf", "args": ["-l", "osd", "--psm", "0", "$TMPDIR/000004_rasterize_preview.jpg", "stdout"]}
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.asx0dv8k/000001_ocr.png"; bbox 0 0 1600 1962; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.sh2wg519/000001_ocr.png"; bbox 0 0 1600 1962; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 84 216 786 1605">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 84 216 786 1605">
<span class='ocr_line' id='line_1_1' title="bbox 84 216 786 1605; baseline 0 0; x_size 694.5; x_descenders -347.25; x_ascenders 347.25">
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.asx0dv8k/000003_ocr.png"; bbox 0 0 2272 1848; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.sh2wg519/000003_ocr.png"; bbox 0 0 2272 1848; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 201 38 2167 118">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 201 38 2167 118">
<span class='ocr_line' id='line_1_1' title="bbox 201 38 2167 118; baseline -0.001 -17; x_size 79; x_descenders 17; x_ascenders 16">
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.asx0dv8k/000004_ocr.png"; bbox 0 0 2272 1848; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.sh2wg519/000004_ocr.png"; bbox 0 0 2272 1848; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 200 38 2168 120">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 200 38 2168 120">
<span class='ocr_line' id='line_1_1' title="bbox 200 38 2168 120; baseline 0 -18; x_size 80; x_descenders 18; x_ascenders 16">
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.asx0dv8k/000005_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.sh2wg519/000005_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 70 403 1182 833">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 70 403 1182 833">
<span class='ocr_line' id='line_1_1' title="bbox 70 403 1180 461; baseline 0.002 -14; x_size 59; x_descenders 13; x_ascenders 15">
@@ -9,7 +9,7 @@
<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/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/com.github.ocrmypdf.asx0dv8k/000006_ocr.png"; bbox 0 0 3300 2550; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/var/folders/2s/7t022mgj0h5cprbq0dtb1ksm0000gn/T/ocrmypdf.io.sh2wg519/000006_ocr.png"; bbox 0 0 3300 2550; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 438 20 1559 450">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 438 20 1559 450">
<span class='ocr_line' id='line_1_1' title="bbox 438 20 1556 74; baseline 0 -12; x_size 54; x_descenders 12; x_ascenders 14">

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