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