Compare commits

...
696 Commits
Author SHA1 Message Date
James R. Barlow 7b2dd892e5 v16.10.0 release notes 2025-02-26 15:16:18 -08:00
James R. Barlow c05ed7297c Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2025-02-26 15:16:07 -08:00
Odin DahlströmandJames R. Barlow c29f58a8b7 Process hOCR textangle attribute 2025-02-26 15:09:59 -08:00
jbarlowandGitHub eb303fef1a Merge pull request #1441 from aliemjay/fix-prog-bar 2025-02-26 15:05:54 -08:00
James R. Barlow 2a55ceadd0 Merge branch 'pr/rugk/1489' 2025-02-26 14:59:06 -08:00
James R. Barlow 71991ad09b Remove podman 2025-02-26 14:58:43 -08:00
James R. Barlow bd60d6ccd9 Merge branch 'pr/rugk/1488' 2025-02-26 14:57:46 -08:00
James R. Barlow 83b4469ef1 Word wrap 2025-02-26 14:57:18 -08:00
James R. Barlow d2a7caf496 Merge branch 'feature/remove-ttyd' 2025-02-26 14:54:40 -08:00
James R. Barlow ff0ea45bf2 Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2025-02-26 14:53:48 -08:00
James R. Barlow b5bc1d209c Remove ttyd 2025-02-26 14:53:13 -08:00
rugkandGitHub 53270b8eb1 Doc: Update docker.rst to use
I prefer to write the name in full aka `jbarlow83/ocrmypdf-alpine` and I'd also suggest to document this because:
* if you use `docker tag` this AFAIK only tags the currently downloaded (=pulled) version of that image
* in case a new update comes out, the new one will not be pulled automatically and one would have to pull and tag the image locally, again
* This `docker tag`  command is easily overlooked, if users just run `docker run ocrmypdf` this may or may not work, depending on how it is resolved.
   Also, AFAIK if one could get Docker to register https://hub.docker.com/ocrmypdf then this would suddenly be used instead of your image (currently `podman pull docker.io/ocrmypdf` returns a 404 for me, though)
* It is more common to write at least the user namespace there and the project, to prevent such errors.

Also, default [Docker has many shortcuts for this and e.g. assumes Docker-Hub is always being used](https://stackoverflow.com/questions/37861791/how-are-docker-image-names-parsed). Podman usually does not, that's why I personally prefer to use the very full and clear `docker.io/jbarlow83/ocrmypdf-alpine:latest` e.g. for alpine. This makes it not only clear which version is used, but also where it is pulled from (should one have configured different Docker registries).
2025-02-26 02:43:46 +01:00
rugkandGitHub 3049a10757 doc: Update docker.rst to explain how to use with podman
I've fiddled/struggled with this by myself, by getting a permission error like this one:
```shell
OutputFileAccessError: Output file location (./output.pdf) is not a writable file.
``` 

I've loosely followed and found https://github.com/containers/podlet?tab=readme-ov-file#in-a-container and explained the required flags in a similar way, but adapted for this tool (it likely won't be used so much on system files).

I've tested it and it works fine for me. The same issue may be on Docker rootless, but I guess people will get that and I cannot test it here.
2025-02-26 02:30:25 +01:00
jbarlowandGitHub 53002f65d9 Merge pull request #1485 from alexpdp7/main
Correct the installation instructions for Windows
2025-02-22 11:18:14 -08:00
alex acea9529ea Correct the installation instructions for Windows 2025-02-22 10:46:22 +01:00
James R. Barlow 32322a9fe9 Fix broken test_hocrtransform_matches_sandwich
Expect word similarity rather than exact match. Difference appears to be due to quote styles.

Thanks @QuLogic for reporting.
2025-02-09 13:57:50 -08:00
James R. Barlow 6b09129911 Fix github release yaml 2025-02-07 16:25:29 -08:00
James R. Barlow e4274a956d v16.9.0 release notes 2025-02-07 00:53:08 -08:00
James R. Barlow 19af116034 Tidy whitespace 2025-02-06 00:40:35 -08:00
jbarlowandGitHub a5896c45e8 Merge pull request #1466 from 0dinD/fix-hocr-caption 2025-02-06 00:38:59 -08:00
Odin Dahlström b7d63f3dc1 Process ocr_caption lines 2025-01-30 17:49:06 +01:00
James R. Barlow 137b054f43 Adjust test again for older Ghostscript 2025-01-27 23:44:37 -08:00
James R. Barlow e6daa28c6d Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2025-01-25 16:55:53 -08:00
James R. Barlow 2512093076 Don't build PDF documentation anymore to fix RTD build issues 2025-01-15 16:17:12 -08:00
Quentin FuxaandGitHub 66bc4a3733 Improve docs for _progressbar.py (#1456) 2025-01-09 12:21:21 -08:00
James R. Barlow 65df44f670 Modify tests to deal with variety of Ghostscript versions 2025-01-09 02:14:29 -08:00
James R. Barlow 6edc749023 Fix error handling when PDF contains an invalid image with both ImageMask and ColorSpace set
Fixes #1453
2025-01-07 00:27:07 -08:00
James R. Barlow cff98d258e Upgrade to Alpine 3.21 2025-01-05 08:53:02 -08:00
James R. Barlow d1fc77e1b6 docs: add imgconverter 2025-01-04 12:39:55 -08:00
James R. Barlow 17eed0529a Update notes 2025-01-04 12:21:46 -08:00
James R. Barlow f02353686d s/input/output 2025-01-04 12:18:07 -08:00
James R. Barlow 32813a3c3d Merge remote-tracking branch 'origin/dependabot/github_actions/astral-sh/setup-uv-5' 2025-01-04 12:10:18 -08:00
James R. Barlow 073a434ab3 Fix webservice interactions with Docker 2025-01-04 12:09:32 -08:00
James R. Barlow f390e7f9d1 Add cache to Dockerfiles 2025-01-04 12:09:11 -08:00
James R. Barlow bfbe571f12 docs: fix more rst formatting issues 2025-01-04 10:59:52 -08:00
James R. Barlow 368568b8ea Change yaml strings in release script 2025-01-04 01:05:27 -08:00
James R. Barlow 55e7177dbe Present similar interface in webservice.py 2025-01-04 01:04:58 -08:00
James R. Barlow b486df7e2d docs: auto update year 2025-01-04 01:04:29 -08:00
James R. Barlow 74a84b6ae9 Fix numerous documentation build problems 2025-01-03 12:23:42 -08:00
James R. Barlow cfebf1dc8b alpine: fix pyarrow name again 2025-01-01 20:30:12 -08:00
James R. Barlow 1aaff4af6f alpine: use pyarrow package for webservice 2025-01-01 18:45:27 -08:00
James R. Barlow 36c82e0659 Add debugging helper scripts 2025-01-01 18:03:15 -08:00
James R. Barlow 522f9d5f56 Merge branch 'pr/pajowu/1448' 2025-01-01 18:00:52 -08:00
James R. Barlow 796e424ee5 graft: Handle stack underflow 2025-01-01 18:00:39 -08:00
James R. Barlow d87db6cad0 Merge branch 'pr/pajowu/1446' 2025-01-01 17:50:33 -08:00
James R. Barlow dd6ed4c5f8 Switch to streamlit based web app 2025-01-01 17:26:22 -08:00
James R. Barlow 206bab74bc Improve diagnostics for unidentified image 2025-01-01 17:13:57 -08:00
dependabot[bot]andGitHub b333480749 Bump astral-sh/setup-uv from 4 to 5
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 4 to 5.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v4...v5)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 10:08:40 +00:00
James R. Barlow f71a5ffd61 hocr: comment typo 2024-12-23 01:46:00 -08:00
James R. Barlow b7c3ea70ed Try triage workflow helper 2024-12-12 17:00:21 -08:00
Kara Engelhardt 636623ab49 graft: fix invisible text appearing after strip_invisible_text
strip_invisible_text resets the text render mode on each `BT` (begin text) command. However the text state is not actually reset for each text element, only for each page.

The pdf reference says:

> The text state operators can appear outside text objects, and the values they set
> are retained across text objects in a single content stream. Like other graphics
> state parameters, these parameters are initialized to their default values at the
> beginning of each page.
>
> -- https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.7old.pdf#page=397

With the current implementation, a text object is only deleted if it contains a `3 Tr` command (setting the text rendering mode to invalid). However the rendering mode may be set once and then not changed for multiple text objects or set outside of a text object.
In that case only the first text object (which contains the `3 Tr`-command) is removed. This not only leaves the other text objects in the pdf, but also makes them visible, since the text object that contained the `3 Tr`-command is removed.

This PR updates `strip_invisible_text` to not reset the rendering mode for each object and to keep track of the rendering mode when the graphic state is pushed/popped.
2024-12-11 18:01:12 +01:00
Kara Engelhardt 74253e5fc8 hocr: only add space if boxwidth is positive 2024-12-11 16:23:45 +01:00
James R. Barlow 02d85ff070 Tell uv not to sync dev harder 2024-12-08 23:20:18 -08:00
James R. Barlow 179c36151b Tell uv not to sync dev 2024-12-08 17:27:31 -08:00
James R. Barlow 3c4b099cb1 Don't try to install dev dependencies in build 2024-12-08 14:03:04 -08:00
James R. Barlow 15df9c370c Update notes 2024-12-08 12:20:40 -08:00
James R. Barlow 86d92ef490 Disable logging of markup
Fixes #1444
2024-12-08 12:17:19 -08:00
Elliott Sales de AndradeandGitHub 8f44b29ca3 Update intersphinx mapping to current format (#1443)
This will break somewhere in Sphinx 8.
2024-12-05 23:54:21 -08:00
joskezelenskyandGitHub 5a08a6cfeb Update cookbook.rst (#1440) 2024-12-05 00:49:13 -08:00
Ali MJ Al-Nasrawy 6d2d870711 Fix "Scanning contents" progress bar with --redo-ocr 2024-12-04 18:45:46 +03:00
James R. Barlow cc058be4b2 v16.7.0 release notes 2024-12-02 11:45:01 -08:00
James R. Barlow 7565d20c0a Fix Docker build and restore ubuntu 24.04 as base Docker image
Ubuntu version changed on suspicion of Ghostscript 10, but that issue is resolved.
2024-12-02 11:36:26 -08:00
James R. Barlow 9a075039b5 Remove empty test file 2024-12-02 11:23:35 -08:00
James R. Barlow 5a1c043331 Check in uv.lock and update uv version 2024-11-27 17:14:07 -08:00
James R. Barlow fe89be5dc0 Fix test broken in commit 85d6fb8c 2024-11-27 15:44:12 -08:00
James R. Barlow d70296b97a Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-11-27 00:10:32 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7d7658018d Bump codecov/codecov-action from 4 to 5 (#1433)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-27 00:04:27 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8fb8e9f72c Bump astral-sh/setup-uv from 3 to 4 (#1436)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-27 00:04:04 -08:00
James R. Barlow 85d6fb8ce9 Don't use stdout for Ghostscript
Apparently, Ghostscript simply uses temporary files when asked to write to stdout. We're already using temporary files internally, so this eliminates some redundant copies.
2024-11-21 17:02:03 -08:00
James R. Barlow 828e741c24 README add more features 2024-11-17 13:43:47 -08:00
James R. Barlow 36837f8353 Remove conda from installables list (conda's ocrmypdf is old/unmaintained) 2024-11-17 13:38:17 -08:00
James R. Barlow 12fd4f70f1 docs: fix typo 2024-11-17 13:37:31 -08:00
James R. Barlow 250615561d v16.6.2 release notes 2024-11-16 19:05:20 -08:00
James R. Barlow a659f83d67 Remove invalid hyperlink annotations to satisfy Ghostscript 10.x during PDF/A conversion
Closes #1425
2024-11-16 19:02:10 -08:00
James R. Barlow 08f95c0b13 v16.6.1 release notes 2024-11-10 02:09:10 -08:00
James R. Barlow dbd3c93757 Fix issue with unpickling HOCRResult
Fixes [Bug]: HOCRResult.from_json() not unpickling correctly #1427
2024-11-10 02:05:57 -08:00
James R. Barlow 5d128a91d2 Adjust tesseract-ocr5 package and revert to Ubuntu 22.04
22.04 has older Ghostscript which has fewer regressions.
2024-11-08 15:20:40 -08:00
James R. Barlow a1b8113d56 Add bisect script 2024-11-08 11:09:13 -08:00
James R. Barlow f052e910c9 docs: Improve batch command 2024-11-07 00:09:55 -08:00
James R. Barlow 116e2692d0 Also use stable Tesseract 5 for Docker 2024-11-01 15:47:25 -07:00
James R. Barlow b2669c7d71 Use stable tesseract ppa for build 2024-11-01 15:08:53 -07:00
James R. Barlow c8c53d38a3 Remove .git from Docker images nad fix link to alternate services 2024-11-01 10:32:29 -07:00
James R. Barlow d303b42c86 Add documentation to PdfMinerState 2024-10-27 22:01:36 -07:00
James R. Barlow f77f701a50 Fix quadratic time performance regression on scanning pages 2024-10-27 21:57:53 -07:00
James R. Barlow 1c3b7d1507 Tidy release notes 2024-10-27 19:35:53 -07:00
James R. Barlow bf62562787 Merge branch 'feature/docker-ubuntu-24' 2024-10-27 17:39:06 -07:00
James R. Barlow 6c6cbfd4d6 Make images extracted for jbig2enc optimization have unique filenames
Fixes #1396
2024-10-27 17:38:52 -07:00
James R. Barlow ee5acbe94e Repair PDF before all processing
Some PDFs choke both pdfminer.six and Ghostscript but the issues can be fixed first.

Fixes #1403
2024-10-27 17:37:46 -07:00
James R. Barlow 5e478a7774 do_get_pdfinfo: typing 2024-10-27 17:23:20 -07:00
James R. Barlow 92c5200ad2 Update release notes for 16.6.0 2024-10-27 16:56:41 -07:00
James R. Barlow 86a102f8e6 Update docker docs 2024-10-27 16:49:54 -07:00
James R. Barlow 2463b91051 Remove unneeded build steps from Alpine 2024-10-27 16:46:57 -07:00
James R. Barlow 07f7c6b812 Reinstate jbig2 building 2024-10-27 16:33:37 -07:00
James R. Barlow 8138664287 Remove Ubuntu 22.04 container 2024-10-27 16:14:08 -07:00
James R. Barlow 120ca72393 Update Ubuntu Dockerfile 2024-10-27 16:12:42 -07:00
James R. Barlow f9b3e9a97b Add Dockerfile for Ubuntu 24.04 LTS 2024-10-27 15:33:34 -07:00
James R. Barlow 1e87930bbb Use uv to construct Alpine Docker image 2024-10-27 15:32:46 -07:00
James R. Barlow fe4725658e unpaper: fix regex 2024-10-27 14:57:21 -07:00
James R. Barlow 9d042767cc Attempt to fix matrix and unpaper version error 2024-10-27 14:04:45 -07:00
James R. Barlow 23bc247b9c Improve Linux coverage matrix 2024-10-27 13:52:09 -07:00
James R. Barlow e44bf46d77 Fix img2pdf and python version 2024-10-27 13:32:06 -07:00
James R. Barlow f50620c244 Convert to uv build 2024-10-27 13:26:41 -07:00
James R. Barlow 6f755321b8 Ignore unpaper warning message when checking version
Fixes #1409
2024-10-27 13:06:30 -07:00
James R. Barlow 706681deb8 Improve some type checks 2024-10-27 12:54:29 -07:00
James R. Barlow c283cf0a0d Fix incorrect return value 2024-10-27 12:32:40 -07:00
James R. Barlow 0f82d7223e Fix some typing issues 2024-10-27 12:31:16 -07:00
James R. Barlow 9a6150ae53 Refactor get_pdfinfo common code 2024-10-27 12:09:33 -07:00
James R. Barlow fec0948a13 If inside a container, remind user that path is relative to container 2024-10-27 11:55:38 -07:00
James R. Barlow 18b59c57b4 Refactor our tests that check if we are in a container 2024-10-27 11:55:22 -07:00
Mayeul KauffmannandGitHub a67a11e61c Doc: new infix for temp files; snap temp files folder (#1404) 2024-10-26 11:18:25 -07:00
James R. Barlow 6ca4940a32 Upgrade docker alpine to latest 3.19.x and add note about 3.20.x 2024-09-15 16:43:35 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e4cce2642 Bump sigstore/gh-action-sigstore-python from 2.1.1 to 3.0.0 (#1392)
Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 2.1.1 to 3.0.0.
- [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases)
- [Changelog](https://github.com/sigstore/gh-action-sigstore-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v2.1.1...v3.0.0)

---
updated-dependencies:
- dependency-name: sigstore/gh-action-sigstore-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-05 01:46:26 -07:00
James R. Barlow 8fca0c71dc v16.5.0 release notes 2024-08-31 02:12:07 -07:00
James R. Barlow 944d99bdc1 Fix PROGRAM_NAME 2024-08-31 01:23:55 -07:00
James R. Barlow 5bb6e1c5d7 Modify GitHub release strategy to use sigstore 2024-08-31 01:17:51 -07:00
James R. Barlow 8d7a8f0f98 Update documentation to transition from setuptools to hatchling 2024-08-31 01:14:51 -07:00
James R. Barlow b9dd0a5e3c Use hatchling and hatch-vcs as build backend 2024-08-31 01:08:23 -07:00
James R. Barlow 6949ad2c5d pyproject: link changelog 2024-08-31 00:45:46 -07:00
James R. Barlow b3324c3b4e Don't assume /Mask is always a Stream
Fixes #1377
2024-08-31 00:37:52 -07:00
James R. Barlow b38cac6931 Drop wheel from build requires
Not usually needed anymore
2024-08-28 01:31:56 -07:00
Elliott Sales de AndradeandGitHub bb4c47e707 Fix broken test_rotate_page_level (#1382)
Before 42ff7fc842, `make_rotate_test`
always used `resources / 'typewriter.png'`, but after the change the
second call accidentally used just `resources`, which is a directory,
and fails to open.
2024-08-21 01:25:07 -07:00
James R. Barlow 5e1e2497ab Enable Python 3.13 experimental 2024-08-15 01:32:33 -07:00
James R. Barlow cd910fbf21 Improve "PDF/A conversion failed" message 2024-08-10 01:34:02 -07:00
James R. Barlow 1225269a4b Clarify opporutnities available with OCR_JSON_SETTINGS 2024-08-10 01:02:05 -07:00
James R. Barlow 3a75b20740 v16.4.3 release notes 2024-07-31 02:14:12 -07:00
James R. Barlow d35d008806 Increase pdfminer's bufsiz to mitigate token splitting issue
Fixes #1361
2024-07-31 02:11:47 -07:00
James R. Barlow f5662d5eb0 Consider Masks and stencil masks when calculating DPI
Fixes #1362
2024-07-29 15:46:30 -07:00
James R. Barlow 39010dd255 Handle incompatible jbig2.exe from TeX Live
Fixes #1363
2024-07-27 01:09:34 -07:00
James R. Barlow fbaad570c7 v16.4.2 release notes 2024-07-22 15:02:53 -07:00
James R. Barlow f974e3b3c1 ghostscript: change input filename order for 10.03.1
Ghostscript now expects the pdfa.ps file to precede other files. Fixes #1359.
2024-07-22 14:56:03 -07:00
James R. Barlow 46b49cc176 Suppress missing jbig2dec warning message
Windows users can't resolve it easily.
2024-07-22 14:54:40 -07:00
Johannes KalliauerandGitHub 5256e74d0c Update installation.rst "python -m venv .venv" (#1355) 2024-07-18 06:28:07 -07:00
James R. Barlow 621d6a0b89 Fix image size calculation when SMask dimensions do not match image
Closes [Bug]: Ghostscript rasterizing failed #1351
2024-07-16 13:36:48 -07:00
IrisandGitHub 08be7c8bbe update arch base-devel install command (#1354)
the '--needed' flag only installs the package if it isn't installed, otherwise it would reinstall it if already installed.
2024-07-15 13:28:36 -07:00
James R. Barlow 980a5472b6 Fix test failures due to 4dde378 2024-07-09 15:54:53 -07:00
James R. Barlow 51c618e357 Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-07-09 14:46:58 -07:00
James R. Barlow 4dde3786c2 Fix KeyError '/Subtype'
Fixes #1350
2024-07-09 14:46:47 -07:00
James Barlow d544342602 Merge branch 'main' of https://github.com/ocrmypdf/OCRmyPDF 2024-07-04 22:59:33 -07:00
James R. Barlow fac91fca2a v16.4.1 release notes 2024-06-30 00:11:27 -07:00
James R. Barlow 6edf756849 optimize: trap Hifi..Error
Fixes [Bug]: NotImplementedError: not sure how to get colorspace #1315
2024-06-30 00:08:51 -07:00
James R. Barlow 4fb1bb4de6 pipeline: fix typo in message 2024-06-30 00:08:31 -07:00
James Barlow 6a8eb7daaa docs: page seg mode 2024-06-26 01:16:31 -07:00
James R. Barlow 0544d06c3d Fix calculation of image printed area (used in finding weighted DPI for OCR)
Fixes #1334
2024-06-21 15:13:51 -07:00
James R. Barlow 34c285c9ac v16.4.0 release notes (3) 2024-06-17 14:40:22 -07:00
James R. Barlow 2f53b27651 Disable progbar for linearizing when --no-progress-bar set
Fixes #1332
2024-06-14 14:18:33 -07:00
James R. Barlow 772677746b Update issue templates to improve data collection for 3rd party apps 2024-06-13 15:25:50 -07:00
James R. Barlow f0bad87ea6 Restore choco since winget isn't supported (still) 2024-06-13 00:40:05 -07:00
James R. Barlow 44e71f8c14 Attempt to deal with jbig2dec warnings 2024-06-13 00:27:33 -07:00
James R. Barlow 964b30ca26 v16.4.0 release notes (2) 2024-06-11 16:55:33 -07:00
James R. Barlow 214a333e2d Block Tesseract 5.4.0 2024-06-11 14:44:54 -07:00
James R. Barlow ec6401ab57 Merge branch 'pr/helkaluin/1300' 2024-06-09 15:34:19 -07:00
James R. Barlow cbc5e8ce8d Revert "Delete and de-list snap because it no longer works"
This reverts commit 3a721e6578.
2024-06-09 15:32:34 -07:00
James R. Barlow a1c4cfe8f1 Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-06-08 01:37:19 -07:00
James R. Barlow 3a721e6578 Delete and de-list snap because it no longer works 2024-06-08 01:24:47 -07:00
Omid RahaandGitHub e6b716cdde Update docker.rst (#1327) 2024-06-08 01:20:51 -07:00
James R. Barlow 02c39998b8 Note that alpine is now available for arm 2024-06-08 01:20:25 -07:00
James R. Barlow 0774bc7f14 v16.4.0 release notes 2024-06-01 02:01:11 -07:00
James R. Barlow c6a98b3d0b Merge branch 'feature/alpine-arm' 2024-06-01 01:55:26 -07:00
James R. Barlow 981bbf1105 optimize: add a recursion guard to avoid chasing cyclic form xobjects 2024-06-01 01:52:21 -07:00
James R. Barlow 2b0c6cfd40 v16.4.0 release ntoes 2024-06-01 00:26:07 -07:00
James R. Barlow 59f6bc8306 More Tesseract-specific language checks to its plugin 2024-06-01 00:15:50 -07:00
James R. Barlow 653c4ffb45 hocr: accept multiple spaces in bounding boxes
Fixes #1322
2024-05-31 16:22:27 -07:00
James R. Barlow d947ca258e Prevent issuing equ and osd as languages 2024-05-25 01:17:57 -07:00
James R. Barlow d5ff7f7db9 batch: fix issues flagged by ruff 2024-05-21 01:52:57 -07:00
James R. Barlow 579cef3649 watcher: Ensure output files are .pdf 2024-05-21 01:51:30 -07:00
James R. Barlow cb2f090c60 v16.3.1 release notes 2024-05-21 01:39:30 -07:00
James R. Barlow f3d6387bca Fix "OCR" progress bar not matching actual progress 2024-05-21 01:35:14 -07:00
James R. Barlow abf9729c61 Semfree test: accept pdfa conversion failed as a valid return code
Fixes #1316
2024-05-21 01:26:11 -07:00
James R. Barlow 442e9c9f0d Add missing codecov token where missed & drop unneeded brew openssl 2024-05-19 01:07:38 -07:00
James R. Barlow 397fad249d v16.3.0 release notes 2024-05-19 00:50:59 -07:00
James R. Barlow 9a3c5a3f7c Add progressbar for metadata_fixup
Might take time for big files. Pdf.open() potentially is expensive as well, but QPDF doesn't give us progress feedback for that.

Closes Show progress during postprocessing #1313
2024-05-19 00:46:50 -07:00
James R. Barlow 950c700274 Fix Ghostscript PDF/A progressbar not displaying 2024-05-19 00:44:21 -07:00
James R. Barlow 26432c38a9 Raise exception if rotate pages threshold adjusted without --rotate-pages
Fixes Make usage of --rotate-pages-threshold clearer #1309
2024-05-18 23:49:27 -07:00
James R. Barlow 28be50136c hocr: If a line box's coords are invalid, log and error and don't render
Addresses [Bug]: Crash on multiple .pdf files #1312

Not actually a fix, but at least it will get us better diagnostics. Appears old Tesseract 4.x generates bad line boxes at times.
2024-05-18 23:32:18 -07:00
James R. Barlow 0c62f2de5d Issue template: check for EOL OS 2024-05-17 19:51:15 -07:00
James R. Barlow 5caf654f22 Add new codecov token 2024-05-11 01:03:41 -07:00
James R. Barlow 205593445e Change test to run on macos x64 and arm64 2024-05-11 00:13:08 -07:00
James R. Barlow f25fb8c63a Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-05-08 00:39:27 -07:00
James R. Barlow 99c78650b6 Add better error message for PDFs with invalid CTMs
Closes #1303
2024-05-07 14:00:30 -07:00
helkaluin 69355886a8 Fix wrong env var for GS path in Snap 2024-04-26 16:45:04 +08:00
Ahmed AbdouandGitHub 08e89e2dbe Adding language install docs for archlinux (#1296)
Adding language install docs for archlinux
2024-04-24 14:46:05 -07:00
James R. Barlow 0e013df161 v16.2.0 release notes 2024-04-16 00:37:03 -07:00
James R. Barlow 9ba4e3ab46 Log unusual exceptions when trying to obtain a version
Fixes #1262
2024-04-07 14:39:08 -07:00
James R. Barlow 5fdcb7602b Make downsampling large images that Tesseract would otherwise error on into default behavior
Fixes #1281
2024-04-07 13:43:20 -07:00
James R. Barlow b4db1b741f optimize: fix handling of [/FlateDecode none] - type images
Closes #1271
2024-04-07 01:44:08 -07:00
James R. Barlow 7a8cc21e31 Add support for sidecar output to io.BytesIO
Closes #1252
2024-04-07 01:38:55 -07:00
James R. Barlow 0674829d8f Remove tool.black config 2024-04-07 00:36:52 -07:00
James R. Barlow 315aa0474b Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-04-07 00:34:51 -07:00
Ben BeasleyandGitHub df3451e779 Update the typer[all] dependency to typer-slim[standard] (#1287)
In 0.12.1, Typer was significantly reorganized.

- `typer-slim` is the library (for `import typer`)
- `typer-slim[standard]` adds optional dependencies (currently `rich`
  and `shellingham`, basically equivalent to the old `typer[all]`)
- `typer-cli` is the `typer` command-line tool
- `typer` is now basically a metapackage that brings in *all of the
  above*, and it no longer has an `all` extra

Pip will warn about this and proceed,

```
WARNING: typer 0.12.1 does not provide the extra 'all'
```

but there are other tools that will fail hard when asked to resolve a
(now) nonexistent extra.

Since this project doesn’t need the `typer` command-line tool, it looks
like changing the dependency to `typer-slim[standard]` is the best way
forward.

See https://typer.tiangolo.com/release-notes/#0121 and
tiangolo/typer#785 for further discussion
and details.
2024-04-07 00:34:34 -07:00
akierigandGitHub 3ba42802d1 added Macports install information (#1286) 2024-04-07 00:33:57 -07:00
James R. Barlow d6342cb8c2 Add heif/heic input image support 2024-04-07 00:33:13 -07:00
James R. Barlow 065bddbc6c Reformat with ruff format 2024-04-07 00:25:32 -07:00
James R. Barlow 067f429dde Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-03-26 15:34:00 -07:00
Daniel LovegroveandGitHub 6895c2d70f Fix Broken Documentation Links (#1275)
* Update URL for PDFMARK documentation

For reference, here is a link to the old PDF:
https://web.archive.org/web/20190806035303/https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf

It appears Adobe converted the PDF into a webpage-based document, the
wording seems to almost identical b/w the PDF and the website.

* Fix cross-references to JBIG2 page

* Fix links for Fedora + Arch + HEAD revision install

Fedora 39 has been released, and the package tracker no longer includes
a release overview for Fedora 37 hence why it was removed here.
2024-03-22 14:38:52 -07:00
James R. Barlow 686481982a Fix naming of hOCR rendered files 2024-03-22 13:27:20 -07:00
James R. Barlow a9e1d19b78 v16.1.2 release notes 2024-03-20 12:56:13 -07:00
James R. Barlow f95aa63718 Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-03-20 12:26:02 -07:00
James Barlow 855de287b2 Fix test suite failure with Ghostscript >= 10.3
Ghostscript is more picky about a specific case with SMask that cannot be converted to PDF/A

Details here
https://github.com/ArtifexSoftware/ghostpdl/commit/4dcfae36bb4dcbc4ef3b5e5afc98bcde0d6b9ddc
2024-03-19 17:20:33 -07:00
NilsRoandGitHub feeb9f213f batch example: added archive, small corrections and optimizations (#1277)
* Added archive, small corrections

Added a function to archive originals and avoid calling ocrmypdf if they are still is PDF/A.

* Added Copyright
2024-03-18 13:22:24 -07:00
Emiel MolenaarandGitHub e7eb8fa805 Update Dockerfile.alpine (#1268)
Use Alpine 3.19 as base image to ensure we get GhostScript 10.2.1 to eliminate serious regressions that corrupt PDFs with existing text.
2024-03-13 14:49:42 -07:00
James R. Barlow 8a747f005a pixels -> megapixels
Fixes #1265
2024-02-29 15:31:07 -08:00
James R. Barlow 16ab4a8b4e Fix error message about missing Python exec
Message is
unable to start container process: exec: "python": executable file not found in $PATH: unknown.

Closes #1260
2024-02-21 23:54:41 -08:00
James R. Barlow 8d30cff4ef Undo future annotations from watcher.py till Typer fixes its issue
Fixes #1258
2024-02-20 19:14:39 -08:00
James R. Barlow 59d5b0d1bd v16.1.1 release notes 2024-02-15 16:56:25 -08:00
James R. Barlow 9ec0745ab8 Try pypy3.10 2024-02-14 14:25:13 -08:00
James R. Barlow 3a3635f7f9 Python 3.10 cleanup, manual fixes 2024-02-14 12:48:17 -08:00
James R. Barlow 6a746a1cbb ruff linting/Python 3.10 cleanup 2024-02-14 12:41:51 -08:00
James R. Barlow 906c130f96 Update rust toml settings 2024-02-14 12:32:26 -08:00
James R. Barlow 4a78458821 v16.1.0 release notes 2024-02-12 01:46:21 -08:00
James R. Barlow fddf3ce2f4 Clarify warnings filter 2024-02-12 01:43:47 -08:00
James R. Barlow 353b34e695 Merge branch 'feature/pageboxes' 2024-02-12 01:41:56 -08:00
James R. Barlow 7d63355c3c Use hocr renderer for LTR languages 2024-02-12 01:41:41 -08:00
James R. Barlow 42ff7fc842 Fix handling of pages that are restored to correct orientation with /Rotate
Appears inversion of CTM was incorrect, introduced in commit 9898904
2024-02-12 01:32:26 -08:00
James R. Barlow 26470fe16a Suppress reportlab deprecation warning 2024-02-12 01:17:08 -08:00
James R. Barlow 3b9d4b7f0a Attempt to deal with oddball mediaboxes 2024-02-11 15:34:54 -08:00
James R. Barlow 11f53fe9a9 First cut at propagating page boxes
This would fix the immediate issue, but does not address an offset mediabox.
2024-02-11 15:34:54 -08:00
James R. Barlow 123c0c766f Mention pipx, install --user --upgrade
Closes #1249
2024-02-08 09:42:00 -08:00
James R. Barlow 6a9be2142e Advise Homebrew on Linux for Ubuntu 20.04 2024-02-07 19:52:50 -08:00
James R. Barlow 0bc350f55e Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-02-06 01:28:10 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7a6edf62ba Bump codecov/codecov-action from 3 to 4 (#1247)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 03:55:13 -08:00
James R. Barlow 07b6f06f11 optimize: log images with unclear decode tables 2024-02-01 15:42:40 -08:00
nisbet-hubbardandGitHub 2005f622bb Update gs dependency & instructions for RHEL (#1228)
* Update gs depedency to 9.54

* Add instructions for RHEL

* Update instruction for using python3.11 on RHEL
2024-01-24 10:07:58 -08:00
James R. Barlow cca04fd799 Document ability to remove all OCR 2024-01-23 23:35:43 -08:00
MaxiandGitHub 75bf8e4ba2 Add autotools automake libtool and leptonica requirements (#1230)
These tools are required for a successful install of JBIG2 on Ubuntu
2024-01-09 11:58:17 -08:00
Anish SapkotaandGitHub daabb5b100 Minor english correction in Docs (#1229) 2024-01-08 10:18:26 -08:00
James R. Barlow 035ebea72f Change Windows build to use older Ghostscript 2024-01-07 01:56:38 -08:00
James R. Barlow a499956462 v16.0.4 release notes 2024-01-07 01:39:06 -08:00
James R. Barlow 74d2a156c4 Update cache 2024-01-07 01:35:05 -08:00
James R. Barlow f87fc7b12d gs: check for text-corrupting versions 2024-01-07 01:33:49 -08:00
James R. Barlow 602f5632cb hocr: fix unbound local 2024-01-07 01:33:36 -08:00
James R. Barlow 9fbbcf7599 Merge branch 'feature/hocrdebug' 2024-01-07 01:22:54 -08:00
James R. Barlow 9498f01f59 hocr: Pass text direction for standalone words 2024-01-06 16:21:21 -08:00
James R. Barlow 2c59aca5a1 Fix render_mode ON_TOP when previous content stream does not restore CTM 2024-01-06 16:20:55 -08:00
James R. Barlow 51301d69c9 Change default graft render mode back to underneath
It was mistakenly changed to on top at some point
2024-01-06 16:20:30 -08:00
James R. Barlow 7e608fd1df Use Courier as debug font 2024-01-06 14:24:25 -08:00
James R. Barlow ecc79315df Adjust issue template files description 2024-01-04 00:27:06 -08:00
James R. Barlow 14365d10b8 Skip testing oom killer on Python 3.12
Need to investigate further if there's a safe way to do this test.
2024-01-02 16:28:22 -08:00
James R. Barlow 5e5320020f v16.0.3 release notes 2024-01-02 02:49:54 -08:00
James R. Barlow 103c3e0cd6 Drop Ghostscript CVE warning and adjust version down to 9.54
Most distros have probably now backported the CVE fix. Dropped version to 9.54 so RHEL 9 can use recent ocrmypdf.
2024-01-01 13:02:57 -08:00
Anthony NabilandGitHub 7a1c89edd9 fixed a spelling mistake (#1222) 2023-12-30 15:53:22 -08:00
James R. Barlow a5ff3d2f42 Update instructions about custom language packages
Closes #1218
2023-12-27 16:46:10 -08:00
James R. Barlow b71d16dd96 Draw RTL text with reversed matrix instead of reversing characters 2023-12-24 01:34:03 -08:00
James R. Barlow fd593eb5e9 Reversing character order for RTL helps output 2023-12-24 01:24:44 -08:00
James R. Barlow a0b98abb94 Improve and externalize debug rendering settings 2023-12-24 00:10:09 -08:00
James R. Barlow 18353e1e94 Also fix space rendering 2023-12-24 00:09:47 -08:00
James R. Barlow 9adcad84da Overhaul debug rendering to support possible use of Helvetica for testing 2023-12-23 23:47:35 -08:00
James R. Barlow f2714586d8 Fix upside down glyphless font 2023-12-23 23:44:37 -08:00
James R. Barlow 0b6fb62967 Update issue template 2023-12-23 16:21:17 -08:00
James R. Barlow 1db8b0b943 Temporarily revert default renderer to sandwich while investigating regressions in macOS Preview 2023-12-23 16:19:44 -08:00
James R. Barlow f38aebb3d5 Update issue template 2023-12-23 15:46:43 -08:00
James R. Barlow 7162c36d37 Name artifacts explicitly 2023-12-20 13:53:31 -08:00
James R. Barlow f4d4ea46c8 Update artifact actions 2023-12-20 12:44:43 -08:00
James R. Barlow 2fd1a0f178 v16.0.1 release notes 2023-12-20 12:33:41 -08:00
James R. Barlow 73ed33a086 Tighten dependencies 2023-12-20 12:33:18 -08:00
James R. Barlow e6095a9949 Fix text rendering issue with new hOCR text renderer 2023-12-20 12:26:06 -08:00
James R. Barlow 16f05af401 Fix release notes - drop rc from version 2023-12-18 20:08:45 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1631afc878 Bump actions/setup-python from 4 to 5 (#1205)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-17 15:35:13 -08:00
Robin RichtsfeldandGitHub 63d87fc440 Fix --fast-web-view documentation (#1206) 2023-12-17 14:54:38 -08:00
James R. Barlow 9489c01259 Skip test_encrypted on Py3.12 + macOS 2023-12-08 00:12:24 -08:00
James R. Barlow 30d92ad83f Fix build settings to adjust for dropping py39 2023-12-07 23:40:45 -08:00
James R. Barlow a4987733c4 Filter rl_safe_eval deprecation warning
Full message
eportlab/lib/rl_safe_eval.py:11: DeprecationWarning: ast.NameConstant is deprecated and will be removed in Python 3.14; use ast.Constant instead
    haveNameConstant = hasattr(ast,'NameConstant')

Warning triggered by reportlab-4.0.7 and Python 3.12
2023-12-07 23:40:23 -08:00
James R. Barlow 39eee05230 v16.0.0rc1 release notes
Fixes #1009, #1191, #1157
2023-12-03 15:44:34 -08:00
James R. Barlow 5b2f2e6290 Merge branch 'feature/modernhocr' 2023-12-03 15:17:02 -08:00
James R. Barlow 445617a1a5 Rebuild cache for hocr default case 2023-12-03 15:16:18 -08:00
James R. Barlow f6e90a5934 hOCR renderer is now default 2023-12-02 19:58:00 -08:00
James R. Barlow 43618e6b3f Move canvas API to pikepdf and import it 2023-12-02 19:42:35 -08:00
James R. Barlow e97f89de3b Refactor font so glyphless isn't as hard coded 2023-12-02 08:55:01 -08:00
James R. Barlow 11d3e32f1e Fix hocrtransform CLI 2023-12-02 08:08:29 -08:00
James R. Barlow 2affa83efe Remove code that attempted to manage xattrs out of output file
Feature requested in issue #1179, but caused #1195. On further review,
there is no platform independent way to manage extended attributes
and it is not clear copying them through is necessarily the sensible
thing to do.

Closes #1179.
2023-11-29 23:25:51 -08:00
James R. Barlow c90d5cd84b Fix Ghostscript installation instructions and add warning 2023-11-29 14:10:04 -08:00
James R. Barlow aacaba3d26 Ignore pypy for now 2023-11-21 01:05:23 -08:00
James R. Barlow fec53be841 Remove next major release deprecations 2023-11-21 00:47:51 -08:00
James R. Barlow 3f7b540f76 Drop Python 3.9 support 2023-11-21 00:46:00 -08:00
James R. Barlow d217856166 Make hocrdebug work, and try to handle CJK spacing better 2023-11-21 00:33:02 -08:00
James R. Barlow e2be457e9b Avoid divzero 2023-11-20 23:08:00 -08:00
James R. Barlow 4850f486d2 Make text API more like an accessor 2023-11-20 22:59:50 -08:00
James R. Barlow 729c7febd9 Fix placement of spaces in debug mode 2023-11-20 22:44:12 -08:00
James R. Barlow 6c6aca2f1e Refactor save_state 2023-11-20 22:29:21 -08:00
James R. Barlow c69823f496 Refactor; accumulate content stream as bytes rather than discrete pikepdf objects 2023-11-20 22:11:59 -08:00
James R. Barlow 73f8f6aac8 Add RTL output - seems to work, but debug does not 2023-11-20 20:28:07 -08:00
James R. Barlow d944254e45 hocr: typing cont'd 2023-11-20 17:07:52 -08:00
James R. Barlow f7ddffe554 hocr: typing 2023-11-20 16:52:55 -08:00
James R. Barlow 8a73ed5d5a Fix JBIG2 not updating progress bar 2023-11-20 16:25:30 -08:00
James R. Barlow 03669183d7 Rationalize canvas interface 2023-11-20 15:54:13 -08:00
James R. Barlow 74e101a2fa Improve canvas interface with chaining 2023-11-20 14:42:48 -08:00
James R. Barlow 532cf18ad3 Restructure hocrtransform submodule to avoid having everything in __init__ 2023-11-20 00:57:58 -08:00
James R. Barlow 0b90b697e2 More tidying 2023-11-20 00:43:43 -08:00
James R. Barlow 6be7c5f7c8 Fix colors and space box rendering 2023-11-20 00:30:54 -08:00
James R. Barlow db2e5132e6 Remove some obsolete parameters 2023-11-20 00:10:55 -08:00
James R. Barlow b14f6f778a Tidying new hOCR renderer 2023-11-19 23:51:27 -08:00
James R. Barlow 415de77457 imageops: fix annots since not using singledispatch anymore 2023-11-19 23:51:27 -08:00
James R. Barlow a9466c4f58 Improve word box positioning 2023-11-19 23:51:27 -08:00
James R. Barlow d9ae453a63 Significantly improvement overall 2023-11-19 23:51:27 -08:00
James R. Barlow 9841e09233 More adjustments 2023-11-19 23:51:27 -08:00
James R. Barlow 0ca314e066 Replace Rect with pikepdf.Rectangle, migrate line matrix to page 2023-11-19 23:51:27 -08:00
James R. Barlow d7680cae27 Correcting Matrix logic helps
The good: don't have to do inverse and intermediate transforms.

The bad: skew looks bad, partly because the hOCR coordinate system is inconsistent around skew?
2023-11-19 23:51:27 -08:00
James R. Barlow 491b6bdb1f Remove concept of HOCR_OK_LANGS 2023-11-19 23:51:27 -08:00
James R. Barlow c591f9601a Remove Latin hOCR test 2023-11-19 23:51:27 -08:00
James R. Barlow 8d1e75017e Remote reportlab backend and make reportlab a test-only dependency 2023-11-19 23:51:27 -08:00
James R. Barlow 94615f7ad4 hOCR now works for all languages 2023-11-19 23:51:27 -08:00
James R. Barlow e5df8e1315 Nearly pixel perfect 2023-11-19 23:51:27 -08:00
James R. Barlow d739b91aef Tidy up 2023-11-19 23:51:27 -08:00
James R. Barlow 686cfb2539 Add rendering of space between boxes 2023-11-19 23:51:27 -08:00
James R. Barlow 2633716bb7 Render interword spaces separately and avoid box overlap 2023-11-19 23:51:27 -08:00
James R. Barlow 0a07c0a44e Fix more things 2023-11-19 23:51:27 -08:00
James R. Barlow 2ca6e110ca Fix private accessors, rename pdf to canvas 2023-11-19 23:51:27 -08:00
James R. Barlow 334a07c839 Refactor debug printing 2023-11-19 23:51:27 -08:00
James R. Barlow a57c39358d Refactor: extract methods 2023-11-19 23:51:27 -08:00
James R. Barlow 30a0c315fb Further exploratory improvements 2023-11-19 23:51:27 -08:00
James R. Barlow b860f0d94c Make coordinate system more consistent 2023-11-19 23:51:27 -08:00
James R. Barlow 14f4c19f5a WIP improve text positioning (not there yet) 2023-11-19 23:51:27 -08:00
James R. Barlow 7ab5c55d46 More colors 2023-11-19 23:51:27 -08:00
James R. Barlow 8b6ecd5971 Fix line and rect drawing 2023-11-19 23:51:27 -08:00
James R. Barlow 7b0871ae4c Fix position errors; ignore non-glyphless font 2023-11-19 23:51:27 -08:00
James R. Barlow b73af7ce10 Fix dashes 2023-11-19 23:51:26 -08:00
James R. Barlow 60645717e2 Test pikepdf canvas - renders... something at this point 2023-11-19 23:51:26 -08:00
James R. Barlow 1cbf578538 Fix import of pdf.ttf 2023-11-19 23:51:26 -08:00
James R. Barlow e966c1fceb Add pdf.ttf 2023-11-19 23:51:26 -08:00
James R. Barlow d0133f8641 Create pikepdf backend renderer 2023-11-19 23:51:26 -08:00
James R. Barlow 6d30b497dc Refactor reportlab into generic backend 2023-11-19 23:51:26 -08:00
James R. Barlow f3b89e66eb Refactor reportlab backend out of hocrtransform 2023-11-19 23:51:26 -08:00
James R. Barlow 04154e207c hocrtransform: move to module 2023-11-19 23:51:26 -08:00
James R. Barlow 9898904be7 Fix pikepdf PdfMatrix deprecation warning; v15.4.3 release notes 2023-11-15 20:27:16 -08:00
James R. Barlow 27d5229842 Make logger names unique 2023-11-09 23:03:39 -08:00
James R. Barlow 4a9a575ef0 ghostscript: better comments 2023-11-09 22:39:49 -08:00
James R. Barlow 52fd9a630d v15.4.2 release notes 2023-11-09 22:35:51 -08:00
James R. Barlow a596ccf844 Raise exception if resulting PDF might appear blank in a known in some PDF viewers
Fixes #1187
2023-11-09 22:33:22 -08:00
James R. Barlow e7fa97731f ghostscript duplicate filter: filter within a window of previous messages 2023-11-09 22:32:39 -08:00
James R. Barlow 290aa28108 Fix error on attempt to write to debug log after removing debug log handler 2023-11-09 16:02:41 -08:00
James R. Barlow a95640ed9e v15.4.1 release notes
Closes #1185
Closes #1183
2023-11-07 23:57:30 -08:00
James R. Barlow f69267bb67 watcher: restore ability to read json from file or command line string 2023-11-07 18:05:29 -08:00
James R. Barlow e36d5a309f Make grafting a little bit more configurable 2023-11-05 14:01:01 -08:00
James R. Barlow 55566d9830 Fix watcher.py kwarg error 2023-11-05 13:58:24 -08:00
James R. Barlow f02ea20678 docs: plugin documentation missing key special members 2023-11-05 00:10:51 -07:00
James R. Barlow 372c22d42b docs: improve 2023-11-04 02:43:08 -07:00
James R. Barlow 949265bbd0 graft: improve typing and remove procset tracking
ProcSet is optional and deprecated in PDF 2.0, and does little anyway; so
we removed it.
2023-11-04 02:32:39 -07:00
James R. Barlow 916106733c Skip semfree unless on Linux 2023-10-30 00:33:21 -07:00
James R. Barlow 44bcafd3aa Add missing file header 2023-10-30 00:28:09 -07:00
James R. Barlow 71166f7be8 Make hocr API experimental for now
This commit can be reverted when we are ready to release a new version.
2023-10-30 00:07:10 -07:00
James R. Barlow 580252a1a0 Merge branch 'feature/gscan2pdf'
Reconcile release notes and copy_final() with new pipeline.
2023-10-30 00:01:28 -07:00
James R. Barlow c0b60dae6a build: add repository -y 2023-10-29 23:41:36 -07:00
James R. Barlow ae123fd209 Try to retain/copy xattrs 2023-10-28 01:42:06 -07:00
James R. Barlow 454ad0acc5 build/macos: add openssl 2023-10-28 01:41:28 -07:00
James R. Barlow 0c306ac328 v15.3.1 release ntoes 2023-10-28 00:49:37 -07:00
James R. Barlow 52d99732b1 Fix mistakes with watcher loglevel handling 2023-10-28 00:47:40 -07:00
James R. Barlow 5b5827983b Tweak documentation of --output-type 2023-10-26 23:57:08 -07:00
James R. Barlow 56f9bc311d Improve verbosity of colorspace selection 2023-10-25 00:38:56 -07:00
James R. Barlow eb17dc1ecf Fix pdf save settings at metadata_fixup 2023-10-25 00:13:17 -07:00
James R. Barlow 6f8115a052 Fix import of metadata_fixup 2023-10-25 00:06:26 -07:00
James R. Barlow aac913c666 tesseract: EAFP 2023-10-24 13:50:04 -07:00
James R. Barlow b5e73ac4e4 Drop check for obsolete .dockerinit file 2023-10-24 13:49:46 -07:00
James R. Barlow 9e98c90891 docs: note on docker performance 2023-10-24 13:34:26 -07:00
James R. Barlow ca2592c1d9 Update draft release notes 2023-10-24 00:56:00 -07:00
James R. Barlow a31f17bb9d Update comments and make worker functions private 2023-10-24 00:56:00 -07:00
James R. Barlow 1cb46afa94 Update pluginspec docs 2023-10-24 00:56:00 -07:00
James R. Barlow 5a759947dd Update release notes so far 2023-10-24 00:56:00 -07:00
James R. Barlow db3df13e95 Remove ocrmypdf._sync 2023-10-24 00:54:31 -07:00
James R. Barlow 2a8bc03167 optimize: typing 2023-10-24 00:54:31 -07:00
James R. Barlow d2297b39d0 info: clarify ICC -> components checking 2023-10-24 00:54:31 -07:00
James R. Barlow e4cd081d4d info: clarify pageinfo context management 2023-10-24 00:54:31 -07:00
James R. Barlow d2dbea6cf8 Reorganize progress bars so they can be typed properly 2023-10-24 00:54:31 -07:00
James R. Barlow 46a279a49a Improve passing of arguments to workers
The executor system was built around passing only a single
argument to workers, which was
always PageContext. For other tasks, all actual arguments were packed in
a tuple, which meant we needed intermediate functions to unpack the
tuple.

The situation is now rationlized and resembles how Python handles
argument passing to familiar multiprocessing tools.
2023-10-24 00:54:31 -07:00
James R. Barlow 299f0c4003 Update dep5 2023-10-24 00:54:31 -07:00
James R. Barlow 9ffb45f283 Remove public domain congress.jpg and replace with baiona_color.jpg
For reuse compliance we are phasing out public domain licenses
2023-10-24 00:54:31 -07:00
James R. Barlow cd61c4efd9 pngquant: remove unused ability to quantize a non-PNG
Covering testing showed this branch was never used, and when tested it didn't work.
2023-10-24 00:54:31 -07:00
James R. Barlow a06ab2a1c5 unpaper: Remove format conversion
Code is no longer reachable since we rasterize a 1/L/RGB image prior to this point.
2023-10-24 00:54:31 -07:00
James R. Barlow dfa4ebf1a6 Simplify function signature of extract_image_filter 2023-10-24 00:54:31 -07:00
James R. Barlow 58f388c69d optimize: better coverage 2023-10-24 00:54:31 -07:00
James R. Barlow 990b462a94 Fix coverage settings and cover semfree 2023-10-24 00:54:31 -07:00
James R. Barlow b928dc0808 Skip fewer tests 2023-10-24 00:54:31 -07:00
James R. Barlow 8916955f45 Convert many run_ocrmypdf -> run_ocrmypdf_api 2023-10-24 00:54:31 -07:00
James R. Barlow 82bef40aa6 Eliminate more run_ocrmypdf calls 2023-10-24 00:54:31 -07:00
James R. Barlow 1c45f32941 tests: replace many run_ocrmypdf -> run_ocrmypdf_api 2023-10-24 00:54:31 -07:00
James R. Barlow fadc0cf69b Replace cryptic test error messages with more informative ones 2023-10-24 00:54:31 -07:00
James R. Barlow 7ce9d08b2d Define progress bar plugins formally instead of "tqdm-like" 2023-10-24 00:54:31 -07:00
James R. Barlow eb3a51e33a Prefer pikepdf's newer Page.mediabox accessor over .MediaBox 2023-10-24 00:54:31 -07:00
James R. Barlow f3dd733773 optimize: explore page container as objects instead of page helpers 2023-10-24 00:54:31 -07:00
James R. Barlow 4dbc5e1dba Fix some typing issues 2023-10-24 00:54:31 -07:00
James R. Barlow c0637c287e vscode isn't ready for black py312, revert 2023-10-24 00:54:31 -07:00
James R. Barlow 6127f7abd6 tqdm_kwargs to progress_kwargs 2023-10-24 00:54:31 -07:00
James R. Barlow a4059762e6 Fix hocrtransform test to generate blank hocr 2023-10-24 00:54:31 -07:00
James R. Barlow 40afcd68a7 pluginspec: spacing 2023-10-24 00:54:31 -07:00
James R. Barlow f238e721ed Improve documentation of new public hOCR APIs 2023-10-24 00:54:31 -07:00
James R. Barlow 16eb5627a7 Fix unused imports and other trivia 2023-10-24 00:54:31 -07:00
James R. Barlow fbf0674189 hocr_to_ocr_pdf: handle missing hocr json file 2023-10-24 00:54:31 -07:00
James R. Barlow 62c4f65fc3 Remove duplicate thread local storage of page numbers 2023-10-24 00:54:31 -07:00
James R. Barlow e400112f32 Improve ._pipelines naming 2023-10-24 00:54:31 -07:00
James R. Barlow 7935914f55 Use empty .hocr file instead of dummy template for symmetry with sandwich 2023-10-24 00:54:31 -07:00
James R. Barlow ad3a1dbbad deps: update PyMuPDF req 2023-10-24 00:54:31 -07:00
James R. Barlow 0655f8e7ae Add py312 to black coverage 2023-10-24 00:54:30 -07:00
James R. Barlow 04a9372584 docs: some copyediting 2023-10-24 00:54:30 -07:00
James R. Barlow b9646b6f85 Enable multiprocessing freeze_support (for Windows) and enable forkserver
Since we use a thread for logging among other possibilities, we were
never able to use threads safely when forking. forkserver will do the job.
2023-10-24 00:54:30 -07:00
James R. Barlow 53c953a561 Fix use_threads logic for get_pdfinfo
Some debug code was level in place that forced pdfinfo to run with only
one worker when --use-threads was issued. That is how it ought to be,
since threaded pdfinfo workers just fight over the GIL and there is no
sense in parallelizing them.

Also, the user's --use-threads or --no-use-threads would be ignored in
the case of pdfinfo. By setting max_workers=1 we disabled worker processes.

This fixes how that decision is made (putting it in the relevant code, which
knows its constraints) and allows the user to influence the
thread/process decision again.
2023-10-24 00:54:30 -07:00
James R. Barlow c278fecb34 Rename post_process -> postprocess
For consistency with preprocess
2023-10-24 00:54:30 -07:00
James R. Barlow 23951c9e38 Working HOCR folder to PDF converter 2023-10-24 00:54:30 -07:00
James R. Barlow e8ae370ceb Eliminate api= kwarg and implicit creation of pluginmanager 2023-10-24 00:54:30 -07:00
James R. Barlow 67be4d1904 Refactor CLI exception handling 2023-10-24 00:54:30 -07:00
James R. Barlow 6f82097d14 Refactor setup_pipeline to decouple manage_work_folder 2023-10-24 00:54:30 -07:00
James R. Barlow fc6f959d21 Refactor debug log and work folder context cleanup 2023-10-24 00:54:30 -07:00
James R. Barlow e38d569d8f languages: kwargs are overkill 2023-10-24 00:54:30 -07:00
James R. Barlow 0856750ee2 Fix exit code error on Ghostscript failure 2023-10-24 00:54:30 -07:00
James R. Barlow 05721ba84a Fix error on no languages available 2023-10-24 00:54:30 -07:00
James R. Barlow 38c3422e5e Automatically set document language to OCR language 2023-10-24 00:54:30 -07:00
James R. Barlow d153a6f6df Refactor metadata handling 2023-10-24 00:54:30 -07:00
James R. Barlow 1a7738a925 Refactor -migrate metadata repair to new module 2023-10-24 00:54:30 -07:00
James R. Barlow 8985c0dfe9 Refactor setup_pipeline to context manager 2023-10-24 00:52:31 -07:00
James R. Barlow ebfe008432 Refactor logging record thread local storage 2023-10-24 00:52:31 -07:00
James R. Barlow 1f16eb6f50 Refactor main pipeline into discrete pipelines
- test_ghostscript_pdfa_failure fails
- haven't dealt with logging record factories

Further refactoring and duplicate elimination
2023-10-24 00:52:31 -07:00
James R. Barlow cbb0868ae3 Add hocr to ocr pdf pipeline 2023-10-24 00:52:31 -07:00
James R. Barlow 68bb38d0ad pdf_to_hocr: improve plugin handling 2023-10-24 00:52:31 -07:00
James R. Barlow 0443e87345 Introduce pdf_to_hocr API 2023-10-24 00:52:31 -07:00
James R. Barlow b3de5833d3 Refactor conversion of ocrmypdf.ocr() arguments to cmdline 2023-10-24 00:52:31 -07:00
James R. Barlow 95b14ee282 Refactor lossless reconstruction setter into separate function
Still messy but good enough as a start.
2023-10-24 00:52:31 -07:00
James R. Barlow 07b89e6a19 Plugin manager: set reasonable default when called without params 2023-10-24 00:52:31 -07:00
James R. Barlow 8991d2cb33 Refactor main pipeline and start hocr pipeline 2023-10-24 00:52:31 -07:00
James R. Barlow 86a20c4130 Refactor exec_page_sync to outputs 2023-10-24 00:52:31 -07:00
James R. Barlow 6827a6efe8 Refactor exec_page_sync -> extract _process_page 2023-10-24 00:52:31 -07:00
James R. Barlow c6b5332699 Test on release py312 not py312-rc 2023-10-21 15:17:07 -07:00
Michael FlaggandGitHub 68610046c6 Correct the archive dir name in Watched folders with Docker (#1173) 2023-10-21 14:59:33 -07:00
James R. Barlow 880326868d v15.3.0 release notes 2023-10-21 14:59:09 -07:00
James R. Barlow c6be3ba076 watcher: Improve parameter validation 2023-10-20 20:11:00 -07:00
James R. Barlow 0565cb0b10 misc/watcher.py: use Typer and dotenv to improve ease of use 2023-10-20 19:56:39 -07:00
James R. Barlow dc49906704 Improve wait_for_file_ready loop 2023-10-20 19:55:50 -07:00
James R. Barlow 93fda0dd00 Detect and warn about Tagged PDFs 2023-10-12 01:03:09 -07:00
James R. Barlow d4110e78cb Fix "pikepdf mmap disabled" spam on macOS 2023-10-11 01:24:29 -07:00
James R. Barlow 5285d68fcc Add check for filenotfound due to being inside a snap 2023-10-11 01:02:50 -07:00
James R. Barlow 2b0e149809 Update docker document and v15.2.0 release notes 2023-10-10 01:25:26 -07:00
James R. Barlow b7ce5b0d7d Don't build docker alpine aarch64 for now and add alias tag for Ubuntu 2023-10-09 17:36:08 -07:00
James R. Barlow ffd6a64ce9 Attempt at alpine on arm64 - not working
pip attempts to download musllinux_1_1 wheels and can't find pikepdf's which is only 1_2. Not clear how to fix.
2023-10-09 17:34:28 -07:00
James R. Barlow 5727f1e081 Test docker alpine build and ubuntu aliasing 2023-10-09 15:08:38 -07:00
James R. Barlow b75a7eca2a Alpine image: Fix missing OSD and path 2023-10-09 14:59:56 -07:00
James R. Barlow 2b01676434 docker docs: remove VirtualBox content and update a few explanations 2023-10-09 14:57:55 -07:00
James R. Barlow e11c386c58 Add Alpine dockerfile 2023-10-09 14:35:52 -07:00
James R. Barlow 9346d1f970 Simplify deep nested with-block 2023-10-09 02:30:30 -07:00
James R. Barlow 012cbef865 v15.1.0 release notes 2023-10-04 00:16:22 -07:00
James R. Barlow 0687568e1b sync: update documentation 2023-10-04 00:05:27 -07:00
James R. Barlow 3086cfc3d9 pipeline: documentation and tweak merge_sidecars 2023-10-04 00:05:19 -07:00
James R. Barlow 91a14660b3 Require Pillow >= 10.0.1 and drop shims for older versions 2023-10-04 00:04:28 -07:00
James R. Barlow 539f0ee0ce Document some missing CLI options to API 2023-10-03 23:54:24 -07:00
James R. Barlow 7172817cd6 Replace ExitStack with contextmanager 2023-10-02 16:52:06 -07:00
James R. Barlow d9cc759142 Fix typos in release notes 2023-10-02 02:49:01 -07:00
James R. Barlow 364799fc3e Fix typing for pdinfo.layout and modernize
Also changed handling of undefined chars to be more Liskov-consistent with parent class.
2023-10-01 01:18:22 -07:00
James R. Barlow f4c211fa2d Use Python 3.9-style type hinting for tuple[] and AbstractSet -> Set 2023-10-01 00:09:05 -07:00
James R. Barlow 113a6b45bd ruff autofixes (mostly typing.* -> collections.abc.*) 2023-10-01 00:02:53 -07:00
James R. Barlow e9419d2c40 Update notes on concurrency 2023-09-30 17:05:58 -07:00
James R. Barlow fb006ef39f v15.0.2 release notes 2023-09-29 00:56:09 -07:00
James R. Barlow 890b994403 docs: update to discuss some v15 features not yet documented 2023-09-27 00:40:05 -07:00
James R. Barlow 01bbf7d144 docs: mention new features in v15, fix 32-bit text again 2023-09-27 00:14:19 -07:00
James R. Barlow 468de5324a Add Python 3.12 to test matrix 2023-09-26 20:20:33 -07:00
James R. Barlow 072db75fa3 v15.0.1 release notes (again) 2023-09-26 20:09:04 -07:00
James R. Barlow 8519b3f625 docs: clarify situation around 32-bit support
Maintainers of ARM 32-bit in particular don't necessarily need to drop support....
2023-09-26 20:06:33 -07:00
James R. Barlow dd7c4f3eaa docs: update install notes for some things missed with v15 release 2023-09-26 20:04:30 -07:00
James R. Barlow c8e6f20f8d Change 32-bit message from error to warning 2023-09-26 20:03:06 -07:00
James R. Barlow 10530a8698 Change Ghostscript version skip to fail
Reported to fail on earlier versions than the check tested for.
2023-09-26 20:02:52 -07:00
James R. Barlow 207866abf5 v15.0.1 release notes 2023-09-26 15:56:53 -07:00
James R. Barlow 3829af16fb Fix bdist_wheel tag set to py38 2023-09-26 15:46:47 -07:00
James R. Barlow 24db31b4c5 Innocuous change to bump tag 2023-09-26 12:25:02 -07:00
James R. Barlow 8132a4ae10 Update release notes and files 2023-09-26 00:37:04 -07:00
James R. Barlow d5128c5cf5 Further improvements to image DPI calculation 2023-09-26 00:28:54 -07:00
James R. Barlow 270e31fa67 Fix pluginmanager typing 2023-09-25 14:42:48 -07:00
James R. Barlow 85e31d0a19 Tidy imports and line length 2023-09-25 01:04:01 -07:00
James R. Barlow ea36aedb5f Overhaul version checkers to prefer Version to str 2023-09-25 00:59:44 -07:00
James R. Barlow bd4d44e182 Remove Python 3.8 shim for missing str.removeprefix 2023-09-25 00:31:48 -07:00
James R. Barlow 8fcf358934 Rename pike local variable to pdf for consistency 2023-09-25 00:22:26 -07:00
James R. Barlow 47b0f28564 Minor documentation and typing fixes 2023-09-25 00:19:48 -07:00
James R. Barlow 7018e2b247 Refactor ghostscript error message deduplicating 2023-09-24 20:22:04 -07:00
James R. Barlow 8d12ecb798 Refactor docinfo repair code 2023-09-24 20:21:26 -07:00
James R. Barlow 0ab29ec0ba Update release notes 2023-09-24 01:30:12 -07:00
James R. Barlow 179714770a Update release ntoes 2023-09-24 01:24:00 -07:00
James R. Barlow f04f45545c ghostscript: fix missing type annotation 2023-09-24 01:19:56 -07:00
James R. Barlow a3a083c125 logging: Avoid possible multiplication by None 2023-09-23 15:08:55 -07:00
James R. Barlow d855f63985 Remove single dispatch version of calculate_downsample 2023-09-23 15:05:04 -07:00
James R. Barlow d4863cbf0f helpers: fix typing 2023-09-23 14:59:11 -07:00
James R. Barlow 7b8f081fbf Enable pikepdf mmap again 2023-09-23 14:50:56 -07:00
James R. Barlow 7d33039bcd Add comment re: reportlab shim 2023-09-21 00:27:45 -07:00
James R. Barlow fde886baf4 Revert "Drop reportlab warning shim"
This reverts commit 162a47f98e.
2023-09-21 00:26:50 -07:00
James R. Barlow 146da79c00 Regenerate test cache 2023-09-21 00:24:55 -07:00
James R. Barlow 2fc3b0d973 Move issue*.pdf into separate folder 2023-09-21 00:19:13 -07:00
James R. Barlow 5667424530 Lower pngquant version req 2023-09-21 00:13:03 -07:00
James R. Barlow 8add531ffd Drop Ubuntu 20.04 from build matrix 2023-09-21 00:12:07 -07:00
James R. Barlow 0388c23ae7 Merge branch 'feature/jbig2thresh' into v15 2023-09-21 00:07:05 -07:00
James R. Barlow 9b77daae7c Merge branch 'feature/snap22' into v15 2023-09-21 00:06:14 -07:00
James R. Barlow 3e1b3ec98d Merge branch 'feature/fix-raster-dpi-too-high' into v15 2023-09-21 00:05:56 -07:00
James R. Barlow 0f0ca6f517 Draft v15 release notes 2023-09-21 00:05:39 -07:00
James R. Barlow c93349c350 Convert workflow to trusted PyPI publisher 2023-09-21 00:05:39 -07:00
James R. Barlow 0c287929c2 Don't use really old Python for deliverable building 2023-09-21 00:05:39 -07:00
James R. Barlow 23a37fc35c Document that Ubuntu 22.04 and 20.04 both use Tesseract 4.1.1 2023-09-21 00:05:39 -07:00
James R. Barlow 162a47f98e Drop reportlab warning shim 2023-09-21 00:05:39 -07:00
James R. Barlow 0239f69912 Tigthen Python dependencies 2023-09-21 00:05:39 -07:00
James R. Barlow 2ad8961d0b Require pngquant 2.13.1 or newer 2023-09-21 00:05:39 -07:00
James R. Barlow eec8a2b574 Change minimum Ghostscript version to 9.55 2023-09-21 00:05:39 -07:00
James R. Barlow 6c78076bea Drop support for gswin32c / 32-bit Ghostscript for Windows 2023-09-21 00:05:39 -07:00
James R. Barlow 2637e84691 Complain about all 32-bit interpreters, not just windows 2023-09-21 00:05:39 -07:00
James R. Barlow e8c82ee4b6 Remove shim for img2pdf < 0.4.4 2023-09-21 00:05:39 -07:00
James R. Barlow de2bb5ce8c Remove tqdm dependency and TqdmConsole
Might be too aggressive? No deprecation warning....
2023-09-21 00:05:39 -07:00
James R. Barlow ec1c377532 Drop Python 3.8 too
(cherry picked from commit a8cdc5a191bdf1fe401e96c2dc9a5fba39b90d79)
2023-09-21 00:05:39 -07:00
James R. Barlow 173428e81a Added weighted DPI rendering
To address #1010 and other issues.
2023-09-21 00:04:50 -07:00
James R. Barlow 67ed29dcea Fix incorrect printed_area calculation 2023-09-21 00:02:02 -07:00
James R. Barlow 3454c050ed Introduce Resolution.take_min 2023-09-21 00:01:42 -07:00
James R. Barlow 5ee99b26e7 Update snap to use core22 2023-09-20 17:01:36 -07:00
James R. Barlow ac3aa67d8a Revise to PageResolutionProfile 2023-09-20 16:58:59 -07:00
James R. Barlow 1768a1eda9 Improve documentation for large image downsampling 2023-09-20 16:58:59 -07:00
James R. Barlow 5902fe45c1 Rename Resolution.mean to to_scalar 2023-09-20 16:58:59 -07:00
James R. Barlow 78981641f0 Introduce new functions to calculate DPI exceptions 2023-09-20 16:58:59 -07:00
James R. Barlow c77ae4b34c Change hookspec to migration parameters for generate_pdf to options object
Breaking change for PDF rendering plugins (although none are known to exist).
This provides better separation of Ghostscript specific concerns from
the generic plugin interface.
2023-09-20 15:40:18 -07:00
James R. Barlow b2cbbf0099 Migrate --pdfa-image-compression argument to being managed by Ghostscript plugin 2023-09-20 15:20:42 -07:00
James R. Barlow 6b6c34af01 Fix test artifact generated incorrectly
Test still passed either way
2023-09-20 15:20:13 -07:00
James R. Barlow be12f7a728 Make fish completion a bit smarter 2023-09-20 14:45:22 -07:00
James R. Barlow e3c813fc67 Added support for changing color conversion strategy 2023-09-20 01:08:15 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
35a1eaf62a Bump docker/setup-qemu-action from 2 to 3 (#1151)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 12:06:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d393d18c13 Bump docker/login-action from 2 to 3 (#1152)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 12:06:30 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
54e622ad10 Bump docker/setup-buildx-action from 2 to 3 (#1153)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 12:06:19 -07:00
James R. Barlow 330352aeed Update completions for jbig2 threshold 2023-09-17 14:47:46 -07:00
James R. Barlow ac2fc49208 Add support for adjusting jbig2 threshold 2023-09-17 14:44:40 -07:00
Srikar SundaramandGitHub 4bee7355e9 Change skip-ocr to skip-text (#1146) 2023-09-14 17:22:34 -07:00
dependabot[bot]andGitHub 86f2b1f9a7 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:37:22 +00:00
James R. Barlow 3002409e49 Remove older installation caveats 2023-08-22 01:25:57 -07:00
James R. Barlow 0cf6828c20 Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2023-08-19 13:05:06 -07:00
James R. Barlow 331c829b6e docs: remove obsolete reference to setuptools-scm-git-archive 2023-08-19 13:03:31 -07:00
James R. Barlow 06a5e0c3f6 docs: various updates 2023-08-14 00:13:17 -07:00
811f23381a Add installation instructions for Gentoo Linux to README.md (#1134)
* Add installation instructions for Gentoo Linux to README.md

* Correct installation instructions for macOS (nix) in README.md

* Move installation instructions for Gentoo Linux from README.md to docs/installation.rst

---------

Co-authored-by: Fonic <5cx5jbnmt@mozmail.com>
2023-08-13 01:21:14 -07:00
James R. Barlow a371655052 v14.4.0 docs and release notes 2023-08-12 01:55:00 -07:00
James R. Barlow a6ce35b13a Add argument to override digital signatures 2023-08-12 01:31:36 -07:00
James R. Barlow 45added738 Implement digital signature detection 2023-08-12 01:20:20 -07:00
James R. Barlow 6e20439c91 Remove oddball log.error/raise Exception() pattern
Instead present error message in exception.
2023-08-12 01:02:42 -07:00
James R. Barlow 72e056436c Add simple signature detection 2023-08-12 00:59:54 -07:00
James R. Barlow e02ba19097 Modernize pdfinfo usage of pikepdf to Name.X 2023-08-12 00:54:45 -07:00
James R. Barlow d3b858f994 Improve progress bar presentation/alignment 2023-08-11 01:57:25 -07:00
James R. Barlow 19045c4f21 Replace coloredlogs and tqdm with rich 2023-08-11 01:47:42 -07:00
James R. Barlow f4d89fe6cc Fix typo 2023-08-11 01:47:42 -07:00
Trenton HandGitHub ab85c0f5a9 Enables creation of a release and uploading the build assets to it (#1132) 2023-08-11 01:37:38 -07:00
James R. Barlow 32693b683d Revert "Add Python 3.12 prerelease to build matrix"
This reverts commit b5dc276ba1.

Would require building pikepdf from source on Python 3.12, which is
not worth the effort at this point.
2023-08-01 01:11:15 -07:00
James R. Barlow b5dc276ba1 Add Python 3.12 prerelease to build matrix 2023-08-01 00:32:11 -07:00
James R. Barlow 7c38c71794 gh-action-pypi-publish doesn't have a main - use release/v1 2023-06-20 03:11:46 -07:00
James R. Barlow a80e7a127b Fix some references to master not renamed to main 2023-06-20 01:37:26 -07:00
James R. Barlow cf3309555f Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2023-06-20 01:12:53 -07:00
James R. Barlow f80dd0d86a v14.3.0 release notes 2023-06-20 01:12:45 -07:00
James R. Barlow 1ba2bce486 Merge branch 'docs' 2023-06-20 01:08:09 -07:00
050dd1f5a8 Allow title, subject, author, and keywords to be unset with an empty string argument (#1117)
Co-authored-by: Frederick D. Hansen <frederick.hansen@gmail.com>
2023-06-20 01:07:23 -07:00
James R. Barlow e44a57aec0 Try a screencast/terminal demo 2023-06-20 00:48:42 -07:00
James R. Barlow d94d2671c3 Merge branch 'abwiersma-fix_random_ordering_languages' 2023-06-19 23:27:03 -07:00
James R. Barlow 5124daa79f Fix test failures from preceding 2023-06-19 23:25:31 -07:00
James R. Barlow 7293847da7 Merge branch 'fix_random_ordering_languages' of github.com:abwiersma/OCRmyPDF into abwiersma-fix_random_ordering_languages 2023-06-19 23:21:04 -07:00
Abe 59fd0ac587 Fix randomly ordered languages from set() 2023-06-16 22:14:40 +02:00
James R. Barlow 90619b308c pipeline: improve documentation 2023-06-12 19:50:23 -07:00
James R. Barlow d0d49ce989 docs: Prefer long arguments 2023-06-12 01:00:01 -07:00
James R. Barlow bf0224faa4 Remove final bits of isort 2023-06-11 16:44:09 -07:00
James R. Barlow ae2f8ed8f1 pipeline: describe page/canvas DPI 2023-06-11 13:40:14 -07:00
James R. Barlow 14ac9b0560 docs: some reorganizing 2023-06-11 13:37:53 -07:00
James R. Barlow dbe6148d41 Update release notes
Closes #1083
Closes #1101
2023-06-03 00:27:42 -07:00
James R. Barlow 36d4c2dbbc Merge commit '6eadd65dfba3f983702087605d582f880efc020b' 2023-06-03 00:23:18 -07:00
James R. Barlow adbffb7bd9 Merge branch 'feature/soft-error' 2023-06-03 00:23:10 -07:00
James R. Barlow 05ecb6ca46 Resolve Windows errors with -dPDFSTOPONERROR 2023-06-02 02:48:38 -07:00
James R. Barlow 0a7b60cda5 Fix error on metadata block 2023-06-02 02:47:41 -07:00
James R. Barlow 5f211ecf6f Add test cases for soft errors 2023-06-02 02:47:41 -07:00
James R. Barlow 417ee067a2 Remove obsolete universal_newlines check 2023-06-02 02:47:41 -07:00
James R. Barlow c4649dabef Clarify optimization ratio
Fixes #1070
2023-06-02 01:34:07 -07:00
James R. Barlow 6eadd65dfb Log deskew angle
Closes #1101
2023-06-02 01:12:49 -07:00
James R. Barlow e8ed510543 Add stop on soft render errors and option to override 2023-06-01 23:49:34 -07:00
James R. Barlow 22d35c199d Add Windows package manager to list 2023-06-01 22:57:31 -07:00
James R. Barlow 5a82ad63c9 Modernize issue templates 2023-06-01 22:55:33 -07:00
James R. Barlow 4769a6c50b Merge branches 'master' and 'master' of github.com:ocrmypdf/OCRmyPDF 2023-06-01 16:53:25 -07:00
jbarlow83andGitHub 9004009adc Create bug-report-general.yml 2023-06-01 16:52:08 -07:00
James R. Barlow 11221f9912 Improve rasterization by including more fonts 2023-05-23 13:24:09 -07:00
James R. Barlow 177349cc84 v14.2.1 release notes 2023-05-23 13:21:46 -07:00
James R. Barlow 070c9772ce Add test to confirm we can find formx optimizable images 2023-05-23 00:33:54 -07:00
James R. Barlow 1bc09045a5 optimize: Add recursion into Form XObjects 2023-05-23 00:07:15 -07:00
James R. Barlow e46a18dd2f Refactor searching for optimizable images 2023-05-22 23:42:10 -07:00
James R. Barlow c64871c2ed pyproject: Make classifiers more generic 2023-05-22 23:37:42 -07:00
James R. Barlow de909fb99a Docker arm64 needs to build pycairo; add its dependencies 2023-05-10 01:31:31 -07:00
James R. Barlow 731b2fc477 Fix interaction with old Python and singledispatch and typing 2023-05-10 00:15:34 -07:00
James R. Barlow 214f6ec759 v14.2.0 release notes 2023-05-09 23:32:57 -07:00
James R. Barlow 080aa4dbd1 imageops: fix rare assert failure 2023-05-09 23:27:08 -07:00
James R. Barlow 7af5dcd4a4 Improve directions about install other languages 2023-05-05 13:35:27 -07:00
James R. Barlow fe9f52fbe7 Remove Ghostscript UserUnit warning
All supported Ghostscript versions have proper UserUnit support.
2023-04-29 00:42:38 -07:00
James R. Barlow fcbdeb8dbe Fix resampling attributeerror when Pillow < 9.1.0 2023-04-25 15:17:45 -07:00
James R. Barlow cb251a8d03 Merge branches 'feature/better-imageops' and 'feature/cleanup-fitz' 2023-04-16 01:41:46 -07:00
James R. Barlow 3731fdfd72 Add --tesseract-downsample-above X 2023-04-16 01:41:19 -07:00
James R. Barlow b2e6a6431e Tidy use of pikepdf.open without with 2023-04-15 20:17:44 -07:00
James R. Barlow 9ff1e56bf6 Tidy test_metadata - importorskip idiom 2023-04-15 17:56:13 -07:00
James R. Barlow 2b30f74fce Improve imageops
Fix issues and add better tests. Require hypothesis.
2023-04-15 17:48:33 -07:00
James R. Barlow 10f4c48e0b Fix mypy errors 2023-04-14 13:49:14 -07:00
James R. Barlow 37d5c086bb Fix things not caught by ruff 2023-04-14 13:44:47 -07:00
James R. Barlow 91830627e5 v14.1.0 2023-04-14 13:11:10 -07:00
James R. Barlow f2fc37b257 Merge feature to downsample very large images 2023-04-14 13:09:15 -07:00
James R. Barlow a99e40fa84 Update notes about ocrmypdf.ocr usage 2023-04-14 11:54:58 -07:00
James R. Barlow 9ce692a6f1 ruff: further fixes 2023-04-14 02:39:36 -07:00
James R. Barlow a3c49b8f31 ruff: more lint 2023-04-14 02:19:19 -07:00
James R. Barlow 33b70be7d5 ruff: more fixes, mainly missing docstrings 2023-04-14 02:16:38 -07:00
James R. Barlow 4924b11b6b Additional ruff fixes 2023-04-14 01:25:16 -07:00
James R. Barlow 1d0e4e7c9f ruff: fix from __future__ and drop isort 2023-04-14 00:49:02 -07:00
James R. Barlow 9b8d14d16e Accept most of ruff's delinting 2023-04-14 00:45:34 -07:00
James R. Barlow b7eb93eb79 Adopt ruff and fix prelim lints 2023-04-14 00:19:17 -07:00
James R. Barlow 42c0d0f48f Update pre-commit 2023-04-14 00:06:13 -07:00
James R. Barlow 5fce50ff7c Change linter to ruff 2023-04-14 00:05:43 -07:00
James R. Barlow 765ed4c386 Remove older linters 2023-04-14 00:03:12 -07:00
James R. Barlow 1b2849ec0a Add different timeout control for non-OCR
This is mainly to permit the use of --tesseract-timeout 0 as a way of
disabling OCR but still allowing deskew and orientation detection to work.

The better solution will be introducing an explicit argument to
"don't ocr my pdf", but that will take more rework.
2023-04-13 23:59:58 -07:00
James R. Barlow 4f604591b4 Convert to image size based 2023-04-13 23:49:12 -07:00
James R. Barlow 01dc8e23ff As you were 2023-04-13 23:27:58 -07:00
James R. Barlow b432770cfc Add different timeout control for non-OCR
This is mainly to permit the use of --tesseract-timeout 0 as a way of
disabling OCR but still allowing deskew and orientation detection to work.

The better solution will be introducing an explicit argument to
"don't ocr my pdf", but that will take more rework.
2023-04-13 22:58:12 -07:00
James R. Barlow 5502fb8d9f jobcontext: typo 2023-04-13 16:04:46 -07:00
林博仁(Buo-ren, Lin)andGitHub e66922b030 Fix snap package building (#1082) (#1089)
* Fix snap package building (#1082)

Currently the snap package no longer contains the main Python package
of OCRmyPDF due to the fact that [Snapcraft doesn't run the `pip install .`
command when the setup.py isn't detected in the source tree][1].  This
patch incorporates a workaround from [@cjp256 at the Snapcraft Forum][2]
to generate a setup.py file from the pyproject.toml to make the package
works again.

[1]: [Line 143 of snapcraft/python.py at 0e2cf91 · snapcore/snapcraft · GitHub](https://github.com/snapcore/snapcraft/blob/0e2cf91/snapcraft_legacy/plugins/v2/python.py#L143)
[2]: [Ian Johnson's reply in "Building a core20 python snap using pyproject.toml - snapcraft - snapcraft.io"](https://forum.snapcraft.io/t/building-a-core20-python-snap-using-pyproject-toml/22028/2)

Fixes #1082.

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>

* Remove unnecessary cloning of own source tree to build the snap package

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>

---------

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2023-04-13 00:06:19 -07:00
James R. Barlow 00e9759b16 readme: improve 2023-04-12 23:56:50 -07:00
Kian-Meng AngandGitHub ba10c5345b Fix typos (#1087)
Found via `codespell -S tests,LICENSES -L flate`
2023-03-29 21:57:32 -07:00
James R. Barlow 8a5f94988a v14.0.4 release notes 2023-03-17 14:32:04 -07:00
Frederick RobinsonandGitHub aa73e3c69f handle case when candidate is None (#1066) 2023-03-17 14:27:27 -07:00
James R. Barlow 9d5fa05a00 v14.0.3 2023-02-15 12:00:26 -08:00
997380e567 Avoid deleting /dev/null when run as root (#1068)
Co-authored-by: Andrew Wood <andrew@fluidgravity.co.uk>
2023-01-31 16:20:24 -08:00
comzineandGitHub 2685f910b1 watcher: added setting RETRIES_LOADING_FILE to avoid giving up to early (#1063) 2023-01-25 17:36:54 -08:00
Natanael ArndtandGitHub bfcc586032 tesseract-osd is also required on fedora (#1064) 2023-01-25 17:36:23 -08:00
Lucas LarsonandGitHub 2d77b95fd9 substitute broken link (#1057) (#1058)
use `formulae.brew.sh` as seen in other repositories¹ instead of
`brewformulas.org`, which has been offline since 2020,² to fix #1057.

1. https://github.com/search?type=code&q=/shields.io\/homebrew/+language:reStructuredText
2. https://gitlab.com/zedtux/brewformulas.org/issues/223#note_322019672

Signed-off-by: Lucas Larson <LucasLarson@riseup.net>

Signed-off-by: Lucas Larson <LucasLarson@riseup.net>
2023-01-15 13:13:41 -08:00
James R. Barlow f072e91120 v14.0.2 2023-01-03 17:07:53 -08:00
James R. Barlow efa2bca8a3 Github Actions ignores ||, force it to do the right thing 2023-01-03 14:51:09 -08:00
James R. Barlow b039010d3e Ignore brew update errors 2023-01-03 14:36:21 -08:00
James R. Barlow 3f7cefcf5d macos - drop Python 3.10 since homebrew is having trouble 2023-01-02 13:08:42 -08:00
James R. Barlow 45f97d1565 macos build: brew first? 2023-01-02 12:36:28 -08:00
James R. Barlow 1281f8eb68 Workaround 2to3 linking error in macos build 2023-01-02 01:10:20 -08:00
James R. Barlow 9ef61864fb Update Python versions 2023-01-01 19:13:50 -08:00
James R. Barlow 90b2119ad3 pdfinfo: skip null FormX entries 2023-01-01 16:58:14 -08:00
James R. Barlow f0cc7f2230 docs: explicitly end win32 support 2023-01-01 16:58:14 -08:00
M.Yasoob Ullah Khalid ☺andGitHub d60a384aab Fixed the source installation instructions (#1048) 2022-12-22 02:54:53 -08:00
James R. Barlow 14a6093636 Remove old issue templates 2022-12-14 17:33:00 -08:00
James R. Barlow 54b42d73ab Merge remote-tracking branch 'origin/master' 2022-12-14 17:31:56 -08:00
M.Yasoob Ullah Khalid ☺andGitHub 9abed14f1f fixed interchanged words (#1039) 2022-12-14 17:24:55 -08:00
Doug RinckesandGitHub d09f61d4fe log completion message (#1044)
This logs the "done" message if neither delete nor archive options are set.
2022-12-14 17:24:41 -08:00
M.Yasoob Ullah Khalid ☺andGitHub 4a0130649e Fixed some wording (#1045) 2022-12-14 17:24:22 -08:00
jbarlow83andGitHub a0224d94ca Update issue templates 2022-12-14 17:23:57 -08:00
Carlos GarcésandGitHub 9e7b9de830 Fix tesseract documentation url (#1047) 2022-12-14 11:58:21 -08:00
James R. Barlow 08fc5fc01d Fix issue templates - Github doesn't support comments above 2022-10-24 14:55:08 -07:00
James R. Barlow 110c75cba2 Use actual TesseractVersion for comparison 2022-10-03 23:28:36 -07:00
James R. Barlow 5afca3f342 Use relaxed version parsing 2022-10-03 22:44:35 -07:00
James R. Barlow d9eb0ba7ef v14.0.1 release notes 2022-10-03 22:02:50 -07:00
James R. Barlow 46d0978a09 Update version scripts to support Ghostscript 10.0 2022-10-03 21:59:31 -07:00
James R. Barlow 4e35100978 Docker image: add jbig2dec 2022-10-03 20:08:28 -07:00
James R. Barlow 7bd0e43243 Merge branch 'feature/py311' 2022-09-21 02:42:57 -07:00
James R. Barlow 9cd97da5f2 dockerfile: toml 2022-09-21 02:20:28 -07:00
James R. Barlow d002703c41 docker: install gpg-agent 2022-09-21 01:04:34 -07:00
James R. Barlow b1fecf3b05 Temporarily disable PyPy 3.8 and 3.9 testing
Minor pikepdf issue?
2022-09-21 01:00:08 -07:00
James R. Barlow c2ccc7f29d Fix test failure due to new logging from pikepdf 2022-09-21 01:00:08 -07:00
James R. Barlow 36dfd12e2c Enable Python 3.11 testing on Linux 2022-09-21 00:59:44 -07:00
James R. Barlow 7da4e6ca7f Address some linter warnings 2022-09-21 00:05:12 -07:00
James R. Barlow 16fc52079d Drop setup.py - use pyproject.toml exclusively 2022-09-20 23:47:40 -07:00
James R. Barlow f37decf3b3 Remove old pdfminer.six shim 2022-09-20 23:02:17 -07:00
James R. Barlow 4ad4a13ef0 Simplify dependencies 2022-09-20 22:50:44 -07:00
James R. Barlow 6f61f69a8d v14.0.0 release notes 2022-09-20 22:47:07 -07:00
James R. Barlow 88831e8ab1 Merge branch 'feature/tess5' 2022-09-20 22:33:07 -07:00
James R. Barlow 2ebc36fcec Merge branch 'feature/drop-3.7' 2022-09-20 22:32:59 -07:00
James R. Barlow 1709e23701 pipeline: add further typing details 2022-09-19 20:19:15 -07:00
James R. Barlow 2e55cb5980 helpers: improve typing of Resolution 2022-09-19 20:03:54 -07:00
James R. Barlow 6dbaebdc0c Merge branch 'master' into feature/drop-3.7 2022-09-15 23:00:27 -07:00
James R. Barlow 5156fe7662 Test PyPy 3.8 and 3.9 2022-09-15 22:56:44 -07:00
James R. Barlow 2c99c89e45 ghostscript: document why we treat gs 9.56.0 differently 2022-09-15 22:51:04 -07:00
James R. Barlow 74286e7e1e Use tesseract 5 again in Docker image 2022-08-23 13:39:45 -07:00
James R. Barlow 2e937dee9f Refactor cache manifest creation 2022-08-19 00:19:38 -07:00
James R. Barlow 23f3830533 Merge remote-tracking branch 'origin/master' 2022-08-14 17:01:10 -07:00
belamuandGitHub 305e6594be add Fedora packages in jbig2 doc (#1006)
Packages needed to install jbig2 encoding are mentioned for Ubuntu systems but are missing for other systems. This commit adds the infomation in the documentation what the packages are called on Fedora systems.
2022-08-11 10:31:09 -07:00
James R. Barlow f4155dca77 tests: convert all uses of multipage.pdf to fixture 2022-08-11 01:13:10 -07:00
James R. Barlow 545cd031b0 Replace public domain graph.pdf and derivates with licensed version 2022-08-11 01:09:00 -07:00
James R. Barlow a1c7826336 dep5: mention mawi 2022-08-06 15:47:45 -07:00
James R. Barlow c5359bd990 jbig2 is from linn now 2022-08-06 15:36:20 -07:00
James R. Barlow 7f77308846 Remake palette.pdf using baiona-colormapped file 2022-08-06 15:35:47 -07:00
James R. Barlow 8e7c5f3001 dep5: correction, graph.pdf is actually public domain 2022-08-06 15:34:38 -07:00
James R. Barlow 79db985181 Improve encryption tests; drop some public domain resources
Generate the encrypted files we need and remove special test files we retained for this.

Replace jbig2.pdf based on congress.jpg with version based on ccitt.pdf.
2022-08-06 14:37:45 -07:00
James R. Barlow 7d23a661fc Track .reuse/dep5 2022-08-06 14:32:09 -07:00
James R. Barlow 99e94807c5 .gitignore: updates 2022-08-06 14:30:05 -07:00
James R. Barlow 8412de9344 Merge branch 'master' into feature/drop-3.7 2022-08-06 14:04:23 -07:00
James R. Barlow b458b1422b Merge remote-tracking branch 'origin/master' 2022-08-06 03:06:07 -07:00
James R. Barlow 76bd8cab13 Drop Ubuntu 18.04 content 2022-08-06 02:58:26 -07:00
James R. Barlow ef70c9499e Don't deprecate falsy filter_page_image 2022-08-05 01:07:46 -07:00
James R. Barlow 47dcb6fcd0 Revert "Remove deprecated falsy handling of filter_page_image"
This reverts commit c9389c7713.
2022-08-05 01:01:09 -07:00
James R. Barlow 88d2949e6b Abolish setup.cfg and migrate to pyproject.toml 2022-08-04 05:00:46 -07:00
James R. Barlow c9389c7713 Remove deprecated falsy handling of filter_page_image 2022-08-04 04:21:15 -07:00
James R. Barlow 4d2f499f97 Remove optional status of coloredlogs
Everything optional is a possible complication.
Better to remove the option.
2022-08-04 04:15:56 -07:00
James R. Barlow 4104904a1e Document reason for suppress some third party deprecation warnings 2022-08-04 04:13:02 -07:00
James R. Barlow 1a0a797ca6 Remove our @deprecated decorator and use standard package 2022-08-04 04:00:25 -07:00
James R. Barlow 670ce2b969 Remove support for non-callable version checker 2022-08-04 03:58:10 -07:00
James R. Barlow 7e97981114 hocrtx: unused imports 2022-08-04 03:56:44 -07:00
James R. Barlow 53db866ef9 Remove deprecated exception PdfMergeFailedError 2022-08-04 03:54:55 -07:00
James R. Barlow d591a3e059 resources readme: remove license and copyright info
Better to not repeat ourselves and present this info in example one place.
2022-08-04 03:42:22 -07:00
James R. Barlow 37c050aa4f readme: fix copyright 2022-08-04 03:41:45 -07:00
James R. Barlow 4b9ea40a0c spdx: move identifiers to files that support them
If the apparent license changed, take this commit as correct.
2022-08-04 03:26:54 -07:00
林博仁(Buo-ren, Lin)andGitHub 165432486b snap: add JBIG2 support (#1001)
This patch includes a jbig2enc build into the snap package, allow the
user to run OcrMyPDF with the --jbig2-lossy option or other options that trigger JBIG2 encoding.

Signed-off-by: 林博仁(Buo-ren Lin) <Buo.Ren.Lin@gmail.com>
2022-08-02 16:35:01 -07:00
James R. Barlow d619fac0bd unpaper: tidy file 2022-08-02 15:30:20 -07:00
James R. Barlow acc70036cc Set minimum Tesseract to 4.1.1 2022-08-02 15:20:29 -07:00
James R. Barlow 80b7cf6330 Update black target versions 2022-08-02 15:07:43 -07:00
James R. Barlow 8a8c06c79c Update pre-commit for py3.8+ 2022-08-02 15:05:43 -07:00
James R. Barlow 67773da309 Drop support for Ghostscript <9.50 2022-08-02 15:01:10 -07:00
James R. Barlow d5a9861d5c readme: freebsd calls it py-ocrmypdf now 2022-08-02 14:49:09 -07:00
James R. Barlow 9ffe829a10 Remove external importlib_metadata since Python 3.8 provides it directly 2022-08-02 14:48:11 -07:00
James R. Barlow 8a3b82e364 Make Python 3.8 minimum requirement 2022-08-02 14:46:01 -07:00
James R. Barlow 580822a6a2 Fix Windows ghostscript path scanning 2022-08-02 14:39:23 -07:00
James R. Barlow 9f3a52fd12 windows: compare Ghostscript versions properly 2022-08-01 13:58:24 -07:00
James R. Barlow 52e829d845 windows: tidy path shimming 2022-08-01 13:23:57 -07:00
James R. Barlow 2b2e5c271a Don't lru_cache versions
lru_cache could interfere with testing and we don't have evidence this
is slow enough to justify optimizing.
2022-08-01 13:23:31 -07:00
James R. Barlow 5fe3102e4e tests: new test to confirm correct printing of tesseract install advice 2022-08-01 12:31:37 -07:00
James R. Barlow 5b57520c98 tests: simplify some validation tests 2022-08-01 12:31:05 -07:00
360 changed files with 18683 additions and 8063 deletions
+41 -25
View File
@@ -1,7 +1,7 @@
# OCRmyPDF
#
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
FROM ubuntu:22.04 as base
FROM ubuntu:24.04 AS base
ENV LANG=C.UTF-8
ENV TZ=UTC
@@ -9,51 +9,65 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
libqpdf-dev \
zlib1g \
liblept5
python-is-python3
FROM base as builder
FROM base AS builder
# Note we need leptonica here to build jbig2
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool \
libleptonica-dev \
zlib1g-dev \
python3-dev \
python3-distutils \
libffi-dev \
ca-certificates \
curl \
git
# Get the latest pip (Ubuntu version doesn't support manylinux2010)
RUN \
curl https://bootstrap.pypa.io/get-pip.py | python3
git \
libcairo2-dev \
pkg-config
# Compile and install jbig2
# Needs libleptonica-dev, zlib1g-dev
RUN \
mkdir jbig2 \
&& curl -L https://github.com/agl/jbig2enc/archive/ea6a40a.tar.gz | \
&& curl -L https://github.com/agl/jbig2enc/archive/c0141bf.tar.gz | \
tar xz -C jbig2 --strip-components=1 \
&& cd jbig2 \
&& ./autogen.sh && ./configure && make && make install \
&& cd .. \
&& rm -rf jbig2
COPY . /app
WORKDIR /app
RUN pip3 install --no-cache-dir .[test,webservice,watcher]
# Copy uv from ghcr
COPY --from=ghcr.io/astral-sh/uv:0.5.5 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen \
--extra test --extra webservice --extra watcher --no-dev \
--no-install-package pyarrow
FROM base
RUN apt-get update && apt-get install -y software-properties-common
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr5
RUN apt-get update && apt-get install -y --no-install-recommends \
ghostscript \
img2pdf \
libsm6 libxext6 libxrender-dev \
fonts-droid-fallback \
jbig2dec \
pngquant \
tesseract-ocr \
tesseract-ocr-chi-sim \
@@ -70,11 +84,13 @@ WORKDIR /app
COPY --from=builder /usr/local/lib/ /usr/local/lib/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder /app/misc/webservice.py /app/
COPY --from=builder /app/misc/watcher.py /app/
COPY --from=builder --chown=app:app /app /app
# Copy minimal project files to get the test suite.
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
COPY --from=builder /app/tests /app/tests
RUN rm -rf /app/.git && \
ln -s /app/misc/webservice.py /app/webservice.py && \
ln -s /app/misc/watcher.py /app/watcher.py
ENV PATH="/app/.venv/bin:${PATH}"
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
ENTRYPOINT ["/usr/local/bin/ocrmypdf"]
+80
View File
@@ -0,0 +1,80 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Note: Alpine 3.20 builds tesseract with --enable-opencl, which is not
# supported by anyone. OCRmyPDF is not compatible with Alpine 3.20.0
# through 3.20.3. The issue is fixed in 3.21.
# Details
# https://gitlab.alpinelinux.org/alpine/aports/-/issues/16143
# https://github.com/ocrmypdf/OCRmyPDF/issues/1395
FROM alpine:3.21 AS base
ENV LANG=C.UTF-8
ENV TZ=UTC
RUN apk add --no-cache \
python3 \
zlib
FROM base AS builder
# Yes it really is python3-dev, and py3-package
RUN apk add --no-cache \
ca-certificates \
git \
python3-dev \
py3-pyarrow \
curl
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:0.5.5 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
RUN uv venv --system-site-packages .venv
# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen \
--extra test --extra webservice --extra watcher --no-dev \
--no-install-package pyarrow
FROM base
RUN apk add --no-cache \
ghostscript \
jbig2dec \
jbig2enc \
pngquant \
tesseract-ocr \
tesseract-ocr-data-chi_sim \
tesseract-ocr-data-deu \
tesseract-ocr-data-eng \
tesseract-ocr-data-fra \
tesseract-ocr-data-osd \
tesseract-ocr-data-por \
tesseract-ocr-data-spa \
ttf-droid \
unpaper \
&& rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=builder --chown=app:app /app /app
RUN rm -rf /app/.git && \
ln -s /app/misc/webservice.py /app/webservice.py && \
ln -s /app/misc/watcher.py /app/watcher.py
ENV PATH="/app/.venv/bin:${PATH}"
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# dotfiles
.*
!.coveragerc
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Always use Unix convention for new lines
* text eol=lf
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
@@ -0,0 +1,72 @@
name: Installation, packaging, dependencies
description: Installation, packages, dependencies, "nothing works", test suite failures...
title: "[Bug]: "
labels: ["triage"]
assignees:
- jbarlow83
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
If your issue involves using OCRmyPDF on specific file(s) and not getting
good results, this is the *wrong* issue template. Please use the recommended
template to ensure we have enough information to help.
- type: textarea
id: what-happened
attributes:
label: What were you trying to do?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: dropdown
id: packaging-system
attributes:
label: Where are you installing/running from?
multiple: true
options:
- PyPI (pip, poetry, pipx, etc.)
- Linux package manager (apt, dnf, etc.)
- Wndows package manager (chocolatey, etc.)
- Homebrew
- Docker container
- Ubuntu snap
- Conda
- source build
validations:
required: true
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
- type: dropdown
id: operating-system
attributes:
label: What operating system are you working on?
multiple: true
options:
- Linux
- Windows
- macOS
- BSD
- type: input
id: os_version
attributes:
label: Operating system details and version
- type: checkboxes
attributes:
label: Simple sanity checks
description: Select all that apply
options:
- label: Operating system is currently supported by its vendor (not end of life)
- label: Python version is compatible with OCRmyPDF
- label: This issue is not about a specific input file
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: plain text
@@ -1,32 +0,0 @@
---
name: General issues
about: Installation, packages, dependencies, "nothing works", test suite failures...
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
What's the problem?
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
What did you expected to happen?
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System (please complete the following information):**
- OS:
- 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.
@@ -1,40 +0,0 @@
---
name: Problem with a specific input file
about: Something went wrong while trying to OCR a specific file
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
What command line or API call were you trying to run?
```bash
ocrmypdf ...arguments... input.pdf output.pdf
```
Run with verbosity or higher `-v1` to see more detailed logging. This information may be helpful.
**Example file**
If your issue is a problem that affects only certain files, and we will require an input file (PDF or image) that demonstrates your issue.
Please provide an input file with no personal or confidential information. At your option you may [GPG-encrypt the file](https://github.com/ocrmypdf/OCRmyPDF/wiki) for OCRmyPDF's author only.
Links to files hosted elsewhere are perfectly acceptable. You could also look in ``tests/resources`` and see if any of those files reproduce your issue.
*(Issues without example files usually cannot be resolved. It's like reporting an issue against a web browser without providing a URL.)*
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System**
- OS: [e.g. Linux, Windows, macOS]
- OCRmyPDF Version: ``ocrmypdf --version``
- How did you install ocrmypdf? Did you use a system package manager, `pip`, or a Docker image?
@@ -0,0 +1,75 @@
name: Problem with specific file
description: Something went wrong while trying to OCR a specific file
title: "[Bug]: "
labels: ["triage"]
assignees:
- jbarlow83
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to describe this issue with a particular file.
- type: textarea
id: what-happened
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Please include steps to reproduce.
value: |
1. Run ocrmypdf -v1 ...arguments... input.pdf output.pdf
2. Open output.pdf
3. ...
render: plain text
- type: textarea
id: files
attributes:
label: Files
description: |
Please attach the input and output files, or any screenshots that may be helpful.
If you cannot provide a test file, we probably won't be able to help with the issue.
PDF is a complex file format, and there may be technical details in the PDF that are
causing the issue. There's really no substitute for a test file.
We understand files may contain personal or sensitive information. Here are some options:
- Try reproducing the issue with a file from the OCRmyPDF test suite. (See tests/resources)
- Try to create another file in the same way as your private file.
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
omits personal information.
placeholder: |
Drag and drop files here.
- type: dropdown
id: packaging-system
attributes:
label: How did you download and install the software?
multiple: true
options:
- PyPI (pip, poetry, pipx, etc.)
- Linux package manager (apt, dnf, etc.)
- Windows package manager (chocolatey, etc.)
- Homebrew
- Docker container
- Ubuntu snap
- Conda
- source build
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
placeholder: ocrmypdf --version
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
placeholder: Run OCRmyPDF with verbosity `-v1` to get more detailed logging output.
render: plain text
+83
View File
@@ -0,0 +1,83 @@
name: Problem with third party app that uses OCRmyPDF
description: |
For PDF generation issues with third party software such as Paperless-ngx that
uses OCRmyPDF to perform OCR or generate PDFs.
title: "[3rdparty]: "
labels: ["triage"]
assignees:
- jbarlow83
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to describe this issue with a particular file
and third party app.
If you are comfortable using OCRmyPDF, please trying to install OCRmyPDF,
run it on your file, and see if it works. It's easier for everyone
if you can confirm that the issue occurs with OCRmyPDF and not with
the third party app.
- type: checkboxes
attributes:
label: Simple sanity checks
description: Select all that apply
options:
- label: This is an issue with an app that uses OCRmyPDF for OCR
- label: I am using a recent version of the third party app
- label: I will include a file that reproduces the issuse
- type: input
id: thirdparty-app-name-version
attributes:
label: Third party app name and version
description: e.g. Paperless-ngx 2.9.0
- type: textarea
id: what-happened
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Please include steps to reproduce.
value: |
1. Import attached file into Paperless-ngx
2. Trigger OCR
3. Check log file
4. ...
render: plain text
- type: textarea
id: files
attributes:
label: Files
description: |
Please attach the input and output files, or any screenshots that may be helpful.
If you cannot provide a test file, we probably won't be able to help with the issue.
PDF is a complex file format, and there may be technical details in the PDF that are
causing the issue. There's really no substitute for a test file.
We understand files may contain personal or sensitive information. Here are some options:
- Try reproducing the issue with a file from the test suite. (See tests/resources)
- Try to create another file in the same way as your private file.
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
omits personal information.
placeholder: |
Drag and drop files here.
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
placeholder: ocrmypdf --version
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
placeholder: Run OCRmyPDF with verbosity `-v1` to get more detailed logging output.
render: plain text
@@ -1,27 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered. Please include the versions of OCRmyPDF and other supporting programs (Tesseract OCR, Ghostscript) - maybe an alternative already exists in a newer version.
**Example file**
If your issue concerns how OCRmyPDF processes certain files, and please provide an example file that helps illustrate how OCRmyPDF's output could be improve.
Please provide an input file with no personal or confidential information. At your option you may [GPG-encrypt the file](https://github.com/ocrmypdf/OCRmyPDF/wiki) for OCRmyPDF's author only.
Links to files hosted elsewhere are perfectly acceptable. You could also look in ``tests/resources`` and see if any of those files reproduce your issue.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,12 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature]: "
labels: ["enhancement", "triage"]
assignees:
- jbarlow83
body:
- type: textarea
id: feature
attributes:
label: Describe the proposed feature
description: A clear and concise description of what the desired is.
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
+171 -82
View File
@@ -1,9 +1,11 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
name: Test and deploy
on:
push:
branches:
- master
- main
- ci
- release/*
- feature/*
@@ -19,41 +21,38 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
python: ["3.10", "3.11", "3.12", "3.13"]
include:
- os: ubuntu-18.04
python: "3.7"
- os: ubuntu-20.04
python: "3.8"
- os: ubuntu-20.04
python: "3.9"
- os: ubuntu-20.04
- os: ubuntu-22.04
tesseract_ppa: "ppa"
python: "3.10"
- os: ubuntu-latest
python: "3.9"
- os: ubuntu-latest
python: "pypy-3.8"
- os: ubuntu-latest
python: "3.9"
tesseract5: true
- os: ubuntu-24.04
python: "pypy3.10"
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
name: Install Python
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.5.x"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install Tesseract 5
if: matrix.tesseract5
- name: Install Tesseract from PPA
if: matrix.tesseract_ppa == 'ppa'
run: |
sudo add-apt-repository ppa:alex-p/tesseract-ocr-devel
sudo add-apt-repository -y ppa:alex-p/tesseract-ocr5.3
- name: Install common packages
run: |
@@ -61,7 +60,9 @@ jobs:
sudo apt-get install -y --no-install-recommends \
curl \
ghostscript \
jbig2dec \
img2pdf \
libexempi8 \
libffi-dev \
libsm6 libxext6 libxrender-dev \
pngquant \
@@ -73,18 +74,6 @@ jobs:
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' || matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install -y --no-install-recommends \
libexempi8
- name: Install Ubuntu packages for PyPy
if: startsWith(matrix.python, 'pypy')
run: |
@@ -95,8 +84,7 @@ jobs:
- name: Install Python packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install .[test]
uv sync --extra test --no-dev
- name: Report versions
run: |
@@ -104,14 +92,16 @@ jobs:
gs --version
pngquant --version
unpaper --version
img2pdf --version
uv run --no-dev img2pdf --version
- name: Test
run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -121,24 +111,20 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
python: ["3.9", "3.10"]
os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64
python: ["3.10", "3.11", "3.12", "3.13"]
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
name: Install Python
with:
python-version: ${{ matrix.python }}
- name: Install Homebrew deps
continue-on-error: true
run: |
brew update
brew install \
@@ -149,24 +135,35 @@ jobs:
pngquant \
tesseract
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.5.x"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install Python packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install .[test]
uv sync --extra test --no-dev
- name: Report versions
run: |
tesseract --version
gs --version
pngquant --version
img2pdf --version
uv run --no-dev img2pdf --version
- name: Test
run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -177,38 +174,44 @@ jobs:
strategy:
matrix:
os: [windows-latest]
python: ["3.9", "3.10"]
python: ["3.10", "3.11", "3.12", "3.13"]
env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
name: Install Python
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.5.x"
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install system packages
run: |
choco install --yes --no-progress --pre tesseract
choco install --yes --no-progress --ignore-checksums ghostscript
choco install --yes --no-progress --ignore-checksums ghostscript --version 9.56.1
- name: Install Python packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install .[test]
uv sync --extra test --no-dev
- name: Test
run: |
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -217,23 +220,22 @@ jobs:
name: Build sdist and wheels
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
name: Install Python
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.7"
version: "0.5.x"
- name: Make wheels and sdist
run: |
python -m pip install --upgrade pip wheel
python setup.py sdist
python setup.py bdist_wheel
uv build --sdist --wheel
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: artifact
path: |
./dist/*.whl
./dist/*.tar.gz
@@ -242,21 +244,63 @@ jobs:
name: Deploy artifacts to PyPI
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # mandatory for PyPI publishing
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
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/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
docker:
name: Build Docker images
create_release:
name: Create GitHub release
needs: [upload_pypi]
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
permissions:
# Required to create a release
contents: write
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: artifact
path: dist
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: |
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
"$GITHUB_REF_NAME"
--repo "$GITHUB_REPOSITORY"
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
"$GITHUB_REF_NAME" dist/**
--repo "$GITHUB_REPOSITORY"
docker_ubuntu:
name: Build Ubuntu-based Docker image
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
@@ -264,9 +308,9 @@ jobs:
- name: Set image tag to release or branch
run: echo "DOCKER_IMAGE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: If master, set to latest
- name: If main, set to latest
run: echo 'DOCKER_IMAGE_TAG=latest' >> $GITHUB_ENV
if: env.DOCKER_IMAGE_TAG == 'master'
if: env.DOCKER_IMAGE_TAG == 'main'
- name: Set Docker Hub repository to username
run: echo "DOCKER_REPOSITORY=jbarlow83" >> $GITHUB_ENV
@@ -274,22 +318,22 @@ jobs:
- name: Set image name
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: jbarlow83
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Print image tag
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
@@ -300,4 +344,49 @@ jobs:
--push \
--platform linux/arm64/v8,linux/amd64 \
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}-ubuntu:${DOCKER_IMAGE_TAG}" \
--file .docker/Dockerfile .
docker_alpine:
name: Build Alpine-based Docker images
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Set image tag to release or branch
run: echo "DOCKER_IMAGE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: If main, set to latest
run: echo 'DOCKER_IMAGE_TAG=latest' >> $GITHUB_ENV
if: env.DOCKER_IMAGE_TAG == 'main'
- name: Set Docker Hub repository to username
run: echo "DOCKER_REPOSITORY=jbarlow83" >> $GITHUB_ENV
- name: Set image name
run: echo "DOCKER_IMAGE_NAME=ocrmypdf-alpine" >> $GITHUB_ENV
- uses: actions/checkout@v4
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: jbarlow83
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- 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/amd64,linux/arm64 \
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
--file .docker/Dockerfile.alpine .
+32
View File
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
name: Remove Triage Label on Reply
on:
issue_comment:
types:
- created
jobs:
remove-triage-label:
runs-on: ubuntu-latest
steps:
- name: Check if comment is by the repository owner
id: check_comment
run: |
echo "::set-output name=is_owner::$(
if [[ '${{ github.event.comment.user.login }}' == 'jbarlow83' ]]; then
echo 'true';
else
echo 'false';
fi
)"
- name: Remove 'triage' label
if: ${{ steps.check_comment.outputs.is_owner == 'true' }}
uses: actions-ecosystem/action-remove-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: triage
+13 -11
View File
@@ -1,14 +1,15 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: CC-BY-SA-4.0
# dotfiles
.*
!.coveragerc
!.dockerignore
!.git_archival.txt
!.gitattributes
!.gitignore
!.pre-commit-config.yaml
!.readthedocs.yaml
!.github/
!.docker/
.coverage
.venv*/
.tox/
.vscode/
.hypothesis/
.ipynb_checkpoints/
.mypy_cache/
.pytest_cache/
# Dev scratch
*.ipynb
@@ -26,6 +27,7 @@ venv*/
*.traineddata
/private
/coverage.xml
/issuepdf
# Package building
*.egg-info/
@@ -42,4 +44,4 @@ docs/_build/
docs/_static/
docs/_templates/
docs/Makefile
ocrmypdf/lib/_*.py
src/ocrmypdf/_version.py
+11 -16
View File
@@ -1,33 +1,28 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/pycqa/isort
rev: 5.10.1
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.0.261"
hooks:
- id: isort
args: ["--profile", "black", "-a", "from __future__ import annotations"]
- id: ruff
files: "src/.*\\.pyi?$"
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black
rev: 22.6.0
rev: 23.3.0
hooks:
- id: black
language_version: python
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.2
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/pyupgrade
rev: v2.37.2
hooks:
- id: pyupgrade
args: ["--py37-plus"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
rev: v1.2.0
hooks:
- id: mypy
additional_dependencies:
+8 -5
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
@@ -8,13 +11,13 @@ version: 2
sphinx:
configuration: docs/conf.py
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
# Optionally set the version of Python and requirements required to build your docs
build:
os: ubuntu-22.04
tools:
python: "3.10"
python:
version: "3.7"
install:
- method: pip
path: .
+140
View File
@@ -0,0 +1,140 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: OCRmyPDF
Upstream-Contact: James R. Barlow <james@purplerock.ca>
Source: https://github.com/ocrmypdf/OCRmyPDF
Files:
.git_archival.txt
docs/images/logo-social.png
docs/images/logo-square-256.svg
docs/images/logo-square.png
docs/images/logo-square.svg
docs/images/logo.svg
Copyright: (C) 2022 James R. Barlow
License: MPL-2.0
Files:
.github/ISSUE_TEMPLATE/*.yml
docs/images/macos-workflow.png
Copyright: (C) 2023 James R. Barlow
License: CC-BY-SA-4.0
Files:
tests/resources/acroform.pdf
tests/resources/aspect.pdf
tests/resources/blank.pdf
tests/resources/cmyk.pdf
tests/resources/crom.png
tests/resources/enormous.pdf
tests/resources/formxobject.pdf
tests/resources/francais.pdf
tests/resources/hugemono.pdf
tests/resources/invalid.pdf
tests/resources/kcs.pdf
tests/resources/livecycle.pdf
tests/resources/meta.pdf
tests/resources/missing_docinfo.pdf
tests/resources/negzero.pdf
tests/resources/no_contents.pdf
tests/resources/tagged*
tests/resources/toc.pdf
tests/resources/trivial.pdf
tests/resources/truetype_font_nomapping.pdf
tests/resources/type3_font_nomapping.pdf
Copyright: (C) 2023 James R. Barlow
License: CC-BY-SA-4.0
Files:
tests/resources/graph.pdf
tests/resources/graph_ocred.pdf
Copyright: (C) 2012 SmokeyJoe
License: GFDL-1.2-or-later or CC-BY-SA-3.0
Files: tests/resources/c02-22.pdf
tests/resources/multipage.pdf
Copyright: Public domain
License: public-domain
Copyright on these files has expired.
Files: docs/images/bitmap_vs_svg.svg
Copyright: (C) 2006 Yug
License: CC-BY-SA-2.5
Files: tests/cache/*
Copyright: (C) 2022 James R. Barlow
License: CC-BY-SA-4.0
Files: tests/resources/linn.png
tests/resources/linn.pdf
tests/resources/linn.txt
tests/resources/ccitt.pdf
tests/resources/cardinal.pdf
tests/resources/jbig2.pdf
tests/resources/jbig2_baddevicen.pdf
tests/resources/skew.pdf
tests/resources/rotated_skew.pdf
tests/resources/poster.pdf
Copyright: (C) 1985 Forat Electronics
License: GFDL-1.2-or-later or CC-BY-SA-3.0
Files: tests/resources/lichtenstein.pdf
Copyright: (C) 2001 Andreas Tille
(C) 2007 Alessio Damato
License: GFDL-1.2-or-later or CC-BY-SA-3.0
Files: tests/resources/masks.pdf
Copyright: held by the contributors to the German Wikipedia article "Linux"
see: https://de.wikipedia.org/w/index.php?title=Linux&action=history
(masks.pdf generated from Wikipedia article as of 2016-08-24)
License: CC-BY-SA-3.0
Files: tests/resources/epson.pdf
Copyright: held by the contributors to the Wikipedia article "Optical character recognition"
see: https://en.wikipedia.org/w/index.php?title=Optical_character_recognition&action=history
(epson.pdf generated from Wikipedia article as of 2016-09-14)
License: CC-BY-SA-3.0
Files: tests/resources/typewriter.png tests/resources/2400dpi.pdf
Copyright: (C) 2005 Ellywa
License: GFDL-1.2-or-later or CC-BY-SA-1.0 or CC-BY-SA-2.0 or CC-BY-SA-2.5 or CC-BY-SA-3.0
Comment:
Obtained from: https://commons.wikimedia.org/wiki/File:Triumph.typewriter_text_Linzensoep.gif
Files: tests/resources/overlay.pdf
Copyright: (C) 2017 Max Anderson
License: MIT
Files:
tests/resources/baiona*.png
tests/resources/baiona*.jpg
tests/resources/link.pdf
tests/resources/palette.pdf
Copyright: (C) 2014 Euskaldunaa
License: CC-BY-SA-4.0
Files: tests/resources/vector.pdf
Copyright: (C) 2018 Catscratch
License: MIT
Files: src/ocrmypdf/data/sRGB.icc
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
Marti Maria <www.littlecms.com>
Photogamut <www.photogamut.org>
Graeme Gill <www.argyllcms.com>
ColorSolutions <www.basICColor.com>
License: Zlib
Files: src/ocrmypdf/data/pdf.ttf
Copyright: (C) 2014 Ray Smith
(C) 2015 Ken Sharp
(C) 2016 James R. Barlow
(C) 2016 Jeff Breidenbach
(C) 2017 Zdenko Podobný
License: Apache-2.0
Files: tests/resources/3small.pdf
Copyright: (C) 2014 Euskaldunaa
(C) 2017 James R. Barlow
(C) 2005 Ellywa
License: CC-BY-SA-4.0 and (GFDL-1.2-or-later or CC-BY-SA-1.0 or CC-BY-SA-2.0 or CC-BY-SA-2.5 or CC-BY-SA-3.0)
Comment: concatenation of baiona_gray.png, crom.png and typewriter.png/2400dpi.pdf
+38 -10
View File
@@ -1,3 +1,7 @@
<!-- SPDX-FileCopyrightText: 2014 Julien Pfefferkorn -->
<!-- SPDX-FileCopyrightText: 2015 James R. Barlow -->
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
<img src="docs/images/logo.svg" width="240" alt="OCRmyPDF">
[![Build Status](https://github.com/ocrmypdf/OCRmyPDF/actions/workflows/build.yml/badge.svg)](https://github.com/ocrmypdf/OCRmyPDF/actions/workflows/build.yml) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
@@ -35,8 +39,10 @@ ocrmypdf # it's a scriptable command line program
- Distributes work across all available CPU cores
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata)
- Keeps your private data private.
- Scales properly to handle files with thousands of pages
- Battle-tested on millions of PDFs
- Scales properly to handle files with thousands of pages.
- Battle-tested on millions of PDFs.
<img src="misc/screencast/demo.svg" alt="Demo of OCRmyPDF in a terminal session">
For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/).
@@ -64,10 +70,10 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
| Fedora | ``dnf install ocrmypdf`` |
| macOS (Homebrew) | ``brew install ocrmypdf`` |
| macOS (nix) | ``nix-env -i ocrmypdf`` |
| macOS (MacPorts) | ``port install ocrmypdf`` |
| macOS (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py37-ocrmypdf`` |
| Conda | ``conda install ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` |
| Ubuntu Snap | ``snap install ocrmypdf`` |
For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps.
@@ -92,10 +98,7 @@ 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.
OCRmyPDF supports Tesseract 4.0 and the beta versions of Tesseract 5.0. It will
automatically use whichever version it finds first on the `PATH` environment
variable. On Windows, if `PATH` does not provide a Tesseract binary, we use
the highest version number that is installed according to the Windows Registry.
OCRmyPDF supports Tesseract 4.1.1+. It will automatically use whichever version it finds first on the `PATH` environment variable. On Windows, if `PATH` does not provide a Tesseract binary, we use the highest version number that is installed according to the Windows Registry.
## Documentation and support
@@ -109,9 +112,33 @@ Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/e
Please report issues on our [GitHub issues](https://github.com/ocrmypdf/OCRmyPDF/issues) page, and follow the issue template for quick response.
## Feature demo
```bash
# Add an OCR layer and convert to PDF/A
ocrmypdf input.pdf output.pdf
# Convert an image to single page PDF
ocrmypdf input.jpg output.pdf
# Add OCR to a file in place (only modifies file on success)
ocrmypdf myfile.pdf myfile.pdf
# OCR with non-English languages (look up your language's ISO 639-3 code)
ocrmypdf -l fra LeParisien.pdf LeParisien.pdf
# OCR multilingual documents
ocrmypdf -l eng+fra Bilingual-English-French.pdf Bilingual-English-French.pdf
# Deskew (straighten crooked pages)
ocrmypdf --deskew input.pdf output.pdf
```
For more features, see the [documentation](https://ocrmypdf.readthedocs.io/en/latest/index.html).
## Requirements
In addition to the required Python version (3.7+), OCRmyPDF requires external program installations of Ghostscript and Tesseract OCR. OCRmyPDF is pure Python, and runs on pretty much everything: Linux, macOS, Windows and FreeBSD.
In addition to the required Python version, OCRmyPDF requires external program installations of Ghostscript and Tesseract OCR. OCRmyPDF is pure Python, and runs on pretty much everything: Linux, macOS, Windows and FreeBSD.
## Press & Media
@@ -122,6 +149,7 @@ In addition to the required Python version (3.7+), OCRmyPDF requires external pr
- [heise Durchsuchbare PDF-Dokumente mit OCRmyPDF erstellen](https://www.heise.de/ratgeber/Durchsuchbare-PDF-Dokumente-mit-OCRmyPDF-erstellen-4607592.html)
- [Excellent Utilities: OCRmyPDF](https://www.linuxlinks.com/excellent-utilities-ocrmypdf-add-ocr-text-layer-scanned-pdfs/)
- [LinuxUser Texterkennung mit OCRmyPDF und Scanbd automatisieren](https://www.linux-community.de/ausgaben/linuxuser/2021/06/texterkennung-mit-ocrmypdf-und-scanbd-automatisieren/)
- [Y Combinator discussion](https://news.ycombinator.com/item?id=32028752)
## Business enquiries
+189 -46
View File
@@ -10,7 +10,7 @@ Control of unpaper
OCRmyPDF uses ``unpaper`` to provide the implementation of the
``--clean`` and ``--clean-final`` arguments.
`unpaper <https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md>`__
`unpaper <https://github.com/Flameeyes/unpaper/blob/main/doc/basic-concepts.md>`__
provides a variety of image processing filters to improve images.
By default, OCRmyPDF uses only ``unpaper`` arguments that were found to
@@ -47,8 +47,8 @@ and clean up the margins of both.
Some ``unpaper`` features cause multiple input or output files to be
consumed or produced. OCRmyPDF requires ``unpaper`` to consume one
file and produce one file. An deviation from that condition will
result in errors.
file and produce one file; errors will result if this assumption is not
met.
.. note::
@@ -82,14 +82,17 @@ is stripped out. Then an image of each page is created with visible text
masked out. The page image is sent for OCR, and any additional text is
inserted as OCR. If a file contains a mix of text and bitmap images that
contain text, OCRmyPDF will locate the additional text in images without
disrupting the existing text.
disrupting the existing text. Some PDF OCR solutions render text as
technically printable or visible in some way, perhaps by drawing it and
then painting over it. OCRmyPDF cannot distinguish this type of OCR
text from real text, so it will not be "redone".
If ``--force-ocr`` is issued, then all pages will be rasterized to
images, discarding any hidden OCR text, and rasterizing any printable
text. This is useful for redoing OCR, for fixing OCR text with a damaged
character map (text is selectable but not searchable), and destroying
redacted information. Any forms and vector graphics will be rasterized
as well.
images, discarding any hidden OCR text, rasterizing any printable
text, and flattening form fields or interactive objects into their visual
representation. This is useful for redoing OCR, for fixing OCR text
with a damaged character map (text is selectable but not searchable),
and destroying redacted information.
Time and image size limits
--------------------------
@@ -104,13 +107,49 @@ was requested, the preprocessed image layer will be inserted.
If you want to adjust the amount of time spent on OCR, change
``--tesseract-timeout``. You can also automatically skip images that
exceed a certain number of megapixels with ``--skip-big``. (A 300 DPI,
8.5×11" page is 8.4 megapixels.)
8.5×11" page image is 8.4 megapixels.)
.. code-block:: bash
# Allow 300 seconds for OCR; skip any page larger than 50 megapixels
ocrmypdf --tesseract-timeout 300 --skip-big 50 bigfile.pdf output.pdf
OCR for huge images
-------------------
Tesseract has internal limits on the size
of images it will process. By default,
``--tesseract-downsample-large-images`` is enabled, and OCRmyPDF will
downsample images to fit Tesseract limits. (The limits are usually encountered
only for scanned images of oversized media, such as large maps or blueprints exceeding
110 cm or 43 inches in either dimension, and at high DPI.) This feature can disabled
using ``--no-tesseract-downsample-large-images``.
``--tesseract-downsample-above Npixels`` adjusts the threshold at which images
will be downsampled. By default, only images that exceed any of Tesseract's
internal limits are downsampled (32767 pixels on either dimension).
You will also need to set ``--tesseract-timeout`` high enough to allow
for processing.
Only the image sent for OCR is downsampled. The original image is
preserved.
.. code-block:: bash
# Allow 600 seconds for OCR on huge images
ocrmypdf --tesseract-timeout 600 \
--tesseract-downsample-large-images \
bigfile.pdf output.pdf
# Downsample images above 5000 pixels on the longest dimension to
# 5000 pixels
ocrmypdf --tesseract-timeout 120 \
--tesseract-downsample-large-images \
--tesseract-downsample-above 5000 \
bigfile.pdf output_downsampled_ocr.pdf
Overriding default tesseract
----------------------------
@@ -154,13 +193,14 @@ In addition to tesseract, OCRmyPDF uses the following external binaries:
- ``jbig2``
In each case OCRmyPDF will search the ``PATH`` environment variable to
locate the binaries.
locate the binaries. By modifying the ``PATH`` environment variable, you
can override the binaries that OCRmyPDF uses.
Changing tesseract configuration variables
Changing Tesseract configuration variables
------------------------------------------
You can override tesseract's default `control
parameters <https://github.com/tesseract-ocr/tesseract/wiki/ControlParams>`__
You can override Tesseract's default `control
parameters <https://tesseract-ocr.github.io/tessdoc/tess3/ControlParams.html>`__
with a configuration file.
As an example, this configuration will disable Tesseract's dictionary
@@ -188,6 +228,59 @@ then run ocrmypdf as follows (along with any other desired arguments):
Some combinations of control parameters will break Tesseract or break
assumptions that OCRmyPDF makes about Tesseract's output.
Changing page segmentation mode
-------------------------------
The directive ``--tesseract-pagesegmode Nmode`` forwards the desired page segmentation
mode to Tesseract OCR. The default is 3.
Page segmentation can improve OCR results when you know that a PDF ought to be
analyzed a particular way, such as PDFs whose pages contain only a single line of
text. For the vast majority of users, changing the page segmentation mode will only
make things worse.
As of June 2024, the Tesseract page segmentation modes are:
+-----+----------------------------------------------------------------------------------+
| ID | Description |
+=====+==================================================================================+
| 0 | Orientation and script detection (OSD) only. |
+-----+----------------------------------------------------------------------------------+
| 1 | Automatic page segmentation with OSD. |
+-----+----------------------------------------------------------------------------------+
| 2 | Automatic page segmentation, but no OSD, or OCR. (not implemented) |
+-----+----------------------------------------------------------------------------------+
| 3 | Fully automatic page segmentation, but no OSD. (Default) |
+-----+----------------------------------------------------------------------------------+
| 4 | Assume a single column of text of variable sizes. |
+-----+----------------------------------------------------------------------------------+
| 5 | Assume a single uniform block of vertically aligned text. |
+-----+----------------------------------------------------------------------------------+
| 6 | Assume a single uniform block of text. |
+-----+----------------------------------------------------------------------------------+
| 7 | Treat the image as a single text line. |
+-----+----------------------------------------------------------------------------------+
| 8 | Treat the image as a single word. |
+-----+----------------------------------------------------------------------------------+
| 9 | Treat the image as a single word in a circle. |
+-----+----------------------------------------------------------------------------------+
| 10 | Treat the image as a single character. |
+-----+----------------------------------------------------------------------------------+
| 11 | Sparse text. Find as much text as possible in no particular order. |
+-----+----------------------------------------------------------------------------------+
| 12 | Sparse text with OSD. |
+-----+----------------------------------------------------------------------------------+
| 13 | Raw line. Treat the image as a single text line, bypassing hacks that are |
| | Tesseract-specific. |
+-----+----------------------------------------------------------------------------------+
Modes 0, 1, 2, and 12 (all of those that enable orientation and script detection)
are not compatible with OCRmyPDF, which performs OSD in a separate step from OCR.
Their use may interfere with ``--rotate-pages`` and other features.
It is currently not possible to use advanced Tesseract OCR features, such as creating
OCR information, when using Tesseract through OCRmyPDF.
Changing the PDF renderer
=========================
@@ -200,46 +293,77 @@ rendering
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The
renderer may be selected using ``--pdf-renderer``. The default is
``auto`` which lets OCRmyPDF select the renderer to use. Currently,
``auto`` always selects ``sandwich``.
``auto`` always selects ``hocr``.
The ``hocr`` renderer
---------------------
.. versionchanged:: 16.0.0
In both renderers, a text-only layer is rendered and sandwiched (overlaid)
on to either the original PDF page, or newly rasterized version of the
original PDF page (when ``--force-ocr`` is used). In this way, loss
of PDF information is generally avoided. (You may need to disable PDF/A
conversion and optimization to eliminate all lossy transformations.)
The current approach used by the new hOCR renderer is a re-implementation
of Tesseract's PDF renderer, using the same Glyphless font and general
ideas, but fixing many technical issues that impeded it. The new hocr
provides better text placement accuracy, avoids issues with word
segmentation, and provides better positioning of skewed text.
Using the experimental API, it is also possible to edit the OCR output
from Tesseract, using any tool that is capable of editing hOCR files.
Older versions of this renderer did not support non-Latin languages, but
it is now universal.
The ``sandwich`` renderer
-------------------------
The ``sandwich`` renderer uses Tesseract's new text-only PDF feature,
which produces a PDF page that lays out the OCR in invisible text. This
page is then "sandwiched" onto the original PDF page, allowing lossless
application of OCR even to PDF pages that contain other vector objects.
The ``sandwich`` renderer uses Tesseract's text-only PDF feature,
which produces a PDF page that lays out the OCR in invisible text.
Currently this is the best renderer for most uses, however it is
implemented in Tesseract so OCRmyPDF cannot influence it. Currently some
problematic PDF viewers like Mozilla PDF.js and macOS Preview have
problems with segmenting its text output, and
mightrunseveralwordstogether.
Currently some problematic PDF viewers like Mozilla PDF.js and macOS
Preview have problems with segmenting its text output, and
mightrunseveralwordstogether. It also does not implement right to left
fonts (Arabic, Hebrew, Persian). The output of this renderer cannot
be edited. The sandwich renderer is retained for testing.
When image preprocessing features like ``--deskew`` are used, the
original PDF will be rendered as a full page and the OCR layer will be
placed on top.
The ``hocr`` renderer
---------------------
Rendering and rasterizing options
=================================
The ``hocr`` renderer works with older versions of Tesseract. The image
layer is copied from the original PDF page if possible, avoiding
potentially lossy transcoding or loss of other PDF information. If
preprocessing is specified, then the image layer is a new PDF. (You may
need to disable PDF/A conversion nad optimization to eliminate all
lossy transformations.)
.. versionadded:: 14.3.0
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
looking to customize how OCR is presented should look here. A major
disadvantage of this renderer is it not capable of correctly handling
text outside the Latin alphabet (specifically, it supports the ISO 8859-1
character). Pull requests to improve the situation are welcome.
The ``--continue-on-soft-render-error`` option allows OCRmyPDF to
proceed if a page cannot be rasterized/rendered. This is useful if you are
trying to get the best possible OCR from a PDF that is not well-formed,
and you are willing to accept some pages that may not visually match the
input, and that may not OCR well.
Currently, this renderer has the best compatibility with Mozilla's
PDF.js viewer.
Color conversion strategy
=========================
This works in all versions of Tesseract.
.. versionadded:: 15.0.0
OCRmyPDF uses Ghostscript to convert PDF to PDF/A. In some cases, this
conversion requires color conversion. The default strategy is to convert
using the ``LeaveColorUnchanged`` strategy, which preserves the original
color space wherever possible (some rare color spaces might still be
converted).
Usually document scanners produce PDFs in the sRGB color space, and do
not need to be converted, so the default strategy is appropriate.
Suppose that you have a document that was prepared for professional
printing in a Separation or CMYK color space, and text was converted to
curves. In this case, you may want to use a different color conversion
strategy. The ``--color-conversion-strategy`` option allows you to select a
different strategy, such as ``RGB``.
Return code policy
==================
@@ -299,22 +423,41 @@ stable user interface. They may be imported from
- The program was interrupted by pressing Ctrl+C.
.. _tmpdir:
Changing temporary storage location
===================================
OCRmyPDF generates many temporary files during processing.
To change where temporary files are stored, change the ``TMPDIR``
environment variable for ocrmypdf's environment. (Python's
``tempfile.gettempdir()`` returns the root directory in which temporary
files will be stored.) For example, one could redirect ``TMPDIR`` to a
large RAM disk to avoid wear on HDD/SSD and potentially improve
performance.
On Windows, the ``TEMP`` environment variable is used instead.
Debugging the intermediate files
================================
OCRmyPDF normally saves its intermediate results to a temporary folder
and deletes this folder when it exits, whether it succeeded or failed.
If the ``-k`` argument is issued on the command line, OCRmyPDF will keep
the temporary folder and print the location, whether it succeeded or
failed (provided the Python interpreter did not crash). An example
message is:
If the ``--keep-temporary-files`` (``-k``) argument is issued on the
command line, OCRmyPDF will keep the temporary folder and print the location,
whether it succeeded or failed. An example message is:
.. code-block:: none
Temporary working files retained at:
/tmp/ocrmypdf.io.u20wpz07
When OCRmyPDF is launched as a snap, this corresponds to the snap filesystem, for instance:
/tmp/snap-private-tmp/snap.ocrmypdf/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
working files on a per page basis have the page number as a prefix
@@ -326,9 +469,9 @@ suffix indicates the file type. Some important files include:
on arguments this may differ from the presentation image
- ``_pp_deskew.png`` - the image, after deskewing
- ``_pp_clean.png`` - the image, after cleaning with unpaper
- ``_ocr_tess.pdf`` - the OCR file; appears as a blank page with invisible
- ``_ocr_hocr.pdf`` - the OCR file; appears as a blank page with invisible
text embedded
- ``_ocr_tess.txt`` - the OCR text (not necessarily all text on the page,
- ``_ocr_hocr.txt`` - the OCR text (not necessarily all text on the page,
if the page is mixed format)
- ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo
data structure
+26 -27
View File
@@ -39,9 +39,7 @@ Parent process requirements
The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line
execution. To do this, it will:
- create a monitoring thread
- create worker processes (on Linux, forking itself; on Windows and macOS, by
spawning)
- create worker processes or threads
- manage the signal flags of its worker processes
- execute other subprocesses (forking and executing other programs)
@@ -54,7 +52,19 @@ processes.
Creating a child process to call :func:`ocrmypdf.ocr()` is suggested. That
way your application will survive and remain interactive even if
OCRmyPDF fails for any reason.
OCRmyPDF fails for any reason. For example:
.. code-block:: python
from multiprocessing import Process
def ocrmypdf_process():
ocrmypdf.ocr('input.pdf', 'output.pdf')
def call_ocrmypdf_from_my_app():
p = Process(target=ocrmypdf_process)
p.start()
p.join()
Programs that call :func:`ocrmypdf.ocr()` should also install a SIGBUS signal
handler (except on Windows), to raise an exception if access to a memory
@@ -72,14 +82,6 @@ OCRmyPDF, use processes.
not take at least one of these steps, process semantics will prevent
OCRmyPDF from working correctly.
.. warning::
On macOS with Python 3.7, you must call
:func:`multiprocessing.set_start_method("spawn")`. Without this, multiprocessing
will be unstable. From the command line, OCRmyPDF does this automatically,
but as an API user you must do this. See Python bpo-33725 for details.
Python 3.8+ also resolve this automatically.
Logging
-------
@@ -97,12 +99,23 @@ your use case.
Progress monitoring
-------------------
OCRmyPDF uses the ``tqdm`` package to implement its progress bars.
OCRmyPDF uses the ``rich`` package to implement its progress bars.
:func:`ocrmypdf.configure_logging` will set up logging output to
``sys.stderr`` in a way that is compatible with the display of the
progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable
the progress bar.
Standard output
---------------
OCRmyPDF is strict about not writing to standard output so that
users can safely use it in a pipeline and produce a valid output
file. A caller application will have to ensure it does not write to
standard output either, if it wants to be compatible with this
behavior and support piping to a file. Another benefit of running
OCRmyPDF in a child process, as recommended above, is that it will
not interfere with the parent process's standard output.
Exceptions
----------
@@ -112,18 +125,4 @@ exceptions, some exceptions related to multiprocessing, and
handler. OCRmyPDF will clean up its temporary files and worker processes
automatically when an exception occurs.
Programs that call OCRmyPDF should consider trapping KeyboardInterrupt
so that they allow OCR to terminate with the whole program terminating.
When OCRmyPDF succeeds conditionally, it returns an integer exit code.
Reference
---------
.. autofunction:: ocrmypdf.ocr
.. autoclass:: ocrmypdf.Verbosity
:members:
:undoc-members:
.. autofunction:: ocrmypdf.configure_logging
+5 -8
View File
@@ -3,19 +3,16 @@
.. SPDX-License-Identifier: CC-BY-SA-4.0
=============
API Reference
API reference
=============
This page summarizes the rest of the public API. Generally speaking this
should mainly of interest to plugin developers.
should be mainly of interest to plugin developers.
ocrmypdf
========
ocrmypdf.api
============
.. autoclass:: ocrmypdf.PageContext
:members:
.. autoclass:: ocrmypdf.PdfContext
.. automodule:: ocrmypdf.api
:members:
ocrmypdf.exceptions
+33 -34
View File
@@ -21,8 +21,8 @@ processors. To maximize parallelism without overloading your system with
processes, consider using ``parallel -j 2`` to limit parallel to running
two jobs at once.
This command will run all ocrmypdf all files named ``*.pdf`` in the
current directory and write them to the previous created ``output/``
This command will run ``ocrmypdf`` on all files named ``*.pdf`` in the
current directory and write them to the previously created ``output/``
folder. It will not search subdirectories.
The ``--tag`` argument tells parallel to print the filename as a prefix
@@ -44,17 +44,7 @@ place, and printing each filename in between runs:
.. code-block:: bash
find . -printf '%p\n' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
Alternatively, with a Docker container and streaming the file through
standard input and output:
.. code-block:: bash
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
pdfout=$(mktemp)
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
done
find . -name '*.pdf' -printf '%p\n' -exec ocrmypdf '{}' '{}' \;
This only runs one ``ocrmypdf`` process at a time. This variation uses
``find`` to create a directory list and ``parallel`` to parallelize runs
@@ -70,6 +60,15 @@ In a Windows batch file, use
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
With a Docker container, you will need to stream through standard input and output:
.. code-block:: bash
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
pdfout=$(mktemp)
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
done
Sample script
-------------
@@ -88,9 +87,9 @@ package <https://www.synology.com/en-global/dsm/packages/Docker>`__ is
installed. Attached is a script to address particular quirks of using
OCRmyPDF on one of these devices.
This is only possible for x86-based Synology products. Some Synology
products use ARM or Power processors and do not support Docker. Further
adjustments might be needed to deal with the Synology's relatively
At the time this script was written, it only worked for x86-based Synology
products. It is not known if it will work on ARM-based Synology products.
Further adjustments might be needed to deal with the Synology's relatively
limited CPU and RAM.
.. literalinclude:: ../misc/synology.py
@@ -133,10 +132,10 @@ Users may need to customize the script to meet their requirements.
"OCR_OUTPUT_DIRECTORY", "Set output directory (should not be under input)"
"OCR_ARCHIVE_DIRECTORY", "Set archive directory for processed originals (should not be under input, requires ``OCR_ON_SUCCESS_ARCHIVE`` to be set)"
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
"OCR_ON_SUCCESS_ARCHIVE", "This will move the processed orignal file to ``OCR_ARCHIVE_DIRECTORY`` if the exit code is 0 (OK). Note that ``OCR_ON_SUCCESS_DELETE`` takes precedence over this option, i.e. if both options are set, the input file will be deleted."
"OCR_ON_SUCCESS_ARCHIVE", "This will move the processed original file to ``OCR_ARCHIVE_DIRECTORY`` if the exit code is 0 (OK). Note that ``OCR_ON_SUCCESS_DELETE`` takes precedence over this option, i.e. if both options are set, the input file will be deleted."
"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, ""optimize"": ""3""}'``."
"OCR_POLL_NEW_FILE_SECONDS", "Polling interval"
"OCR_LOGLEVEL", "Level of log messages to report"
@@ -151,32 +150,32 @@ The watcher service is included in the OCRmyPDF Docker image. To run it:
.. code-block:: bash
docker run \
-v <path to files to convert>:/input \
-v <path to store results>:/output \
-v <path to store processed originals>:/archive \
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
-e OCR_ON_SUCCESS_ARCHIVE=1 \
-e OCR_DESKEW=1 \
-e PYTHONUNBUFFERED=1 \
-it --entrypoint python3 \
--volume <path to files to convert>:/input \
--volume <path to store results>:/output \
--volume <path to store processed originals>:/processed \
--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
--env OCR_ON_SUCCESS_ARCHIVE=1 \
--env OCR_DESKEW=1 \
--env PYTHONUNBUFFERED=1 \
--interactive --tty --entrypoint python3 \
jbarlow83/ocrmypdf \
watcher.py
This service will watch for a file that matches ``/input/\*.pdf``,
convert it to a OCRed PDF in ``/output/``, and move the processed
original to ``/archive``. The parameters to this image are:
original to ``/processed``. The parameters to this image are:
.. csv-table:: watcher.py parameters for Docker
:header: "Parameter", "Description"
:widths: 50, 50
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
"``-v <path to store processed originals>:/archive``", "Archive processed originals here"
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable ``OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1`` to place files in the output in ``{output}/{year}/{month}/{filename}``"
"``-e OCR_ON_SUCCESS_ARCHIVE=1``", "Define environment variable ``OCR_ON_SUCCESS_ARCHIVE`` to move processed originals"
"``-e OCR_DESKEW=1``", "Define environment variable ``OCR_DESKEW`` to apply deskew to crooked input PDFs"
"``-e PYTHONBUFFERED=1``", "This will force ``STDOUT`` to be unbuffered and allow you to see messages in docker logs"
"``--volume <path to files to convert>:/input``", "Files placed in this location will be OCRed"
"``--volume <path to store results>:/output``", "This is where OCRed files will be stored"
"``--volume <path to store processed originals>:/processed``", "Archive processed originals here"
"``--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable ``OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1`` to place files in the output in ``{output}/{year}/{month}/{filename}``"
"``--env OCR_ON_SUCCESS_ARCHIVE=1``", "Define environment variable ``OCR_ON_SUCCESS_ARCHIVE`` to move processed originals"
"``--env OCR_DESKEW=1``", "Define environment variable ``OCR_DESKEW`` to apply deskew to crooked input PDFs"
"``--env PYTHONBUFFERED=1``", "This will force ``STDOUT`` to be unbuffered and allow you to see messages in docker logs"
This service relies on polling to check for changes to the filesystem. It
may not be suitable for some environments, such as filesystems shared on a
+92
View File
@@ -0,0 +1,92 @@
.. SPDX-FileCopyrightText: 2023 James R. Barlow
..
.. SPDX-License-Identifier: CC-BY-SA-4.0
.. _ocr-service:
==================
Online deployments
==================
OCRmyPDF is designed to be used as a command line tool, but it can be
used in a web service. This document describes some considerations for
doing so.
A basic web service implementation is provided in the source code
repository, as ``misc/webservice.py``. It is only demonstration quality
and is not intended for production use.
OCRmyPDF is not designed for use as a public web service where a
malicious user could upload a chosen PDF. In particular, it is not
necessarily secure against PDF malware or PDFs that cause denial of
service. For further discussino of security, see :ref:`security`.
OCRmyPDF relies on Ghostscript, and therefore, if deployed
online one should be prepared to comply with Ghostscript's Affero GPL
license, and any other licenses.
Setting aside these concerns, a side effect of OCRmyPDF is that it may
incidentally sanitize PDFs containing certain types of malware. It
repairs the PDF with pikepdf/libqpdf, which could correct malformed PDF
structures that are part of an attack. When PDF/A output is selected
(the default), the input PDF is partially reconstructed by Ghostscript.
When ``--force-ocr`` is used, all pages are rasterized and reconverted
to PDF, which could remove malware in embedded images.
Limiting CPU usage
------------------
OCRmyPDF will attempt to use all available CPUs and storage, so
executing ``nice ocrmypdf`` or limiting the number of jobs with the
``--jobs`` argument may ensure the server remains responsive. Another option
would be to run OCRmyPDF jobs inside a Docker container, a virtual machine,
or a cloud instance, which can impose its own limits on CPU usage and be
terminated "from orbit" if it fails to complete.
Temporary storage requirements
------------------------------
OCRmyPDF will use a large amount of temporary storage for its work,
proportional to the total number of pixels needed to rasterize the PDF.
The raster image of a 8.5×11" color page at 300 DPI takes 25 MB
uncompressed; OCRmyPDF saves its intermediates as PNG, but that still
means it requires about 9 MB per intermediate based on average
compression ratios. Multiple intermediates per page are also required,
depending on the command line given. A rule of thumb would be to allow
100 MB of temporary storage per page in a file meaning that a small
cloud servers or small VM partitions should be provisioned with plenty
of extra space, if say, a 500 page file might be sent.
To change the temporary directory, see :ref:`tmpdir`.
On Amazon Web Services or other cloud vendors, consider setting your
temporary directory to `empheral
storage <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html>`__.
Timeouts
--------
To prevent excessively long OCR jobs consider setting
``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big``
is particularly helpful if your PDFs include documents such as reports
on standard page sizes with large images attached - often large images
are not worth OCR'ing anyway.
Document management systems
---------------------------
If you are looking for a full document management system, consider
`paperless-ngx <https://github.com/paperless-ngx/paperless-ngx>`__,
which is a web application that uses OCRmyPDF to automatically OCR and
archive documents.
Commercial OCR alternatives
---------------------------
The author also provides professional services that include OCR and
building databases around PDFs, and is happy to provide consultation.
Abbyy Cloud OCR is viable commercial alternative with a web services
API. Amazon Textract, Google Cloud Vision, and Microsoft Azure
Computer Vision provide advanced OCR but have less PDF rendering capability.
+17 -17
View File
@@ -2,6 +2,8 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: CC-BY-SA-4.0
# ruff: noqa: E402
# ocrmypdf documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 4 14:29:43 2016.
#
@@ -22,14 +24,14 @@
# import sys
# sys.path.insert(0, os.path.abspath('.'))
"""isort:skip_file"""
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
import datetime
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
@@ -38,11 +40,12 @@ extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.autosummary',
'sphinx.ext.napoleon',
'sphinx.ext.imgconverter', # PDF docs needs this for SVG to PNG conversion
'sphinx_issues',
]
# Extension settings
intersphinx_mapping = {'https://docs.python.org/': None}
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
napoleon_use_rtype = False
issues_github_path = "ocrmypdf/OCRmyPDF"
@@ -50,10 +53,7 @@ issues_github_path = "ocrmypdf/OCRmyPDF"
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = {'.rst': 'restructuredtext', '.md': 'markdown'}
# The encoding of source files.
#
@@ -64,8 +64,11 @@ master_doc = 'index'
# General information about the project.
project = 'ocrmypdf'
year = str(datetime.date.today().year)
copyright = (
'2022, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
f'{year}, James R. Barlow. ',
'Licensed under Creative Commons Attribution-ShareAlike 4.0.',
)
author = 'James R. Barlow'
@@ -76,6 +79,7 @@ author = 'James R. Barlow'
# The short X.Y version.
import os
from importlib.metadata import version as package_version
on_rtd = os.environ.get('READTHEDOCS') == 'True'
@@ -91,15 +95,12 @@ if on_rtd:
MOCK_MODULES = [
'pikepdf',
'pikepdf.canvas',
'pikepdf.models',
'pikepdf.models.metadata',
]
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
try:
from importlib_metadata import version as package_version
except ModuleNotFoundError:
from importlib.metadata import version as package_version
# The full version, including alpha/beta/rc tags.
release = package_version('ocrmypdf')
@@ -111,7 +112,7 @@ version = '.'.join(release.split('.')[:2])
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -161,19 +162,18 @@ todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
import sphinx_rtd_theme
import sphinx_rtd_theme # noqa: F401
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {'display_version': False}
html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
@@ -201,7 +201,7 @@ html_theme_options = {'display_version': False}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
+27 -10
View File
@@ -18,8 +18,8 @@ work you're contemplating is already half-done in a development branch.
Code style
==========
We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The
settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull
We use PEP8, ``black`` for code formatting and ``ruff`` for everything else. The
settings for these programs are in ``pyproject.toml``. Pull
requests should follow the style guide. One difference we use from "black" style
is that strings shown to the user are always in double quotes (``"``) and strings
for internal uses are in single quotes (``'``).
@@ -29,12 +29,17 @@ Tests
New features should come with tests that confirm their correctness.
New Python dependencies
=======================
New dependencies
================
If you are proposing a change that will require a new Python dependency, we
If you are proposing a change that will require a new dependency, we
prefer dependencies that are already packaged by Debian or Red Hat. This makes
life much easier for our downstream package maintainers.
life much easier for our downstream package maintainers. A package that is only
available on PyPI or GitHub, and not more widely packaged, may not be accepted.
We are unlikely to accept a dependency on CUDA or other GPU-based libraries,
because these are still difficult to package and install on many systems.
We recommend implementing these changes as plugins.
Python dependencies must also be license-compatible. GPLv3 or AGPLv3 are likely
incompatible with the project's license, but LGPLv3 is compatible.
@@ -43,7 +48,19 @@ New non-Python dependencies
===========================
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
its functionality. In general we prefer to avoid adding new external programs.
its functionality. In general we prefer to avoid adding new external programs,
and if we are to add external programs, we prefer those that are already
packaged by Debian or Red Hat.
Plugins
=======
Some new features may be a good fit for a plugin. Plugins are a way to add
features to OCRmyPDF without adding them to the core program. Plugins are
installed separately from OCRmyPDF. They are written in Python and can be
installed from PyPI. See the `plugin documentation <https://ocrmypdf.readthedocs.io/en/latest/plugins.html>`_.
We are happy to link users to your plugin from the documentation.
Style guide: Is it OCRmyPDF or ocrmypdf?
========================================
@@ -53,8 +70,8 @@ The program/project is OCRmyPDF and the name of the executable or library is ocr
Copyright and license
=====================
For contributions over 10 lines of code, please include your name to list of
For contributions over 10 lines of code, please add your name to list of
copyright holders for that file. The core program is licensed under MPL-2.0,
test files and documentation under CC-BY-SA 4.0, and miscellaneous files under
MIT. Please contribute code only that you wrote and you have the permission to
contribute or license to us.
MIT, with a few minor exceptions. Please contribute only content that you own
or have the right to contribute under these licenses.
+43 -7
View File
@@ -187,7 +187,7 @@ might remove desirable content, especially from poor quality scans.
background from grayscale or color images. Monochrome images are
ignored. This should not be used on documents that contain color
photos as it may remove them.
- ``--deskew`` will correct pages were scanned at a skewed angle by
- ``--deskew`` will correct pages that were scanned at a skewed angle by
rotating them back into place.
- ``--clean`` uses
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
@@ -231,13 +231,34 @@ Don't actually OCR my PDF
=========================
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image
processing without performing OCR, if all you want to is to apply image
processing or PDF/A conversion.
processing without performing OCR (by causing OCR to time out). This works
if all you want to is to apply image processing or PDF/A conversion.
.. code-block:: bash
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
.. versionchanged:: v14.1.0
Prior to this version, ``--tesseract-timeout 0`` would prevent other
uses of Tesseract, such as deskewing, from working. This is no longer
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
for non-OCR operations, if needed.
Remove all text or OCR from my PDF
----------------------------------
This is getting ridiculous, but OCRmyPDF can complete strip all textual
information from a PDF and reconstruct it as a "bag of images" PDF.
.. code-block::
ocrmypdf --tesseract-timeout 0 --force-ocr input.pdf output.pdf
Why would you want to do this? Perhaps you have a PDF where OCR
fails to produce useful results, and just want to get rid of all OCR information.
This command also removes OCR generated by third party tools.
Optimize images without performing OCR
--------------------------------------
@@ -261,9 +282,10 @@ Hyphens denote a range of pages and commas separate page numbers. If you prefer
to use spaces, quote all of the page numbers: ``--pages '2, 3, 5, 7'``.
OCRmyPDF will warn if your list of page numbers contains duplicates or
overlap pages. OCRmyPDF does not currently account for document page numbers,
overlapping pages. OCRmyPDF does not currently account for document page numbers,
such as an introduction section of a book that uses Roman numerals. It simply
counts the number of virtual pieces of paper since the start.
counts the number of virtual pieces of paper since the start. If your list of
pages is out of numerical order, OCRmyPDF will sort it for you.
Regardless of the argument to ``--pages``, OCRmyPDF will optimize all pages/images
in the file and convert it to PDF/A, unless you disable those options. Both of these
@@ -283,7 +305,7 @@ argument. (Normally, OCRmyPDF will exit with an error if asked to modify
a file with OCR.)
This may be helpful for users who want to take advantage of accuracy
improvements in Tesseract 4.0 for files they previously OCRed with an
improvements in Tesseract for files they previously OCRed with an
earlier version of Tesseract and OCRmyPDF.
.. code-block:: bash
@@ -360,7 +382,7 @@ The types of optimization available may expand over time. By default,
OCRmyPDF compresses data streams inside PDFs, and will change
inefficient compression modes to more modern versions. A program like
``qpdf`` can be used to change encodings, e.g. to inspect the internals
fo a PDF.
for a PDF.
.. code-block:: bash
@@ -372,3 +394,17 @@ Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
Image processing and PDF/A conversion can also introduce lossy transformations
to your PDF images, even when ``--optimize 1`` is in use.
Digitally signed PDFs
=====================
OCRmyPDF cannot preserve digital signatures in PDFs and also add OCR to them.
By default, it will refuse to modify a signed PDF regardless of other settings. You can
override this behavior with ``--invalidate-digital-signatures``; as the name suggests,
any digital signatures will be invalidated.
OCRmyPDF cannot open documents that are encrypted with a digital certificate.
Versions of OCRmyPDF prior to 14.4.0 would invalidate existing digital signatures
without warning.
+32
View File
@@ -0,0 +1,32 @@
.. SPDX-FileCopyrightText: 2023 James R. Barlow
.. SPDX-License-Identifier: CC-BY-SA-4.0
============
Design notes
============
Why doesn't OCRmyPDF use PyTesseract?
=====================================
PyTesseract is a Python wrapper around the Tesseract OCR engine. When OCRmyPDF was
first written, PyTesseract used ABI bindings to call the Tesseract library. This
was not a good fit for OCRmyPDF because ABI bindings can be fragile.
PyTesseract has since evolved calling the Tesseract executable, abandoning the ABI
approach and using the CLI instead, just like OCRmyPDF does. If it were written from
scratch today, OCRmyPDF might use PyTesseract.
PyTesseract has more features don't particularly need PDF output, but less features
than OCRmyPDF's API for creating PDFs.
What is ``executor()``?
=======================
OCRmyPDF uses a custom concurrent executor which can support either threads or
processes with the same interface. This is useful because OCRmyPDF can use
either threads or processes to parallelize work, whichever is more appropriate
for the task at hand.
The interface is currently private and subject to change. In particular, if
experiments with asyncio and anyio are successful, the interface will change.
+89 -35
View File
@@ -8,17 +8,16 @@
OCRmyPDF Docker image
=====================
OCRmyPDF is also available in a Docker image that packages recent
OCRmyPDF is also available in Docker images that packages recent
versions of all dependencies.
For users who already have Docker installed this may be an easy and
convenient option. However, it is less performant than a system
installation and may require Docker engine configuration.
convenient option.
OCRmyPDF needs a generous amount of RAM, CPU cores, temporary storage
space, whether running in a Docker container or on its own. It may be
necessary to ensure the container is provisioned with additional
resources.
On platforms other than Linux, Docker runs in a virtual machine, and so may
be less performant. You may also want to adjust the Docker virtual machine's
memory and CPU allocation. On Linux, the Docker image runs natively and
performance is comparable to a system installation.
.. _docker-install:
@@ -35,29 +34,44 @@ execute the image:
docker run hello-world
The recommended OCRmyPDF Docker image is currently named ``ocrmypdf``:
.. list-table:: Docker images
:widths: 30 20 50
:header-rows: 1
* - Image
- Architecture
- Description
* - ``jbarlow83/ocrmypdf-alpine``
- x86_64 and arm64
- Recommended image, based on Alpine Linux.
* - ``jbarlow83/ocrmypdf-ubuntu``
- x86_64 and arm64
- Alternate image, based on Ubuntu. When the Alpine image is considered
stable and available for arm64, this image will be deprecated.
* - ``jbarlow83/ocrmypdf``
- x86_64 and arm64
- Currently an alias for ocrmypdf-ubuntu. When the Alpine image is
considered stable and available for arm64, this name point to the
Alpine image. If you don't about the difference between Alpine and
Ubuntu, use this image.
To install:
.. code-block:: bash
docker pull jbarlow83/ocrmypdf
docker pull jbarlow83/ocrmypdf-alpine
The ``ocrmypdf`` image is also available, but is deprecated and will be removed
in the future.
OCRmyPDF will use all available CPU cores. By default, the VirtualBox
machine instance on Windows and macOS has only a single CPU core
enabled. Use the VirtualBox Manager to determine the name of your Docker
engine host, and then follow these optional steps to enable multiple
CPUs:
OCRmyPDF will use all available CPU cores. See the Docker documentation for
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__
if you are using Docker on macOS or Windows, where you may need to manually assign
more resources. On Linux, all resources will be available automatically.
.. code-block:: bash
# Optional step for Mac OS X users
docker-machine stop "yourVM"
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
docker-machine start "yourVM"
eval $(docker-machine env "yourVM")
See the Docker documentation for
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__.
The underlying operating system and other details in Docker images are considered
implementation details and **subject to change at minor releases**. If you are
modifying the image, you should pin the version you intend to use.
Using the Docker image on the command line
==========================================
@@ -66,13 +80,14 @@ Using the Docker image on the command line
container is ephemeral it runs for one OCR job and terminates, just like a
command line program. We are using Docker to deliver an application (as opposed
to the more conventional case, where a Docker container runs as a server).
For that reason we usually use the ``--rm`` argument to delete the container
when it exits.
To start a Docker container (instance of the image):
.. code-block:: bash
docker tag jbarlow83/ocrmypdf ocrmypdf
docker run --rm -i ocrmypdf (... all other arguments here...) - -
docker run --rm -i jbarlow83/ocrmypdf-alpine (... all other arguments here...) - -
For convenience, create a shell alias to hide the Docker command. It is
easier to send the input file as stdin and read the output from
@@ -80,7 +95,7 @@ stdout **this avoids the messy permission issues with Docker entirely**.
.. code-block:: bash
alias docker_ocrmypdf='docker run --rm -i ocrmypdf'
alias docker_ocrmypdf='docker run --rm -i jbarlow83/ocrmypdf-alpine'
docker_ocrmypdf --version # runs docker version
docker_ocrmypdf - - <input.pdf >output.pdf
@@ -88,7 +103,7 @@ Or in the wonderful `fish shell <https://fishshell.com/>`__:
.. code-block:: fish
alias docker_ocrmypdf 'docker run --rm ocrmypdf'
alias docker_ocrmypdf 'docker run --rm jbarlow83/ocrmypdf-alpine'
funcsave docker_ocrmypdf
Alternately, you could mount the local current working directory as a
@@ -96,9 +111,27 @@ Docker volume:
.. code-block:: bash
alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" --workdir /data -v "$PWD:/data" ocrmypdf'
alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" --workdir /data -v "$PWD:/data" jbarlow83/ocrmypdf-alpine'
docker_ocrmypdf /data/input.pdf /data/output.pdf
Podman
======
Especially if you use `Podman <https://podman.io/>`__ (or have SELinux enabled on your
system), you may need to add ``--userns keep-id`` there, otherwise you may get access
errors, because the user is otherwise not mapped to the same UID as on the host:
.. code-block:: bash
alias podman_ocrmypdf='podman run --rm -i --user "$(id -u):$(id -g)" --userns keep-id --workdir /data -v "$PWD:/data" ocrmypdf'
podman_ocrmypdf /data/input.pdf /data/output.pdf
If you use SELinux you may additionally need to add the ``:Z`` `suffix to the volume
<https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options>`__
or disable SELinux for the container using ``--security-opt label=disable``, which is
suggested for system files as they should not be re-labelled. Please refer to the „Note”
section at the end of the linked podman documentation for details.
.. _docker-lang-packs:
Adding languages to the Docker image
@@ -116,7 +149,7 @@ Dockerfile based on the public one.
# Example: add Italian
RUN apt install tesseract-ocr-ita
To install language packs (training data) such as the
To install language packs (training data) such as the
`tessdata_best <https://github.com/tesseract-ocr/tessdata_best>`_ suite or
custom data, you first need to determine the version of Tesseract data files, which
may differ from the Tesseract program version. Use this command to determine the data
@@ -132,17 +165,35 @@ You can then add new data with either a Dockerfile:
.. code-block:: dockerfile
FROM jbarlow83/ocrmypdf
FROM jbarlow83/ocrmypdf:{TAG}
# Example: add a tessdata_best file
COPY chi_tra_vert.traineddata /usr/share/tesseract-ocr/<data version>/tessdata/
When creating your own image, you should always pin a specific version of the
OCRmyPDF Docker image. This ensures that your image will not break when a new
version of OCRmyPDF is released.
Alternately, you can copy training data into a Docker container as follows:
.. code-block:: bash
docker cp mycustomtraining.traineddata name_of_container:/usr/share/tesseract-ocr/<tesseract version>/tessdata/
Extending the Docker image
==========================
You can extend the Docker image with your own customizations, similar to the way
it is extended to add language packs.
Note that the Docker image is subject to change at any time. For example, the base
image may be updated to a newer version of Ubuntu or Debian. Such changes will be
noted in the release notes but might occur at minor versions releases, unless the
way a "casual" user of the Docker image is affected.
If you extend the Docker image, you should pin a specific version of the OCRmyPDF
Docker image.
Executing the test suite
========================
@@ -150,16 +201,16 @@ The OCRmyPDF test suite is installed with image. To run it:
.. code-block:: bash
docker run --entrypoint python3 jbarlow83/ocrmypdf -m pytest
docker run --rm --entrypoint python jbarlow83/ocrmypdf -m pytest
Accessing the shell
===================
To use the bash shell in the Docker image:
To use the shell in the Docker image:
.. code-block:: bash
docker run -it --entrypoint bash jbarlow83/ocrmypdf
docker run -it --entrypoint sh jbarlow83/ocrmypdf
Using the OCRmyPDF web service wrapper
======================================
@@ -169,7 +220,10 @@ service. The webservice may be launched as follows:
.. code-block:: bash
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf webservice.py
docker run --entrypoint python -p 5000:5000 jbarlow83/ocrmypdf webservice.py
We omit the ``--rm`` parameter so that the container will not be
automatically deleted when it exits.
This will configure the machine to listen on port 5000. On Linux machines
this is port 5000 of localhost. On macOS or Windows machines running
+4 -2
View File
@@ -12,7 +12,7 @@ files, allowing them to be searched.
PDF is the best format for storing and exchanging scanned documents.
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
image processing and OCR to existing PDFs.
image processing and OCR (recognized, searchable text) to existing PDFs.
.. toctree::
:maxdepth: 1
@@ -20,7 +20,6 @@ image processing and OCR to existing PDFs.
introduction
release_notes
installation
optimizer
languages
jbig2
@@ -29,9 +28,11 @@ image processing and OCR to existing PDFs.
:maxdepth: 2
cookbook
optimizer
docker
advanced
batch
cloud
performance
pdfsecurity
errors
@@ -43,6 +44,7 @@ image processing and OCR to existing PDFs.
api
plugins
apiref
design_notes
contributing
maintainers
+172 -162
View File
@@ -21,16 +21,16 @@ These platforms have one-liner installs:
+-------------------------------+-----------------------------------------+
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| Fedora | ``dnf install ocrmypdf`` |
| Fedora | ``dnf install ocrmypdf tesseract-osd`` |
+-------------------------------+-----------------------------------------+
| macOS | ``brew install ocrmypdf`` |
| macOS (Homebrew) | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| macOS (MacPorts) | ``port install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| LinuxBrew | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| FreeBSD | ``pkg install textproc/py-ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| Conda (WSL, macOS, Linux) | ``conda install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| Snap (snapcraft packaging) | ``snap install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
@@ -44,7 +44,7 @@ install, or install a more recent version than your platform provides, read on.
Installing on Linux
===================
Debian and Ubuntu 18.04 or newer
Debian and Ubuntu 20.04 or newer
--------------------------------
.. |deb-11| image:: https://repology.org/badge/version-for-repo/debian_11/ocrmypdf.svg
@@ -56,9 +56,6 @@ Debian and Ubuntu 18.04 or newer
.. |deb-unstable| image:: https://repology.org/badge/version-for-repo/debian_unstable/ocrmypdf.svg
:alt: Debian unstable
.. |ubu-1804| image:: https://repology.org/badge/version-for-repo/ubuntu_18_04/ocrmypdf.svg
:alt: Ubuntu 18.04 LTS
.. |ubu-2004| image:: https://repology.org/badge/version-for-repo/ubuntu_20_04/ocrmypdf.svg
:alt: Ubuntu 20.04 LTS
@@ -72,15 +69,14 @@ Debian and Ubuntu 18.04 or newer
+-----------------------------------------------+
| |deb-11| |deb-12| |deb-unstable| |
+-----------------------------------------------+
| |ubu-1804| |ubu-2004| |ubu-2204| |
| |ubu-2004| |ubu-2204| |
+-----------------------------------------------+
Users of Debian 9 ("stretch") or later, or Ubuntu 18.04 or later, including users
of Windows Subsystem for Linux, may simply
Users of Debian or Ubuntu may simply
.. code-block:: bash
apt-get install ocrmypdf
apt install ocrmypdf
As indicated in the table above, Debian and Ubuntu releases may lag
behind the latest version. If the version available for your platform is
@@ -103,11 +99,11 @@ For full details on version availability for your platform, check the
Fedora
------
.. |fedora-35| image:: https://repology.org/badge/version-for-repo/fedora_35/ocrmypdf.svg
:alt: Fedora 35
.. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
:alt: Fedora 38
.. |fedora-36| image:: https://repology.org/badge/version-for-repo/fedora_36/ocrmypdf.svg
:alt: Fedora 36
.. |fedora-39| image:: https://repology.org/badge/version-for-repo/fedora_39/ocrmypdf.svg
:alt: Fedora 39
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
@@ -117,17 +113,17 @@ Fedora
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |fedora-35| |fedora-36| |fedora-rawhide| |
| |fedora-38| |fedora-39| |fedora-rawhide| |
+-----------------------------------------------+
Users of Fedora 29 or later may simply
Users of Fedora may simply
.. code-block:: bash
dnf install ocrmypdf
dnf install ocrmypdf tesseract-osd
For full details on version availability, check the `Fedora Package
Tracker <https://apps.fedoraproject.org/packages/ocrmypdf>`__.
Tracker <https://packages.fedoraproject.org/pkgs/ocrmypdf/ocrmypdf/>`__.
If the version available for your platform is out of date, you could opt
to install the latest version from source. See `Installing HEAD revision
@@ -139,10 +135,36 @@ from sources <#installing-head-revision-from-sources>`__.
issues. OCRmyPDF works fine without it but will produce larger output
files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later
will automatically detect it on the ``PATH``. To add JBIG2 encoding,
see `Installing the JBIG2 encoder <jbig2>`__.
see :ref:`Installing the JBIG2 encoder <jbig2>`.
.. _ubuntu-lts-latest:
RHEL 9
------
Prepare the environment by getting Python 3.11:
.. code-block:: bash
dnf install python3.11 python3.11-pip
Then, follow `Requirements for pip and HEAD install <#requirements-for-pip-and-head-install>`__ to instal dependencies:
.. code-block:: bash
dnf install ghostscript tesseract
and build ocrmypdf in virtual environment:
.. code-block:: bash
python3.11 -m venv .venv
To add JBIG2 encoding, see :ref:`Installing the JBIG2 encoder <jbig2>`.
Note Fedora packages for language data haven't been branched for RHEL/EPEL, but you can get traineddata files directly from `tesseract
<https://github.com/tesseract-ocr/tessdata/>`__ and place them in ``/usr/share/tesseract/tessdata``.
Installing the latest version on Ubuntu 22.04 LTS
-------------------------------------------------
@@ -159,84 +181,20 @@ user, follow these steps:
If you get the message ``WARNING: The script ocrmypdf is installed in
'/home/$USER/.local/bin' which is not on PATH.``, you may need to re-login
or open a new shell, or manually add this to your user's PATH.
or open a new shell, or manually adjust your PATH.
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 20.04 LTS
----------------
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To
install a more recent version, uninstall the system-provided version of
ocrmypdf, and install the following dependencies:
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. The
most convenient way to install recent OCRmyPDF on older Ubuntu is to use
Homebrew on Linux (Linuxbrew).
.. code-block:: bash
sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed
sudo apt-get -y update
sudo apt-get -y install \
ghostscript \
icc-profiles-free \
libxml2 \
pngquant \
python3-pip \
tesseract-ocr \
zlib1g
To install ocrmypdf for the system:
.. code-block:: bash
pip3 install ocrmypdf
To install for the current user only:
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 18.04 LTS
----------------
Ubuntu 18.04 includes ocrmypdf 6.1.2 - you can install that with ``apt``, but
it is quite old now. To install a more recent version, uninstall the old version
of ocrmypdf, and install the following dependencies:
.. code-block:: bash
sudo apt-get -y remove ocrmypdf
sudo apt-get -y update
sudo apt-get -y install \
ghostscript \
icc-profiles-free \
libxml2 \
pngquant \
python3-distutils \
python3-pkg-resources \
python3-reportlab \
qpdf \
tesseract-ocr \
zlib1g \
unpaper
We will need a newer version of ``pip`` then was available for Ubuntu 18.04:
.. code-block:: bash
wget https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py
Then install the most recent ocrmypdf for the local user and set the
user's ``PATH`` to check for the user's Python packages.
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
python3 -m pip install --user ocrmypdf
To add JBIG2 encoding, see :ref:`jbig2`.
brew install ocrmypdf
Arch Linux (AUR)
----------------
@@ -259,12 +217,12 @@ you are using a VM image, such as `the official Vagrant image
be completed for you.
Next you should install the `base-devel package group
<https://www.archlinux.org/groups/x86_64/base-devel/>`__. This includes the
<https://archlinux.org/packages/core/any/base-devel/>`__. This includes the
standard tooling needed to build packages, such as a compiler and binary tools.
.. code-block:: bash
sudo pacman -S base-devel
sudo pacman -S --needed base-devel
Now you are ready to install the OCRmyPDF package.
@@ -302,7 +260,7 @@ page.
<https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed
using the same series of steps as for the installation OCRmyPDF AUR
package. Alternatively, it may be built manually from source following the
instructions in `Installing the JBIG2 encoder <jbig2>`__. If JBIG2 is
instructions in :ref:`Installing the JBIG2 encoder <jbig2>`. If JBIG2 is
installed, OCRmyPDF 7.0.0 and later will automatically detect it.
Alpine Linux
@@ -318,6 +276,21 @@ To install OCRmyPDF for Alpine Linux:
apk add ocrmypdf
Gentoo Linux
------------
.. image:: https://repology.org/badge/version-for-repo/gentoo_ovl_guru/ocrmypdf.svg
:alt: Gentoo Linux
:target: https://repology.org/metapackage/ocrmypdf
To install OCRmyPDF on Gentoo Linux, use the following commands:
.. code-block:: bash
eselect repository enable guru
emaint sync --repo guru
emerge --ask app-text/OCRmyPDF
Other Linux packages
--------------------
@@ -336,7 +309,7 @@ Homebrew
.. image:: https://img.shields.io/homebrew/v/ocrmypdf.svg
:alt: homebrew
:target: http://brewformulas.org/Ocrmypdf
:target: https://formulae.brew.sh/formula/ocrmypdf
OCRmyPDF is now a standard `Homebrew <https://brew.sh>`__ formula. To
install on macOS:
@@ -352,6 +325,22 @@ languages you can optionally install them all:
brew install tesseract-lang # Optional: Install all language packs
MacPorts
--------
.. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fports.macports.org%2Fapi%2Fv1%2Fports%2Focrmypdf%2F%3Fformat%3Djson&query=version&label=MacPorts
:alt: Macports Version Information
:target: https://ports.macports.org/port/ocrmypdf
OCRmyPDF is includes in MacPorts:
.. code-block:: bash
sudo port install ocrmypdf
Note that while this will install tesseract you will need to install
the appropriate tesseract `language ports <https://ports.macports.org/search/?selected_facets=categories_exact%3Atextproc&installed_file=&q=tesseract&name=on>`__.
Manual installation on macOS
----------------------------
@@ -387,18 +376,12 @@ Update the homebrew pip:
pip install --upgrade pip
You can then install OCRmyPDF from PyPI, for the current user:
You can then install OCRmyPDF from PyPI for the current user:
.. code-block:: bash
pip install --user ocrmypdf
or system-wide:
.. code-block:: bash
pip install ocrmypdf
The command line program should now be available:
.. code-block:: bash
@@ -411,37 +394,64 @@ Installing on Windows
Native Windows
--------------
..
If you have a Windows that is not the Home edition, you can use Windows Sandbox to test on a blank Windows instance.
https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/
.. note::
Administrator privileges will be required for some of these steps.
You must install the following for Windows:
* Python 3.7 (64-bit) or later
* Tesseract 4.0 or later
* Ghostscript 9.50 or later
* Python 64-bit
* Tesseract 64-bit
* Ghostscript 64-bit
Using the `Chocolatey <https://chocolatey.org/>`_ package manager, install the
following when running in an Administrator command prompt:
Using the `winget <https://docs.microsoft.com/en-us/windows/package-manager/winget/>`_
package manager:
* ``winget install -e --id Python.Python.3.11``
* ``winget install -e --id UB-Mannheim.TesseractOCR``
You will need to install Ghostscript manually, `since it does not support automated
installs anymore <https://artifex.com/news/ghostscript-10.01.0-disabling-silent-install-option>`_.
* `Ghostscript download page <https://ghostscript.com/releases/gsdnld.html>`_.`
(Or alternately, 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)
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.
Either set of commands will install the required software. At the moment there is no
single command to install Windows.
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
Administrator.):
* ``pip install ocrmypdf``
* ``python3 -m 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.
..
The Windows Python versions do not place any python or python3 executable in the path.
They add the py launcher to the path:
https://docs.python.org/3/using/windows.html#python-launcher-for-windows
If you installed Python using WinGet, then use the following command instead:
* ``py -m pip install ocrmypdf``
and use:
* ``py -m ocrmypdf``
To start OCRmyPDF.
If you intend to use more Python software on your Windows machine, consider the use of
`pipx <https://pipx.pypa.io/stable/>`_ or a similar tool to create isolated Python
environments for each Python software that you want to use.
OCRmyPDF will check the Windows Registry and standard locations in your Program Files
for third party software it needs (specifically, Tesseract and Ghostscript). To
@@ -453,9 +463,13 @@ to change the PATH.
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
Please download Python from Python.org or a package manager instead of the
Microsoft Store version.
.. warning::
32-bit Windows is not supported.
Windows Subsystem for Linux
---------------------------
@@ -481,7 +495,7 @@ Cygwin64
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
python37 (or later)
python310 (or later)
python3?-devel
python3?-pip
python3?-lxml
@@ -490,7 +504,7 @@ First install the the following prerequisite Cygwin packages using ``setup-x86_6
(where 3? means match the version of python3 you installed)
gcc-g++
ghostscript (<=9.50 or >=9.52-2 see note below)
ghostscript
libexempi3
libexempi-devel
libffi6
@@ -501,13 +515,6 @@ First install the the following prerequisite Cygwin packages using ``setup-x86_6
tesseract-ocr
tesseract-ocr-devel
.. note::
The Cygwin package for Ghostscript in versions 9.52 and
9.52-1 contained a bug that caused an exception to occur when
ocrmypdf invoked gs. Make sure you have either 9.50 (or earlier)
or 9.52-2 (or later).
Then open a Cygwin terminal (i.e. ``mintty``), run the following commands. Note
that if you are using the version of ``pip`` that was installed with the Cygwin
Python package, the command name will be ``pip3``. If you have since updated
@@ -559,21 +566,6 @@ the latest version. However, PyPI and ``pip`` cannot address the fact
that ``ocrmypdf`` depends on certain non-Python system libraries and
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
``ocrmypdf``, using the instructions elsewhere in this document. Then
@@ -589,23 +581,24 @@ try:
pip install --user ocrmypdf
(If the message appears ``Requirement already satisfied: ocrmypdf in...``,
you will need to use ``pip install --user --upgrade ocrmypdf``.)
You should then be able to run ``ocrmypdf --version`` and see that the
latest version was located.
Since ``pip install --user`` does not work correctly on some platforms,
notably Ubuntu 16.04 and older, and the Homebrew version of Python,
instead use this for a system wide installation:
Installing with pipx
====================
Some users may prefer pipx. As with the method above, you will need to
satisfy all non-Python dependencies. Then if pipx is installed, you
can use
.. code-block:: bash
pip install ocrmypdf
pipx run ocrmypdf
.. note::
AArch64 (ARM64) users: this process will be difficult because most
Python packages are not available as binary wheels for your platform.
You're probably better off using a platform install on Debian, Ubuntu,
or Fedora.
(If not installed, pipx will install first.)
Requirements for pip and HEAD install
-------------------------------------
@@ -616,13 +609,16 @@ manager. ``pip`` cannot provide them.
The following versions are required:
- Python 3.7 or newer
- Ghostscript 9.23 or newer
- Tesseract 4.0.0 or newer
- Python 3.10 or newer
- Ghostscript 9.54 or newer
- Tesseract 4.1.1 or newer
- jbig2enc 0.29 or newer
- pngquant 2.5 or newer
- unpaper 6.1
We recommend 64-bit versions of all software. (32-bit versions are not
supported, although on Linux, they may still work.)
jbig2enc, pngquant, and unpaper are optional. If missing certain
features are disabled. OCRmyPDF will discover them as soon as they are
available.
@@ -649,7 +645,7 @@ unfortunately, the ``pip install`` command cannot satisfy all of them.
Installing HEAD revision from sources
=====================================
If you have ``git`` and Python 3.7 or newer installed, you can install
If you have ``git`` and Python 3.10 or newer installed, you can install
from source. When the ``pip`` installer runs, it will alert you if
dependencies are missing.
@@ -665,8 +661,7 @@ environment:
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
Or, to install in `development
mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
Or, to install in editable mode
allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash
@@ -678,9 +673,9 @@ system-wide:
.. code-block:: bash
git clone -b master https://github.com/ocrmypdf/OCRmyPDF.git
python3 -m venv
source venv/bin/activate
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
python3 -m venv .venv
source .venv/bin/activate
cd OCRmyPDF
pip install .
@@ -705,9 +700,9 @@ To install all of the development and test requirements:
.. code-block:: bash
git clone -b master https://github.com/ocrmypdf/OCRmyPDF.git
python -m venv
source venv/bin/activate
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
python -m venv .venv
source .venv/bin/activate
cd OCRmyPDF
pip install -e .[test]
@@ -728,3 +723,18 @@ To manually install the ``bash`` completion, copy
To manually install the ``fish`` completion, copy
``misc/completion/ocrmypdf.fish`` to
``~/.config/fish/completions/ocrmypdf.fish``.
Note on 32-bit support
======================
Many Python libraries no longer provide 32-bit binary wheels for Linux. This
includes many of the libraries that OCRmyPDF depends on, such as
Pillow. The easiest way to express this to end users is to say we don't
support 32-bit Linux.
However, if your Linux distribution still supports 32-bit binaries, you
can still install and use OCRmyPDF. A warning message will appear.
In practice, OCRmyPDF may need more than 32-bit memory space to run when
large documents are processed, so there are practical limitations to what
users can accomplish with it. Still, for the common use case of an 32-bit
ARM NAS or Raspberry Pi processing small documents, it should work.
+109 -127
View File
@@ -6,23 +6,23 @@
Introduction
============
OCRmyPDF is an application and library that adds text "layers" to images
in PDFs, making scanned image PDFs searchable. It uses OCR to guess what text
is contained in images. It is written in Python. OCRmyPDF supports plugins
that allow customization of its processing steps, and is very tolerant of
PDFs that contain scanned images and "born digital" content that needs no
text recognition.
OCRmyPDF is a Python application and library that adds text "layers" to images in
PDFs, making scanned image PDFs searchable. It uses OCR to guess the text
contained in images. OCRmyPDF also supports plugins
that enable customization of its processing steps, and it is highly tolerant
of PDFs containing scanned images and "born digital" content that doesn't
require text recognition.
About OCR
=========
`Optical character
recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`__
is technology that converts images of typed or handwritten text, such as
in a scanned document, to computer text that can be selected, searched and copied.
is a technology that converts images of typed or handwritten text, such as
in a scanned document, into computer text that can be selected, searched and copied.
OCRmyPDF uses
`Tesseract <https://github.com/tesseract-ocr/tesseract>`__, the best
`Tesseract <https://github.com/tesseract-ocr/tesseract>`__, a widely
available open source OCR engine, to perform OCR.
.. _raster-vector:
@@ -30,19 +30,19 @@ available open source OCR engine, to perform OCR.
About PDFs
==========
PDFs are page description files that attempts to preserve a layout
PDFs are page description files that attempt to preserve a layout
exactly. They contain `vector
graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`__
that can contain raster objects such as scanned images. Because PDFs can
that can contain raster objects, such as scanned images. Because PDFs can
contain multiple pages (unlike many image formats) and can contain fonts
and text, it is a good format for exchanging scanned documents.
and text, they are a suitable format for exchanging scanned documents.
|image|
A PDF page might contain multiple images, even if it only appears to
have one image. Some scanners or scanning software will segment pages
into monochromatic text and color regions for example, to improve the
compression ratio and appearance of the page.
A PDF page may contain multiple images, even if it appears to have only
one image. Some scanners or scanning software may segment pages into
monochromatic text and color regions, for example, to enhance the compression
ratio and appearance of the page.
Rasterizing a PDF is the process of generating corresponding raster images.
OCR engines like Tesseract work with images, not scalable vector graphics
@@ -54,148 +54,131 @@ About PDF/A
`PDF/A <https://en.wikipedia.org/wiki/PDF/A>`__ is an ISO-standardized
subset of the full PDF specification that is designed for archiving (the
'A' stands for Archive). PDF/A differs from PDF primarily by omitting
features that would make it difficult to read the file in the future,
features that could complicate future file readability,
such as embedded Javascript, video, audio and references to external
fonts. All fonts and resources needed to interpret the PDF must be
contained within it. Because PDF/A disables Javascript and other types
of embedded content, it is probably more secure.
of embedded content, it is likely more secure.
There are various conformance levels and versions, such as "PDF/A-2b".
Generally speaking, the best format for scanned documents is PDF/A. Some
In general, the preferred format for scanned documents is PDF/A. Some
governments and jurisdictions, US Courts in particular, `mandate the use
of PDF/A <https://pdfblog.com/2012/02/13/what-is-pdfa/>`__ for scanned
documents.
Since most people who scan documents are interested in reading them
indefinitely into the future, OCRmyPDF generates PDF/A-2b by default.
Since most individuals scanning documents aim for long-term readability,
OCRmyPDF defaults to generating PDF/A-2b.
PDF/A has a few drawbacks. Some PDF viewers include an alert that the
file is a PDF/A, which may confuse some users. It also tends to produce
larger files than PDF, because it embeds certain resources even if they
are commonly available. PDF/A files can be digitally signed, but may not
be encrypted, to ensure they can be read in the future. Fortunately,
converting from PDF/A to a regular PDF is trivial, and any PDF viewer
can view PDF/A.
PDF/A does have a few drawbacks. Some PDF viewers display an alert
indicating that the file is in PDF/A format, which may confuse some users.
Additionally, it tends to result in larger files than standard PDFs because
it embeds certain resources, even if they are widely available. PDF/A
files can be digitally signed but may not be encrypted to ensure future
readability. Fortunately, converting from PDF/A to a regular PDF is
straightforward, and any PDF viewer can handle PDF/A files.
What OCRmyPDF does
==================
OCRmyPDF analyzes each page of a PDF to determine the colorspace and
resolution (DPI) needed to capture all of the information on that page
without losing content. It uses
`Ghostscript <http://ghostscript.com/>`__ to rasterize the page, and
then performs on OCR the rasterized image to create an OCR "layer".
The layer is then grafted back onto the original PDF.
OCRmyPDF analyzes each page of a PDF to determine the required colorspace
and resolution (DPI) for capturing all the information on that page without
losing content. It uses
`Ghostscript <http://ghostscript.com/>`__ to rasterize each page and subsequently
performs OCR on the rasterized image to generate an OCR "layer." This layer
is then integrated back into the original PDF.
While one can use a program like Ghostscript or ImageMagick to get an
image and put the image through Tesseract, that actually creates a new
PDF and many details may be lost. OCRmyPDF can produce a minimally
changed PDF as output.
While it is possible to use a program like Ghostscript or ImageMagick to
obtain an image and then run that image through Tesseract OCR, this process
actually generates a new PDF, potentially resulting in the loss of various
details (such as the document's metadata). In contrast, OCRmyPDF can produce
a minimally altered PDF as the output.
OCRmyPDF also provides some image processing options, like deskew, which
improves the appearance of files and quality of OCR. When these are used,
the OCR layer is grafted onto the processed image instead.
OCRmyPDF also offers several image processing options, such as deskew, which
enhances the visual quality of files and the accuracy of OCR. When these
options are utilized, the OCR layer is integrated into the processed image.
By default, OCRmyPDF produces archival PDFs PDF/A, which are a
stricter subset of PDF features designed for long term archives. If
regular PDFs are desired, this can be disabled with
``--output-type pdf``.
By default, OCRmyPDF generates archival PDFs in the PDF/A format, which is
a more rigid subset of PDF features designed for long-term archives. If you
prefer regular PDFs, you can disable this feature using the
``--output-type pdf`` option.
Why you shouldn't do this manually
==================================
A PDF is similar to an HTML file, in that it contains document structure
along with images. Sometimes a PDF does nothing more than present a full
page image, but often there is additional content that would be lost.
along with images. While some PDFs may solely display a full-page image,
they often contain additional content that would be forfeited if not preserved.
A manual process could work like either of these:
A manual process could take one of these approaches:
1. Rasterize each page as an image, OCR the images, and combine the
output into a PDF. This preserves the layout of each page, but
resamples all images (possibly losing quality, increasing file size,
introducing compression artifacts, etc.).
2. Extract each image, OCR, and combine the output into a PDF. This
loses the context in which images are used in the PDF, meaning that
cropping, rotation and scaling of pages may be lost. Some scanned
PDFs use multiple images segmented into black and white, grayscale
1. Rasterize each page as an image, perform OCR on the images, and then merge the
output into a PDF. This method preserves the layout of each page, but
resamples all images potentially leading to quality loss, increased file size,
and the introduction of compression artifacts, among other issues.
2. Extract each image, OCR, and combine the output into a PDF. This approach
loses the context in which images are used in the PDF, potentially resulting
in loss of information related to scaling and position of images. Some scanned
PDFs contain multiple images segmented into black and white, grayscale
and color regions, with stencil masks to prevent overlap, as this can
enhance the appearance of a file while reducing file size. Clearly,
reassembling these images will be easy. This also loses and text or
vector art on any pages in a PDF with both scanned and pure digital
content.
enhance the appearance of a file while reducing file size.
Reassembling these images can be challenging, and risks losing vector art
or text that is not part of an image.
In the case of a PDF that is nothing other than a container of images
(no rotation, scaling, cropping, one image per page), the second
approach can be lossless.
In cases where a PDF solely serves as a container for images without any
rotation, scaling, or cropping, the second approach can be lossless.
OCRmyPDF uses several strategies depending on input options and the
input PDF itself, but generally speaking it rasterizes a page for OCR
and then grafts the OCR back onto the original. As such it can handle
complex PDFs and still preserve their contents as much as possible.
OCRmyPDF uses various strategies depending on input options and the input PDF
itself. Generally, it rasterizes a page for OCR and then integrates the OCR
data back into the original PDF. This approach allows it to handle complex
PDFs and preserve their content as much as possible.
OCRmyPDF also supports a many, many edge cases that have cropped over
several years of development. We support PDF features like images inside
of Form XObjects, and pages with UserUnit scaling. We support rare image
formats like non-monochrome 1-bit images. We warn about files you may
not to OCR. Thanks to pikepdf and QPDF, we auto-repair PDFs that are
damaged. (Not that you need to know what any of these are! You should be
able to throw any PDF at it.)
Furthermore, OCRmyPDF supports a wide range of edge cases that have emerged
during several years of development. It accommodates PDF features like
images within Form XObjects and pages with UserUnit scaling. It also
supports less common image formats like non-monochrome 1-bit images and
provides warnings about files you may not want to OCR. Thanks to tools
like pikepdf and QPDF, it can auto-repair damaged PDFs. You don't need to
understand the intricacies of these issues; you should be able to use
OCRmyPDF with any PDF file, and expect reasonable results.
Limitations
===========
OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences
these limitations, as do any other programs that rely on Tesseract:
OCRmyPDF is subject to limitations imposed by the Tesseract OCR engine.
These limitations are inherent to any software relying on Tesseract:
- The OCR is not as accurate as commercial OCR solutions.
- It is not capable of recognizing handwriting.
- It may find gibberish and report this as OCR output.
- If a document contains languages outside of those given in the
``-l LANG`` arguments, results may be poor.
- It is not always good at analyzing the natural reading order of
documents. For example, it may fail to recognize that a document
contains two columns, and may try to join text across columns.
- Poor quality scans may produce poor quality OCR. Garbage in, garbage
out.
- It does not expose information about what font family text belongs
to.
OCRmyPDF is also limited by the PDF specification:
- PDF encodes the position of text glyphs but does not encode document
structure. There is no markup that divides a document in sections,
paragraphs, sentences, or even words (since blank spaces are not
represented). As such all elements of document structure including
the spaces between words must be derived heuristically. Some PDF
viewers do a better job of this than others.
- Because some popular open source PDF viewers have a particularly hard
time with spaces between words, OCRmyPDF appends a space to each text
element as a workaround (when using ``--pdf-renderer hocr``). While
this mixes document structure with graphical information that ideally
should be left to the PDF viewer to interpret, it improves
compatibility with some viewers and does not cause problems for
better ones.
- The OCR accuracy may not match that of commercial OCR solutions.
- It is incapable of recognizing handwriting.
- It may detect gibberish and report it as OCR output.
- Results may be subpar when a document contains languages not specified
in the ``-l LANG`` argument.
- Tesseract may struggle to analyze the natural reading order of documents.
For instance, it might fail to recognize two columns in a document and
attempt to join text across columns.
- Poor quality scans can result in subpar OCR quality. In other words, the
quality of the OCR output depends on the quality of the input.
- Tesseract does not provide information about the font family to which text
belongs.
- Tesseract does not divide text into paragraphs or headings. It only provides
the text and its bounding box. As such, the generated PDF does not
contain any information about the document's structure.
Ghostscript also imposes some limitations:
- PDFs containing JBIG2-encoded content will be converted to CCITT
Group4 encoding, which has lower compression ratios, if Ghostscript
PDF/A is enabled.
- PDFs containing JPEG 2000-encoded content will be converted to JPEG
- PDFs containing JPEG 2000-encoded content may be converted to JPEG
encoding, which may introduce compression artifacts, if Ghostscript
PDF/A is enabled.
- Ghostscript may transcode grayscale and color images, either lossy to
lossless or lossless to lossy, based on an internal algorithm. This
- Ghostscript may transcode grayscale and color images, potentially
lossily, based on an internal algorithm. This
behavior can be suppressed by setting ``--pdfa-image-compression`` to
``jpeg`` or ``lossless`` to set all images to one type or the other.
Ghostscript has no option to maintain the input image's format.
(Ghostscript 9.25+ can copy JPEG images without transcoding them;
earlier versions will transcode.)
Ghostscript lacks an option to maintain the input image's format.
(Modern Ghostscript can copy JPEG images without transcoding them.)
- Ghostscript's PDF/A conversion removes any XMP metadata that is not
one of the standard XMP metadata namespaces for PDFs. In particular,
PRISM Metdata is removed.
- Ghostscript's PDF/A conversion seems to remove or deactivate
PRISM Metadata is removed.
- Ghostscript's PDF/A conversion may remove or deactivate
hyperlinks and other active content.
You can use ``--output-type pdf`` to disable PDF/A conversion and produce
@@ -203,7 +186,7 @@ a standard, non-archival PDF.
Regarding OCRmyPDF itself:
- PDFs that use transparency are not currently represented in the test
- PDFs using transparency are not currently represented in the test
suite
Similar programs
@@ -211,11 +194,7 @@ Similar programs
To the author's knowledge, OCRmyPDF is the most feature-rich and
thoroughly tested command line OCR PDF conversion tool. If it does not
meet your needs, contributions and suggestions are welcome. If not,
consider one of these similar open source programs:
- pdf2pdfocr
- pdfsandwich
meet your needs, contributions and suggestions are welcome.
Ghostscript recently added three "pdfocr" output devices. They work by
rasterizing all content and converting all pages to a single colour space.
@@ -223,16 +202,19 @@ rasterizing all content and converting all pages to a single colour space.
Web front-ends
==============
The Docker image ``ocrmypdf`` provides a web service front-end
that allows files to submitted over HTTP and the results "downloaded".
This is an HTTP server intended to simplify web services deployments; it
is not intended to be deployed on the public internet and no real
security measures to speak of.
The Docker image of OCRmyPDF provides a web service front-end
that allows files to submitted over HTTP, and the results can be downloaded.
This is an HTTP server intended to demonstrate how OCRmyPDF can be
integrated into a web service. It is not intended to be deployed on the
public internet and does not provide any security measures.
In addition, the following third-party integrations are available:
- `Paperless-ngx <https://docs.paperless-ngx.com/>`__ is a free software
document management system that uses OCRmyPDF to perform OCR on
uploaded documents.
- `Nextcloud OCR <https://github.com/janis91/ocr>`__ is a free software
plugin for the Nextcloud private cloud software
plugin for the Nextcloud private cloud software.
OCRmyPDF is not designed to be secure against malware-bearing PDFs (see
`Using OCRmyPDF online <ocr-service>`__). Users should ensure they
+30 -11
View File
@@ -14,17 +14,20 @@ expired as of 2017, but it is possible that unknown patents exist.
JBIG2 encoding is recommended for OCRmyPDF and is used to losslessly
create smaller PDFs. If JBIG2 encoding is not available, lower quality
encodings will be used.
CCITT encoding will be used for monochrome images.
JBIG2 decoding is not patented and is performed automatically by most
PDF viewers. It is widely supported and has been part of the PDF
specification since 2001.
On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by
default. The Docker image for OCRmyPDF also builds its own JBIG2 encoder
from source.
JBIG encoding is automatically provided by these OCRmyPDF packages:
- Docker image (both Ubuntu and Alpine)
- Snap package
- ArchLinux AUR package
- Alpine Linux package
- Homebrew on macOS
For all other Linux, you must build a JBIG2 encoder from source:
For all other platforms, you would need to build the JBIG2 encoder from source:
.. code-block:: bash
@@ -34,29 +37,45 @@ For all other Linux, you must build a JBIG2 encoder from source:
./configure && make
[sudo] make install
.. _jbig2-lossy:
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
are packaged as libtool and libleptonica-dev.
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
as libtool and leptonica-devel. For this to work, please make sure to install
``autotools``, ``automake``, ``libtool`` and ``leptonica`` first if not already
installed.
.. code-block:: bash
[sudo] apt install autotools-dev automake libtool libleptonica-dev
.. _jbig2-lossy:
Lossy mode JBIG2
================
OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should
OCRmyPDF provides lossy mode JBIG2 as an advanced and potentially dangerous
feature. Users should
`review the technical concerns with JBIG2 in lossy
mode <https://en.wikipedia.org/wiki/JBIG2#Disadvantages>`__
and decide if this feature is acceptable for their use case.
and decide if this feature is acceptable for their use case. In general,
this mode should not be used for archival purposes, should not be used when
the original document is not available or will be destroyed, and should
not be used when numbers present in the document are important, because
there is a risk of 6/8 and 8/6 substitution errors.
JBIG2 lossy mode does achieve higher compression ratios than any other
monochrome (bitonal) compression technology; for large text documents
the savings are considerable. JBIG2 lossless still gives great
compression ratios and is a major improvement over the older CCITT G4
standard. As explained above, there is some risk of substitution errors.
standard.
To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``.
``--optimize {1,2,3}`` are necessary for the argument to take effect
also required. Also, a JBIG2 encoder must be installed as described in
the previous section.
You can adjust the threshold for JBIG2 compression with the
``--jbig2-threshold``. The default is 0.85, meaning that if two symbols
are 85% similar, they will be compressed together.
*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by
default.*
+55 -21
View File
@@ -12,22 +12,32 @@ OCRmyPDF uses Tesseract for OCR, and relies on its language packs for all langua
On most platforms, English is installed with Tesseract by default, but not always.
Tesseract supports `most
languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__.
languages <https://github.com/tesseract-ocr/tesseract/blob/main/doc/tesseract.1.asc#languages>`__.
Languages are identified by standardized three-letter codes (called ISO 639-2 Alpha-3).
Tesseract's documentation also lists the three-letter code for your language.
Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others
are not, e.g. German is ``deu`` and French is ``fra``.
Language packs (strictly speaking, Tesseract "traineddata" files) generally correspond
to the language in question, but different language packs are used in certain
situations. For German, the "Fraktur" language pack can assist with reading older
materials in the Fraktur typeface family (``deu_frak``). Some communities have changed
their script from Cyrillic to Latin; the Cyrillic version of Uzbek is available
as ``uzb_cyrl`` and the Latin version is ``uzb``.
After you have installed a language pack, you can use it with ``ocrmypdf -l <language>``,
for example ``ocrmypdf -l spa``. For multilingual documents, you can specify
all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French.
English is assumed by default unless other language(s) are specified.
For Linux users, you can often find packages that provide language
packs:
packs.
Debian and Ubuntu users
=======================
Platform install steps
======================
Debian and Ubuntu (apt)
-----------------------
.. code-block:: bash
@@ -42,8 +52,8 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Fedora users
============
Fedora
------
.. code-block:: bash
@@ -58,8 +68,24 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo users
============
Arch Linux
----------
.. code-block:: bash
# Display a list of all Tesseract language packs
pacman -Ss tesseract-data
# Install German language pack
pacman -S tesseract-data-deu
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo
------
On Gentoo the package ``app-text/tessdata_fast``, which ``app-text/tesseract`` depends on, handles Tesseract languages.
It accepts USE flags to select what languages should be installed, these can be set in ``/etc/portage/package.use``.
@@ -70,13 +96,13 @@ This enables these languages for all packages (e.g. including aspell).
# Display a list of all Tesseract language packs
equery uses app-text/tessdata_fast
# Add English and German language support for Tesseract only
echo 'app-text/tessdata_fast l10n_de l10n_en' >> /etc/portage/package.use
# Add global English and German language support (the `l10n_` from equery has to be omited)
# Add global English and German language support (the `l10n_` from equery has to be omitted)
echo L10N="de en" >> /etc/portage/make.conf
# update system to reflect changed USE flags
emerge --update --deep --newuse @world
@@ -85,23 +111,31 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
macOS users
===========
macOS
-----
You can install additional language packs by
:ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
Docker users
============
Docker
------
Users of the OCRmyPDF Docker image should install language packs into a
derived Docker image as
:ref:`described in that section <docker-lang-packs>`.
Windows users
=============
Windows
-------
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
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).
Custom language packs
=====================
If you have fine-tuned or trained Tesseract and generated custom trained data, you can
copy your ``customlang.traineddata`` file into your Tesseract "tessdata" folder, and
then use the ``-l customlang`` argument to tell OCRmyPDF to pass that language on to
Tesseract.
+21 -8
View File
@@ -38,17 +38,13 @@ on ARM and x86_64. Performance may be poor on other processor architectures.
Versioning scheme
-----------------
OCRmyPDF uses setuptools-scm for versioning, which derives the version from
OCRmyPDF uses hatch-vcs for versioning, which derives the version from
Git as a single source of truth. This may be unsuitable for some distributions, e.g.
to indicate that your distribution modifies OCRmyPDF in some way.
You can patch the ``__version__`` variable in ``src/ocrmypdf/_version.py`` if
necessary.
OCRmyPDF uses setuptools-scm-git-archive to ensure that tarballs downloaded from
GitHub contain version information. Unfortunately, these tarballs are not always
deterministic. See this
`issue <https://github.com/ocrmypdf/OCRmyPDF/issues/841#issuecomment-936562696>`_.
necessary, or set the environment variable ``SETUPTOOLS_SCM_PRETEND_VERSION``
to the required version, if you need to override versioning for some reason.
jbig2enc
--------
@@ -61,4 +57,21 @@ improve OCRmyPDF's compression.
Command line completions
------------------------
Please ensure that command line completions are installed.
Please ensure that command line completions are installed, as described in the
installation documentation.
32-bit Linux support
--------------------
If you maintain a Linux distribution that supports 32-bit x86 or ARM, OCRmyPDF
should continue to work as long as all of its dependencies continue to be
available in 32-bit form. Please note we do not test on 32-bit platforms.
HEIF/HEIC
---------
OCRmyPDF defaults to installing the pi-heif PyPI package, which supports converting
HEIF (High Efficiency Image File Format) images to PDF from the command line.
If your distribution does not have this library available, you can exclude it and
OCRmyPDF will gracefully degrade automatically, losing only support for this
feature.
+32 -11
View File
@@ -13,21 +13,40 @@ tuned. Optimization occurs after OCR, and only if OCR succeeded. It does not
perform other possible optimizations such as deduplicating resources,
consolidating fonts, simplifying vector drawings, or anything of that nature.
Optimization ranges from ``-O0`` through ``-O3``, where ``0`` disables
optimization and ``3`` implements all options. ``1``, the default, performs only
safe and lossless optimizations. (This is similar to GCC's optimization
parameter.) The exact type of optimizations performed will vary over time.
.. list-table:: Title
:widths: 33 6 60
:header-rows: 1
PDF optimization requires third-party, optional tools for certain optimizations.
If these are not installed or cannot be found by OCRmyPDF, optimization will not
be as good.
* - Optimization level
- Shorthand
- Description
* - ``--optimize 0``
- ``-O0``
- Disable most optimizations.
* - ``--optimize 1`` (default)
- ``-O1``
- Safe and lossless optimizations.
* - ``--optimize 2``
- ``-O2``
- Safe and lossy optimizations.
* - ``--optimize 3``
- ``-O3``
- Aggressive lossy optimizations.
The exact type of optimizations performed will vary over time, and depend on
the availability of third-party tools.
Despite optimizations, OCRmyPDF might still increase the overall file size,
since it must embed information about the recognized text, and depending on the
settings chosen, may not be able to represent the output file as compactly as
the input file.
Optimizations that always occurs
================================
OCRmyPDF will automatically replace obsolete or inferior compression schemes
such as RLE or LZW with superior schemes such as Deflate and converting
monochrome images to CCITT G4. Since this is harmless it always occurs and there
such as RLE or LZW with superior schemes such as Deflate, and convert
monochrome images to CCITT G4. Since this is lossless, it always occurs and there
is no way to disable it. Other non-image compressed objects are compressed as
well.
@@ -37,12 +56,14 @@ Fast web view
OCRmyPDF automatically optimizes PDFs for "fast web view" in Adobe Acrobat's
parlance, or equivalently, linearizes PDFs so that the resources they reference
are presented in the order a viewer needs them for sequential display. This
reduces the latency of viewing a PDF both online and from local storage. This
actually slightly increases the file size.
reduces the latency of viewing a PDF both online and from local storage, in
exchange for a slight increase in file size.
To disable this optimization and all others, use ``ocrmypdf --optimize 0 ...``
or the shorthand ``-O0``.
Adobe Acrobat might not report the file as being "fast web view".
Lossless optimizations
======================
+63 -102
View File
@@ -2,6 +2,8 @@
..
.. SPDX-License-Identifier: CC-BY-SA-4.0
.. _security:
===================
PDF security issues
===================
@@ -29,13 +31,9 @@ attack vectors.
In short, PDFs `may contain
viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`__.
This
`article <https://theinvisiblethings.blogspot.ca/2013/02/converting-untrusted-pdfs-into-trusted.html>`__
describes a high-paranoia method which allows potentially hostile PDFs
to be viewed and rasterized safely in a disposable virtual machine. A
trusted PDF created in this manner is converted to images and loses all
information making it searchable and losing all compression. OCRmyPDF
could be used to restore searchability.
If you do not trust a PDF or its source, do not open it or use OCRmyPDF
on it. Consider using a Docker container or virtual machine to isolate
an untrusted PDF from your system.
How OCRmyPDF processes PDFs
===========================
@@ -43,11 +41,11 @@ How OCRmyPDF processes PDFs
OCRmyPDF must open and interpret your PDF in order to insert an OCR
layer. First, it runs all PDFs through
`pikepdf <https://github.com/pikepdf/pikepdf>`__, a library based on
`qpdf <https://github.com/qpdf/qpdf>`__, a program that repairs PDFs
`QPDF <https://github.com/qpdf/qpdf>`__, a program that repairs PDFs
with syntax errors. This is done because, in the author's experience, a
significant number of PDFs in the wild, especially those created by
scanners, are not well-formed files. qpdf makes it more likely that
OCRmyPDF will succeed, but offers no security guarantees. qpdf is also
scanners, are not well-formed files. QPDF makes it more likely that
OCRmyPDF will succeed, but offers no security guarantees. QPDF is also
used to split the PDF into single page PDFs.
Finally, OCRmyPDF rasterizes each page of the PDF using
@@ -58,109 +56,72 @@ into the existing PDF or it may essentially reconstruct ("re-fry") a
visually identical PDF that may be quite different at the binary level.
That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
.. _ocr-service:
Using OCRmyPDF online or as a service
=====================================
OCRmyPDF is not designed for use as a public web service where a
malicious user could upload a chosen PDF. In particular, it is not
necessarily secure against PDF malware or PDFs that cause denial of
service. OCRmyPDF relies on Ghostscript, and therefore, if deployed
online one should be prepared to comply with Ghostscript's Affero GPL
license, and any other licenses.
Setting aside these concerns, a side effect of OCRmyPDF is that it may
incidentally sanitize PDFs containing certain types of malware. It
repairs the PDF with pikepdf/libqpdf, which could correct malformed PDF
structures that are part of an attack. When PDF/A output is selected
(the default), the input PDF is partially reconstructed by Ghostscript.
When ``--force-ocr`` is used, all pages are rasterized and reconverted
to PDF, which could remove malware in embedded images.
OCRmyPDF should be relatively safe to use in a trusted intranet, with
some considerations:
Limiting CPU usage
------------------
OCRmyPDF will attempt to use all available CPUs and storage, so
executing ``nice ocrmypdf`` or limiting the number of jobs with the
``-j`` argument may ensure the server remains available. Another option
would be to run OCRmyPDF jobs inside a Docker container, a virtual machine,
or a cloud instance, which can impose its own limits on CPU usage and be
terminated "from orbit" if it fails to complete.
Temporary storage requirements
------------------------------
OCRmyPDF will use a large amount of temporary storage for its work,
proportional to the total number of pixels needed to rasterize the PDF.
The raster image of a 8.5×11" color page at 300 DPI takes 25 MB
uncompressed; OCRmyPDF saves its intermediates as PNG, but that still
means it requires about 9 MB per intermediate based on average
compression ratios. Multiple intermediates per page are also required,
depending on the command line given. A rule of thumb would be to allow
100 MB of temporary storage per page in a file meaning that a small
cloud servers or small VM partitions should be provisioned with plenty
of extra space, if say, a 500 page file might be sent.
To check temporary storage usage on actual files, run
``ocrmypdf -k ...`` which will preserve and print the path to temporary
storage when the job is done.
To change where temporary files are stored, change the ``TMPDIR``
environment variable for ocrmypdf's environment. (Python's
``tempfile.gettempdir()`` returns the root directory in which temporary
files will be stored.) For example, one could redirect ``TMPDIR`` to a
large RAM disk to avoid wear on HDD/SSD and potentially improve
performance. On Amazon Web Services, ``TMPDIR`` can be set to `empheral
storage <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html>`__.
Timeouts
--------
To prevent excessively long OCR jobs consider setting
``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big``
is particularly helpful if your PDFs include documents such as reports
on standard page sizes with large images attached - often large images
are not worth OCR'ing anyway.
Commercial alternatives
-----------------------
The author also provides professional services that include OCR and
building databases around PDFs, and is happy to provide consultation.
Abbyy Cloud OCR is viable commercial alternative with a web services
API. Amazon Textract, Google Cloud Vision, and Microsoft Azure
Computer Vision provide advanced OCR but have less PDF rendering capability.
Password protection, digital signatures and certification
=========================================================
Password protected PDFs
=======================
Password protected PDFs usually have two passwords, and owner and user
password. When the user password is set to empty, PDF readers will open
the file automatically and marked it as "(SECURED)". While not as
reliable as a digital signature, this indicates that whoever set the
password approved of the file at that time. When the user password is
set, the document cannot be viewed without the password.
the file automatically and mark it as "(SECURED)". Password security can
also request certain restrictions on the PDF, but anyone can remove these
restrictions if they have either the owner *or* user password. Passwords
mainly present a barrier for casual users.
Either way, OCRmyPDF does not remove passwords from PDFs and exits with
an error on encountering them.
OCRmyPDF cannot remove passwords from PDFs. If you want to remove a
password from a PDF, you must use other software, such as ``qpdf``.
``qpdf`` can remove passwords. If the owner and user password are set, a
If the owner and user password are set, a
password is required for ``qpdf``. If only the owner password is set, then the
password can be stripped, even if one does not have the owner password.
password can be stripped, even if one does not have the owner password. To
remove the password from a using QPDF, use:
After OCR is applied, password protection is not permitted on PDF/A
documents but the file can be converted to regular PDF.
.. code-block:: bash
qpdf --decrypt --password='abc123' input.pdf no_password.pdf
Then you can run OCRmyPDF on the file.
In its default mode, OCRmyPDF generates PDF/A. Passwords may not be set on PDF/A
documents. If you want to set a password on the output PDF, you must
specify ``--output-type pdf``.
Signature images
================
Many programs exist which are capable of inserting an image of someone's
signature. On its own, this offers no security guarantees. It is trivial
to remove the signature image and apply it to other files. This practice
offers no real security.
Digital signatures
==================
Important documents can be digitally signed and certified to attest to
their authorship. OCRmyPDF cannot do this. Open source tools such as
pdfbox (Java) have this capability as does Adobe Acrobat.
their authorship, approval or execution of a legal agreement. OCRmyPDF
will detect signed PDFs and will not modify them, unless the
``--invalidate-digital-signatures`` option is used, which will
invalidate any signatures. (The signature may still be present in the PDF
if opened, but PDF readers will not validate it.)
A digital signature adds a cryptographic hash of the document to the
document, so tamper protection is provided. That also precludes OCRmyPDF
from modifying the document and preserving the signature.
Digital signatures are not the same as a signature image. A digital
signature is a cryptographic hash of the document that is encrypted with
the author's private key. The signature is decrypted with the author's
public key. The public key is usually distributed by a certificate
authority. The signature is then verified by the PDF reader. If the
document is modified, the signature will be invalidated.
Certificate-encrypted PDFs
==========================
PDFs can be encrypted with a certificate. This is a more secure form of
encryption than a password. The certificate is usually issued by a
certificate authority. A certificate is used to encrypt the document using
the public key for the benefit of a specific recipient who possesses
the private key.
OCRmyPDF cannot open certificate-encrypted PDFs. If you have the
certificate, you can use other PDF software, such as Acrobat, to
decrypt the PDF.
+1 -1
View File
@@ -17,7 +17,7 @@ If running OCRmyPDF quickly is your main goal, you can use settings such as:
* ``--optimize 0`` to disable file size optimization
* ``--output-type pdf`` to disable PDF/A generation
* ``--fast-web-view 0`` to disable fast web view optimization
* ``--fast-web-view 999999`` to disable fast web view optimization
* ``--skip-big`` to skip large images, if some pages have large images
You can also avoid:
+19 -21
View File
@@ -29,6 +29,9 @@ conventions. Note that: plugins installed with as setuptools entrypoints are
not checked currently, because OCRmyPDF assumes you may not want to enable
plugins for all files.
See [OCRmyPDF-EasyOCR](https://github.com/ocrmypdf/OCRmyPDF-EasyOCR) for an
example of a straightforward, fully working plugin.
Script plugins
==============
@@ -70,33 +73,23 @@ similar to ``pytest`` packages such as ``pytest-cov`` (the package) and
module), just like pytest plugins. At the same time, please make it clear
that your package is not official.
Setuptools plugins
==================
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.
installed in the same virtual environment, using a project entrypoint.
OCRmyPDF uses the entrypoint namespace "ocrmypdf".
Your package's ``setup.py`` would need to contain the following, for a plugin
named ``ocrmypdf-exampleplugin``:
For example, ``pyproject.toml`` would need to contain the following, for a plugin named
``ocrmypdf-exampleplugin``:
.. code-block:: python
.. code-block:: toml
# sample ./setup.py file
from setuptools import setup
[project]
name = "ocrmypdf-exampleplugin"
setup(
name="ocrmypdf-exampleplugin",
packages=["exampleplugin"],
# the following makes a plugin available to pytest
entry_points={"ocrmypdf": ["exampleplugin = exampleplugin.pluginmodule"]},
)
.. code-block:: ini
# equivalent setup.cfg
[options.entry_points]
ocrmypdf =
exampleplugin = exampleplugin.pluginmodule
[project.entry-points."ocrmypdf"]
exampleplugin = "exampleplugin.pluginmodule"
Plugin requirements
===================
@@ -181,8 +174,13 @@ Custom command line arguments
Execution and progress reporting
--------------------------------
.. autoclass:: ocrmypdf.pluginspec.ProgressBar
:members:
:special-members: __init__, __enter__, __exit__
.. autoclass:: ocrmypdf.pluginspec.Executor
:members:
:special-members: __call__
.. autofunction:: ocrmypdf.pluginspec.get_logging_console
+493 -1
View File
@@ -20,6 +20,8 @@ The most recent release of OCRmyPDF is |OCRmyPDF PyPI|. Any newer versions
referred to in these notes may exist the main branch but have not been
tagged yet.
OCRmyPDF typically supports the three most recent Python versions.
.. note::
Attention maintainers: these release notes may be updated with information
@@ -28,6 +30,496 @@ tagged yet.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.10.0
========
- Added hocr textangle processing, improving handling of text at angles.
Thanks @0dinD :issue:`1467`
- Docker documentation updates related to podman. Thanks @rugk. :issue:`1489,1488`
- Dropped webservice.py's fragile use of ttyd. Instead, messages from ocrmypdf are
printed to the console.
- Fixed broken test test_hocrtransform_matches_sandwich, which had become
an invalid test. Thanks @QuLogic for reporting.
- Improved install instructions for Windows. Thanks @alex.
v16.9.0
=======
- Added hocr caption processing. Thanks @0dinD :issue:`1466`
- ocrmypdf-alpine Docker image is now built with Alpine 3.21.
- Fixed error handling of PDFs that contain invalid images with both ImageMask
and ColorSpace defined. :issue:`1453`
- Fixed test suite regression when only older Ghostscripts are installed.
- Improved documetnation of _progressbar.py. Thanks @QuentinFuxa. :issue:`1456`
- Disabling building of documentation as PDF on ReadTheDocs, as this caused
complex build issues deemed not worth solving.
v16.8.0
=======
- Upgraded webservice.py demonstration using streamlit. It's now possible to
exercise most of OCRmyPDF's functionality in a simple web UI.
- Added cache to Dockerfiles to improve build speed.
- Fixed numerous formatting errors in the documentation that prevented some
parts of documentation from generating correctly.
- Improved OCR text rendering by suppressing negative-width spaces. Thanks
@pajowu. :issue:`1446`
- Improved detecting of invisible text when using `--redo-ocr`. Thanks
@pajowu. :issue:`1448``
v16.7.0
=======
- Fixed further issues with Docker build and updated some versions.
- Main Docker image returned to Ubuntu 24.04 since the fix in v16.6.2 resolved
that concern.
- Code that previously sent Ghostscript output to stdout has been changed to
output to temporary files, since Ghostscript was doing that anyway internally.
This is a modest efficiency improvement.
- Fixed an issue with debug log output being parsed as rich markup. :issue:`1444`
v16.6.2
=======
- Remove invalid hyperlink annotations to satisfy Ghostscript 10.x during PDF/A
conversion. :issue:`1425`
v16.6.1
=======
- Fixed some issues with Docker build, such as removing unnecessary content and using
a stable Tesseract version.
- Reverted Docker image to Ubuntu 22.04 to access older/more stable Ghostscript
for now.
- Clarified batch commands in documentation.
- Fixed an issue with JSON serialization and pickling of HOCRResult. :issue:`1427`
v16.6.0
=======
- Fixed an issue where damaged PDFs would fail with ``--redo-ocr``. :issue:`1403`
- Fixed an error that prevented JBIG2 optimization on Windows if the image
was optimized in an earlier step. :issue:`1396`
- Fixed an error detecting the version of unpaper 7.0.0. :issue:`1409`
- Fixed a performance regression when scanning pages. :issue:`1378`. Thanks @aliemjay.
- Fixed Alpine Docker image by enforcing Alpine 3.19. Alpine 3.20 includes a
defective version of Tesseract OCR and so is not usable.
- Upgraded Ubuntu Docker image to use Ubuntu 24.04.
- Build and test scripts/actions switched to uv.
- When running in a container, we now remind the user that temporary folders
are inside the container and may not be accessible.
- Fixed Linux test coverage matrix, which was missing some key versions.
v16.5.0
=======
- Fixed issue with interpreting PDFs that have images with array masks.
:issue:`1377`
- Enabled testing on Python 3.13.
- Fixed a test that did not work correctly but still passed. :issue:`1382`
- Improved "PDF/A conversion failed" warning message to better describe implications.
- Updated documentation to better explain OCR_JSON_SETTINGS in batch processing.
- Build backend changed from setuptools to hatchling.
v16.4.3
=======
- Work around pdfminer.six issue where a token on the buffer boundary is incorrectly
parsed as two tokens. :issue:`1361`
- New rules are applied to stencil masks and explicit masks when calculating the
optimal page DPI for rendering. :issue:`1362`
- Fixed attempts to use an incompatible jbig2.EXE provided by TeX Live. :issue:`1363`
v16.4.2
=======
- Fixed order of filenames passed to Ghostscript for PDF/A generation. :issue:`1359`
- Suppressed missing jbig2dec warning message. :issue:`1358`
- Fixed calculation of image size when soft mask dimensions don't match image
dimension. :issue:`1351`
- Several fixes to documentation. Thanks to users Iris and JoKalliauer
who contributed these changes.
- Fixed error on processing PDFs that are missing certain image metadata. :issue:`1315`
v16.4.1
=======
- Fixed calculation of image printed area (used in finding weighted DPI for OCR).
:issue:`1334`
- Fixed "NotImplementedError: not sure how to get colorspace" error
messages in logs which simply records a failure to optimize images with
print production colorspaces. :issue:`1315`
v16.4.0
=======
- Selecting the ``osd`` and ``equ`` pseudo-languages with ``-l/--language`` now
exits with an error when using Tesseract OCR, because these are not
regular Tesseract languages but implementation details implemented.
Using them can cause Tesseract to crash.
- The hOCR renderer is more tolerant of extra whitespace in input files.
- watcher.py now changes the output file extension to .pdf when the input is not
.pdf.
- Improved handling of PDFs that contain circularly referenced Form XObjects.
:issue:`1321`
- Fixed Alpine Docker image for ARM64, which was not building correctly.
- Docker images now use pikepdf 9.0.0.
- Prevent use of Tesseract OCR 5.4.0, a version with known regressions.
- Disabled progressbar for "Linearizing" when ``--no-progress-bar`` set.
- Fixed some tests that warn about missing JBIG2 decoding via pikepdf, by
installing the necessary libraries during tests.
v16.3.1
=======
- Fixed a test suite failure with Ghostscript 10.03.0+. :issue:`1316`
- Fixed an issue with the presentation of the "OCR" progress bar. :issue:`1313`
v16.3.0
=======
- Fixed progress bar not displaying for Ghostscript PDF/A conversion. :issue:`1313`
- Added progress bar for linearization. :issue:`1313`
- If `--rotate-pages-threshold` issued without `--rotate-pages` we now exit with
an error since the user likely intended to use `--rotate-pages`. :issue:`1309`
- If Tesseract hOCR gives an invalid line box, print an error message instead of
exiting with an error. :issue:`1312`
v16.2.0
=======
- Fixed issue 'NoneType' object has no attribute 'get' when optimizing certain PDFs.
:issue:`1293,1271`
- Switched formatting from black to ruff.
- Added support for sending sidecar output to io.BytesIO.
- Added support for converting HEIF/HEIC images (the native image of iPhones and
some other devices) to PDFs, when the appropriate pi-hief library is installed.
This library is marked as a dependency, but maintainers may opt out if needed.
- We now default to downsampling large images that would exceed Tesseract's internal
limits, but only if it cause processing to fail. Previously, this behavior only
occurred if specifically requested on command line. It can still be configured
and disabled. See the --tesseract command line options.
- Added Macports install instructions. Thanks @akierig.
- Improved logging output when an unexpected error occurs while trying to obtain
the version of a third party program.
v16.1.2
=======
- Fixed test suite failure when using Ghostscript 10.3.
- Other minor corrections.
v16.1.1
=======
- Fixed PyPy 3.10 support.
v16.1.0
=======
- Improved hOCR renderer is now default for left to right languages.
- Improved handling of rotated pages. Previously, OCR text might be missing for
pages that were rotated with a /Rotate tag on the page entry.
- Improved handling of cropped pages. Previously, in some cases a page with a
crop box would not have its OCR applied correctly and misalignment between
OCR text and visible text coudl occur.
- Documentation improvements, especially installation instructions for less
common platforms.
v16.0.4
=======
- Fixed some issues for left-to-right text with the new hOCR renderer. It is still
not default yet but will be made so soon. Right-to-left text is still in progress.
- Added an error to prevent use of several versions of Ghostscript that seem
corrupt existing text in input PDFs. Newly generated OCR is not affected.
For best results, use Ghostscript 10.02.1 or newer, which contains the fix
for the issue.
v16.0.3
=======
- Changed minimum required Ghostscript to 9.54, to support users of RHEL 9 and its
derivatives, since that is the latest version available there.
- Removed warning message about CVE-2023-43115, on the assumption that most
distributions have backported the patch by now.
v16.0.2
=======
- Temporarily changed PDF text renderer back to sandwich by default to address
regressions in macOS Preview.
v16.0.1
=======
- Fixed text rendering issue with new hOCR text renderer - extraneous byte order
marks.
- Tightened dependencies.
v16.0.0
=======
- Added OCR text renderer, combined the best ideas of Tesseract's PDF
generator and the older hOCR transformer renderer. The result is a hopefully
permanent fix for wordssmushedtogetherwithoutspaces issues in extracted text,
better registration/position of text on skewed baselines :issue:`1009`,
fixes to character output when the German Fraktur script is used :issue:`1191`,
proper rendering of right to left languages (Arabic, Hebrew, Persian) :issue:`1157`.
Asian languages may still have excessive word breaks compared to expectations.
The new renderer is the default; the old sandwich renderer is still available
using ``--pdf-renderer sandwich``; the old hOCR renderer is no more.
- The ``ocrmypdf.hocrtransform`` API has changed substantially.
- Support for Python 3.9 has been dropped. Python 3.10+ is now required.
- pikepdf >= 8.8.0 is now required.
v15.4.4
=======
- Fixed documentation for installing Ghostscript on Windows. :issue:`1198`
- Added warning message about security issue in older versions of Ghostscript.
v15.4.3
=======
- Fixed deprecation warning in pikepdf older than 8.7.1; pikepdf >= 8.7.1 is
now required.
v15.4.2
=======
- We now raise an exception on a certain class of PDFs that likely need an
explicit color conversion strategy selected to display correctly
for PDF/A conversion.
- Fixed an error that occurred while trying to write a log message after the
debug log handler was removed.
v15.4.1
=======
- Fixed misc/watcher.py regressions: accept ``--ocr-json-settings`` as either
filename or JSON string, as previously; and argument count mismatch.
:issue:`1183,1185`
- We no longer attempt to set /ProcSet in the PDF output, since this is an
obsolete PDF feature.
- Documentation improvements.
v15.4.0
=======
- Added new experimental APIs to support offline editing of the final text.
Specifically, one can now generate hOCR files with OCRmyPDF, edit them with
some other tool, and then finalize the PDF. They are experimental and
subject to change, including details of how the working folder is used.
There is no command line interface.
- Code reorganization: executors, progress bars, initialization and setup.
- Fixed test coverage in cases where the coverage tool did not properly trace
into threads or subprocesses. This code was still being tested but appeared
as not covered.
- In the test suite, reduced use of subprocesses and other techniques that
interfere with coverage measurement.
- Improved error check for when we appear to be running inside a snap container
and files are not available.
- Plugin specification now properly defines progress bars as a protocol rather
than defining them as "tqdm-like".
- We now default to using "forkserver" process creation on POSIX platforms
rather than fork, since this is method is more robust and avoids some
issues when threads are present.
- Fixed an instance where the user's request to ``--no-use-threads`` was ignored.
- If a PDF does not have language metadata on its top level object, we add
the OCR language.
- Replace some cryptic test error messages with more helpful ones.
- Debug messages for how OCRmyPDF picks the colorspace for a page are now
more descriptive.
v15.3.1
=======
- Fixed an issue with logging settings for misc/watcher.py introduced in the
previous release. :issue:`1180`
- We now attempt to preserve the input's extended attributes when creating
the output file.
- For some reason, the macOS build now needs OpenSSL explicitly installed.
- Updated documentation on Docker performance concerns.
v15.3.0
=======
- Update misc/watcher.py to improve command line interface using Typer, and
support ``.env`` specification of environment variables. Improved error
messages. Thanks to @mflagg2814 for the PR that prompted this improvement.
- Improved error message when a file cannot be read because we are running in
a snap container.
v15.2.0
=======
- Added a Docker image based on Alpine Linux. This image is smaller than the
Ubuntu-based image and may be useful in some situations. Currently hosted at
jbarlow83/ocrmypdf-alpine. Currently not available in ARM flavor.
- The Ubuntu Docker is now aliased to jbarlow83/ocrmypdf-ubuntu.
- Updated Docker documentation.
v15.1.0
=======
- We now require Pillow 10.0.1, due a serious security vulnerability in all earlier
versions of that dependency. The vulnerability concerns WebP images and could
be triggered in OCRmyPDF when creating a PDF from a malicious WebP image.
- Added some keyword arguments to ``ocrmypdf.ocr`` that were previously accepted
but undocumented.
- Documentation updates and typing improvements.
v15.0.2
=======
- Added Python 3.12 to test matrix.
- Updated documentation for notes on Python 3.12, 32-bit support and some new
features in v15.
v15.0.1
=======
- Wheels Python tag changed to py39.
- Marked as a expected fail a test that fails on recent Ghostscript versions.
- Clarified documentation and release notes around the extent of 32-bit support.
- Updated installation documentation to changes in v15.
v15.0.0
=======
- Dropped support for Python 3.8.
- Dropped support some older dependencies, specifically ``coloredlogs`` and
``tqdm`` in favor of rich - see ``pyproject.toml`` for details.
Generally speaking, Ubuntu 22.04 is our new baseline system.
- Tightened version requirements for some dependencies.
- Dropped support for 32-bit Linux wheels. We strongly recommend a 64-bit operating
system, and 64-bit versions of Python, Tesseract and Ghostscript to use OCRmyPDF.
Many of our dependencies are dropping 32-bit builds (e.g. Pillow), and we are
following suit. (Maintainers may still build 32-bit versions from source.)
- Changed to trusted release for PyPI publishing.
- pikepdf memory mapping is enabled again for improved performance, now that an
issue with feature in pikepdf is fixed.
- ``ocrmypdf.helpers.calculate_downsample`` previously had two variants, one
that took a ``PIL.Image`` and one that took a ``tuple[int, int]``. The latter
was removed.
- The snap version of ocrmypdf is now based on Ubuntu core22.
- We now account for situations where a small portion of an image on a page is drawn
at high DPI (resolution). Previously, the entire page would be rasterized at the
highest resolution of any feature, which caused performance problems. Now,
the page is rasterized
at a resolution based on the average DPI of the page, weighted by the area that
each feature occupies. Typically, small areas of high resolution in PDFs are
errors or quirks from the repeated use of assets and high resolution is not
beneficial. :issue:`1010,1104,1004,1079,1010`
- Ghostscript color conversion strategy is now configurable using
``--color-conversion-strategy``. :issue:`1143`
- JBIG2 threshold for optimization is now configurable using
``--jbig2-threshold``. :issue:`1133`
v14.4.0
=======
- Digitally signed PDFs are now detected. If the PDF is signed, OCRmyPDF will
refuse to modify it. Previously, only encrypted PDFs were detected, not
those that were signed but not encrypted. :issue:`1040`
- In addition, ``--invalidate-digital-signatures`` can be used to override the
above behavior and modify the PDF anyway. :issue:`1040`
- tqdm progress bars replaced with "rich" progress bars. The rich library is
a new dependency. Certain APIs that used tqdm are now deprecated and will
be removed in the next major release.
- Improved integration with GitHub Releases. Thanks to @stumpylog.
v14.3.0
=======
- Renamed master branch to main.
- Improve PDF rasterization accuracy by using the ``-dPDFSTOPONERROR`` option
to Ghostscript. Use ``--continue-on-soft-render-error`` if you want to render
the PDF anyway. The plugin specification was adjusted to support this feature;
plugin authors may want to adapt PDF rasterizing and rendering
plugins. :issue:`1083`
- The calculated deskew angle is now recorded in the logged output. :issue:`1101`
- Metadata can now be unset by setting a metadata type such as ``--title`` to an
empty string. :issue:`1117,1059`
- Fixed random order of languages due to use of a set. This may have caused output
to vary when multiple languages were set for OCR. :issue:`1113`
- Clarified the optimization ratio reported in the log output.
- Documentation improvements.
v14.2.1
=======
- Fixed :issue:`977`, where images inside Form XObjects were always excluded
from image optimization.
v14.2.0
=======
- Added ``--tesseract-downsample-above`` to downsample larger images even when
they do not exceed Tesseract's internal limits. This can be used to speed
up OCR, possibly sacrificing accuracy.
- Fixed resampling AttributeError on older Pillow. :issue:`1096`
- Removed an error about using Ghostscript on PDFs with that have the /UserUnit
feature in use. Previously, Ghostscript would fail to process these PDFs,
but in all supported versions it is now supported, so the error is no longer
needed.
- Improved documentation around installing other language packs for Tesseract.
v14.1.0
=======
- Added ``--tesseract-non-ocr-timeout``. This allows using Tesseract's deskew
and other non-OCR features while disabling OCR using ``--tesseract-timeout 0``.
- Added ``--tesseract-downsample-large-images``. This downsamples larges images
that exceed the maximum image size Tesseract can handle. Large images may still
take a long time to process, but this allows them to be processed if that
is desired.
- Fixed :issue:`1082`, an issue with snap packaged building.
- Change linter to ruff, fix lint errors, update documentation.
v14.0.4
=======
- Fixed :issue:`1066, 1075`, an exception when processing certain malformed PDFs.
v14.0.3
=======
- Fixed :issue:`1068`, avoid deleting /dev/null when running as root.
- Other documentation fixes.
v14.0.2
=======
- Fixed :issue:`1052`, an exception on attempting to process certain nonconforming PDFs.
- Explicitly documented that Windows 32-bit is no longer supported.
- Fixed source installation instructions.
- Other documentation fixes.
v14.0.1
=======
- Fixed some version checks done with smart version comparison.
- Added missing jbig2dec to Docker image.
v14.0.0
=======
- Dropped support for Python 3.7.
- Dropped support generally speaking, all dependencies older than what Ubuntu 20.04
provides.
- Ghostscript 9.50 or newer is now required. Shims to support old versions were
removed.
- Tesseract 4.1.1 or newer is now required. Shims to support old versions were
removed.
- Docker image now uses Tesseract 5.
- Dropped setup.cfg configuration for pyproject.toml.
- Removed deprecation exception PdfMergeFailedError.
- A few more public domain test files were removed or replaced. We are aiming for
100% compliance with SPDX and generally towards simplifying copyright.
v13.7.0
=======
@@ -681,7 +1173,7 @@ v10.3.2
v10.3.1
=======
- Fixed a number of test suite failures with pdfminer.six older than veresion 20200402.
- Fixed a number of test suite failures with pdfminer.six older than version 20200402.
- Enabled support for pdfminer.six 20200720.
v10.3.0
+241
View File
@@ -0,0 +1,241 @@
# SPDX-FileCopyrightText: 2025 James R. Barlow
# SPDX-License-Identifier: AGPL-3.0-or-later
"""This is a simple web service/HTTP wrapper for OCRmyPDF.
This may be more convenient than the command line tool for some Docker users.
Note that OCRmyPDF uses Ghostscript, which is licensed under AGPLv3+. While
OCRmyPDF is under GPLv3, this file is distributed under the Affero GPLv3+ license,
to emphasize that SaaS deployments should make sure they comply with
Ghostscript's license as well as OCRmyPDF's.
"""
from __future__ import annotations
import os
import subprocess
import sys
import time
from functools import partial
from operator import getitem
from pathlib import Path
from shutil import which
from tempfile import NamedTemporaryFile
import pikepdf
import streamlit as st
from streamlit.components.v1 import iframe
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
def get_host_url_with_port(port: int) -> str:
"""Get the host URL for the web service. Hacky."""
host_url = st.context.headers["host"]
try:
host, _streamlit_port = host_url.split(":", maxsplit=1)
except ValueError:
host = host_url
return f"//{host}:{port}" # Use the same protocol
st.title("OCRmyPDF Web Service")
uploaded = st.file_uploader("Upload input PDF or image", type=["pdf"], key="file")
mode = st.selectbox("Mode", options=["normal", "skip-text", "force-ocr", "redo-ocr"])
with st.expander("Input options"):
invalidate_digital_signatures = st.checkbox(
"Invalidate digital signatures", value=False
)
language = st.selectbox("Language", options=["eng", "deu", "fra", "spa"])
image_dpi = st.slider(
"Image DPI", value=300, key="image_dpi", min_value=1, max_value=5000, step=50
)
with st.expander("Preprocessing"):
skip_big = st.checkbox("Skip OCR on big pages", value=False, key="skip_big")
oversample = st.slider("Oversample", min_value=0, max_value=5000, value=0, step=50)
rotate_pages = st.checkbox("Rotate pages", value=False, key="rotate")
deskew = st.checkbox("Deskew pages", value=False, key="deskew")
clean = st.checkbox("Clean pages before OCR", value=False, key="clean")
clean_final = st.checkbox("Clean final", value=False, key="clean_final")
remove_vectors = st.checkbox("Remove vectors", value=False, key="remove_vectors")
with st.expander("Output options"):
output_type = st.selectbox(
"Output type", options=["pdfa", "pdfa", "pdfa-1", "pdfa-2", "pdfa-3", "none"]
)
pdf_renderer = st.selectbox(
"PDF rendereer", options=["auto", "hocr", "hocrdebug", "sandwich"]
)
optimize = st.selectbox("Optimize", options=["0", "1", "2", "3"])
st.selectbox("PDF/A compression", options=["auto", "jpeg", "lossless"])
with st.expander("Metadata"):
title = author = keywords = subject = None
if uploaded:
with pikepdf.open(uploaded) as pdf, pdf.open_metadata() as meta:
st.code(str(meta), language="xml")
title = st.text_input("Title", value=meta.get('dc:title', ''))
author = st.text_input("Author", value=meta.get('dc:creator', ''))
keywords = st.text_input("Keywords", value=meta.get('dc:subject', ''))
subject = st.text_input("Subject", value=meta.get('dc:description', ''))
with st.expander("Optimization after OCR"):
jpeg_quality = st.slider(
"JPEG quality", min_value=0, max_value=100, value=75, key="jpeg_quality"
)
png_quality = st.slider(
"PNG quality", min_value=0, max_value=100, value=75, key="png_quality"
)
jbig2_lossy = st.checkbox("JBIG2 lossy (dangerous)", value=False, key="jbig2_lossy")
jbig2_threshold = st.number_input("JBIG2 threshold", value=0, key="jbig2_threshold")
with st.expander("Advanced options"):
jobs = st.slider(
"Threads",
min_value=1,
max_value=os.cpu_count(),
value=os.cpu_count(),
key="threads",
)
pages = st.text_input(
"Pages", value="", help="Comma-separated list of pages to process"
)
max_image_mpixels = st.number_input(
"Max image size",
value=250.0,
min_value=0.0,
help="Maximum image size in megapixels",
)
rotate_pages_threshold = st.number_input(
"Rotate pages threshold",
value=DEFAULT_ROTATE_PAGES_THRESHOLD,
min_value=0.0,
max_value=1000.0,
help="Threshold for automatic page rotation",
)
fast_web_view = st.number_input(
"Fast web view",
value=1.0,
min_value=0.0,
help="Linearize files above this size in MB",
)
continue_on_soft_render_error = st.checkbox(
"Continue on soft render error", value=True
)
verbose_labels = ["quiet", "default", "debug", "debug_all"]
verbose = st.selectbox(
"Verbosity level",
options=[-1, 0, 1, 2],
index=1,
format_func=partial(getitem, verbose_labels),
)
if uploaded:
args = []
if mode and mode != 'normal':
args.append(f"--{mode}")
if language:
args.append(f"--language={language}")
if not uploaded.name.lower().endswith(".pdf") and image_dpi:
args.append(f"--image-dpi={image_dpi}")
if skip_big:
args.append("--skip-big")
if oversample:
args.append(f"--oversample={oversample}")
if rotate_pages:
args.append("--rotate-pages")
if deskew:
args.append("--deskew")
if clean:
args.append("--clean")
if clean_final:
args.append("--clean-final")
if remove_vectors:
args.append("--remove-vectors")
if output_type:
args.append(f"--output-type={output_type}")
if pdf_renderer:
args.append(f"--pdf-renderer={pdf_renderer}")
if optimize:
args.append(f"--optimize={optimize}")
if title:
args.append(f"--title={title}")
if author:
args.append(f"--author={author}")
if keywords:
args.append(f"--keywords={keywords}")
if subject:
args.append(f"--subject={subject}")
if pages:
args.append(f"--pages={pages}")
if max_image_mpixels:
args.append(f"--max-image-mpixels={max_image_mpixels}")
if rotate_pages_threshold:
args.append(f"--rotate-pages-threshold={rotate_pages_threshold}")
if fast_web_view:
args.append(f"--fast-web-view={fast_web_view}")
if continue_on_soft_render_error:
args.append("--continue-on-soft-render-error")
if verbose:
args.append(f"--verbose={verbose}")
if optimize > '0' and jpeg_quality:
args.append(f"--jpeg-quality={jpeg_quality}")
if optimize > '0' and png_quality:
args.append(f"--png-quality={png_quality}")
if jbig2_lossy:
args.append("--jbig2-lossy")
if jbig2_threshold:
args.append(f"--jbig2-threshold={jbig2_threshold}")
if jobs:
args.append(f"--jobs={jobs}")
input_file = NamedTemporaryFile(delete=True, suffix=f"_{uploaded.name}")
input_file.write(uploaded.getvalue())
input_file.flush()
input_file.seek(0)
args.append(str(input_file.name))
output_file = NamedTemporaryFile(delete=True, suffix=".pdf")
args.append(str(output_file.name))
st.session_state['running'] = (
'run_button' in st.session_state and st.session_state.run_button
)
if st.button(
"Run OCRmyPDF",
disabled=st.session_state.get("running", False),
key='run_button',
):
st.session_state['running'] = True
args = [sys.executable, '-u', '-m', "ocrmypdf"] + args
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
with st.container(border=True):
while proc.poll() is None:
line = proc.stderr.readline()
if line:
st.html("<code>" + line.decode().strip() + "</code>")
if proc.returncode != 0:
st.error(f"ocrmypdf failed with exit code {proc.returncode}")
st.session_state['running'] = False
st.stop()
if Path(output_file.name).stat().st_size == 0:
st.error("No output PDF file was generated")
st.stop()
st.download_button(
label="Download output PDF",
data=input_file.read(),
file_name=uploaded.name,
mime="application/pdf",
)
st.session_state['running'] = False
+56 -11
View File
@@ -1,12 +1,24 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2016 findingorder <https://github.com/findingorder>
# SPDX-FileCopyrightText: 2024 nilsro <https://github.com/nilsro>
# SPDX-License-Identifier: MIT
"""Example of using ocrmypdf as a library in a script.
This script will recursively search a directory for PDF files and run OCR on
them. It will log the results. It runs OCR on every file, even if it already
has text. OCRmyPDF will detect files that already have text.
You should edit this script to meet your needs.
"""
from __future__ import annotations
# This script must be edited to meet your needs.
import filecmp
import logging
import os
import posixpath
import shutil
import sys
from pathlib import Path
@@ -15,32 +27,65 @@ import ocrmypdf
# pylint: disable=logging-format-interpolation
# pylint: disable=logging-not-lazy
def filecompare(a, b):
try:
return filecmp.cmp(a, b, shallow=True)
except FileNotFoundError:
return False
script_dir = Path(__file__).parent
# set archive_dir to a path for backup original documents. Leave empty if not required.
archive_dir = "/pdfbak"
if len(sys.argv) > 1:
start_dir = Path(sys.argv[1])
else:
start_dir = Path('.')
start_dir = Path(".")
if len(sys.argv) > 2:
log_file = Path(sys.argv[2])
else:
log_file = script_dir.with_name('ocr-tree.log')
log_file = script_dir.with_name("ocr-tree.log")
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(message)s',
format="%(asctime)s %(message)s",
filename=log_file,
filemode='a',
filemode="a",
)
logging.info(f"Start directory {start_dir}")
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
for filename in start_dir.glob("**/*.py"):
for filename in start_dir.glob("**/*.pdf"):
logging.info(f"Processing {filename}")
result = ocrmypdf.ocr(filename, filename, deskew=True)
if result == ocrmypdf.ExitCode.already_done_ocr:
logging.error("Skipped document because it already contained text")
elif result == ocrmypdf.ExitCode.ok:
if ocrmypdf.pdfa.file_claims_pdfa(filename)["pass"]:
logging.info("Skipped document because it already contained text")
else:
archive_filename = archive_dir + str(filename)
if len(archive_dir) > 0 and not filecompare(filename, archive_filename):
logging.info(f"Archiving document to {archive_filename}")
try:
shutil.copy2(filename, posixpath.dirname(archive_filename))
except OSError:
os.makedirs(posixpath.dirname(archive_filename))
shutil.copy2(filename, posixpath.dirname(archive_filename))
try:
result = ocrmypdf.ocr(filename, filename, deskew=True)
logging.info(result)
except ocrmypdf.exceptions.EncryptedPdfError:
logging.info("Skipped document because it is encrypted")
except ocrmypdf.exceptions.PriorOcrFoundError:
logging.info("Skipped document because it already contained text")
except ocrmypdf.exceptions.DigitalSignatureError:
logging.info("Skipped document because it has a digital signature")
except ocrmypdf.exceptions.TaggedPDFError:
logging.info(
"Skipped document because it does not need ocr as it is tagged"
)
except Exception:
logging.error("Unhandled error occured")
logging.info("OCR complete")
logging.info(result)
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MIT
"""Helper script for bisecting PDFs to find a page with an issue."""
import sys
import pikepdf
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <input.pdf>")
sys.exit(1)
with pikepdf.open(sys.argv[1]) as pdf:
num_pages = len(pdf.pages)
low = 0
high = num_pages - 1
while low <= high:
mid = (low + high) // 2
with pikepdf.new() as new_pdf:
new_pdf.pages.extend(pdf.pages[low : mid + 1])
new_pdf.save(f"bisect-issue-{low + 1}-{mid + 1}.pdf")
print(f"Is bisect-issue-{low + 1}-{mid + 1}.pdf good or bad?", end=" ")
while True:
response = input().lower()
if response == "good":
low = mid + 1
break
elif response == "bad":
high = mid - 1
break
else:
print("Please respond with 'good' or 'bad'.")
print(f"The issue is on page {low + 1} of the original PDF.")
with pikepdf.new() as new_pdf:
new_pdf.pages.extend(pdf.pages[low])
new_pdf.save(f"bisect-issue-bad-{low + 1}.pdf")
with pikepdf.new() as new_pdf:
new_pdf.pages.extend(pdf.pages[:low])
new_pdf.pages.extend(pdf.pages[low + 1 :])
new_pdf.save(f"bisect-issue-good-{low + 1}.pdf")
+68 -46
View File
@@ -6,52 +6,56 @@ set -o errexit
__ocrmypdf_arguments()
{
local arguments="--help (show help message)
--language (language(s) of the file to be OCRed)
--image-dpi (assume this DPI if input image DPI is unknown)
--output-type (select PDF output options)
--sidecar (write OCR to text file)
--version (print program version and exit)
--jobs (how many worker processes to use)
--quiet (suppress INFO messages)
--verbose (set verbosity level)
--title (set metadata)
--author (set metadata)
--subject (set metadata)
--keywords (set metadata)
--rotate-pages (rotate pages to correct orientation)
--remove-background (attempt to remove background from pages)
--deskew (fix small horizontal alignment skew)
--clean (clean document images before OCR)
--clean-final (clean document images and keep result)
--unpaper-args (a quoted string of arguments to pass to unpaper)
--oversample (oversample images to this DPI)
--remove-vectors (don\'t send vector objects to OCR)
--threshold (threshold images before OCR)
--force-ocr (OCR documents that already have printable text)
--skip-text (skip OCR on any pages that already contain text)
--redo-ocr (redo OCR on any pages that seem to have OCR already)
--skip-big (skip OCR on pages larger than this many MPixels)
--optimize (select optimization level)
--jpeg-quality (JPEG quality [0..100])
--png-quality (PNG quality [0..100])
--jbig2-lossy (enable lossy JBIG2 (see docs))
--pages (apply OCR to only the specified pages)
--max-image-mpixels (image decompression bomb threshold)
--pdf-renderer (select PDF renderer options)
--rotate-pages-threshold (page rotation confidence)
--pdfa-image-compression (set PDF/A image compression options)
--fast-web-view (if file size if above this amount in MB linearize PDF)
--plugin (name of plugin to import)
--keep-temporary-files (keep temporary files (debug)
--tesseract-config (set custom tesseract config file)
--tesseract-pagesegmode (set tesseract --psm)
--tesseract-oem (set tesseract --oem)
--tesseract-thresholding (set tesseract image thresholding)
--tesseract-timeout (maximum number of seconds to wait for OCR)
--user-words (specify location of user words file)
--user-patterns (specify location of user patterns file)
--no-progress-bar (disable the progress bar)
local arguments="\
--help (show help message)
--language (language(s) of the file to be OCRed)
--image-dpi (assume this DPI if input image DPI is unknown)
--output-type (select PDF output options)
--sidecar (write OCR to text file)
--version (print program version and exit)
--jobs (how many worker processes to use)
--quiet (suppress INFO messages)
--verbose (set verbosity level)
--title (set metadata)
--author (set metadata)
--subject (set metadata)
--keywords (set metadata)
--rotate-pages (rotate pages to correct orientation)
--remove-background (attempt to remove background from pages)
--deskew (fix small horizontal alignment skew)
--clean (clean document images before OCR)
--clean-final (clean document images and keep result)
--unpaper-args (a quoted string of arguments to pass to unpaper)
--oversample (oversample images to this DPI)
--remove-vectors (don\'t send vector objects to OCR)
--threshold (threshold images before OCR)
--force-ocr (OCR documents that already have printable text)
--skip-text (skip OCR on any pages that already contain text)
--redo-ocr (redo OCR on any pages that seem to have OCR already)
--invalidate-digital-signatures (remove digital signatures from PDF)
--skip-big (skip OCR on pages larger than this many MPixels)
--optimize (select optimization level)
--jpeg-quality (JPEG quality [0..100])
--png-quality (PNG quality [0..100])
--jbig2-lossy (enable lossy JBIG2 (see docs))
--jbig2-threshold (set JBIG2 threshold (see docs))
--pages (apply OCR to only the specified pages)
--max-image-mpixels (image decompression bomb threshold)
--pdf-renderer (select PDF renderer options)
--rotate-pages-threshold (page rotation confidence)
--pdfa-image-compression (set PDF/A image compression options)
--fast-web-view (if file size if above this amount in MB linearize PDF)
--plugin (name of plugin to import)
--keep-temporary-files (keep temporary files (debug)
--tesseract-config (set custom tesseract config file)
--tesseract-pagesegmode (set tesseract --psm)
--tesseract-oem (set tesseract --oem)
--tesseract-thresholding (set tesseract image thresholding)
--tesseract-timeout (maximum number of seconds to wait for OCR)
--user-words (specify location of user words file)
--user-patterns (specify location of user patterns file)
--no-progress-bar (disable the progress bar)
--color-conversion-strategy (select color conversion strategy)
"
COMPREPLY=( $( compgen -W "$arguments" -- "$cur") )
@@ -191,6 +195,20 @@ sauvola (use Sauvola thresholding)"
fi
}
__ocrmypdf_color-conversion-strategy()
{
local choices="LeaveColorUnchanged (default)
CMYK (convert to CMYK)
Gray (convert to grayscale)
RGB (convert to RGB)
UseDeviceIndependentColor (convert with device independent color)"
COMPREPLY=( $( compgen -W "$choices" -- "$cur") )
# Remove description if only one completion exists
if [[ ${#COMPREPLY[*]} -eq 1 ]]; then
COMPREPLY=( ${COMPREPLY[0]%% *} )
fi
}
__ocrmypdf_check_previous()
{
@@ -250,6 +268,10 @@ __ocrmypdf_check_previous()
_filedir
return 0
;;
--color-conversion-strategy)
__ocrmypdf_color-conversion-strategy
return 0
;;
esac
return 1
+27 -2
View File
@@ -14,8 +14,9 @@ complete -c ocrmypdf -s i -l clean-final -d "clean document images and keep resu
complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
complete -c ocrmypdf -s s -l skip-text -d "skip OCR on any pages that already contain text"
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
complete -c ocrmypdf -l invalidate-digital-signatures -d "invalidate digital signatures and allow OCR to proceed"
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
@@ -83,6 +84,7 @@ complete -c ocrmypdf -x -l skip-big -d "skip OCR on pages larger than this many
complete -c ocrmypdf -x -l jpeg-quality -d "JPEG quality [0..100]"
complete -c ocrmypdf -x -l png-quality -d "PNG quality [0..100]"
complete -c ocrmypdf -x -l jbig2-lossy -d "enable lossy JBIG2 (see docs)"
complete -c ocrmypdf -x -l jbig2-threshold -d "JBIG2 compression threshold (see docs)"
complete -c ocrmypdf -x -l max-image-mpixels -d "image decompression bomb threshold"
complete -c ocrmypdf -x -l pages -d "apply OCR to only the specified pages"
complete -c ocrmypdf -x -l tesseract-config -d "set custom tesseract config file"
@@ -128,4 +130,27 @@ 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; __fish_complete_suffix .PDF; __fish_complete_suffix .jpg; __fish_complete_suffix .png)"
function __fish_ocrmypdf_color_conversion_strategy
echo -e "LeaveColorUnchanged\t"(_ "do not convert color spaces (default)")
echo -e "CMYK\t"(_ "convert all color spaces to CMYK")
echo -e "Gray\t"(_ "convert all color spaces to grayscale")
echo -e "RGB\t"(_ "convert all color spaces to RGB")
echo -e "UseDeviceIndependentColor\t"(_ "convert all color spaces to ICC-based color spaces")
end
complete -c ocrmypdf -x -l color-conversion-strategy -a '(__fish_ocrmypdf_color_conversion_strategy)' -d "set color conversion strategy"
function __fish_ocrmypdf_input_file_given
set -l tokens (commandline -opc)
for token in $tokens
if string match -q -r '^-' -- $token
continue
end
if test -f "$token"
return 0
end
end
return 1
end
complete -c ocrmypdf -x -n 'not __fish_ocrmypdf_input_file_given' -a "(__fish_complete_suffix .pdf)" -d "input file"
+2
View File
@@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MIT
---
version: "3.3"
services:
+1 -2
View File
@@ -1,8 +1,7 @@
# SPDX-FileCopyrightText: 2022 James R Barlow: https://github.com/jbarlow83
# SPDX-License-Identifier: MIT
"""
An example of an OCRmyPDF plugin.
"""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
+123
View File
@@ -0,0 +1,123 @@
# SPDX-FileCopyrightText: 2025 James R. Barlow
# SPDX-License-Identifier: MIT
"""Run OCRmyPDF on the same PDF with different options."""
from __future__ import annotations
import os
import shlex
from io import BytesIO
from pathlib import Path
from subprocess import check_output, run
from tempfile import TemporaryDirectory
import pikepdf
import pymupdf
import streamlit as st
from lxml import etree
from streamlit_pdf_viewer import pdf_viewer
def main():
st.set_page_config(layout="wide")
st.title("OCRmyPDF Compare")
st.write("Run OCRmyPDF on the same PDF with different options.")
uploaded_pdf = st.file_uploader("Upload a PDF", type=["pdf"])
if uploaded_pdf is None:
return
pdf_bytes = uploaded_pdf.read()
with pikepdf.open(BytesIO(pdf_bytes)) as p, TemporaryDirectory() as d:
with st.expander("PDF Metadata"):
with p.open_metadata() as meta:
xml_txt = str(meta)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.fromstring(xml_txt, parser=parser)
st.code(
etree.tostring(tree, pretty_print=True).decode("utf-8"),
language="xml",
)
st.write(p.docinfo)
st.write("Number of pages:", len(p.pages))
col1, col2 = st.columns(2)
with col1:
cli1 = st.text_area(
"Command line arguments for A",
key="args1",
value="ocrmypdf {in_} {out}",
)
env1 = st.text_area("Environment variables for A", key="env1")
args1 = shlex.split(
cli1.format(
in_=os.path.join(d, "input.pdf"),
out=os.path.join(d, "output1.pdf"),
)
)
st.code(shlex.join(args1))
with col2:
cli2 = st.text_area(
"Command line arguments for B",
key="args2",
value="ocrmypdf {in_} {out}",
)
env2 = st.text_area("Environment variables for B", key="env2")
args2 = shlex.split(
cli2.format(
in_=os.path.join(d, "input.pdf"),
out=os.path.join(d, "output2.pdf"),
)
)
st.code(shlex.join(args2))
if not st.button("Execute and Compare"):
return
with st.spinner("Executing..."):
Path(d, "input.pdf").write_bytes(pdf_bytes)
run(args1, env=dict(os.environ, **eval(env1 or "{}")))
run(args2, env=dict(os.environ, **eval(env2 or "{}")))
col1, col2 = st.columns(2)
with col1:
st.text(
"Ghostscript version A: "
+ check_output(
["gs", "--version"],
env=dict(os.environ, **eval(env1 or "{}")),
text=True,
)
)
with col2:
st.text(
"Ghostscript version B: "
+ check_output(
["gs", "--version"],
env=dict(os.environ, **eval(env2 or "{}")),
text=True,
)
)
doc1 = pymupdf.open(os.path.join(d, "output1.pdf"))
doc2 = pymupdf.open(os.path.join(d, "output2.pdf"))
for i, page1_2 in enumerate(zip(doc1, doc2)):
st.write(f"Page {i+1}")
page1, page2 = page1_2
col1, col2 = st.columns(2)
with col1, st.container(border=True):
st.write(page1.get_text())
with col2, st.container(border=True):
st.write(page2.get_text())
col1, col2 = st.columns(2)
with col1, st.expander("PDF Viewer"):
pdf_viewer(Path(d, "output1.pdf"))
with col2, st.expander("PDF Viewer"):
pdf_viewer(Path(d, "output2.pdf"))
if __name__ == "__main__":
main()
+83
View File
@@ -0,0 +1,83 @@
# SPDX-FileCopyrightText: 2025 James R. Barlow
# SPDX-License-Identifier: MIT
"""Compare two PDFs."""
from __future__ import annotations
import os
from io import BytesIO
from pathlib import Path
from tempfile import TemporaryDirectory
import pikepdf
import pymupdf
import streamlit as st
from lxml import etree
from streamlit_pdf_viewer import pdf_viewer
def do_metadata(pdf):
with pikepdf.open(pdf) as pdf:
with pdf.open_metadata() as meta:
xml_txt = str(meta)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.fromstring(xml_txt, parser=parser)
st.code(
etree.tostring(tree, pretty_print=True).decode("utf-8"),
language="xml",
)
st.write(pdf.docinfo)
st.write("Number of pages:", len(pdf.pages))
def main():
st.set_page_config(layout="wide")
st.title("PDF Compare")
st.write("Compare two PDFs.")
col1, col2 = st.columns(2)
with col1:
uploaded_pdf1 = st.file_uploader("Upload a PDF", type=["pdf"], key='pdf1')
with col2:
uploaded_pdf2 = st.file_uploader("Upload a PDF", type=["pdf"], key='pdf2')
if uploaded_pdf1 is None or uploaded_pdf2 is None:
return
pdf_bytes1 = uploaded_pdf1.getvalue()
pdf_bytes2 = uploaded_pdf2.getvalue()
with st.expander("PDF Metadata"):
col1, col2 = st.columns(2)
with col1:
do_metadata(BytesIO(pdf_bytes1))
with col2:
do_metadata(BytesIO(pdf_bytes2))
with TemporaryDirectory() as d:
Path(d, "1.pdf").write_bytes(pdf_bytes1)
Path(d, "2.pdf").write_bytes(pdf_bytes2)
with st.expander("Text"):
doc1 = pymupdf.open(os.path.join(d, "1.pdf"))
doc2 = pymupdf.open(os.path.join(d, "2.pdf"))
for i, page1_2 in enumerate(zip(doc1, doc2)):
st.write(f"Page {i+1}")
page1, page2 = page1_2
col1, col2 = st.columns(2)
with col1, st.container(border=True):
st.write(page1.get_text())
with col2, st.container(border=True):
st.write(page2.get_text())
with st.expander("PDF Viewer"):
col1, col2 = st.columns(2)
with col1:
pdf_viewer(Path(d, "1.pdf"), key='pdf_viewer1', render_text=True)
with col2:
pdf_viewer(Path(d, "2.pdf"), key='pdf_viewer2', render_text=True)
if __name__ == "__main__":
main()
+46
View File
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: 2025 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Compare text in PDFs."""
from __future__ import annotations
from subprocess import run
from tempfile import NamedTemporaryFile
from typing import Annotated
import typer
def main(
pdf1: Annotated[typer.FileBinaryRead, typer.Argument()],
pdf2: Annotated[typer.FileBinaryRead, typer.Argument()],
engine: Annotated[str, typer.Option()] = 'pdftotext',
):
"""Compare text in PDFs."""
text1 = run(
['pdftotext', '-layout', '-', '-'], stdin=pdf1, capture_output=True, check=True
)
text2 = run(
['pdftotext', '-layout', '-', '-'], stdin=pdf2, capture_output=True, check=True
)
with NamedTemporaryFile() as f1, NamedTemporaryFile() as f2:
f1.write(text1.stdout)
f1.flush()
f2.write(text2.stdout)
f2.flush()
diff = run(
['diff', '--color=always', '--side-by-side', f1.name, f2.name],
capture_output=True,
)
run(['less', '-R'], input=diff.stdout, check=True)
if text1.stdout.strip() != text2.stdout.strip():
return 1
return 0
if __name__ == '__main__':
typer.run(main)
+31
View File
@@ -0,0 +1,31 @@
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
To regenerate
=============
Using asciinema and svg-term (`npm install -g svg-term-cli`).
Create `~/.config/asciinema/config` to disable prompt.
```
[record]
command = fish --init-command 'alias fish_prompt="echo \>\ "'
```
Run asciinema
```
asciinema rec new_input.cast
```
Re-record faster version with fewer pauses
```
asciinema rec demo.cast -c "asciinema play new_input.cast --speed 2 --idle-time-limit 0.5"
```
Convert to SVG
```
svg-term --in=misc/screencast/demo.cast --out=misc/screencast/demo.svg --window
```
+65
View File
@@ -0,0 +1,65 @@
{"version": 2, "width": 131, "height": 24, "timestamp": 1687247006, "env": {"SHELL": "/usr/bin/fish", "TERM": "xterm-256color"}}
[0.103649, "o", "\u001b[?2004h\u001b]7; \u0007"]
[0.104223, "o", "\u001b]0;fish \u0007\u001b[30m\u001b(B\u001b[m\r> \u001b[K\r\u001b[C\u001b[C"]
[0.604542, "o", "o\r\u001b[3C\b\u001b[38;2;255;0;0mo\r\u001b[3C\u001b[30m\u001b(B\u001b[m\u001b[38;2;85;85;85mcrmypdf multipage.pdf multipage_with_ocr.pdf\r\u001b[3C\u001b[30m\u001b(B\u001b[m"]
[0.679571, "o", "\u001b[38;2;255;0;0mc\u001b[38;2;85;85;85mrmypdf multipage.pdf multipage_with_ocr.pdf\r\u001b[4C\u001b[30m\u001b(B\u001b[m"]
[0.767271, "o", "\u001b[38;2;255;0;0mr\u001b[38;2;85;85;85mmypdf multipage.pdf multipage_with_ocr.pdf\r\u001b[5C\u001b[30m\u001b(B\u001b[m"]
[0.814505, "o", "\u001b[38;2;255;0;0mm\u001b[38;2;85;85;85mypdf multipage.pdf multipage_with_ocr.pdf\r\u001b[6C\u001b[30m\u001b(B\u001b[m"]
[0.938919, "o", "\u001b[38;2;255;0;0my\u001b[38;2;85;85;85mpdf multipage.pdf multipage_with_ocr.pdf\r\u001b[7C\u001b[30m\u001b(B\u001b[m"]
[0.967347, "o", "\u001b[38;2;255;0;0mp\u001b[38;2;85;85;85mdf multipage.pdf multipage_with_ocr.pdf\r\u001b[8C\u001b[30m\u001b(B\u001b[m"]
[1.009954, "o", "\u001b[38;2;255;0;0md\u001b[38;2;85;85;85mf multipage.pdf multipage_with_ocr.pdf\r\u001b[9C\u001b[30m\u001b(B\u001b[m"]
[1.034488, "o", "\u001b[38;2;255;0;0mf\u001b[38;2;85;85;85m multipage.pdf multipage_with_ocr.pdf\r\u001b[10C\u001b[30m\u001b(B\u001b[m\b\b\b\b\b\b\b\b\u001b[38;2;0;95;215mocrmypdf\u001b[38;2;85;85;85m multipage.pdf multipage_with_ocr.pdf\r\u001b[10C\u001b[30m\u001b(B\u001b[m"]
[1.069226, "o", "\u001b[38;2;0;95;215m \u001b[38;2;85;85;85mmultipage.pdf multipage_with_ocr.pdf\r\u001b[11C\u001b[30m\u001b(B\u001b[m\b \u001b[38;2;85;85;85mmultipage.pdf multipage_with_ocr.pdf\r\u001b[11C\u001b[30m\u001b(B\u001b[m"]
[1.569682, "o", "-\u001b[K\r\u001b[12C\u001b[38;2;85;85;85m-version\r\u001b[12C\u001b[30m\u001b(B\u001b[m\b\u001b[38;2;0;175;255m-\u001b[38;2;85;85;85m-version\r\u001b[12C\u001b[30m\u001b(B\u001b[m"]
[1.642096, "o", "\u001b[38;2;0;175;255m-\u001b[38;2;85;85;85mversion\r\u001b[13C\u001b[30m\u001b(B\u001b[m"]
[1.71793, "o", "\u001b[38;2;0;175;255ms\u001b[30m\u001b(B\u001b[m\u001b[K\r\u001b[14C"]
[1.771483, "o", "\u001b[38;2;0;175;255mk\r\u001b[15C\u001b[30m\u001b(B\u001b[m"]
[1.864664, "o", "\u001b[38;2;0;175;255mi\r\u001b[16C\u001b[30m\u001b(B\u001b[m"]
[1.876085, "o", "\u001b[38;2;0;175;255mp\r\u001b[17C\u001b[30m\u001b(B\u001b[m"]
[2.092979, "o", "\u001b[38;2;0;175;255m-\r\u001b[18C\u001b[30m\u001b(B\u001b[m"]
[2.138821, "o", "\u001b[38;2;0;175;255mt\r\u001b[19C\u001b[30m\u001b(B\u001b[m"]
[2.18017, "o", "\u001b[38;2;0;175;255me\r\u001b[20C\u001b[30m\u001b(B\u001b[m"]
[2.268222, "o", "\u001b[38;2;0;175;255mx\r\u001b[21C\u001b[30m\u001b(B\u001b[m"]
[2.277031, "o", "\u001b[38;2;0;175;255mt\r\u001b[22C\u001b[30m\u001b(B\u001b[m"]
[2.322469, "o", "\u001b[38;2;0;175;255m \r\u001b[23C\u001b[30m\u001b(B\u001b[m\b \r\u001b[23C"]
[2.824696, "o", "m\r\u001b[24C\b\u001b[38;2;0;175;255m\u001b[4mm\r\u001b[24C\u001b[30m\u001b(B\u001b[m\u001b[38;2;85;85;85masks.pdf \r\u001b[24C\u001b[30m\u001b(B\u001b[m"]
[2.923234, "o", "\u001b[38;2;0;175;255m\u001b[4mu\u001b[30m\u001b(B\u001b[m\u001b[K\r\u001b[25C\u001b[38;2;85;85;85mltipage.pdf \r\u001b[25C\u001b[30m\u001b(B\u001b[m"]
[2.960685, "o", "\u001b[38;2;0;175;255m\u001b[4ml\u001b[38;2;85;85;85m\u001b[24mtipage.pdf \r\u001b[26C\u001b[30m\u001b(B\u001b[m"]
[3.03365, "o", "\u001b[38;2;0;175;255m\u001b[4mt\u001b[38;2;85;85;85m\u001b[24mipage.pdf \r\u001b[27C\u001b[30m\u001b(B\u001b[m"]
[3.479338, "o", "\u001b[38;2;0;175;255m\u001b[4mipage.pdf \r\u001b[37C\u001b[30m\u001b(B\u001b[m\b \r\u001b[37C"]
[3.754818, "o", "m\r\u001b[38C\b\u001b[38;2;0;175;255m\u001b[4mm\r\u001b[38C\u001b[30m\u001b(B\u001b[m\u001b[38;2;85;85;85masks.pdf \r\u001b[38C\u001b[30m\u001b(B\u001b[m"]
[3.873318, "o", "\u001b[38;2;0;175;255m\u001b[4mu\u001b[30m\u001b(B\u001b[m\u001b[K\r\u001b[39C\u001b[38;2;85;85;85mltipage.pdf \r\u001b[39C\u001b[30m\u001b(B\u001b[m"]
[3.926829, "o", "\u001b[38;2;0;175;255m\u001b[4ml\u001b[38;2;85;85;85m\u001b[24mtipage.pdf \r\u001b[40C\u001b[30m\u001b(B\u001b[m"]
[4.272251, "o", "\u001b[38;2;0;175;255m\u001b[4mtipage.pdf \r\u001b[51C\u001b[30m\u001b(B\u001b[m\b \r\u001b[51C"]
[4.343464, "o", "\r\u001b[50C"]
[4.416286, "o", "\r\u001b[49C"]
[4.490574, "o", "\r\u001b[48C"]
[4.564115, "o", "\r\u001b[47C"]
[4.630398, "o", "\r\u001b[46C"]
[4.76825, "o", "\u001b[38;2;0;175;255m\u001b[4m_.pd\u001b[30m\u001b(B\u001b[mf \r\u001b[47C\u001b[10D\u001b[38;2;0;175;255mmultipage_.pdf\u001b[30m\u001b(B\u001b[m \r\u001b[47C"]
[5.012506, "o", "\u001b[38;2;0;175;255mo.pd\u001b[30m\u001b(B\u001b[mf \r\u001b[48C\u001b[3C\u001b[38;2;0;175;255mf\u001b[30m\u001b(B\u001b[m \r\u001b[48C"]
[5.053615, "o", "\u001b[38;2;0;175;255mc.pd\u001b[30m\u001b(B\u001b[mf \r\u001b[49C\u001b[3C\u001b[38;2;0;175;255mf\u001b[30m\u001b(B\u001b[m \r\u001b[49C"]
[5.103957, "o", "\u001b[38;2;0;175;255mr.pd\u001b[30m\u001b(B\u001b[mf \r\u001b[50C\u001b[3C\u001b[38;2;0;175;255mf\u001b[30m\u001b(B\u001b[m \r\u001b[50C"]
[5.226183, "o", "\r\u001b[55C"]
[5.728321, "o", "\r\n\u001b[30m\u001b(B\u001b[m\u001b[?2004l\u001b]0;ocrmypdf --skip-text multipage.pdf multipage_ocr.pdf /home/jb/src/ocrmypdf/tests/resources\u0007\u001b[30m\u001b(B\u001b[m\r"]
[5.801032, "o", "\rScanning contents: 0%| | 0/6 [00:00<?, ?page/s]"]
[5.802664, "o", "\rScanning contents: 100%|█████████████████████████████████████████████████████████████████████████| 6/6 [00:00<00:00, 1270.68page/s]\r\n"]
[5.802747, "o", "Start processing 6 pages concurrently\r\n"]
[5.803488, "o", "\rOCR: 0%| | 0.0/6.0 [00:00<?, ?page/s]"]
[5.804896, "o", "\r \r 4 skipping all processing on this page\r\n\rOCR: 0%| | 0.0/6.0 [00:00<?, ?page/s]"]
[5.896969, "o", "\rOCR: 25%|█████████████████████▎ | 1.5/6.0 [00:00<00:00, 8.12page/s]"]
[6.170021, "o", "\rOCR: 42%|███████████████████████████████████▍ | 2.5/6.0 [00:00<00:01, 3.05page/s]"]
[6.292338, "o", "\rOCR: 58%|█████████████████████████████████████████████████▌ | 3.5/6.0 [00:00<00:00, 3.39page/s]"]
[6.586017, "o", "\rOCR: 75%|███████████████████████████████████████████████████████████████▊ | 4.5/6.0 [00:01<00:00, 2.49page/s]"]
[7.087058, "o", "\rOCR: 92%|█████████████████████████████████████████████████████████████████████████████▉ | 5.5/6.0 [00:06<00:00, 1.98s/page]\rOCR: 100%|█████████████████████████████████████████████████████████████████████████████████████| 6.0/6.0 [00:06<00:00, 1.09s/page]\r\nPostprocessing...\r\n"]
[7.104927, "o", "\rPDF/A conversion: 0%| | 0/6 [00:00<?, ?page/s]"]
[7.607392, "o", "\rPDF/A conversion: 50%|██████████████████████████████████████ | 3/6 [00:01<00:01, 1.61page/s]"]
[7.653781, "o", "\rPDF/A conversion: 83%|███████████████████████████████████████████████████████████████▎ | 5/6 [00:01<00:00, 2.90page/s]"]
[7.774532, "o", "\rPDF/A conversion: 100%|████████████████████████████████████████████████████████████████████████████| 6/6 [00:02<00:00, 2.71page/s]\r\n"]
[7.778252, "o", "\u001b[33mSome input metadata could not be copied because it is not permitted in PDF/A. You may wish to examine the output PDF's XMP metadata.\u001b[0m\r\n"]
[8.280789, "o", "\rRecompressing JPEGs: 0image [00:00, ?image/s]\rRecompressing JPEGs: 0image [00:00, ?image/s]\r\n\rDeflating JPEGs: 0%| | 0/4 [00:00<?, ?image/s]\rDeflating JPEGs: 100%|███████████████████████████████████████████████████████████████████████████| 4/4 [00:00<00:00, 238.28image/s]\r\n"]
[8.28149, "o", "\rJBIG2: 0item [00:00, ?item/s]\rJBIG2: 0item [00:00, ?item/s]\r\n"]
[8.289998, "o", "Image optimization ratio: 1.01 savings: 1.3%\r\nTotal file size ratio: 1.02 savings: 1.6%\r\n"]
[8.291209, "o", "Output file is a PDF/A-2B (as expected)\r\n"]
[8.361316, "o", "\u001b[2m⏎\u001b(B\u001b[m \r⏎ \r\u001b[K\u001b[?2004h\u001b]0;fish /home/jb/src/ocrmypdf/tests/resources\u0007\u001b[30m\u001b(B\u001b[m> \u001b[K\r\u001b[C\u001b[C"]
[8.862206, "o", "\r\n\u001b[30m\u001b(B\u001b[m\u001b[30m\u001b(B\u001b[m\u001b[?2004l"]
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

+7 -7
View File
@@ -2,6 +2,8 @@
# SPDX-FileCopyrightText: 2017 Enantiomerie
# SPDX-License-Identifier: MIT
"""Example OCRmyPDF for Synology NAS."""
from __future__ import annotations
# This script must be edited to meet your needs.
@@ -25,10 +27,7 @@ logging.basicConfig(
filemode='w',
)
if len(sys.argv) > 1:
start_dir = sys.argv[1]
else:
start_dir = '.'
start_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
for dir_name, _subdirs, file_list in os.walk(start_dir):
logging.info(dir_name)
@@ -54,9 +53,10 @@ for dir_name, _subdirs, file_list in os.walk(start_dir):
]
logging.info(cmd)
full_path_ocr = os.path.join(dir_name, filename_ocr)
with open(filename, 'rb') as input_file, open(
full_path_ocr, 'wb'
) as output_file:
with (
open(filename, 'rb') as input_file,
open(full_path_ocr, 'wb') as output_file,
):
proc = subprocess.run(
cmd,
stdin=input_file,
+243 -74
View File
@@ -3,165 +3,334 @@
# SPDX-FileCopyrightText: 2020 James R Barlow <https://github.com/jbarlow83>
# SPDX-License-Identifier: MIT
from __future__ import annotations
"""Watch a directory for new PDFs and OCR them."""
# Do not enable annotations!
# https://github.com/tiangolo/typer/discussions/598
import json
import logging
import os
import shutil
import sys
import time
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Annotated, Any
import pikepdf
import typer
from dotenv import load_dotenv
from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver
import ocrmypdf
load_dotenv()
# pylint: disable=logging-format-interpolation
def getenv_bool(name: str, default: str = 'False'):
return os.getenv(name, default).lower() in ('true', 'yes', 'y', '1')
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
ARCHIVE_DIRECTORY = os.getenv('OCR_ARCHIVE_DIRECTORY', '/processed')
OUTPUT_DIRECTORY_YEAR_MONTH = getenv_bool('OCR_OUTPUT_DIRECTORY_YEAR_MONTH')
ON_SUCCESS_DELETE = getenv_bool('OCR_ON_SUCCESS_DELETE')
ON_SUCCESS_ARCHIVE = getenv_bool('OCR_ON_SUCCESS_ARCHIVE')
DESKEW = getenv_bool('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 = getenv_bool('OCR_USE_POLLING')
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO')
PATTERNS = ['*.pdf', '*.PDF']
app = typer.Typer(name="ocrmypdf-watcher")
log = logging.getLogger('ocrmypdf-watcher')
def get_output_dir(root, basename):
if OUTPUT_DIRECTORY_YEAR_MONTH:
class LoggingLevelEnum(str, Enum):
"""Enum for logging levels."""
DEBUG = "DEBUG"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
CRITICAL = "CRITICAL"
def get_output_path(root: Path, basename: str, output_dir_year_month: bool) -> Path:
assert '/' not in basename, "basename must not contain '/'"
if output_dir_year_month:
today = datetime.today()
output_directory_year_month = (
Path(root) / str(today.year) / f'{today.month:02d}'
)
output_directory_year_month = root / str(today.year) / f'{today.month:02d}'
if not output_directory_year_month.exists():
output_directory_year_month.mkdir(parents=True, exist_ok=True)
output_path = Path(output_directory_year_month) / basename
output_path = Path(output_directory_year_month) / Path(basename).with_suffix(
'.pdf'
)
else:
output_path = Path(OUTPUT_DIRECTORY) / basename
output_path = root / Path(basename).with_suffix('.pdf')
return output_path
def wait_for_file_ready(file_path):
def wait_for_file_ready(
file_path: Path, poll_new_file_seconds: int, retries_loading_file: int
):
# This loop waits to make sure that the file is completely loaded on
# disk before attempting to read. Docker sometimes will publish the
# watchdog event before the file is actually fully on disk, causing
# pikepdf to fail.
retries = 5
while retries:
tries = retries_loading_file + 1
while tries:
try:
pdf = pikepdf.open(file_path)
except (FileNotFoundError, pikepdf.PdfError) as e:
with pikepdf.Pdf.open(file_path) as pdf:
log.debug(f"{file_path} ready with {pdf.pages} pages")
return True
except (FileNotFoundError, OSError) as e:
log.info(f"File {file_path} is not ready yet")
log.debug("Exception was", exc_info=e)
time.sleep(POLL_NEW_FILE_SECONDS)
retries -= 1
else:
pdf.close()
return True
time.sleep(poll_new_file_seconds)
tries -= 1
except pikepdf.PdfError as e:
log.info(f"File {file_path} is not full written yet")
log.debug("Exception was", exc_info=e)
time.sleep(poll_new_file_seconds)
tries -= 1
return False
def execute_ocrmypdf(file_path):
file_path = Path(file_path)
output_path = get_output_dir(OUTPUT_DIRECTORY, file_path.name)
def execute_ocrmypdf(
*,
file_path: Path,
archive_dir: Path,
output_dir: Path,
ocrmypdf_kwargs: dict[str, Any],
on_success_delete: bool,
on_success_archive: bool,
poll_new_file_seconds: int,
retries_loading_file: int,
output_dir_year_month: bool,
):
output_path = get_output_path(output_dir, file_path.name, output_dir_year_month)
log.info("-" * 20)
log.info(f'New file: {file_path}. Waiting until fully loaded...')
if not wait_for_file_ready(file_path):
log.info(f'New file: {file_path}. Waiting until fully written...')
if not wait_for_file_ready(file_path, poll_new_file_seconds, retries_loading_file):
log.info(f"Gave up waiting for {file_path} to become ready")
return
log.info(f'Attempting to OCRmyPDF to: {output_path}')
log.debug(
f'OCRmyPDF input_file={file_path} output_file={output_path} '
f'kwargs: {ocrmypdf_kwargs}'
)
exit_code = ocrmypdf.ocr(
input_file=file_path,
output_file=output_path,
deskew=DESKEW,
**OCR_JSON_SETTINGS,
**ocrmypdf_kwargs,
)
if exit_code == 0:
if ON_SUCCESS_DELETE:
if on_success_delete:
log.info(f'OCR is done. Deleting: {file_path}')
file_path.unlink()
elif ON_SUCCESS_ARCHIVE:
log.info(f'OCR is done. Archiving {file_path.name} to {ARCHIVE_DIRECTORY}')
shutil.move(file_path, f'{ARCHIVE_DIRECTORY}/{file_path.name}')
elif on_success_archive:
log.info(f'OCR is done. Archiving {file_path.name} to {archive_dir}')
shutil.move(file_path, f'{archive_dir}/{file_path.name}')
else:
log.info('OCR is done')
else:
log.info('OCR is done')
class HandleObserverEvent(PatternMatchingEventHandler):
def __init__( # noqa: D107
self,
patterns=None,
ignore_patterns=None,
ignore_directories=False,
case_sensitive=False,
settings={},
):
super().__init__(
patterns=patterns,
ignore_patterns=ignore_patterns,
ignore_directories=ignore_directories,
case_sensitive=case_sensitive,
)
self._settings = settings
def on_any_event(self, event):
if event.event_type in ['created']:
execute_ocrmypdf(event.src_path)
execute_ocrmypdf(file_path=Path(event.src_path), **self._settings)
def main():
@app.command()
def main(
input_dir: Annotated[
Path,
typer.Argument(
envvar='OCR_INPUT_DIRECTORY',
exists=True,
file_okay=False,
dir_okay=True,
readable=True,
resolve_path=True,
),
] = '/input',
output_dir: Annotated[
Path,
typer.Argument(
envvar='OCR_OUTPUT_DIRECTORY',
exists=True,
file_okay=False,
dir_okay=True,
writable=True,
resolve_path=True,
),
] = '/output',
archive_dir: Annotated[
Path,
typer.Argument(
envvar='OCR_ARCHIVE_DIRECTORY',
exists=True,
file_okay=False,
dir_okay=True,
writable=True,
resolve_path=True,
),
] = '/processed',
output_dir_year_month: Annotated[
bool,
typer.Option(
envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH',
help='Create a subdirectory in the output directory for each year/month',
),
] = False,
on_success_delete: Annotated[
bool,
typer.Option(
envvar='OCR_ON_SUCCESS_DELETE',
help='Delete the input file after successful OCR',
),
] = False,
on_success_archive: Annotated[
bool,
typer.Option(
envvar='OCR_ON_SUCCESS_ARCHIVE',
help='Archive the input file after successful OCR',
),
] = False,
deskew: Annotated[
bool,
typer.Option(
envvar='OCR_DESKEW',
help='Deskew the input file before OCR',
),
] = False,
ocr_json_settings: Annotated[
str,
typer.Option(
envvar='OCR_JSON_SETTINGS',
help='JSON settings to pass to OCRmyPDF (JSON string or file path)',
),
] = None,
poll_new_file_seconds: Annotated[
int,
typer.Option(
envvar='OCR_POLL_NEW_FILE_SECONDS',
help='Seconds to wait before polling a new file',
min=0,
),
] = 1,
use_polling: Annotated[
bool,
typer.Option(
envvar='OCR_USE_POLLING',
help='Use polling instead of filesystem events',
),
] = False,
retries_loading_file: Annotated[
int,
typer.Option(
envvar='OCR_RETRIES_LOADING_FILE',
help='Number of times to retry loading a file before giving up',
min=0,
),
] = 5,
loglevel: Annotated[
LoggingLevelEnum,
typer.Option(
envvar='OCR_LOGLEVEL',
help='Logging level',
),
] = LoggingLevelEnum.INFO,
patterns: Annotated[
str,
typer.Option(
envvar='OCR_PATTERNS',
help='File patterns to watch',
),
] = '*.pdf,*.PDF',
):
ocrmypdf.configure_logging(
verbosity=(
ocrmypdf.Verbosity.default
if LOGLEVEL != 'DEBUG'
if loglevel != LoggingLevelEnum.DEBUG
else ocrmypdf.Verbosity.debug
),
manage_root_logger=True,
)
log.setLevel(LOGLEVEL)
log.setLevel(loglevel.value)
log.info(
f"Starting OCRmyPDF watcher with config:\n"
f"Input Directory: {INPUT_DIRECTORY}\n"
f"Output Directory: {OUTPUT_DIRECTORY}\n"
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
f"Archive Directory: {ARCHIVE_DIRECTORY}"
f"Input Directory: {input_dir}\n"
f"Output Directory: {output_dir}\n"
f"Output Directory Year & Month: {output_dir_year_month}\n"
f"Archive Directory: {archive_dir}"
)
log.debug(
f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n"
f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n"
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
f"ARCHIVE_DIRECTORY: {ARCHIVE_DIRECTORY}\n"
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
f"ON_SUCCESS_ARCHIVE: {ON_SUCCESS_ARCHIVE}\n"
f"DESKEW: {DESKEW}\n"
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}"
f"INPUT_DIRECTORY: {input_dir}\n"
f"OUTPUT_DIRECTORY: {output_dir}\n"
f"OUTPUT_DIRECTORY_YEAR_MONTH: {output_dir_year_month}\n"
f"ARCHIVE_DIRECTORY: {archive_dir}\n"
f"ON_SUCCESS_DELETE: {on_success_delete}\n"
f"ON_SUCCESS_ARCHIVE: {on_success_archive}\n"
f"DESKEW: {deskew}\n"
f"ARGS: {ocr_json_settings}\n"
f"POLL_NEW_FILE_SECONDS: {poll_new_file_seconds}\n"
f"RETRIES_LOADING_FILE: {retries_loading_file}\n"
f"USE_POLLING: {use_polling}\n"
f"LOGLEVEL: {loglevel.value}"
)
if 'input_file' in OCR_JSON_SETTINGS or 'output_file' in OCR_JSON_SETTINGS:
log.error('OCR_JSON_SETTINGS should not specify input file or output file')
if ocr_json_settings and Path(ocr_json_settings).exists():
json_settings = json.loads(Path(ocr_json_settings).read_text())
else:
json_settings = json.loads(ocr_json_settings or '{}')
if 'input_file' in json_settings or 'output_file' in json_settings:
log.error(
'OCR_JSON_SETTINGS (--ocr-json-settings) may not specify input/output file'
)
sys.exit(1)
handler = HandleObserverEvent(patterns=PATTERNS)
if USE_POLLING:
handler = HandleObserverEvent(
patterns=patterns.split(','),
settings={
'archive_dir': archive_dir,
'output_dir': output_dir,
'ocrmypdf_kwargs': json_settings | {'deskew': deskew},
'on_success_delete': on_success_delete,
'on_success_archive': on_success_archive,
'poll_new_file_seconds': poll_new_file_seconds,
'retries_loading_file': retries_loading_file,
'output_dir_year_month': output_dir_year_month,
},
)
if use_polling:
observer = PollingObserver()
else:
observer = Observer()
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
observer.schedule(handler, input_dir, recursive=True)
observer.start()
typer.echo(f"Watching {input_dir} for new PDFs. Press Ctrl+C to exit.")
try:
while True:
time.sleep(1)
time.sleep(30)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
main()
app()
Regular → Executable
+23 -101
View File
@@ -1,107 +1,29 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2019 James R. Barlow
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2025 James R. Barlow
# SPDX-License-Identifier: AGPL-3.0-or-later
"""This is a simple web service/HTTP wrapper for OCRmyPDF
This may be more convenient than the command line tool for some Docker users.
Note that OCRmyPDF uses Ghostscript, which is licensed under AGPLv3+. While
OCRmyPDF is under GPLv3, this file is distributed under the Affero GPLv3+ license,
to emphasize that SaaS deployments should make sure they comply with
Ghostscript's license as well as OCRmyPDF's.
"""
from __future__ import annotations
"""Run the OCRmyPDF web service."""
import os
import shlex
from subprocess import PIPE, run
from tempfile import TemporaryDirectory
import sys
from flask import Flask, Response, request, send_from_directory
from werkzeug.utils import secure_filename
try:
import streamlit # noqa: F401
except ImportError:
raise ImportError(
'You need to install streamlit in the Python environment '
'to run the web service.\n'
)
app = Flask(__name__)
app.secret_key = "secret"
app.config['MAX_CONTENT_LENGTH'] = 50_000_000
app.config.from_envvar("OCRMYPDF_WEBSERVICE_SETTINGS", silent=True)
ALLOWED_EXTENSIONS = {"pdf"}
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def do_ocrmypdf(file):
uploaddir = TemporaryDirectory(prefix="ocrmypdf-upload")
downloaddir = TemporaryDirectory(prefix="ocrmypdf-download")
filename = secure_filename(file.filename)
up_file = os.path.join(uploaddir.name, filename)
file.save(up_file)
down_file = os.path.join(downloaddir.name, filename)
cmd_args = [arg for arg in shlex.split(request.form["params"])]
if "--sidecar" in cmd_args:
return Response("--sidecar not supported", 501, mimetype='text/plain')
ocrmypdf_args = ["ocrmypdf", *cmd_args, up_file, down_file]
proc = run(ocrmypdf_args, capture_output=True, encoding="utf-8")
if proc.returncode != 0:
stderr = proc.stderr
return Response(stderr, 400, mimetype='text/plain')
return send_from_directory(downloaddir.name, filename)
@app.route("/", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
if "file" not in request.files:
return Response("No file in POST", 400, mimetype='text/plain')
file = request.files["file"]
if file.filename == "":
return Response("Empty filename", 400, mimetype='text/plain')
if not allowed_file(file.filename):
return Response("Invalid filename", 400, mimetype='text/plain')
if file and allowed_file(file.filename):
return do_ocrmypdf(file)
return Response("Some other problem", 400, mimetype='text/plain')
return """
<!doctype html>
<title>OCRmyPDF webservice</title>
<h1>Upload a PDF (debug UI)</h1>
<form method=post enctype=multipart/form-data>
<label for="args">Command line parameters</label>
<input type=textbox name=params>
<label for="file">File to upload</label>
<input type=file name=file>
<input type=submit value=Upload>
</form>
<h4>Notice</h2>
<div style="font-size: 70%; max-width: 34em;">
<p>This is a webservice wrapper for OCRmyPDF.</p>
<p>Copyright 2019 James R. Barlow</p>
<p>This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
</p>
<p>This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
</p>
<p>
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.
</p>
</div>
"""
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
if __name__ == '__main__':
os.execvp(
sys.executable,
[
sys.executable,
'-m',
'streamlit',
'run',
'misc/_webservice.py',
*sys.argv[1:],
],
)
+118 -59
View File
@@ -1,40 +1,87 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
[build-system]
requires = [
"setuptools >= 52",
"setuptools_scm[toml] >= 7.0.5",
"wheel"
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "ocrmypdf"
dynamic = ["version"]
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
readme = "README.md"
license = { text = "MPL-2.0" }
requires-python = ">=3.10"
dependencies = [
"deprecation>=2.1.0",
"img2pdf>=0.5",
"packaging>=20",
"pdfminer.six>=20220319",
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
"pikepdf>=8.10.1",
"Pillow>=10.0.1",
"pluggy>=1",
"rich>=13",
]
build-backend = "setuptools.build_meta"
authors = [{ name = "James R. Barlow", email = "james@purplerock.ca" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Operating System :: POSIX :: BSD",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Image Recognition",
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
]
keywords = ["PDF", "OCR", "optical character recognition", "PDF/A", "scanning"]
[tool.setuptools_scm]
[project.urls]
Documentation = "https://ocrmypdf.readthedocs.io/"
Source = "https://github.com/ocrmypdf/OCRmyPDF"
Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues"
Changelog = "https://github.com/ocrmypdf/OCRmyPDF/docs/release_notes.rst"
[tool.black]
line-length = 88
target-version = ["py37", "py38"]
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| docs
| misc
| \.egg-info
)/
'''
[project.optional-dependencies]
docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"]
extended_test = ["PyMuPDF>=1.19.1"]
test = [
"coverage[toml]>=6.2",
"hypothesis>=6.36.0",
"pytest>=6.2.5",
"pytest-cov>=3.0.0",
"pytest-xdist>=2.5.0",
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
"reportlab>=3.6.8",
"types-Pillow",
"types-humanfriendly",
]
watcher = ["watchdog>=1.0.2", "typer-slim[standard]", "python-dotenv"]
webservice = ["streamlit>=1.41.0"]
[project.scripts]
ocrmypdf = "ocrmypdf.__main__:run"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "src/ocrmypdf/_version.py"
[tool.distutils.bdist_wheel]
python-tag = "py310"
[tool.coverage.run]
branch = true
parallel = true
concurrency = ["multiprocessing"]
concurrency = ["multiprocessing", "thread"]
sigterm = true
[tool.coverage.paths]
source = ["src/ocrmypdf"]
@@ -51,28 +98,7 @@ exclude_lines = [
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:"
]
[tool.isort]
profile = "black"
known_first_party = "ocrmypdf"
known_third_party = [
"PIL",
"flask",
"img2pdf",
"ocrmypdf",
"pdfminer",
"pikepdf",
"pkg_resources",
"pluggy",
"pytest",
"reportlab",
"setuptools",
"sphinx_rtd_theme",
"tqdm",
"watchdog",
"werkzeug"
"if TYPE_CHECKING:",
]
[tool.pytest.ini_options]
@@ -81,25 +107,58 @@ norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
testpaths = ["tests"]
addopts = "-n auto"
markers = ["slow"]
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
filterwarnings = [
"ignore:.*XMLParser.*:DeprecationWarning",
"ignore:.*ast.NameConstant.*:DeprecationWarning:reportlab",
"ignore:.*distutils.*:DeprecationWarning:libxmp",
]
[tool.mypy]
[[tool.mypy.overrides]]
module = [
'pluggy',
'tqdm',
'coloredlogs',
'img2pdf',
'pdfminer.*',
'reportlab.*',
'fitz',
'libxmp.utils',
'importlib_metadata'
]
ignore_missing_imports = true
[tool.pylint.basic]
good-names = ["i", "j", "k", "ex", "Run", "_", "e", "p", "im", "w", "h", "m", "x", "y", "a", "b", "fp", "n", "f", "s", "v", "q", "dx", "dy"]
logging-format-style = "old"
disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "logging-fstring-interpolation", "missing-function-docstring", "too-few-public-methods"]
[tool.ruff]
target-version = "py310"
[tool.ruff.lint]
"select" = [
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"I001", # isort
"UP", # pyupgrade
]
[tool.ruff.lint.isort]
known-first-party = ["ocrmypdf"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "D101", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"misc/*.py" = ["D103", "D101", "D102"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
[tool.ruff.format]
quote-style = "preserve"
[dependency-groups]
dev = [
"mypy>=1.13.0",
"pymupdf>=1.24.14",
"streamlit-pdf-viewer>=0.0.19",
"streamlit>=1.40.2",
"ipykernel>=6.29.5",
]
-116
View File
@@ -1,116 +0,0 @@
[metadata]
name = ocrmypdf
description = OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/ocrmypdf/OCRmyPDF
author = James R. Barlow
author_email = james@purplerock.ca
license = MPL-2.0
license_file = LICENSE
license_files =
LICENSE
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: End Users/Desktop
Intended Audience :: Science/Research
Intended Audience :: System Administrators
License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows :: Windows 10
Operating System :: POSIX
Operating System :: POSIX :: BSD
Operating System :: POSIX :: Linux
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Scientific/Engineering :: Image Recognition
Topic :: Text Processing :: Indexing
Topic :: Text Processing :: Linguistic
keywords =
PDF
OCR
optical character recognition
PDF/A
scanning
project_urls =
Documentation = https://ocrmypdf.readthedocs.io/
Source = https://github.com/ocrmypdf/OCRmyPDF
Tracker = https://github.com/ocrmypdf/OCRmyPDF/issues
[options]
packages = find:
install_requires =
Pillow>=8.2.0
coloredlogs>=14.0 # strictly optional
img2pdf>=0.3.0 # pure Python
packaging>=20
pdfminer.six!=20200720,>=20191110
pikepdf!=5.0.0,>=4.0.0
pluggy>=0.13.0
reportlab>=3.5.66
tqdm>=4
importlib-metadata>=4;python_version<'3.8' # until Python 3.8
importlib-resources>=5;python_version<'3.9' # until Python 3.9
typing-extensions>=4;python_version<'3.10'
python_requires = >=3.7
include_package_data = True
package_dir =
=src
platforms = any
setup_requires =
setuptools-scm
setuptools-scm-git-archive
zip_safe = False
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
ocrmypdf = ocrmypdf.__main__:run
[options.extras_require]
docs =
sphinx
sphinx-issues
sphinx-rtd-theme
extended_test =
PyMuPDF==1.19.1
test =
coverage[toml]>=5
pytest>=6.0.0
pytest-cov>=2.11.1
pytest-xdist>=2.2.0
python-xmp-toolkit==2.0.1 # also requires apt-get install libexempi3
types-Pillow
types-humanfriendly
watcher =
watchdog>=1.0.2
webservice =
Flask>=1
[options.package_data]
ocrmypdf =
data/sRGB.icc
py.typed
[bdist_wheel]
python-tag = py37
[aliases]
test = pytest
[check-manifest]
ignore =
.github
[flake8]
ignore = D203,F401,W503,E501,E203,F841
exclude = .git,__pycache__,docs/conf.py,build,dist,.venv,.venvpp,.eggs,tmp,src/ocrmypdf/lib/
max-complexity = 10
max-line-length = 100
-10
View File
@@ -1,10 +0,0 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""setup.py to support older setuptools and pip."""
from __future__ import annotations
from setuptools import setup
setup()
+27 -5
View File
@@ -1,8 +1,13 @@
# SPDX-FileCopyrightText: 2022 Alexander Langanke
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-FileCopyrightText: 2023 林博仁(Buo-ren, Lin) <Buo.Ren.Lin@gmail.com>
# SPDX-License-Identifier: MPL-2.0
name: ocrmypdf
title: OCRmyPDF
base: core20
base: core22
version: git
summary: OCRmyPDF adds optical character recognition (OCR) to PDFs
summary: OCRmyPDF adds a searchable text layer to scanned PDF files
description: OCRmyPDF packaged for snap
grade: stable
confinement: strict
@@ -13,8 +18,8 @@ architectures: [amd64]
environment:
TESSDATA_PREFIX: $SNAP/usr/share/tesseract-ocr/4.00/tessdata
GS_LIB: $SNAP/usr/share/ghostscript/9.50/Resource/Init
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.50/Resource/Font
GS_LIB: $SNAP/usr/share/ghostscript/9.55.0/Resource/Init
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55.0/Resource/Font
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
apps:
@@ -41,9 +46,19 @@ parts:
stage-packages:
- lib32stdc++6
jbig2enc:
plugin: autotools
source: https://github.com/agl/jbig2enc.git
source-tag: "0.29"
build-packages:
- libleptonica-dev
ocrmypdf:
plugin: python
source: https://github.com/ocrmypdf/OCRmyPDF.git
source: .
build-packages:
- python3-pip
stage-packages:
- ghostscript
@@ -66,7 +81,14 @@ parts:
- setuptools
- tqdm
- pipe
- wheel
override-build: |
pip3 install --user dephell[full]
$HOME/.local/bin/dephell deps convert \
--from-path pyproject.toml \
--from-format pyproject \
--to-path setup.py \
--to-format setuppy
snapcraftctl build
ln -sf ../usr/lib/libsnapcraft-preload.so $SNAPCRAFT_PART_INSTALL/lib/libsnapcraft-preload.so
+5 -2
View File
@@ -1,3 +1,6 @@
<!-- SPDX-FileCopyrightText: 2022 James R. Barlow -->
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
# Release checklist
## Patch release
@@ -14,11 +17,11 @@
- Check README.md
- Check setup.py
- Check pyproject.toml
- Are classifiers up to date?
- Is `python_requires` correct?
- Python 3.6 is EOL on December 2021-12. Could drop support then.
- Is it to drop support for older Pythons?
- Can we tighten any `install_requires` dependencies?
- Search for old version shims we can remove
+41 -3
View File
@@ -9,9 +9,17 @@ from pluggy import HookimplMarker as _HookimplMarker
from ocrmypdf import helpers, hocrtransform, pdfa, pdfinfo
from ocrmypdf._concurrent import Executor
from ocrmypdf._defaults import PROGRAM_NAME
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._version import PROGRAM_NAME, __version__
from ocrmypdf.api import Verbosity, configure_logging, ocr
from ocrmypdf._pipelines._common import (
configure_debug_logging,
)
from ocrmypdf._version import __version__
from ocrmypdf.api import (
Verbosity,
configure_logging,
ocr,
)
from ocrmypdf.exceptions import (
BadArgsError,
DpiError,
@@ -21,7 +29,6 @@ from ocrmypdf.exceptions import (
InputFileError,
MissingDependencyError,
OutputFileAccessError,
PdfMergeFailedError,
PriorOcrFoundError,
SubprocessOutputError,
TesseractConfigError,
@@ -30,3 +37,34 @@ from ocrmypdf.exceptions import (
from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence
hookimpl = _HookimplMarker('ocrmypdf')
__all__ = [
'__version__',
'BadArgsError',
'configure_debug_logging',
'configure_logging',
'DpiError',
'EncryptedPdfError',
'Executor',
'ExitCode',
'ExitCodeException',
'helpers',
'hocrtransform',
'hookimpl',
'InputFileError',
'MissingDependencyError',
'ocr',
'OcrEngine',
'OrientationConfidence',
'OutputFileAccessError',
'PageContext',
'pdfa',
'PdfContext',
'pdfinfo',
'PriorOcrFoundError',
'PROGRAM_NAME',
'SubprocessOutputError',
'TesseractConfigError',
'UnsupportedImageFormatError',
'Verbosity',
]
+12 -5
View File
@@ -7,15 +7,15 @@
from __future__ import annotations
import logging
import multiprocessing
import os
import signal
import sys
from contextlib import suppress
from multiprocessing import set_start_method
from ocrmypdf import __version__
from ocrmypdf._pipelines.ocr import run_pipeline_cli
from ocrmypdf._plugin_manager import get_parser_options_plugins
from ocrmypdf._sync import run_pipeline
from ocrmypdf._validation import check_options
from ocrmypdf.api import Verbosity, configure_logging
from ocrmypdf.exceptions import (
@@ -29,10 +29,16 @@ log = logging.getLogger('ocrmypdf')
def sigbus(*args):
"""Handle SIGBUS signals.
pikepdf, depending on configuration, may use mmap so SIGBUS is a
possibility.
"""
raise InputFileError("Lost access to the input file")
def run(args=None):
"""Run the ocrmypdf command line interface."""
_parser, options, plugin_manager = get_parser_options_plugins(args=args)
with suppress(AttributeError, PermissionError):
@@ -66,11 +72,12 @@ def run(args=None):
with suppress(AttributeError, OSError):
signal.signal(signal.SIGBUS, sigbus)
result = run_pipeline(options=options, plugin_manager=plugin_manager)
result = run_pipeline_cli(options=options, plugin_manager=plugin_manager)
return result
if __name__ == '__main__':
if sys.platform == 'darwin' and sys.version_info < (3, 8):
set_start_method('spawn') # see python bpo-33725
multiprocessing.freeze_support()
if os.name == 'posix':
multiprocessing.set_start_method('forkserver')
sys.exit(run())
+66
View File
@@ -0,0 +1,66 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""OCRmyPDF PDF annotation cleanup."""
from __future__ import annotations
import logging
from pikepdf import Dictionary, Name, NameTree, Pdf
log = logging.getLogger(__name__)
def remove_broken_goto_annotations(pdf: Pdf) -> bool:
"""Remove broken goto annotations from a PDF.
If a PDF contains a GoTo Action that points to a named destination that does not
exist, Ghostscript PDF/A conversion will fail. In any event, a named destination
that is not defined is not useful.
Args:
pdf: Opened PDF file.
Returns:
bool: True if the file was modified, False if not.
"""
modified = False
# Check if there are any named destinations
if Name.Names not in pdf.Root:
return modified
if Name.Dests not in pdf.Root[Name.Names]:
return modified
dests = pdf.Root[Name.Names][Name.Dests]
if not isinstance(dests, Dictionary):
return modified
nametree = NameTree(dests)
# Create a set of all named destinations
names = set(k for k in nametree.keys())
for n, page in enumerate(pdf.pages):
if Name.Annots not in page:
continue
for annot in page[Name.Annots]:
if not isinstance(annot, Dictionary):
continue
if Name.A not in annot or Name.D not in annot[Name.A]:
continue
# We found an annotation that points to a named destination
named_destination = str(annot[Name.A][Name.D])
if named_destination not in names:
# If there is no corresponding named destination, remove the
# annotation. Having no destination set is still valid and just
# makes the link non-functional.
log.warning(
f"Disabling a hyperlink annotation on page {n + 1} to a "
"non-existent named destination "
f"{named_destination}."
)
del annot[Name.A][Name.D]
modified = True
return modified
+19 -28
View File
@@ -7,27 +7,20 @@ from __future__ import annotations
import threading
from abc import ABC, abstractmethod
from typing import Callable, Iterable
from collections.abc import Callable, Iterable
from typing import Any, TypeVar
from ocrmypdf._progressbar import NullProgressBar, ProgressBar
T = TypeVar('T')
def _task_noop(*_args, **_kwargs):
return
class NullProgressBar:
"""Progress bar API that takes no actions."""
def __init__(self, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
def update(self, _arg=None):
return
def _task_finished_noop(_result: Any, pbar: ProgressBar):
pbar.update()
class Executor(ABC):
@@ -45,14 +38,13 @@ class Executor(ABC):
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
progress_kwargs: dict,
worker_initializer: Callable | None = None,
task: Callable | None = None,
task: Callable[..., T] | None = None,
task_arguments: Iterable | None = None,
task_finished: Callable | None = None,
task_finished: Callable[[T, ProgressBar], None] | None = None,
) -> None:
"""
Set up parallel execution and progress reporting.
"""Set up parallel execution and progress reporting.
Args:
use_threads: If ``False``, the workload is the sort that will benefit from
@@ -60,7 +52,7 @@ class Executor(ABC):
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.
progress_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.
@@ -73,13 +65,12 @@ class Executor(ABC):
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
task_finished = _task_finished_noop
if not task:
task = _task_noop
@@ -87,7 +78,7 @@ class Executor(ABC):
self._execute(
use_threads=use_threads,
max_workers=max_workers,
tqdm_kwargs=tqdm_kwargs,
progress_kwargs=progress_kwargs,
worker_initializer=worker_initializer,
task=task,
task_arguments=task_arguments,
@@ -100,7 +91,7 @@ class Executor(ABC):
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
progress_kwargs: dict,
worker_initializer: Callable,
task: Callable,
task_arguments: Iterable,
@@ -126,13 +117,13 @@ class SerialExecutor(Executor):
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
progress_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:
with self.pbar_class(**progress_kwargs) as pbar:
for args in task_arguments:
result = task(args)
result = task(*args)
task_finished(result, pbar)
+10
View File
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Enforce English hegemony
DEFAULT_LANGUAGE = 'eng'
# Default rotation threshold
DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0
PROGRAM_NAME = 'OCRmyPDF'
+1 -1
View File
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Manage third party executables"""
"""Manage third party executables."""
from __future__ import annotations
+129 -82
View File
@@ -1,65 +1,79 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Interface to Ghostscript executable"""
"""Interface to Ghostscript executable."""
from __future__ import annotations
import logging
import os
import re
import sys
from collections import deque
from io import BytesIO
from os import fspath
from pathlib import Path
from subprocess import PIPE, CalledProcessError
from packaging.version import Version
from PIL import Image, UnidentifiedImageError
from ocrmypdf.exceptions import SubprocessOutputError
from ocrmypdf.exceptions import (
ColorConversionNeededError,
InputFileError,
SubprocessOutputError,
)
from ocrmypdf.helpers import Resolution
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
# Remove this workaround when we require Pillow >= 10
try:
Transpose = Image.Transpose # type: ignore
except AttributeError:
# Pillow 9 shim
Transpose = Image # type: ignore
COLOR_CONVERSION_STRATEGIES = frozenset(
[
'CMYK',
'Gray',
'LeaveColorUnchanged',
'RGB',
'UseDeviceIndependentColor',
]
)
# Ghostscript executable - gswin32c is not supported
GS = 'gswin64c' if os.name == 'nt' else 'gs'
log = logging.getLogger(__name__)
# Most reliable what to get the bitness of Python interpreter, according to Python docs
_IS_64BIT = sys.maxsize > 2**32
_GSWIN = None
if os.name == 'nt':
if _IS_64BIT:
_GSWIN = 'gswin64c'
else:
_GSWIN = 'gswin32c'
class DuplicateFilter(logging.Filter):
"""Filter out duplicate log messages.
GS = _GSWIN if _GSWIN else 'gs'
del _GSWIN
def version():
return get_version(GS)
def jpeg_passthrough_available() -> bool:
"""Returns True if the installed version of Ghostscript supports JPEG passthru
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. (Ghostscript 9.24 has its own problems is blacklisted.)
A context window of default 5 messages is used to determine if a message is a
duplicate. This is because some Ghostscript messages are word wrapped.
"""
return version() >= '9.24'
def __init__(self, logger: logging.Logger, context_window=5):
self.window: deque[str] = deque([], maxlen=context_window)
self.logger = logger
self.levelno = logging.DEBUG
self.count = 0
def filter(self, record):
if record.msg in self.window:
self.count += 1
self.levelno = record.levelno
return False
else:
if self.count >= 1:
rep_msg = f"(suppressed {self.count} repeated lines)"
self.count = 0 # Avoid infinite recursion
self.logger.log(self.levelno, rep_msg)
self.window.clear()
self.window.append(record.msg)
return True
log.addFilter(DuplicateFilter(log))
def version() -> Version:
return Version(get_version(GS))
def _gs_error_reported(stream) -> bool:
@@ -67,6 +81,20 @@ def _gs_error_reported(stream) -> bool:
return bool(match)
def _gs_devicen_reported(stream) -> bool:
"""Did Ghostscript warn about a DeviceN with inappropriate alternate?
If so, we need the user to select a color conversion, or the resulting PDF will
not present correctly in some PDF viewers.
"""
match = re.search(
r'DeviceN.*inappropriate alternate',
stream,
flags=re.IGNORECASE | re.MULTILINE,
)
return bool(match)
def rasterize_pdf(
input_file: os.PathLike,
output_file: os.PathLike,
@@ -77,6 +105,7 @@ def rasterize_pdf(
page_dpi: Resolution | None = None,
rotation: int | None = None,
filter_vector: bool = False,
stop_on_error: bool = False,
):
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units."""
raster_dpi = raster_dpi.round(6)
@@ -86,7 +115,6 @@ def rasterize_pdf(
args_gs = (
[
GS,
'-dQUIET',
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
@@ -97,9 +125,10 @@ def rasterize_pdf(
f'-r{raster_dpi.x:f}x{raster_dpi.y:f}',
]
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
+ [
'-o',
'-',
fspath(output_file),
'-sstdout=%stderr', # Literal %s, not string interpolation
'-dAutoRotatePages=/None', # Probably has no effect on raster
'-f',
@@ -111,33 +140,48 @@ def rasterize_pdf(
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
except CalledProcessError as e:
log.error(e.stderr.decode(errors='replace'))
raise SubprocessOutputError('Ghostscript rasterizing failed') from e
else:
stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr):
log.error(stderr)
Path(output_file).unlink(missing_ok=True)
raise SubprocessOutputError("Ghostscript rasterizing failed") from e
stderr = p.stderr.decode(errors='replace')
if _gs_error_reported(stderr):
log.error(stderr)
if stop_on_error and "recoverable image error" in stderr:
Path(output_file).unlink(missing_ok=True)
raise InputFileError(
"Ghostscript rasterizing failed. The input file contains errors that "
"cause PDF viewers to interpret it differently and incorrectly. "
"Try using --continue-on-soft-render-error and manually inspect the "
"input and output files to check for visual differences or errors."
)
try:
with Image.open(BytesIO(p.stdout)) as im:
with Image.open(output_file) as im:
if rotation is not None:
log.debug("Rotating output by %i", rotation)
# rotation is a clockwise angle and Image.ROTATE_* is
# counterclockwise so this cancels out the rotation
if rotation == 90:
im = im.transpose(Transpose.ROTATE_90)
im = im.transpose(Image.Transpose.ROTATE_90)
elif rotation == 180:
im = im.transpose(Transpose.ROTATE_180)
im = im.transpose(Image.Transpose.ROTATE_180)
elif rotation == 270:
im = im.transpose(Transpose.ROTATE_270)
im = im.transpose(Image.Transpose.ROTATE_270)
if rotation % 180 == 90:
page_dpi = page_dpi.flip_axis()
im.save(fspath(output_file), dpi=page_dpi)
im.save(output_file, dpi=page_dpi)
except UnidentifiedImageError:
log.error(
f"Ghostscript (using {raster_device} at {raster_dpi} dpi) produced "
"an invalid page image file."
)
raise
except OSError as e:
log.error(
f"Ghostscript (using {raster_device} at {raster_dpi} dpi) produced "
"an invalid page image file."
)
raise UnidentifiedImageError() from e
class GhostscriptFollower:
@@ -151,6 +195,17 @@ class GhostscriptFollower:
self.progressbar_class = progressbar_class
self.progressbar = None
def __enter__(self):
# We can't actually set up the progressbar here, because we don't know
# how many pages there are until the first __call__() happens. So we
# do it in __call__().
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.progressbar:
return self.progressbar.__exit__(exc_type, exc_value, traceback)
return False
def __call__(self, line):
if not self.progressbar_class:
return
@@ -161,7 +216,8 @@ class GhostscriptFollower:
self.progressbar = self.progressbar_class(
total=self.count, desc="PDF/A conversion", unit='page'
)
return
# Now that we know the count, we can set up the progressbar.
self.progressbar.__enter__()
else:
if self.re_page.match(line.strip()):
self.progressbar.update()
@@ -172,9 +228,11 @@ def generate_pdfa(
output_file: os.PathLike,
*,
compression: str,
color_conversion_strategy: str,
pdf_version: str = '1.5',
pdfa_part: str = '2',
progressbar_class=None,
stop_on_error: bool = False,
):
# 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.
@@ -200,23 +258,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.
gs_version = version()
strategy = ('/' + strategy) if gs_version < '9.19' else strategy
if gs_version == '9.23':
# 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')
elif gs_version == '9.56.0':
# 9.56.0 breaks our OCR...?
if gs_version == Version('9.56.0'):
# 9.56.0 breaks our OCR, should be fixed in 9.56.1
# https://bugs.ghostscript.com/show_bug.cgi?id=705187
compression_args.append('-dNEWPDF=false')
if os.name == 'nt':
# Windows has lots of fatal "permission denied" errors
stop_on_error = False
# nb no need to specify ProcessColorModel when ColorConversionStrategy
# is set; see:
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
@@ -226,34 +277,33 @@ def generate_pdfa(
"-dBATCH",
"-dNOPAUSE",
"-dSAFER",
"-dCompatibilityLevel=" + str(pdf_version),
f"-dCompatibilityLevel={str(pdf_version)}",
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
f"-sColorConversionStrategy={color_conversion_strategy}",
]
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
+ compression_args
+ [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
f"-dPDFA={pdfa_part}",
"-dPDFACompatibilityPolicy=1",
"-o",
"-",
fspath(output_file),
"-sstdout=%stderr", # Literal %s, not string interpolation
]
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try:
with Path(output_file).open('wb') as output:
with GhostscriptFollower(progressbar_class) as pbar:
p = run_polling_stderr(
args_gs,
stdout=output,
stderr=PIPE,
check=True,
text=True,
encoding='utf-8',
errors='replace',
callback=GhostscriptFollower(progressbar_class),
callback=pbar,
)
except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create
@@ -265,14 +315,11 @@ def generate_pdfa(
# If there is an error we log the whole stderr, except for filtering
# duplicates.
if _gs_error_reported(stderr):
last_part = None
repcount = 0
# Ghostscript outputs the pattern **** Error: .... frequently.
# Occasionally the error message is spammed many times. We filter
# out duplicates of this message using the filter above. We use
# the **** pattern to split the stderr into parts.
for part in stderr.split('****'):
if part != last_part:
if repcount > 1:
log.error(f"(previous error message repeated {repcount} times)")
repcount = 0
log.error(part)
else:
repcount += 1
last_part = part
log.error(part)
if _gs_devicen_reported(stderr):
raise ColorConversionNeededError()
+19 -17
View File
@@ -1,18 +1,26 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Interface to jbig2 executable"""
"""Interface to jbig2 executable."""
from __future__ import annotations
from subprocess import PIPE
from subprocess import PIPE, CalledProcessError
from packaging.version import Version
from ocrmypdf.exceptions import MissingDependencyError
from ocrmypdf.subprocess import get_version, run
def version():
return get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*')
def version() -> Version:
try:
version = get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*')
except CalledProcessError as e:
# TeX Live for Windows provides an incompatible jbig2.EXE which may
# be on the PATH.
raise MissingDependencyError('jbig2enc') from e
return Version(version)
def available():
@@ -23,15 +31,17 @@ def available():
return True
def convert_group(*, cwd, infiles, out_prefix):
def convert_group(cwd, infiles, out_prefix, threshold):
args = [
'jbig2',
'-b',
out_prefix,
'-s', # symbol mode (lossy)
'--symbol-mode', # symbol mode (lossy)
'-t',
str(threshold), # threshold
# '-r', # refinement mode (lossless symbol mode, currently disabled in
# jbig2)
'-p',
'--pdf',
]
args.extend(infiles)
proc = run(args, cwd=cwd, stdout=PIPE, stderr=PIPE)
@@ -39,17 +49,9 @@ 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]
def convert_single(cwd, infile, outfile, threshold):
args = ['jbig2', '--pdf', '-t', str(threshold), 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])
+13 -24
View File
@@ -1,23 +1,21 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Interface to pngquant executable"""
"""Interface to pngquant executable."""
from __future__ import annotations
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from subprocess import PIPE
from PIL import Image
from packaging.version import Version
from ocrmypdf.exceptions import MissingDependencyError
from ocrmypdf.subprocess import get_version, run
def version():
return get_version('pngquant', regex=r'(\d+(\.\d+)*).*')
def version() -> Version:
return Version(get_version('pngquant', regex=r'(\d+(\.\d+)*).*'))
def available():
@@ -28,21 +26,16 @@ def available():
return True
@contextmanager
def input_as_png(input_file: Path):
if not input_file.name.endswith('.png'):
with Image.open(input_file) as im:
bio = BytesIO()
im.save(bio, format='png')
bio.seek(0)
yield bio
else:
with open(input_file, 'rb') as f:
yield f
def quantize(input_file: Path, output_file: Path, quality_min: int, quality_max: int):
with input_as_png(input_file) as input_stream:
"""Quantize a PNG image using pngquant.
Args:
input_file: Input PNG image
output_file: Output PNG image
quality_min: Minimum quality to use
quality_max: Maximum quality to use
"""
with open(input_file, 'rb') as input_stream:
args = [
'pngquant',
'--force',
@@ -57,7 +50,3 @@ 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)
+17 -41
View File
@@ -1,19 +1,19 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Interface to Tesseract executable"""
"""Interface to Tesseract executable."""
from __future__ import annotations
import logging
import re
from contextlib import suppress
from math import pi
from os import fspath
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
from packaging.version import Version
from PIL import Image
from ocrmypdf.exceptions import (
MissingDependencyError,
@@ -26,23 +26,6 @@ from ocrmypdf.subprocess import get_version, run
log = logging.getLogger(__name__)
HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name='ocr-system' content='tesseract 4.0.0' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "_blank.png"; bbox 0 0 {0} {1}; ppageno 0'>
</div>
</body>
</html>
"""
TESSERACT_THRESHOLDING_METHODS: dict[str, int] = {
'auto': 0,
'otsu': 0,
@@ -52,7 +35,7 @@ TESSERACT_THRESHOLDING_METHODS: dict[str, int] = {
class TesseractLoggerAdapter(logging.LoggerAdapter):
"Prepend [tesseract] to messages emitted from tesseract"
"""Prepend [tesseract] to messages emitted from tesseract."""
def process(self, msg, kwargs):
kwargs['extra'] = self.extra
@@ -104,28 +87,20 @@ TESSERACT_VERSION_PATTERN = r"""
class TesseractVersion(Version):
"Modify standard packaging.Version regex to support Tesseract idiosyncracies."
"""Modify standard packaging.Version regex to support Tesseract idiosyncrasies."""
_regex = re.compile(
r"^\s*" + TESSERACT_VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE
)
def version() -> str:
return get_version('tesseract', regex=r'tesseract\s(.+)')
def has_user_words() -> bool:
"""Does Tesseract have --user-words capability?
Not available in 4.0, but available in 4.1. Also available in 3.x, but
we no longer support 3.x.
"""
return version() >= '4.1'
def version() -> Version:
return TesseractVersion(get_version('tesseract', regex=r'tesseract\s(.+)'))
def has_thresholding() -> bool:
"""Does Tesseract have -c thresholding method capability?"""
return version() >= '5.0'
return version() >= Version('5.0')
def get_languages() -> set[str]:
@@ -239,12 +214,14 @@ def get_deskew(
parsed = _parse_tesseract_output(p.stdout)
deskew_radians = float(parsed.get('Deskew angle', 0))
deskew_degrees = 180 / pi * deskew_radians
log.debug(f"Deskew angle: {deskew_degrees:.3f}")
return deskew_degrees
def tesseract_log_output(stream: bytes) -> None:
tlog = TesseractLoggerAdapter(
log, extra=log.extra if hasattr(log, 'extra') else None
log,
extra=log.extra if hasattr(log, 'extra') else None, # type: ignore
)
if not stream:
@@ -289,12 +266,11 @@ def page_timedout(timeout: float) -> None:
def _generate_null_hocr(output_hocr: Path, output_text: Path, image: Path) -> None:
"""Produce a .hocr file that reports no text detected on a page that is
the same size as the input image."""
with Image.open(image) as im:
w, h = im.size
"""Produce an empty .hocr file.
output_hocr.write_text(HOCR_TEMPLATE.format(w, h), encoding='utf-8')
Ensures page is the same size as the input image.
"""
output_hocr.write_text('', encoding='utf-8')
output_text.write_text('[skipped page]', encoding='utf-8')
@@ -353,7 +329,7 @@ def generate_hocr(
tesseract_log_output(stdout)
# The sidecar text file will get the suffix .txt; rename it to
# whatever caller wants it named
if prefix.with_suffix('.txt').exists():
with suppress(FileNotFoundError):
prefix.with_suffix('.txt').replace(output_text)
@@ -409,7 +385,7 @@ def generate_pdf(
try:
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
stdout = p.stdout
if prefix.with_suffix('.txt').exists():
with suppress(FileNotFoundError):
prefix.with_suffix('.txt').replace(output_text)
except TimeoutExpired:
page_timedout(timeout)
+19 -69
View File
@@ -1,51 +1,33 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
"""Interface to unpaper executable."""
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
"""Interface to unpaper executable"""
from __future__ import annotations
import logging
import os
import shlex
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from decimal import Decimal
from pathlib import Path
from subprocess import PIPE, STDOUT
from typing import Iterator, Union
from tempfile import TemporaryDirectory
from packaging.version import Version
from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.exceptions import SubprocessOutputError
from ocrmypdf.subprocess import get_version, run
if sys.version_info >= (3, 10):
from tempfile import TemporaryDirectory
else:
from tempfile import TemporaryDirectory as _TemporaryDirectory
class TemporaryDirectory(_TemporaryDirectory):
"""Shim to consume ignore_cleanup_errors kwarg on Python 3.9 and older.
The argument is consumed without action. If users are getting errors related
to temporary file cleanup, they should upgrade to Python 3.10 which properly
cleans up temporary directories on Windows.
See: https://github.com/python/cpython/pull/24793
"""
def __init__(self, ignore_cleanup_errors=False, **kwargs):
super().__init__(**kwargs)
del _TemporaryDirectory
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/main/doc/basic-concepts.md
UNPAPER_IMAGE_PIXEL_LIMIT = 256 * 1024 * 1024
DecFloat = Union[Decimal, float]
DecFloat = Decimal | float
log = logging.getLogger(__name__)
@@ -65,34 +47,8 @@ class UnpaperImageTooLargeError(Exception):
super().__init__(self.message)
def version() -> str:
return get_version('unpaper')
SUPPORTED_MODES = {'1', 'L', 'RGB'}
def _convert_image(im: Image.Image) -> tuple[Image.Image, bool]:
im_modified = False
if im.mode not in SUPPORTED_MODES:
log.info("Converting image to other colorspace")
try:
if im.mode == 'P' and len(im.getcolors()) == 2:
im = im.convert(mode='1')
else:
im = im.convert(mode='RGB')
except OSError as e:
raise MissingDependencyError(
"Could not convert image with type " + im.mode
) from e
else:
im_modified = True
if im.mode not in SUPPORTED_MODES:
raise MissingDependencyError(
"Failed to convert image to a supported format."
) from None
return im, im_modified
def version() -> Version:
return Version(get_version('unpaper', regex=r'(?m).*?(\d+(\.\d+)(\.\d+)?)'))
@contextmanager
@@ -100,21 +56,15 @@ def _setup_unpaper_io(input_file: Path) -> Iterator[tuple[Path, Path, Path]]:
with Image.open(input_file) as im:
if im.width * im.height >= UNPAPER_IMAGE_PIXEL_LIMIT:
raise UnpaperImageTooLargeError(w=im.width, h=im.height)
im, im_modified = _convert_image(im)
with TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
tmppath = Path(tmpdir)
if im_modified or input_file.suffix != '.png':
input_png = tmppath / 'input.png'
im.save(input_png, format='PNG')
else:
# No changes, PNG input, just use the file we already have
input_png = input_file
# unpaper can write .png too, but it seems to write them slowly
# adds a few seconds to test suite - so just use pnm
output_pnm = tmppath / 'output.pnm'
yield input_png, output_pnm, tmppath
with TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
tmppath = Path(tmpdir)
# No changes, PNG input, just use the file we already have
input_png = input_file
# unpaper can write .png too, but it seems to write them slowly
# adds a few seconds to test suite - so just use pnm
output_pnm = tmppath / 'output.pnm'
yield input_png, output_pnm, tmppath
def run_unpaper(
+85 -56
View File
@@ -7,65 +7,85 @@ from __future__ import annotations
import logging
from contextlib import suppress
from enum import Enum
from pathlib import Path
from pikepdf import (
Dictionary,
Matrix,
Name,
Object,
Operator,
Page,
Pdf,
PdfError,
PdfMatrix,
Stream,
parse_content_stream,
unparse_content_stream,
)
from ocrmypdf._jobcontext import PdfContext
class RenderMode(Enum):
ON_TOP = 0
UNDERNEATH = 1
log = logging.getLogger(__name__)
MAX_REPLACE_PAGES = 100
def _ensure_dictionary(obj, name):
def _ensure_dictionary(obj: Dictionary | Stream, name: Name):
if name not in obj:
obj[name] = Dictionary({})
return obj[name]
def _update_resources(*, obj, font, font_key, procset):
def _update_resources(
*,
obj: Dictionary | Stream,
font: Dictionary | None,
font_key: Name | None,
):
"""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
# Reassign /ProcSet to one that just lists everything - ProcSet is
# obsolete and doesn't matter but recommended for old viewer support
if procset:
resources['/ProcSet'] = procset
def strip_invisible_text(pdf, page):
def strip_invisible_text(pdf: Pdf, page: Page):
stream = []
in_text_obj = False
render_mode = 0
render_mode_stack = []
text_objects = []
for operands, operator in parse_content_stream(page, ''):
if operator == Operator('Tr'):
render_mode = operands[0]
if operator == Operator('q'):
render_mode_stack.append(render_mode)
if operator == Operator('Q'):
try:
render_mode = render_mode_stack.pop()
except IndexError:
# Stack underflow: content stream is malformed
# but try to carry on
pass
if not in_text_obj:
if operator == Operator('BT'):
in_text_obj = True
render_mode = 0
text_objects.append((operands, operator))
else:
stream.append((operands, operator))
else:
if operator == Operator('Tr'):
render_mode = operands[0]
text_objects.append((operands, operator))
if operator == Operator('ET'):
in_text_obj = False
@@ -80,22 +100,20 @@ def strip_invisible_text(pdf, page):
class OcrGrafter:
"""Manages grafting text-only PDFs onto regular PDFs."""
def __init__(self, context):
def __init__(self, context: PdfContext):
self.context = context
self.path_base = context.origin
self.pdf_base = Pdf.open(self.path_base)
self.font, self.font_key = None, None
self.font: Dictionary | None = None
self.font_key: Name | None = None
self.pdfinfo = context.pdfinfo
self.output_file = context.get_path('graft_layers.pdf')
self.procset = self.pdf_base.make_indirect(
Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
)
self.emplacements = 1
self.interim_count = 0
self.render_mode = RenderMode.UNDERNEATH
def graft_page(
self,
@@ -120,7 +138,9 @@ class OcrGrafter:
foreign_image_page = pdf_image.pages[0]
self.pdf_base.pages.append(foreign_image_page)
local_image_page = self.pdf_base.pages[-1]
self.pdf_base.pages[pageno].emplace(local_image_page)
self.pdf_base.pages[pageno].emplace(
local_image_page, retain=(Name.Parent,)
)
del self.pdf_base.pages[-1]
emplaced_page = True
@@ -131,10 +151,13 @@ class OcrGrafter:
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}"
f"({text_rotation}, {autorotate_correction}, {content_rotation}) -> "
f"{text_misaligned}"
)
if textpdf and self.font:
if self.font_key is None:
raise ValueError("Font key is not set")
# 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
@@ -144,7 +167,6 @@ class OcrGrafter:
font=self.font,
font_key=self.font_key,
text_rotation=text_misaligned,
procset=self.procset,
strip_old_text=strip_old,
)
@@ -159,18 +181,15 @@ class OcrGrafter:
if self.emplacements % MAX_REPLACE_PAGES == 0:
self.save_and_reload()
def save_and_reload(self):
def save_and_reload(self) -> None:
"""Save and reload the Pdf.
This will keep a lid on our memory usage for very large files. Attach
the font to page 1 even if page 1 doesn't use it, so we have a way to get it
back.
"""
page0 = self.pdf_base.pages[0]
_update_resources(
obj=page0, font=self.font, font_key=self.font_key, procset=self.procset
)
_update_resources(obj=page0.obj, font=self.font, font_key=self.font_key)
# 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.
@@ -189,7 +208,6 @@ class OcrGrafter:
self.pdf_base.close()
self.pdf_base = Pdf.open(next_file)
self.procset = self.pdf_base.pages[0].Resources.ProcSet
self.font, self.font_key = None, None # Ensure we reacquire this information
self.interim_count += 1
@@ -198,25 +216,32 @@ class OcrGrafter:
self.pdf_base.close()
return self.output_file
def _find_font(self, text):
"""Copy a font from the filename text into pdf_base"""
def _find_font(self, text: Path) -> tuple[Dictionary | None, Name | None]:
"""Copy a font from the filename text into pdf_base."""
font, font_key = None, None
possible_font_names = ('/f-0-0', '/F1')
try:
with Pdf.open(text) as pdf_text:
try:
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
pdf_text_fonts = pdf_text.pages[0].Resources.get(
Name.Font, Dictionary()
)
except (AttributeError, IndexError, KeyError):
return None, None
if not isinstance(pdf_text_fonts, Dictionary):
log.warning("Page fonts are not stored in a dictionary")
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:
font_key = f
font_key = Name(f)
break
if pdf_text_font:
font = self.pdf_base.copy_foreign(pdf_text_font)
if not isinstance(font, Dictionary):
log.warning("Font is not a dictionary")
font, font_key = None, None
return font, font_key
except (FileNotFoundError, PdfError):
# PdfError occurs if a 0-length file is written e.g. due to OCR timeout
@@ -227,14 +252,12 @@ class OcrGrafter:
*,
page_num: int,
textpdf: Path,
font: Object,
font_key: Object,
procset: Object,
font: Dictionary,
font_key: Name,
text_rotation: int,
strip_old_text: bool,
):
"""Insert the text layer from text page 0 on to pdf_base at page_num"""
"""Insert the text layer from text page 0 on to pdf_base at page_num."""
# pylint: disable=invalid-name
log.debug("Grafting")
@@ -251,19 +274,19 @@ class OcrGrafter:
# content may have a rotation applied. Wrap the text stream with a rotation
# so it will be oriented the same way as the rest of the page content.
# (Previous versions OCRmyPDF rotated the content layer to match the text.)
mediabox = [float(pdf_text.pages[0].MediaBox[v]) for v in range(4)]
mediabox = pdf_text.pages[0].mediabox
wt, ht = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
mediabox = [float(base_page.MediaBox[v]) for v in range(4)]
mediabox = base_page.mediabox
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
translate = PdfMatrix().translated(-wt / 2, -ht / 2)
untranslate = PdfMatrix().translated(wp / 2, hp / 2)
corner = PdfMatrix().translated(mediabox[0], mediabox[1])
translate = Matrix().translated(-wt / 2, -ht / 2)
untranslate = Matrix().translated(wp / 2, hp / 2)
corner = Matrix().translated(mediabox[0], mediabox[1])
# -rotation because the input is a clockwise angle and this formula
# uses CCW
text_rotation = -text_rotation % 360
rotate = PdfMatrix().rotated(text_rotation)
rotate = Matrix().rotated(text_rotation)
# Because of rounding of DPI, we might get a text layer that is not
# identically sized to the target page. Scale to adjust. Normally this
@@ -274,14 +297,15 @@ class OcrGrafter:
scale_y = hp / ht
# log.debug('%r', scale_x, scale_y)
scale = PdfMatrix().scaled(scale_x, scale_y)
scale = Matrix().scaled(scale_x, scale_y)
# Translate the text so it is centered at (0, 0), rotate it there, adjust
# for a size different between initial and text PDF, then untranslate, and
# finally move the lower left corner to match the mediabox
# finally move the lower left corner to match the mediabox.
ctm = translate @ rotate @ scale @ untranslate @ corner
log.debug("Grafting with ctm %r", ctm)
base_resources = _ensure_dictionary(base_page, Name.Resources)
base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
text_xobj_name = Name.random(prefix="OCR-")
xobj = self.pdf_base.make_stream(pdf_text_contents)
@@ -290,9 +314,7 @@ class OcrGrafter:
xobj.Subtype = Name.Form
xobj.FormType = 1
xobj.BBox = mediabox
_update_resources(
obj=xobj, font=font, font_key=font_key, procset=[Name.PDF]
)
_update_resources(obj=xobj, font=font, font_key=font_key)
pdf_draw_xobj = (
(b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
@@ -301,9 +323,16 @@ class OcrGrafter:
if strip_old_text:
strip_invisible_text(self.pdf_base, base_page)
base_page.contents_add(new_text_layer, prepend=True)
_update_resources(
obj=base_page, font=font, font_key=font_key, procset=procset
base_page.contents_coalesce()
if self.render_mode == RenderMode.ON_TOP:
# Add q/Q to ensure content we append is drawn correctly
# Strictly speaking this needs to trace the whole q/Q stack in case
# stack is not balanced.
original = base_page.Contents.read_bytes()
base_page.Contents.write(b'q\n' + original + b'\nQ\n')
base_page.contents_add(
new_text_layer, prepend=self.render_mode == RenderMode.UNDERNEATH
)
base_page.contents_coalesce()
_update_resources(obj=base_page.obj, font=font, font_key=font_key)
+10 -13
View File
@@ -6,12 +6,10 @@
from __future__ import annotations
import os
import shutil
import sys
from argparse import Namespace
from collections.abc import Iterator
from copy import copy
from pathlib import Path
from typing import Iterator
from pluggy import PluginManager
@@ -55,11 +53,17 @@ class PdfContext:
for n in range(npages):
yield PageContext(self, n)
def get_page_context_args(self) -> Iterator[tuple[PageContext]]:
"""Get all ``PageContext`` for this PDF packaged in tuple for args-splatting."""
npages = len(self.pdfinfo)
for n in range(npages):
yield (PageContext(self, n),)
class PageContext:
"""Holds our context for a page.
Must be pickable, so stores only intrinsic/simple data elements or those
Must be pickle-able, so stores only intrinsic/simple data elements or those
capable of their serializing themselves via ``__getstate__``.
"""
@@ -89,15 +93,8 @@ class PageContext:
state = self.__dict__.copy()
state['options'] = copy(self.options)
if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)):
if not isinstance(state['options'].input_file, str | bytes | os.PathLike):
state['options'].input_file = 'stream'
if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)):
if not isinstance(state['options'].output_file, str | bytes | os.PathLike):
state['options'].output_file = 'stream'
return state
def cleanup_working_files(work_folder: Path, options: Namespace):
if options.keep_temporary_files:
print(f"Temporary working files retained at:\n{work_folder}", file=sys.stderr)
else:
shutil.rmtree(work_folder, ignore_errors=True)
+7 -20
View File
@@ -6,9 +6,9 @@
from __future__ import annotations
import logging
from contextlib import suppress
from tqdm import tqdm
from rich.console import Console
from rich.logging import RichHandler
class PageNumberFilter(logging.Filter):
@@ -23,21 +23,8 @@ class PageNumberFilter(logging.Filter):
return True
class TqdmConsole:
"""Wrapper to log messages in a way that is compatible with tqdm progress bar
This routes log messages through tqdm so that it can print them above the
progress bar, and then refresh the progress bar, rather than overwriting
it which looks messy.
"""
def __init__(self, file):
self.file = file
def write(self, msg):
# When no progress bar is active, tqdm.write() routes to print()
tqdm.write(msg.rstrip(), end='\n', file=self.file)
def flush(self):
with suppress(AttributeError):
self.file.flush()
class RichLoggingHandler(RichHandler):
def __init__(self, console: Console, **kwargs):
super().__init__(
console=console, show_level=False, show_time=False, markup=False, **kwargs
)
+215
View File
@@ -0,0 +1,215 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""OCRmyPDF page processing pipeline functions."""
from __future__ import annotations
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from pikepdf import Dictionary, Name, Pdf
from pikepdf import __version__ as PIKEPDF_VERSION
from pikepdf.models.metadata import PdfMetadata, encode_pdf_date
from ocrmypdf._annots import remove_broken_goto_annotations
from ocrmypdf._defaults import PROGRAM_NAME
from ocrmypdf._jobcontext import PdfContext
from ocrmypdf._version import __version__ as OCRMYPF_VERSION
from ocrmypdf.languages import iso_639_2_from_3
log = logging.getLogger(__name__)
def get_docinfo(base_pdf: Pdf, context: PdfContext) -> dict[str, str]:
"""Read the document info and store it in a dictionary."""
options = context.options
def from_document_info(key):
try:
s = base_pdf.docinfo[key]
return str(s)
except (KeyError, TypeError):
return ''
pdfmark = {
k: from_document_info(k)
for k in ('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')
}
if options.title:
pdfmark['/Title'] = options.title
if options.author:
pdfmark['/Author'] = options.author
if options.keywords:
pdfmark['/Keywords'] = options.keywords
if options.subject:
pdfmark['/Subject'] = options.subject
creator_tag = context.plugin_manager.hook.get_ocr_engine().creator_tag(options)
pdfmark['/Creator'] = f'{PROGRAM_NAME} {OCRMYPF_VERSION} / {creator_tag}'
pdfmark['/Producer'] = f'pikepdf {PIKEPDF_VERSION}'
pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc))
return pdfmark
def report_on_metadata(options, missing):
if not missing:
return
if options.output_type.startswith('pdfa'):
log.warning(
"Some input metadata could not be copied because it is not "
"permitted in PDF/A. You may wish to examine the output "
"PDF's XMP metadata."
)
log.debug("The following metadata fields were not copied: %r", missing)
else:
log.error(
"Some input metadata could not be copied."
"You may wish to examine the output PDF's XMP metadata."
)
log.info("The following metadata fields were not copied: %r", missing)
def repair_docinfo_nuls(pdf):
"""If the DocumentInfo block contains NUL characters, remove them.
If the DocumentInfo block is malformed, log an error and continue.
"""
modified = False
try:
if not isinstance(pdf.docinfo, Dictionary):
raise TypeError("DocumentInfo is not a dictionary")
for k, v in pdf.docinfo.items():
if isinstance(v, str) and b'\x00' in bytes(v):
pdf.docinfo[k] = bytes(v).replace(b'\x00', b'')
modified = True
except TypeError:
# TypeError can also be raised if dictionary items are unexpected types
log.error("File contains a malformed DocumentInfo block - continuing anyway.")
return modified
def should_linearize(working_file: Path, context: PdfContext) -> bool:
"""Determine whether the PDF should be linearized.
For smaller files, linearization is not worth the effort.
"""
filesize = os.stat(working_file).st_size
if filesize > (context.options.fast_web_view * 1_000_000):
return True
return False
def _fix_metadata(meta_original: PdfMetadata, meta_pdf: PdfMetadata):
# If xmp:CreateDate is missing, set it to the modify date to
# ensure consistency with Ghostscript.
if 'xmp:CreateDate' not in meta_pdf:
meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '')
if meta_pdf.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_pdf['dc:title']
def _unset_empty_metadata(meta: PdfMetadata, options):
"""Unset metadata fields that were explicitly set to empty strings.
If the user explicitly specified an empty string for any of the
following, they should be unset and not reported as missing in
the output pdf. Note that some metadata fields use differing names
between PDF/A and PDF.
"""
if options.title == '' and 'dc:title' in meta:
del meta['dc:title'] # PDF/A and PDF
if options.author == '':
if 'dc:creator' in meta:
del meta['dc:creator'] # PDF/A (Not xmp:CreatorTool)
if 'pdf:Author' in meta:
del meta['pdf:Author'] # PDF
if options.subject == '':
if 'dc:description' in meta:
del meta['dc:description'] # PDF/A
if 'dc:subject' in meta:
del meta['dc:subject'] # PDF
if options.keywords == '' and 'pdf:Keywords' in meta:
del meta['pdf:Keywords'] # PDF/A and PDF
def _set_language(pdf: Pdf, languages: list[str]):
"""Set the language of the PDF."""
if Name.Lang in pdf.Root or not languages:
return # Already set or can't change
primary_language_iso639_3 = languages[0]
if not primary_language_iso639_3:
return
iso639_2 = iso_639_2_from_3(primary_language_iso639_3)
if not iso639_2:
return
pdf.Root.Lang = iso639_2
class MetadataProgress:
def __init__(self, progressbar_class, enable: bool = True):
self.progressbar_class = progressbar_class
self.progressbar = self.progressbar_class(
total=100, desc="Linearizing", unit='%', disable=not enable
)
def __enter__(self):
self.progressbar.__enter__()
return self
def __exit__(self, exc_type, exc_value, traceback):
return self.progressbar.__exit__(exc_type, exc_value, traceback)
def __call__(self, percent: int):
if not self.progressbar_class:
return
self.progressbar.update(completed=percent)
def metadata_fixup(
working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
) -> Path:
"""Fix certain metadata fields whether PDF or PDF/A.
Override some of Ghostscript's metadata choices.
Also report on metadata in the input file that was not retained during
conversion.
"""
output_file = context.get_path('metafix.pdf')
options = context.options
pbar_class = context.plugin_manager.hook.get_progressbar_class()
with (
Pdf.open(context.origin) as original,
Pdf.open(working_file) as pdf,
MetadataProgress(pbar_class, options.progress_bar) as pbar,
):
docinfo = get_docinfo(original, context)
with (
original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original,
pdf.open_metadata() as meta_pdf,
):
meta_pdf.load_from_docinfo(
docinfo, delete_missing=False, raise_failure=False
)
_fix_metadata(meta_original, meta_pdf)
_unset_empty_metadata(meta_original, options)
_unset_empty_metadata(meta_pdf, options)
meta_missing = set(meta_original.keys()) - set(meta_pdf.keys())
report_on_metadata(options, meta_missing)
_set_language(pdf, options.languages)
pdf.save(output_file, progress=pbar, **pdf_save_settings)
return output_file
+436 -253
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
+488
View File
@@ -0,0 +1,488 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
import argparse
import json
import logging
import logging.handlers
import os
import shutil
import sys
import threading
from collections.abc import Callable, Sequence
from concurrent.futures.process import BrokenProcessPool
from concurrent.futures.thread import BrokenThreadPool
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import NamedTuple, cast
import PIL
from pikepdf import Pdf
from ocrmypdf._annots import remove_broken_goto_annotations
from ocrmypdf._concurrent import Executor, setup_executor
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._logging import PageNumberFilter
from ocrmypdf._metadata import metadata_fixup
from ocrmypdf._pipeline import (
convert_to_pdfa,
create_ocr_image,
create_pdf_page_from_image,
create_visible_page_jpg,
generate_postscript_stub,
get_orientation_correction,
get_pdf_save_settings,
get_pdfinfo,
optimize_pdf,
preprocess_clean,
preprocess_deskew,
preprocess_remove_background,
rasterize,
rasterize_preview,
should_linearize,
should_visible_page_image_use_jpg,
)
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
from ocrmypdf._validation import (
report_output_file_size,
)
from ocrmypdf.exceptions import ExitCode, ExitCodeException
from ocrmypdf.helpers import (
available_cpu_count,
check_pdf,
pikepdf_enable_mmap,
running_in_docker,
running_in_snap,
samefile,
)
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import PdfInfo
log = logging.getLogger(__name__)
tls = threading.local()
tls.pageno = None
def _set_logging_tls(tls):
"""Inject current page number (when available) into log records."""
old_factory = logging.getLogRecordFactory()
def wrapper(*args, **kwargs):
record = old_factory(*args, **kwargs)
if hasattr(tls, 'pageno'):
record.pageno = tls.pageno
return record
logging.setLogRecordFactory(wrapper)
_set_logging_tls(tls)
def set_thread_pageno(pageno: int | None):
"""Set page number (1-based) that the current thread is processing."""
tls.pageno = pageno
class PageResult(NamedTuple):
"""Result when a page is finished processing."""
pageno: int
"""Page number, 0-based."""
pdf_page_from_image: Path | None = None
"""Single page PDF from image."""
ocr: Path | None = None
"""Single page OCR PDF."""
text: Path | None = None
"""Single page text file."""
orientation_correction: int = 0
"""Orientation correction in degrees."""
class HOCRResultEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Path):
return {'Path': str(obj)}
return super().default(obj)
class HOCRResultDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.dict_to_object, *args, **kwargs)
def dict_to_object(self, d):
if 'Path' in d:
return Path(d['Path'])
return d
@dataclass
class HOCRResult:
"""Result when hOCR is finished processing."""
pageno: int
"""Page number, 0-based."""
pdf_page_from_image: Path | None = None
"""Single page PDF from image."""
hocr: Path | None = None
"""Single page hOCR file."""
textpdf: Path | None = None
"""hOCR file after conversion to PDF."""
orientation_correction: int = 0
"""Orientation correction in degrees."""
@classmethod
def from_json(cls, json_str: str) -> HOCRResult:
"""Create an instance from a dict."""
return cls(**json.loads(json_str, cls=HOCRResultDecoder))
def to_json(self) -> str:
"""Serialize to a JSON string."""
return json.dumps(self.__dict__, cls=HOCRResultEncoder)
def configure_debug_logging(
log_filename: Path, prefix: str = ''
) -> tuple[logging.FileHandler, Callable[[], None]]:
"""Create a debug log file at a specified location.
Returns the log handler, and a function to remove the handler.
Args:
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(
'[%(asctime)s] - %(name)s - %(levelname)7s -%(pageno)s %(message)s'
)
log_file_handler.setFormatter(formatter)
log_file_handler.addFilter(PageNumberFilter())
logging.getLogger(prefix).addHandler(log_file_handler)
def remover():
try:
logging.getLogger(prefix).removeHandler(log_file_handler)
log_file_handler.close()
except OSError as e:
print(e, file=sys.stderr)
return log_file_handler, remover
def worker_init(max_pixels: int | None) -> None:
"""Initialize a worker thread or process."""
# In Windows, child process will not inherit our change to this value in
# the parent process, so ensure workers get it set. Not needed when running
# threaded, but harmless to set again.
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
pikepdf_enable_mmap()
@contextmanager
def manage_debug_log_handler(
*,
options: argparse.Namespace,
work_folder: Path,
):
remover = None
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
'PYTEST_CURRENT_TEST', ''
):
# 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, remover = configure_debug_logging(
work_folder / "debug.log", prefix=""
) # pragma: no cover
try:
yield
finally:
if remover:
remover()
def _print_temp_folder_location(work_folder: Path):
"""Print the location of the temporary work folder."""
msgs = [f"Temporary working files retained at:\n{work_folder}"]
if running_in_docker(): # pragma: no cover
msgs.append(
"OCRmyPDF is running in a Docker container, "
"so the files will be inside the container."
)
elif running_in_snap(): # pragma: no cover
msgs.append(
"OCRmyPDF is running in a Snap container, "
"so the files will be inside the container."
)
print('\n'.join(msgs), file=sys.stderr)
@contextmanager
def manage_work_folder(*, work_folder: Path, retain: bool, print_location: bool):
try:
yield work_folder
finally:
if retain:
if print_location:
_print_temp_folder_location(work_folder)
else:
shutil.rmtree(work_folder, ignore_errors=True)
def cli_exception_handler(
fn: Callable[[argparse.Namespace, OcrmypdfPluginManager], ExitCode],
options: argparse.Namespace,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
"""Convert exceptions into command line error messages and exit codes.
When known exceptions are raised, the exception message is printed to stderr
and the program exits with a non-zero exit code. When unknown exceptions are
raised, the exception traceback is printed to stderr and the program exits
with a non-zero exit code.
"""
try:
# We cannot use a generator and yield here, as would be the usual pattern
# for exception handling context managers, because we need to return an exit
# code.
return fn(options, plugin_manager)
except KeyboardInterrupt:
if options.verbose >= 1:
log.exception("KeyboardInterrupt")
else:
log.error("KeyboardInterrupt")
return ExitCode.ctrl_c
except ExitCodeException as e:
e = cast(ExitCodeException, e)
if options.verbose >= 1:
log.exception("ExitCodeException")
elif str(e):
log.error("%s: %s", type(e).__name__, str(e))
else:
log.error(type(e).__name__)
return e.exit_code
except PIL.Image.DecompressionBombError:
log.exception(
"A decompression bomb error was encountered while executing the "
"pipeline. Use the argument --max-image-mpixels to raise the maximum "
"image pixel limit."
)
return ExitCode.other_error
except (
BrokenProcessPool,
BrokenThreadPool,
):
log.exception(
"A worker process was terminated unexpectedly. This is known to occur if "
"processing your file takes all available swap space and RAM. It may "
"help to try again with a smaller number of jobs, using the --jobs "
"argument."
)
return ExitCode.child_process_error
except Exception: # pylint: disable=broad-except
log.exception("An exception occurred while executing the pipeline")
return ExitCode.other_error
def setup_pipeline(
options: argparse.Namespace,
plugin_manager: OcrmypdfPluginManager,
) -> Executor:
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
# options.input_file, options.pdf_renderer are already bound.)
if not options.jobs:
options.jobs = available_cpu_count()
pikepdf_enable_mmap()
executor = setup_executor(plugin_manager)
return executor
def do_get_pdfinfo(
pdf_path: Path, executor: Executor, options: argparse.Namespace
) -> PdfInfo:
return get_pdfinfo(
pdf_path,
executor=executor,
detailed_analysis=options.redo_ocr,
progbar=options.progress_bar,
max_workers=options.jobs,
use_threads=options.use_threads,
check_pages=options.pages,
)
def preprocess(
page_context: PageContext,
image: Path,
remove_background: bool,
deskew: bool,
clean: bool,
) -> Path:
"""Preprocess an image."""
if remove_background:
image = preprocess_remove_background(image, page_context)
if deskew:
image = preprocess_deskew(image, page_context)
if clean:
image = preprocess_clean(image, page_context)
return image
def make_intermediate_images(
page_context: PageContext, orientation_correction: int
) -> tuple[Path, Path | None]:
"""Create intermediate and preprocessed images for OCR."""
options = page_context.options
ocr_image = preprocess_out = None
rasterize_out = rasterize(
page_context.origin,
page_context,
correction=orientation_correction,
remove_vectors=False,
)
if not any([options.clean, options.clean_final, options.remove_vectors]):
ocr_image = preprocess_out = preprocess(
page_context,
rasterize_out,
options.remove_background,
options.deskew,
clean=False,
)
else:
if not options.lossless_reconstruction:
preprocess_out = preprocess(
page_context,
rasterize_out,
options.remove_background,
options.deskew,
clean=options.clean_final,
)
if options.remove_vectors:
rasterize_ocr_out = rasterize(
page_context.origin,
page_context,
correction=orientation_correction,
remove_vectors=True,
output_tag='_ocr',
)
else:
rasterize_ocr_out = rasterize_out
if (
preprocess_out
and rasterize_ocr_out == rasterize_out
and options.clean == options.clean_final
):
# Optimization: image for OCR is identical to presentation image
ocr_image = preprocess_out
else:
ocr_image = preprocess(
page_context,
rasterize_ocr_out,
options.remove_background,
options.deskew,
clean=options.clean,
)
return ocr_image, preprocess_out
def process_page(page_context: PageContext) -> tuple[Path, Path | None, int]:
"""Process page to create OCR image, visible page image and orientation."""
options = page_context.options
orientation_correction = 0
if options.rotate_pages:
# Rasterize
rasterize_preview_out = rasterize_preview(page_context.origin, page_context)
orientation_correction = get_orientation_correction(
rasterize_preview_out, page_context
)
ocr_image, preprocess_out = make_intermediate_images(
page_context, orientation_correction
)
ocr_image_out = create_ocr_image(ocr_image, page_context)
pdf_page_from_image_out = None
if not options.lossless_reconstruction:
assert preprocess_out
visible_image_out = preprocess_out
if should_visible_page_image_use_jpg(page_context.pageinfo):
visible_image_out = create_visible_page_jpg(visible_image_out, page_context)
filtered_image = page_context.plugin_manager.hook.filter_page_image(
page=page_context, image_filename=visible_image_out
)
if filtered_image is not None: # None if no hook is present
visible_image_out = filtered_image
pdf_page_from_image_out = create_pdf_page_from_image(
visible_image_out, page_context, orientation_correction
)
return ocr_image_out, pdf_page_from_image_out, orientation_correction
def postprocess(
pdf_file: Path, context: PdfContext, executor: Executor
) -> tuple[Path, Sequence[str]]:
"""Postprocess the PDF file."""
# pdf_out = pdf_file
with Pdf.open(pdf_file) as pdf:
fix_annots = context.get_path('fix_annots.pdf')
if remove_broken_goto_annotations(pdf):
pdf.save(fix_annots)
pdf_out = fix_annots
else:
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)
optimizing = context.plugin_manager.hook.is_optimization_enabled(context=context)
save_settings = get_pdf_save_settings(context.options.output_type)
save_settings['linearize'] = not optimizing and should_linearize(pdf_out, context)
pdf_out = metadata_fixup(pdf_out, context, pdf_save_settings=save_settings)
return optimize_pdf(pdf_out, context, executor)
def report_output_pdf(options, start_input_file, optimize_messages) -> ExitCode:
if options.output_file == '-':
log.info("Output sent to stdout")
elif hasattr(options.output_file, 'writable') and options.output_file.writable():
log.info("Output written to stream")
elif samefile(options.output_file, Path(os.devnull)):
pass # Say nothing when sending to dev null
else:
if options.output_type.startswith('pdfa'):
pdfa_info = file_claims_pdfa(options.output_file)
if pdfa_info['pass']:
log.info("Output file is a %s (as expected)", pdfa_info['conformance'])
else:
log.warning(
"Output file is a valid PDF, but conversion to PDF/A did not "
"succeed (issue: %s)",
pdfa_info['conformance'],
)
return ExitCode.pdfa_conversion_failed
if not check_pdf(options.output_file):
log.warning('Output file: The generated PDF is INVALID')
return ExitCode.invalid_output_pdf
report_output_file_size(
options, start_input_file, options.output_file, optimize_messages
)
return ExitCode.ok
+125
View File
@@ -0,0 +1,125 @@
# SPDX-FileCopyrightText: 2019-2023 James R. Barlow
# SPDX-FileCopyrightText: 2019 Martin Wind
# SPDX-License-Identifier: MPL-2.0
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
import logging
import logging.handlers
from collections.abc import Sequence
from functools import partial
import PIL
from ocrmypdf._concurrent import Executor
from ocrmypdf._graft import OcrGrafter
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipeline import (
copy_final,
render_hocr_page,
)
from ocrmypdf._pipelines._common import (
HOCRResult,
do_get_pdfinfo,
manage_work_folder,
postprocess,
report_output_pdf,
set_thread_pageno,
setup_pipeline,
worker_init,
)
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
from ocrmypdf._progressbar import ProgressBar
from ocrmypdf.exceptions import ExitCode
log = logging.getLogger(__name__)
def _exec_hocrtransform_sync(page_context: PageContext) -> HOCRResult:
"""Process each page."""
hocr_json = page_context.get_path('hocr.json')
if not hocr_json.exists():
# No hOCR file, so no OCR was performed on this page.
return HOCRResult(pageno=page_context.pageno)
hocr_result = HOCRResult.from_json(hocr_json.read_text())
hocr_result.textpdf = render_hocr_page(
page_context.get_path('ocr_hocr.hocr'), page_context
)
return hocr_result
def exec_hocr_to_ocr_pdf(context: PdfContext, executor: Executor) -> Sequence[str]:
"""Convert hOCR files to OCR PDF."""
# Run exec_page_sync on every page
options = context.options
max_workers = min(len(context.pdfinfo), options.jobs)
if max_workers > 1:
log.info("Continue processing %d pages concurrently", max_workers)
ocrgraft = OcrGrafter(context)
def graft_page(result: HOCRResult, pbar: ProgressBar):
"""Graft text only PDF on to main PDF's page."""
try:
set_thread_pageno(result.pageno + 1)
pbar.update()
ocrgraft.graft_page(
pageno=result.pageno,
image=result.pdf_page_from_image,
textpdf=result.textpdf,
autorotate_correction=result.orientation_correction,
)
pbar.update()
finally:
set_thread_pageno(None)
executor(
use_threads=options.use_threads,
max_workers=max_workers,
progress_kwargs=dict(
total=(2 * len(context.pdfinfo)),
desc='Grafting hOCR to PDF',
unit='page',
unit_scale=0.5,
disable=not options.progress_bar,
),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
task=_exec_hocrtransform_sync,
task_arguments=context.get_page_context_args(),
task_finished=graft_page,
)
pdf = ocrgraft.finalize()
messages: Sequence[str] = []
if options.output_type != 'none':
# PDF/A and metadata
log.info("Postprocessing...")
pdf, messages = postprocess(pdf, context, executor)
# Copy PDF file to destination (we don't know the input PDF file name)
copy_final(pdf, options.output_file, None)
return messages
def run_hocr_to_ocr_pdf_pipeline(
options: argparse.Namespace,
*,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
"""Run pipeline to convert hOCR to final output PDF."""
with manage_work_folder(
work_folder=options.work_folder, retain=True, print_location=False
) as work_folder:
executor = setup_pipeline(options, plugin_manager)
origin_pdf = work_folder / 'origin.pdf'
# Gather pdfinfo and create context
pdfinfo = do_get_pdfinfo(origin_pdf, executor, options)
context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager)
plugin_manager.hook.check_options(options=options)
optimize_messages = exec_hocr_to_ocr_pdf(context, executor)
return report_output_pdf(options, origin_pdf, optimize_messages)
+214
View File
@@ -0,0 +1,214 @@
# SPDX-FileCopyrightText: 2019-2023 James R. Barlow
# SPDX-FileCopyrightText: 2019 Martin Wind
# SPDX-License-Identifier: MPL-2.0
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
import logging
import logging.handlers
from collections.abc import Sequence
from functools import partial
from pathlib import Path
from tempfile import mkdtemp
import PIL
from ocrmypdf._concurrent import Executor
from ocrmypdf._graft import OcrGrafter
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipeline import (
copy_final,
is_ocr_required,
merge_sidecars,
ocr_engine_hocr,
ocr_engine_textonly_pdf,
render_hocr_page,
triage,
validate_pdfinfo_options,
)
from ocrmypdf._pipelines._common import (
PageResult,
cli_exception_handler,
do_get_pdfinfo,
manage_debug_log_handler,
manage_work_folder,
postprocess,
process_page,
report_output_pdf,
set_thread_pageno,
setup_pipeline,
worker_init,
)
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
from ocrmypdf._progressbar import ProgressBar
from ocrmypdf._validation import (
check_requested_output_file,
create_input_file,
)
from ocrmypdf.exceptions import ExitCode
log = logging.getLogger(__name__)
def _image_to_ocr_text(
page_context: PageContext, ocr_image_out: Path
) -> tuple[Path, Path]:
"""Run OCR engine on image to create OCR PDF and text file."""
options = page_context.options
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)
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 ocr_out, text_out
def _exec_page_sync(page_context: PageContext) -> PageResult:
"""Execute a pipeline for a single page synchronously."""
set_thread_pageno(page_context.pageno + 1)
if not is_ocr_required(page_context):
return PageResult(pageno=page_context.pageno)
ocr_image_out, pdf_page_from_image_out, orientation_correction = process_page(
page_context
)
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
return PageResult(
pageno=page_context.pageno,
pdf_page_from_image=pdf_page_from_image_out,
ocr=ocr_out,
text=text_out,
orientation_correction=orientation_correction,
)
def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
"""Execute the OCR pipeline concurrently."""
options = context.options
max_workers = min(len(context.pdfinfo), options.jobs)
if max_workers > 1:
log.info("Start processing %d pages concurrently", max_workers)
sidecars: list[Path | None] = [None] * len(context.pdfinfo)
ocrgraft = OcrGrafter(context)
def update_page(result: PageResult, pbar: ProgressBar):
"""After OCR is complete for a page, update the PDF."""
try:
set_thread_pageno(result.pageno + 1)
sidecars[result.pageno] = result.text
pbar.update(0.5)
ocrgraft.graft_page(
pageno=result.pageno,
image=result.pdf_page_from_image,
textpdf=result.ocr,
autorotate_correction=result.orientation_correction,
)
pbar.update(0.5)
finally:
set_thread_pageno(None)
executor(
use_threads=options.use_threads,
max_workers=max_workers,
progress_kwargs=dict(
total=len(context.pdfinfo),
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
unit='page',
disable=not options.progress_bar,
),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
task=_exec_page_sync,
task_arguments=context.get_page_context_args(),
task_finished=update_page,
)
# Output sidecar text
if options.sidecar:
text = merge_sidecars(sidecars, context)
# Copy text file to destination
copy_final(text, options.sidecar, options.input_file)
# Merge layers to one single pdf
pdf = ocrgraft.finalize()
messages: Sequence[str] = []
if options.output_type != 'none':
# PDF/A and metadata
log.info("Postprocessing...")
pdf, messages = postprocess(pdf, context, executor)
# Copy PDF file to destination
copy_final(pdf, options.output_file, options.input_file)
return messages
def _run_pipeline(
options: argparse.Namespace,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
with (
manage_work_folder(
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
retain=options.keep_temporary_files,
print_location=options.keep_temporary_files,
) as work_folder,
manage_debug_log_handler(options=options, work_folder=work_folder),
):
executor = setup_pipeline(options, plugin_manager)
check_requested_output_file(options)
start_input_file, original_filename = create_input_file(options, work_folder)
# Triage image or pdf
origin_pdf = triage(
original_filename, start_input_file, work_folder / 'origin.pdf', options
)
# Gather pdfinfo and create context
pdfinfo = do_get_pdfinfo(origin_pdf, executor, options)
context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager)
# Validate options are okay for this pdf
validate_pdfinfo_options(context)
# Execute the pipeline
optimize_messages = exec_concurrent(context, executor)
exitcode = report_output_pdf(options, start_input_file, optimize_messages)
return exitcode
def run_pipeline_cli(
options: argparse.Namespace,
*,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
"""Run the OCR pipeline with command line exception handling.
Args:
options: The parsed command line options.
plugin_manager: The plugin manager to use. If not provided, one will be
created.
"""
return cli_exception_handler(_run_pipeline, options, plugin_manager)
def run_pipeline(
options: argparse.Namespace,
*,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
"""Run the OCR pipeline without command line exception handling.
Args:
options: The parsed command line options.
plugin_manager: The plugin manager to use. If not provided, one will be
created.
"""
return _run_pipeline(options, plugin_manager)
+108
View File
@@ -0,0 +1,108 @@
# SPDX-FileCopyrightText: 2019-2023 James R. Barlow
# SPDX-FileCopyrightText: 2019 Martin Wind
# SPDX-License-Identifier: MPL-2.0
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
import logging
import logging.handlers
import shutil
from functools import partial
import PIL
from ocrmypdf._concurrent import Executor
from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._pipeline import (
is_ocr_required,
ocr_engine_hocr,
validate_pdfinfo_options,
)
from ocrmypdf._pipelines._common import (
HOCRResult,
do_get_pdfinfo,
manage_work_folder,
process_page,
set_thread_pageno,
setup_pipeline,
worker_init,
)
from ocrmypdf._plugin_manager import OcrmypdfPluginManager
from ocrmypdf._validation import (
set_lossless_reconstruction,
)
log = logging.getLogger(__name__)
def _exec_page_hocr_sync(page_context: PageContext) -> HOCRResult:
"""Execute a pipeline for a single page hOCR."""
set_thread_pageno(page_context.pageno + 1)
if not is_ocr_required(page_context):
return HOCRResult(pageno=page_context.pageno)
ocr_image_out, pdf_page_from_image_out, orientation_correction = process_page(
page_context
)
hocr_out, _ = ocr_engine_hocr(ocr_image_out, page_context)
result = HOCRResult(
pageno=page_context.pageno,
pdf_page_from_image=pdf_page_from_image_out,
hocr=hocr_out,
orientation_correction=orientation_correction,
)
page_context.get_path('hocr.json').write_text(result.to_json())
return result
def exec_pdf_to_hocr(context: PdfContext, executor: Executor) -> None:
"""Execute the OCR pipeline concurrently and output hOCR."""
# Run exec_page_sync on every page
options = context.options
max_workers = min(len(context.pdfinfo), options.jobs)
if max_workers > 1:
log.info("Start processing %d pages concurrently", max_workers)
executor(
use_threads=options.use_threads,
max_workers=max_workers,
progress_kwargs=dict(
total=(2 * len(context.pdfinfo)),
desc='hOCR',
unit='page',
unit_scale=0.5,
disable=not options.progress_bar,
),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
task=_exec_page_hocr_sync,
task_arguments=context.get_page_context_args(),
)
def run_hocr_pipeline(
options: argparse.Namespace,
*,
plugin_manager: OcrmypdfPluginManager,
) -> None:
"""Run pipeline to output hOCR."""
with manage_work_folder(
work_folder=options.output_folder, retain=True, print_location=False
) as work_folder:
executor = setup_pipeline(options, plugin_manager)
origin_pdf = work_folder / 'origin.pdf'
shutil.copy2(options.input_file, origin_pdf)
# Gather pdfinfo and create context
pdfinfo = do_get_pdfinfo(origin_pdf, executor, options)
context = PdfContext(
options, work_folder, options.input_file, pdfinfo, plugin_manager
)
# Validate options are okay for this pdf
set_lossless_reconstruction(options)
validate_pdfinfo_options(context)
exec_pdf_to_hocr(context, executor)
+6 -4
View File
@@ -10,8 +10,8 @@ import importlib
import importlib.util
import pkgutil
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import Sequence
import pluggy
@@ -33,7 +33,7 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
def __init__(
self,
*args,
plugins: list[str | Path],
plugins: Sequence[str | Path],
builtins: bool = True,
**kwargs,
):
@@ -100,10 +100,12 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
self.register(module)
def get_plugin_manager(plugins: list[str | Path], builtins=True):
def get_plugin_manager(
plugins: Sequence[str | Path] | None = None, builtins=True
) -> OcrmypdfPluginManager:
return OcrmypdfPluginManager(
project_name='ocrmypdf',
plugins=plugins,
plugins=plugins if plugins is not None else [],
builtins=builtins,
)
+243
View File
@@ -0,0 +1,243 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Defines progress bar API."""
from __future__ import annotations
from typing import Protocol
from rich.console import Console
from rich.progress import (
BarColumn,
MofNCompleteColumn,
Progress,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from rich.table import Column
class ProgressBar(Protocol):
"""The protocol that OCRmyPDF expects progress bar classes to be compatible with.
In practice this could be used for any time of monitoring, not just a progress bar.
Calling the class should return a new progress bar object, which is activated
with ``__enter__`` and terminated with ``__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.
Progress bars should never write to ``sys.stdout``, or they will corrupt the
output if OCRmyPDF writes a PDF to standard output.
Note:
The type of events that OCRmyPDF reports to a progress bar may change in
minor releases.
Args:
total (int | float | None):
The total number of work units expected. If ``None``, the total is unknown.
For example, if you are processing pages, this might be the number of pages,
or if you are measuring overall progress in percent, this might be 100.
desc (str | None):
A brief description of the current step (e.g. "Scanning contents",
"OCR", "PDF/A conversion"). OCRmyPDF updates this before each major step.
unit (str | None):
A short label for the type of work being tracked (e.g. "page", "%", "image").
disable (bool):
If ``True``, progress updates are suppressed (no output). Defaults to ``False``.
**kwargs:
Future or extra parameters that OCRmyPDF might pass. Implementations
should accept and ignore unrecognized keywords gracefully.
Example:
A simple plugin implementation could look like this:
.. code-block:: python
from ocrmypdf.pluginspec import ProgressBar
from ocrmypdf import hookimpl
class ConsoleProgressBar(ProgressBar):
def __init__(self, *, total=None, desc=None, unit=None, disable=False, **kwargs):
self.total = total
self.desc = desc
self.unit = unit
self.disable = disable
self.current = 0
def __enter__(self):
if not self.disable:
print(f"Starting {self.desc or 'an OCR task'} (total={self.total} {self.unit})")
return self
def __exit__(self, exc_type, exc_value, traceback):
if not self.disable:
if exc_type is None:
print("Completed successfully.")
else:
print(f"Task ended with error: {exc_value}")
return False # Let OCRmyPDF raise any exceptions
def update(self, n=1, *, completed=None):
if completed is not None:
# If 'completed' is given, you could set self.current = completed
# but let's just read it to show usage
print(f"Absolute completion reported: {completed}")
# Otherwise, we increment by 'n'
self.current += n
if not self.disable:
if self.total:
percent = (self.current / self.total) * 100
print(f"{self.desc}: {self.current}/{self.total} ({percent:.1f}%)")
else:
print(f"{self.desc}: {self.current} units done")
@hookimpl
def get_progressbar_class():
return MyProgressBar
"""
def __init__(
self,
*,
total: int | float | None,
desc: str | None,
unit: str | None,
disable: bool = False,
**kwargs,
):
"""Initialize a progress bar.
This is called once before any work is done. OCRmyPDF supplies the total
number of units (or None if unknown), a description of the work, and the
type of units. The ``disable`` parameter can be used to turn off progress
reporting. Unrecognized keyword arguments should be ignored.
Args:
total (int | float | None):
The total amount of work. If ``None``, the total is unknown.
desc (str | None):
A description of the current task. May change for different stages.
unit (str | None):
A short label for the unit of work.
disable (bool):
If ``True``, no output or logging should be displayed.
**kwargs:
Extra parameters that may be passed by OCRmyPDF in future versions.
"""
def __enter__(self):
"""Enter a progress bar context."""
def __exit__(self, *args):
"""Exit a progress bar context."""
def update(self, n: float = 1, *, completed: float | None = None):
"""Increment the progress bar by ``n`` units, or set an absolute completion.
OCRmyPDF calls this method repeatedly while processing pages or other tasks.
If your total is known and you track it, you might do something like:
.. code-block:: python
self.current += n
percent = (self.current / total) * 100
The ``completed`` argument can indicate an absolute position, which is
particularly helpful if you're tracking a percentage of work (e.g., 0 to 100)
and want precise updates. In contrast, the incremental parameter ``n`` is
often more useful for page-based increments.
Args:
n (float, optional):
The amount to increment the progress by. Defaults to 1. May be
fractional if OCRmyPDF performs partial steps. If you are tracking
pages, this is typically how many pages have been processed in the
most recent step.
completed (float | None, optional):
The absolute amount of work completed so far. This can override or
supplement the simple increment logic. It's particularly useful
for percentage-based tracking (e.g., when ``total`` is 100).
"""
class NullProgressBar:
"""Progress bar API that takes no actions."""
def __init__(self, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return False
def update(self, _arg=None, *, completed=None):
return
class RichProgressBar:
"""Display progress bar using rich."""
def __init__(
self,
*,
console: Console,
desc: str,
total: float | None = None,
unit: str | None = None,
unit_scale: float | None = 1.0,
disable: bool = False,
**kwargs,
):
self._entered = False
self.progress = Progress(
TextColumn(
"[progress.description]{task.description}",
table_column=Column(min_width=20),
),
BarColumn(),
TaskProgressColumn(),
MofNCompleteColumn(),
TimeRemainingColumn(),
console=console,
auto_refresh=True,
redirect_stderr=True,
redirect_stdout=False,
disable=disable,
**kwargs,
)
self.unit_scale = unit_scale
self.progress_bar = self.progress.add_task(
desc,
total=total * self.unit_scale
if total is not None and self.unit_scale is not None
else None,
unit=unit,
)
def __enter__(self):
self.progress.start()
self._entered = True
return self
def __exit__(self, exc_type, exc_value, traceback):
self.progress.refresh()
self.progress.stop()
return False
def update(self, n=1, *, completed=None):
assert self._entered, "Progress bar must be entered before updating"
if completed is None:
advance = self.unit_scale if n is None else n
self.progress.update(self.progress_bar, advance=advance)
else:
self.progress.update(self.progress_bar, completed=completed)
-467
View File
@@ -1,467 +0,0 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
import logging
import logging.handlers
import os
import sys
import threading
from concurrent.futures.process import BrokenProcessPool
from concurrent.futures.thread import BrokenThreadPool
from functools import partial
from pathlib import Path
from tempfile import mkdtemp
from typing import NamedTuple, Sequence, cast
import PIL
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
from ocrmypdf._pipeline import (
convert_to_pdfa,
copy_final,
create_ocr_image,
create_pdf_page_from_image,
create_visible_page_jpg,
generate_postscript_stub,
get_orientation_correction,
get_pdfinfo,
is_ocr_required,
merge_sidecars,
metadata_fixup,
ocr_engine_hocr,
ocr_engine_textonly_pdf,
optimize_pdf,
preprocess_clean,
preprocess_deskew,
preprocess_remove_background,
rasterize,
rasterize_preview,
render_hocr_page,
should_visible_page_image_use_jpg,
triage,
validate_pdfinfo_options,
)
from ocrmypdf._plugin_manager import OcrmypdfPluginManager, get_plugin_manager
from ocrmypdf._validation import (
check_requested_output_file,
create_input_file,
report_output_file_size,
)
from ocrmypdf.exceptions import ExitCode, ExitCodeException
from ocrmypdf.helpers import (
NeverRaise,
available_cpu_count,
check_pdf,
pikepdf_enable_mmap,
samefile,
)
from ocrmypdf.pdfa import file_claims_pdfa
log = logging.getLogger(__name__)
class PageResult(NamedTuple):
"""Result when a page is finished processing."""
pageno: int
pdf_page_from_image: Path | None
ocr: Path | None
text: Path | None
orientation_correction: int
tls = threading.local()
tls.pageno = None
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
if hasattr(tls, 'pageno'):
record.pageno = tls.pageno
return record
logging.setLogRecordFactory(record_factory)
def preprocess(
page_context: PageContext,
image: Path,
remove_background: bool,
deskew: bool,
clean: bool,
) -> Path:
if remove_background:
image = preprocess_remove_background(image, page_context)
if deskew:
image = preprocess_deskew(image, page_context)
if clean:
image = preprocess_clean(image, page_context)
return image
def make_intermediate_images(
page_context: PageContext, orientation_correction: int
) -> tuple[Path, Path | None]:
options = page_context.options
ocr_image = preprocess_out = None
rasterize_out = rasterize(
page_context.origin,
page_context,
correction=orientation_correction,
remove_vectors=False,
)
if not any([options.clean, options.clean_final, options.remove_vectors]):
ocr_image = preprocess_out = preprocess(
page_context,
rasterize_out,
options.remove_background,
options.deskew,
clean=False,
)
else:
if not options.lossless_reconstruction:
preprocess_out = preprocess(
page_context,
rasterize_out,
options.remove_background,
options.deskew,
clean=options.clean_final,
)
if options.remove_vectors:
rasterize_ocr_out = rasterize(
page_context.origin,
page_context,
correction=orientation_correction,
remove_vectors=True,
output_tag='_ocr',
)
else:
rasterize_ocr_out = rasterize_out
if (
preprocess_out
and rasterize_ocr_out == rasterize_out
and options.clean == options.clean_final
):
# Optimization: image for OCR is identical to presentation image
ocr_image = preprocess_out
else:
ocr_image = preprocess(
page_context,
rasterize_ocr_out,
options.remove_background,
options.deskew,
clean=options.clean,
)
return ocr_image, preprocess_out
def exec_page_sync(page_context: PageContext) -> PageResult:
options = page_context.options
tls.pageno = page_context.pageno + 1
if not is_ocr_required(page_context):
return PageResult(
pageno=page_context.pageno,
pdf_page_from_image=None,
ocr=None,
text=None,
orientation_correction=0,
)
orientation_correction = 0
if options.rotate_pages:
# Rasterize
rasterize_preview_out = rasterize_preview(page_context.origin, page_context)
orientation_correction = get_orientation_correction(
rasterize_preview_out, page_context
)
ocr_image, preprocess_out = make_intermediate_images(
page_context, orientation_correction
)
ocr_image_out = create_ocr_image(ocr_image, page_context)
pdf_page_from_image_out = None
if not options.lossless_reconstruction:
assert preprocess_out
visible_image_out = preprocess_out
if should_visible_page_image_use_jpg(page_context.pageinfo):
visible_image_out = create_visible_page_jpg(visible_image_out, page_context)
filtered_image = page_context.plugin_manager.hook.filter_page_image(
page=page_context, image_filename=visible_image_out
)
if filtered_image:
visible_image_out = filtered_image
pdf_page_from_image_out = create_pdf_page_from_image(
visible_image_out, page_context, orientation_correction
)
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)
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,
pdf_page_from_image=pdf_page_from_image_out,
ocr=ocr_out,
text=text_out,
orientation_correction=orientation_correction,
)
def post_process(
pdf_file: Path, context: PdfContext, executor: Executor
) -> tuple[Path, Sequence[str]]:
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, executor)
def worker_init(max_pixels: int) -> None:
# In Windows, child process will not inherit our change to this value in
# the parent process, so ensure workers get it set. Not needed when running
# threaded, but harmless to set again.
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
pikepdf_enable_mmap()
def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
"""Execute the pipeline concurrently"""
# Run exec_page_sync on every page context
options = context.options
max_workers = min(len(context.pdfinfo), options.jobs)
if max_workers > 1:
log.info("Start processing %d pages concurrently", max_workers)
sidecars: list[Path | None] = [None] * len(context.pdfinfo)
ocrgraft = OcrGrafter(context)
def update_page(result: PageResult, pbar):
try:
tls.pageno = result.pageno + 1
sidecars[result.pageno] = result.text
pbar.update()
ocrgraft.graft_page(
pageno=result.pageno,
image=result.pdf_page_from_image,
textpdf=result.ocr,
autorotate_correction=result.orientation_correction,
)
pbar.update()
finally:
tls.pageno = None
executor(
use_threads=options.use_threads,
max_workers=max_workers,
tqdm_kwargs=dict(
total=(2 * len(context.pdfinfo)),
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
unit='page',
unit_scale=0.5,
disable=not options.progress_bar,
),
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 options.sidecar:
text = merge_sidecars(sidecars, context)
# Copy text file to destination
copy_final(text, options.sidecar, context)
# Merge layers to one single pdf
pdf = ocrgraft.finalize()
messages: Sequence[str] = []
if options.output_type != 'none':
# PDF/A and metadata
log.info("Postprocessing...")
pdf, messages = post_process(pdf, context, executor)
# Copy PDF file to destination
copy_final(pdf, options.output_file, context)
return messages
def configure_debug_logging(
log_filename: Path, prefix: str = ''
) -> logging.FileHandler:
"""
Create a debug log file at a specified location.
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(
'[%(asctime)s] - %(name)s - %(levelname)7s -%(pageno)s %(message)s'
)
log_file_handler.setFormatter(formatter)
log_file_handler.addFilter(PageNumberFilter())
logging.getLogger(prefix).addHandler(log_file_handler)
return log_file_handler
def run_pipeline(
options: argparse.Namespace,
*,
plugin_manager: OcrmypdfPluginManager | None,
api: bool = False,
) -> ExitCode:
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
# options.input_file, options.pdf_renderer are already bound.)
if not options.jobs:
options.jobs = available_cpu_count()
if not plugin_manager:
plugin_manager = get_plugin_manager(options.plugins)
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', '')
and not api
):
# 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)
# Triage image or pdf
origin_pdf = triage(
original_filename, start_input_file, work_folder / 'origin.pdf', options
)
# 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
check_pages=options.pages,
)
context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager)
# Validate options are okay for this pdf
validate_pdfinfo_options(context)
# Execute the pipeline
optimize_messages = exec_concurrent(context, executor)
if options.output_file == '-':
log.info("Output sent to stdout")
elif (
hasattr(options.output_file, 'writable') and options.output_file.writable()
):
log.info("Output written to stream")
elif samefile(options.output_file, Path(os.devnull)):
pass # Say nothing when sending to dev null
else:
if options.output_type.startswith('pdfa'):
pdfa_info = file_claims_pdfa(options.output_file)
if pdfa_info['pass']:
log.info(
"Output file is a %s (as expected)", pdfa_info['conformance']
)
else:
log.warning(
"Output file is okay but is not PDF/A (seems to be %s)",
pdfa_info['conformance'],
)
return ExitCode.pdfa_conversion_failed
if not check_pdf(options.output_file):
log.warning('Output file: The generated PDF is INVALID')
return ExitCode.invalid_output_pdf
report_output_file_size(
options, start_input_file, options.output_file, optimize_messages
)
except (KeyboardInterrupt if not api else NeverRaise):
if options.verbose >= 1:
log.exception("KeyboardInterrupt")
else:
log.error("KeyboardInterrupt")
return ExitCode.ctrl_c
except (ExitCodeException if not api else NeverRaise) as e:
e = cast(ExitCodeException, e)
if options.verbose >= 1:
log.exception("ExitCodeException")
elif str(e):
log.error("%s: %s", type(e).__name__, str(e))
else:
log.error(type(e).__name__)
return e.exit_code
except (PIL.Image.DecompressionBombError if not api else NeverRaise):
log.exception(
"A decompression bomb error was encountered while executing the "
"pipeline. Use the argument --max-image-mpixels to raise the maximum "
"image pixel limit."
)
return ExitCode.other_error
except (
BrokenProcessPool if not api else NeverRaise,
BrokenThreadPool if not api else NeverRaise,
):
log.exception(
"A worker process was terminated unexpectedly. This is known to occur if "
"processing your file takes all available swap space and RAM. It may "
"help to try again with a smaller number of jobs, using the --jobs "
"argument."
)
return ExitCode.child_process_error
except (Exception if not api else NeverRaise): # pylint: disable=broad-except
log.exception("An exception occurred while executing the pipeline")
return ExitCode.other_error
finally:
if debug_log_handler:
try:
debug_log_handler.close()
log.removeHandler(debug_log_handler)
except OSError as e:
print(e, file=sys.stderr)
cleanup_working_files(work_folder, options)
return ExitCode.ok
+57 -50
View File
@@ -12,14 +12,15 @@ import os
import sys
import unicodedata
from argparse import Namespace
from collections.abc import Sequence
from pathlib import Path
from shutil import copyfileobj
from typing import Sequence
import pikepdf
import PIL
from pluggy import PluginManager
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._exec import unpaper
from ocrmypdf.exceptions import (
BadArgsError,
@@ -27,59 +28,60 @@ from ocrmypdf.exceptions import (
MissingDependencyError,
OutputFileAccessError,
)
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
from ocrmypdf.helpers import (
is_file_writable,
monotonic,
running_in_docker,
running_in_snap,
safe_symlink,
)
from ocrmypdf.subprocess import check_external_program
# -------------
# External dependencies
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
log = logging.getLogger(__name__)
# --------
def check_platform() -> None:
if os.name == 'nt' and sys.maxsize <= 2**32: # pragma: no cover
# 32-bit interpreter on Windows
log.error(
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
if sys.maxsize <= 2**32: # pragma: no cover
log.warning(
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter. "
"This is not supported. 32-bit does not have enough address space "
"to process large files. "
"Please use a 64-bit (x86-64) version of Python."
)
def check_options_languages(options: Namespace, ocr_engine_languages: set[str]) -> None:
def check_options_languages(
options: Namespace, ocr_engine_languages: list[str]
) -> None:
if not options.languages:
options.languages = {DEFAULT_LANGUAGE}
options.languages = [DEFAULT_LANGUAGE]
system_lang = locale.getlocale()[0]
if system_lang and not system_lang.startswith('en'):
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
if not ocr_engine_languages:
return
missing_languages = options.languages - ocr_engine_languages
missing_languages = set(options.languages) - set(ocr_engine_languages)
if missing_languages:
lang_text = '\n'.join(lang for lang in missing_languages)
msg = (
"OCR engine does not have language data for the following "
"requested languages: \n"
f"{lang_text}\n"
"Please install the appropriate language data for your OCR engine.\n"
"\n"
"See the online documentation for instructions:\n"
" https://ocrmypdf.readthedocs.io/en/latest/languages.html\n"
"\n"
"Note: most languages are identified by a 3-letter ISO 639-2 Code.\n"
"For example, English is 'eng', German is 'deu', and Spanish is 'spa'.\n"
"Simplified Chinese is 'chi_sim' and Traditional Chinese is 'chi_tra'."
"\n"
)
msg += '\n'.join(lang for lang in missing_languages)
msg += '\nNote: most languages are identified by a 3-digit ISO 639-2 Code'
raise MissingDependencyError(msg)
def check_options_output(options: Namespace) -> None:
is_latin = options.languages.issubset(HOCR_OK_LANGS)
if options.pdf_renderer.startswith('hocr') and not is_latin:
log.warning(
"The 'hocr' PDF renderer is known to cause problems with one "
"or more of the languages in your document. Use "
"`--pdf-renderer auto` (the default) to avoid this issue."
)
if options.output_type == 'none' and options.output_file not in (os.devnull, '-'):
raise BadArgsError(
"Since you specified `--output-type none`, the output file "
@@ -87,6 +89,8 @@ def check_options_output(options: Namespace) -> None:
f"`-` to suppress this message."
)
def set_lossless_reconstruction(options: Namespace) -> None:
lossless_reconstruction = False
if not any(
(
@@ -109,12 +113,10 @@ def check_options_output(options: Namespace) -> None:
def check_options_sidecar(options: Namespace) -> None:
if options.sidecar == '\0':
if options.output_file == '-':
raise BadArgsError(
"--sidecar filename must be specified when output file is stdout."
)
raise BadArgsError("--sidecar filename needed when output file is stdout.")
elif options.output_file == os.devnull:
raise BadArgsError(
"--sidecar filename must be specified when output file is /dev/null or NUL."
"--sidecar filename needed when output file is /dev/null or NUL."
)
options.sidecar = options.output_file + '.txt'
if options.sidecar == options.input_file or options.sidecar == options.output_file:
@@ -128,13 +130,18 @@ def check_options_preprocessing(options: Namespace) -> None:
options.clean = True
if options.unpaper_args and not options.clean:
raise BadArgsError("--clean is required for --unpaper-args")
if (
options.rotate_pages_threshold != DEFAULT_ROTATE_PAGES_THRESHOLD
and not options.rotate_pages
):
raise BadArgsError("--rotate-pages is required for --rotate-pages-threshold")
if options.clean:
check_external_program(
program='unpaper',
package='unpaper',
version_checker=unpaper.version,
need_version='6.1',
required_for=['--clean, --clean-final'],
required_for="--clean, --clean-final", # Problem arguments
)
try:
if options.unpaper_args:
@@ -195,16 +202,6 @@ def check_options_ocr_behavior(options: Namespace) -> None:
options.pages = _pages_from_ranges(options.pages)
def check_options_advanced(options: Namespace) -> None:
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
'pdfa'
):
log.warning(
"--pdfa-image-compression argument only applies when "
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
)
def check_options_metadata(options: Namespace) -> None:
docinfo = [options.title, options.author, options.keywords, options.subject]
for s in (m for m in docinfo if m):
@@ -221,17 +218,17 @@ def check_options_metadata(options: Namespace) -> None:
def check_options_pillow(options: Namespace) -> None:
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000)
if PIL.Image.MAX_IMAGE_PIXELS == 0:
PIL.Image.MAX_IMAGE_PIXELS = None
PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore
def _check_plugin_invariant_options(options: Namespace) -> None:
check_platform()
check_options_metadata(options)
check_options_output(options)
set_lossless_reconstruction(options)
check_options_sidecar(options)
check_options_preprocessing(options)
check_options_ocr_behavior(options)
check_options_advanced(options)
check_options_pillow(options)
@@ -269,14 +266,24 @@ def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]
return target, os.fspath(options.input_file)
except FileNotFoundError as e:
msg = f"File not found - {options.input_file}"
if Path('/.dockerenv').exists(): # pragma: no cover
if running_in_docker(): # pragma: no cover
msg += (
"\nDocker cannot your working directory unless you "
"\nDocker cannot access your working directory unless you "
"explicitly share it with the Docker container and set up"
"permissions correctly.\n"
"You may find it easier to use stdin/stdout:"
"\n"
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf\n"
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf"
"\n"
)
elif running_in_snap(): # pragma: no cover
msg += (
"\nSnap applications cannot access files outside of "
"your home directory unless you explicitly allow it. "
"You may find it easier to use stdin/stdout:"
"\n"
"\tsnap run ocrmypdf - - <input.pdf >output.pdf"
"\n"
)
raise InputFileError(msg) from e
@@ -333,7 +340,7 @@ def report_output_file_size(
for arg in image_preproc:
if getattr(options, arg, False):
reasons.append(
f"The argument --{arg.replace('_', '-')} was issued, causing transcoding."
f"--{arg.replace('_', '-')} was issued, causing transcoding."
)
reasons.extend(optimize_messages)
-19
View File
@@ -1,19 +0,0 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""Get version by introspecting package information.
OCRmyPDF uses setuptools_scm to derive version from git tags.
"""
from __future__ import annotations
try:
from importlib.metadata import version as _package_version
except ImportError:
from importlib_metadata import version as _package_version # type: ignore
PROGRAM_NAME = 'ocrmypdf'
# Official PEP 396
__version__ = _package_version('ocrmypdf')
+297 -94
View File
@@ -9,31 +9,31 @@ import logging
import os
import sys
import threading
from argparse import Namespace
from collections.abc import Iterable, Sequence
from enum import IntEnum
from io import IOBase
from pathlib import Path
from typing import AnyStr, BinaryIO, Iterable, Union
from typing import BinaryIO
from warnings import warn
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
import pluggy
from ocrmypdf._logging import PageNumberFilter
from ocrmypdf._pipelines.hocr_to_ocr_pdf import run_hocr_to_ocr_pdf_pipeline
from ocrmypdf._pipelines.ocr import run_pipeline, run_pipeline_cli
from ocrmypdf._pipelines.pdf_to_hocr import run_hocr_pipeline
from ocrmypdf._plugin_manager import get_plugin_manager
from ocrmypdf._sync import run_pipeline
from ocrmypdf._validation import check_options
from ocrmypdf.cli import ArgumentParser, get_parser
from ocrmypdf.helpers import is_iterable_notstr
try:
import coloredlogs
except ModuleNotFoundError:
coloredlogs = None # pylint: disable=invalid-name
if coloredlogs:
from humanfriendly.terminal import enable_ansi_support
StrPath = Union[Path, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
StrPath = Path | str | bytes
PathOrIO = BinaryIO | StrPath
# Installing plugins affects the global state of the Python interpreter,
# so we need to use a lock to prevent multiple threads from installing
# plugins at the same time.
_api_lock = threading.Lock()
@@ -52,7 +52,7 @@ def configure_logging(
*,
progress_bar_friendly: bool = True,
manage_root_logger: bool = False,
plugin_manager=None,
plugin_manager: pluggy.PluginManager | None = None,
):
"""Set up logging.
@@ -92,7 +92,6 @@ def configure_logging(
Returns:
The toplevel logger for ocrmypdf (or the root logger, if we are managing it).
"""
prefix = '' if manage_root_logger else 'ocrmypdf'
log = logging.getLogger(prefix)
@@ -119,14 +118,7 @@ def configure_logging(
else:
fmt = '%(pageno)s%(message)s'
use_colors = progress_bar_friendly
formatter = None
if coloredlogs and use_colors:
use_colors = enable_ansi_support()
if use_colors:
use_colors = coloredlogs.terminal_supports_colors()
if use_colors:
formatter = coloredlogs.ColoredFormatter(fmt=fmt)
if not formatter:
formatter = logging.Formatter(fmt=fmt)
@@ -146,20 +138,19 @@ def configure_logging(
return log
def create_options(
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
):
cmdline = []
deferred = []
def _kwargs_to_cmdline(
*, defer_kwargs: set[str], **kwargs
) -> tuple[list[str | bytes], dict[str, str | bytes]]:
"""Convert kwargs to command line arguments."""
cmdline: list[str | bytes] = []
deferred = {}
for arg, val in kwargs.items():
if val is None:
continue
# These arguments with special handling for which we bypass
# argparse
if arg in {'progress_bar', 'plugins'}:
deferred.append((arg, val))
# Skip arguments that are handled elsewhere
if arg in defer_kwargs:
deferred[arg] = val
continue
cmd_style_arg = arg.replace('_', '-')
@@ -178,7 +169,7 @@ def create_options(
# We have a parameter
cmdline.append(f"--{cmd_style_arg}")
if isinstance(val, (int, float)):
if isinstance(val, int | float):
cmdline.append(str(val))
elif isinstance(val, str):
cmdline.append(val)
@@ -186,82 +177,129 @@ def create_options(
cmdline.append(str(val))
else:
raise TypeError(f"{arg}: {val} ({type(val)})")
return cmdline, deferred
if isinstance(input_file, (BinaryIO, IOBase)):
def create_options(
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
) -> Namespace:
"""Construct an options object from the input/output files and keyword arguments.
Args:
input_file: Input file path or file object.
output_file: Output file path or file object.
parser: ArgumentParser object.
**kwargs: Keyword arguments.
Returns:
argparse.Namespace: A Namespace object containing the parsed arguments.
Raises:
TypeError: If the type of a keyword argument is not supported.
"""
cmdline, deferred = _kwargs_to_cmdline(
defer_kwargs={'progress_bar', 'plugins', 'parser', 'input_file', 'output_file'},
**kwargs,
)
if isinstance(input_file, BinaryIO | IOBase):
cmdline.append('stream://input_file')
else:
cmdline.append(os.fspath(input_file))
if isinstance(output_file, (BinaryIO, IOBase)):
if isinstance(output_file, BinaryIO | IOBase):
cmdline.append('stream://output_file')
else:
cmdline.append(os.fspath(output_file))
if 'sidecar' in kwargs and isinstance(kwargs['sidecar'], BinaryIO | IOBase):
cmdline.append('--sidecar')
cmdline.append('stream://sidecar')
parser.enable_api_mode()
options = parser.parse_args(cmdline)
for keyword, val in deferred:
for keyword, val in deferred.items():
setattr(options, keyword, val)
if options.input_file == 'stream://input_file':
options.input_file = input_file
if options.output_file == 'stream://output_file':
options.output_file = output_file
if options.sidecar == 'stream://sidecar':
options.sidecar = kwargs['sidecar']
return options
def ocr( # pylint: disable=unused-argument
def ocr( # noqa: D417
input_file: PathOrIO,
output_file: PathOrIO,
*,
language: Iterable[str] = None,
image_dpi: int = None,
output_type=None,
sidecar: StrPath | None = None,
jobs: int = None,
use_threads: bool = None,
title: str = None,
author: str = None,
subject: str = None,
keywords: str = None,
rotate_pages: bool = None,
remove_background: bool = None,
deskew: bool = None,
clean: bool = None,
clean_final: bool = None,
unpaper_args: str = None,
oversample: int = None,
remove_vectors: bool = None,
force_ocr: bool = None,
skip_text: bool = None,
redo_ocr: bool = None,
skip_big: float = None,
optimize: int = None,
jpg_quality: int = None,
png_quality: int = None,
jbig2_lossy: bool = None,
jbig2_page_group_size: int = None,
pages: str = None,
max_image_mpixels: float = None,
tesseract_config: Iterable[str] = None,
tesseract_pagesegmode: int = None,
tesseract_oem: int = None,
tesseract_thresholding: int = None,
pdf_renderer=None,
tesseract_timeout: float = None,
rotate_pages_threshold: float = None,
pdfa_image_compression=None,
user_words: os.PathLike = None,
user_patterns: os.PathLike = None,
fast_web_view: float = None,
plugins: Iterable[StrPath] = None,
language: Iterable[str] | None = None,
image_dpi: int | None = None,
output_type: str | None = None,
sidecar: PathOrIO | None = None,
jobs: int | None = None,
use_threads: bool | None = None,
title: str | None = None,
author: str | None = None,
subject: str | None = None,
keywords: str | None = None,
rotate_pages: bool | None = None,
remove_background: bool | None = None,
deskew: bool | None = None,
clean: bool | None = None,
clean_final: bool | None = None,
unpaper_args: str | None = None,
oversample: int | None = None,
remove_vectors: bool | None = None,
force_ocr: bool | None = None,
skip_text: bool | None = None,
redo_ocr: bool | None = None,
skip_big: float | None = None,
optimize: int | None = None,
jpg_quality: int | None = None,
png_quality: int | None = None,
jbig2_lossy: bool | None = None,
jbig2_page_group_size: int | None = None,
jbig2_threshold: float | None = None,
pages: str | None = None,
max_image_mpixels: float | None = None,
tesseract_config: Iterable[str] | None = None,
tesseract_pagesegmode: int | None = None,
tesseract_oem: int | None = None,
tesseract_thresholding: int | None = None,
pdf_renderer: str | None = None,
tesseract_timeout: float | None = None,
tesseract_non_ocr_timeout: float | None = None,
tesseract_downsample_above: int | None = None,
tesseract_downsample_large_images: bool | None = None,
rotate_pages_threshold: float | None = None,
pdfa_image_compression: str | None = None,
color_conversion_strategy: str | None = None,
user_words: os.PathLike | None = None,
user_patterns: os.PathLike | None = None,
fast_web_view: float | None = None,
continue_on_soft_render_error: bool | None = None,
invalidate_digital_signatures: bool | None = None,
plugins: Iterable[Path | str] | None = None,
plugin_manager=None,
keep_temporary_files: bool = None,
progress_bar: bool = None,
keep_temporary_files: bool | None = None,
progress_bar: bool | None = None,
**kwargs,
):
"""Run OCRmyPDF on one PDF or image.
For most arguments, see documentation for the equivalent command line parameter.
This API takes a threading lock, because OCRmyPDF uses global state in particular
for the plugin system. The jobs parameter will be used to create a pool of
worker threads or processes at different times, subject to change. A Python
process can only run one OCRmyPDF task at a time.
To run parallelize instances OCRmyPDF, use separate Python processes to scale
horizontally. Generally speaking you should set jobs=sqrt(cpu_count) and run
sqrt(cpu_count) processes as a starting point. If you have files with a high page
count, run fewer processes and more jobs per process. If you have a lot of short
files, run more processes and fewer jobs per process.
A few specific arguments are discussed here:
Args:
@@ -283,9 +321,8 @@ def ocr( # pylint: disable=unused-argument
When a stream is used as output, whether via a writable object or
``"-"``, some final validation steps are not performed (we do not read
back the stream after it is written).
Raises:
ocrmypdf.PdfMergeFailedError: If the input PDF is malformed, preventing merging
with the OCR layer.
ocrmypdf.MissingDependencyError: If a required dependency program is missing or
was not found on PATH.
ocrmypdf.UnsupportedImageFormatError: If the input file type was an image that
@@ -311,23 +348,21 @@ def ocr( # pylint: disable=unused-argument
if not plugins:
plugins = []
elif isinstance(plugins, (str, Path)):
elif isinstance(plugins, str | Path):
plugins = [plugins]
else:
plugins = list(plugins)
# 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 = {
k: v
for k, v in locals().items()
if k not in {'input_file', 'output_file', 'kwargs'}
}
create_options_kwargs.update(kwargs)
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
@@ -335,14 +370,181 @@ def ocr( # pylint: disable=unused-argument
if 'verbose' in kwargs:
warn("ocrmypdf.ocr(verbose=) is ignored. Use ocrmypdf.configure_logging().")
options = create_options(**create_options_kwargs)
options = create_options(
input_file=input_file,
output_file=output_file,
parser=parser,
**create_options_kwargs,
)
check_options(options, plugin_manager)
return run_pipeline(options=options, plugin_manager=plugin_manager, api=True)
return run_pipeline(options=options, plugin_manager=plugin_manager)
def _pdf_to_hocr( # noqa: D417
input_pdf: Path,
output_folder: Path,
*,
language: Iterable[str] | None = None,
image_dpi: int | None = None,
jobs: int | None = None,
use_threads: bool | None = None,
title: str | None = None,
author: str | None = None,
subject: str | None = None,
keywords: str | None = None,
rotate_pages: bool | None = None,
remove_background: bool | None = None,
deskew: bool | None = None,
clean: bool | None = None,
clean_final: bool | None = None,
unpaper_args: str | None = None,
oversample: int | None = None,
remove_vectors: bool | None = None,
force_ocr: bool | None = None,
skip_text: bool | None = None,
redo_ocr: bool | None = None,
skip_big: float | None = None,
pages: str | None = None,
max_image_mpixels: float | None = None,
tesseract_config: Iterable[str] | None = None,
tesseract_pagesegmode: int | None = None,
tesseract_oem: int | None = None,
tesseract_thresholding: int | None = None,
tesseract_timeout: float | None = None,
tesseract_non_ocr_timeout: float | None = None,
tesseract_downsample_above: int | None = None,
tesseract_downsample_large_images: bool | None = None,
rotate_pages_threshold: float | None = None,
user_words: os.PathLike | None = None,
user_patterns: os.PathLike | None = None,
continue_on_soft_render_error: bool | None = None,
invalidate_digital_signatures: bool | None = None,
plugin_manager=None,
plugins: Sequence[Path | str] | None = None,
keep_temporary_files: bool | None = None,
**kwargs,
):
"""Partially run OCRmyPDF and produces an output folder containing hOCR files.
Given a PDF file, this function will run OCRmyPDF up to the point where
the PDF is rasterized to images, OCRed, and the hOCR files are produced,
all of which are saved to the output folder. This is useful for applications
that want to provide an interface for users to edit the text before
rendering the final PDF.
Use :func:`hocr_to_ocr_pdf` to produce the final PDF.
For arguments not explicitly documented here, see documentation for the
equivalent command line parameter.
This API is **experimental** and subject to change.
Args:
input_pdf: Input PDF file path.
output_folder: Output folder path.
**kwargs: Keyword arguments.
"""
# 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 not in {'input_pdf', 'output_folder', 'kwargs'}
}
create_options_kwargs.update(kwargs)
parser = get_parser()
with _api_lock:
if not plugin_manager:
plugin_manager = get_plugin_manager(plugins)
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
cmdline, deferred = _kwargs_to_cmdline(
defer_kwargs={'input_pdf', 'output_folder', 'plugins'},
**create_options_kwargs,
)
cmdline.append(str(input_pdf))
cmdline.append(str(output_folder))
parser.enable_api_mode()
options = parser.parse_args(cmdline)
for keyword, val in deferred.items():
setattr(options, keyword, val)
delattr(options, 'output_file')
setattr(options, 'output_folder', output_folder)
return run_hocr_pipeline(options=options, plugin_manager=plugin_manager)
def _hocr_to_ocr_pdf( # noqa: D417
work_folder: Path,
output_file: Path,
*,
jobs: int | None = None,
use_threads: bool | None = None,
optimize: int | None = None,
jpg_quality: int | None = None,
png_quality: int | None = None,
jbig2_lossy: bool | None = None,
jbig2_page_group_size: int | None = None,
jbig2_threshold: float | None = None,
pdfa_image_compression: str | None = None,
color_conversion_strategy: str | None = None,
fast_web_view: float | None = None,
plugin_manager=None,
plugins: Sequence[Path | str] | None = None,
**kwargs,
):
"""Run OCRmyPDF on a work folder and produce an output PDF.
After running :func:`pdf_to_hocr`, this function will run OCRmyPDF on the work
folder to produce an output PDF. This function consolidates any changes made
to the hOCR files in the work folder and produces a final PDF.
For arguments not explicitly documented here, see documentation for the
equivalent command line parameter.
This API is **experimental** and subject to change.
Args:
work_folder: Work folder path, as generated by :func:`pdf_to_hocr`.
output_file: Output PDF file path.
**kwargs: Keyword arguments.
"""
# 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 not in {'work_folder', 'output_pdf', 'kwargs'}
}
create_options_kwargs.update(kwargs)
parser = get_parser()
with _api_lock:
if not plugin_manager:
plugin_manager = get_plugin_manager(plugins)
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
cmdline, deferred = _kwargs_to_cmdline(
defer_kwargs={'work_folder', 'output_file', 'plugins'},
**create_options_kwargs,
)
cmdline.append(str(work_folder))
cmdline.append(str(output_file))
parser.enable_api_mode()
options = parser.parse_args(cmdline)
for keyword, val in deferred.items():
setattr(options, keyword, val)
delattr(options, 'input_file')
setattr(options, 'work_folder', work_folder)
return run_hocr_to_ocr_pdf_pipeline(
options=options, plugin_manager=plugin_manager
)
__all__ = [
'PageNumberFilter',
'TqdmConsole',
'Verbosity',
'check_options',
'configure_logging',
@@ -351,4 +553,5 @@ __all__ = [
'get_plugin_manager',
'ocr',
'run_pipeline',
'run_pipeline_cli',
]
+2 -4
View File
@@ -1,8 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
"""Plugins in this package are automatically loaded by ocrmypdf."""
# 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.
from __future__ import annotations
+45 -32
View File
@@ -1,9 +1,9 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
"""OCRmyPDF's multiprocessing/multithreading abstraction layer."""
from __future__ import annotations
import logging
import logging.handlers
import multiprocessing
@@ -12,25 +12,29 @@ import queue
import signal
import sys
import threading
from collections.abc import Callable, Iterable
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from contextlib import suppress
from typing import Callable, Iterable, Type, Union
from typing import Union
from tqdm import tqdm
from rich.console import Console as RichConsole
from ocrmypdf import Executor, hookimpl
from ocrmypdf._logging import TqdmConsole
from ocrmypdf._logging import RichLoggingHandler
from ocrmypdf._progressbar import RichProgressBar
from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers
FuturesExecutorClass = Union[Type[ThreadPoolExecutor], Type[ProcessPoolExecutor]]
Queue = Union[multiprocessing.Queue, queue.Queue]
FuturesExecutorClass = Union[ # noqa: UP007
type[ThreadPoolExecutor], type[ProcessPoolExecutor]
]
Queue = Union[multiprocessing.Queue, queue.Queue] # noqa: UP007
UserInit = Callable[[], None]
WorkerInit = Callable[[Queue, UserInit, int], None]
def log_listener(q: Queue):
"""Listen to the worker processes and forward the messages to logging
"""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
@@ -39,7 +43,6 @@ def log_listener(q: Queue):
See:
https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
"""
while True:
try:
record = q.get()
@@ -55,12 +58,12 @@ def log_listener(q: Queue):
def process_sigbus(*args):
"""Handle SIGBUS signal at the worker level."""
raise InputFileError("A worker process lost access to an input file")
def process_init(q: Queue, user_init: UserInit, loglevel) -> None:
"""Initialize a process pool worker"""
"""Initialize a process pool worker."""
# Ignore SIGINT (our parent process will kill us gracefully)
signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -69,7 +72,7 @@ def process_init(q: Queue, user_init: UserInit, loglevel) -> None:
# 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
# Remove any log handlers inherited from the parent process
root = logging.getLogger()
remove_all_log_handlers(root)
@@ -82,6 +85,7 @@ def process_init(q: Queue, user_init: UserInit, loglevel) -> None:
def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
"""Begin a thread pool worker."""
del q # unused but required argument
del loglevel # unused but required argument
# As a thread, block SIGBUS so the main thread deals with it...
@@ -95,21 +99,12 @@ def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
class StandardExecutor(Executor):
"""Standard OCRmyPDF concurrent task executor."""
def _cancel_futures_kwargs(self):
"""Shim older Pythons that do not have Executor.shutdown(...cancel_futures=).
Remove this code when support for Python 3.8 is dropped.
"""
if sys.version_info[:2] < (3, 9):
return {}
return dict(cancel_futures=True)
def _execute(
self,
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
progress_kwargs: dict,
worker_initializer: Callable,
task: Callable,
task_arguments: Iterable,
@@ -127,22 +122,30 @@ class StandardExecutor(Executor):
# 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.
# If use_threads is False, we are currently guilty of creating a thread before
# forking on Linux, which is not recommended. However, we take a big
# performance hit in pdfinfo if we can't fork. Long term solution is to
# replace most of this with an asyncio implementation, and probably to
# migrate some of pdfinfo into C++ or Rust.
listener = threading.Thread(target=log_listener, args=(log_queue,))
listener.start()
with self.pbar_class(**tqdm_kwargs) as pbar, executor_class(
max_workers=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor:
futures = [executor.submit(task, args) for args in task_arguments]
with (
self.pbar_class(**progress_kwargs) as pbar,
executor_class(
max_workers=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor,
):
futures = [executor.submit(task, *args) for args in task_arguments]
try:
for future in as_completed(futures):
result = future.result()
task_finished(result, pbar)
except KeyboardInterrupt:
# Terminate pool so we exit instantly
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
executor.shutdown(wait=False, cancel_futures=True)
raise
except Exception:
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
@@ -151,7 +154,7 @@ class StandardExecutor(Executor):
# results will be discard. But if the condition above is True,
# then we are running in pytest, and we want everything to exit
# as cleanly as possible so that we get good error messages.
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
executor.shutdown(wait=False, cancel_futures=True)
raise
finally:
# Terminate log listener
@@ -164,14 +167,24 @@ class StandardExecutor(Executor):
@hookimpl
def get_executor(progressbar_class):
"""Return the default executor."""
return StandardExecutor(pbar_class=progressbar_class)
RICH_CONSOLE = RichConsole(stderr=True)
@hookimpl
def get_progressbar_class():
return tqdm
"""Return the default progress bar class."""
def partial_RichProgressBar(*args, **kwargs):
return RichProgressBar(*args, **kwargs, console=RICH_CONSOLE)
return partial_RichProgressBar
@hookimpl
def get_logging_console():
return logging.StreamHandler(stream=TqdmConsole(sys.stderr))
"""Return the default logging console handler."""
return RichLoggingHandler(console=RICH_CONSOLE)
@@ -8,7 +8,5 @@ from ocrmypdf import hookimpl
@hookimpl
def filter_pdf_page(
page, image_filename, output_pdf
): # pylint: disable=unused-argument
def filter_pdf_page(page, image_filename, output_pdf): # pylint: disable=unused-argument
return output_pdf
+67 -24
View File
@@ -6,50 +6,86 @@ from __future__ import annotations
import logging
from packaging.version import Version
from ocrmypdf import hookimpl
from ocrmypdf._exec import ghostscript
from ocrmypdf._validation import HOCR_OK_LANGS
from ocrmypdf.exceptions import MissingDependencyError
from ocrmypdf.subprocess import check_external_program
log = logging.getLogger(__name__)
# Currently all blacklisted versions are lower than 9.55, so none need to
# be added here. If a future version is blacklisted, add it here.
BLACKLISTED_GS_VERSIONS: frozenset[Version] = frozenset()
@hookimpl
def add_options(parser):
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
gs.add_argument(
'--color-conversion-strategy',
action='store',
type=str,
metavar='STRATEGY',
choices=ghostscript.COLOR_CONVERSION_STRATEGIES,
default='LeaveColorUnchanged',
help="Set Ghostscript color conversion strategy",
)
gs.add_argument(
'--pdfa-image-compression',
choices=['auto', 'jpeg', 'lossless'],
default='auto',
help="Specify how to compress images in the output PDF/A. 'auto' lets "
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
"JPEG compression. 'lossless' uses PNG-style lossless compression "
"for all images. Monochrome images are always compressed using a "
"lossless codec. Compression settings "
"are applied to all pages, including those for which OCR was "
"skipped. Not supported for --output-type=pdf ; that setting "
"preserves the original compression of all images.",
)
@hookimpl
def check_options(options):
"""Check that the options are valid for this plugin."""
check_external_program(
program='gs',
package='ghostscript',
version_checker=ghostscript.version,
need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports
need_version='9.54', # RHEL 9's version; Ubuntu 22.04 has 9.55
)
gs_version = ghostscript.version()
if gs_version in ('9.24', '9.51'):
if gs_version in BLACKLISTED_GS_VERSIONS:
raise MissingDependencyError(
f"Ghostscript {gs_version} contains serious regressions and is not "
"supported. Please upgrade to a newer version, or downgrade to the "
"previous version."
"supported. Please upgrade to a newer version."
)
# We have these constraints to check for.
# 1. Ghostscript < 9.20 mangles multibyte Unicode
# 2. hocr doesn't work on non-Latin languages (so don't select it)
is_latin = options.languages.issubset(HOCR_OK_LANGS)
if gs_version < '9.20' and options.output_type != 'pdf' and not is_latin:
# https://bugs.ghostscript.com/show_bug.cgi?id=696874
# Ghostscript < 9.20 fails to encode multibyte characters properly
log.warning(
f"The installed version of Ghostscript ({gs_version}) does not work "
"correctly with the OCR languages you specified. Use --output-type pdf or "
"upgrade to Ghostscript 9.20 or later to avoid this issue."
if Version('10.0.0') <= gs_version < Version('10.02.1') and (
options.skip_text or options.redo_ocr
):
raise MissingDependencyError(
f"Ghostscript 10.0.0 through 10.02.0 (your version: {gs_version}) "
"contain serious regressions that corrupt PDFs with existing text, "
"such as those processed using --skip-text or --redo-ocr. "
"Please upgrade to a "
"newer version, or use --output-type pdf to avoid Ghostscript, or "
"use --force-ocr to discard existing text."
)
if options.output_type == 'pdfa':
options.output_type = 'pdfa-2'
if options.output_type == 'pdfa-3' and ghostscript.version() < '9.19':
raise MissingDependencyError(
"--output-type pdfa-3 requires Ghostscript 9.19 or later"
if options.color_conversion_strategy not in ghostscript.COLOR_CONVERSION_STRATEGIES:
raise ValueError(
f"Invalid color conversion strategy: {options.color_conversion_strategy}"
)
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
'pdfa'
):
log.warning(
"--pdfa-image-compression argument only applies when "
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
)
@@ -63,7 +99,9 @@ def rasterize_pdf_page(
page_dpi,
rotation,
filter_vector,
stop_on_soft_error,
):
"""Rasterize a single page of a PDF file using Ghostscript."""
ghostscript.rasterize_pdf(
input_file,
output_file,
@@ -73,6 +111,7 @@ def rasterize_pdf_page(
page_dpi=page_dpi,
rotation=rotation,
filter_vector=filter_vector,
stop_on_error=stop_on_soft_error,
)
return output_file
@@ -82,17 +121,21 @@ def generate_pdfa(
pdf_pages,
pdfmark,
output_file,
compression,
context,
pdf_version,
pdfa_part,
progressbar_class,
stop_on_soft_error,
):
"""Generate a PDF/A from the list of PDF pages and PDF/A metadata."""
ghostscript.generate_pdfa(
pdf_pages=[*pdf_pages, pdfmark],
pdf_pages=[pdfmark, *pdf_pages],
output_file=output_file,
compression=compression,
compression=context.options.pdfa_image_compression,
color_conversion_strategy=context.options.color_conversion_strategy,
pdf_version=pdf_version,
pdfa_part=pdfa_part,
progressbar_class=progressbar_class,
stop_on_error=stop_on_soft_error,
)
return output_file
+12 -2
View File
@@ -6,8 +6,8 @@ from __future__ import annotations
import argparse
import logging
from collections.abc import Sequence
from pathlib import Path
from typing import Sequence
from ocrmypdf import Executor, PdfContext, hookimpl
from ocrmypdf._exec import jbig2enc, pngquant
@@ -86,6 +86,16 @@ def add_options(parser):
# Adjust number of pages to consider at once for JBIG2 compression
help=argparse.SUPPRESS,
)
optimizing.add_argument(
'--jbig2-threshold',
type=numeric(float, 0.4, 0.9),
default=0.85,
metavar='T',
help=(
"Adjust JBIG2 symbol code classification threshold "
"(default 0.85), range 0.4 to 0.9."
),
)
@hookimpl
@@ -95,7 +105,7 @@ def check_options(options):
program='pngquant',
package='pngquant',
version_checker=pngquant.version,
need_version='2.0.1',
need_version='2.12.2',
required_for='--optimize {2,3}',
)
+108 -18
View File
@@ -2,16 +2,21 @@
# SPDX-License-Identifier: MPL-2.0
"""Built-in plugin to implement OCR using Tesseract."""
from __future__ import annotations
import argparse
import logging
import os
from PIL import Image
from ocrmypdf import hookimpl
from ocrmypdf._exec import tesseract
from ocrmypdf._jobcontext import PageContext
from ocrmypdf.cli import numeric, str_to_int
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
from ocrmypdf.helpers import clamp
from ocrmypdf.imageops import calculate_downsample, downsample_image
from ocrmypdf.pluginspec import OcrEngine
from ocrmypdf.subprocess import check_external_program
@@ -26,7 +31,7 @@ def add_options(parser):
action='append',
metavar='CFG',
default=[],
help="Additional Tesseract configuration files -- see documentation",
help="Additional Tesseract configuration files -- see documentation.",
)
tess.add_argument(
'--tesseract-pagesegmode',
@@ -34,7 +39,7 @@ def add_options(parser):
type=int,
metavar='PSM',
choices=range(0, 14),
help="Set Tesseract page segmentation mode (see tesseract --help)",
help="Set Tesseract page segmentation mode (see tesseract --help).",
)
tess.add_argument(
'--tesseract-oem',
@@ -43,7 +48,7 @@ def add_options(parser):
metavar='MODE',
choices=range(0, 4),
help=(
"Set Tesseract 4.0+ OCR engine mode: "
"Set Tesseract 4+ OCR engine mode: "
"0 - original Tesseract only; "
"1 - neural nets LSTM only; "
"2 - Tesseract + LSTM; "
@@ -58,7 +63,7 @@ def add_options(parser):
metavar='METHOD',
help=(
"Set Tesseract 5.0+ input image thresholding mode. This may improve OCR "
"results on low quality images or those that contain high constrast color. "
"results on low quality images or those that contain high contrast color. "
"legacy-otsu is the Tesseract default; adaptive-otsu is an improved Otsu "
"algorithm with improved sort for background color changes; sauvola is "
"based on local standard deviation."
@@ -69,8 +74,52 @@ def add_options(parser):
default=180.0,
type=numeric(float, 0),
metavar='SECONDS',
help='Give up on OCR after the timeout, but copy the preprocessed page '
'into the final output',
help=(
"Give up on OCR after the timeout, but copy the preprocessed page "
"into the final output. This timeout is only used when using Tesseract "
"for OCR. When Tesseract is used for other operations such as "
"deskewing and orientation, the timeout is controlled by "
"--tesseract-non-ocr-timeout."
),
)
tess.add_argument(
'--tesseract-non-ocr-timeout',
default=180.0,
type=numeric(float, 0),
metavar='SECONDS',
help=(
"Give up on non-OCR operations such as deskewing and orientation "
"after timeout. This is a separate timeout from --tesseract-timeout "
"because these operations are not as expensive as OCR."
),
)
tess.add_argument(
'--tesseract-downsample-large-images',
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Downsample large images before OCR. Tesseract has an upper limit on the "
"size images it will support. If this argument is given, OCRmyPDF will "
"downsample large images to fit Tesseract. This may reduce OCR quality, "
"on large images the most desirable text is usually larger. If this "
"parameter is not supplied, Tesseract will error out and produce no OCR "
"on the page in question. This argument should be used with a high value "
"of --tesseract-timeout to ensure Tesseract has enough to time."
),
)
tess.add_argument(
'--tesseract-downsample-above',
action='store',
type=numeric(int, 100, 32767),
default=32767,
help=(
"Downsample images larger than this size pixel size in either dimension "
"before OCR. --tesseract-downsample-large-images downsamples only when "
"an image exceeds Tesseract's internal limits. This argument causes "
"downsampling to occur when an image exceeds the given size. This may "
"reduce OCR quality, but on large images the most desirable text is "
"usually larger."
),
)
tess.add_argument(
'--user-words',
@@ -93,19 +142,24 @@ def check_options(options):
program='tesseract',
package={'linux': 'tesseract-ocr'},
version_checker=tesseract.version,
need_version='4.0.0-beta.1', # using backport for Travis CI
need_version='4.1.1', # Ubuntu 22.04 version (also 20.04)
version_parser=tesseract.TesseractVersion,
)
tess_version = tesseract.version()
if tess_version == tesseract.TesseractVersion('5.4.0'):
raise MissingDependencyError(
"Tesseract 5.4.0 is not supported due to regressions in this version. "
"Please upgrade to a newer or supported older version."
)
# Decide on what renderer to use
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'sandwich'
if {'ara', 'heb', 'fas', 'per'} & set(options.languages):
log.info("Using sandwich renderer since there is an RTL language")
options.pdf_renderer = 'sandwich'
else:
options.pdf_renderer = 'hocr'
if not tesseract.has_user_words() and (options.user_words or options.user_patterns):
log.warning(
"Tesseract 4.0 (which you have installed) ignores --user-words and "
"--user-patterns, so these arguments have no effect."
)
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
log.warning(
"The installed version of Tesseract does not support changes to its "
@@ -117,6 +171,14 @@ def check_options(options):
"The --tesseract-pagesegmode argument you select will disable OCR. "
"This may cause processing to fail."
)
DENIED_LANGUAGES = {'equ', 'osd'}
if DENIED_LANGUAGES & set(options.languages):
raise BadArgsError(
"The following languages for Tesseract's internal use and should not "
"be issued explicitly: "
f"{', '.join(DENIED_LANGUAGES & set(options.languages))}\n"
"Remove them from the -l/--language argument."
)
@hookimpl
@@ -136,17 +198,45 @@ def validate(pdfinfo, options):
tess_threads = int(os.environ['OMP_THREAD_LIMIT'])
log.debug("Using Tesseract OpenMP thread limit %d", tess_threads)
if (
options.tesseract_downsample_above != 32767
and not options.tesseract_downsample_large_images
):
log.warning(
"The --tesseract-downsample-above argument will have no effect unless "
"--tesseract-downsample-large-images is also given."
)
@hookimpl
def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
"""Filter the image before OCR.
Tesseract cannot handle images with more than 32767 pixels in either axis,
or more than 2**31 bytes. This function resizes the image to fit within
those limits.
"""
threshold = min(page.options.tesseract_downsample_above, 32767)
options = page.options
if options.tesseract_downsample_large_images:
size = calculate_downsample(
image, max_size=(threshold, threshold), max_bytes=(2**31) - 1
)
image = downsample_image(image, size)
return image
class TesseractOcrEngine(OcrEngine):
"""Implements OCR with Tesseract."""
@staticmethod
def version():
return tesseract.version()
return str(tesseract.version())
@staticmethod
def creator_tag(options):
tag = '-PDF' if options.pdf_renderer == 'sandwich' else ''
tag = '-PDF' if options.pdf_renderer == 'sandwich' else '-hOCR'
return f"Tesseract OCR{tag} {TesseractOcrEngine.version()}"
def __str__(self):
@@ -161,7 +251,7 @@ class TesseractOcrEngine(OcrEngine):
return tesseract.get_orientation(
input_file,
engine_mode=options.tesseract_oem,
timeout=options.tesseract_timeout,
timeout=options.tesseract_non_ocr_timeout,
)
@staticmethod
@@ -170,7 +260,7 @@ class TesseractOcrEngine(OcrEngine):
input_file,
languages=options.languages,
engine_mode=options.tesseract_oem,
timeout=options.tesseract_timeout,
timeout=options.tesseract_non_ocr_timeout,
)
@staticmethod
+46 -25
View File
@@ -6,16 +6,22 @@
from __future__ import annotations
import argparse
from typing import Any, Callable, Mapping, TypeVar
from collections.abc import Callable, Mapping
from typing import Any, TypeVar
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._defaults import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION
T = TypeVar('T', int, float)
def numeric(basetype: Callable[[Any], T], min_: T | None = None, max_: T | None = None):
"""Validator for numeric params"""
"""Validator for numeric command line parameters.
Stipulates that the value must be of type basetype (typically int or float), and
optionally, within the range [min_, max_].
"""
min_ = basetype(min_) if min_ is not None else None
max_ = basetype(max_) if max_ is not None else None
@@ -46,7 +52,7 @@ def str_to_int(mapping: Mapping[str, int]):
class ArgumentParser(argparse.ArgumentParser):
"""Override parser's default behavior of calling sys.exit()
"""Override parser's default behavior of calling sys.exit().
https://stackoverflow.com/questions/5943249/python-argparse-and-controlling-overriding-the-exit-status-code
@@ -57,13 +63,21 @@ class ArgumentParser(argparse.ArgumentParser):
"""
def __init__(self, *args, **kwargs):
"""Initialize the parser."""
super().__init__(*args, **kwargs)
self._api_mode = False
def enable_api_mode(self):
"""Enable API mode.
When set, the parser will not call sys.exit() on error. OCRmyPDF was originally
a command line program, but now it has an API. The API works by synthesizing
command line arguments.
"""
self._api_mode = True
def error(self, message):
"""Override the default argparse error behavior."""
if not self._api_mode:
super().error(message)
return
@@ -74,19 +88,22 @@ class LanguageSetAction(argparse.Action):
"""Manages a list of languages."""
def __init__(self, option_strings, dest, default=None, **kwargs):
"""Initialize the action."""
if default is None:
default = set()
default = list()
super().__init__(option_strings, dest, default=default, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
"""Add a language to the set."""
dest = getattr(namespace, self.dest)
if '+' in values:
dest.update(lang for lang in values.split('+'))
[dest.append(lang) for lang in values.split('+')]
else:
dest.add(values)
dest.append(values)
def get_parser():
"""Get the main CLI parser."""
parser = ArgumentParser(
prog=_PROGRAM_NAME,
allow_abbrev=True,
@@ -166,7 +183,9 @@ Online documentation is located at:
'--image-dpi',
metavar='DPI',
type=int,
help="For input image instead of PDF, use this DPI instead of file's.",
help="When the input file is an image, not a PDF, use this DPI instead "
"of the DPI claimed by the input file. If the input does not claim a "
"sensible DPI, this option will be required.",
)
parser.add_argument(
'--output-type',
@@ -175,8 +194,8 @@ Online documentation is located at:
help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for "
"long term archiving (default, recommended) but may not suitable "
"for users who want their file altered as little as possible. 'pdfa' "
"also has problems with full Unicode text. 'pdf' attempts to "
"preserve file contents as much as possible. 'pdf-a1' creates a "
"also has problems with full Unicode text. 'pdf' minimizes changes "
"to the input file. 'pdf-a1' creates a "
"PDF/A1-b file. 'pdf-a2' is equivalent to 'pdfa'. 'pdf-a3' creates a "
"PDF/A3-b file. 'none' will produce no output, which may be helpful if "
"only the --sidecar is desired.",
@@ -348,6 +367,13 @@ Online documentation is located at:
help="Skip OCR on pages larger than the specified amount of megapixels, "
"but include skipped pages in final output",
)
ocrsettings.add_argument(
'--invalidate-digital-signatures',
action='store_true',
help="Normally, OCRmyPDF will refuse to OCR a PDF that has a digital "
"signature. This option allows OCR to proceed, but the digital signature "
"will be invalidated.",
)
advanced = parser.add_argument_group(
"Advanced", "Advanced options to control OCRmyPDF"
@@ -365,7 +391,7 @@ Online documentation is located at:
action='store',
type=numeric(float, 0),
metavar='MPixels',
help="Set maximum number of pixels to unpack before treating an image as a "
help="Set maximum number of megapixels to unpack before treating an image as a "
"decompression bomb",
default=250.0,
)
@@ -378,25 +404,12 @@ Online documentation is located at:
)
advanced.add_argument(
'--rotate-pages-threshold',
default=14.0,
default=DEFAULT_ROTATE_PAGES_THRESHOLD,
type=numeric(float, 0, 1000),
metavar='CONFIDENCE',
help="Only rotate pages when confidence is above this value (arbitrary "
"units reported by tesseract)",
)
advanced.add_argument(
'--pdfa-image-compression',
choices=['auto', 'jpeg', 'lossless'],
default='auto',
help="Specify how to compress images in the output PDF/A. 'auto' lets "
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
"JPEG compression. 'lossless' uses PNG-style lossless compression "
"for all images. Monochrome images are always compressed using a "
"lossless codec. Compression settings "
"are applied to all pages, including those for which OCR was "
"skipped. Not supported for --output-type=pdf ; that setting "
"preserves the original compression of all images.",
)
advanced.add_argument(
'--fast-web-view',
type=numeric(float, 0),
@@ -409,6 +422,14 @@ Online documentation is located at:
"which do not benefit. If the threshold is 0 it will be apply to all files. "
"Set the threshold very high to disable.",
)
advanced.add_argument(
'--continue-on-soft-render-error',
action='store_true',
help="Continue processing pages after a recoverable PDF rendering error. "
"A recoverable error is one that does not prevent the page from being "
"rendered, but may result in visual differences compared to the input "
"file. Missing fonts are a typical source of these errors.",
)
advanced.add_argument(
'--plugin',
dest='plugins',
Binary file not shown.
+40 -22
View File
@@ -35,6 +35,7 @@ class ExitCodeException(Exception):
message = ""
def __str__(self):
"""Return a string representation of the exception."""
super_msg = super().__str__() # Don't do str(super())
if self.message:
return self.message.format(super_msg)
@@ -47,26 +48,6 @@ class BadArgsError(ExitCodeException):
exit_code = ExitCode.bad_args
class PdfMergeFailedError(ExitCodeException): # deprecated
"""An intermediate PDF can't be merged.
No longer in use.
"""
exit_code = ExitCode.input_file
message = dedent(
'''\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is malformed and
ocrmypdf cannot correct the problem on its own.
Try using
ocrmypdf --pdf-renderer sandwich [..other args..]
'''
)
class MissingDependencyError(ExitCodeException):
"""A third-party dependency is missing."""
@@ -114,7 +95,7 @@ class EncryptedPdfError(ExitCodeException):
exit_code = ExitCode.encrypted_pdf
message = dedent(
'''\
"""\
Input PDF is encrypted. The encryption must be removed to
perform OCR.
@@ -123,7 +104,7 @@ class EncryptedPdfError(ExitCodeException):
You can remove the encryption using
qpdf --decrypt [--password=[password]] infilename
'''
"""
)
@@ -132,3 +113,40 @@ class TesseractConfigError(ExitCodeException):
exit_code = ExitCode.invalid_config
message = "Error occurred while parsing a Tesseract configuration file"
class DigitalSignatureError(InputFileError):
"""PDF has a digital signature."""
message = dedent(
"""\
Input PDF has a digital signature. OCR would alter the document,
invalidating the signature.
"""
)
class TaggedPDFError(InputFileError):
"""PDF is tagged."""
message = dedent(
"""\
This PDF is marked as a Tagged PDF. This often indicates
that the PDF was generated from an office document and does
not need OCR. Use --force-ocr, --skip-text or --redo-ocr to
override this error.
"""
)
class ColorConversionNeededError(BadArgsError):
"""PDF needs color conversion."""
message = dedent(
"""\
The input PDF has an unusual color space. Use
--color-conversion-strategy to convert to a common color space
such as RGB, or use --output-type pdf to skip PDF/A conversion
and retain the original color space.
"""
)
+6
View File
@@ -1,3 +1,9 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
#
# SPDX-License-Identifier: MPL-2.0
"""Extra plugins. These are not automatically inserted when ocrmypdf is run.
You can use these plugins by specifying them on the command line, e.g.:
ocrmypdf --plugin ocrmypdf.extra_plugins.semfree ...
"""
+18 -11
View File
@@ -12,7 +12,7 @@ worker communicates only with the main process.
This is not without drawbacks. If the tasks are not "even" in size, which cannot
be guaranteed, some workers may end up with too much work while others are idle.
It is less efficient than the standard implementation, so not th edefault.
It is less efficient than the standard implementation, so not the default.
"""
from __future__ import annotations
@@ -20,12 +20,12 @@ from __future__ import annotations
import logging
import logging.handlers
import signal
from collections.abc import Callable, Iterable, Iterator
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 ocrmypdf import Executor, hookimpl
from ocrmypdf._concurrent import NullProgressBar
@@ -54,6 +54,7 @@ def split_every(n: int, iterable: Iterable) -> Iterator:
def process_sigbus(*args):
"""Handle SIGBUS signal at the worker level."""
raise InputFileError("A worker process lost access to an input file")
@@ -61,20 +62,21 @@ class ConnectionLogHandler(logging.handlers.QueueHandler):
"""Handler used by child processes to forward log messages to parent."""
def __init__(self, conn: Connection) -> None:
"""Initialize the handler."""
# sets the parent's queue to None - parent only touches queue
# in enqueue() which we override
super().__init__(None) # type: ignore
self.conn = conn
def enqueue(self, record):
"""Enqueue a log message."""
self.conn.send(('log', record))
def process_loop(
conn: Connection, user_init: Callable[[], None], loglevel, task, task_args
):
"""Initialize a process pool worker"""
"""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
@@ -91,7 +93,7 @@ def process_loop(
for args in task_args:
try:
result = task(args)
result = task(*args)
except Exception as e: # pylint: disable=broad-except
conn.send((MessageType.exception, e))
break
@@ -111,16 +113,16 @@ class LambdaExecutor(Executor):
*,
use_threads: bool,
max_workers: int,
tqdm_kwargs: dict,
progress_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:
with self.pbar_class(**progress_kwargs) as pbar:
for args in task_arguments:
result = task(args)
result = task(*args)
task_finished(result, pbar)
return
@@ -154,7 +156,7 @@ class LambdaExecutor(Executor):
for process in processes:
process.start()
with self.pbar_class(**tqdm_kwargs) as pbar:
with self.pbar_class(**progress_kwargs) as pbar:
while connections:
for result in wait(connections):
if not isinstance(result, Connection):
@@ -166,8 +168,7 @@ class LambdaExecutor(Executor):
continue
if msg_type == MessageType.result:
if task_finished:
task_finished(msg, pbar)
task_finished(msg, pbar)
elif msg_type == 'log':
record = msg
logger = logging.getLogger(record.name)
@@ -185,14 +186,20 @@ class LambdaExecutor(Executor):
@hookimpl
def get_executor(progressbar_class):
"""Return a LambdaExecutor instance."""
return LambdaExecutor(pbar_class=progressbar_class)
@hookimpl
def get_logging_console():
"""Return a logging.StreamHandler instance."""
return logging.StreamHandler()
@hookimpl
def get_progressbar_class():
"""Return a NullProgressBar instance.
This executor cannot use a progress bar.
"""
return NullProgressBar

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