Compare commits

...
124 Commits
Author SHA1 Message Date
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
222 changed files with 5634 additions and 4568 deletions
+1
View File
@@ -59,6 +59,7 @@ RUN add-apt-repository -y ppa:alex-p/tesseract-ocr-devel
RUN apt-get update && apt-get install -y --no-install-recommends \
ghostscript \
fonts-droid-fallback \
jbig2dec \
img2pdf \
libsm6 libxext6 libxrender-dev \
@@ -0,0 +1,51 @@
name: General issues
description: Installation, packages, dependencies, "nothing works", test suite failures...
title: "[Bug]: "
labels: ["bug", "triage"]
assignees:
- jbarlow83
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- 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 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: dropdown
id: operating-system
attributes:
label: What operating system are you working on?
multiple: true
options:
- Linux
- Windows
- macOS
- BSD
- 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
@@ -0,0 +1,62 @@
name: Problem with specific file
description: Something went wrong while trying to OCR a specific file
title: "[Bug]: "
labels: ["bug", "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.
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
@@ -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.
-23
View File
@@ -1,23 +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. You could also look in ``tests/resources`` and see if any of those files demonstrates your issue.
**Additional context**
Add any other context or screenshots about the feature request here.
-33
View File
@@ -1,33 +0,0 @@
---
name: General issues
about: Installation, packages, dependencies, "nothing works", test suite failures...
title: "[BUG]"
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:
- Platform: x64 or ARM
**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 specific file
about: Something went wrong while trying to OCR a specific file
title: "[BUG]"
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?
+62 -42
View File
@@ -5,7 +5,7 @@ name: Test and deploy
on:
push:
branches:
- master
- main
- ci
- release/*
- feature/*
@@ -22,39 +22,35 @@ jobs:
strategy:
matrix:
include:
- os: ubuntu-20.04
python: "3.8"
- os: ubuntu-20.04
- os: ubuntu-22.04
python: "3.9"
- os: ubuntu-20.04
- os: ubuntu-22.04
python: "3.10"
- os: ubuntu-latest
python: "3.9"
- os: ubuntu-latest
python: "3.10"
- os: ubuntu-latest
- os: ubuntu-22.04
python: "3.11"
# - os: ubuntu-latest
# python: "pypy3.8"
#- os: ubuntu-latest
# python: "pypy3.9"
- os: ubuntu-latest
- os: ubuntu-22.04
python: "3.9"
tesseract5: true
- os: ubuntu-latest
python: "3.12-dev"
tesseract5: true
#- os: ubuntu-latest
# python: "pypy3.9"
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: Setup Python
with:
python-version: ${{ matrix.python }}
cache: "pip"
- name: Install Tesseract 5
if: matrix.tesseract5
@@ -68,6 +64,7 @@ jobs:
curl \
ghostscript \
img2pdf \
libexempi8 \
libffi-dev \
libsm6 libxext6 libxrender-dev \
pngquant \
@@ -79,12 +76,6 @@ jobs:
unpaper \
zlib1g
- 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: |
@@ -122,14 +113,14 @@ jobs:
strategy:
matrix:
os: [macos-latest]
python: ["3.10", "3.11"]
python: ["3.10", "3.11", "3.12-dev"]
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
@@ -146,9 +137,10 @@ jobs:
tesseract
- uses: actions/setup-python@v4
name: Install Python
name: Setup Python
with:
python-version: ${{ matrix.python }}
cache: "pip"
- name: Install Python packages
run: |
@@ -178,21 +170,22 @@ jobs:
strategy:
matrix:
os: [windows-latest]
python: ["3.10", "3.11"]
python: ["3.10", "3.11", "3.12-dev"]
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: Setup Python
with:
python-version: ${{ matrix.python }}
cache: "pip"
- name: Install system packages
run: |
@@ -218,14 +211,15 @@ 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: Setup Python
with:
python-version: "3.7"
python-version: "3.9"
cache: "pip"
- name: Make wheels and sdist
run: |
@@ -242,6 +236,9 @@ 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
@@ -249,11 +246,34 @@ jobs:
name: artifact
path: dist
- uses: pypa/gh-action-pypi-publish@master
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
create_release:
name: Create GitHub release
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
permissions:
# Required to create a release
contents: write
steps:
- uses: actions/download-artifact@v3
with:
user: __token__
password: ${{ secrets.TOKEN_PYPI }}
# repository_url: https://test.pypi.org/legacy/
name: artifact
path: dist
- name: Create Release
id: create-release
uses: shogo82148/actions-create-release@v1
- name: Upload Assets
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ steps.create-release.outputs.upload_url }}
asset_path: |
./dist/*.whl
./dist/*.tar.gz
docker:
name: Build Docker images
@@ -264,9 +284,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 +294,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}"
+1
View File
@@ -27,6 +27,7 @@ venv*/
*.traineddata
/private
/coverage.xml
/issuepdf
# Package building
*.egg-info/
+5 -1
View File
@@ -16,8 +16,12 @@ formats:
- pdf
# Optionally set the version of Python and requirements required to build your docs
build:
os: ubuntu-22.04
tools:
python: "3.9"
python:
version: "3.8"
install:
- method: pip
path: .
+1
View File
@@ -41,6 +41,7 @@ Files:
tests/resources/trivial.pdf
tests/resources/truetype_font_nomapping.pdf
tests/resources/type3_font_nomapping.pdf
misc/screencast/*
Copyright: (C) 2022 James R. Barlow
License: CC-BY-SA-4.0
+3 -1
View File
@@ -42,6 +42,8 @@ ocrmypdf # it's a scriptable command line program
- 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/).
## Motivation
@@ -68,7 +70,7 @@ 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 (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` |
| Conda | ``conda install ocrmypdf`` |
+103 -18
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,48 @@ 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. If you issue
``--tesseract-downsample-large-images``, OCRmyPDF will downsample images
to fit Tesseract limits. (The limits are usually entered 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.)
``--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.
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,12 +192,13 @@ 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
You can override Tesseract's default `control
parameters <https://tesseract-ocr.github.io/tessdoc/tess3/ControlParams.html>`__
with a configuration file.
@@ -234,13 +273,44 @@ 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.
character set). Pull requests to improve the situation are welcome.
Currently, this renderer has the best compatibility with Mozilla's
PDF.js viewer.
This works in all versions of Tesseract.
Rendering and rasterizing options
=================================
.. versionadded:: 14.3.0
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.
Color conversion strategy
=========================
.. 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,16 +369,31 @@ 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
+27 -28
View File
@@ -46,16 +46,6 @@ place, and printing each filename in between runs:
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
This only runs one ``ocrmypdf`` process at a time. This variation uses
``find`` to create a directory list and ``parallel`` to parallelize runs
of ``ocrmypdf``, again updating files in place.
@@ -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
@@ -151,14 +150,14 @@ 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>:/archive \
--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
@@ -170,13 +169,13 @@ original to ``/archive``. The parameters to this image are:
: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>:/archive``", "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
+87
View File
@@ -0,0 +1,87 @@
.. _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.
+1 -1
View File
@@ -65,7 +65,7 @@ master_doc = 'index'
# General information about the project.
project = 'ocrmypdf'
copyright = (
'2022, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
'2023, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
)
author = 'James R. Barlow'
+26 -9
View File
@@ -18,7 +18,7 @@ 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
We use PEP8, ``black`` for code formatting and ``ruff`` for everything else. The
settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull
requests should follow the style guide. One difference we use from "black" style
is that strings shown to the user are always in double quotes (``"``) and strings
@@ -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.
+26 -4
View File
@@ -231,13 +231,20 @@ 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.
Optimize images without performing OCR
--------------------------------------
@@ -261,9 +268,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
@@ -372,3 +380,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 to 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.
+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
+71 -78
View File
@@ -72,8 +72,7 @@ Debian and Ubuntu 20.04 or newer
| |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
@@ -100,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-37| image:: https://repology.org/badge/version-for-repo/fedora_37/ocrmypdf.svg
:alt: Fedora 37
.. |fedora-36| image:: https://repology.org/badge/version-for-repo/fedora_36/ocrmypdf.svg
:alt: Fedora 36
.. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
:alt: Fedora 38
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
@@ -114,10 +113,10 @@ Fedora
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |fedora-35| |fedora-36| |fedora-rawhide| |
| |fedora-37| |fedora-38| |fedora-rawhide| |
+-----------------------------------------------+
Users of Fedora 29 or later may simply
Users of Fedora may simply
.. code-block:: bash
@@ -156,7 +155,7 @@ 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`.
@@ -275,6 +274,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
--------------------
@@ -344,18 +358,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
@@ -374,29 +382,33 @@ Native Windows
You must install the following for Windows:
* Python 3.8 (64-bit) or later
* Tesseract 4.1.1 (64-bit) or later
* Ghostscript 9.50 (64-bit) 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``
* ``winget install -e --id ArtifexSoftware.GhostScript``
(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 mmoment 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``
Chocolatey automatically selects appropriate versions of these applications. Please make sure
you are installing the 64-bit versions.
* ``python3 -m pip install ocrmypdf``
OCRmyPDF will check the Windows Registry and standard locations in your Program Files
for third party software it needs (specifically, Tesseract and Ghostscript). To
@@ -408,12 +420,12 @@ 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 might work, but is not supported.
32-bit Windows is not supported.
Windows Subsystem for Linux
---------------------------
@@ -440,7 +452,7 @@ Cygwin64
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
python38 (or later)
python39 (or later)
python3?-devel
python3?-pip
python3?-lxml
@@ -449,7 +461,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
@@ -460,13 +472,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
@@ -518,21 +523,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 information 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
@@ -551,21 +541,6 @@ try:
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:
.. code-block:: bash
pip install 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.
Requirements for pip and HEAD install
-------------------------------------
@@ -575,13 +550,16 @@ manager. ``pip`` cannot provide them.
The following versions are required:
- Python 3.8 or newer
- Ghostscript 9.50 or newer
- Python 3.9 or newer
- Ghostscript 9.55 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.
@@ -608,7 +586,7 @@ unfortunately, the ``pip install`` command cannot satisfy all of them.
Installing HEAD revision from sources
=====================================
If you have ``git`` and Python 3.8 or newer installed, you can install
If you have ``git`` and Python 3.9 or newer installed, you can install
from source. When the ``pip`` installer runs, it will alert you if
dependencies are missing.
@@ -637,9 +615,9 @@ system-wide:
.. code-block:: bash
git clone -b master https://github.com/ocrmypdf/OCRmyPDF.git
python3 -m venv 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 .
@@ -664,9 +642,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
source .venv/bin/activate
cd OCRmyPDF
pip install -e .[test]
@@ -687,3 +665,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 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.
+4
View File
@@ -59,5 +59,9 @@ To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``.
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.*
+7 -7
View File
@@ -12,7 +12,7 @@ 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
@@ -70,13 +70,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 omitted)
echo L10N="de en" >> /etc/portage/make.conf
# update system to reflect changed USE flags
emerge --update --deep --newuse @world
@@ -101,7 +101,7 @@ derived Docker image as
Windows users
=============
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).
+9 -6
View File
@@ -45,11 +45,6 @@ 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>`_.
jbig2enc
--------
@@ -61,4 +56,12 @@ 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.
+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
======================
+61 -102
View File
@@ -29,13 +29,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 +39,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 +54,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.
+81 -1
View File
@@ -28,10 +28,90 @@ tagged yet.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
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 many older dependencies - see ``pyproject.toml`` for details.
Generally speaking, Ubuntu 22.04 is our baseline system.
- 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 an issue
with pikepdf has been 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 situations where a small portion of an image on a page reports a
high DPI (resolution). Previously, the entire page would be rasterized at the
highest resolution, 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
- 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`
+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"
+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

+31 -68
View File
@@ -1,11 +1,7 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
[build-system]
requires = [
"setuptools >= 61",
"setuptools_scm[toml] >= 7.0.5",
"wheel"
]
requires = ["setuptools >= 61", "setuptools_scm[toml] >= 7.0.5", "wheel"]
build-backend = "setuptools.build_meta"
[project]
@@ -13,23 +9,21 @@ 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.8"
license = { text = "MPL-2.0" }
requires-python = ">=3.9"
dependencies = [
"Pillow>=8.2.0",
"coloredlogs>=14.0",
"Pillow>=9.0.1",
"deprecation>=2.1.0",
"img2pdf>=0.3.0", # pure Python
"img2pdf>=0.4.4",
"packaging>=20",
"pdfminer.six>=20201018",
"pikepdf>=5.0.1",
"pdfminer.six>=20220319",
"pikepdf>=8",
"pluggy>=0.13.0",
"reportlab>=3.5.66",
"tqdm>=4",
"importlib-resources>=5;python_version<'3.9'", # until Python 3.9
"reportlab>=3.6.8",
"rich>=13",
"typing-extensions>=4;python_version<'3.10'",
]
authors = [{name = "James R. Barlow", email="james@purplerock.ca"}]
authors = [{ name = "James R. Barlow", email = "james@purplerock.ca" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
@@ -37,27 +31,17 @@ classifiers = [
"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 :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Operating System :: POSIX :: BSD",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"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",
]
keywords = ["PDF", "OCR", "optical character recognition", "PDF/A", "scanning"]
[project.urls]
Documentation = "https://ocrmypdf.readthedocs.io/"
@@ -68,17 +52,17 @@ Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues"
docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"]
extended_test = ["PyMuPDF==1.19.1"]
test = [
"coverage[toml]>=5",
"hypothesis>=6.0.0",
"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
"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
"types-Pillow",
"types-humanfriendly",
]
watcher = ["watchdog>=1.0.2"]
webservice = ["Flask>=1"]
webservice = ["Flask>=2.0.1"]
[project.scripts]
ocrmypdf = "ocrmypdf.__main__:run"
@@ -93,11 +77,11 @@ namespaces = false
[tool.setuptools_scm]
[tool.distutils.bdist_wheel]
python-tag = "py38"
python-tag = "py39"
[tool.black]
line-length = 88
target-version = ["py38", "py39", "py310", "py311"]
target-version = ["py39", "py310", "py311"]
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
@@ -138,28 +122,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]
@@ -181,20 +144,20 @@ module = [
'pdfminer.*',
'reportlab.*',
'fitz',
'libxmp.utils'
'libxmp.utils',
]
ignore_missing_imports = true
[tool.ruff]
select = [
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"I001", # isort
"UP", # pyupgrade
"UP", # pyupgrade
]
target-version = "py38"
target-version = "py39"
[tool.ruff.isort]
known-first-party = ["ocrmypdf"]
@@ -207,4 +170,4 @@ convention = "google"
"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"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
+5 -5
View File
@@ -5,9 +5,9 @@
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
@@ -18,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/Resource/Init
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55/Resource/Font
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
apps:
@@ -49,7 +49,7 @@ parts:
jbig2enc:
plugin: autotools
source: https://github.com/agl/jbig2enc.git
source-tag: '0.29'
source-tag: "0.29"
build-packages:
- libleptonica-dev
+56 -27
View File
@@ -28,24 +28,50 @@ except AttributeError:
# Pillow 9 shim
Transpose = Image # type: ignore
COLOR_CONVERSION_STRATEGIES = frozenset(
[
'CMYK',
'Gray',
'LeaveColorUnchanged',
'RGB',
'UseDeviceIndependentColor',
]
)
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 __init__(self, logger: logging.Logger):
self.last: logging.LogRecord | None = None
self.count = 0
self.logger = logger
def filter(self, record):
if self.last and record.msg == self.last.msg:
self.count += 1
return False
else:
if self.count >= 1:
rep_msg = f"(previous message repeated {self.count} times)"
self.count = 0 # Avoid infinite recursion
self.logger.log(self.last.levelno, rep_msg)
self.last = record
return True
def version():
return get_version(GS)
log.addFilter(DuplicateFilter(log))
# Ghostscript executable - gswin32c is not supported
GS = 'gswin64c' if os.name == 'nt' else 'gs'
def version() -> Version:
return Version(get_version(GS))
def _gs_error_reported(stream) -> bool:
@@ -63,6 +89,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)
@@ -83,6 +110,7 @@ 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',
'-',
@@ -158,9 +186,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.
@@ -186,13 +216,16 @@ def generate_pdfa(
"-dAutoFilterGrayImages=true",
]
strategy = 'LeaveColorUnchanged'
gs_version = Version(version())
gs_version = version()
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
@@ -202,15 +235,16 @@ 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",
"-",
@@ -241,14 +275,9 @@ 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)
+17 -9
View File
@@ -7,12 +7,14 @@ from __future__ import annotations
from subprocess import PIPE
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:
return Version(get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*'))
def available():
@@ -23,15 +25,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)
@@ -40,11 +44,13 @@ def convert_group(*, cwd, infiles, out_prefix):
def convert_group_mp(args):
return convert_group(cwd=args[0], infiles=args[1], out_prefix=args[2])
return convert_group(
cwd=args[0], infiles=args[1], out_prefix=args[2], threshold=args[3]
)
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()
@@ -52,4 +58,6 @@ def convert_single(*, cwd, infile, outfile):
def convert_single_mp(args):
return convert_single(cwd=args[0], infile=args[1], outfile=args[2])
return convert_single(
cwd=args[0], infile=args[1], outfile=args[2], threshold=args[3]
)
+3 -2
View File
@@ -10,14 +10,15 @@ from io import BytesIO
from pathlib import Path
from subprocess import PIPE
from packaging.version import Version
from PIL import Image
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():
+4 -3
View File
@@ -113,13 +113,13 @@ class TesseractVersion(Version):
)
def version() -> str:
return get_version('tesseract', regex=r'tesseract\s(.+)')
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]:
@@ -233,6 +233,7 @@ 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
+4 -3
View File
@@ -15,13 +15,14 @@ from pathlib import Path
from subprocess import PIPE, STDOUT
from typing import Iterator, Union
from packaging.version import Version
from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.subprocess import get_version, run
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
# https://github.com/Flameeyes/unpaper/blob/main/doc/basic-concepts.md
if sys.version_info >= (3, 10):
@@ -67,8 +68,8 @@ class UnpaperImageTooLargeError(Exception):
super().__init__(self.message)
def version() -> str:
return get_version('unpaper')
def version() -> Version:
return Version(get_version('unpaper'))
SUPPORTED_MODES = {'1', 'L', 'RGB'}
+65 -16
View File
@@ -6,9 +6,18 @@
from __future__ import annotations
import logging
from contextlib import suppress
from tqdm import tqdm
from rich.console import Console
from rich.logging import RichHandler
from rich.progress import (
BarColumn,
MofNCompleteColumn,
Progress,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
)
from rich.table import Column
class PageNumberFilter(logging.Filter):
@@ -23,21 +32,61 @@ class PageNumberFilter(logging.Filter):
return True
class TqdmConsole:
"""Wrapper to log messages in a way that is compatible with tqdm progress bar.
class RichLoggingHandler(RichHandler):
def __init__(self, console: Console, **kwargs):
super().__init__(
console=console, show_level=False, show_time=False, markup=True, **kwargs
)
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
class RichTqdmProgressAdapter:
"""Adapt tqdm API to rich progress bar."""
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 __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.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 flush(self):
with suppress(AttributeError):
self.file.flush()
def __enter__(self):
self.progress.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.progress.refresh()
self.progress.stop()
return False
def update(self, value=None):
advance = self.unit_scale if value is None else value
self.progress.update(self.progress_bar, advance=advance)
+256 -85
View File
@@ -14,7 +14,7 @@ from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path
from shutil import copyfileobj
from typing import Any, BinaryIO, Iterable, Sequence, cast
from typing import Any, BinaryIO, Iterable, Iterator, Sequence, cast
import img2pdf
import pikepdf
@@ -27,6 +27,7 @@ from ocrmypdf._jobcontext import PageContext, PdfContext
from ocrmypdf._version import PROGRAM_NAME
from ocrmypdf._version import __version__ as VERSION
from ocrmypdf.exceptions import (
DigitalSignatureError,
DpiError,
EncryptedPdfError,
InputFileError,
@@ -37,6 +38,7 @@ from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
from ocrmypdf.hocrtransform import HocrTransform
from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
from ocrmypdf.pluginspec import OrientationConfidence
# Remove this workaround when we require Pillow >= 10
try:
@@ -51,6 +53,20 @@ VECTOR_PAGE_DPI = 400
def triage_image_file(input_file: Path, output_file: Path, options) -> None:
"""Triage the input image file.
If the input file is an image, check its resolution and convert it to PDF.
Args:
input_file: The path to the input file.
output_file: The path to the output file.
options: An object containing the options passed to the OCRmyPDF command.
Raises:
UnsupportedImageFormatError: If the input file is not a supported image format.
DpiError: If the input image has no resolution (DPI) in its metadata or if the
resolution is not credible.
"""
log.info("Input file is not a PDF, checking if it is an image...")
try:
im = Image.open(input_file)
@@ -65,34 +81,32 @@ def triage_image_file(input_file: Path, output_file: Path, options) -> None:
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
log.info("Image size: (%d, %d)", *im.size)
log.info("Image resolution: (%d, %d)", *im.info['dpi'])
log.error(
raise DpiError(
"Input file is an image, but the resolution (DPI) is "
"not credible. Estimate the resolution at which the "
"image was scanned and specify it using --image-dpi."
)
raise DpiError()
elif not options.image_dpi:
log.info("Image size: (%d, %d)", *im.size)
log.error(
raise DpiError(
"Input file is an image, but has no resolution (DPI) "
"in its metadata. Estimate the resolution at which "
"image was scanned and specify it using --image-dpi."
)
raise DpiError()
if im.mode in ('RGBA', 'LA'):
log.error(
raise UnsupportedImageFormatError(
"The input image has an alpha channel. Remove the alpha "
"channel first."
)
raise UnsupportedImageFormatError()
if 'iccprofile' not in im.info:
if im.mode == 'RGB':
log.info("Input image has no ICC profile, assuming sRGB")
elif im.mode == 'CMYK':
log.error("Input CMYK image has no ICC profile, not usable")
raise UnsupportedImageFormatError()
raise UnsupportedImageFormatError(
"Input CMYK image has no ICC profile, not usable"
)
try:
log.info("Image seems valid. Try converting to PDF...")
@@ -110,7 +124,6 @@ def triage_image_file(input_file: Path, output_file: Path, options) -> None:
)
log.info("Successfully converted to PDF, processing...")
except img2pdf.ImageOpenError as e:
log.error(e)
raise UnsupportedImageFormatError() from e
@@ -180,18 +193,21 @@ def validate_pdfinfo_options(context: PdfContext) -> None:
options = context.options
if pdfinfo.needs_rendering:
log.error(
raise InputFileError(
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
)
raise InputFileError()
if pdfinfo.has_signature:
if options.invalidate_digital_signatures:
log.warning("All digital signatures will be invalidated")
else:
raise DigitalSignatureError()
if pdfinfo.has_acroform:
if options.redo_ocr:
log.error(
raise InputFileError(
"This PDF has a user fillable form. --redo-ocr is not "
"currently possible on such files."
)
raise InputFileError()
else:
log.warning(
"This PDF has a fillable form. "
@@ -208,28 +224,23 @@ def validate_pdfinfo_options(context: PdfContext) -> None:
def _vector_page_dpi(pageinfo: PageInfo) -> int:
"""Get a DPI to use for vector pages, if the page has vector content."""
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0
def get_page_dpi(pageinfo: PageInfo, options) -> Resolution:
"""Get the DPI when nonsquare DPI is tolerable."""
xres = max(
pageinfo.dpi.x or VECTOR_PAGE_DPI,
options.oversample or 0.0,
_vector_page_dpi(pageinfo),
)
yres = max(
pageinfo.dpi.y or VECTOR_PAGE_DPI,
options.oversample or 0,
_vector_page_dpi(pageinfo),
)
return Resolution(float(xres), float(yres))
def get_page_square_dpi(
page_context: PageContext, image_dpi: Resolution | None = None
) -> Resolution:
"""Get the DPI when we require xres == yres, scaled to physical units.
def get_page_square_dpi(pageinfo: PageInfo, options) -> Resolution:
"""Get the DPI when we require xres == yres, scaled to physical units."""
xres = pageinfo.dpi.x or 0.0
yres = pageinfo.dpi.y or 0.0
Page DPI includes UserUnit scaling.
"""
pageinfo = page_context.pageinfo
options = page_context.options
if not image_dpi:
image_dpi = pageinfo.dpi
xres = image_dpi.x or 0.0
yres = image_dpi.y or 0.0
userunit = float(pageinfo.userunit) or 1.0
units = float(
max(
@@ -242,12 +253,23 @@ def get_page_square_dpi(pageinfo: PageInfo, options) -> Resolution:
return Resolution(units, units)
def get_canvas_square_dpi(pageinfo: PageInfo, options) -> Resolution:
"""Get the DPI when we require xres == yres, in Postscript units."""
def get_canvas_square_dpi(
page_context: PageContext, image_dpi: Resolution | None = None
) -> Resolution:
"""Get the DPI when we require xres == yres, in Postscript units.
Canvas DPI is independent of PDF UserUnit scaling, which is
used to describe situations where the PDF user space is not 1:1 with
the physical units of the page.
"""
pageinfo = page_context.pageinfo
options = page_context.options
if not image_dpi:
image_dpi = pageinfo.dpi
units = float(
max(
(pageinfo.dpi.x) or VECTOR_PAGE_DPI,
(pageinfo.dpi.y) or VECTOR_PAGE_DPI,
image_dpi.x or VECTOR_PAGE_DPI,
image_dpi.y or VECTOR_PAGE_DPI,
_vector_page_dpi(pageinfo),
options.oversample or 0.0,
)
@@ -256,6 +278,7 @@ def get_canvas_square_dpi(pageinfo: PageInfo, options) -> Resolution:
def is_ocr_required(page_context: PageContext) -> bool:
"""Check if the page needs to be OCR'd."""
pageinfo = page_context.pageinfo
options = page_context.options
@@ -331,9 +354,12 @@ def is_ocr_required(page_context: PageContext) -> bool:
def rasterize_preview(input_file: Path, page_context: PageContext) -> Path:
"""Generate a lower quality preview image."""
output_file = page_context.get_path('rasterize_preview.jpg')
canvas_dpi = get_canvas_square_dpi(page_context.pageinfo, page_context.options)
page_dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
canvas_dpi = Resolution(300.0, 300.0).take_min(
[get_canvas_square_dpi(page_context)]
)
page_dpi = Resolution(300.0, 300.0).take_min([get_page_square_dpi(page_context)])
page_context.plugin_manager.hook.rasterize_pdf_page(
input_file=input_file,
output_file=output_file,
@@ -343,12 +369,15 @@ def rasterize_preview(input_file: Path, page_context: PageContext) -> Path:
page_dpi=page_dpi,
rotation=0,
filter_vector=False,
stop_on_soft_error=not page_context.options.continue_on_soft_render_error,
)
return output_file
def describe_rotation(page_context: PageContext, orient_conf, correction: int) -> str:
"""Describe the page rotation we are going to perform."""
def describe_rotation(
page_context: PageContext, orient_conf: OrientationConfidence, correction: int
) -> str:
"""Describe the page rotation we are going to perform (or not perform)."""
direction = {0: '', 90: '', 180: '', 270: ''}
turns = {0: ' ', 90: '', 180: '', 270: ''}
@@ -375,7 +404,7 @@ def describe_rotation(page_context: PageContext, orient_conf, correction: int) -
def get_orientation_correction(preview: Path, page_context: PageContext) -> int:
"""Work out orientation correct for each page.
"""Work out orientation correction for each page.
We ask Ghostscript to draw a preview page, which will rasterize with the
current /Rotate applied, and then ask OCR which way the page is
@@ -404,13 +433,58 @@ def get_orientation_correction(preview: Path, page_context: PageContext) -> int:
return 0
def calculate_image_dpi(page_context: PageContext) -> Resolution:
pageinfo = page_context.pageinfo
dpi_profile = pageinfo.page_dpi_profile()
if dpi_profile and dpi_profile.average_to_max_dpi_ratio < 0.8:
image_dpi = Resolution(dpi_profile.weighted_dpi, dpi_profile.weighted_dpi)
else:
image_dpi = pageinfo.dpi
return image_dpi
def calculate_raster_dpi(page_context: PageContext):
"""Calculate the DPI for rasterization."""
# Produce the page image with square resolution or else deskew and OCR
# will not work properly.
image_dpi = calculate_image_dpi(page_context)
dpi_profile = page_context.pageinfo.page_dpi_profile()
canvas_dpi = get_canvas_square_dpi(page_context, image_dpi)
page_dpi = get_page_square_dpi(page_context, image_dpi)
if dpi_profile and dpi_profile.average_to_max_dpi_ratio < 0.8:
log.warning(
"Weight average image DPI is %0.1f, max DPI is %0.1f. "
"The discrepancy may indicate a high detail region on this page, "
"but could also indicate a problem with the input PDF file. "
"Page image will be rendered at %0.1f DPI.",
dpi_profile.weighted_dpi,
dpi_profile.max_dpi,
canvas_dpi.to_scalar(),
)
return canvas_dpi, page_dpi
def rasterize(
input_file: Path,
page_context: PageContext,
correction: int = 0,
output_tag: str = '',
remove_vectors=None,
):
remove_vectors: bool | None = None,
) -> Path:
"""Rasterize a PDF page to a PNG image.
Args:
input_file: The input PDF file path.
page_context: The page context object.
correction: The orientation correction angle. Defaults to 0.
output_tag: The output tag. Defaults to ''.
remove_vectors: Whether to remove vectors. Defaults to None, which means
the value from the page context options will be used. If the value
is True or False, it will override the page context options.
Returns:
Path: The output PNG file path.
"""
colorspaces = ['pngmono', 'pnggray', 'png256', 'png16m']
device_idx = 0
@@ -441,10 +515,7 @@ def rasterize(
log.debug(f"Rasterize with {device}, rotation {correction}")
# Produce the page image with square resolution or else deskew and OCR
# will not work properly.
canvas_dpi = get_canvas_square_dpi(pageinfo, page_context.options)
page_dpi = get_page_square_dpi(pageinfo, page_context.options)
canvas_dpi, page_dpi = calculate_raster_dpi(page_context)
page_context.plugin_manager.hook.rasterize_pdf_page(
input_file=input_file,
@@ -455,6 +526,7 @@ def rasterize(
pageno=pageinfo.pageno + 1,
rotation=correction,
filter_vector=remove_vectors,
stop_on_soft_error=not page_context.options.continue_on_soft_render_error,
)
return output_file
@@ -470,8 +542,17 @@ def preprocess_remove_background(input_file: Path, page_context: PageContext) ->
def preprocess_deskew(input_file: Path, page_context: PageContext) -> Path:
"""Deskews the input image using the OCR engine and saves the output to a file.
Args:
input_file: The input image file to deskew.
page_context: The context of the page being processed.
Returns:
Path: The path to the deskewed image file.
"""
output_file = page_context.get_path('pp_deskew.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
ocr_engine = page_context.plugin_manager.hook.get_ocr_engine()
deskew_angle_degrees = ocr_engine.get_deskew(input_file, page_context.options)
@@ -491,11 +572,11 @@ def preprocess_deskew(input_file: Path, page_context: PageContext) -> Path:
def preprocess_clean(input_file: Path, page_context: PageContext) -> Path:
output_file = page_context.get_path('pp_clean.png')
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
return unpaper.clean(
input_file,
output_file,
dpi=dpi.x,
dpi=dpi.to_scalar(),
unpaper_args=page_context.options.unpaper_args,
)
@@ -565,7 +646,16 @@ def ocr_engine_hocr(input_file: Path, page_context: PageContext) -> tuple[Path,
def should_visible_page_image_use_jpg(pageinfo: PageInfo) -> bool:
# If all images were JPEGs originally, produce a JPEG as output
"""Determines whether the visible page image should be saved as a JPEG.
If all images were JPEGs originally, permit a JPEG as output.
Args:
pageinfo: The PageInfo object containing information about the page.
Returns:
A boolean indicating whether the visible page image should be saved as a JPEG.
"""
return bool(pageinfo.images) and all(
im.enc == Encoding.jpeg for im in pageinfo.images
)
@@ -583,7 +673,7 @@ def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
dpi = Resolution(*im.info['dpi'])
else:
# Fallback to page-implied DPI
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
# Pillow requires integer DPI
im.save(output_file, format='JPEG', dpi=dpi.to_int())
@@ -626,7 +716,7 @@ def create_pdf_page_from_image(
def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
options = page_context.options
output_file = page_context.get_path('ocr_hocr.pdf')
dpi = get_page_square_dpi(page_context.pageinfo, options)
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
debug_mode = options.pdf_renderer == 'hocrdebug'
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.x) # square
@@ -689,6 +779,14 @@ def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
def generate_postscript_stub(context: PdfContext) -> Path:
"""Generates a PostScript file stub for the given PDF context.
Args:
context: The PDF context to generate the PostScript file stub for.
Returns:
Path: The path to the generated PostScript file stub.
"""
output_file = context.get_path('pdfa.ps')
generate_pdfa_ps(output_file)
return output_file
@@ -705,21 +803,8 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -
# NULs in DocumentInfo seem to be common since older Acrobats included them.
# pikepdf can deal with this, but we make the world a better place by
# stamping them out as soon as possible.
modified = False
with pikepdf.open(input_pdf) as pdf_file:
try:
len(pdf_file.docinfo)
except TypeError:
log.error(
"File contains a malformed DocumentInfo block - continuing anyway"
)
else:
if pdf_file.docinfo:
for k, v in pdf_file.docinfo.items():
if b'\x00' in bytes(v):
pdf_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
modified = True
if modified:
if _repair_docinfo_nuls(pdf_file):
pdf_file.save(fix_docinfo_file)
else:
safe_symlink(input_pdf, fix_docinfo_file)
@@ -729,18 +814,38 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -
pdf_pages=[fix_docinfo_file],
pdfmark=input_ps_stub,
output_file=output_file,
compression=options.pdfa_image_compression,
context=context,
pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3
progressbar_class=(
context.plugin_manager.hook.get_progressbar_class()
if options.progress_bar
else None
),
stop_on_soft_error=not options.continue_on_soft_render_error,
)
return output_file
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, pikepdf.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:
filesize = os.stat(working_file).st_size
if filesize > (context.options.fast_web_view * 1_000_000):
@@ -789,24 +894,45 @@ def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
docinfo = get_docinfo(original, context)
with pdf.open_metadata() as meta:
meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
with pdf.open_metadata() as meta_pdf:
meta_pdf.load_from_docinfo(
docinfo, delete_missing=False, raise_failure=False
)
# If xmp:CreateDate is missing, set it to the modify date to
# match Ghostscript, for consistency
if 'xmp:CreateDate' not in meta:
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
# ensure consistency with Ghostscript.
if 'xmp:CreateDate' not in meta_pdf:
meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '')
with original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original:
if meta.get('dc:title') == 'Untitled':
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['dc:title']
missing = set(meta_original.keys()) - set(meta.keys())
report_on_metadata(missing)
del meta_pdf['dc:title']
# 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.
for meta in [meta_pdf, meta_original]:
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
meta_missing = set(meta_original.keys()) - set(meta_pdf.keys())
report_on_metadata(meta_missing)
optimizing = context.plugin_manager.hook.is_optimization_enabled(
context=context
@@ -822,6 +948,29 @@ def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
return output_file
def _file_size_ratio(
input_file: Path, output_file: Path
) -> tuple[float | None, float | None]:
"""Calculate ratio of input to output file sizes and percentage savings.
Args:
input_file (Path): The path to the input file.
output_file (Path): The path to the output file.
Returns:
tuple[float | None, float | None]: A tuple containing the file size
ratio and the percentage savings achieved by the output file size
compared to the input file size.
"""
input_size = input_file.stat().st_size
output_size = output_file.stat().st_size
if output_size == 0:
return None, None
ratio = input_size / output_size
savings = 1 - output_size / input_size
return ratio, savings
def optimize_pdf(
input_file: Path, context: PdfContext, executor: Executor
) -> tuple[Path, Sequence[str]]:
@@ -834,17 +983,29 @@ def optimize_pdf(
linearize=should_linearize(input_file, context),
)
input_size = input_file.stat().st_size
output_size = output_file.stat().st_size
if output_size > 0:
ratio = input_size / output_size
savings = 1 - output_size / input_size
log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
ratio, savings = _file_size_ratio(input_file, output_file)
if ratio:
log.info(f"Image optimization ratio: {ratio:.2f} savings: {(savings):.1%}")
ratio, savings = _file_size_ratio(context.origin, output_file)
if ratio:
log.info(f"Total file size ratio: {ratio:.2f} savings: {(savings):.1%}")
return output_pdf, messages
def enumerate_compress_ranges(iterable):
def enumerate_compress_ranges(
iterable: Iterable,
) -> Iterator[tuple[tuple[int, int], Any]]:
"""Enumerate the ranges of non-empty elements in an iterable.
Compresses consecutive ranges of length 1 into single elements.
Args:
iterable: An iterable of elements to enumerate.
Yields:
A tuple containing a range of indices and the corresponding element.
If the element is None, the range represents a skipped range of indices.
"""
skipped_from, index = None, None
for index, txt_file in enumerate(iterable):
index += 1
@@ -888,6 +1049,16 @@ def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Pat
def copy_final(
input_file: Path, output_file: str | Path | BinaryIO, _context: PdfContext
) -> None:
"""Copy the final temporary file to the output destination.
Args:
input_file (Path): The input file to copy.
output_file (str | Path | BinaryIO): The output file to copy to.
_context (PdfContext): The PDF context.
Returns:
None
"""
log.debug('%s -> %s', input_file, output_file)
with input_file.open('rb') as input_stream:
if output_file == '-':
+3 -1
View File
@@ -100,7 +100,9 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
self.register(module)
def get_plugin_manager(plugins: list[str | Path], builtins=True):
def get_plugin_manager(
plugins: list[str | Path], builtins=True
) -> OcrmypdfPluginManager:
return OcrmypdfPluginManager(
project_name='ocrmypdf',
plugins=plugins,
+14 -21
View File
@@ -43,23 +43,26 @@ 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 = (
@@ -71,15 +74,16 @@ def check_options_languages(options: Namespace, ocr_engine_languages: set[str])
"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-digit ISO 639-2 Code.\n"
"For example, English is 'eng', German is 'deu', and Spanish is 'spa'."
"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"
)
raise MissingDependencyError(msg)
def check_options_output(options: Namespace) -> None:
is_latin = options.languages.issubset(HOCR_OK_LANGS)
is_latin = set(options.languages).issubset(HOCR_OK_LANGS)
if options.pdf_renderer.startswith('hocr') and not is_latin:
log.warning(
@@ -201,16 +205,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):
@@ -237,7 +231,6 @@ def _check_plugin_invariant_options(options: Namespace) -> None:
check_options_sidecar(options)
check_options_preprocessing(options)
check_options_ocr_behavior(options)
check_options_advanced(options)
check_options_pillow(options)
+20 -14
View File
@@ -9,16 +9,16 @@ import logging
import os
import sys
import threading
from argparse import Namespace
from enum import IntEnum
from io import IOBase
from pathlib import Path
from typing import AnyStr, BinaryIO, Iterable, Union
from warnings import warn
import coloredlogs
from humanfriendly.terminal import enable_ansi_support
import pluggy
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
from ocrmypdf._logging import PageNumberFilter
from ocrmypdf._plugin_manager import get_plugin_manager
from ocrmypdf._sync import run_pipeline
from ocrmypdf._validation import check_options
@@ -46,7 +46,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.
@@ -112,14 +112,7 @@ def configure_logging(
else:
fmt = '%(pageno)s%(message)s'
use_colors = progress_bar_friendly
formatter = None
if 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)
@@ -141,8 +134,21 @@ def configure_logging(
def create_options(
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
):
"""Construct an options object from the input/output files and keyword arguments."""
) -> 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 = []
@@ -248,6 +254,7 @@ def ocr( # noqa: ruff: disable=D417
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,
plugins: Iterable[StrPath] | None = None,
plugin_manager=None,
keep_temporary_files: bool | None = None,
@@ -348,7 +355,6 @@ def ocr( # noqa: ruff: disable=D417
__all__ = [
'PageNumberFilter',
'TqdmConsole',
'Verbosity',
'check_options',
'configure_logging',
+14 -16
View File
@@ -16,10 +16,10 @@ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_compl
from contextlib import suppress
from typing import Callable, Iterable, Type, 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, RichTqdmProgressAdapter
from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers
@@ -68,7 +68,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)
@@ -95,15 +95,6 @@ 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,
*,
@@ -142,7 +133,7 @@ class StandardExecutor(Executor):
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 +142,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
@@ -168,13 +159,20 @@ def get_executor(progressbar_class):
return StandardExecutor(pbar_class=progressbar_class)
RICH_CONSOLE = RichConsole(stderr=True)
@hookimpl
def get_progressbar_class():
"""Return the default progress bar class."""
return tqdm
def partial_RichTqdmProgressAdapter(*args, **kwargs):
return RichTqdmProgressAdapter(*args, **kwargs, console=RICH_CONSOLE)
return partial_RichTqdmProgressAdapter
@hookimpl
def get_logging_console():
"""Return the default logging console handler."""
return logging.StreamHandler(stream=TqdmConsole(sys.stderr))
return RichLoggingHandler(console=RICH_CONSOLE)
+51 -4
View File
@@ -13,6 +13,37 @@ 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[str] = 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):
@@ -21,10 +52,10 @@ def check_options(options):
program='gs',
package='ghostscript',
version_checker=ghostscript.version,
need_version='9.50', # Ubuntu 20.04's version
need_version='9.55', # Ubuntu 22.04's version
)
gs_version = ghostscript.version()
if gs_version in ('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 "
@@ -33,6 +64,17 @@ def check_options(options):
if options.output_type == 'pdfa':
options.output_type = 'pdfa-2'
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'"
)
@hookimpl
@@ -45,6 +87,7 @@ 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(
@@ -56,6 +99,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
@@ -65,18 +109,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],
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
+11 -1
View File
@@ -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}',
)
+17 -5
View File
@@ -30,7 +30,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',
@@ -38,7 +38,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',
@@ -75,7 +75,10 @@ def add_options(parser):
metavar='SECONDS',
help=(
"Give up on OCR after the timeout, but copy the preprocessed page "
"into the final output."
"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(
@@ -137,7 +140,7 @@ def check_options(options):
program='tesseract',
package={'linux': 'tesseract-ocr'},
version_checker=tesseract.version,
need_version='4.1.1', # Ubuntu 20.04 version
need_version='4.1.1', # Ubuntu 22.04 version (also 20.04)
version_parser=tesseract.TesseractVersion,
)
@@ -175,6 +178,15 @@ 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:
@@ -200,7 +212,7 @@ class TesseractOcrEngine(OcrEngine):
@staticmethod
def version():
return tesseract.version()
return str(tesseract.version())
@staticmethod
def creator_tag(options):
+26 -18
View File
@@ -15,7 +15,11 @@ 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
@@ -84,16 +88,16 @@ class LanguageSetAction(argparse.Action):
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():
@@ -177,7 +181,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',
@@ -359,6 +365,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"
@@ -395,19 +408,6 @@ Online documentation is located at:
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),
@@ -420,6 +420,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',
+12
View File
@@ -108,6 +108,18 @@ class EncryptedPdfError(ExitCodeException):
)
class DigitalSignatureError(ExitCodeException):
"""PDF has a digital signature."""
exit_code = ExitCode.input_file
message = dedent(
"""\
Input PDF has a digital signature. OCR would alter the document,
invalidating the signature.
"""
)
class TesseractConfigError(ExitCodeException):
"""Tesseract config can't be parsed."""
+1 -1
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
+56 -39
View File
@@ -12,28 +12,30 @@ import shutil
import warnings
from collections.abc import Iterable
from contextlib import suppress
from decimal import Decimal
from io import StringIO
from math import isclose, isfinite
from pathlib import Path
from typing import Any, Generic, Sequence, SupportsFloat, SupportsRound, TypeVar
from statistics import harmonic_mean
from typing import (
Any,
Callable,
Generic,
Sequence,
SupportsFloat,
SupportsRound,
TypeVar,
)
import img2pdf
import pikepdf
from packaging.version import Version
log = logging.getLogger(__name__)
if Version(img2pdf.__version__) < Version('0.4.0'):
IMG2PDF_KWARGS = dict(without_pdfw=True)
elif Version(img2pdf.__version__) < Version('0.4.3'):
IMG2PDF_KWARGS = dict(engine=img2pdf.Engine.pikepdf)
else:
IMG2PDF_KWARGS = dict(
engine=img2pdf.Engine.pikepdf, rotation=img2pdf.Rotation.ifvalid
)
IMG2PDF_KWARGS = dict(engine=img2pdf.Engine.pikepdf, rotation=img2pdf.Rotation.ifvalid)
T = TypeVar('T', bound=SupportsRound[Any])
T = TypeVar('T', float, int, Decimal)
class Resolution(Generic[T]):
@@ -77,21 +79,40 @@ class Resolution(Generic[T]):
@property
def is_finite(self) -> bool:
"""True if both x and y are finite numbers."""
if isinstance(self.x, SupportsFloat) and isinstance(self.y, SupportsFloat):
return isfinite(self.x) and isfinite(self.y)
return True
return isfinite(self.x) and isfinite(self.y)
def to_scalar(self) -> float:
"""Return the harmonic mean of x and y as a 1D approximation.
In most cases, Resolution is 2D, but typically it is "square" (x == y) and
can be approximated as a single number. When not square, the harmonic mean
is used to approximate the 2D resolution as a single number.
"""
return harmonic_mean([float(self.x), float(self.y)])
def _take_minmax(
self, vals: Iterable[Any], yvals: Iterable[Any] | None, cmp: Callable
) -> Resolution:
"""Return a new Resolution object with the maximum resolution of inputs."""
if yvals is not None:
return Resolution(cmp(self.x, *vals), cmp(self.y, *yvals))
cmp_x, cmp_y = self.x, self.y
for x, y in vals:
cmp_x = cmp(x, cmp_x)
cmp_y = cmp(y, cmp_y)
return Resolution(cmp_x, cmp_y)
def take_max(
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
) -> Resolution:
"""Return a new Resolution object with the maximum resolution of inputs."""
if yvals is not None:
return Resolution(max(self.x, *vals), max(self.y, *yvals))
max_x, max_y = self.x, self.y
for x, y in vals:
max_x = max(x, max_x)
max_y = max(y, max_y)
return Resolution(max_x, max_y)
return self._take_minmax(vals, yvals, max)
def take_min(
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
) -> Resolution:
"""Return a new Resolution object with the minimum resolution of inputs."""
return self._take_minmax(vals, yvals, min)
def flip_axis(self) -> Resolution[T]:
"""Return a new Resolution object with x and y swapped."""
@@ -103,11 +124,11 @@ class Resolution(Generic[T]):
def __str__(self):
"""Return a string representation of the resolution."""
return f"{self.x:f}x{self.y:f}"
return f"{self.x:f}×{self.y:f}"
def __repr__(self): # pragma: no cover
"""Return a repr() of the resolution."""
return f"Resolution({self.x}x{self.y} dpi)"
return f"Resolution({self.x!r}, {self.y!r})"
def __eq__(self, other):
"""Return True if the resolution is equal to another resolution."""
@@ -122,7 +143,7 @@ class NeverRaise(Exception):
"""An exception that is never raised."""
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike) -> None:
"""Create a symbolic link at ``soft_link_name``, which references ``input_file``.
Think of this as copying ``input_file`` to ``soft_link_name`` with less overhead.
@@ -137,7 +158,7 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
# Guard against soft linking to oneself
if input_file == soft_link_name:
log.warning(
"No symbolic link created. You are using the original data directory "
"No symbolic link created. You are using the original data directory "
"as the working directory."
)
return
@@ -163,7 +184,7 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
os.symlink(os.path.abspath(input_file), soft_link_name)
def samefile(file1: os.PathLike, file2: os.PathLike):
def samefile(file1: os.PathLike, file2: os.PathLike) -> bool:
"""Return True if two files are the same file.
Attempts to account for different relative paths to the same file.
@@ -282,12 +303,12 @@ def check_pdf(input_file: Path) -> bool:
return False
def clamp(n, smallest, largest): # mypy doesn't understand types for this
def clamp(n: T, smallest: T, largest: T) -> T:
"""Clamps the value of ``n`` to between ``smallest`` and ``largest``."""
return max(smallest, min(n, largest))
def remove_all_log_handlers(logger):
def remove_all_log_handlers(logger: logging.Logger) -> None:
"""Remove all log handlers, usually used in a child process.
The child process inherits the log handlers from the parent process when
@@ -300,15 +321,11 @@ def remove_all_log_handlers(logger):
handler.close() # To ensure handlers with opened resources are released
def pikepdf_enable_mmap():
def pikepdf_enable_mmap() -> None:
"""Enable pikepdf mmap."""
# try:
# if pikepdf._qpdf.set_access_default_mmap(True):
# log.debug("pikepdf mmap enabled")
# except AttributeError:
# log.debug("pikepdf mmap not available")
# We found a race condition probably related to pybind issue #2252 that can
# cause a crash. For now, disable pikepdf mmap to be on the safe side.
# Fix is not in pybind11 2.6.0
# log.debug("pikepdf mmap disabled")
return
try:
if pikepdf._core.set_access_default_mmap(True):
log.debug("pikepdf mmap enabled")
except AttributeError:
log.debug("pikepdf mmap not available")
log.debug("pikepdf mmap disabled")
+1
View File
@@ -19,6 +19,7 @@ from xml.etree import ElementTree
with warnings.catch_warnings():
# reportlab uses deprecated load_module
# shim can be removed when we require reportlab >= 3.7
warnings.filterwarnings(
'ignore', category=DeprecationWarning, message=r".*load_module.*"
)
+3 -7
View File
@@ -6,7 +6,6 @@
from __future__ import annotations
import logging
from functools import singledispatch
from math import floor, sqrt
from typing import Optional, Tuple
@@ -43,8 +42,7 @@ def bytes_per_pixel(mode: str) -> int:
return 4
@singledispatch
def calculate_downsample(
def _calculate_downsample(
image_size: Tuple[int, int],
bytes_per_pixel: int,
*,
@@ -105,10 +103,8 @@ def calculate_downsample(
return size
@calculate_downsample.register
def _(
def calculate_downsample(
image: Image.Image,
arg: None = None,
*,
max_size: Optional[Tuple[int, int]] = None,
max_pixels: Optional[int] = None,
@@ -126,7 +122,7 @@ def _(
max_bytes: The maximum number of bytes in the image. RGB is counted as 4
bytes; all other modes are counted as 1 byte.
"""
return calculate_downsample(
return _calculate_downsample(
image.size,
bytes_per_pixel(image.mode),
max_size=max_size,
+107 -67
View File
@@ -68,10 +68,10 @@ def jpg_name(root: Path, xref: Xref) -> Path:
def extract_image_filter(
pike: Pdf, root: Path, image: Stream, xref: Xref
pdf: Pdf, root: Path, image: Stream, xref: Xref
) -> tuple[PdfImage, tuple[Name, Object]] | None:
"""Determine if an image is extractable."""
del pike # unused args
del pdf # unused args
del root
if image.Subtype != Name.Image:
@@ -126,12 +126,12 @@ def extract_image_filter(
def extract_image_jbig2(
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
*, pdf: Pdf, root: Path, image: Stream, xref: Xref, options
) -> XrefExt | None:
"""Extract an image, saving it as a JBIG2 file."""
del options # unused arg
result = extract_image_filter(pike, root, image, xref)
result = extract_image_filter(pdf, root, image, xref)
if result is None:
return None
pim, filtdp = result
@@ -168,10 +168,10 @@ def extract_image_jbig2(
def extract_image_generic(
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
*, pdf: Pdf, root: Path, image: Stream, xref: Xref, options
) -> XrefExt | None:
"""Generic image extraction."""
result = extract_image_filter(pike, root, image, xref)
result = extract_image_filter(pdf, root, image, xref)
if result is None:
return None
pim, filtdp = result
@@ -230,8 +230,67 @@ def extract_image_generic(
return None
def _find_image_xrefs_container(
pdf: Pdf,
container: Object,
pageno: int,
include_xrefs: MutableSet[Xref],
exclude_xrefs: MutableSet[Xref],
pageno_for_xref: dict[Xref, int],
depth: int = 0,
):
"""Find all image XRefs or Form XObject and add to the include/exclude sets."""
if depth > 10:
log.warning("Recursion depth exceeded in _find_image_xrefs_page")
return
try:
xobjs = container.Resources.XObject
except AttributeError:
return
for _imname, image in dict(xobjs).items():
if image.objgen[1] != 0:
continue # Ignore images in an incremental PDF
if Name.Subtype in image and image.Subtype == Name.Form:
# Recurse into Form XObjects
log.debug(f"Recursing into Form XObject {_imname} in page {pageno}")
_find_image_xrefs_container(
pdf,
image,
pageno,
include_xrefs,
exclude_xrefs,
pageno_for_xref,
depth + 1,
)
continue
xref = Xref(image.objgen[0])
if Name.SMask in image:
# Ignore soft masks
smask_xref = Xref(image.SMask.objgen[0])
exclude_xrefs.add(smask_xref)
log.debug(f"xref {smask_xref}: skipping image because it is an SMask")
include_xrefs.add(xref)
log.debug(f"xref {xref}: treating as an optimization candidate")
if xref not in pageno_for_xref:
pageno_for_xref[xref] = pageno
def _find_image_xrefs(pdf: Pdf):
include_xrefs: MutableSet[Xref] = set()
exclude_xrefs: MutableSet[Xref] = set()
pageno_for_xref: dict[Xref, int] = {}
for pageno, page in enumerate(pdf.pages):
_find_image_xrefs_container(
pdf, page, pageno, include_xrefs, exclude_xrefs, pageno_for_xref
)
working_xrefs = include_xrefs - exclude_xrefs
return working_xrefs, pageno_for_xref
def extract_images(
pike: Pdf,
pdf: Pdf,
root: Path,
options,
extract_fn: Callable[..., XrefExt | None],
@@ -250,35 +309,13 @@ def extract_images(
it does a tuple should be returned: (xref, ext) where .ext is the file
extension. extract_fn must also extract the file it finds interesting.
"""
include_xrefs: MutableSet[Xref] = set()
exclude_xrefs: MutableSet[Xref] = set()
pageno_for_xref = {}
errors = 0
for pageno, page in enumerate(pike.pages):
try:
xobjs = page.Resources.XObject
except AttributeError:
continue
for _imname, image in dict(xobjs).items():
if image.objgen[1] != 0:
continue # Ignore images in an incremental PDF
xref = Xref(image.objgen[0])
if Name.SMask in image:
# Ignore soft masks
smask_xref = Xref(image.SMask.objgen[0])
exclude_xrefs.add(smask_xref)
log.debug(f"xref {smask_xref}: skipping image because it is an SMask")
include_xrefs.add(xref)
log.debug(f"xref {xref}: treating as an optimization candidate")
if xref not in pageno_for_xref:
pageno_for_xref[xref] = pageno
working_xrefs = include_xrefs - exclude_xrefs
working_xrefs, pageno_for_xref = _find_image_xrefs(pdf)
for xref in working_xrefs:
image = pike.get_object((xref, 0))
image = pdf.get_object((xref, 0))
try:
result = extract_fn(
pike=pike, root=root, image=image, xref=xref, options=options
pdf=pdf, root=root, image=image, xref=xref, options=options
)
except Exception: # pylint: disable=broad-except
log.exception(
@@ -292,12 +329,12 @@ def extract_images(
def extract_images_generic(
pike: Pdf, root: Path, options
pdf: Pdf, root: Path, options
) -> tuple[list[Xref], list[Xref]]:
"""Extract any >=2bpp image we think we can improve."""
jpegs = []
pngs = []
for _, xref_ext in extract_images(pike, root, options, extract_image_generic):
for _, xref_ext in extract_images(pdf, root, options, extract_image_generic):
log.debug('%s', xref_ext)
if xref_ext.ext == '.png':
pngs.append(xref_ext.xref)
@@ -307,10 +344,10 @@ def extract_images_generic(
return jpegs, pngs
def extract_images_jbig2(pike: Pdf, root: Path, options) -> dict[int, list[XrefExt]]:
def extract_images_jbig2(pdf: Pdf, root: Path, options) -> dict[int, list[XrefExt]]:
"""Extract any bitonal image that we think we can improve as JBIG2."""
jbig2_groups = defaultdict(list)
for pageno, xref_ext in extract_images(pike, root, options, extract_image_jbig2):
for pageno, xref_ext in extract_images(pdf, root, options, extract_image_jbig2):
group = pageno // options.jbig2_page_group_size
jbig2_groups[group].append(xref_ext)
@@ -330,6 +367,7 @@ def _produce_jbig2_images(
fspath(root), # =cwd
(img_name(root, xref, ext) for xref, ext in xref_exts), # =infiles
prefix, # =out_prefix
options.jbig2_threshold,
)
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
@@ -342,6 +380,7 @@ def _produce_jbig2_images(
fspath(root),
img_name(root, xref, ext),
root / f'{prefix}.{n:04d}',
options.jbig2_threshold,
)
if options.jbig2_page_group_size > 1:
@@ -366,7 +405,7 @@ def _produce_jbig2_images(
def convert_to_jbig2(
pike: Pdf,
pdf: Pdf,
jbig2_groups: dict[int, list[XrefExt]],
root: Path,
options,
@@ -393,7 +432,7 @@ def convert_to_jbig2(
jbig2_symfile = root / (prefix + '.sym')
if jbig2_symfile.exists():
jbig2_globals_data = jbig2_symfile.read_bytes()
jbig2_globals = Stream(pike, jbig2_globals_data)
jbig2_globals = Stream(pdf, jbig2_globals_data)
jbig2_globals_dict = Dictionary(JBIG2Globals=jbig2_globals)
elif options.jbig2_page_group_size == 1:
jbig2_globals_dict = None
@@ -404,7 +443,7 @@ def convert_to_jbig2(
xref, _ = xref_ext
jbig2_im_file = root / (prefix + f'.{n:04d}')
jbig2_im_data = jbig2_im_file.read_bytes()
im_obj = pike.get_object(xref, 0)
im_obj = pdf.get_object(xref, 0)
im_obj.write(
jbig2_im_data, filter=Name.JBIG2Decode, decode_parms=jbig2_globals_dict
)
@@ -424,7 +463,7 @@ def _optimize_jpeg(args: tuple[Xref, Path, Path, int]) -> tuple[Xref, Path | Non
def transcode_jpegs(
pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor
pdf: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor
) -> None:
"""Optimize JPEGs according to optimization settings."""
@@ -438,7 +477,7 @@ def transcode_jpegs(
xref, opt_jpg = result
if opt_jpg:
compdata = opt_jpg.read_bytes() # JPEG can inserted into PDF as is
im_obj = pike.get_object(xref, 0)
im_obj = pdf.get_object(xref, 0)
im_obj.write(compdata, filter=Name.DCTDecode)
pbar.update()
@@ -458,9 +497,9 @@ def transcode_jpegs(
def _find_deflatable_jpeg(
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
*, pdf: Pdf, root: Path, image: Stream, xref: Xref, options
) -> XrefExt | None:
result = extract_image_filter(pike, root, image, xref)
result = extract_image_filter(pdf, root, image, xref)
if result is None:
return None
_pim, filtdp = result
@@ -472,9 +511,9 @@ def _find_deflatable_jpeg(
def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, bytes]:
pike, lock, xref, complevel = args
pdf, lock, xref, complevel = args
with lock:
xobj = pike.get_object(xref, 0)
xobj = pdf.get_object(xref, 0)
try:
data = xobj.read_raw_bytes()
except PdfError:
@@ -485,7 +524,7 @@ def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, by
return xref, compdata
def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
def deflate_jpegs(pdf: Pdf, root: Path, options, executor: Executor) -> None:
"""Apply FlateDecode to JPEGs.
This is a lossless compression method that is supported by all PDF viewers,
@@ -493,7 +532,7 @@ def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
images.
"""
jpegs = []
for _pageno, xref_ext in extract_images(pike, root, options, _find_deflatable_jpeg):
for _pageno, xref_ext in extract_images(pdf, root, options, _find_deflatable_jpeg):
xref = xref_ext.xref
log.debug(f'xref {xref}: marking this JPEG as deflatable')
jpegs.append(xref)
@@ -505,13 +544,13 @@ def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
def deflate_args() -> Iterator:
for xref in jpegs:
yield pike, lock, xref, complevel
yield pdf, lock, xref, complevel
def finish(result, pbar):
xref, compdata = result
if len(compdata) > 0:
with lock:
xobj = pike.get_object(xref, 0)
xobj = pdf.get_object(xref, 0)
xobj.write(compdata, filter=[Name.FlateDecode, Name.DCTDecode])
pbar.update()
@@ -530,16 +569,16 @@ def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
)
def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
def _transcode_png(pdf: Pdf, filename: Path, xref: Xref) -> bool:
output = filename.with_suffix('.png.pdf')
with output.open('wb') as f:
img2pdf.convert(fspath(filename), outputstream=f, **IMG2PDF_KWARGS)
with Pdf.open(output) as pdf_image:
foreign_image = next(iter(pdf_image.pages[0].images.values()))
local_image = pike.copy_foreign(foreign_image)
local_image = pdf.copy_foreign(foreign_image)
im_obj = pike.get_object(xref, 0)
im_obj = pdf.get_object(xref, 0)
im_obj.write(
local_image.read_raw_bytes(),
filter=local_image.Filter,
@@ -572,7 +611,7 @@ def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
def transcode_pngs(
pike: Pdf,
pdf: Pdf,
images: Sequence[Xref],
image_name_fn: Callable[[Path, Xref], Path],
root: Path,
@@ -613,7 +652,7 @@ def transcode_pngs(
for xref in modified:
filename = png_name(root, xref)
_transcode_png(pike, filename, xref)
_transcode_png(pdf, filename, xref)
DEFAULT_EXECUTOR = SerialExecutor()
@@ -639,24 +678,24 @@ def optimize(
if options.jbig2_page_group_size == 0:
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
with Pdf.open(input_file) as pike:
with Pdf.open(input_file) as pdf:
root = output_file.parent / 'images'
root.mkdir(exist_ok=True)
jpegs, pngs = extract_images_generic(pike, root, options)
transcode_jpegs(pike, jpegs, root, options, executor)
deflate_jpegs(pike, root, options, executor)
jpegs, pngs = extract_images_generic(pdf, root, options)
transcode_jpegs(pdf, jpegs, root, options, executor)
deflate_jpegs(pdf, root, options, executor)
# if options.optimize >= 2:
# Try pngifying the jpegs
# transcode_pngs(pike, jpegs, jpg_name, root, options)
transcode_pngs(pike, pngs, png_name, root, options, executor)
# transcode_pngs(pdf, jpegs, jpg_name, root, options)
transcode_pngs(pdf, pngs, png_name, root, options, executor)
jbig2_groups = extract_images_jbig2(pike, root, options)
convert_to_jbig2(pike, jbig2_groups, root, options, executor)
jbig2_groups = extract_images_jbig2(pdf, root, options)
convert_to_jbig2(pdf, jbig2_groups, root, options, executor)
target_file = output_file.with_suffix('.opt.pdf')
pike.remove_unreferenced_resources()
pike.save(target_file, **save_settings)
pdf.remove_unreferenced_resources()
pdf.save(target_file, **save_settings)
input_size = input_file.stat().st_size
output_size = target_file.stat().st_size
@@ -673,9 +712,9 @@ def optimize(
"optimizations will not be used"
)
# We still need to save the file
with Pdf.open(input_file) as pike:
pike.remove_unreferenced_resources()
pike.save(output_file, **save_settings)
with Pdf.open(input_file) as pdf:
pdf.remove_unreferenced_resources()
pdf.save(output_file, **save_settings)
else:
safe_symlink(target_file, output_file)
@@ -700,6 +739,7 @@ def main(infile, outfile, level, jobs=1):
self.png_quality = png_quality
self.jbig2_page_group_size = 0
self.jbig2_lossy = jb2lossy
self.jbig2_threshold = 0.85
self.quiet = True
self.progress_bar = False
+1 -5
View File
@@ -6,14 +6,10 @@
from __future__ import annotations
import base64
from importlib.resources import files as package_files
from pathlib import Path
from typing import Iterator
try:
from importlib.resources import files as package_files
except ImportError:
from importlib_resources import files as package_files # type: ignore
import pikepdf
SRGB_ICC_PROFILE_NAME = 'sRGB.icc'
+119 -33
View File
@@ -9,6 +9,8 @@ from __future__ import annotations
import atexit
import logging
import re
import statistics
import sys
from collections import defaultdict
from contextlib import ExitStack
from decimal import Decimal
@@ -21,7 +23,9 @@ from typing import Container, Iterable, Iterator, Mapping, NamedTuple, Sequence,
from warnings import warn
from pikepdf import (
Name,
Object,
Page,
Pdf,
PdfImage,
PdfInlineImage,
@@ -418,12 +422,12 @@ class ImageInfo:
return self._type
@property
def width(self):
def width(self) -> int:
"""Width of the image in pixels."""
return self._width
@property
def height(self):
def height(self) -> int:
"""Height of the image in pixels."""
return self._height
@@ -456,17 +460,24 @@ class ImageInfo:
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
@property
def dpi(self):
def dpi(self) -> Resolution:
"""Dots per inch of the image.
Calculated based on where and how the image is drawn in the PDF.
"""
return _get_dpi(self._shorthand, (self._width, self._height))
@property
def printed_area(self) -> float:
"""Physical area of the image in square inches."""
if not self.renderable:
return 0.0
return float(self.width * self.dpi.x * self.height * self.dpi.y)
def __repr__(self):
"""Return a string representation of the image."""
return (
f"<ImageInfo '{self.name}' {self.type_} {self.width}x{self.height} "
f"<ImageInfo '{self.name}' {self.type_} {self.width}×{self.height} "
f"{self.color} {self.comp} {self.bpc} {self.enc} {self.dpi}>"
)
@@ -491,15 +502,15 @@ def _image_xobjects(container) -> Iterator[tuple[Object, str]]:
since the object does not know its own name.
"""
if '/Resources' not in container:
if Name.Resources not in container:
return
resources = container['/Resources']
if '/XObject' not in resources:
resources = container[Name.Resources]
if Name.XObject not in resources:
return
for key, candidate in resources['/XObject'].items():
if candidate is None or '/Subtype' not in candidate:
for key, candidate in resources[Name.XObject].items():
if candidate is None or Name.Subtype not in candidate:
continue
if candidate['/Subtype'] == '/Image':
if candidate[Name.Subtype] == Name.Image:
pdfimage = candidate
yield (pdfimage, key)
@@ -535,15 +546,15 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content
The container may be a page, or a parent Form XObject.
"""
if '/Resources' not in container:
if Name.Resources not in container:
return
resources = container['/Resources']
if '/XObject' not in resources:
resources = container[Name.Resources]
if Name.XObject not in resources:
return
xobjs = resources['/XObject'].as_dict()
xobjs = resources[Name.XObject].as_dict()
for xobj in xobjs:
candidate = xobjs[xobj]
if candidate is None or candidate['/Subtype'] != '/Form':
if candidate is None or candidate[Name.Subtype] != Name.Form:
continue
form_xobject = candidate
@@ -581,16 +592,19 @@ def _process_content_streams(
downsampling.
"""
if container.get('/Type') == '/Page' and '/Contents' in container:
if container.get(Name.Type) == Name.Page and Name.Contents in container:
initial_shorthand = shorthand or UNIT_SQUARE
elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form':
elif (
container.get(Name.Type) == Name.XObject
and container[Name.Subtype] == Name.Form
):
# Set the CTM to the state it was when the "Do" operator was
# encountered that is drawing this instance of the Form XObject
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
# A Form XObject may provide its own matrix to map form space into
# user space. Get this if one exists
form_shorthand = container.get('/Matrix', PdfMatrix.identity())
form_shorthand = container.get(Name.Matrix, PdfMatrix.identity())
form_matrix = PdfMatrix(form_shorthand)
# Concatenate form matrix with CTM to ensure CTM is correct for
@@ -742,12 +756,38 @@ def _pdf_pageinfo_concurrent(
return pages
class PageResolutionProfile(NamedTuple):
"""Information about the resolutions of a page."""
weighted_dpi: float
"""The weighted average DPI of the page, weighted by the area of each image."""
max_dpi: float
"""The maximum DPI of an image on the page."""
average_to_max_dpi_ratio: float
"""The average DPI of the page divided by the maximum DPI of the page.
This indicates the intensity of the resolution variation on the page.
If the average is 1.0 or close to 1.0, has all of its content at a uniform
resolution. If the average is much lower than 1.0, some content is at a
higher resolution than the rest of the page.
"""
area_ratio: float
"""The maximum-DPI area of the page divided by the total drawn area.
This indicates the prevalence of high-resolution content on the page.
"""
class PageInfo:
"""Information about type of contents on each page in a PDF."""
_has_text: bool | None
_has_vector: bool | None
_images: list[ImageInfo]
_images: list[ImageInfo] = []
def __init__(
self,
@@ -771,7 +811,7 @@ class PageInfo:
check_pages: Container[int],
detailed_analysis: bool,
):
page = pdf.pages[pageno]
page: Page = pdf.pages[pageno]
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
@@ -779,7 +819,7 @@ class PageInfo:
check_this_page = pageno in check_pages
if check_this_page and detailed_analysis:
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
pscript5_mode = str(pdf.docinfo.get(Name.Creator)).startswith('PScript5')
miner = get_page_analysis(infile, pageno, pscript5_mode)
self._textboxes = list(simplify_textboxes(miner, get_text_boxes))
bboxes = (box.bbox for box in self._textboxes)
@@ -789,17 +829,13 @@ class PageInfo:
self._textboxes = []
self._has_text = None # i.e. "no information"
userunit = page.get('/UserUnit', Decimal(1.0))
userunit = page.get(Name.UserUnit, Decimal(1.0))
if not isinstance(userunit, Decimal):
userunit = Decimal(userunit)
self._userunit = userunit
self._width_inches = width_pt * userunit / Decimal(72.0)
self._height_inches = height_pt * userunit / Decimal(72.0)
try:
self._rotate = int(page['/Rotate'])
except KeyError:
self._rotate = 0
self._rotate = int(getattr(page.obj, 'Rotate', 0))
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
@@ -938,6 +974,47 @@ class PageInfo:
else:
return '1.5'
def page_dpi_profile(self) -> PageResolutionProfile | None:
"""Return information about the DPIs of the page.
This is useful to detect pages with a small proportion of high-resolution
content that is forcing us to use a high DPI for the whole page. The ratio
is weighted by the area of each image. If images overlap, the overlapped
area counts.
Vector graphics and text are ignored.
Returns None if there is no meaningful DPI for the page.
"""
image_dpis = [
image.dpi.to_scalar() for image in self._images if image.renderable
]
image_areas = [image.printed_area for image in self._images if image.renderable]
total_drawn_area = sum(image_areas)
if total_drawn_area == 0:
return None
weights = [area / total_drawn_area for area in image_areas]
# Calculate harmonic mean of DPIs weighted by area
if sys.version_info >= (3, 10):
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
else:
weighted_dpi = sum(weights) / sum(
weight / dpi for weight, dpi in zip(weights, image_dpis)
)
max_dpi = max(image_dpis)
dpi_average_max_ratio = weighted_dpi / max_dpi
arg_max_dpi = image_dpis.index(max_dpi)
max_area_ratio = image_areas[arg_max_dpi] / total_drawn_area
return PageResolutionProfile(
weighted_dpi,
max_dpi,
dpi_average_max_ratio,
max_area_ratio,
)
def __repr__(self):
"""Return string representation."""
return (
@@ -953,6 +1030,10 @@ DEFAULT_EXECUTOR = SerialExecutor()
class PdfInfo:
"""Get summary information about a PDF."""
_has_acroform: bool = False
_has_signature: bool = False
_needs_rendering: bool = False
def __init__(
self,
infile,
@@ -980,13 +1061,13 @@ class PdfInfo:
check_pages=check_pages,
detailed_analysis=detailed_analysis,
)
self._needs_rendering = pdf.Root.get('/NeedsRendering', False)
self._has_acroform = False
if '/AcroForm' in pdf.Root:
if len(pdf.Root.AcroForm.get('/Fields', [])) > 0:
self._needs_rendering = pdf.Root.get(Name.NeedsRendering, False)
if Name.AcroForm in pdf.Root:
if len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0:
self._has_acroform = True
elif '/XFA' in pdf.Root.AcroForm:
elif Name.XFA in pdf.Root.AcroForm:
self._has_acroform = True
self._has_signature = bool(pdf.Root.AcroForm.get(Name.SigFlags, 0) & 1)
@property
def pages(self) -> Sequence[PageInfo | None]:
@@ -1006,9 +1087,14 @@ class PdfInfo:
@property
def has_acroform(self) -> bool:
"""Return True if any page has an AcroForm."""
"""Return True if the document catalog has an AcroForm."""
return self._has_acroform
@property
def has_signature(self) -> bool:
"""Return True if the document annotations has a digital signature."""
return self._has_signature
@property
def filename(self) -> str | Path:
"""Return filename of PDF."""
+29 -14
View File
@@ -203,6 +203,7 @@ def rasterize_pdf_page(
page_dpi: Resolution | None,
rotation: int | None,
filter_vector: bool,
stop_on_soft_error: bool,
) -> Path:
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
@@ -213,19 +214,26 @@ def rasterize_pdf_page(
Args:
input_file: The PDF to rasterize.
output_file: The desired name of the rasterized image.
raster_device: Type of image to produce at output_file
raster_dpi: Resolution at which to rasterize page
pageno: Page number to rasterize (beginning at page 1)
page_dpi: Resolution, overriding output image DPI
rotation: Cardinal angle, clockwise, to rotate page
filter_vector: If True, remove vector graphics objects
raster_device: Type of image to produce at output_file.
raster_dpi: Resolution in dots per inch at which to rasterize page.
pageno: Page number to rasterize (beginning at page 1).
page_dpi: Resolution, overriding output image DPI.
rotation: Cardinal angle, clockwise, to rotate page.
filter_vector: If True, remove vector graphics objects.
stop_on_soft_error: If there is an "soft error" such that PDF page image
generation can proceed, but may visually differ from the original,
the implementer of this hook should raise a detailed exception. If
``False``, continue processing and report by logging it. If the hook
cannot proceed, it should always raise an exception, regardless of
this setting. One "soft error" would be a missing font that is
required to properly rasterize the PDF.
Returns:
Path: output_file if successful
Note:
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@@ -343,7 +351,6 @@ def filter_pdf_page(page: PageContext, image_filename: Path, output_pdf: Path) -
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@@ -458,10 +465,11 @@ def generate_pdfa(
pdf_pages: list[Path],
pdfmark: Path,
output_file: Path,
compression: str,
context: PdfContext,
pdf_version: str,
pdfa_part: str,
progressbar_class,
stop_on_soft_error: bool,
) -> Path:
"""Generate a PDF/A.
@@ -475,11 +483,7 @@ def generate_pdfa(
pdfmark: A PostScript file intended for Ghostscript with details on
how to perform the PDF/A conversion.
output_file: The name of the desired output file.
compression: One of ``'jpeg'``, ``'lossless'``, ``''``. For ``'jpeg'``,
the PDF/A generator should convert all images to JPEG encoding where
possible. For lossless, all images should be converted to FlateEncode
(lossless PNG). If an empty string, the PDF generator should make its
own decisions about how to encode images.
context: The current context.
pdf_version: The minimum PDF version that the output file should be.
At its own discretion, the PDF/A generator may raise the version,
but should not lower it.
@@ -492,6 +496,12 @@ def generate_pdfa(
and the name of the work units ("page"). Then ``instance.update()``
will be called when a work unit is completed. If ``None``, no
progress information is reported.
stop_on_soft_error: If there is an "soft error" such that PDF/A generation
can proceed and produce a valid PDF/A, but output may be invalid or
may not visually resemble the original, the implementer of this hook
should raise a detailed exception. If ``False``, continue processing
and report by logging it. If the hook cannot proceed, it should always
raise an exception, regardless of this setting.
Returns:
Path: If successful, the hook should return ``output_file``.
@@ -499,6 +509,11 @@ def generate_pdfa(
Note:
This is a :ref:`firstresult hook<firstresult>`.
Note:
Before version 15.0.0, the ``context`` was not provided and ``compression``
was provided instead. Plugins should now read the context object to determine
if compression is requested.
See Also:
https://github.com/tqdm/tqdm
"""
+8 -18
View File
@@ -118,8 +118,6 @@ def run_polling_stderr(
def _fix_process_args(
args: Args, env: OsEnviron | None, kwargs
) -> tuple[Args, OsEnviron, logging.Logger, bool]:
assert 'universal_newlines' not in kwargs, "Use text= instead of universal_newlines"
if not env:
env = os.environ
@@ -294,21 +292,12 @@ def _error_old_version(
_error_trailer(**locals())
def _remove_leading_v(s: str) -> str:
if sys.version_info >= (3, 9):
return s.removeprefix('v')
if s.startswith('v'):
return s[1:]
return s
def check_external_program(
*,
program: str,
package: str,
version_checker: Callable[[], str],
need_version: str,
version_checker: Callable[[], Version],
need_version: str | Version,
required_for: str | None = None,
recommended: bool = False,
version_parser: type[Version] = Version,
@@ -328,6 +317,8 @@ def check_external_program(
version_parser: A class that should be used to parse and compare version
numbers. Used when version numbers do not follow standard conventions.
"""
if not isinstance(need_version, Version):
need_version = version_parser(need_version)
try:
found_version = version_checker()
except (CalledProcessError, FileNotFoundError) as e:
@@ -341,11 +332,10 @@ def check_external_program(
raise
return
found_version = _remove_leading_v(found_version)
need_version = _remove_leading_v(need_version)
if found_version and version_parser(found_version) < version_parser(need_version):
_error_old_version(program, package, need_version, found_version, required_for)
if found_version and found_version < need_version:
_error_old_version(
program, package, str(need_version), str(found_version), required_for
)
if not recommended:
raise MissingDependencyError(program)
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -1,7 +1,5 @@
i a la Waterman
4 ons linzen
3 liter water
@@ -17,11 +15,10 @@ laurier, kruidnagel, kerrie, zout
De linzgen wassen en in-l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten., De helft van
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruidnagel.
Alle uien, kerrie en zgout bij de linzen
voegen, Alles aan de kook brengen, Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gfgaar Zijn is de soep klaar.
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -1,25 +1,11 @@
Tarnose
Bokale oa
Lehuntze
Mugerre
Milafranga Komunikabideak
BAIONA zeiteninsiie —
@@ -27,4 +13,3 @@ BAIONA zeiteninsiie —
7 Trenbideak -----
t\ Basusarri — spmeans:20141004 ae: . _ ~
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -1,2 +1 @@
Covfefe is a perfectly cromulent word.
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -1,7 +1,5 @@
Linzensoep a la Waterman
4 ons linzen
3 liter water
@@ -17,11 +15,10 @@ laurier, kruidnagel, kerrie, zout
De linzgen wassen en in-l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten., De helft van
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruidnagel.
Alle uien, kerrie en zgout bij de linzen
voegen, Alles aan de kook brengen, Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gfgaar Zijn is de soep klaar.
@@ -5,27 +5,21 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
<meta name='ocr-system' content='tesseract 5.3.2' />
<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 "/tmp/ocrmypdf.io.gzrr1v_b/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0'>
<div class='ocr_carea' id='block_1_1' title="bbox 296 96 704 504">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 296 96 704 504">
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336">
<span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'> </span>
</span>
</p>
</div>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.9_hn68nz/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0; scan_res 200 200'>
<div class='ocr_photo' id='block_1_1' title="bbox 296 96 704 504"></div>
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 150 592 841 622">
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 95'>This</span>
<span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 95'>should</span>
<span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 94'>a</span>
<span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 94'>perfect</span>
<span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 61'>circle:</span>
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 150 592 841 622">
<span class='ocr_line' id='line_1_1' title="bbox 150 592 841 622; baseline 0 -5; x_size 30; x_descenders 5; x_ascenders 7">
<span class='ocrx_word' id='word_1_1' title='bbox 150 592 230 617; x_wconf 96'>This</span>
<span class='ocrx_word' id='word_1_2' title='bbox 260 592 384 617; x_wconf 96'>should</span>
<span class='ocrx_word' id='word_1_3' title='bbox 413 592 449 617; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_4' title='bbox 479 599 493 617; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_5' title='bbox 523 592 668 622; x_wconf 95'>perfect</span>
<span class='ocrx_word' id='word_1_6' title='bbox 698 592 841 617; x_wconf 25'>circle.</span>
</span>
</p>
</div>
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -1,4 +1 @@
This should be a perfect circle:
This should be a perfect circle.
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -1,4 +1 @@
This should be a perfect circle:
This should be a perfect circle.
@@ -5,11 +5,11 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
<meta name='ocr-system' content='tesseract 5.3.2' />
<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 "/tmp/ocrmypdf.io.8je4vgpg/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -136,9 +136,9 @@
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
@@ -150,7 +150,7 @@
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
</span>
</p>
</div>
@@ -165,13 +165,13 @@
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
@@ -256,8 +256,8 @@
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
</span>
</p>
@@ -278,8 +278,8 @@
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
@@ -288,7 +288,7 @@
</span>
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
@@ -297,7 +297,7 @@
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
</span>
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you</span>
@@ -620,7 +620,7 @@
</span>
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
</span>
</p>
</div>
@@ -668,8 +668,8 @@
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
</span>
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
@@ -697,7 +697,7 @@
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
@@ -829,9 +829,9 @@
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
@@ -912,12 +912,12 @@
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
@@ -928,12 +928,12 @@
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
</span>
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
@@ -995,7 +995,7 @@
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
@@ -1011,7 +1011,7 @@
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
</span>
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
@@ -1022,8 +1022,8 @@
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
</span>
</p>
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -5,11 +5,11 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
<meta name='ocr-system' content='tesseract 5.3.2' />
<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 "/tmp/ocrmypdf.io.8je4vgpg/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -136,9 +136,9 @@
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
@@ -150,7 +150,7 @@
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
</span>
</p>
</div>
@@ -165,13 +165,13 @@
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
@@ -256,8 +256,8 @@
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
</span>
</p>
@@ -278,8 +278,8 @@
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
@@ -288,7 +288,7 @@
</span>
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
@@ -297,7 +297,7 @@
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
</span>
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you</span>
@@ -620,7 +620,7 @@
</span>
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
</span>
</p>
</div>
@@ -668,8 +668,8 @@
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
</span>
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
@@ -697,7 +697,7 @@
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
@@ -829,9 +829,9 @@
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
@@ -912,12 +912,12 @@
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
@@ -928,12 +928,12 @@
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
</span>
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
@@ -995,7 +995,7 @@
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
@@ -1011,7 +1011,7 @@
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
</span>
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
@@ -1022,8 +1022,8 @@
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
</span>
</p>
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -5,11 +5,11 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
<meta name='ocr-system' content='tesseract 5.3.2' />
<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 "/tmp/ocrmypdf.io.8je4vgpg/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -136,9 +136,9 @@
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
@@ -150,7 +150,7 @@
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
</span>
</p>
</div>
@@ -165,13 +165,13 @@
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
@@ -256,8 +256,8 @@
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
</span>
</p>
@@ -278,8 +278,8 @@
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
@@ -288,7 +288,7 @@
</span>
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
@@ -297,7 +297,7 @@
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
</span>
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you</span>
@@ -620,7 +620,7 @@
</span>
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
</span>
</p>
</div>
@@ -668,8 +668,8 @@
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
</span>
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
@@ -697,7 +697,7 @@
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
@@ -829,9 +829,9 @@
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
@@ -912,12 +912,12 @@
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
@@ -928,12 +928,12 @@
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
</span>
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
@@ -995,7 +995,7 @@
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
@@ -1011,7 +1011,7 @@
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
</span>
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
@@ -1022,8 +1022,8 @@
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
</span>
</p>
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -5,11 +5,11 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
<meta name='ocr-system' content='tesseract 5.3.2' />
<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 "/tmp/ocrmypdf.io.8je4vgpg/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -136,9 +136,9 @@
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
@@ -150,7 +150,7 @@
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
</span>
</p>
</div>
@@ -165,13 +165,13 @@
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
@@ -256,8 +256,8 @@
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
</span>
</p>
@@ -278,8 +278,8 @@
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
@@ -288,7 +288,7 @@
</span>
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
@@ -297,7 +297,7 @@
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
</span>
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you</span>
@@ -620,7 +620,7 @@
</span>
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
</span>
</p>
</div>
@@ -668,8 +668,8 @@
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
</span>
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
@@ -697,7 +697,7 @@
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
@@ -829,9 +829,9 @@
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
@@ -912,12 +912,12 @@
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
@@ -928,12 +928,12 @@
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
</span>
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
@@ -995,7 +995,7 @@
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
@@ -1011,7 +1011,7 @@
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
</span>
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
@@ -1022,8 +1022,8 @@
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
</span>
</p>
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
18720 Oxnard Street, Tarzana, CA 91356
(818) 708-8131 TELEX #298949 LINN UR
@@ -1 +0,0 @@
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica

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