Compare commits

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

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

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

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

Pip will warn about this and proceed,

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

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

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

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

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

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

* Fix cross-references to JBIG2 page

* Fix links for Fedora + Arch + HEAD revision install

Fedora 39 has been released, and the package tracker no longer includes
a release overview for Fedora 37 hence why it was removed here.
2024-03-22 14:38:52 -07:00
James R. Barlow 686481982a Fix naming of hOCR rendered files 2024-03-22 13:27:20 -07:00
53 changed files with 631 additions and 181 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
FROM alpine:3.19 as base
FROM alpine:3.19.1 as base
ENV LANG=C.UTF-8
ENV TZ=UTC
@@ -1,7 +1,7 @@
name: Installation, packaging, dependencies
description: Installation, packages, dependencies, "nothing works", test suite failures...
title: "[Bug]: "
labels: ["bug", "triage"]
labels: ["triage"]
assignees:
- jbarlow83
body:
@@ -24,7 +24,7 @@ body:
- type: dropdown
id: packaging-system
attributes:
label: Where are you installing from?
label: Where are you installing/running from?
multiple: true
options:
- PyPI (pip, poetry, pipx, etc.)
@@ -37,6 +37,11 @@ body:
- source build
validations:
required: true
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
- type: dropdown
id: operating-system
attributes:
@@ -47,6 +52,18 @@ body:
- Windows
- macOS
- BSD
- type: input
id: os_version
attributes:
label: Operating system details and version
- type: checkboxes
attributes:
label: Simple sanity checks
description: Select all that apply
options:
- label: Operating system is currently supported by its vendor (not end of life)
- label: Python version is compatible with OCRmyPDF
- label: This issue is not about a specific input file
- type: textarea
id: logs
attributes:
@@ -1,7 +1,7 @@
name: Problem with specific file
description: Something went wrong while trying to OCR a specific file
title: "[Bug]: "
labels: ["bug", "triage"]
labels: ["triage"]
assignees:
- jbarlow83
body:
@@ -39,7 +39,7 @@ body:
causing the issue. There's really no substitute for a test file.
We understand files may contain personal or sensitive information. Here are some options:
- Try reproducing the issue with a file from the test suite. (See tests/resources)
- Try reproducing the issue with a file from the OCRmyPDF test suite. (See tests/resources)
- Try to create another file in the same way as your private file.
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
+83
View File
@@ -0,0 +1,83 @@
name: Problem with third party app that uses OCRmyPDF
description: |
For PDF generation issues with third party software such as Paperless-ngx that
uses OCRmyPDF to perform OCR or generate PDFs.
title: "[3rdparty]: "
labels: ["triage"]
assignees:
- jbarlow83
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to describe this issue with a particular file
and third party app.
If you are comfortable using OCRmyPDF, please trying to install OCRmyPDF,
run it on your file, and see if it works. It's easier for everyone
if you can confirm that the issue occurs with OCRmyPDF and not with
the third party app.
- type: checkboxes
attributes:
label: Simple sanity checks
description: Select all that apply
options:
- label: This is an issue with an app that uses OCRmyPDF for OCR
- label: I am using a recent version of the third party app
- label: I will include a file that reproduces the issuse
- type: input
id: thirdparty-app-name-version
attributes:
label: Third party app name and version
description: e.g. Paperless-ngx 2.9.0
- type: textarea
id: what-happened
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Please include steps to reproduce.
value: |
1. Import attached file into Paperless-ngx
2. Trigger OCR
3. Check log file
4. ...
render: plain text
- type: textarea
id: files
attributes:
label: Files
description: |
Please attach the input and output files, or any screenshots that may be helpful.
If you cannot provide a test file, we probably won't be able to help with the issue.
PDF is a complex file format, and there may be technical details in the PDF that are
causing the issue. There's really no substitute for a test file.
We understand files may contain personal or sensitive information. Here are some options:
- Try reproducing the issue with a file from the test suite. (See tests/resources)
- Try to create another file in the same way as your private file.
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
omits personal information.
placeholder: |
Drag and drop files here.
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
placeholder: ocrmypdf --version
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
placeholder: Run OCRmyPDF with verbosity `-v1` to get more detailed logging output.
render: plain text
+10 -6
View File
@@ -61,6 +61,7 @@ jobs:
sudo apt-get install -y --no-install-recommends \
curl \
ghostscript \
jbig2dec \
img2pdf \
libexempi8 \
libffi-dev \
@@ -101,16 +102,19 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
test_macos:
name: Test macOS
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64
python: ["3.10", "3.11", "3.12"]
env:
@@ -131,7 +135,6 @@ jobs:
ghostscript \
jbig2enc \
openjpeg \
openssl \
pngquant \
tesseract
@@ -159,6 +162,8 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -202,6 +207,8 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -352,9 +359,6 @@ jobs:
username: jbarlow83
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -366,6 +370,6 @@ jobs:
run: |
docker buildx build \
--push \
--platform linux/amd64 \
--platform linux/amd64,linux/arm64 \
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
--file .docker/Dockerfile.alpine .
+1
View File
@@ -70,6 +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 (MacPorts) | ``port install ocrmypdf`` |
| macOS (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` |
+60 -6
View File
@@ -118,15 +118,16 @@ 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.)
of images it will process. By default,
``--tesseract-downsample-large-images`` is enabled, and OCRmyPDF will
downsample images to fit Tesseract limits. (The limits are usually encountered
only for scanned images of oversized media, such as large maps or blueprints exceeding
110 cm or 43 inches in either dimension, and at high DPI.) This feature can disabled
using ``--no-tesseract-downsample-large-images``.
``--tesseract-downsample-above Npixels`` adjusts the threshold at which images
will be downsampled. By default, only images that exceed any of Tesseract's
internal limits are downsampled.
internal limits are downsampled (32767 pixels on either dimension).
You will also need to set ``--tesseract-timeout`` high enough to allow
for processing.
@@ -227,6 +228,59 @@ then run ocrmypdf as follows (along with any other desired arguments):
Some combinations of control parameters will break Tesseract or break
assumptions that OCRmyPDF makes about Tesseract's output.
Changing page segmentation mode
-------------------------------
The directive ``--tesseract-pagesegmode Nmode`` forwards the desired page segmentation
mode to Tesseract OCR. The default is 3.
Page segmentation can improve OCR results when you know that a PDF ought to be
analyzed a particular way, such as PDFs whose pages contain only a single line of
text. For the vast majority of users, changing the page segmentation mode will only
make things worse.
As of June 2024, the Tesseract page segmentation modes are:
+-----+----------------------------------------------------------------------------------+
| ID | Description |
+=====+==================================================================================+
| 0 | Orientation and script detection (OSD) only. |
+-----+----------------------------------------------------------------------------------+
| 1 | Automatic page segmentation with OSD. |
+-----+----------------------------------------------------------------------------------+
| 2 | Automatic page segmentation, but no OSD, or OCR. (not implemented) |
+-----+----------------------------------------------------------------------------------+
| 3 | Fully automatic page segmentation, but no OSD. (Default) |
+-----+----------------------------------------------------------------------------------+
| 4 | Assume a single column of text of variable sizes. |
+-----+----------------------------------------------------------------------------------+
| 5 | Assume a single uniform block of vertically aligned text. |
+-----+----------------------------------------------------------------------------------+
| 6 | Assume a single uniform block of text. |
+-----+----------------------------------------------------------------------------------+
| 7 | Treat the image as a single text line. |
+-----+----------------------------------------------------------------------------------+
| 8 | Treat the image as a single word. |
+-----+----------------------------------------------------------------------------------+
| 9 | Treat the image as a single word in a circle. |
+-----+----------------------------------------------------------------------------------+
| 10 | Treat the image as a single character. |
+-----+----------------------------------------------------------------------------------+
| 11 | Sparse text. Find as much text as possible in no particular order. |
+-----+----------------------------------------------------------------------------------+
| 12 | Sparse text with OSD. |
+-----+----------------------------------------------------------------------------------+
| 13 | Raw line. Treat the image as a single text line, bypassing hacks that are |
| | Tesseract-specific. |
+-----+----------------------------------------------------------------------------------+
Modes 0, 1, 2, and 12 (all of those that enable orientation and script detection)
are not compatible with OCRmyPDF, which performs OSD in a separate step from OCR.
Their use may interfere with ``--rotate-pages`` and other features.
It is currently not possible to use advanced Tesseract OCR features, such as creating
OCR information, when using Tesseract through OCRmyPDF.
Changing the PDF renderer
=========================
+2 -2
View File
@@ -42,7 +42,7 @@ execute the image:
- Architecture
- Description
* - ``jbarlow83/ocrmypdf-alpine``
- x86_64 only
- x86_64 and arm64
- Recommended image, based on Alpine Linux.
* - ``jbarlow83/ocrmypdf-ubuntu``
- x86_64 and arm64
@@ -81,7 +81,7 @@ To start a Docker container (instance of the image):
.. code-block:: bash
docker tag jbarlow83/ocrmypdf ocrmypdf
docker tag jbarlow83/ocrmypdf-alpine ocrmypdf
docker run --rm -i ocrmypdf (... all other arguments here...) - -
For convenience, create a shell alias to hide the Docker command. It is
+31 -13
View File
@@ -23,7 +23,9 @@ These platforms have one-liner installs:
+-------------------------------+-----------------------------------------+
| Fedora | ``dnf install ocrmypdf tesseract-osd`` |
+-------------------------------+-----------------------------------------+
| macOS | ``brew install ocrmypdf`` |
| macOS (Homebrew) | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| macOS (MacPorts) | ``port install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| LinuxBrew | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
@@ -99,12 +101,12 @@ For full details on version availability for your platform, check the
Fedora
------
.. |fedora-37| image:: https://repology.org/badge/version-for-repo/fedora_37/ocrmypdf.svg
:alt: Fedora 37
.. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
:alt: Fedora 38
.. |fedora-39| image:: https://repology.org/badge/version-for-repo/fedora_39/ocrmypdf.svg
:alt: Fedora 39
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
@@ -113,7 +115,7 @@ Fedora
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |fedora-37| |fedora-38| |fedora-rawhide| |
| |fedora-38| |fedora-39| |fedora-rawhide| |
+-----------------------------------------------+
Users of Fedora may simply
@@ -123,7 +125,7 @@ Users of Fedora may simply
dnf install ocrmypdf tesseract-osd
For full details on version availability, check the `Fedora Package
Tracker <https://apps.fedoraproject.org/packages/ocrmypdf>`__.
Tracker <https://packages.fedoraproject.org/pkgs/ocrmypdf/ocrmypdf/>`__.
If the version available for your platform is out of date, you could opt
to install the latest version from source. See `Installing HEAD revision
@@ -135,7 +137,7 @@ from sources <#installing-head-revision-from-sources>`__.
issues. OCRmyPDF works fine without it but will produce larger output
files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later
will automatically detect it on the ``PATH``. To add JBIG2 encoding,
see `Installing the JBIG2 encoder <jbig2>`__.
see :ref:`Installing the JBIG2 encoder <jbig2>`.
.. _ubuntu-lts-latest:
@@ -160,7 +162,7 @@ and build ocrmypdf in virtual environment:
python3.11 -m venv .venv
To add JBIG2 encoding, see `Installing the JBIG2 encoder <jbig2>`__.
To add JBIG2 encoding, see :ref:`Installing the JBIG2 encoder <jbig2>`.
Note Fedora packages for language data haven't been branched for RHEL/EPEL, but you can get traineddata files directly from `tesseract
<https://github.com/tesseract-ocr/tessdata/>`__ and place them in ``/usr/share/tesseract/tessdata``.
@@ -217,12 +219,12 @@ you are using a VM image, such as `the official Vagrant image
be completed for you.
Next you should install the `base-devel package group
<https://www.archlinux.org/groups/x86_64/base-devel/>`__. This includes the
<https://archlinux.org/packages/core/any/base-devel/>`__. This includes the
standard tooling needed to build packages, such as a compiler and binary tools.
.. code-block:: bash
sudo pacman -S base-devel
sudo pacman -S --needed base-devel
Now you are ready to install the OCRmyPDF package.
@@ -260,7 +262,7 @@ page.
<https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed
using the same series of steps as for the installation OCRmyPDF AUR
package. Alternatively, it may be built manually from source following the
instructions in `Installing the JBIG2 encoder <jbig2>`__. If JBIG2 is
instructions in :ref:`Installing the JBIG2 encoder <jbig2>`. If JBIG2 is
installed, OCRmyPDF 7.0.0 and later will automatically detect it.
Alpine Linux
@@ -325,6 +327,22 @@ languages you can optionally install them all:
brew install tesseract-lang # Optional: Install all language packs
MacPorts
--------
.. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fports.macports.org%2Fapi%2Fv1%2Fports%2Focrmypdf%2F%3Fformat%3Djson&query=version&label=MacPorts
:alt: Macports Version Information
:target: https://ports.macports.org/port/ocrmypdf
OCRmyPDF is includes in MacPorts:
.. code-block:: bash
sudo port install ocrmypdf
Note that while this will install tesseract you will need to install
the appropriate tesseract `language ports <https://ports.macports.org/search/?selected_facets=categories_exact%3Atextproc&installed_file=&q=tesseract&name=on>`__.
Manual installation on macOS
----------------------------
@@ -623,7 +641,7 @@ environment:
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
Or, to install in `development
mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
mode <https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#working-in-development-mode>`__,
allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash
@@ -663,7 +681,7 @@ To install all of the development and test requirements:
.. code-block:: bash
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
python -m .venv
python -m venv .venv
source .venv/bin/activate
cd OCRmyPDF
pip install -e .[test]
+17 -1
View File
@@ -68,6 +68,22 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Archlinux
------
.. code-block:: bash
# Display a list of all Tesseract language packs
pacman -Ss tesseract-data
# Install German language pack
pacman -S tesseract-data-deu
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo
------
@@ -122,4 +138,4 @@ Custom language packs
If you have fine-tuned or trained Tesseract and generated custom trained data, you can
copy your ``customlang.traineddata`` file into your Tesseract "tessdata" folder, and
then use the ``-l customlang`` argument to tell OCRmyPDF to pass that language on to
Tesseract.
Tesseract.
+10 -1
View File
@@ -64,4 +64,13 @@ installation documentation.
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.
available in 32-bit form. Please note we do not test on 32-bit platforms.
HEIF/HEIC
---------
OCRmyPDF defaults to installing the pi-heif PyPI package, which supports converting
HEIF (High Efficiency Image File Format) images to PDF from the command line.
If your distribution does not have this library available, you can exclude it and
OCRmyPDF will gracefully degrade automatically, losing only support for this
feature.
+82
View File
@@ -30,6 +30,88 @@ OCRmyPDF typically supports the three most recent Python versions.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.4.3
=======
- Work around pdfminer.six issue where a token on the buffer boundary is incorrectly
parsed as two tokens. :issue:`1361`
- New rules are applied to stencil masks and explicit masks when calculating the
optimal page DPI for rendering. :issue:`1362`
- Fixed attempts to use an incompatible jbig2.EXE provided by TeX Live. :issue:`1363`
v16.4.2
=======
- Fixed order of filenames passed to Ghostscript for PDF/A generation. :issue:`1359`
- Suppressed missing jbig2dec warning message. :issue:`1358`
- Fixed calculation of image size when soft mask dimensions don't match image
dimension. :issue:`1351`
- Several fixes to documentation. Thanks to users Iris and JoKalliauer
who contributed these changes.
- Fixed error on processing PDFs that are missing certain image metadata. :issue:`1315`
v16.4.1
=======
- Fixed calculation of image printed area (used in finding weighted DPI for OCR).
:issue:`1334`
- Fixed "NotImplementedError: not sure how to get colorspace" error
messages in logs which simply records a failure to optimize images with
print production colorspaces. :issue:`1315`
v16.4.0
=======
- Selecting the ``osd`` and ``equ`` pseudo-languages with ``-l/--language`` now
exits with an error when using Tesseract OCR, because these are not
regular Tesseract languages but implementation details implemented.
Using them can cause Tesseract to crash.
- The hOCR renderer is more tolerant of extra whitespace in input files.
- watcher.py now changes the output file extension to .pdf when the input is not
.pdf.
- Improved handling of PDFs that contain circularly referenced Form XObjects.
:issue:`1321`
- Fixed Alpine Docker image for ARM64, which was not building correctly.
- Docker images now use pikepdf 9.0.0.
- Prevent use of Tesseract OCR 5.4.0, a version with known regressions.
- Disabled progressbar for "Linearizing" when ``--no-progress-bar`` set.
- Fixed some tests that warn about missing JBIG2 decoding via pikepdf, by
installing the necessary libraries during tests.
v16.3.1
=======
- Fixed a test suite failure with Ghostscript 10.03.0+. :issue:`1316`
- Fixed an issue with the presentation of the "OCR" progress bar. :issue:`1313`
v16.3.0
=======
- Fixed progress bar not displaying for Ghostscript PDF/A conversion. :issue:`1313`
- Added progress bar for linearization. :issue:`1313`
- If `--rotate-pages-threshold` issued without `--rotate-pages` we now exit with
an error since the user likely intended to use `--rotate-pages`. :issue:`1309`
- If Tesseract hOCR gives an invalid line box, print an error message instead of
exiting with an error. :issue:`1312`
v16.2.0
=======
- Fixed issue 'NoneType' object has no attribute 'get' when optimizing certain PDFs.
:issue:`1293,1271`
- Switched formatting from black to ruff.
- Added support for sending sidecar output to io.BytesIO.
- Added support for converting HEIF/HEIC images (the native image of iPhones and
some other devices) to PDFs, when the appropriate pi-hief library is installed.
This library is marked as a dependency, but maintainers may opt out if needed.
- We now default to downsampling large images that would exceed Tesseract's internal
limits, but only if it cause processing to fail. Previously, this behavior only
occurred if specifically requested on command line. It can still be configured
and disabled. See the --tesseract command line options.
- Added Macports install instructions. Thanks @akierig.
- Improved logging output when an unexpected error occurs while trying to obtain
the version of a third party program.
v16.1.2
=======
+9 -6
View File
@@ -14,12 +14,12 @@ You should edit this script to meet your needs.
from __future__ import annotations
import filecmp
import logging
import sys
import os
import posixpath
import shutil
import filecmp
import sys
from pathlib import Path
import ocrmypdf
@@ -27,7 +27,8 @@ import ocrmypdf
# pylint: disable=logging-format-interpolation
# pylint: disable=logging-not-lazy
def filecompare(a,b):
def filecompare(a, b):
try:
return filecmp.cmp(a, b, shallow=True)
except FileNotFoundError:
@@ -69,7 +70,7 @@ for filename in start_dir.glob("**/*.pdf"):
logging.info(f"Archiving document to {archive_filename}")
try:
shutil.copy2(filename, posixpath.dirname(archive_filename))
except IOError as io_err:
except OSError:
os.makedirs(posixpath.dirname(archive_filename))
shutil.copy2(filename, posixpath.dirname(archive_filename))
try:
@@ -82,7 +83,9 @@ for filename in start_dir.glob("**/*.pdf"):
except ocrmypdf.exceptions.DigitalSignatureError:
logging.info("Skipped document because it has a digital signature")
except ocrmypdf.exceptions.TaggedPDFError:
logging.info("Skipped document because it does not need ocr as it is tagged")
except:
logging.info(
"Skipped document because it does not need ocr as it is tagged"
)
except Exception:
logging.error("Unhandled error occured")
logging.info("OCR complete")
+4 -3
View File
@@ -53,9 +53,10 @@ for dir_name, _subdirs, file_list in os.walk(start_dir):
]
logging.info(cmd)
full_path_ocr = os.path.join(dir_name, filename_ocr)
with open(filename, 'rb') as input_file, open(
full_path_ocr, 'wb'
) as output_file:
with (
open(filename, 'rb') as input_file,
open(full_path_ocr, 'wb') as output_file,
):
proc = subprocess.run(
cmd,
stdin=input_file,
+7 -4
View File
@@ -46,15 +46,18 @@ class LoggingLevelEnum(str, Enum):
CRITICAL = "CRITICAL"
def get_output_dir(root: Path, basename: str, output_dir_year_month: bool) -> Path:
def get_output_path(root: Path, basename: str, output_dir_year_month: bool) -> Path:
assert '/' not in basename, "basename must not contain '/'"
if output_dir_year_month:
today = datetime.today()
output_directory_year_month = root / str(today.year) / f'{today.month:02d}'
if not output_directory_year_month.exists():
output_directory_year_month.mkdir(parents=True, exist_ok=True)
output_path = Path(output_directory_year_month) / basename
output_path = Path(output_directory_year_month) / Path(basename).with_suffix(
'.pdf'
)
else:
output_path = root / basename
output_path = root / Path(basename).with_suffix('.pdf')
return output_path
@@ -98,7 +101,7 @@ def execute_ocrmypdf(
retries_loading_file: int,
output_dir_year_month: bool,
):
output_path = get_output_dir(output_dir, file_path.name, output_dir_year_month)
output_path = get_output_path(output_dir, file_path.name, output_dir_year_month)
log.info("-" * 20)
log.info(f'New file: {file_path}. Waiting until fully written...')
+6 -25
View File
@@ -12,12 +12,13 @@ readme = "README.md"
license = { text = "MPL-2.0" }
requires-python = ">=3.10"
dependencies = [
"Pillow>=10.0.1",
"deprecation>=2.1.0",
"img2pdf>=0.5",
"packaging>=20",
"pdfminer.six>=20220319",
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
"pikepdf>=8.10.1",
"Pillow>=10.0.1",
"pluggy>=1",
"rich>=13",
]
@@ -60,7 +61,7 @@ test = [
"types-Pillow",
"types-humanfriendly",
]
watcher = ["watchdog>=1.0.2", "typer[all]", "python-dotenv"]
watcher = ["watchdog>=1.0.2", "typer-slim[standard]", "python-dotenv"]
webservice = ["Flask>=2.0.1"]
[project.scripts]
@@ -78,29 +79,6 @@ namespaces = false
[tool.distutils.bdist_wheel]
python-tag = "py310"
[tool.black]
line-length = 88
target-version = ["py310", "py311", "py312"]
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| docs
| misc
| \.egg-info
)/
'''
[tool.coverage.run]
branch = true
parallel = true
@@ -176,3 +154,6 @@ convention = "google"
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"misc/*.py" = ["D103", "D101", "D102"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
[tool.ruff.format]
quote-style = "preserve"
+2 -2
View File
@@ -18,8 +18,8 @@ architectures: [amd64]
environment:
TESSDATA_PREFIX: $SNAP/usr/share/tesseract-ocr/4.00/tessdata
GS_LIB: $SNAP/usr/share/ghostscript/9.55/Resource/Init
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55/Resource/Font
GS_LIB: $SNAP/usr/share/ghostscript/9.55.0/Resource/Init
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55.0/Resource/Font
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
apps:
+8
View File
@@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Enforce English hegemony
DEFAULT_LANGUAGE = 'eng'
# Default rotation threshold
DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0
+18 -3
View File
@@ -177,6 +177,17 @@ class GhostscriptFollower:
self.progressbar_class = progressbar_class
self.progressbar = None
def __enter__(self):
# We can't actually set up the progressbar here, because we don't know
# how many pages there are until the first __call__() happens. So we
# do it in __call__().
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.progressbar:
return self.progressbar.__exit__(exc_type, exc_value, traceback)
return False
def __call__(self, line):
if not self.progressbar_class:
return
@@ -187,7 +198,8 @@ class GhostscriptFollower:
self.progressbar = self.progressbar_class(
total=self.count, desc="PDF/A conversion", unit='page'
)
return
# Now that we know the count, we can set up the progressbar.
self.progressbar.__enter__()
else:
if self.re_page.match(line.strip()):
self.progressbar.update()
@@ -265,7 +277,10 @@ def generate_pdfa(
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try:
with Path(output_file).open('wb') as output:
with (
Path(output_file).open('wb') as output,
GhostscriptFollower(progressbar_class) as pbar,
):
p = run_polling_stderr(
args_gs,
stdout=output,
@@ -274,7 +289,7 @@ def generate_pdfa(
text=True,
encoding='utf-8',
errors='replace',
callback=GhostscriptFollower(progressbar_class),
callback=pbar,
)
except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create
+8 -2
View File
@@ -5,7 +5,7 @@
from __future__ import annotations
from subprocess import PIPE
from subprocess import PIPE, CalledProcessError
from packaging.version import Version
@@ -14,7 +14,13 @@ from ocrmypdf.subprocess import get_version, run
def version() -> Version:
return Version(get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*'))
try:
version = get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*')
except CalledProcessError as e:
# TeX Live for Windows provides an incompatible jbig2.EXE which may
# be on the PATH.
raise MissingDependencyError('jbig2enc') from e
return Version(version)
def available():
+2 -1
View File
@@ -220,7 +220,8 @@ def get_deskew(
def tesseract_log_output(stream: bytes) -> None:
tlog = TesseractLoggerAdapter(
log, extra=log.extra if hasattr(log, 'extra') else None # type: ignore
log,
extra=log.extra if hasattr(log, 'extra') else None, # type: ignore
)
if not stream:
+37 -7
View File
@@ -153,22 +153,52 @@ def _set_language(pdf: Pdf, languages: list[str]):
pdf.Root.Lang = iso639_2
class MetadataProgress:
def __init__(self, progressbar_class, enable: bool = True):
self.progressbar_class = progressbar_class
self.progressbar = self.progressbar_class(
total=100, desc="Linearizing", unit='%', disable=not enable
)
def __enter__(self):
self.progressbar.__enter__()
return self
def __exit__(self, exc_type, exc_value, traceback):
return self.progressbar.__exit__(exc_type, exc_value, traceback)
def __call__(self, percent: int):
if not self.progressbar_class:
return
self.progressbar.update(completed=percent)
def metadata_fixup(
working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
) -> Path:
"""Fix certain metadata fields after Ghostscript PDF/A conversion.
"""Fix certain metadata fields whether PDF or PDF/A.
Override some of Ghostscript's metadata choices.
Also report on metadata in the input file that was not retained during
PDF/A conversion.
conversion.
"""
output_file = context.get_path('metafix.pdf')
options = context.options
with Pdf.open(context.origin) as original, Pdf.open(working_file) as pdf:
pbar_class = context.plugin_manager.hook.get_progressbar_class()
with (
Pdf.open(context.origin) as original,
Pdf.open(working_file) as pdf,
MetadataProgress(pbar_class, options.progress_bar) as pbar,
):
docinfo = get_docinfo(original, context)
with original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original, pdf.open_metadata() as meta_pdf:
with (
original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original,
pdf.open_metadata() as meta_pdf,
):
meta_pdf.load_from_docinfo(
docinfo, delete_missing=False, raise_failure=False
)
@@ -179,6 +209,6 @@ def metadata_fixup(
report_on_metadata(options, meta_missing)
_set_language(pdf, options.languages)
pdf.save(output_file, **pdf_save_settings)
pdf.save(output_file, progress=pbar, **pdf_save_settings)
return output_file
+16 -3
View File
@@ -41,12 +41,23 @@ from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
from ocrmypdf.pluginspec import OrientationConfidence
try:
from pi_heif import register_heif_opener
except ImportError:
def register_heif_opener():
pass
T = TypeVar("T")
log = logging.getLogger(__name__)
VECTOR_PAGE_DPI = 400
register_heif_opener()
def triage_image_file(input_file: Path, output_file: Path, options) -> None:
"""Triage the input image file.
@@ -131,7 +142,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
"""
with open(input_file, 'rb') as f:
signature = f.read(search_window)
m = re.search(br'%PDF-(\d\.\d)', signature)
m = re.search(rb'%PDF-(\d\.\d)', signature)
if m:
return m.group(1).decode('ascii')
return ''
@@ -464,7 +475,7 @@ def calculate_raster_dpi(page_context: PageContext):
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. "
"Weighted 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.",
@@ -767,7 +778,9 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
font=Courier(),
)
HocrTransform(
hocr_filename=hocr, dpi=dpi.to_scalar(), **debug_kwargs # square
hocr_filename=hocr,
dpi=dpi.to_scalar(),
**debug_kwargs, # square
).to_pdf(
out_filename=output_file,
image_filename=None,
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
+10 -11
View File
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
@@ -104,14 +103,14 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
try:
set_thread_pageno(result.pageno + 1)
sidecars[result.pageno] = result.text
pbar.update()
pbar.update(0.5)
ocrgraft.graft_page(
pageno=result.pageno,
image=result.pdf_page_from_image,
textpdf=result.ocr,
autorotate_correction=result.orientation_correction,
)
pbar.update()
pbar.update(0.5)
finally:
set_thread_pageno(None)
@@ -119,10 +118,9 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
use_threads=options.use_threads,
max_workers=max_workers,
progress_kwargs=dict(
total=(2 * len(context.pdfinfo)),
total=len(context.pdfinfo),
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
unit='page',
unit_scale=0.5,
disable=not options.progress_bar,
),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
@@ -155,12 +153,13 @@ def _run_pipeline(
options: argparse.Namespace,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
with manage_work_folder(
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
retain=options.keep_temporary_files,
print_location=options.keep_temporary_files,
) as work_folder, manage_debug_log_handler(
options=options, work_folder=work_folder
with (
manage_work_folder(
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
retain=options.keep_temporary_files,
print_location=options.keep_temporary_files,
) as work_folder,
manage_debug_log_handler(options=options, work_folder=work_folder),
):
executor = setup_pipeline(options, plugin_manager)
check_requested_output_file(options)
-1
View File
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
+11 -5
View File
@@ -66,7 +66,7 @@ class ProgressBar(Protocol):
def __exit__(self, *args):
"""Exit a progress bar context."""
def update(self, n=1):
def update(self, n=1, *, completed=None):
"""Update the progress bar by an increment.
For use within a progress bar context.
@@ -85,7 +85,7 @@ class NullProgressBar:
def __exit__(self, exc_type, exc_value, traceback):
return False
def update(self, _arg=None):
def update(self, _arg=None, *, completed=None):
return
@@ -103,6 +103,7 @@ class RichProgressBar:
disable: bool = False,
**kwargs,
):
self._entered = False
self.progress = Progress(
TextColumn(
"[progress.description]{task.description}",
@@ -130,6 +131,7 @@ class RichProgressBar:
def __enter__(self):
self.progress.start()
self._entered = True
return self
def __exit__(self, exc_type, exc_value, traceback):
@@ -137,6 +139,10 @@ class RichProgressBar:
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)
def update(self, n=1, *, completed=None):
assert self._entered, "Progress bar must be entered before updating"
if completed is None:
advance = self.unit_scale if n is None else n
self.progress.update(self.progress_bar, advance=advance)
else:
self.progress.update(self.progress_bar, completed=completed)
+7 -8
View File
@@ -20,6 +20,7 @@ import pikepdf
import PIL
from pluggy import PluginManager
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._exec import unpaper
from ocrmypdf.exceptions import (
BadArgsError,
@@ -30,17 +31,9 @@ from ocrmypdf.exceptions import (
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
from ocrmypdf.subprocess import check_external_program
# -------------
# External dependencies
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
log = logging.getLogger(__name__)
# --------
def check_platform() -> None:
if sys.maxsize <= 2**32: # pragma: no cover
log.warning(
@@ -61,6 +54,7 @@ def check_options_languages(
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
if not ocr_engine_languages:
return
missing_languages = set(options.languages) - set(ocr_engine_languages)
if missing_languages:
lang_text = '\n'.join(lang for lang in missing_languages)
@@ -130,6 +124,11 @@ def check_options_preprocessing(options: Namespace) -> None:
options.clean = True
if options.unpaper_args and not options.clean:
raise BadArgsError("--clean is required for --unpaper-args")
if (
options.rotate_pages_threshold != DEFAULT_ROTATE_PAGES_THRESHOLD
and not options.rotate_pages
):
raise BadArgsError("--rotate-pages is required for --rotate-pages-threshold")
if options.clean:
check_external_program(
program='unpaper',
+6 -1
View File
@@ -209,6 +209,9 @@ def create_options(
cmdline.append('stream://output_file')
else:
cmdline.append(os.fspath(output_file))
if 'sidecar' in kwargs and isinstance(kwargs['sidecar'], BinaryIO | IOBase):
cmdline.append('--sidecar')
cmdline.append('stream://sidecar')
parser.enable_api_mode()
options = parser.parse_args(cmdline)
@@ -219,6 +222,8 @@ def create_options(
options.input_file = input_file
if options.output_file == 'stream://output_file':
options.output_file = output_file
if options.sidecar == 'stream://sidecar':
options.sidecar = kwargs['sidecar']
return options
@@ -230,7 +235,7 @@ def ocr( # noqa: D417
language: Iterable[str] | None = None,
image_dpi: int | None = None,
output_type: str | None = None,
sidecar: StrPath | None = None,
sidecar: PathOrIO | None = None,
jobs: int | None = None,
use_threads: bool | None = None,
title: str | None = None,
@@ -8,7 +8,5 @@ from ocrmypdf import hookimpl
@hookimpl
def filter_pdf_page(
page, image_filename, output_pdf
): # pylint: disable=unused-argument
def filter_pdf_page(page, image_filename, output_pdf): # pylint: disable=unused-argument
return output_pdf
+1 -1
View File
@@ -129,7 +129,7 @@ def generate_pdfa(
):
"""Generate a PDF/A from the list of PDF pages and PDF/A metadata."""
ghostscript.generate_pdfa(
pdf_pages=[*pdf_pages, pdfmark],
pdf_pages=[pdfmark, *pdf_pages],
output_file=output_file,
compression=context.options.pdfa_image_compression,
color_conversion_strategy=context.options.color_conversion_strategy,
+19 -3
View File
@@ -2,9 +2,9 @@
# SPDX-License-Identifier: MPL-2.0
"""Built-in plugin to implement OCR using Tesseract."""
from __future__ import annotations
import argparse
import logging
import os
@@ -14,6 +14,7 @@ from ocrmypdf import hookimpl
from ocrmypdf._exec import tesseract
from ocrmypdf._jobcontext import PageContext
from ocrmypdf.cli import numeric, str_to_int
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
from ocrmypdf.helpers import clamp
from ocrmypdf.imageops import calculate_downsample, downsample_image
from ocrmypdf.pluginspec import OcrEngine
@@ -94,7 +95,8 @@ def add_options(parser):
)
tess.add_argument(
'--tesseract-downsample-large-images',
action='store_true',
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Downsample large images before OCR. Tesseract has an upper limit on the "
"size images it will support. If this argument is given, OCRmyPDF will "
@@ -143,6 +145,12 @@ def check_options(options):
need_version='4.1.1', # Ubuntu 22.04 version (also 20.04)
version_parser=tesseract.TesseractVersion,
)
tess_version = tesseract.version()
if tess_version == tesseract.TesseractVersion('5.4.0'):
raise MissingDependencyError(
"Tesseract 5.4.0 is not supported due to regressions in this version. "
"Please upgrade to a newer or supported older version."
)
# Decide on what renderer to use
if options.pdf_renderer == 'auto':
@@ -163,6 +171,14 @@ def check_options(options):
"The --tesseract-pagesegmode argument you select will disable OCR. "
"This may cause processing to fail."
)
DENIED_LANGUAGES = {'equ', 'osd'}
if DENIED_LANGUAGES & set(options.languages):
raise BadArgsError(
"The following languages for Tesseract's internal use and should not "
"be issued explicitly: "
f"{', '.join(DENIED_LANGUAGES & set(options.languages))}\n"
"Remove them from the -l/--language argument."
)
@hookimpl
@@ -220,7 +236,7 @@ class TesseractOcrEngine(OcrEngine):
@staticmethod
def creator_tag(options):
tag = '-PDF' if options.pdf_renderer == 'sandwich' else 'hOCR'
tag = '-PDF' if options.pdf_renderer == 'sandwich' else '-hOCR'
return f"Tesseract OCR{tag} {TesseractOcrEngine.version()}"
def __str__(self):
+2 -1
View File
@@ -9,6 +9,7 @@ import argparse
from collections.abc import Callable, Mapping
from typing import Any, TypeVar
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION
@@ -403,7 +404,7 @@ Online documentation is located at:
)
advanced.add_argument(
'--rotate-pages-threshold',
default=14.0,
default=DEFAULT_ROTATE_PAGES_THRESHOLD,
type=numeric(float, 0, 1000),
metavar='CONFIDENCE',
help="Only rotate pages when confidence is above this value (arbitrary "
+3 -1
View File
@@ -268,7 +268,9 @@ def check_pdf(input_file: Path) -> bool:
return False
else:
with pdf:
messages = pdf.check()
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message=r'pikepdf.*JBIG2.*')
messages = pdf.check()
success = True
for msg in messages:
if 'error' in msg.lower():
+23 -10
View File
@@ -61,15 +61,25 @@ class HocrTransform:
"""A class for converting documents from the hOCR format.
For details of the hOCR format, see:
http://kba.cloud/hocr-spec/.
http://kba.github.io/hocr-spec/1.2/.
"""
box_pattern = re.compile(r'bbox (\d+) (\d+) (\d+) (\d+)')
box_pattern = re.compile(
r'''
bbox \s+
(\d+) \s+ # left: uint
(\d+) \s+ # top: uint
(\d+) \s+ # right: uint
(\d+) # bottom: uint
''',
re.VERBOSE,
)
baseline_pattern = re.compile(
r'''
baseline \s+
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
([\-\+]?\d+) # +/- int''',
([\-\+]?\d+) # +/- int
''',
re.VERBOSE,
)
@@ -117,15 +127,12 @@ class HocrTransform:
# Stop after first div that has page coordinates
break
def _get_element_text(self, element: Element):
def _get_element_text(self, element: Element) -> str:
"""Return the textual content of the element and its children."""
text = ''
if element.text is not None:
text += element.text
text = element.text if element.text is not None else ''
for child in element:
text += self._get_element_text(child)
if element.tail is not None:
text += element.tail
text += element.tail if element.tail is not None else ''
return text
@classmethod
@@ -286,7 +293,13 @@ class HocrTransform:
line_box = self.element_coordinates(line)
if not line_box:
return
assert line_box.ury > line_box.lly # lly is top, ury is bottom
if line_box.ury <= line_box.lly:
log.error(
"line box is invalid so we cannot render it: box=%s text=%s",
line_box,
self._get_element_text(line),
)
return
self._debug_draw_line_bbox(canvas, line_box)
+5 -3
View File
@@ -7,12 +7,12 @@ Derived from
https://www.loc.gov/standards/iso639-2/ascii_8bits.html
"""
from typing import NamedTuple
class ISOCodeData(NamedTuple):
"""Data for a single ISO 639 code."""
alt: str
alpha_2: str
english: str
@@ -168,8 +168,10 @@ ISO_639_3 = {
'chu': ISOCodeData(
'',
'cu',
('Church Slavic; Old Slavonic; Church Slavonic;'
' Old Bulgarian; Old Church Slavonic'),
(
'Church Slavic; Old Slavonic; Church Slavonic;'
' Old Bulgarian; Old Church Slavonic'
),
"slavon d'église; vieux slave; slavon liturgique; vieux bulgare",
),
'chv': ISOCodeData('', 'cv', 'Chuvash', 'tchouvache'),
+6 -3
View File
@@ -3,7 +3,6 @@
"""Post-processing image optimization of OCR PDFs."""
from __future__ import annotations
import logging
@@ -29,6 +28,7 @@ from pikepdf import (
Stream,
UnsupportedImageTypeError,
)
from pikepdf.models.image import HifiPrintImageNotTranscodableError
from PIL import Image
from ocrmypdf._concurrent import Executor, SerialExecutor
@@ -90,6 +90,7 @@ def extract_image_filter(
if (
len(pim.filter_decodeparms) == 2
and first_filtdp[0] == Name.FlateDecode
and first_filtdp[1] is not None
and first_filtdp[1].get(Name.Predictor, 1) == 1
and second_filtdp[0] == Name.DCTDecode
and not second_filtdp[1]
@@ -200,7 +201,7 @@ def extract_image_generic(
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except UnsupportedImageTypeError:
except (UnsupportedImageTypeError, HifiPrintImageNotTranscodableError):
return None
return XrefExt(xref, ext)
elif (
@@ -256,6 +257,9 @@ def _find_image_xrefs_container(
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 xref in include_xrefs or xref in exclude_xrefs:
continue # Already processed
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}")
@@ -269,7 +273,6 @@ def _find_image_xrefs_container(
depth + 1,
)
continue
xref = Xref(image.objgen[0])
if Name.SMask in image:
# Ignore soft masks
smask_xref = Xref(image.SMask.objgen[0])
+1 -1
View File
@@ -90,7 +90,7 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
icc: ICC identifier such as 'sRGB'
References:
Adobe PDFMARK Reference:
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
https://opensource.adobe.com/dc-acrobat-sdk-docs/library/pdfmark/
"""
if icc != 'sRGB':
raise NotImplementedError("Only supporting sRGB")
+41 -10
View File
@@ -239,7 +239,13 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
# to do. Just pretend nothing happened, keep calm and carry on.
warn("PDF graphics stack underflowed - PDF may be malformed")
elif operator == 'cm':
ctm = Matrix(operands) @ ctm
try:
ctm = Matrix(operands) @ ctm
except ValueError:
raise InputFileError(
"PDF content stream is corrupt - this PDF is malformed. "
"Use a PDF editor that is capable of visually inspecting the PDF."
)
elif operator == 'Do':
image_name = operands[0]
settings = XobjectSettings(
@@ -363,8 +369,21 @@ class ImageInfo:
pim = PdfImage(pdfimage)
else:
raise ValueError("Either pdfimage or inline must be set")
self._width = pim.width
self._height = pim.height
if (smask := pim.obj.get(Name.SMask, None)) is not None:
# SMask is pretty much an alpha channel, but in PDF it's possible
# for channel to have different dimensions than the image
# itself. Some PDF writers use this to create a grayscale stencil
# mask. For our purposes, the effective size is the size of the
# larger component (image or smask).
self._width = max(smask.get(Name.Width, 0), self._width)
self._height = max(smask.get(Name.Height, 0), self._height)
if (mask := pim.obj.get(Name.Mask, None)) is not None:
# If the image has a /Mask entry, it has an explicit mask.
self._width = max(mask.get(Name.Width, 0), self._width)
self._height = max(mask.get(Name.Height, 0), self._height)
# If /ImageMask is true, then this image is a stencil mask
# (Images that draw with this stencil mask will have a reference to
@@ -468,9 +487,18 @@ class ImageInfo:
def renderable(self) -> bool:
"""Whether the image is renderable.
Some PDFs in the wild have invalid images that are not renderable.
Some PDFs in the wild have invalid images that are not renderable,
due to unusual dimensions.
Stencil masks are not also not renderable, since they are not
drawn, but rather they control how rendering happens.
"""
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
return (
self.dpi.is_finite
and self.width >= 0
and self.height >= 0
and self.type_ != 'stencil'
)
@property
def dpi(self) -> Resolution:
@@ -485,7 +513,7 @@ class ImageInfo:
"""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)
return float((self.width / self.dpi.x) * (self.height / self.dpi.y))
def __repr__(self):
"""Return a string representation of the image."""
@@ -567,7 +595,7 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content
xobjs = resources[Name.XObject].as_dict()
for xobj in xobjs:
candidate = xobjs[xobj]
if candidate is None or candidate[Name.Subtype] != Name.Form:
if candidate is None or candidate.get(Name.Subtype) != Name.Form:
continue
form_xobject = candidate
@@ -1049,10 +1077,14 @@ class PageInfo:
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]
image_dpis = []
image_areas = []
for image in self._images:
if not image.renderable:
continue
image_dpis.append(image.dpi.to_scalar())
image_areas.append(image.printed_area)
total_drawn_area = sum(image_areas)
if total_drawn_area == 0:
return None
@@ -1065,7 +1097,6 @@ class PageInfo:
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,
+5 -3
View File
@@ -17,6 +17,7 @@ import pdfminer
import pdfminer.encodingdb
import pdfminer.pdfdevice
import pdfminer.pdfinterp
import pdfminer.psparser
from pdfminer.converter import PDFLayoutAnalyzer
from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox
from pdfminer.pdfcolor import PDFColorSpace
@@ -58,9 +59,10 @@ def pdfsimplefont__init__(
setattr(PDFSimpleFont, '__init__', pdfsimplefont__init__)
#
# pdfminer patches when creator is PScript5.dll
#
# Patch pdfminer.six buffer size
# The parser doesn't properly handle keyword tokens are split across the end of the
# buffer, so increase the buffer size something far larger than will ever be seen.
pdfminer.psparser.PSBaseParser.BUFSIZ = 256 * 1024 * 1024
def pdftype3font__pscript5_get_height(self):
-1
View File
@@ -3,7 +3,6 @@
"""Utilities to measure OCR quality."""
from __future__ import annotations
import re
+1
View File
@@ -171,6 +171,7 @@ def get_version(
) from e
except CalledProcessError as e:
if e.returncode != 0:
log.exception(e)
raise MissingDependencyError(
f"Ran program '{program}' but it exited with an error:\n{e.output}"
) from e
+1 -1
View File
@@ -79,7 +79,7 @@ def registry_path_ghostscript(env=None) -> Iterator[Path]:
registry_subkeys(k), key=ghostscript_version_key, default=(0, 0, 0)
)
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
winreg.HKEY_LOCAL_MACHINE, rf"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
) as k:
for _, gs_path, _ in registry_values(k):
yield Path(gs_path) / 'bin'
+12 -8
View File
@@ -169,22 +169,25 @@ class CacheOcrEngine(TesseractOcrEngine):
@staticmethod
def get_orientation(input_file, options):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
with (
CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
):
return TesseractOcrEngine.get_orientation(input_file, options)
@staticmethod
def get_deskew(input_file, options) -> float:
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
with (
CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
):
return TesseractOcrEngine.get_deskew(input_file, options)
@staticmethod
def generate_hocr(input_file, output_hocr, output_text, options):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
with (
CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
):
TesseractOcrEngine.generate_hocr(
input_file, output_hocr, output_text, options
@@ -192,8 +195,9 @@ class CacheOcrEngine(TesseractOcrEngine):
@staticmethod
def generate_pdf(input_file, output_pdf, output_text, options):
with CacheOcrEngine.lock, patch(
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)
with (
CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
):
TesseractOcrEngine.generate_pdf(
input_file, output_pdf, output_text, options
+4 -3
View File
@@ -72,9 +72,10 @@ class FixedRotateNoopOcrEngine(OcrEngine):
@staticmethod
def generate_hocr(input_file, output_hocr, output_text, options):
with Image.open(input_file) as im, open(
output_hocr, 'w', encoding='utf-8'
) as f:
with (
Image.open(input_file) as im,
open(output_hocr, 'w', encoding='utf-8') as f,
):
w, h = im.size
f.write(HOCR_TEMPLATE.format(str(w), str(h)))
with open(output_text, 'w') as f:
+4 -3
View File
@@ -70,9 +70,10 @@ class NoopOcrEngine(OcrEngine):
@staticmethod
def generate_hocr(input_file, output_hocr, output_text, options):
with Image.open(input_file) as im, open(
output_hocr, 'w', encoding='utf-8'
) as f:
with (
Image.open(input_file) as im,
open(output_hocr, 'w', encoding='utf-8') as f,
):
w, h = im.size
f.write(HOCR_TEMPLATE.format(str(w), str(h)))
with open(output_text, 'w') as f:
@@ -9,6 +9,7 @@ ensure we fail with an error rather than deadlock in such cases.
Page 4 was chosen because of this number's association with bad luck
in many East Asian cultures.
"""
# type: ignore
from __future__ import annotations
+13
View File
@@ -29,6 +29,18 @@ def test_stream_api(resources: Path):
assert b'%PDF' in out.read(1024)
def test_sidecar_stringio(resources: Path, outdir: Path, outpdf: Path):
s = BytesIO()
ocrmypdf.ocr(
resources / 'ccitt.pdf',
outpdf,
plugins=['tests/plugins/tesseract_cache.py'],
sidecar=s
)
s.seek(0)
assert b'the' in s.getvalue()
def test_hocr_api_multipage(resources: Path, outdir: Path, outpdf: Path):
ocrmypdf.api._pdf_to_hocr(
resources / 'multipage.pdf',
@@ -62,3 +74,4 @@ def test_hocr_to_pdf_api(resources: Path, outdir: Path, outpdf: Path):
text = extract_text(outpdf)
assert 'hocr' in text and 'the' not in text
+5 -3
View File
@@ -11,9 +11,11 @@ import ocrmypdf
def test_no_glyphless_graft(resources, outdir):
with pikepdf.open(resources / 'francais.pdf') as pdf, pikepdf.open(
resources / 'aspect.pdf'
) as pdf_aspect, pikepdf.open(resources / 'cmyk.pdf') as pdf_cmyk:
with (
pikepdf.open(resources / 'francais.pdf') as pdf,
pikepdf.open(resources / 'aspect.pdf') as pdf_aspect,
pikepdf.open(resources / 'cmyk.pdf') as pdf_cmyk,
):
pdf.pages.extend(pdf_aspect.pages)
pdf.pages.extend(pdf_cmyk.pages)
pdf.save(outdir / 'test.pdf')
+4 -3
View File
@@ -35,9 +35,10 @@ def test_preserve_docinfo(output_type, resources, outpdf):
'--plugin',
'tests/plugins/tesseract_noop.py',
)
with pikepdf.open(resources / 'graph.pdf') as pdf_before, pikepdf.open(
output
) as pdf_after:
with (
pikepdf.open(resources / 'graph.pdf') as pdf_before,
pikepdf.open(output) as pdf_after,
):
for key in ('/Title', '/Author'):
assert pdf_before.docinfo[key] == pdf_after.docinfo[key]
pdfa_info = file_claims_pdfa(str(output))
+1 -1
View File
@@ -23,4 +23,4 @@ def test_semfree(resources, outpdf):
'--plugin',
'tests/plugins/tesseract_noop.py',
)
assert exitcode == ExitCode.ok
assert exitcode in (ExitCode.ok, ExitCode.pdfa_conversion_failed)
+9 -2
View File
@@ -13,9 +13,9 @@ import pytest
from ocrmypdf import pdfinfo
from ocrmypdf._exec import tesseract
from ocrmypdf.exceptions import MissingDependencyError
from ocrmypdf.exceptions import BadArgsError, ExitCode, MissingDependencyError
from .conftest import check_ocrmypdf
from .conftest import check_ocrmypdf, run_ocrmypdf_api
# pylint: disable=redefined-outer-name
@@ -144,3 +144,10 @@ def test_tesseract_log_output_raises(caplog):
with pytest.raises(tesseract.TesseractConfigError):
tesseract.tesseract_log_output(b'parameter not found: moo')
assert 'not found' in caplog.text
def test_blocked_language(resources, no_outpdf):
infile = resources / 'masks.pdf'
for bad_lang in ['osd', 'equ']:
with pytest.raises(BadArgsError):
run_ocrmypdf_api(infile, no_outpdf, '-l', bad_lang)