Compare commits

...
55 Commits
Author SHA1 Message Date
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
Ahmed AbdouandGitHub 08e89e2dbe Adding language install docs for archlinux (#1296)
Adding language install docs for archlinux
2024-04-24 14:46:05 -07:00
James R. Barlow 0e013df161 v16.2.0 release notes 2024-04-16 00:37:03 -07:00
James R. Barlow 9ba4e3ab46 Log unusual exceptions when trying to obtain a version
Fixes #1262
2024-04-07 14:39:08 -07:00
James R. Barlow 5fdcb7602b Make downsampling large images that Tesseract would otherwise error on into default behavior
Fixes #1281
2024-04-07 13:43:20 -07:00
James R. Barlow b4db1b741f optimize: fix handling of [/FlateDecode none] - type images
Closes #1271
2024-04-07 01:44:08 -07:00
James R. Barlow 7a8cc21e31 Add support for sidecar output to io.BytesIO
Closes #1252
2024-04-07 01:38:55 -07:00
James R. Barlow 0674829d8f Remove tool.black config 2024-04-07 00:36:52 -07:00
James R. Barlow 315aa0474b Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-04-07 00:34:51 -07:00
Ben BeasleyandGitHub df3451e779 Update the typer[all] dependency to typer-slim[standard] (#1287)
In 0.12.1, Typer was significantly reorganized.

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

Pip will warn about this and proceed,

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

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

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

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

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

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

* Fix cross-references to JBIG2 page

* Fix links for Fedora + Arch + HEAD revision install

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

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

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

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

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

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

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

* Add instructions for RHEL

* Update instruction for using python3.11 on RHEL
2024-01-24 10:07:58 -08:00
James R. Barlow cca04fd799 Document ability to remove all OCR 2024-01-23 23:35:43 -08:00
MaxiandGitHub 75bf8e4ba2 Add autotools automake libtool and leptonica requirements (#1230)
These tools are required for a successful install of JBIG2 on Ubuntu
2024-01-09 11:58:17 -08:00
Anish SapkotaandGitHub daabb5b100 Minor english correction in Docs (#1229) 2024-01-08 10:18:26 -08:00
61 changed files with 689 additions and 294 deletions
+1
View File
@@ -64,6 +64,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
img2pdf \ img2pdf \
libsm6 libxext6 libxrender-dev \ libsm6 libxext6 libxrender-dev \
pngquant \ pngquant \
python-is-python3 \
tesseract-ocr \ tesseract-ocr \
tesseract-ocr-chi-sim \ tesseract-ocr-chi-sim \
tesseract-ocr-deu \ tesseract-ocr-deu \
+1 -1
View File
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow # SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
FROM alpine:3.18 as base FROM alpine:3.19 as base
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV TZ=UTC ENV TZ=UTC
@@ -24,7 +24,7 @@ body:
- type: dropdown - type: dropdown
id: packaging-system id: packaging-system
attributes: attributes:
label: Where are you installing from? label: Where are you installing/running from?
multiple: true multiple: true
options: options:
- PyPI (pip, poetry, pipx, etc.) - PyPI (pip, poetry, pipx, etc.)
@@ -37,6 +37,11 @@ body:
- source build - source build
validations: validations:
required: true required: true
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
- type: dropdown - type: dropdown
id: operating-system id: operating-system
attributes: attributes:
@@ -47,6 +52,17 @@ body:
- Windows - Windows
- macOS - macOS
- BSD - 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
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
+9 -6
View File
@@ -32,8 +32,8 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
python: "3.12" python: "3.12"
tesseract5: true tesseract5: true
# - os: ubuntu-latest - os: ubuntu-latest
# python: "pypy3.10" python: "pypy3.10"
env: env:
OS: ${{ matrix.os }} OS: ${{ matrix.os }}
@@ -100,17 +100,20 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
files: ./coverage.xml files: ./coverage.xml
env_vars: OS,PYTHON env_vars: OS,PYTHON
test_macos: test_macos:
name: Test macOS name: Test macOS
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: 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"] python: ["3.10", "3.11", "3.12"]
env: env:
@@ -158,7 +161,7 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
files: ./coverage.xml files: ./coverage.xml
env_vars: OS,PYTHON env_vars: OS,PYTHON
@@ -201,7 +204,7 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/ python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
files: ./coverage.xml files: ./coverage.xml
env_vars: OS,PYTHON env_vars: OS,PYTHON
+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`` | | Windows Subsystem for Linux | ``apt install ocrmypdf`` |
| Fedora | ``dnf install ocrmypdf`` | | Fedora | ``dnf install ocrmypdf`` |
| macOS (Homebrew) | ``brew install ocrmypdf`` | | macOS (Homebrew) | ``brew install ocrmypdf`` |
| macOS (MacPorts) | ``port install ocrmypdf`` |
| macOS (nix) | ``nix-env -i ocrmypdf`` | | macOS (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` | | LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` | | FreeBSD | ``pkg install py-ocrmypdf`` |
+7 -6
View File
@@ -118,15 +118,16 @@ OCR for huge images
------------------- -------------------
Tesseract has internal limits on the size Tesseract has internal limits on the size
of images it will process. If you issue of images it will process. By default,
``--tesseract-downsample-large-images``, OCRmyPDF will downsample images ``--tesseract-downsample-large-images`` is enabled, and OCRmyPDF will
to fit Tesseract limits. (The limits are usually entered only for scanned downsample images to fit Tesseract limits. (The limits are usually encountered
images of oversized media, such as large maps or blueprints exceeding 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.) 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 ``--tesseract-downsample-above Npixels`` adjusts the threshold at which images
will be downsampled. By default, only images that exceed any of Tesseract's 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 You will also need to set ``--tesseract-timeout`` high enough to allow
for processing. for processing.
+3 -1
View File
@@ -112,7 +112,9 @@ OCRmyPDF is strict about not writing to standard output so that
users can safely use it in a pipeline and produce a valid output users can safely use it in a pipeline and produce a valid output
file. A caller application will have to ensure it does not write to file. A caller application will have to ensure it does not write to
standard output either, if it wants to be compatible with this standard output either, if it wants to be compatible with this
behavior and support piping to a file. behavior and support piping to a file. Another benefit of running
OCRmyPDF in a child process, as recommended above, is that it will
not interfere with the parent process's standard output.
Exceptions Exceptions
---------- ----------
+15 -1
View File
@@ -187,7 +187,7 @@ might remove desirable content, especially from poor quality scans.
background from grayscale or color images. Monochrome images are background from grayscale or color images. Monochrome images are
ignored. This should not be used on documents that contain color ignored. This should not be used on documents that contain color
photos as it may remove them. photos as it may remove them.
- ``--deskew`` will correct pages were scanned at a skewed angle by - ``--deskew`` will correct pages that were scanned at a skewed angle by
rotating them back into place. rotating them back into place.
- ``--clean`` uses - ``--clean`` uses
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up `unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
@@ -245,6 +245,20 @@ if all you want to is to apply image processing or PDF/A conversion.
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
for non-OCR operations, if needed. for non-OCR operations, if needed.
Remove all text or OCR from my PDF
----------------------------------
This is getting ridiculous, but OCRmyPDF can complete strip all textual
information from a PDF and reconstruct it as a "bag of images" PDF.
.. code-block::
ocrmypdf --tesseract-timeout 0 --force-ocr input.pdf output.pdf
Why would you want to do this? Perhaps you have a PDF where OCR
fails to produce useful results, and just want to get rid of all OCR information.
This command also removes OCR generated by third party tools.
Optimize images without performing OCR Optimize images without performing OCR
-------------------------------------- --------------------------------------
+76 -40
View File
@@ -23,7 +23,9 @@ These platforms have one-liner installs:
+-------------------------------+-----------------------------------------+ +-------------------------------+-----------------------------------------+
| Fedora | ``dnf install ocrmypdf tesseract-osd`` | | Fedora | ``dnf install ocrmypdf tesseract-osd`` |
+-------------------------------+-----------------------------------------+ +-------------------------------+-----------------------------------------+
| macOS | ``brew install ocrmypdf`` | | macOS (Homebrew) | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| macOS (MacPorts) | ``port install ocrmypdf`` |
+-------------------------------+-----------------------------------------+ +-------------------------------+-----------------------------------------+
| LinuxBrew | ``brew install ocrmypdf`` | | LinuxBrew | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+ +-------------------------------+-----------------------------------------+
@@ -99,12 +101,12 @@ For full details on version availability for your platform, check the
Fedora 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 .. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
:alt: Fedora 38 :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 .. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide :alt: Fedore Rawhide
@@ -113,7 +115,7 @@ Fedora
+-----------------------------------------------+ +-----------------------------------------------+
| |latest| | | |latest| |
+-----------------------------------------------+ +-----------------------------------------------+
| |fedora-37| |fedora-38| |fedora-rawhide| | | |fedora-38| |fedora-39| |fedora-rawhide| |
+-----------------------------------------------+ +-----------------------------------------------+
Users of Fedora may simply Users of Fedora may simply
@@ -123,7 +125,7 @@ Users of Fedora may simply
dnf install ocrmypdf tesseract-osd dnf install ocrmypdf tesseract-osd
For full details on version availability, check the `Fedora Package 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 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 to install the latest version from source. See `Installing HEAD revision
@@ -135,10 +137,36 @@ from sources <#installing-head-revision-from-sources>`__.
issues. OCRmyPDF works fine without it but will produce larger output issues. OCRmyPDF works fine without it but will produce larger output
files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later
will automatically detect it on the ``PATH``. To add JBIG2 encoding, 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: .. _ubuntu-lts-latest:
RHEL 9
------
Prepare the environment by getting Python 3.11:
.. code-block:: bash
dnf install python3.11 python3.11-pip
Then, follow `Requirements for pip and HEAD install <#requirements-for-pip-and-head-install>`__ to instal dependencies:
.. code-block:: bash
dnf install ghostscript tesseract
and build ocrmypdf in virtual environment:
.. code-block:: bash
python3.11 -m venv .venv
To add JBIG2 encoding, see :ref:`Installing the JBIG2 encoder <jbig2>`.
Note Fedora packages for language data haven't been branched for RHEL/EPEL, but you can get traineddata files directly from `tesseract
<https://github.com/tesseract-ocr/tessdata/>`__ and place them in ``/usr/share/tesseract/tessdata``.
Installing the latest version on Ubuntu 22.04 LTS Installing the latest version on Ubuntu 22.04 LTS
------------------------------------------------- -------------------------------------------------
@@ -162,37 +190,13 @@ To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 20.04 LTS Ubuntu 20.04 LTS
---------------- ----------------
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. The
install a more recent version, uninstall the system-provided version of most convenient way to install recent OCRmyPDF on older Ubuntu is to use
ocrmypdf, and install the following dependencies: Homebrew on Linux (Linuxbrew).
.. code-block:: bash .. code-block:: bash
sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed brew install ocrmypdf
sudo apt-get -y update
sudo apt-get -y install \
ghostscript \
icc-profiles-free \
libxml2 \
pngquant \
python3-pip \
tesseract-ocr \
zlib1g
To install ocrmypdf for the system:
.. code-block:: bash
pip3 install ocrmypdf
To install for the current user only:
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
To add JBIG2 encoding, see :ref:`jbig2`.
Arch Linux (AUR) Arch Linux (AUR)
---------------- ----------------
@@ -215,7 +219,7 @@ you are using a VM image, such as `the official Vagrant image
be completed for you. be completed for you.
Next you should install the `base-devel package group 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. standard tooling needed to build packages, such as a compiler and binary tools.
.. code-block:: bash .. code-block:: bash
@@ -258,7 +262,7 @@ page.
<https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed <https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed
using the same series of steps as for the installation OCRmyPDF AUR using the same series of steps as for the installation OCRmyPDF AUR
package. Alternatively, it may be built manually from source following the 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. installed, OCRmyPDF 7.0.0 and later will automatically detect it.
Alpine Linux Alpine Linux
@@ -323,6 +327,22 @@ languages you can optionally install them all:
brew install tesseract-lang # Optional: Install all language packs 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 Manual installation on macOS
---------------------------- ----------------------------
@@ -540,9 +560,25 @@ try:
pip install --user ocrmypdf pip install --user ocrmypdf
(If the message appears ``Requirement already satisfied: ocrmypdf in...``,
you will need to use ``pip install --user --upgrade ocrmypdf``.)
You should then be able to run ``ocrmypdf --version`` and see that the You should then be able to run ``ocrmypdf --version`` and see that the
latest version was located. latest version was located.
Installing with pipx
====================
Some users may prefer pipx. As with the method above, you will need to
satisfy all non-Python dependencies. Then if pipx is installed, you
can use
.. code-block:: bash
pipx run ocrmypdf
(If not installed, pipx will install first.)
Requirements for pip and HEAD install Requirements for pip and HEAD install
------------------------------------- -------------------------------------
@@ -553,7 +589,7 @@ manager. ``pip`` cannot provide them.
The following versions are required: The following versions are required:
- Python 3.10 or newer - Python 3.10 or newer
- Ghostscript 9.55 or newer - Ghostscript 9.54 or newer
- Tesseract 4.1.1 or newer - Tesseract 4.1.1 or newer
- jbig2enc 0.29 or newer - jbig2enc 0.29 or newer
- pngquant 2.5 or newer - pngquant 2.5 or newer
@@ -605,7 +641,7 @@ environment:
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
Or, to install in `development 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: allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash .. code-block:: bash
@@ -671,7 +707,7 @@ To manually install the ``fish`` completion, copy
Note on 32-bit support Note on 32-bit support
====================== ======================
Many Python libraries no longer 32-bit binary wheels for Linux. This Many Python libraries no longer provide 32-bit binary wheels for Linux. This
includes many of the libraries that OCRmyPDF depends on, such as 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 Pillow. The easiest way to express this to end users is to say we don't
support 32-bit Linux. support 32-bit Linux.
+9 -1
View File
@@ -41,7 +41,15 @@ For all other platforms, you would need to build the JBIG2 encoder from source:
Dependencies include libtoolize and libleptonica, which on Ubuntu systems Dependencies include libtoolize and libleptonica, which on Ubuntu systems
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
as libtool and leptonica-devel. as libtool and leptonica-devel. For this to work, please make sure to install
``autotools``, ``automake``, ``libtool`` and ``leptonica`` first if not already
installed.
.. code-block:: bash
[sudo] apt install autotools-dev automake libtool libleptonica-dev
..
Lossy mode JBIG2 Lossy mode JBIG2
================ ================
+16
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 requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``. ``-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 Gentoo
------ ------
+9
View File
@@ -65,3 +65,12 @@ installation documentation.
If you maintain a Linux distribution that supports 32-bit x86 or ARM, OCRmyPDF 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 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.
+51
View File
@@ -30,6 +30,57 @@ OCRmyPDF typically supports the three most recent Python versions.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg .. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.3.0
=======
- Fixed progress bar not displaying for Ghostscript PDF/A conversion. :issue:`1313`
- Added progress bar for linearization. :issue:`1313`
- If `--rotate-pages-threshold` issued without `--rotate-pages` we now exit with
an error since the user likely intended to use `--rotate-pages`. :issue:`1309`
- If Tesseract hOCR gives an invalid line box, print an error message instead of
exiting with an error. :issue:`1312`
v16.2.0
=======
- Fixed issue 'NoneType' object has no attribute 'get' when optimizing certain PDFs.
:issue:`1293,1271`
- Switched formatting from black to ruff.
- Added support for sending sidecar output to io.BytesIO.
- Added support for converting HEIF/HEIC images (the native image of iPhones and
some other devices) to PDFs, when the appropriate pi-hief library is installed.
This library is marked as a dependency, but maintainers may opt out if needed.
- We now default to downsampling large images that would exceed Tesseract's internal
limits, but only if it cause processing to fail. Previously, this behavior only
occurred if specifically requested on command line. It can still be configured
and disabled. See the --tesseract command line options.
- Added Macports install instructions. Thanks @akierig.
- Improved logging output when an unexpected error occurs while trying to obtain
the version of a third party program.
v16.1.2
=======
- Fixed test suite failure when using Ghostscript 10.3.
- Other minor corrections.
v16.1.1
=======
- Fixed PyPy 3.10 support.
v16.1.0
=======
- Improved hOCR renderer is now default for left to right languages.
- Improved handling of rotated pages. Previously, OCR text might be missing for
pages that were rotated with a /Rotate tag on the page entry.
- Improved handling of cropped pages. Previously, in some cases a page with a
crop box would not have its OCR applied correctly and misalignment between
OCR text and visible text coudl occur.
- Documentation improvements, especially installation instructions for less
common platforms.
v16.0.4 v16.0.4
======= =======
+48 -10
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# SPDX-FileCopyrightText: 2016 findingorder <https://github.com/findingorder> # SPDX-FileCopyrightText: 2016 findingorder <https://github.com/findingorder>
# SPDX-FileCopyrightText: 2024 nilsro <https://github.com/nilsro>
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
"""Example of using ocrmypdf as a library in a script. """Example of using ocrmypdf as a library in a script.
@@ -15,6 +16,10 @@ from __future__ import annotations
import logging import logging
import sys import sys
import os
import posixpath
import shutil
import filecmp
from pathlib import Path from pathlib import Path
import ocrmypdf import ocrmypdf
@@ -22,32 +27,65 @@ import ocrmypdf
# pylint: disable=logging-format-interpolation # pylint: disable=logging-format-interpolation
# pylint: disable=logging-not-lazy # pylint: disable=logging-not-lazy
def filecompare(a, b):
try:
return filecmp.cmp(a, b, shallow=True)
except FileNotFoundError:
return False
script_dir = Path(__file__).parent script_dir = Path(__file__).parent
# set archive_dir to a path for backup original documents. Leave empty if not required.
archive_dir = "/pdfbak"
if len(sys.argv) > 1: if len(sys.argv) > 1:
start_dir = Path(sys.argv[1]) start_dir = Path(sys.argv[1])
else: else:
start_dir = Path('.') start_dir = Path(".")
if len(sys.argv) > 2: if len(sys.argv) > 2:
log_file = Path(sys.argv[2]) log_file = Path(sys.argv[2])
else: else:
log_file = script_dir.with_name('ocr-tree.log') log_file = script_dir.with_name("ocr-tree.log")
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s %(message)s', format="%(asctime)s %(message)s",
filename=log_file, filename=log_file,
filemode='a', filemode="a",
) )
logging.info(f"Start directory {start_dir}")
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default) ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
for filename in start_dir.glob("**/*.py"): for filename in start_dir.glob("**/*.pdf"):
logging.info(f"Processing {filename}") logging.info(f"Processing {filename}")
result = ocrmypdf.ocr(filename, filename, deskew=True) if ocrmypdf.pdfa.file_claims_pdfa(filename)["pass"]:
if result == ocrmypdf.ExitCode.already_done_ocr: logging.info("Skipped document because it already contained text")
logging.error("Skipped document because it already contained text") else:
elif result == ocrmypdf.ExitCode.ok: archive_filename = archive_dir + str(filename)
if len(archive_dir) > 0 and not filecompare(filename, archive_filename):
logging.info(f"Archiving document to {archive_filename}")
try:
shutil.copy2(filename, posixpath.dirname(archive_filename))
except IOError as io_err:
os.makedirs(posixpath.dirname(archive_filename))
shutil.copy2(filename, posixpath.dirname(archive_filename))
try:
result = ocrmypdf.ocr(filename, filename, deskew=True)
logging.info(result)
except ocrmypdf.exceptions.EncryptedPdfError:
logging.info("Skipped document because it is encrypted")
except ocrmypdf.exceptions.PriorOcrFoundError:
logging.info("Skipped document because it already contained text")
except ocrmypdf.exceptions.DigitalSignatureError:
logging.info("Skipped document because it has a digital signature")
except ocrmypdf.exceptions.TaggedPDFError:
logging.info(
"Skipped document because it does not need ocr as it is tagged"
)
except:
logging.error("Unhandled error occured")
logging.info("OCR complete") logging.info("OCR complete")
logging.info(result)
+4 -3
View File
@@ -53,9 +53,10 @@ for dir_name, _subdirs, file_list in os.walk(start_dir):
] ]
logging.info(cmd) logging.info(cmd)
full_path_ocr = os.path.join(dir_name, filename_ocr) full_path_ocr = os.path.join(dir_name, filename_ocr)
with open(filename, 'rb') as input_file, open( with (
full_path_ocr, 'wb' open(filename, 'rb') as input_file,
) as output_file: open(full_path_ocr, 'wb') as output_file,
):
proc = subprocess.run( proc = subprocess.run(
cmd, cmd,
stdin=input_file, stdin=input_file,
+2 -3
View File
@@ -7,7 +7,6 @@
# Do not enable annotations! # Do not enable annotations!
# https://github.com/tiangolo/typer/discussions/598 # https://github.com/tiangolo/typer/discussions/598
# from __future__ import annotations
import json import json
import logging import logging
@@ -131,7 +130,7 @@ def execute_ocrmypdf(
class HandleObserverEvent(PatternMatchingEventHandler): class HandleObserverEvent(PatternMatchingEventHandler):
def __init__( def __init__( # noqa: D107
self, self,
patterns=None, patterns=None,
ignore_patterns=None, ignore_patterns=None,
@@ -191,7 +190,7 @@ def main(
bool, bool,
typer.Option( typer.Option(
envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH', envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH',
help='Create a subdirectory in the output directory for each year and month', help='Create a subdirectory in the output directory for each year/month',
), ),
] = False, ] = False,
on_success_delete: Annotated[ on_success_delete: Annotated[
+18 -32
View File
@@ -12,12 +12,13 @@ readme = "README.md"
license = { text = "MPL-2.0" } license = { text = "MPL-2.0" }
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"Pillow>=10.0.1",
"deprecation>=2.1.0", "deprecation>=2.1.0",
"img2pdf>=0.5", "img2pdf>=0.5",
"packaging>=20", "packaging>=20",
"pdfminer.six>=20220319", "pdfminer.six>=20220319",
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
"pikepdf>=8.10.1", "pikepdf>=8.10.1",
"Pillow>=10.0.1",
"pluggy>=1", "pluggy>=1",
"rich>=13", "rich>=13",
] ]
@@ -60,7 +61,7 @@ test = [
"types-Pillow", "types-Pillow",
"types-humanfriendly", "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"] webservice = ["Flask>=2.0.1"]
[project.scripts] [project.scripts]
@@ -78,29 +79,6 @@ namespaces = false
[tool.distutils.bdist_wheel] [tool.distutils.bdist_wheel]
python-tag = "py310" 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] [tool.coverage.run]
branch = true branch = true
parallel = true parallel = true
@@ -131,7 +109,11 @@ norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
testpaths = ["tests"] testpaths = ["tests"]
addopts = "-n auto" addopts = "-n auto"
markers = ["slow"] markers = ["slow"]
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"] filterwarnings = [
"ignore:.*XMLParser.*:DeprecationWarning",
"ignore:.*ast.NameConstant.*:DeprecationWarning:reportlab",
"ignore:.*distutils.*:DeprecationWarning:libxmp",
]
[tool.mypy] [tool.mypy]
@@ -149,7 +131,10 @@ module = [
ignore_missing_imports = true ignore_missing_imports = true
[tool.ruff] [tool.ruff]
select = [ target-version = "py310"
[tool.ruff.lint]
"select" = [
"D", # pydocstyle "D", # pydocstyle
"E", # pycodestyle "E", # pycodestyle
"W", # pycodestyle "W", # pycodestyle
@@ -157,17 +142,18 @@ select = [
"I001", # isort "I001", # isort
"UP", # pyupgrade "UP", # pyupgrade
] ]
target-version = "py310"
[tool.ruff.isort] [tool.ruff.lint.isort]
known-first-party = ["ocrmypdf"] known-first-party = ["ocrmypdf"]
required-imports = ["from __future__ import annotations"]
[tool.ruff.pydocstyle] [tool.ruff.lint.pydocstyle]
convention = "google" convention = "google"
[tool.ruff.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "D101", "D105"] "docs/conf.py" = ["D100", "D101", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"] "tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"misc/*.py" = ["D103", "D101", "D102"] "misc/*.py" = ["D103", "D101", "D102"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"] "src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
[tool.ruff.format]
quote-style = "preserve"
+2 -2
View File
@@ -7,8 +7,8 @@ from __future__ import annotations
import threading import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterable from collections.abc import Callable, Iterable
from typing import Any, Callable, TypeVar from typing import Any, TypeVar
from ocrmypdf._progressbar import NullProgressBar, ProgressBar from ocrmypdf._progressbar import NullProgressBar, ProgressBar
+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_class = progressbar_class
self.progressbar = None 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): def __call__(self, line):
if not self.progressbar_class: if not self.progressbar_class:
return return
@@ -187,7 +198,8 @@ class GhostscriptFollower:
self.progressbar = self.progressbar_class( self.progressbar = self.progressbar_class(
total=self.count, desc="PDF/A conversion", unit='page' 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: else:
if self.re_page.match(line.strip()): if self.re_page.match(line.strip()):
self.progressbar.update() self.progressbar.update()
@@ -265,7 +277,10 @@ def generate_pdfa(
) )
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try: 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( p = run_polling_stderr(
args_gs, args_gs,
stdout=output, stdout=output,
@@ -274,7 +289,7 @@ def generate_pdfa(
text=True, text=True,
encoding='utf-8', encoding='utf-8',
errors='replace', errors='replace',
callback=GhostscriptFollower(progressbar_class), callback=pbar,
) )
except CalledProcessError as e: except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create # Ghostscript does not change return code when it fails to create
+2 -1
View File
@@ -220,7 +220,8 @@ def get_deskew(
def tesseract_log_output(stream: bytes) -> None: def tesseract_log_output(stream: bytes) -> None:
tlog = TesseractLoggerAdapter( 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: if not stream:
+2 -4
View File
@@ -8,19 +8,17 @@ from __future__ import annotations
import logging import logging
import os import os
import shlex import shlex
import sys
from collections.abc import Iterator from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from subprocess import PIPE, STDOUT from subprocess import PIPE, STDOUT
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Union
from packaging.version import Version from packaging.version import Version
from PIL import Image from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError from ocrmypdf.exceptions import SubprocessOutputError
from ocrmypdf.subprocess import get_version, run from ocrmypdf.subprocess import get_version, run
# unpaper documentation: # unpaper documentation:
@@ -29,7 +27,7 @@ from ocrmypdf.subprocess import get_version, run
UNPAPER_IMAGE_PIXEL_LIMIT = 256 * 1024 * 1024 UNPAPER_IMAGE_PIXEL_LIMIT = 256 * 1024 * 1024
DecFloat = Union[Decimal, float] DecFloat = Decimal | float
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
+2 -3
View File
@@ -289,9 +289,8 @@ class OcrGrafter:
# Translate the text so it is centered at (0, 0), rotate it there, adjust # Translate the text so it is centered at (0, 0), rotate it there, adjust
# for a size different between initial and text PDF, then untranslate, and # for a size different between initial and text PDF, then untranslate, and
# finally move the lower left corner to match the mediabox. All transforms # finally move the lower left corner to match the mediabox.
# must be premultiplied so they are applied in reverse order here. ctm = translate @ rotate @ scale @ untranslate @ corner
ctm = corner @ untranslate @ scale @ rotate @ translate
log.debug("Grafting with ctm %r", ctm) log.debug("Grafting with ctm %r", ctm)
base_resources = _ensure_dictionary(base_page.obj, Name.Resources) base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
+2 -2
View File
@@ -93,8 +93,8 @@ class PageContext:
state = self.__dict__.copy() state = self.__dict__.copy()
state['options'] = copy(self.options) state['options'] = copy(self.options)
if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)): if not isinstance(state['options'].input_file, str | bytes | os.PathLike):
state['options'].input_file = 'stream' state['options'].input_file = 'stream'
if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)): if not isinstance(state['options'].output_file, str | bytes | os.PathLike):
state['options'].output_file = 'stream' state['options'].output_file = 'stream'
return state return state
+37 -7
View File
@@ -153,22 +153,52 @@ def _set_language(pdf: Pdf, languages: list[str]):
pdf.Root.Lang = iso639_2 pdf.Root.Lang = iso639_2
class MetadataProgress:
def __init__(self, progressbar_class):
self.progressbar_class = progressbar_class
self.progressbar = self.progressbar_class(
total=100, desc="Linearizing", unit='%'
)
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( def metadata_fixup(
working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any] working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
) -> Path: ) -> 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 Also report on metadata in the input file that was not retained during
PDF/A conversion. conversion.
""" """
output_file = context.get_path('metafix.pdf') output_file = context.get_path('metafix.pdf')
options = context.options 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) as pbar,
):
docinfo = get_docinfo(original, context) docinfo = get_docinfo(original, context)
with original.open_metadata( with (
set_pikepdf_as_editor=False, update_docinfo=False, strict=False original.open_metadata(
) as meta_original, pdf.open_metadata() as meta_pdf: set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original,
pdf.open_metadata() as meta_pdf,
):
meta_pdf.load_from_docinfo( meta_pdf.load_from_docinfo(
docinfo, delete_missing=False, raise_failure=False docinfo, delete_missing=False, raise_failure=False
) )
@@ -179,6 +209,6 @@ def metadata_fixup(
report_on_metadata(options, meta_missing) report_on_metadata(options, meta_missing)
_set_language(pdf, options.languages) _set_language(pdf, options.languages)
pdf.save(output_file, **pdf_save_settings) pdf.save(output_file, progress=pbar, **pdf_save_settings)
return output_file return output_file
+82 -8
View File
@@ -12,8 +12,9 @@ import re
import sys import sys
from collections.abc import Iterable, Iterator, Sequence from collections.abc import Iterable, Iterator, Sequence
from contextlib import suppress from contextlib import suppress
from io import BytesIO
from pathlib import Path from pathlib import Path
from shutil import copyfileobj, copystat from shutil import copyfileobj
from typing import Any, BinaryIO, TypeVar, cast from typing import Any, BinaryIO, TypeVar, cast
import img2pdf import img2pdf
@@ -40,12 +41,23 @@ from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
from ocrmypdf.pluginspec import OrientationConfidence from ocrmypdf.pluginspec import OrientationConfidence
try:
from pi_heif import register_heif_opener
except ImportError:
def register_heif_opener():
pass
T = TypeVar("T") T = TypeVar("T")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
VECTOR_PAGE_DPI = 400 VECTOR_PAGE_DPI = 400
register_heif_opener()
def triage_image_file(input_file: Path, output_file: Path, options) -> None: def triage_image_file(input_file: Path, output_file: Path, options) -> None:
"""Triage the input image file. """Triage the input image file.
@@ -130,7 +142,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
""" """
with open(input_file, 'rb') as f: with open(input_file, 'rb') as f:
signature = f.read(search_window) signature = f.read(search_window)
m = re.search(br'%PDF-(\d\.\d)', signature) m = re.search(rb'%PDF-(\d\.\d)', signature)
if m: if m:
return m.group(1).decode('ascii') return m.group(1).decode('ascii')
return '' return ''
@@ -713,19 +725,29 @@ def create_pdf_page_from_image(
pageinfo = page_context.pageinfo pageinfo = page_context.pageinfo
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches) pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
effective_rotation = (pageinfo.rotation - orientation_correction) % 360 effective_rotation = (pageinfo.rotation - orientation_correction) % 360
if effective_rotation % 180 == 90: swap_axis = effective_rotation % 180 == 90
if swap_axis:
pagesize = pagesize[1], pagesize[0] pagesize = pagesize[1], pagesize[0]
# This create a single page PDF # Create a new single page PDF to hold
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf: bio = BytesIO()
with open(image, 'rb') as imfile:
log.debug('convert') log.debug('convert')
layout_fun = img2pdf.get_layout_fun(pagesize) layout_fun = img2pdf.get_layout_fun(pagesize)
img2pdf.convert( img2pdf.convert(
imfile, layout_fun=layout_fun, outputstream=pdf, **IMG2PDF_KWARGS imfile,
layout_fun=layout_fun,
outputstream=bio,
engine=img2pdf.Engine.pikepdf,
rotation=img2pdf.Rotation.ifvalid,
) )
log.debug('convert done') log.debug('convert done')
# img2pdf does not generate boxes correctly, so we fix them
bio.seek(0)
fix_pagepdf_boxes(bio, output_file, page_context, swap_axis=swap_axis)
output_file = page_context.plugin_manager.hook.filter_pdf_page( output_file = page_context.plugin_manager.hook.filter_pdf_page(
page=page_context, image_filename=image, output_pdf=output_file page=page_context, image_filename=image, output_pdf=output_file
) )
@@ -756,7 +778,9 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
font=Courier(), font=Courier(),
) )
HocrTransform( HocrTransform(
hocr_filename=hocr, dpi=dpi.to_scalar(), **debug_kwargs # square hocr_filename=hocr,
dpi=dpi.to_scalar(),
**debug_kwargs, # square
).to_pdf( ).to_pdf(
out_filename=output_file, out_filename=output_file,
image_filename=None, image_filename=None,
@@ -780,7 +804,57 @@ def ocr_engine_textonly_pdf(
output_text=output_text, output_text=output_text,
options=options, options=options,
) )
return (output_pdf, output_text) return output_pdf, output_text
def _offset_rect(rect: tuple[float, float, float, float], offset: tuple[float, float]):
"""Offset a rectangle by a given amount."""
return (
rect[0] + offset[0],
rect[1] + offset[1],
rect[2] + offset[0],
rect[3] + offset[1],
)
def fix_pagepdf_boxes(
infile: Path | BinaryIO,
out_file: Path,
page_context: PageContext,
swap_axis: bool = False,
) -> Path:
"""Fix the bounding boxes in a single page PDF.
The single page PDF is created with a normal MediaBox with its lower left corner
at (0, 0). infile is the single page PDF. page_context.mediabox has the original
file's mediabox, which may have a different origin. We needto adjust the other
boxes in the single page PDF to match the effect they had on the original page.
When correcting page rotation, we create a single page PDF that is correctly
rotated instead of an incorrectly rotated and then setting page.Rotate on it.
If rotation is either 90 or 270 degrees, then this function can be called
with swap_axis to swap the X and Y coordinates of all the boxes.
We are not concerned with solving degenerate cases where the boxes overlap or
or express invalid rectangles. We merely pass the boxes, producing a
transformation equivalent to the change made by constructing a new page image.
"""
with pikepdf.open(infile) as pdf:
for page in pdf.pages:
# page.BleedBox = page_context.pageinfo.bleedbox
# page.ArtBox = page_context.pageinfo.artbox
mediabox = page_context.pageinfo.mediabox
offset = mediabox[0], mediabox[1]
cropbox = _offset_rect(page_context.pageinfo.cropbox, offset)
trimbox = _offset_rect(page_context.pageinfo.trimbox, offset)
if swap_axis:
cropbox = cropbox[1], cropbox[0], cropbox[3], cropbox[2]
trimbox = trimbox[1], trimbox[0], trimbox[3], trimbox[2]
page.CropBox = cropbox
page.TrimBox = trimbox
pdf.save(out_file)
return pdf
def generate_postscript_stub(context: PdfContext) -> Path: def generate_postscript_stub(context: PdfContext) -> Path:
+2 -2
View File
@@ -11,13 +11,13 @@ import os
import shutil import shutil
import sys import sys
import threading import threading
from collections.abc import Sequence from collections.abc import Callable, Sequence
from concurrent.futures.process import BrokenProcessPool from concurrent.futures.process import BrokenProcessPool
from concurrent.futures.thread import BrokenThreadPool from concurrent.futures.thread import BrokenThreadPool
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Callable, NamedTuple, cast from typing import NamedTuple, cast
import PIL import PIL
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline.""" """Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
+7 -8
View File
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline.""" """Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
@@ -80,7 +79,6 @@ def _exec_page_sync(page_context: PageContext) -> PageResult:
page_context page_context
) )
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out) ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
return PageResult( return PageResult(
pageno=page_context.pageno, pageno=page_context.pageno,
pdf_page_from_image=pdf_page_from_image_out, pdf_page_from_image=pdf_page_from_image_out,
@@ -156,12 +154,13 @@ def _run_pipeline(
options: argparse.Namespace, options: argparse.Namespace,
plugin_manager: OcrmypdfPluginManager, plugin_manager: OcrmypdfPluginManager,
) -> ExitCode: ) -> ExitCode:
with manage_work_folder( with (
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")), manage_work_folder(
retain=options.keep_temporary_files, work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
print_location=options.keep_temporary_files, retain=options.keep_temporary_files,
) as work_folder, manage_debug_log_handler( print_location=options.keep_temporary_files,
options=options, work_folder=work_folder ) as work_folder,
manage_debug_log_handler(options=options, work_folder=work_folder),
): ):
executor = setup_pipeline(options, plugin_manager) executor = setup_pipeline(options, plugin_manager)
check_requested_output_file(options) check_requested_output_file(options)
-1
View File
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline.""" """Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
+11 -5
View File
@@ -66,7 +66,7 @@ class ProgressBar(Protocol):
def __exit__(self, *args): def __exit__(self, *args):
"""Exit a progress bar context.""" """Exit a progress bar context."""
def update(self, n=1): def update(self, n=1, *, completed=None):
"""Update the progress bar by an increment. """Update the progress bar by an increment.
For use within a progress bar context. For use within a progress bar context.
@@ -85,7 +85,7 @@ class NullProgressBar:
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
return False return False
def update(self, _arg=None): def update(self, _arg=None, *, completed=None):
return return
@@ -103,6 +103,7 @@ class RichProgressBar:
disable: bool = False, disable: bool = False,
**kwargs, **kwargs,
): ):
self._entered = False
self.progress = Progress( self.progress = Progress(
TextColumn( TextColumn(
"[progress.description]{task.description}", "[progress.description]{task.description}",
@@ -130,6 +131,7 @@ class RichProgressBar:
def __enter__(self): def __enter__(self):
self.progress.start() self.progress.start()
self._entered = True
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
@@ -137,6 +139,10 @@ class RichProgressBar:
self.progress.stop() self.progress.stop()
return False return False
def update(self, value=None): def update(self, n=1, *, completed=None):
advance = self.unit_scale if value is None else value assert self._entered, "Progress bar must be entered before updating"
self.progress.update(self.progress_bar, advance=advance) 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)
+6 -8
View File
@@ -20,6 +20,7 @@ import pikepdf
import PIL import PIL
from pluggy import PluginManager from pluggy import PluginManager
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._exec import unpaper from ocrmypdf._exec import unpaper
from ocrmypdf.exceptions import ( from ocrmypdf.exceptions import (
BadArgsError, BadArgsError,
@@ -30,17 +31,9 @@ from ocrmypdf.exceptions import (
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
from ocrmypdf.subprocess import check_external_program from ocrmypdf.subprocess import check_external_program
# -------------
# External dependencies
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# --------
def check_platform() -> None: def check_platform() -> None:
if sys.maxsize <= 2**32: # pragma: no cover if sys.maxsize <= 2**32: # pragma: no cover
log.warning( log.warning(
@@ -130,6 +123,11 @@ def check_options_preprocessing(options: Namespace) -> None:
options.clean = True options.clean = True
if options.unpaper_args and not options.clean: if options.unpaper_args and not options.clean:
raise BadArgsError("--clean is required for --unpaper-args") 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: if options.clean:
check_external_program( check_external_program(
program='unpaper', program='unpaper',
+13 -8
View File
@@ -14,7 +14,7 @@ from collections.abc import Iterable, Sequence
from enum import IntEnum from enum import IntEnum
from io import IOBase from io import IOBase
from pathlib import Path from pathlib import Path
from typing import AnyStr, BinaryIO, Union from typing import AnyStr, BinaryIO
from warnings import warn from warnings import warn
import pluggy import pluggy
@@ -28,8 +28,8 @@ from ocrmypdf._validation import check_options
from ocrmypdf.cli import ArgumentParser, get_parser from ocrmypdf.cli import ArgumentParser, get_parser
from ocrmypdf.helpers import is_iterable_notstr from ocrmypdf.helpers import is_iterable_notstr
StrPath = Union[Path, AnyStr] StrPath = Path | AnyStr
PathOrIO = Union[BinaryIO, StrPath] PathOrIO = BinaryIO | StrPath
# Installing plugins affects the global state of the Python interpreter, # Installing plugins affects the global state of the Python interpreter,
# so we need to use a lock to prevent multiple threads from installing # so we need to use a lock to prevent multiple threads from installing
@@ -169,7 +169,7 @@ def _kwargs_to_cmdline(
# We have a parameter # We have a parameter
cmdline.append(f"--{cmd_style_arg}") cmdline.append(f"--{cmd_style_arg}")
if isinstance(val, (int, float)): if isinstance(val, int | float):
cmdline.append(str(val)) cmdline.append(str(val))
elif isinstance(val, str): elif isinstance(val, str):
cmdline.append(val) cmdline.append(val)
@@ -201,14 +201,17 @@ def create_options(
defer_kwargs={'progress_bar', 'plugins', 'parser', 'input_file', 'output_file'}, defer_kwargs={'progress_bar', 'plugins', 'parser', 'input_file', 'output_file'},
**kwargs, **kwargs,
) )
if isinstance(input_file, (BinaryIO, IOBase)): if isinstance(input_file, BinaryIO | IOBase):
cmdline.append('stream://input_file') cmdline.append('stream://input_file')
else: else:
cmdline.append(os.fspath(input_file)) cmdline.append(os.fspath(input_file))
if isinstance(output_file, (BinaryIO, IOBase)): if isinstance(output_file, BinaryIO | IOBase):
cmdline.append('stream://output_file') cmdline.append('stream://output_file')
else: else:
cmdline.append(os.fspath(output_file)) 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() parser.enable_api_mode()
options = parser.parse_args(cmdline) options = parser.parse_args(cmdline)
@@ -219,6 +222,8 @@ def create_options(
options.input_file = input_file options.input_file = input_file
if options.output_file == 'stream://output_file': if options.output_file == 'stream://output_file':
options.output_file = output_file options.output_file = output_file
if options.sidecar == 'stream://sidecar':
options.sidecar = kwargs['sidecar']
return options return options
@@ -230,7 +235,7 @@ def ocr( # noqa: D417
language: Iterable[str] | None = None, language: Iterable[str] | None = None,
image_dpi: int | None = None, image_dpi: int | None = None,
output_type: str | None = None, output_type: str | None = None,
sidecar: StrPath | None = None, sidecar: PathOrIO | None = None,
jobs: int | None = None, jobs: int | None = None,
use_threads: bool | None = None, use_threads: bool | None = None,
title: str | None = None, title: str | None = None,
@@ -343,7 +348,7 @@ def ocr( # noqa: D417
if not plugins: if not plugins:
plugins = [] plugins = []
elif isinstance(plugins, (str, Path)): elif isinstance(plugins, str | Path):
plugins = [plugins] plugins = [plugins]
else: else:
plugins = list(plugins) plugins = list(plugins)
+14 -9
View File
@@ -12,10 +12,10 @@ import queue
import signal import signal
import sys import sys
import threading import threading
from collections.abc import Iterable from collections.abc import Callable, Iterable
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from contextlib import suppress from contextlib import suppress
from typing import Callable, Union from typing import Union
from rich.console import Console as RichConsole from rich.console import Console as RichConsole
@@ -25,8 +25,10 @@ from ocrmypdf._progressbar import RichProgressBar
from ocrmypdf.exceptions import InputFileError from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers from ocrmypdf.helpers import remove_all_log_handlers
FuturesExecutorClass = Union[type[ThreadPoolExecutor], type[ProcessPoolExecutor]] FuturesExecutorClass = Union[ # noqa: UP007
Queue = Union[multiprocessing.Queue, queue.Queue] type[ThreadPoolExecutor], type[ProcessPoolExecutor]
]
Queue = Union[multiprocessing.Queue, queue.Queue] # noqa: UP007
UserInit = Callable[[], None] UserInit = Callable[[], None]
WorkerInit = Callable[[Queue, UserInit, int], None] WorkerInit = Callable[[Queue, UserInit, int], None]
@@ -128,11 +130,14 @@ class StandardExecutor(Executor):
listener = threading.Thread(target=log_listener, args=(log_queue,)) listener = threading.Thread(target=log_listener, args=(log_queue,))
listener.start() listener.start()
with self.pbar_class(**progress_kwargs) as pbar, executor_class( with (
max_workers=max_workers, self.pbar_class(**progress_kwargs) as pbar,
initializer=initializer, executor_class(
initargs=(log_queue, worker_initializer, logging.getLogger("").level), max_workers=max_workers,
) as executor: initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor,
):
futures = [executor.submit(task, *args) for args in task_arguments] futures = [executor.submit(task, *args) for args in task_arguments]
try: try:
for future in as_completed(futures): for future in as_completed(futures):
@@ -8,7 +8,5 @@ from ocrmypdf import hookimpl
@hookimpl @hookimpl
def filter_pdf_page( def filter_pdf_page(page, image_filename, output_pdf): # pylint: disable=unused-argument
page, image_filename, output_pdf
): # pylint: disable=unused-argument
return output_pdf return output_pdf
@@ -2,9 +2,9 @@
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
"""Built-in plugin to implement OCR using Tesseract.""" """Built-in plugin to implement OCR using Tesseract."""
from __future__ import annotations from __future__ import annotations
import argparse
import logging import logging
import os import os
@@ -94,7 +94,8 @@ def add_options(parser):
) )
tess.add_argument( tess.add_argument(
'--tesseract-downsample-large-images', '--tesseract-downsample-large-images',
action='store_true', action=argparse.BooleanOptionalAction,
default=True,
help=( help=(
"Downsample large images before OCR. Tesseract has an upper limit on the " "Downsample large images before OCR. Tesseract has an upper limit on the "
"size images it will support. If this argument is given, OCRmyPDF will " "size images it will support. If this argument is given, OCRmyPDF will "
@@ -146,7 +147,11 @@ def check_options(options):
# Decide on what renderer to use # Decide on what renderer to use
if options.pdf_renderer == 'auto': if options.pdf_renderer == 'auto':
options.pdf_renderer = 'sandwich' if {'ara', 'heb', 'fas', 'per'} & set(options.languages):
log.info("Using sandwich renderer since there is an RTL language")
options.pdf_renderer = 'sandwich'
else:
options.pdf_renderer = 'hocr'
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0: if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
log.warning( log.warning(
@@ -216,7 +221,7 @@ class TesseractOcrEngine(OcrEngine):
@staticmethod @staticmethod
def creator_tag(options): 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()}" return f"Tesseract OCR{tag} {TesseractOcrEngine.version()}"
def __str__(self): def __str__(self):
+5 -4
View File
@@ -6,9 +6,10 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from collections.abc import Mapping from collections.abc import Callable, Mapping
from typing import Any, Callable, TypeVar 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 PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION from ocrmypdf._version import __version__ as _VERSION
@@ -390,7 +391,7 @@ Online documentation is located at:
action='store', action='store',
type=numeric(float, 0), type=numeric(float, 0),
metavar='MPixels', metavar='MPixels',
help="Set maximum number of pixels to unpack before treating an image as a " help="Set maximum number of megapixels to unpack before treating an image as a "
"decompression bomb", "decompression bomb",
default=250.0, default=250.0,
) )
@@ -403,7 +404,7 @@ Online documentation is located at:
) )
advanced.add_argument( advanced.add_argument(
'--rotate-pages-threshold', '--rotate-pages-threshold',
default=14.0, default=DEFAULT_ROTATE_PAGES_THRESHOLD,
type=numeric(float, 0, 1000), type=numeric(float, 0, 1000),
metavar='CONFIDENCE', metavar='CONFIDENCE',
help="Only rotate pages when confidence is above this value (arbitrary " help="Only rotate pages when confidence is above this value (arbitrary "
+1 -2
View File
@@ -20,13 +20,12 @@ from __future__ import annotations
import logging import logging
import logging.handlers import logging.handlers
import signal import signal
from collections.abc import Iterable, Iterator from collections.abc import Callable, Iterable, Iterator
from contextlib import suppress from contextlib import suppress
from enum import Enum, auto from enum import Enum, auto
from itertools import islice, repeat, takewhile, zip_longest from itertools import islice, repeat, takewhile, zip_longest
from multiprocessing import Pipe, Process from multiprocessing import Pipe, Process
from multiprocessing.connection import Connection, wait from multiprocessing.connection import Connection, wait
from typing import Callable
from ocrmypdf import Executor, hookimpl from ocrmypdf import Executor, hookimpl
from ocrmypdf._concurrent import NullProgressBar from ocrmypdf._concurrent import NullProgressBar
+3 -2
View File
@@ -10,7 +10,7 @@ import multiprocessing
import os import os
import shutil import shutil
import warnings import warnings
from collections.abc import Iterable, Sequence from collections.abc import Callable, Iterable, Sequence
from contextlib import suppress from contextlib import suppress
from decimal import Decimal from decimal import Decimal
from io import StringIO from io import StringIO
@@ -19,13 +19,13 @@ from pathlib import Path
from statistics import harmonic_mean from statistics import harmonic_mean
from typing import ( from typing import (
Any, Any,
Callable,
Generic, Generic,
TypeVar, TypeVar,
) )
import img2pdf import img2pdf
import pikepdf import pikepdf
from deprecation import deprecated
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -136,6 +136,7 @@ class Resolution(Generic[T]):
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y) return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
@deprecated(deprecated_in='15.4.0')
class NeverRaise(Exception): class NeverRaise(Exception):
"""An exception that is never raised.""" """An exception that is never raised."""
+7 -8
View File
@@ -84,7 +84,6 @@ class HocrTransform:
debug_render_options: DebugRenderOptions | None = None, debug_render_options: DebugRenderOptions | None = None,
): ):
"""Initialize the HocrTransform object.""" """Initialize the HocrTransform object."""
if debug: if debug:
log.warning("Use debug_render_options instead", DeprecationWarning) log.warning("Use debug_render_options instead", DeprecationWarning)
self.render_options = DebugRenderOptions( self.render_options = DebugRenderOptions(
@@ -118,15 +117,12 @@ class HocrTransform:
# Stop after first div that has page coordinates # Stop after first div that has page coordinates
break 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.""" """Return the textual content of the element and its children."""
text = '' text = element.text if element.text is not None else ''
if element.text is not None:
text += element.text
for child in element: for child in element:
text += self._get_element_text(child) text += self._get_element_text(child)
if element.tail is not None: text += element.tail if element.tail is not None else ''
text += element.tail
return text return text
@classmethod @classmethod
@@ -287,7 +283,10 @@ class HocrTransform:
line_box = self.element_coordinates(line) line_box = self.element_coordinates(line)
if not line_box: if not line_box:
return 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) 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 https://www.loc.gov/standards/iso639-2/ascii_8bits.html
""" """
from typing import NamedTuple from typing import NamedTuple
class ISOCodeData(NamedTuple): class ISOCodeData(NamedTuple):
"""Data for a single ISO 639 code.""" """Data for a single ISO 639 code."""
alt: str alt: str
alpha_2: str alpha_2: str
english: str english: str
@@ -168,8 +168,10 @@ ISO_639_3 = {
'chu': ISOCodeData( 'chu': ISOCodeData(
'', '',
'cu', '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", "slavon d'église; vieux slave; slavon liturgique; vieux bulgare",
), ),
'chv': ISOCodeData('', 'cv', 'Chuvash', 'tchouvache'), 'chv': ISOCodeData('', 'cv', 'Chuvash', 'tchouvache'),
+10 -4
View File
@@ -3,7 +3,6 @@
"""Post-processing image optimization of OCR PDFs.""" """Post-processing image optimization of OCR PDFs."""
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -11,11 +10,10 @@ import sys
import tempfile import tempfile
import threading import threading
from collections import defaultdict from collections import defaultdict
from collections.abc import Iterator, MutableSet, Sequence from collections.abc import Callable, Iterator, MutableSet, Sequence
from os import fspath from os import fspath
from pathlib import Path from pathlib import Path
from typing import Any, Callable, NamedTuple, NewType from typing import Any, NamedTuple, NewType
from warnings import warn
from zlib import compress from zlib import compress
import img2pdf import img2pdf
@@ -91,6 +89,7 @@ def extract_image_filter(
if ( if (
len(pim.filter_decodeparms) == 2 len(pim.filter_decodeparms) == 2
and first_filtdp[0] == Name.FlateDecode and first_filtdp[0] == Name.FlateDecode
and first_filtdp[1] is not None
and first_filtdp[1].get(Name.Predictor, 1) == 1 and first_filtdp[1].get(Name.Predictor, 1) == 1
and second_filtdp[0] == Name.DCTDecode and second_filtdp[0] == Name.DCTDecode
and not second_filtdp[1] and not second_filtdp[1]
@@ -155,6 +154,13 @@ def extract_image_jbig2(
with imgname.open('wb') as f: with imgname.open('wb') as f:
ext = pim.extract_to(stream=f) ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext)) imgname.rename(imgname.with_suffix(ext))
except NotImplementedError as e:
if '/Decode' in str(e):
log.debug(
f"xref {xref}: skipping image with unsupported Decode table"
)
return None
raise
except UnsupportedImageTypeError: except UnsupportedImageTypeError:
return None return None
finally: finally:
+1 -1
View File
@@ -90,7 +90,7 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
icc: ICC identifier such as 'sRGB' icc: ICC identifier such as 'sRGB'
References: References:
Adobe PDFMARK Reference: 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': if icc != 'sRGB':
raise NotImplementedError("Only supporting sRGB") raise NotImplementedError("Only supporting sRGB")
+32 -11
View File
@@ -10,9 +10,8 @@ import atexit
import logging import logging
import re import re
import statistics import statistics
import sys
from collections import defaultdict from collections import defaultdict
from collections.abc import Container, Iterable, Iterator, Mapping, Sequence from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager from contextlib import contextmanager
from decimal import Decimal from decimal import Decimal
from enum import Enum, auto from enum import Enum, auto
@@ -20,7 +19,7 @@ from functools import partial
from math import hypot, inf, isclose from math import hypot, inf, isclose
from os import PathLike from os import PathLike
from pathlib import Path from pathlib import Path
from typing import Callable, NamedTuple from typing import NamedTuple
from warnings import warn from warnings import warn
from pdfminer.layout import LTPage, LTTextBox from pdfminer.layout import LTPage, LTTextBox
@@ -240,7 +239,13 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
# to do. Just pretend nothing happened, keep calm and carry on. # to do. Just pretend nothing happened, keep calm and carry on.
warn("PDF graphics stack underflowed - PDF may be malformed") warn("PDF graphics stack underflowed - PDF may be malformed")
elif operator == 'cm': 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': elif operator == 'Do':
image_name = operands[0] image_name = operands[0]
settings = XobjectSettings( settings = XobjectSettings(
@@ -854,6 +859,12 @@ class PageInfo:
width_pt = mediabox[2] - mediabox[0] width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1] height_pt = mediabox[3] - mediabox[1]
# self._artbox = [float(d) for d in page.artbox.as_list()]
# self._bleedbox = [float(d) for d in page.bleedbox.as_list()]
self._cropbox = [float(d) for d in page.cropbox.as_list()]
self._mediabox = [float(d) for d in page.mediabox.as_list()]
self._trimbox = [float(d) for d in page.trimbox.as_list()]
check_this_page = pageno in check_pages check_this_page = pageno in check_pages
if check_this_page and detailed_analysis: if check_this_page and detailed_analysis:
@@ -970,6 +981,21 @@ class PageInfo:
else: else:
raise ValueError("rotation must be a cardinal angle") raise ValueError("rotation must be a cardinal angle")
@property
def cropbox(self) -> FloatRect:
"""Return cropbox of page in PDF coordinates."""
return self._cropbox
@property
def mediabox(self) -> FloatRect:
"""Return mediabox of page in PDF coordinates."""
return self._mediabox
@property
def trimbox(self) -> FloatRect:
"""Return trimbox of page in PDF coordinates."""
return self._trimbox
@property @property
def images(self) -> list[ImageInfo]: def images(self) -> list[ImageInfo]:
"""Return images.""" """Return images."""
@@ -1039,12 +1065,7 @@ class PageInfo:
weights = [area / total_drawn_area for area in image_areas] weights = [area / total_drawn_area for area in image_areas]
# Calculate harmonic mean of DPIs weighted by area # Calculate harmonic mean of DPIs weighted by area
if sys.version_info >= (3, 10): weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
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) max_dpi = max(image_dpis)
dpi_average_max_ratio = weighted_dpi / max_dpi dpi_average_max_ratio = weighted_dpi / max_dpi
@@ -1155,7 +1176,7 @@ class PdfInfo:
@property @property
def filename(self) -> str | Path: def filename(self) -> str | Path:
"""Return filename of PDF.""" """Return filename of PDF."""
if not isinstance(self._infile, (str, Path)): if not isinstance(self._infile, str | Path):
raise NotImplementedError("can't get filename from stream") raise NotImplementedError("can't get filename from stream")
return self._infile return self._infile
+2 -2
View File
@@ -5,12 +5,12 @@
from __future__ import annotations from __future__ import annotations
import re import re
from collections.abc import Mapping from collections.abc import Iterator, Mapping
from contextlib import contextmanager from contextlib import contextmanager
from math import copysign from math import copysign
from os import PathLike from os import PathLike
from pathlib import Path from pathlib import Path
from typing import Any, Iterator from typing import Any
from unittest.mock import patch from unittest.mock import patch
import pdfminer import pdfminer
-1
View File
@@ -3,7 +3,6 @@
"""Utilities to measure OCR quality.""" """Utilities to measure OCR quality."""
from __future__ import annotations from __future__ import annotations
import re import re
+3 -3
View File
@@ -8,12 +8,11 @@ import logging
import os import os
import re import re
import sys import sys
from collections.abc import Mapping, Sequence from collections.abc import Callable, Mapping, Sequence
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
from subprocess import run as subprocess_run from subprocess import run as subprocess_run
from typing import Callable, Union
from packaging.version import Version from packaging.version import Version
@@ -23,7 +22,7 @@ from ocrmypdf.exceptions import MissingDependencyError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
Args = Sequence[Union[Path, str]] Args = Sequence[Path | str]
OsEnviron = os._Environ # pylint: disable=protected-access OsEnviron = os._Environ # pylint: disable=protected-access
@@ -172,6 +171,7 @@ def get_version(
) from e ) from e
except CalledProcessError as e: except CalledProcessError as e:
if e.returncode != 0: if e.returncode != 0:
log.exception(e)
raise MissingDependencyError( raise MissingDependencyError(
f"Ran program '{program}' but it exited with an error:\n{e.output}" f"Ran program '{program}' but it exited with an error:\n{e.output}"
) from e ) from e
+3 -8
View File
@@ -9,18 +9,13 @@ import os
import re import re
import shutil import shutil
import sys import sys
from collections.abc import Iterable, Iterator from collections.abc import Callable, Iterable, Iterator
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
from typing import Any, Callable, TypeVar from typing import Any, TypeAlias, TypeVar
from packaging.version import InvalidVersion, Version from packaging.version import InvalidVersion, Version
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias # pragma: no cover
if sys.platform == 'win32': if sys.platform == 'win32':
# mypy understands 'if sys.platform' better than try/except ModuleNotFoundError # mypy understands 'if sys.platform' better than try/except ModuleNotFoundError
import winreg # pylint: disable=import-error import winreg # pylint: disable=import-error
@@ -84,7 +79,7 @@ def registry_path_ghostscript(env=None) -> Iterator[Path]:
registry_subkeys(k), key=ghostscript_version_key, default=(0, 0, 0) registry_subkeys(k), key=ghostscript_version_key, default=(0, 0, 0)
) )
with winreg.OpenKey( 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: ) as k:
for _, gs_path, _ in registry_values(k): for _, gs_path, _ in registry_values(k):
yield Path(gs_path) / 'bin' yield Path(gs_path) / 'bin'
-1
View File
@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from subprocess import CalledProcessError from subprocess import CalledProcessError
from unittest.mock import patch from unittest.mock import patch
+12 -8
View File
@@ -169,22 +169,25 @@ class CacheOcrEngine(TesseractOcrEngine):
@staticmethod @staticmethod
def get_orientation(input_file, options): def get_orientation(input_file, options):
with CacheOcrEngine.lock, patch( with (
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options) CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
): ):
return TesseractOcrEngine.get_orientation(input_file, options) return TesseractOcrEngine.get_orientation(input_file, options)
@staticmethod @staticmethod
def get_deskew(input_file, options) -> float: def get_deskew(input_file, options) -> float:
with CacheOcrEngine.lock, patch( with (
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options) CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
): ):
return TesseractOcrEngine.get_deskew(input_file, options) return TesseractOcrEngine.get_deskew(input_file, options)
@staticmethod @staticmethod
def generate_hocr(input_file, output_hocr, output_text, options): def generate_hocr(input_file, output_hocr, output_text, options):
with CacheOcrEngine.lock, patch( with (
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options) CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
): ):
TesseractOcrEngine.generate_hocr( TesseractOcrEngine.generate_hocr(
input_file, output_hocr, output_text, options input_file, output_hocr, output_text, options
@@ -192,8 +195,9 @@ class CacheOcrEngine(TesseractOcrEngine):
@staticmethod @staticmethod
def generate_pdf(input_file, output_pdf, output_text, options): def generate_pdf(input_file, output_pdf, output_text, options):
with CacheOcrEngine.lock, patch( with (
'ocrmypdf._exec.tesseract.run', new=partial(cached_run, options) CacheOcrEngine.lock,
patch('ocrmypdf._exec.tesseract.run', new=partial(cached_run, options)),
): ):
TesseractOcrEngine.generate_pdf( TesseractOcrEngine.generate_pdf(
input_file, output_pdf, output_text, options input_file, output_pdf, output_text, options
+4 -3
View File
@@ -72,9 +72,10 @@ class FixedRotateNoopOcrEngine(OcrEngine):
@staticmethod @staticmethod
def generate_hocr(input_file, output_hocr, output_text, options): def generate_hocr(input_file, output_hocr, output_text, options):
with Image.open(input_file) as im, open( with (
output_hocr, 'w', encoding='utf-8' Image.open(input_file) as im,
) as f: open(output_hocr, 'w', encoding='utf-8') as f,
):
w, h = im.size w, h = im.size
f.write(HOCR_TEMPLATE.format(str(w), str(h))) f.write(HOCR_TEMPLATE.format(str(w), str(h)))
with open(output_text, 'w') as f: with open(output_text, 'w') as f:
+4 -3
View File
@@ -70,9 +70,10 @@ class NoopOcrEngine(OcrEngine):
@staticmethod @staticmethod
def generate_hocr(input_file, output_hocr, output_text, options): def generate_hocr(input_file, output_hocr, output_text, options):
with Image.open(input_file) as im, open( with (
output_hocr, 'w', encoding='utf-8' Image.open(input_file) as im,
) as f: open(output_hocr, 'w', encoding='utf-8') as f,
):
w, h = im.size w, h = im.size
f.write(HOCR_TEMPLATE.format(str(w), str(h))) f.write(HOCR_TEMPLATE.format(str(w), str(h)))
with open(output_text, 'w') as f: 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 Page 4 was chosen because of this number's association with bad luck
in many East Asian cultures. in many East Asian cultures.
""" """
# type: ignore # type: ignore
from __future__ import annotations from __future__ import annotations
+13
View File
@@ -29,6 +29,18 @@ def test_stream_api(resources: Path):
assert b'%PDF' in out.read(1024) 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): def test_hocr_api_multipage(resources: Path, outdir: Path, outpdf: Path):
ocrmypdf.api._pdf_to_hocr( ocrmypdf.api._pdf_to_hocr(
resources / 'multipage.pdf', resources / 'multipage.pdf',
@@ -62,3 +74,4 @@ def test_hocr_to_pdf_api(resources: Path, outdir: Path, outpdf: Path):
text = extract_text(outpdf) text = extract_text(outpdf)
assert 'hocr' in text and 'the' not in text 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): def test_no_glyphless_graft(resources, outdir):
with pikepdf.open(resources / 'francais.pdf') as pdf, pikepdf.open( with (
resources / 'aspect.pdf' pikepdf.open(resources / 'francais.pdf') as pdf,
) as pdf_aspect, pikepdf.open(resources / 'cmyk.pdf') as pdf_cmyk: 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_aspect.pages)
pdf.pages.extend(pdf_cmyk.pages) pdf.pages.extend(pdf_cmyk.pages)
pdf.save(outdir / 'test.pdf') pdf.save(outdir / 'test.pdf')
+1 -1
View File
@@ -17,7 +17,7 @@ from PIL import Image
import ocrmypdf import ocrmypdf
from ocrmypdf._exec import tesseract from ocrmypdf._exec import tesseract
from ocrmypdf.exceptions import ExitCode, MissingDependencyError, OutputFileAccessError from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.pdfa import file_claims_pdfa from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
from ocrmypdf.subprocess import get_version from ocrmypdf.subprocess import get_version
+4 -3
View File
@@ -35,9 +35,10 @@ def test_preserve_docinfo(output_type, resources, outpdf):
'--plugin', '--plugin',
'tests/plugins/tesseract_noop.py', 'tests/plugins/tesseract_noop.py',
) )
with pikepdf.open(resources / 'graph.pdf') as pdf_before, pikepdf.open( with (
output pikepdf.open(resources / 'graph.pdf') as pdf_before,
) as pdf_after: pikepdf.open(output) as pdf_after,
):
for key in ('/Title', '/Author'): for key in ('/Title', '/Author'):
assert pdf_before.docinfo[key] == pdf_after.docinfo[key] assert pdf_before.docinfo[key] == pdf_after.docinfo[key]
pdfa_info = file_claims_pdfa(str(output)) pdfa_info = file_claims_pdfa(str(output))
+4 -3
View File
@@ -176,9 +176,10 @@ def test_multiple_pngs(resources, outdir):
) )
mock.assert_called() mock.assert_called()
with pikepdf.open(outdir / 'in.pdf') as inpdf, pikepdf.open( with (
outdir / 'out.pdf' pikepdf.open(outdir / 'in.pdf') as inpdf,
) as outpdf: pikepdf.open(outdir / 'out.pdf') as outpdf,
):
for n in range(len(inpdf.pages)): for n in range(len(inpdf.pages)):
inim = next(iter(inpdf.pages[n].images.values())) inim = next(iter(inpdf.pages[n].images.values()))
outim = next(iter(outpdf.pages[n].images.values())) outim = next(iter(outpdf.pages[n].images.values()))
+7 -2
View File
@@ -9,10 +9,11 @@ import pytest
from PIL import Image from PIL import Image
from ocrmypdf._exec import ghostscript, tesseract from ocrmypdf._exec import ghostscript, tesseract
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.helpers import Resolution from ocrmypdf.helpers import Resolution
from ocrmypdf.pdfinfo import PdfInfo from ocrmypdf.pdfinfo import PdfInfo
from .conftest import check_ocrmypdf, have_unpaper from .conftest import check_ocrmypdf, have_unpaper, run_ocrmypdf
RENDERERS = ['hocr', 'sandwich'] RENDERERS = ['hocr', 'sandwich']
@@ -107,7 +108,7 @@ def test_non_square_resolution(renderer, resources, outpdf):
in_pageinfo = PdfInfo(resources / 'aspect.pdf') in_pageinfo = PdfInfo(resources / 'aspect.pdf')
assert in_pageinfo[0].dpi.x != in_pageinfo[0].dpi.y assert in_pageinfo[0].dpi.x != in_pageinfo[0].dpi.y
check_ocrmypdf( proc = run_ocrmypdf(
resources / 'aspect.pdf', resources / 'aspect.pdf',
outpdf, outpdf,
'--pdf-renderer', '--pdf-renderer',
@@ -115,6 +116,10 @@ def test_non_square_resolution(renderer, resources, outpdf):
'--plugin', '--plugin',
'tests/plugins/tesseract_cache.py', 'tests/plugins/tesseract_cache.py',
) )
# PDF/A conversion can fail for this file if Ghostscript >= 10.3, so don't test
# exit code in that case
if proc.returncode != ExitCode.pdfa_conversion_failed:
proc.check_returncode()
out_pageinfo = PdfInfo(outpdf) out_pageinfo = PdfInfo(outpdf)
+53 -29
View File
@@ -4,10 +4,10 @@
from __future__ import annotations from __future__ import annotations
import operator import operator
import warnings
from io import BytesIO from io import BytesIO
from math import cos, pi, sin from math import cos, pi, sin
from os import fspath from os import fspath
from subprocess import run
import img2pdf import img2pdf
import pikepdf import pikepdf
@@ -22,10 +22,6 @@ from ocrmypdf.pdfinfo import PdfInfo
from .conftest import check_ocrmypdf, run_ocrmypdf_api from .conftest import check_ocrmypdf, run_ocrmypdf_api
warnings.filterwarnings(
"ignore", category=DeprecationWarning, module="reportlab.lib.rl_safe_eval"
)
# pylintx: disable=unused-variable # pylintx: disable=unused-variable
RENDERERS = ['hocr', 'sandwich'] RENDERERS = ['hocr', 'sandwich']
@@ -215,34 +211,35 @@ def test_rotate_deskew_ocr_timeout(resources, outdir):
assert cmp > 0.95 assert cmp > 0.95
def make_rotate_test(imagefile, outdir, prefix, image_angle, page_angle):
memimg = BytesIO()
with Image.open(fspath(imagefile)) as im:
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image.Transpose, f'ROTATE_{ccw_angle}'))
im.save(memimg, format='PNG')
memimg.seek(0)
mempdf = BytesIO()
img2pdf.convert(
memimg.read(),
layout_fun=img2pdf.get_fixed_dpi_layout_fun((200, 200)),
outputstream=mempdf,
**IMG2PDF_KWARGS,
)
mempdf.seek(0)
with pikepdf.open(mempdf) as pdf:
pdf.pages[0].Rotate = page_angle
target = outdir / f'{prefix}_{image_angle}_{page_angle}.pdf'
pdf.save(target)
return target
@pytest.mark.slow @pytest.mark.slow
@pytest.mark.parametrize('page_angle', (0, 90, 180, 270)) @pytest.mark.parametrize('page_angle', (0, 90, 180, 270))
@pytest.mark.parametrize('image_angle', (0, 90, 180, 270)) @pytest.mark.parametrize('image_angle', (0, 90, 180, 270))
def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog): def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog):
def make_rotate_test(prefix, image_angle, page_angle): reference = make_rotate_test(resources / 'typewriter.png', outdir, 'ref', 0, 0)
memimg = BytesIO() test = make_rotate_test(resources, outdir, 'test', image_angle, page_angle)
with Image.open(fspath(resources / 'typewriter.png')) as im:
if image_angle != 0:
ccw_angle = -image_angle % 360
im = im.transpose(getattr(Image.Transpose, f'ROTATE_{ccw_angle}'))
im.save(memimg, format='PNG')
memimg.seek(0)
mempdf = BytesIO()
img2pdf.convert(
memimg.read(),
layout_fun=img2pdf.get_fixed_dpi_layout_fun((200, 200)),
outputstream=mempdf,
**IMG2PDF_KWARGS,
)
mempdf.seek(0)
with pikepdf.open(mempdf) as pdf:
pdf.pages[0].Rotate = page_angle
target = outdir / f'{prefix}_{image_angle}_{page_angle}.pdf'
pdf.save(target)
return target
reference = make_rotate_test('ref', 0, 0)
test = make_rotate_test('test', image_angle, page_angle)
out = test.with_suffix('.out.pdf') out = test.with_suffix('.out.pdf')
exitcode = run_ocrmypdf_api( exitcode = run_ocrmypdf_api(
@@ -258,6 +255,33 @@ def test_rotate_page_level(image_angle, page_angle, resources, outdir, caplog):
assert compare_images_monochrome(outdir, reference, 1, out, 1) > 0.2 assert compare_images_monochrome(outdir, reference, 1, out, 1) > 0.2
@pytest.mark.slow
@pytest.mark.parametrize('page_rotate_angle', (0, 90, 180, 270))
def test_page_rotate_tag(page_rotate_angle, resources, outdir, caplog):
# Check that pages that have an image that is misrotated but restored to
# correct rotation with a /Rotate will be processed correct and yield text.
test = make_rotate_test(
resources / 'crom.png', outdir, 'test', -page_rotate_angle, page_rotate_angle
)
out = test.with_suffix('.out.pdf')
exitcode = run_ocrmypdf_api(
test,
out,
'-O0',
)
assert exitcode == 0, caplog.text
def pdftotext(filename):
return (
run(['pdftotext', '-enc', 'UTF-8', filename, '-'], capture_output=True)
.stdout.strip()
.decode('utf-8')
)
test_text = pdftotext(out)
assert 'is a' in test_text, test_text
def test_rasterize_rotates(resources, tmp_path): def test_rasterize_rotates(resources, tmp_path):
pm = get_plugin_manager([]) pm = get_plugin_manager([])
+1 -2
View File
@@ -7,10 +7,9 @@ from math import isclose
import pytest import pytest
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.pdfinfo import PdfInfo from ocrmypdf.pdfinfo import PdfInfo
from .conftest import check_ocrmypdf, run_ocrmypdf_api from .conftest import check_ocrmypdf
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name