Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21fb6c82ca | ||
|
|
27f7b9f255 | ||
|
|
6f31a92ffb | ||
|
|
da2276788c | ||
|
|
dc6f1a266a | ||
|
|
9c8ddd853d | ||
|
|
014d0302f2 | ||
|
|
65568b3dbc | ||
|
|
05e2b6698d | ||
|
|
2f8e0f7d95 | ||
|
|
7e7553fc6b | ||
|
|
6b425aaebe | ||
|
|
725af43bc3 | ||
|
|
5c60309609 | ||
|
|
7d5cd55909 | ||
|
|
ec4a06fad2 | ||
|
|
48c6e2318e | ||
|
|
2eafa5e070 | ||
|
|
6aa04d7569 | ||
|
|
b9bffa97ba | ||
|
|
777ba99ccc | ||
|
|
001b3324f1 | ||
|
|
b1f2d257e2 | ||
|
|
24a08e5170 | ||
|
|
adf97fd82c | ||
|
|
a60ea72517 | ||
|
|
59f967cdcd | ||
|
|
6e439ee89e | ||
|
|
da38e1b035 | ||
|
|
28c60c4f82 | ||
|
|
a5efc4af9b | ||
|
|
1141235c42 | ||
|
|
2b6b7a4975 | ||
|
|
b062c9e8c0 | ||
|
|
ed632ae366 | ||
|
|
af742229e7 | ||
|
|
e2d998245d | ||
|
|
8c58e95c3a | ||
|
|
61600111d3 | ||
|
|
e4c45e3d3b | ||
|
|
7cabbb125f |
+4
-1
@@ -1 +1,4 @@
|
||||
ref-names: $Format:%D$
|
||||
node: $Format:%H$
|
||||
node-date: $Format:%cI$
|
||||
describe-name: $Format:%(describe:tags=true)$
|
||||
ref-names: $Format:%D$
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
+17
-17
@@ -41,11 +41,11 @@ jobs:
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -129,11 +129,11 @@ jobs:
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -184,11 +184,11 @@ jobs:
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -217,11 +217,11 @@ jobs:
|
||||
name: Build sdist and wheels
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: "3.7"
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
python setup.py sdist
|
||||
python setup.py bdist_wheel
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
./dist/*.whl
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: dist
|
||||
@@ -274,22 +274,22 @@ jobs:
|
||||
- name: Set image name
|
||||
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: jbarlow83
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Print image tag
|
||||
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
@@ -11,23 +11,23 @@ repos:
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
args: ["--profile", "black", "-a", "from __future__ import annotations"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.1
|
||||
rev: v1.20.2
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.32.1
|
||||
rev: v2.37.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py37-plus"]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.950
|
||||
rev: v0.971
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
|
||||
@@ -62,7 +62,8 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
|
||||
| Debian, Ubuntu | ``apt install ocrmypdf`` |
|
||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||
| Fedora | ``dnf install ocrmypdf`` |
|
||||
| macOS | ``brew install ocrmypdf`` |
|
||||
| macOS (Homebrew) | ``brew install ocrmypdf`` |
|
||||
| macOS (nix) | ``nix-env -i ocrmypdf`` |
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
| FreeBSD | ``pkg install py37-ocrmypdf`` |
|
||||
| Conda | ``conda install ocrmypdf`` |
|
||||
|
||||
+3
-1
@@ -223,7 +223,9 @@ The ``hocr`` renderer
|
||||
The ``hocr`` renderer works with older versions of Tesseract. The image
|
||||
layer is copied from the original PDF page if possible, avoiding
|
||||
potentially lossy transcoding or loss of other PDF information. If
|
||||
preprocessing is specified, then the image layer is a new PDF.
|
||||
preprocessing is specified, then the image layer is a new PDF. (You may
|
||||
need to disable PDF/A conversion nad optimization to eliminate all
|
||||
lossy transformations.)
|
||||
|
||||
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
|
||||
looking to customize how OCR is presented should look here. A major
|
||||
|
||||
+20
-12
@@ -36,18 +36,21 @@ Directory trees
|
||||
===============
|
||||
|
||||
This will walk through a directory tree and run OCR on all files in
|
||||
place, printing the output in a way that makes
|
||||
place, and printing each filename in between runs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||
find . -printf '%p\n' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||
|
||||
Alternatively, with a docker container (mounts a volume to the container
|
||||
where the PDFs are stored):
|
||||
Alternatively, with a Docker container and streaming the file through
|
||||
standard input and output:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf '<container dir>/{}' '<container dir>/{}' \;
|
||||
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
|
||||
pdfout=$(mktemp)
|
||||
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
|
||||
done
|
||||
|
||||
This only runs one ``ocrmypdf`` process at a time. This variation uses
|
||||
``find`` to create a directory list and ``parallel`` to parallelize runs
|
||||
@@ -124,7 +127,9 @@ Users may need to customize the script to meet their requirements.
|
||||
|
||||
"OCR_INPUT_DIRECTORY", "Set input directory to monitor (recursive)"
|
||||
"OCR_OUTPUT_DIRECTORY", "Set output directory (should not be under input)"
|
||||
"OCR_ARCHIVE_DIRECTORY", "Set archive directory for processed originals (should not be under input, requires ``OCR_ON_SUCCESS_ARCHIVE`` to be set)"
|
||||
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
|
||||
"OCR_ON_SUCCESS_ARCHIVE", "This will move the processed orignal file to ``OCR_ARCHIVE_DIRECTORY`` if the exit code is 0 (OK). Note that ``OCR_ON_SUCCESS_DELETE`` takes precedence over this option, i.e. if both options are set, the input file will be deleted."
|
||||
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||
"OCR_DESKEW", "Apply deskew to crooked input PDFs"
|
||||
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true}'``."
|
||||
@@ -144,16 +149,18 @@ The watcher service is included in the OCRmyPDF Docker image. To run it:
|
||||
docker run \
|
||||
-v <path to files to convert>:/input \
|
||||
-v <path to store results>:/output \
|
||||
-v <path to store processed originals>:/archive \
|
||||
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
-e OCR_ON_SUCCESS_DELETE=1 \
|
||||
-e OCR_ON_SUCCESS_ARCHIVE=1 \
|
||||
-e OCR_DESKEW=1 \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-it --entrypoint python3 \
|
||||
jbarlow83/ocrmypdf \
|
||||
watcher.py
|
||||
|
||||
This service will watch for a file that matches ``/input/\*.pdf`` and will
|
||||
convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
This service will watch for a file that matches ``/input/\*.pdf``,
|
||||
convert it to a OCRed PDF in ``/output/``, and move the processed
|
||||
original to ``/archive``. The parameters to this image are:
|
||||
|
||||
.. csv-table:: watcher.py parameters for Docker
|
||||
:header: "Parameter", "Description"
|
||||
@@ -161,10 +168,11 @@ convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
|
||||
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1"
|
||||
"``-e OCR_ON_SUCCESS_DELETE=1``", "Define environment variable"
|
||||
"``-e OCR_DESKEW=1``", "Define environment variable"
|
||||
"``-e PYTHONBUFFERED=1``", "This will force STDOUT to be unbuffered and allow you to see messages in docker logs"
|
||||
"``-v <path to store processed originals>:/archive``", "Archive processed originals here"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable ``OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1`` to place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||
"``-e OCR_ON_SUCCESS_ARCHIVE=1``", "Define environment variable ``OCR_ON_SUCCESS_ARCHIVE`` to move processed originals"
|
||||
"``-e OCR_DESKEW=1``", "Define environment variable ``OCR_DESKEW`` to apply deskew to crooked input PDFs"
|
||||
"``-e PYTHONBUFFERED=1``", "This will force ``STDOUT`` to be unbuffered and allow you to see messages in docker logs"
|
||||
|
||||
This service relies on polling to check for changes to the filesystem. It
|
||||
may not be suitable for some environments, such as filesystems shared on a
|
||||
|
||||
+9
-1
@@ -200,7 +200,7 @@ might remove desirable content, especially from poor quality scans.
|
||||
|
||||
.. warning::
|
||||
|
||||
``--clean-final`` and ``-remove-background`` may leave undesirable
|
||||
``--clean-final`` and ``--remove-background`` may leave undesirable
|
||||
visual artifacts in some images where their algorithms have
|
||||
shortcomings. Files should be visually reviewed after using these
|
||||
options.
|
||||
@@ -340,6 +340,9 @@ levels in the GCC compiler.
|
||||
- Enables lossless optimizations, such as transcoding images to more
|
||||
efficient formats. Also compress other uncompressed objects in the
|
||||
PDF and enables the more efficient "object streams" within the PDF.
|
||||
(If ``--jbig2-lossy`` is issued, then lossy JBIG2 optimization is used.
|
||||
The decision to use lossy JBIG2 is separate from standard optimization
|
||||
settings.)
|
||||
* - ``--optimize 2``
|
||||
- All of the above, and enables lossy optimizations and color quantization.
|
||||
* - ``--optimize 3``
|
||||
@@ -360,3 +363,8 @@ fo a PDF.
|
||||
ocrmypdf --optimize 3 in.pdf out.pdf # Make it small
|
||||
|
||||
Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
|
||||
|
||||
.. note::
|
||||
|
||||
Image processing and PDF/A conversion can also introduce lossy transformations
|
||||
to your PDF images, even when ``--optimize 1`` is in use.
|
||||
|
||||
@@ -43,11 +43,11 @@ Installing on Linux
|
||||
Debian and Ubuntu 18.04 or newer
|
||||
--------------------------------
|
||||
|
||||
.. |deb-stable| image:: https://repology.org/badge/version-for-repo/debian_stable/ocrmypdf.svg
|
||||
:alt: Debian stable
|
||||
.. |deb-11| image:: https://repology.org/badge/version-for-repo/debian_11/ocrmypdf.svg
|
||||
:alt: Debian 11
|
||||
|
||||
.. |deb-testing| image:: https://repology.org/badge/version-for-repo/debian_testing/ocrmypdf.svg
|
||||
:alt: Debian testing
|
||||
.. |deb-12| image:: https://repology.org/badge/version-for-repo/debian_12/ocrmypdf.svg
|
||||
:alt: Debian 12
|
||||
|
||||
.. |deb-unstable| image:: https://repology.org/badge/version-for-repo/debian_unstable/ocrmypdf.svg
|
||||
:alt: Debian unstable
|
||||
@@ -66,7 +66,7 @@ Debian and Ubuntu 18.04 or newer
|
||||
+-----------------------------------------------+
|
||||
| |latest| |
|
||||
+-----------------------------------------------+
|
||||
| |deb-stable| |deb-testing| |deb-unstable| |
|
||||
| |deb-11| |deb-12| |deb-unstable| |
|
||||
+-----------------------------------------------+
|
||||
| |ubu-1804| |ubu-2004| |ubu-2204| |
|
||||
+-----------------------------------------------+
|
||||
@@ -142,7 +142,7 @@ from sources <#installing-head-revision-from-sources>`__.
|
||||
Installing the latest version on Ubuntu 22.04 LTS
|
||||
-------------------------------------------------
|
||||
|
||||
Ubuntu 22.04 includes ocrmypdf 13.4.0 - you can install that with
|
||||
Ubuntu 22.04 includes ocrmypdf 13.4.0 - you can install that with
|
||||
``apt install ocrmypdf``. To install a more recent version for the current
|
||||
user, follow these steps:
|
||||
|
||||
@@ -153,7 +153,7 @@ user, follow these steps:
|
||||
|
||||
pip install --user --upgrade ocrmypdf
|
||||
|
||||
If you get the message ``WARNING: The script ocrmypdf is installed in
|
||||
If you get the message ``WARNING: The script ocrmypdf is installed in
|
||||
'/home/$USER/.local/bin' which is not on PATH.``, you may need to re-login
|
||||
or open a new shell, or manually add this to your user's PATH.
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ For all other Linux, you must build a JBIG2 encoder from source:
|
||||
|
||||
.. _jbig2-lossy:
|
||||
|
||||
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.
|
||||
|
||||
Lossy mode JBIG2
|
||||
|
||||
+13
-1
@@ -162,6 +162,11 @@ Examples
|
||||
management system.
|
||||
|
||||
|
||||
Suppressing or overriding other plugins
|
||||
---------------------------------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.initialize
|
||||
|
||||
Custom command line arguments
|
||||
-----------------------------
|
||||
|
||||
@@ -172,7 +177,7 @@ Custom command line arguments
|
||||
Execution and progress reporting
|
||||
--------------------------------
|
||||
|
||||
.. autoclass: ocrmypdf.pluginspec.Executor
|
||||
.. autoclass:: ocrmypdf.pluginspec.Executor
|
||||
:members:
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.get_logging_console
|
||||
@@ -216,3 +221,10 @@ PDF/A production
|
||||
----------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.generate_pdfa
|
||||
|
||||
PDF optimization
|
||||
----------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.optimize_pdf
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.is_optimization_enabled
|
||||
+24
-1
@@ -24,10 +24,33 @@ tagged yet.
|
||||
|
||||
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||
|
||||
v13.6.2
|
||||
=======
|
||||
|
||||
- Added a shim to prevent an "error during error handling" for Python 3.7 and 3.8.
|
||||
- Modernized some type annotations.
|
||||
- Improved annotations on our _windows module to help IDEs and mypy figure out what
|
||||
we're doing.
|
||||
|
||||
v13.6.1
|
||||
=======
|
||||
|
||||
- Require setuptools-scm 7.0.5 to avoid possible issues with source distributions in
|
||||
earlier versions of setuptools-scm.
|
||||
- Suppress a spurious warning, improve tests, improve typing and other miscellany.
|
||||
|
||||
v13.6.0
|
||||
=======
|
||||
|
||||
- Added a new ``initialize`` plugin hook, making it possible to suppress built-in
|
||||
plugins more easily, among other possibilities.
|
||||
- Fixed an issue where unpaper would exit with a "wrong stream" error, probably
|
||||
related to images with an odd integer width. :issue:`887, 665`
|
||||
|
||||
v13.5.0
|
||||
=======
|
||||
|
||||
- Added a new ``optimize_pdf`` pluginhook, making it possible to create plugins that
|
||||
- Added a new ``optimize_pdf`` plugin hook, making it possible to create plugins that
|
||||
replace or enhance OCRmyPDF's PDF optimizer.
|
||||
- Removed all max version restrictions. Our new policy is to blacklist known-bad releases
|
||||
and only block known-bad versions of dependencies.
|
||||
|
||||
+2
-1
@@ -19,8 +19,9 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
from __future__ import annotations
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -37,6 +37,8 @@ To use this as an API:
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PIL import Image
|
||||
|
||||
+2
-1
@@ -19,8 +19,9 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
from __future__ import annotations
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
+16
-4
@@ -20,9 +20,12 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
@@ -44,8 +47,10 @@ def getenv_bool(name: str, default: str = 'False'):
|
||||
|
||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||
ARCHIVE_DIRECTORY = os.getenv('OCR_ARCHIVE_DIRECTORY', '/processed')
|
||||
OUTPUT_DIRECTORY_YEAR_MONTH = getenv_bool('OCR_OUTPUT_DIRECTORY_YEAR_MONTH')
|
||||
ON_SUCCESS_DELETE = getenv_bool('OCR_ON_SUCCESS_DELETE')
|
||||
ON_SUCCESS_ARCHIVE = getenv_bool('OCR_ON_SUCCESS_ARCHIVE')
|
||||
DESKEW = getenv_bool('OCR_DESKEW')
|
||||
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
||||
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
|
||||
@@ -108,9 +113,13 @@ def execute_ocrmypdf(file_path):
|
||||
deskew=DESKEW,
|
||||
**OCR_JSON_SETTINGS,
|
||||
)
|
||||
if exit_code == 0 and ON_SUCCESS_DELETE:
|
||||
log.info(f'OCR is done. Deleting: {file_path}')
|
||||
file_path.unlink()
|
||||
if exit_code == 0:
|
||||
if ON_SUCCESS_DELETE:
|
||||
log.info(f'OCR is done. Deleting: {file_path}')
|
||||
file_path.unlink()
|
||||
elif ON_SUCCESS_ARCHIVE:
|
||||
log.info(f'OCR is done. Archiving {file_path.name} to {ARCHIVE_DIRECTORY}')
|
||||
shutil.move(file_path, f'{ARCHIVE_DIRECTORY}/{file_path.name}')
|
||||
else:
|
||||
log.info('OCR is done')
|
||||
|
||||
@@ -135,13 +144,16 @@ def main():
|
||||
f"Starting OCRmyPDF watcher with config:\n"
|
||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
||||
f"Output Directory: {OUTPUT_DIRECTORY}\n"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||
f"Archive Directory: {ARCHIVE_DIRECTORY}"
|
||||
)
|
||||
log.debug(
|
||||
f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||
f"ARCHIVE_DIRECTORY: {ARCHIVE_DIRECTORY}\n"
|
||||
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
||||
f"ON_SUCCESS_ARCHIVE: {ON_SUCCESS_ARCHIVE}\n"
|
||||
f"DESKEW: {DESKEW}\n"
|
||||
f"ARGS: {OCR_JSON_SETTINGS}\n"
|
||||
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
||||
|
||||
@@ -24,6 +24,8 @@ to emphasize that SaaS deployments should make sure they comply with
|
||||
Ghostscript's license as well as OCRmyPDF's.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
from subprocess import PIPE, run
|
||||
|
||||
+5
-6
@@ -1,14 +1,12 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools >= 30.3.0",
|
||||
"wheel",
|
||||
"setuptools_scm[toml] >= 3.4",
|
||||
"setuptools_scm_git_archive"
|
||||
"setuptools >= 52",
|
||||
"setuptools_scm[toml] >= 7.0.5",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
version_scheme = "post-release"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
@@ -96,7 +94,8 @@ module = [
|
||||
'pdfminer.*',
|
||||
'reportlab.*',
|
||||
'fitz',
|
||||
'libxmp.utils'
|
||||
'libxmp.utils',
|
||||
'importlib_metadata'
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ install_requires =
|
||||
tqdm>=4
|
||||
importlib-metadata>=4;python_version<'3.8' # until Python 3.8
|
||||
importlib-resources>=5;python_version<'3.9' # until Python 3.9
|
||||
typing-extensions>=4;python_version<'3.10'
|
||||
python_requires = >=3.7
|
||||
include_package_data = True
|
||||
package_dir =
|
||||
@@ -86,6 +87,8 @@ test =
|
||||
pytest-cov>=2.11.1
|
||||
pytest-xdist>=2.2.0
|
||||
python-xmp-toolkit==2.0.1 # also requires apt-get install libexempi3
|
||||
types-Pillow
|
||||
types-humanfriendly
|
||||
watcher =
|
||||
watchdog>=1.0.2
|
||||
webservice =
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
|
||||
"""setup.py to support older setuptools and pip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# Minimal setup to support older setuptools/setuptools_scm
|
||||
setup(
|
||||
setup_requires=[ # can be removed whenever we can drop pip 9 support
|
||||
'setuptools_scm', # so that version will work
|
||||
'setuptools_scm_git_archive', # enable version from github tarballs
|
||||
],
|
||||
use_scm_version={'version_scheme': 'post-release'},
|
||||
)
|
||||
setup()
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
"""Adds OCR layer to PDFs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pluggy import HookimplMarker as _HookimplMarker
|
||||
|
||||
from ocrmypdf import helpers, hocrtransform, pdfa, pdfinfo
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""ocrmypdf command line entrypoint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
|
||||
"""OCRmyPDF concurrency abstractions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Iterable, Optional
|
||||
from typing import Callable, Iterable
|
||||
|
||||
|
||||
def _task_noop(*_args, **_kwargs):
|
||||
@@ -47,10 +49,10 @@ class Executor(ABC):
|
||||
use_threads: bool,
|
||||
max_workers: int,
|
||||
tqdm_kwargs: dict,
|
||||
worker_initializer: Optional[Callable] = None,
|
||||
task: Optional[Callable] = None,
|
||||
task_arguments: Optional[Iterable] = None,
|
||||
task_finished: Optional[Callable] = None,
|
||||
worker_initializer: Callable | None = None,
|
||||
task: Callable | None = None,
|
||||
task_arguments: Iterable | None = None,
|
||||
task_finished: Callable | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set up parallel execution and progress reporting.
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
|
||||
|
||||
"""Manage third party executables"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Interface to Ghostscript executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -15,7 +17,6 @@ from io import BytesIO
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, CalledProcessError
|
||||
from typing import Optional
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
@@ -77,8 +78,8 @@ def rasterize_pdf(
|
||||
raster_device: str,
|
||||
raster_dpi: Resolution,
|
||||
pageno: int = 1,
|
||||
page_dpi: Optional[Resolution] = None,
|
||||
rotation: Optional[int] = None,
|
||||
page_dpi: Resolution | None = None,
|
||||
rotation: int | None = None,
|
||||
filter_vector: bool = False,
|
||||
):
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units."""
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Interface to jbig2 executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from subprocess import PIPE
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Interface to pngquant executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
"""Interface to Tesseract executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from math import pi
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from packaging.version import Version
|
||||
from PIL import Image
|
||||
@@ -46,7 +47,7 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
</html>
|
||||
"""
|
||||
|
||||
TESSERACT_THRESHOLDING_METHODS: Dict[str, int] = {
|
||||
TESSERACT_THRESHOLDING_METHODS: dict[str, int] = {
|
||||
'auto': 0,
|
||||
'otsu': 0,
|
||||
'adaptive-otsu': 1,
|
||||
@@ -162,7 +163,7 @@ def get_languages():
|
||||
return {lang.strip() for lang in rest}
|
||||
|
||||
|
||||
def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
||||
def tess_base_args(langs: list[str], engine_mode: int | None) -> list[str]:
|
||||
args = ['tesseract']
|
||||
if langs:
|
||||
args.extend(['-l', '+'.join(langs)])
|
||||
@@ -171,7 +172,7 @@ def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
||||
return args
|
||||
|
||||
|
||||
def _parse_tesseract_output(binary_output: bytes) -> Dict[str, str]:
|
||||
def _parse_tesseract_output(binary_output: bytes) -> dict[str, str]:
|
||||
def gen():
|
||||
for line in binary_output.decode().splitlines():
|
||||
line = line.strip()
|
||||
@@ -183,7 +184,7 @@ def _parse_tesseract_output(binary_output: bytes) -> Dict[str, str]:
|
||||
|
||||
|
||||
def get_orientation(
|
||||
input_file: Path, engine_mode: Optional[int], timeout: float
|
||||
input_file: Path, engine_mode: int | None, timeout: float
|
||||
) -> OrientationConfidence:
|
||||
args_tesseract = tess_base_args(['osd'], engine_mode) + [
|
||||
'--psm',
|
||||
@@ -215,7 +216,7 @@ def get_orientation(
|
||||
|
||||
|
||||
def get_deskew(
|
||||
input_file: Path, languages: List[str], engine_mode: Optional[int], timeout: float
|
||||
input_file: Path, languages: list[str], engine_mode: int | None, timeout: float
|
||||
) -> float:
|
||||
"""Gets angle to deskew this page, in degrees."""
|
||||
args_tesseract = tess_base_args(languages, engine_mode) + [
|
||||
@@ -306,9 +307,9 @@ def generate_hocr(
|
||||
input_file: Path,
|
||||
output_hocr: Path,
|
||||
output_text: Path,
|
||||
languages: List[str],
|
||||
languages: list[str],
|
||||
engine_mode: int,
|
||||
tessconfig: List[str],
|
||||
tessconfig: list[str],
|
||||
timeout: float,
|
||||
pagesegmode: int,
|
||||
thresholding: int,
|
||||
@@ -372,9 +373,9 @@ def generate_pdf(
|
||||
input_file: Path,
|
||||
output_pdf: Path,
|
||||
output_text: Path,
|
||||
languages: List[str],
|
||||
languages: list[str],
|
||||
engine_mode: int,
|
||||
tessconfig: List[str],
|
||||
tessconfig: list[str],
|
||||
timeout: float,
|
||||
pagesegmode: int,
|
||||
thresholding: int,
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
"""Interface to unpaper executable"""
|
||||
|
||||
import logging
|
||||
@@ -18,7 +19,7 @@ from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT
|
||||
from typing import Iterator, List, Optional, Tuple, Union
|
||||
from typing import Iterator, Union
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@@ -72,13 +73,13 @@ def version() -> str:
|
||||
return get_version('unpaper')
|
||||
|
||||
|
||||
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
|
||||
SUPPORTED_MODES = {'1', 'L', 'RGB'}
|
||||
|
||||
|
||||
def _convert_image(im: Image.Image) -> Tuple[Image.Image, bool, str]:
|
||||
def _convert_image(im: Image.Image) -> tuple[Image.Image, bool]:
|
||||
im_modified = False
|
||||
|
||||
if im.mode not in SUFFIXES:
|
||||
if im.mode not in SUPPORTED_MODES:
|
||||
log.info("Converting image to other colorspace")
|
||||
try:
|
||||
if im.mode == 'P' and len(im.getcolors()) == 2:
|
||||
@@ -91,41 +92,41 @@ def _convert_image(im: Image.Image) -> Tuple[Image.Image, bool, str]:
|
||||
) from e
|
||||
else:
|
||||
im_modified = True
|
||||
try:
|
||||
suffix = SUFFIXES[im.mode]
|
||||
except KeyError:
|
||||
raise MissingDependencyError(
|
||||
"Failed to convert image to a supported format."
|
||||
) from None
|
||||
return im, im_modified, suffix
|
||||
if im.mode not in SUPPORTED_MODES:
|
||||
raise MissingDependencyError(
|
||||
"Failed to convert image to a supported format."
|
||||
) from None
|
||||
return im, im_modified
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _setup_unpaper_io(input_file: Path) -> Iterator[Tuple[Path, Path, Path]]:
|
||||
def _setup_unpaper_io(input_file: Path) -> Iterator[tuple[Path, Path, Path]]:
|
||||
with Image.open(input_file) as im:
|
||||
if im.width * im.height >= UNPAPER_IMAGE_PIXEL_LIMIT:
|
||||
raise UnpaperImageTooLargeError(w=im.width, h=im.height)
|
||||
im, im_modified, suffix = _convert_image(im)
|
||||
im, im_modified = _convert_image(im)
|
||||
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
|
||||
tmppath = Path(tmpdir)
|
||||
if im_modified or input_file.suffix != '.pnm':
|
||||
input_pnm = tmppath / 'input.pnm'
|
||||
im.save(input_pnm, format='PPM')
|
||||
if im_modified or input_file.suffix != '.png':
|
||||
input_png = tmppath / 'input.png'
|
||||
im.save(input_png, format='PNG')
|
||||
else:
|
||||
# No changes, PNG input, just use the file we already have
|
||||
input_pnm = input_file
|
||||
input_png = input_file
|
||||
|
||||
output_pnm = tmppath / f'output{suffix}'
|
||||
yield input_pnm, output_pnm, tmppath
|
||||
# unpaper can write .png too, but it seems to write them slowly
|
||||
# adds a few seconds to test suite - so just use pnm
|
||||
output_pnm = tmppath / 'output.pnm'
|
||||
yield input_png, output_pnm, tmppath
|
||||
|
||||
|
||||
def run_unpaper(
|
||||
input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: List[str]
|
||||
input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: list[str]
|
||||
) -> None:
|
||||
args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args
|
||||
|
||||
with _setup_unpaper_io(input_file) as (input_pnm, output_pnm, tmpdir):
|
||||
with _setup_unpaper_io(input_file) as (input_png, output_pnm, tmpdir):
|
||||
# To prevent any shenanigans from accepting arbitrary parameters in
|
||||
# --unpaper-args, we:
|
||||
# 1) run with cwd set to a tmpdir with only unpaper's files
|
||||
@@ -133,7 +134,7 @@ def run_unpaper(
|
||||
# 3) append absolute paths for the input and output file
|
||||
# This should ensure that a user cannot clobber some other file with
|
||||
# their unpaper arguments (whether intentionally or otherwise)
|
||||
args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)])
|
||||
args_unpaper.extend([os.fspath(input_png), os.fspath(output_pnm)])
|
||||
run(
|
||||
args_unpaper,
|
||||
close_fds=True,
|
||||
@@ -154,7 +155,7 @@ def run_unpaper(
|
||||
) from e
|
||||
|
||||
|
||||
def validate_custom_args(args: str) -> List[str]:
|
||||
def validate_custom_args(args: str) -> list[str]:
|
||||
unpaper_args = shlex.split(args)
|
||||
if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args):
|
||||
raise ValueError('No filenames allowed in --unpaper-args')
|
||||
@@ -166,7 +167,7 @@ def clean(
|
||||
output_file: Path,
|
||||
*,
|
||||
dpi: DecFloat,
|
||||
unpaper_args: Optional[List[str]] = None,
|
||||
unpaper_args: list[str] | None = None,
|
||||
) -> Path:
|
||||
default_args = [
|
||||
'--layout',
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
|
||||
"""For grafting text-only PDF pages onto freeform PDF pages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pikepdf import (
|
||||
Dictionary,
|
||||
@@ -103,8 +104,8 @@ class OcrGrafter:
|
||||
self,
|
||||
*,
|
||||
pageno: int,
|
||||
image: Optional[Path],
|
||||
textpdf: Optional[Path],
|
||||
image: Path | None,
|
||||
textpdf: Path | None,
|
||||
autorotate_correction: int,
|
||||
):
|
||||
if textpdf and not self.font:
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
"""Defines context objects that are passed to child processes/threads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -50,7 +52,7 @@ class PdfContext:
|
||||
"""
|
||||
return self.work_folder / name
|
||||
|
||||
def get_page_contexts(self) -> Iterator['PageContext']:
|
||||
def get_page_contexts(self) -> Iterator[PageContext]:
|
||||
"""Get all ``PageContext`` for this PDF."""
|
||||
npages = len(self.pdfinfo)
|
||||
for n in range(npages):
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
"""Logging support classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
+15
-14
@@ -6,6 +6,8 @@
|
||||
|
||||
"""OCRmyPDF page processing pipeline functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -14,7 +16,7 @@ from contextlib import suppress
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import Dict, Iterable, Optional
|
||||
from typing import Iterable
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
@@ -41,7 +43,7 @@ from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
|
||||
# Remove this workaround when we require Pillow >= 10
|
||||
try:
|
||||
BICUBIC = Image.Resampling.BICUBIC # type: ignore
|
||||
except AttributeError:
|
||||
except AttributeError: # pragma: no cover
|
||||
# Pillow 9 shim
|
||||
BICUBIC = Image.BICUBIC # type: ignore
|
||||
|
||||
@@ -494,7 +496,7 @@ def preprocess_deskew(input_file: Path, page_context: PageContext):
|
||||
deskewed = im.rotate(
|
||||
deskew_angle_degrees,
|
||||
resample=BICUBIC,
|
||||
fillcolor=ImageColor.getcolor('white', mode=im.mode),
|
||||
fillcolor=ImageColor.getcolor('white', mode=im.mode), # type: ignore
|
||||
)
|
||||
deskewed.save(output_file, dpi=dpi)
|
||||
|
||||
@@ -665,7 +667,7 @@ def ocr_engine_textonly_pdf(input_image: Path, page_context: PageContext):
|
||||
return (output_pdf, output_text)
|
||||
|
||||
|
||||
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]:
|
||||
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
|
||||
options = context.options
|
||||
|
||||
def from_document_info(key):
|
||||
@@ -679,15 +681,14 @@ def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]:
|
||||
k: from_document_info(k)
|
||||
for k in ('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')
|
||||
}
|
||||
if options is not None:
|
||||
if options.title:
|
||||
pdfmark['/Title'] = options.title
|
||||
if options.author:
|
||||
pdfmark['/Author'] = options.author
|
||||
if options.keywords:
|
||||
pdfmark['/Keywords'] = options.keywords
|
||||
if options.subject:
|
||||
pdfmark['/Subject'] = options.subject
|
||||
if options.title:
|
||||
pdfmark['/Title'] = options.title
|
||||
if options.author:
|
||||
pdfmark['/Author'] = options.author
|
||||
if options.keywords:
|
||||
pdfmark['/Keywords'] = options.keywords
|
||||
if options.subject:
|
||||
pdfmark['/Subject'] = options.subject
|
||||
|
||||
creator_tag = context.plugin_manager.hook.get_ocr_engine().creator_tag(options)
|
||||
|
||||
@@ -867,7 +868,7 @@ def enumerate_compress_ranges(iterable):
|
||||
yield (skipped_from, index), None
|
||||
|
||||
|
||||
def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
|
||||
def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext):
|
||||
output_file = context.get_path('sidecar.txt')
|
||||
with open(output_file, 'w', encoding="utf-8") as stream:
|
||||
for (from_, to_), txt_file in enumerate_compress_ranges(txt_files):
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
|
||||
"""Plugin manager using pluggy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.util
|
||||
import pkgutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Sequence, Tuple, Union
|
||||
from typing import Sequence
|
||||
|
||||
import pluggy
|
||||
|
||||
@@ -34,7 +36,7 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
plugins: List[Union[str, Path]],
|
||||
plugins: list[str | Path],
|
||||
builtins: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -101,7 +103,7 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
|
||||
self.register(module)
|
||||
|
||||
|
||||
def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
|
||||
def get_plugin_manager(plugins: list[str | Path], builtins=True):
|
||||
return OcrmypdfPluginManager(
|
||||
project_name='ocrmypdf',
|
||||
plugins=plugins,
|
||||
@@ -111,11 +113,14 @@ def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
|
||||
|
||||
def get_parser_options_plugins(
|
||||
args: Sequence[str],
|
||||
) -> Tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]:
|
||||
) -> tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]:
|
||||
pre_options, _unused = plugins_only_parser.parse_known_args(args=args)
|
||||
plugin_manager = get_plugin_manager(pre_options.plugins)
|
||||
|
||||
parser = get_parser()
|
||||
plugin_manager.hook.initialize( # pylint: disable=no-member
|
||||
plugin_manager=plugin_manager
|
||||
)
|
||||
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
|
||||
|
||||
options = parser.parse_args(args=args)
|
||||
|
||||
+11
-9
@@ -7,6 +7,8 @@
|
||||
"""Implements the concurrent and page synchronous parts of the pipeline."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.handlers
|
||||
@@ -18,7 +20,7 @@ from concurrent.futures.thread import BrokenThreadPool
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from typing import List, NamedTuple, Optional, Sequence, Tuple, cast
|
||||
from typing import NamedTuple, Sequence, cast
|
||||
|
||||
import PIL
|
||||
|
||||
@@ -74,9 +76,9 @@ class PageResult(NamedTuple):
|
||||
"""Result when a page is finished processing."""
|
||||
|
||||
pageno: int
|
||||
pdf_page_from_image: Optional[Path]
|
||||
ocr: Optional[Path]
|
||||
text: Optional[Path]
|
||||
pdf_page_from_image: Path | None
|
||||
ocr: Path | None
|
||||
text: Path | None
|
||||
orientation_correction: int
|
||||
|
||||
|
||||
@@ -115,7 +117,7 @@ def preprocess(
|
||||
|
||||
def make_intermediate_images(
|
||||
page_context: PageContext, orientation_correction: int
|
||||
) -> Tuple[Path, Optional[Path]]:
|
||||
) -> tuple[Path, Path | None]:
|
||||
options = page_context.options
|
||||
|
||||
ocr_image = preprocess_out = None
|
||||
@@ -232,7 +234,7 @@ def exec_page_sync(page_context: PageContext) -> PageResult:
|
||||
|
||||
def post_process(
|
||||
pdf_file: Path, context: PdfContext, executor: Executor
|
||||
) -> Tuple[Path, Sequence[str]]:
|
||||
) -> tuple[Path, Sequence[str]]:
|
||||
pdf_out = pdf_file
|
||||
if context.options.output_type.startswith('pdfa'):
|
||||
ps_stub_out = generate_postscript_stub(context)
|
||||
@@ -259,7 +261,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||
if max_workers > 1:
|
||||
log.info("Start processing %d pages concurrently", max_workers)
|
||||
|
||||
sidecars: List[Optional[Path]] = [None] * len(context.pdfinfo)
|
||||
sidecars: list[Path | None] = [None] * len(context.pdfinfo)
|
||||
ocrgraft = OcrGrafter(context)
|
||||
|
||||
def update_page(result: PageResult, pbar):
|
||||
@@ -302,7 +304,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||
# Merge layers to one single pdf
|
||||
pdf = ocrgraft.finalize()
|
||||
|
||||
messages: List[str] = []
|
||||
messages: Sequence[str] = []
|
||||
if options.output_type != 'none':
|
||||
# PDF/A and metadata
|
||||
log.info("Postprocessing...")
|
||||
@@ -337,7 +339,7 @@ def configure_debug_logging(
|
||||
def run_pipeline(
|
||||
options: argparse.Namespace,
|
||||
*,
|
||||
plugin_manager: Optional[OcrmypdfPluginManager],
|
||||
plugin_manager: OcrmypdfPluginManager | None,
|
||||
api: bool = False,
|
||||
) -> ExitCode:
|
||||
# Any changes to options will not take effect for options that are already
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Validate a work order from API or command line."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
@@ -14,7 +16,7 @@ import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import List, Optional, Sequence, Set, Tuple
|
||||
from typing import Sequence
|
||||
|
||||
import pikepdf
|
||||
import PIL
|
||||
@@ -144,8 +146,8 @@ def check_options_preprocessing(options):
|
||||
raise BadArgsError("--unpaper-args: " + str(e)) from e
|
||||
|
||||
|
||||
def _pages_from_ranges(ranges: str) -> Set[int]:
|
||||
pages: List[int] = []
|
||||
def _pages_from_ranges(ranges: str) -> set[int]:
|
||||
pages: list[int] = []
|
||||
page_groups = ranges.replace(' ', '').split(',')
|
||||
for group in page_groups:
|
||||
if not group:
|
||||
@@ -241,7 +243,7 @@ def check_options(options, plugin_manager):
|
||||
_check_options(options, plugin_manager, ocr_engine_languages)
|
||||
|
||||
|
||||
def create_input_file(options, work_folder: Path) -> Tuple[Path, str]:
|
||||
def create_input_file(options, work_folder: Path) -> tuple[Path, str]:
|
||||
if options.input_file == '-':
|
||||
# stdin
|
||||
log.info('reading file from standard input')
|
||||
@@ -297,7 +299,7 @@ def report_output_file_size(
|
||||
options,
|
||||
input_file: Path,
|
||||
output_file: Path,
|
||||
optimize_messages: Optional[Sequence[str]] = None,
|
||||
optimize_messages: Sequence[str] | None = None,
|
||||
file_overhead: int = 4000,
|
||||
page_overhead: int = 3000,
|
||||
):
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
OCRmyPDF uses setuptools_scm to derive version from git tags.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from importlib_metadata import version as _package_version
|
||||
except ImportError:
|
||||
from importlib.metadata import version as _package_version
|
||||
except ImportError:
|
||||
from importlib_metadata import version as _package_version # type: ignore
|
||||
|
||||
PROGRAM_NAME = 'ocrmypdf'
|
||||
|
||||
|
||||
+4
-2
@@ -6,6 +6,8 @@
|
||||
|
||||
"""Functions for using ocrmypdf as an API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@@ -13,7 +15,7 @@ import threading
|
||||
from enum import IntEnum
|
||||
from io import IOBase
|
||||
from pathlib import Path
|
||||
from typing import AnyStr, BinaryIO, Iterable, Optional, Union
|
||||
from typing import AnyStr, BinaryIO, Iterable, Union
|
||||
from warnings import warn
|
||||
|
||||
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
|
||||
@@ -217,7 +219,7 @@ def ocr( # pylint: disable=unused-argument
|
||||
language: Iterable[str] = None,
|
||||
image_dpi: int = None,
|
||||
output_type=None,
|
||||
sidecar: Optional[StrPath] = None,
|
||||
sidecar: StrPath | None = None,
|
||||
jobs: int = None,
|
||||
use_threads: bool = None,
|
||||
title: str = None,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# This file exists only mark builtin_plugins as a package.
|
||||
# The plugin manager will not load it, so anything defined here may not be
|
||||
# processed as a module.
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""OCRmyPDF's multiprocessing/multithreading abstraction layer."""
|
||||
|
||||
import logging
|
||||
@@ -104,6 +105,15 @@ def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
||||
class StandardExecutor(Executor):
|
||||
"""Standard OCRmyPDF concurrent task executor."""
|
||||
|
||||
def _cancel_futures_kwargs(self):
|
||||
"""Shim older Pythons that do not have Executor.shutdown(...cancel_futures=).
|
||||
|
||||
Remove this code when support for Python 3.8 is dropped.
|
||||
"""
|
||||
if sys.version_info[:2] < (3, 9):
|
||||
return {}
|
||||
return dict(cancel_futures=True)
|
||||
|
||||
def _execute(
|
||||
self,
|
||||
*,
|
||||
@@ -142,7 +152,7 @@ class StandardExecutor(Executor):
|
||||
task_finished(result, pbar)
|
||||
except KeyboardInterrupt:
|
||||
# Terminate pool so we exit instantly
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
|
||||
raise
|
||||
except Exception:
|
||||
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
||||
@@ -151,7 +161,7 @@ class StandardExecutor(Executor):
|
||||
# results will be discard. But if the condition above is True,
|
||||
# then we are running in pytest, and we want everything to exit
|
||||
# as cleanly as possible so that we get good error messages.
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
|
||||
raise
|
||||
finally:
|
||||
# Terminate log listener
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
"""OCRmyPDF automatically installs these filters as plugins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Built-in plugin to implement PDF page rasterization and PDF/A production."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
"""Built-in plugin to implement PDF page optimization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Sequence, Tuple
|
||||
from typing import Sequence
|
||||
|
||||
from ocrmypdf import PdfContext, hookimpl
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf import Executor, PdfContext, hookimpl
|
||||
from ocrmypdf._exec import jbig2enc, pngquant
|
||||
from ocrmypdf._pipeline import get_pdf_save_settings
|
||||
from ocrmypdf.cli import numeric
|
||||
@@ -131,7 +132,7 @@ def optimize_pdf(
|
||||
context: PdfContext,
|
||||
executor: Executor,
|
||||
linearize: bool,
|
||||
) -> Tuple[Path, Sequence[str]]:
|
||||
) -> tuple[Path, Sequence[str]]:
|
||||
save_settings = dict(
|
||||
linearize=linearize,
|
||||
**get_pdf_save_settings(context.options.output_type),
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"""Built-in plugin to implement OCR using Tesseract."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
+4
-4
@@ -6,8 +6,10 @@
|
||||
|
||||
"""Command line interface customization and validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Any, Callable, Mapping, Optional, TypeVar
|
||||
from typing import Any, Callable, Mapping, TypeVar
|
||||
|
||||
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
||||
from ocrmypdf._version import __version__ as _VERSION
|
||||
@@ -15,9 +17,7 @@ from ocrmypdf._version import __version__ as _VERSION
|
||||
T = TypeVar('T', int, float)
|
||||
|
||||
|
||||
def numeric(
|
||||
basetype: Callable[[Any], T], min_: Optional[T] = None, max_: Optional[T] = None
|
||||
):
|
||||
def numeric(basetype: Callable[[Any], T], min_: T | None = None, max_: T | None = None):
|
||||
"""Validator for numeric params"""
|
||||
min_ = basetype(min_) if min_ is not None else None
|
||||
max_ = basetype(max_) if max_ is not None else None
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
|
||||
|
||||
"""Data files used to generate certain PDFs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
"""OCRmyPDF's exceptions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ be guaranteed, some workers may end up with too much work while others are idle.
|
||||
It is less efficient than the standard implementation, so not th edefault.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import signal
|
||||
@@ -28,7 +30,7 @@ from enum import Enum, auto
|
||||
from itertools import islice, repeat, takewhile, zip_longest
|
||||
from multiprocessing import Pipe, Process
|
||||
from multiprocessing.connection import Connection, wait
|
||||
from typing import Callable, Iterable, Iterator, List
|
||||
from typing import Callable, Iterable, Iterator
|
||||
|
||||
from ocrmypdf import Executor, hookimpl
|
||||
from ocrmypdf._concurrent import NullProgressBar
|
||||
@@ -134,8 +136,8 @@ class LambdaExecutor(Executor):
|
||||
if not grouped_args:
|
||||
return
|
||||
|
||||
processes: List[Process] = []
|
||||
connections: List[Connection] = []
|
||||
processes: list[Process] = []
|
||||
connections: list[Connection] = []
|
||||
for chunk in grouped_args:
|
||||
parent_conn, child_conn = Pipe()
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
"""Support functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
@@ -30,17 +30,24 @@
|
||||
|
||||
"""Transform .hocr and page image to text PDF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from math import atan, cos, sin
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple, Optional, Tuple, Union
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from reportlab.lib.colors import black, cyan, magenta, red
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
'ignore', category=DeprecationWarning, message=r".*load_module.*"
|
||||
)
|
||||
from reportlab.lib.colors import black, cyan, magenta, red
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
# According to Wikipedia these languages are supported in the ISO-8859-1 character
|
||||
# set, meaning reportlab can generate them and they are compatible with hocr,
|
||||
@@ -134,7 +141,7 @@ class HocrTransform:
|
||||
{'ff': 'ff', 'ffi': 'ffi', 'ffl': 'ffl', 'fi': 'fi', 'fl': 'fl'}
|
||||
)
|
||||
|
||||
def __init__(self, *, hocr_filename: Union[str, Path], dpi: float):
|
||||
def __init__(self, *, hocr_filename: str | Path, dpi: float):
|
||||
self.dpi = dpi
|
||||
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
|
||||
|
||||
@@ -198,7 +205,7 @@ class HocrTransform:
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def baseline(cls, element: Element) -> Tuple[float, float]:
|
||||
def baseline(cls, element: Element) -> tuple[float, float]:
|
||||
"""
|
||||
Returns a tuple containing the baseline slope and intercept.
|
||||
"""
|
||||
@@ -214,7 +221,7 @@ class HocrTransform:
|
||||
"""
|
||||
return Rect._make((c / self.dpi * inch) for c in pxl)
|
||||
|
||||
def _child_xpath(self, html_tag: str, html_class: Optional[str] = None) -> str:
|
||||
def _child_xpath(self, html_tag: str, html_class: str | None = None) -> str:
|
||||
xpath = f".//{self.xmlns}{html_tag}"
|
||||
if html_class:
|
||||
xpath += f"[@class='{html_class}']"
|
||||
@@ -241,7 +248,7 @@ class HocrTransform:
|
||||
self,
|
||||
*,
|
||||
out_filename: Path,
|
||||
image_filename: Optional[Path] = None,
|
||||
image_filename: Path | None = None,
|
||||
show_bounding_boxes: bool = False,
|
||||
fontname: str = "Helvetica",
|
||||
invisible_text: bool = False,
|
||||
@@ -344,7 +351,7 @@ class HocrTransform:
|
||||
def _do_line(
|
||||
self,
|
||||
pdf: Canvas,
|
||||
line: Optional[Element],
|
||||
line: Element | None,
|
||||
elemclass: str,
|
||||
fontname: str,
|
||||
invisible_text: bool,
|
||||
|
||||
+20
-29
@@ -7,6 +7,8 @@
|
||||
"""Post-processing image optimization of OCR PDFs."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -14,18 +16,7 @@ import threading
|
||||
from collections import defaultdict
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
MutableSet,
|
||||
NamedTuple,
|
||||
NewType,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
from typing import Callable, Iterator, MutableSet, NamedTuple, NewType, Sequence
|
||||
from zlib import compress
|
||||
|
||||
import img2pdf
|
||||
@@ -78,7 +69,7 @@ def jpg_name(root: Path, xref: Xref) -> Path:
|
||||
|
||||
def extract_image_filter(
|
||||
pike: Pdf, root: Path, image: Stream, xref: Xref
|
||||
) -> Optional[Tuple[PdfImage, Tuple[Name, Object]]]:
|
||||
) -> tuple[PdfImage, tuple[Name, Object]] | None:
|
||||
del pike # unused args
|
||||
del root
|
||||
|
||||
@@ -135,7 +126,7 @@ def extract_image_filter(
|
||||
|
||||
def extract_image_jbig2(
|
||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||
) -> Optional[XrefExt]:
|
||||
) -> XrefExt | None:
|
||||
del options # unused arg
|
||||
|
||||
result = extract_image_filter(pike, root, image, xref)
|
||||
@@ -176,7 +167,7 @@ def extract_image_jbig2(
|
||||
|
||||
def extract_image_generic(
|
||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||
) -> Optional[XrefExt]:
|
||||
) -> XrefExt | None:
|
||||
result = extract_image_filter(pike, root, image, xref)
|
||||
if result is None:
|
||||
return None
|
||||
@@ -240,8 +231,8 @@ def extract_images(
|
||||
pike: Pdf,
|
||||
root: Path,
|
||||
options,
|
||||
extract_fn: Callable[..., Optional[XrefExt]],
|
||||
) -> Iterator[Tuple[int, XrefExt]]:
|
||||
extract_fn: Callable[..., XrefExt | None],
|
||||
) -> Iterator[tuple[int, XrefExt]]:
|
||||
"""Extract image using extract_fn
|
||||
|
||||
Enumerate images on each page, lookup their xref/ID number in the PDF.
|
||||
@@ -300,7 +291,7 @@ def extract_images(
|
||||
|
||||
def extract_images_generic(
|
||||
pike: Pdf, root: Path, options
|
||||
) -> Tuple[List[Xref], List[Xref]]:
|
||||
) -> tuple[list[Xref], list[Xref]]:
|
||||
"""Extract any >=2bpp image we think we can improve"""
|
||||
|
||||
jpegs = []
|
||||
@@ -315,7 +306,7 @@ def extract_images_generic(
|
||||
return jpegs, pngs
|
||||
|
||||
|
||||
def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefExt]]:
|
||||
def extract_images_jbig2(pike: Pdf, root: Path, options) -> dict[int, list[XrefExt]]:
|
||||
"""Extract any bitonal image that we think we can improve as JBIG2"""
|
||||
|
||||
jbig2_groups = defaultdict(list)
|
||||
@@ -328,11 +319,11 @@ def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefE
|
||||
|
||||
|
||||
def _produce_jbig2_images(
|
||||
jbig2_groups: Dict[int, List[XrefExt]], root: Path, options, executor: Executor
|
||||
jbig2_groups: dict[int, list[XrefExt]], root: Path, options, executor: Executor
|
||||
) -> None:
|
||||
"""Produce JBIG2 images from their groups"""
|
||||
|
||||
def jbig2_group_args(root: Path, groups: Dict[int, List[XrefExt]]):
|
||||
def jbig2_group_args(root: Path, groups: dict[int, list[XrefExt]]):
|
||||
for group, xref_exts in groups.items():
|
||||
prefix = f'group{group:08d}'
|
||||
yield (
|
||||
@@ -341,7 +332,7 @@ def _produce_jbig2_images(
|
||||
prefix, # =out_prefix
|
||||
)
|
||||
|
||||
def jbig2_single_args(root, groups: Dict[int, List[XrefExt]]):
|
||||
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
|
||||
for group, xref_exts in groups.items():
|
||||
prefix = f'group{group:08d}'
|
||||
# Second loop is to ensure multiple images per page are unpacked
|
||||
@@ -376,7 +367,7 @@ def _produce_jbig2_images(
|
||||
|
||||
def convert_to_jbig2(
|
||||
pike: Pdf,
|
||||
jbig2_groups: Dict[int, List[XrefExt]],
|
||||
jbig2_groups: dict[int, list[XrefExt]],
|
||||
root: Path,
|
||||
options,
|
||||
executor: Executor,
|
||||
@@ -393,7 +384,7 @@ def convert_to_jbig2(
|
||||
When the JBIG2 symbolic coder is not used, each JBIG2 stands on its own
|
||||
and needs no dictionary. Currently this must be lossless JBIG2.
|
||||
"""
|
||||
jbig2_globals_dict: Optional[Dictionary]
|
||||
jbig2_globals_dict: Dictionary | None
|
||||
|
||||
_produce_jbig2_images(jbig2_groups, root, options, executor)
|
||||
|
||||
@@ -419,7 +410,7 @@ def convert_to_jbig2(
|
||||
)
|
||||
|
||||
|
||||
def _optimize_jpeg(args: Tuple[Xref, Path, Path, int]) -> Tuple[Xref, Optional[Path]]:
|
||||
def _optimize_jpeg(args: tuple[Xref, Path, Path, int]) -> tuple[Xref, Path | None]:
|
||||
xref, in_jpg, opt_jpg, jpeg_quality = args
|
||||
|
||||
with Image.open(in_jpg) as im:
|
||||
@@ -435,13 +426,13 @@ def _optimize_jpeg(args: Tuple[Xref, Path, Path, int]) -> Tuple[Xref, Optional[P
|
||||
def transcode_jpegs(
|
||||
pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor
|
||||
) -> None:
|
||||
def jpeg_args() -> Iterator[Tuple[Xref, Path, Path, int]]:
|
||||
def jpeg_args() -> Iterator[tuple[Xref, Path, Path, int]]:
|
||||
for xref in jpegs:
|
||||
in_jpg = jpg_name(root, xref)
|
||||
opt_jpg = in_jpg.with_suffix('.opt.jpg')
|
||||
yield xref, in_jpg, opt_jpg, options.jpeg_quality
|
||||
|
||||
def finish_jpeg(result: Tuple[Xref, Optional[Path]], pbar):
|
||||
def finish_jpeg(result: tuple[Xref, Path | None], pbar):
|
||||
xref, opt_jpg = result
|
||||
if opt_jpg:
|
||||
compdata = opt_jpg.read_bytes() # JPEG can inserted into PDF as is
|
||||
@@ -466,7 +457,7 @@ def transcode_jpegs(
|
||||
|
||||
def _find_deflatable_jpeg(
|
||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||
) -> Optional[XrefExt]:
|
||||
) -> XrefExt | None:
|
||||
result = extract_image_filter(pike, root, image, xref)
|
||||
if result is None:
|
||||
return None
|
||||
@@ -478,7 +469,7 @@ def _find_deflatable_jpeg(
|
||||
return None
|
||||
|
||||
|
||||
def _deflate_jpeg(args: Tuple[Pdf, threading.Lock, Xref, int]) -> Tuple[Xref, bytes]:
|
||||
def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, bytes]:
|
||||
pike, lock, xref, complevel = args
|
||||
with lock:
|
||||
xobj = pike.get_object(xref, 0)
|
||||
|
||||
@@ -9,14 +9,16 @@
|
||||
Utilities for PDF/A production and confirmation with Ghostspcript.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, Union
|
||||
from typing import Iterator
|
||||
|
||||
try:
|
||||
from importlib_resources import files as package_files
|
||||
except ImportError:
|
||||
from importlib.resources import files as package_files
|
||||
except ImportError:
|
||||
from importlib_resources import files as package_files # type: ignore
|
||||
|
||||
import pikepdf
|
||||
|
||||
@@ -25,7 +27,7 @@ SRGB_ICC_PROFILE_NAME = 'sRGB.icc'
|
||||
|
||||
def _postscript_objdef(
|
||||
alias: str,
|
||||
dictionary: Dict[str, str],
|
||||
dictionary: dict[str, str],
|
||||
*,
|
||||
stream_name: str = None,
|
||||
stream_data: bytes = None,
|
||||
@@ -131,7 +133,7 @@ def file_claims_pdfa(filename: Path):
|
||||
}
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||
pdfa_dict: Dict[str, Union[str, bool]] = {}
|
||||
pdfa_dict: dict[str, str | bool] = {}
|
||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
|
||||
@@ -8,4 +8,6 @@
|
||||
|
||||
"""For extracting information about PDFs prior to OCR."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PdfInfo
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
"""Extract information about the content of a PDF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import re
|
||||
@@ -21,16 +23,13 @@ from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Container,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
from warnings import warn
|
||||
|
||||
@@ -85,7 +84,7 @@ class Encoding(Enum):
|
||||
|
||||
FloatRect = Tuple[float, float, float, float]
|
||||
|
||||
FRIENDLY_COLORSPACE: Dict[str, Colorspace] = {
|
||||
FRIENDLY_COLORSPACE: dict[str, Colorspace] = {
|
||||
'/DeviceGray': Colorspace.gray,
|
||||
'/CalGray': Colorspace.gray,
|
||||
'/DeviceRGB': Colorspace.rgb,
|
||||
@@ -103,7 +102,7 @@ FRIENDLY_COLORSPACE: Dict[str, Colorspace] = {
|
||||
'/I': Colorspace.index,
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING: Dict[str, Encoding] = {
|
||||
FRIENDLY_ENCODING: dict[str, Encoding] = {
|
||||
'/CCITTFaxDecode': Encoding.ccitt,
|
||||
'/DCTDecode': Encoding.jpeg,
|
||||
'/JPXDecode': Encoding.jpeg2000,
|
||||
@@ -117,7 +116,7 @@ FRIENDLY_ENCODING: Dict[str, Encoding] = {
|
||||
'/RL': Encoding.runlength,
|
||||
}
|
||||
|
||||
FRIENDLY_COMP: Dict[Colorspace, int] = {
|
||||
FRIENDLY_COMP: dict[Colorspace, int] = {
|
||||
Colorspace.gray: 1,
|
||||
Colorspace.rgb: 3,
|
||||
Colorspace.cmyk: 4,
|
||||
@@ -139,7 +138,7 @@ class XobjectSettings(NamedTuple):
|
||||
"""Info about an XObject found in a PDF."""
|
||||
|
||||
name: str
|
||||
shorthand: Tuple[float, float, float, float, float, float]
|
||||
shorthand: tuple[float, float, float, float, float, float]
|
||||
stack_depth: int
|
||||
|
||||
|
||||
@@ -147,24 +146,24 @@ class InlineSettings(NamedTuple):
|
||||
"""Info about an inline image found in a PDF."""
|
||||
|
||||
iimage: PdfInlineImage
|
||||
shorthand: Tuple[float, float, float, float, float, float]
|
||||
shorthand: tuple[float, float, float, float, float, float]
|
||||
stack_depth: int
|
||||
|
||||
|
||||
class ContentsInfo(NamedTuple):
|
||||
"""Info about various objects found in a PDF."""
|
||||
|
||||
xobject_settings: List[XobjectSettings]
|
||||
inline_images: List[InlineSettings]
|
||||
xobject_settings: list[XobjectSettings]
|
||||
inline_images: list[InlineSettings]
|
||||
found_vector: bool
|
||||
found_text: bool
|
||||
name_index: Mapping[str, List[XobjectSettings]]
|
||||
name_index: Mapping[str, list[XobjectSettings]]
|
||||
|
||||
|
||||
class TextboxInfo(NamedTuple):
|
||||
"""Info about a text box found in a PDF."""
|
||||
|
||||
bbox: Tuple[float, float, float, float]
|
||||
bbox: tuple[float, float, float, float]
|
||||
is_visible: bool
|
||||
is_corrupt: bool
|
||||
|
||||
@@ -217,8 +216,8 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
||||
|
||||
stack = []
|
||||
ctm = PdfMatrix(initial_shorthand)
|
||||
xobject_settings: List[XobjectSettings] = []
|
||||
inline_images: List[InlineSettings] = []
|
||||
xobject_settings: list[XobjectSettings] = []
|
||||
inline_images: list[InlineSettings] = []
|
||||
name_index = defaultdict(lambda: [])
|
||||
found_vector = False
|
||||
found_text = False
|
||||
@@ -342,21 +341,21 @@ class ImageInfo:
|
||||
|
||||
DPI_PREC = Decimal('1.000')
|
||||
|
||||
_comp: Optional[int]
|
||||
_comp: int | None
|
||||
_name: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name='',
|
||||
pdfimage: Optional[Object] = None,
|
||||
inline: Optional[PdfInlineImage] = None,
|
||||
pdfimage: Object | None = None,
|
||||
inline: PdfInlineImage | None = None,
|
||||
shorthand=None,
|
||||
):
|
||||
self._name = str(name)
|
||||
self._shorthand = shorthand
|
||||
|
||||
pim: Union[PdfInlineImage, PdfImage]
|
||||
pim: PdfInlineImage | PdfImage
|
||||
|
||||
if inline is not None:
|
||||
self._origin = 'inline'
|
||||
@@ -400,7 +399,7 @@ class ImageInfo:
|
||||
self._comp = 4
|
||||
else:
|
||||
self._comp = 3
|
||||
except UnsupportedImageTypeError as ex:
|
||||
except (AttributeError, UnsupportedImageTypeError) as ex:
|
||||
self._comp = None
|
||||
logger.warning(
|
||||
f"An image with a corrupt or unreadable ICC profile was found. "
|
||||
@@ -473,7 +472,7 @@ def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]:
|
||||
)
|
||||
|
||||
|
||||
def _image_xobjects(container) -> Iterator[Tuple[Object, str]]:
|
||||
def _image_xobjects(container) -> Iterator[tuple[Object, str]]:
|
||||
"""Search for all XObject-based images in the container
|
||||
|
||||
Usually the container is a page, but it could also be a Form XObject
|
||||
@@ -561,7 +560,7 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content
|
||||
|
||||
def _process_content_streams(
|
||||
*, pdf: Pdf, container: Object, shorthand=None
|
||||
) -> Iterator[Union[VectorMarker, TextMarker, ImageInfo]]:
|
||||
) -> Iterator[VectorMarker | TextMarker | ImageInfo]:
|
||||
"""Find all individual instances of images drawn in the container
|
||||
|
||||
Usually the container is a page, but it may also be a Form XObject.
|
||||
@@ -691,8 +690,8 @@ def _pdf_pageinfo_concurrent(
|
||||
max_workers,
|
||||
check_pages,
|
||||
detailed_analysis=False,
|
||||
) -> Sequence[Optional['PageInfo']]:
|
||||
pages: Sequence[Optional['PageInfo']] = [None] * len(pdf.pages)
|
||||
) -> Sequence[PageInfo | None]:
|
||||
pages: Sequence[PageInfo | None] = [None] * len(pdf.pages)
|
||||
|
||||
def update_pageinfo(result, pbar):
|
||||
page = result
|
||||
@@ -744,9 +743,9 @@ def _pdf_pageinfo_concurrent(
|
||||
class PageInfo:
|
||||
"""Information about type of contents on each page in a PDF."""
|
||||
|
||||
_has_text: Optional[bool]
|
||||
_has_vector: Optional[bool]
|
||||
_images: List[ImageInfo]
|
||||
_has_text: bool | None
|
||||
_has_vector: bool | None
|
||||
_images: list[ImageInfo]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -879,9 +878,7 @@ class PageInfo:
|
||||
def images(self):
|
||||
return self._images
|
||||
|
||||
def get_textareas(
|
||||
self, visible: Optional[bool] = None, corrupt: Optional[bool] = None
|
||||
):
|
||||
def get_textareas(self, visible: bool | None = None, corrupt: bool | None = None):
|
||||
def predicate(obj, want_visible, want_corrupt):
|
||||
result = True
|
||||
if want_visible is not None:
|
||||
@@ -965,7 +962,7 @@ class PdfInfo:
|
||||
self._has_acroform = True
|
||||
|
||||
@property
|
||||
def pages(self) -> Sequence[Optional[PageInfo]]:
|
||||
def pages(self) -> Sequence[PageInfo | None]:
|
||||
return self._pages
|
||||
|
||||
@property
|
||||
@@ -982,7 +979,7 @@ class PdfInfo:
|
||||
return self._has_acroform
|
||||
|
||||
@property
|
||||
def filename(self) -> Union[str, Path]:
|
||||
def filename(self) -> str | Path:
|
||||
if not isinstance(self._infile, (str, Path)):
|
||||
raise NotImplementedError("can't get filename from stream")
|
||||
return self._infile
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from math import copysign
|
||||
from pathlib import Path
|
||||
|
||||
+44
-23
@@ -6,24 +6,17 @@
|
||||
|
||||
"""OCRmyPDF pluggy plugin specification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from logging import Handler
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AbstractSet,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
from typing import TYPE_CHECKING, AbstractSet, NamedTuple, Sequence
|
||||
|
||||
import pluggy
|
||||
|
||||
from ocrmypdf import PdfContext
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf import Executor, PdfContext
|
||||
from ocrmypdf.helpers import Resolution
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -52,6 +45,33 @@ def get_logging_console() -> Handler:
|
||||
"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def initialize(plugin_manager: pluggy.PluginManager):
|
||||
"""Called when this plugin is first loaded into OCRmyPDF.
|
||||
|
||||
The primary intended use of this is for plugins to check compatibility with other
|
||||
plugins and possibly block other blocks, a plugin that wishes to block ocrmypdf's
|
||||
built-in optimize plugin could do:
|
||||
|
||||
.. code-block::
|
||||
|
||||
plugin_manager.set_blocked('ocrmypdf.builtin_plugins.optimize')
|
||||
|
||||
It would also be reasonable for an plugin implementation to check if it is unable
|
||||
to proceed, for example, because a required dependency is missing. (If the plugin's
|
||||
ability to proceed depends on options and arguments, use ``validate`` instead.)
|
||||
|
||||
Raises:
|
||||
ocrmypdf.exceptions.ExitCodeException: If options are not acceptable
|
||||
and the application should terminate gracefully with an informative
|
||||
message and error code.
|
||||
|
||||
Note:
|
||||
This hook will be called from the main process, and may modify global state
|
||||
before child worker processes are forked.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def add_options(parser: ArgumentParser) -> None:
|
||||
"""Allows the plugin to add its own command line and API arguments.
|
||||
@@ -151,7 +171,7 @@ def get_progressbar_class():
|
||||
|
||||
|
||||
@hookspec
|
||||
def validate(pdfinfo: 'PdfInfo', options: Namespace) -> None:
|
||||
def validate(pdfinfo: PdfInfo, options: Namespace) -> None:
|
||||
"""Called to give a plugin an opportunity to review *options* and *pdfinfo*.
|
||||
|
||||
*options* contains the "work order" to process a particular file. *pdfinfo*
|
||||
@@ -177,8 +197,8 @@ def rasterize_pdf_page(
|
||||
raster_device: str,
|
||||
raster_dpi: Resolution,
|
||||
pageno: int,
|
||||
page_dpi: Optional[Resolution],
|
||||
rotation: Optional[int],
|
||||
page_dpi: Resolution | None,
|
||||
rotation: int | None,
|
||||
filter_vector: bool,
|
||||
) -> Path:
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
|
||||
@@ -207,7 +227,7 @@ def rasterize_pdf_page(
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def filter_ocr_image(page: 'PageContext', image: 'Image.Image') -> 'Image.Image':
|
||||
def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
|
||||
"""Called to filter the image before it is sent to OCR.
|
||||
|
||||
This is the image that OCR sees, not what the user sees when they view the
|
||||
@@ -234,7 +254,7 @@ def filter_ocr_image(page: 'PageContext', image: 'Image.Image') -> 'Image.Image'
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
|
||||
def filter_page_image(page: PageContext, image_filename: Path) -> Path:
|
||||
"""Called to filter the whole page before it is inserted into the PDF.
|
||||
|
||||
A whole page image is only produced when preprocessing command line arguments
|
||||
@@ -274,9 +294,7 @@ def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def filter_pdf_page(
|
||||
page: 'PageContext', image_filename: Path, output_pdf: Path
|
||||
) -> Path:
|
||||
def filter_pdf_page(page: PageContext, image_filename: Path, output_pdf: Path) -> Path:
|
||||
"""Called to convert a filtered whole page image into a PDF.
|
||||
|
||||
A whole page image is only produced when preprocessing command line arguments
|
||||
@@ -419,7 +437,7 @@ def get_ocr_engine() -> OcrEngine:
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def generate_pdfa(
|
||||
pdf_pages: List[Path],
|
||||
pdf_pages: list[Path],
|
||||
pdfmark: Path,
|
||||
output_file: Path,
|
||||
compression: str,
|
||||
@@ -475,7 +493,7 @@ def optimize_pdf(
|
||||
context: PdfContext,
|
||||
executor: Executor,
|
||||
linearize: bool,
|
||||
) -> Tuple[Path, Sequence[str]]:
|
||||
) -> tuple[Path, Sequence[str]]:
|
||||
"""Optimize a PDF after image, OCR and metadata processing.
|
||||
|
||||
If the input_pdf is a PDF/A, the plugin should modify input_pdf in a way
|
||||
@@ -484,6 +502,9 @@ def optimize_pdf(
|
||||
If the implementation fails to produce a smaller file than the input file, it
|
||||
should return input_pdf instead.
|
||||
|
||||
A plugin that implements a new optimizer may need to suppress the built-in
|
||||
optimizer by implementing an ``initialize`` hook.
|
||||
|
||||
Arguments:
|
||||
input_pdf: The input PDF, which has OCR added.
|
||||
output_pdf: The requested filename of the output PDF which should be created
|
||||
@@ -512,8 +533,8 @@ def optimize_pdf(
|
||||
def is_optimization_enabled(context: PdfContext) -> bool:
|
||||
"""For a given PdfContext, OCRmyPDF asks the plugin if optimization is enabled.
|
||||
|
||||
It is assumed that an optimization plugin might be installed but could be
|
||||
disabled by user settings.
|
||||
An optimization plugin might be installed and active but could be disabled by
|
||||
user settings.
|
||||
|
||||
If this returns False, OCRmyPDF will take certain actions to finalize the PDF.
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"""Utilities to measure OCR quality"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
|
||||
"""Wrappers to manage subprocess calls"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
||||
from subprocess import run as subprocess_run
|
||||
from typing import Callable, Optional, Type, Union
|
||||
from typing import Callable, Mapping, Sequence, Union
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
@@ -26,11 +28,14 @@ from ocrmypdf.exceptions import MissingDependencyError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
Args = Sequence[Union[Path, str]]
|
||||
OsEnviron = os._Environ # pylint: disable=protected-access
|
||||
|
||||
|
||||
def run(
|
||||
args,
|
||||
args: Args,
|
||||
*,
|
||||
env=None,
|
||||
env: OsEnviron | None = None,
|
||||
logs_errors_to_stdout: bool = False,
|
||||
check: bool = False,
|
||||
**kwargs,
|
||||
@@ -73,7 +78,12 @@ def run(
|
||||
|
||||
|
||||
def run_polling_stderr(
|
||||
args, *, callback: Callable[[str], None], check: bool = False, env=None, **kwargs
|
||||
args: Args,
|
||||
*,
|
||||
callback: Callable[[str], None],
|
||||
check: bool = False,
|
||||
env: OsEnviron | None = None,
|
||||
**kwargs,
|
||||
) -> CompletedProcess:
|
||||
"""Run a process like ``ocrmypdf.subprocess.run``, and poll stderr.
|
||||
|
||||
@@ -106,7 +116,9 @@ def run_polling_stderr(
|
||||
return CompletedProcess(args, proc.returncode, None, stderr=stderr)
|
||||
|
||||
|
||||
def _fix_process_args(args, env, kwargs):
|
||||
def _fix_process_args(
|
||||
args: Args, env: OsEnviron | None, kwargs
|
||||
) -> tuple[Args, OsEnviron, logging.Logger, bool]:
|
||||
assert 'universal_newlines' not in kwargs, "Use text= instead of universal_newlines"
|
||||
|
||||
if not env:
|
||||
@@ -115,7 +127,7 @@ def _fix_process_args(args, env, kwargs):
|
||||
# Search in spoof path if necessary
|
||||
program = str(args[0])
|
||||
|
||||
if os.name == 'nt':
|
||||
if sys.platform == 'win32':
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from ocrmypdf.subprocess._windows import fix_windows_args
|
||||
|
||||
@@ -123,14 +135,18 @@ def _fix_process_args(args, env, kwargs):
|
||||
|
||||
log.debug("Running: %s", args)
|
||||
process_log = log.getChild(os.path.basename(program))
|
||||
text = kwargs.get('text', False)
|
||||
text = bool(kwargs.get('text', False))
|
||||
|
||||
return args, env, process_log, text
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_version(
|
||||
program: str, *, version_arg: str = '--version', regex=r'(\d+(\.\d+)*)', env=None
|
||||
program: str,
|
||||
*,
|
||||
version_arg: str = '--version',
|
||||
regex=r'(\d+(\.\d+)*)',
|
||||
env: OsEnviron | None = None,
|
||||
) -> str:
|
||||
"""Get the version of the specified program
|
||||
|
||||
@@ -229,7 +245,7 @@ a command prompt to install the missing package:
|
||||
'''
|
||||
|
||||
|
||||
def _get_platform():
|
||||
def _get_platform() -> str:
|
||||
if sys.platform.startswith('freebsd'):
|
||||
return 'freebsd'
|
||||
elif sys.platform.startswith('linux'):
|
||||
@@ -239,7 +255,7 @@ def _get_platform():
|
||||
return sys.platform
|
||||
|
||||
|
||||
def _error_trailer(program, package, **kwargs):
|
||||
def _error_trailer(program: str, package: str | Mapping[str, str], **kwargs) -> None:
|
||||
del kwargs
|
||||
if isinstance(package, Mapping):
|
||||
package = package.get(_get_platform(), program)
|
||||
@@ -252,7 +268,9 @@ def _error_trailer(program, package, **kwargs):
|
||||
log.info(WINDOWS_INSTALL_ADVICE.format(**locals()))
|
||||
|
||||
|
||||
def _error_missing_program(program, package, required_for, recommended):
|
||||
def _error_missing_program(
|
||||
program: str, package: str, required_for: str | None, recommended: bool
|
||||
) -> None:
|
||||
# pylint: disable=unused-argument
|
||||
if recommended:
|
||||
log.warning(MISSING_RECOMMEND_PROGRAM.format(**locals()))
|
||||
@@ -263,7 +281,13 @@ def _error_missing_program(program, package, required_for, recommended):
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def _error_old_version(program, package, need_version, found_version, required_for):
|
||||
def _error_old_version(
|
||||
program: str,
|
||||
package: str,
|
||||
need_version: str,
|
||||
found_version: str,
|
||||
required_for: str | None,
|
||||
) -> None:
|
||||
# pylint: disable=unused-argument
|
||||
if required_for:
|
||||
log.error(OLD_VERSION_REQUIRED_FOR.format(**locals()))
|
||||
@@ -272,7 +296,7 @@ def _error_old_version(program, package, need_version, found_version, required_f
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def _remove_leading_v(s):
|
||||
def _remove_leading_v(s: str) -> str:
|
||||
if sys.version_info >= (3, 9):
|
||||
return s.removeprefix('v')
|
||||
|
||||
@@ -285,12 +309,12 @@ def check_external_program(
|
||||
*,
|
||||
program: str,
|
||||
package: str,
|
||||
version_checker: Callable,
|
||||
version_checker: Callable[[], str],
|
||||
need_version: str,
|
||||
required_for: Optional[str] = None,
|
||||
recommended=False,
|
||||
version_parser: Type[Version] = Version,
|
||||
):
|
||||
required_for: str | None = None,
|
||||
recommended: bool = False,
|
||||
version_parser: type[Version] = Version,
|
||||
) -> None:
|
||||
"""Check for required version of external program and raise exception if not.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -3,31 +3,44 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# type: ignore
|
||||
# Non-Windows mypy now breaks when trying to typecheck winreg
|
||||
|
||||
"""Find Tesseract and Ghostscript binaries on Windows using the registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, Iterator, Set, Tuple, TypeVar
|
||||
from typing import Any, Callable, Iterable, Iterator, TypeVar
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias # pragma: no cover
|
||||
|
||||
if sys.platform == 'win32':
|
||||
# mypy understands 'if sys.platform' better than try/except ModuleNotFoundError
|
||||
import winreg # pylint: disable=import-error
|
||||
|
||||
HKEYType: TypeAlias = winreg.HKEYType
|
||||
else:
|
||||
from unittest.mock import Mock
|
||||
|
||||
winreg = Mock(
|
||||
spec=['HKEYType', 'EnumKey', 'EnumValue', 'HKEY_LOCAL_MACHINE', 'OpenKey']
|
||||
)
|
||||
# mypy does not understand winreg.HKeyType where winreg is a Mock (fair enough!)
|
||||
HKEYType: TypeAlias = Any
|
||||
|
||||
try:
|
||||
import winreg
|
||||
except ModuleNotFoundError as _notfound_ex:
|
||||
raise ModuleNotFoundError("This module is for Windows only") from _notfound_ex
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def ghostscript_version_key(s: str) -> Tuple[int, int, int]:
|
||||
def ghostscript_version_key(s: str) -> tuple[int, int, int]:
|
||||
"""Compare Ghostscript version numbers."""
|
||||
try:
|
||||
release = [int(elem) for elem in s.split('.', maxsplit=3)]
|
||||
@@ -38,9 +51,7 @@ def ghostscript_version_key(s: str) -> Tuple[int, int, int]:
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def registry_enum(
|
||||
key: winreg.HKEYType, enum_fn: Callable[[winreg.HKEYType, int], T]
|
||||
) -> Iterator[T]:
|
||||
def registry_enum(key: HKEYType, enum_fn: Callable[[HKEYType, int], T]) -> Iterator[T]:
|
||||
limit = 999
|
||||
n = 0
|
||||
while n < limit:
|
||||
@@ -53,11 +64,11 @@ def registry_enum(
|
||||
raise ValueError(f"Too many registry keys under {key}")
|
||||
|
||||
|
||||
def registry_subkeys(key: winreg.HKEYType) -> Iterator[str]:
|
||||
def registry_subkeys(key: HKEYType) -> Iterator[str]:
|
||||
return registry_enum(key, winreg.EnumKey)
|
||||
|
||||
|
||||
def registry_values(key: winreg.HKEYType) -> Iterator[Tuple[str, Any, int]]:
|
||||
def registry_values(key: HKEYType) -> Iterator[tuple[str, Any, int]]:
|
||||
return registry_enum(key, winreg.EnumValue)
|
||||
|
||||
|
||||
@@ -73,8 +84,8 @@ def registry_path_ghostscript(env=None) -> Iterator[Path]:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
|
||||
) as k:
|
||||
_, gs_path, _ = next(registry_values(k))
|
||||
yield Path(gs_path) / 'bin'
|
||||
for _, gs_path, _ in registry_values(k):
|
||||
yield Path(gs_path) / 'bin'
|
||||
except OSError as e:
|
||||
log.warning(e)
|
||||
|
||||
@@ -160,7 +171,7 @@ def unique_everseen(iterable: Iterable[T], key: Callable[[T], T]) -> Iterator[T]
|
||||
"List unique elements, preserving order."
|
||||
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||
seen: Set[T] = set()
|
||||
seen: set[T] = set()
|
||||
seen_add = seen.add
|
||||
for element in iterable:
|
||||
k = key(element)
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Empty __init__.py file
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
Tesseract Open Source OCR Engine v4.1.1 with Leptonica
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Portez ce vieux whisky au juge
|
||||
blond qui fume sur son Ile
|
||||
interieure, a cöte de l'alcöve
|
||||
ovoide, oU les büches se
|
||||
consument dans l'ätre, ce qui
|
||||
lui permet de penser & la
|
||||
caenogenese de |'etre dont il
|
||||
est question dans la cause
|
||||
ambigu& entendue a MoY, dans
|
||||
un capharnaüm qui, pense-t-il,
|
||||
diminue ca et la la qualite de son
|
||||
ceuvre.
|
||||
|
||||
Vendored
+1
@@ -82,3 +82,4 @@
|
||||
{"tesseract_version": "4.1.1", "system": "Linux", "python": "3.9.5", "argv_slug": "__-l__eng__--oem__1__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "--oem", "1", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "5.0.0", "system": "Linux", "python": "3.9.5", "argv_slug": "__-l__eng__thresholding_method=1__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "-c", "thresholding_method=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "5.0.0", "system": "Linux", "python": "3.9.5", "argv_slug": "__-l__eng__thresholding_method=2__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "-c", "thresholding_method=2", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "4.1.1", "system": "Linux", "python": "3.10.4", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
|
||||
+3
-1
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
@@ -52,7 +54,7 @@ def resources() -> Path:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ocrmypdf_exec() -> List[str]:
|
||||
def ocrmypdf_exec() -> list[str]:
|
||||
return [sys.executable, '-m', 'ocrmypdf']
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ that is not UTF-8 compatible, so we are forced to check that we can convert it
|
||||
and present it to the user.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -44,6 +44,8 @@ Assumes Tesseract 4.0.0-alpha or higher.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
@@ -31,6 +31,8 @@ In 'pdf' mode, convert the image to PDF using another program.
|
||||
In orientation check mode, report 0, 90, 180, 270... based on page number.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ In 'pdf' mode, convert the image to PDF using another program.
|
||||
In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# type: ignore
|
||||
|
||||
"""Tesseract no-op plugin that simulates the OOM killer on page 4.
|
||||
|
||||
OCRmyPDF can use a lot of memory, even that it might trigger the
|
||||
@@ -30,14 +28,18 @@ 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
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
# type: ignore
|
||||
|
||||
|
||||
# Ugly hack that let us use the NoopOcrEngine without setting up packaging for our
|
||||
# tests.
|
||||
# This hack also requires us to set type: ignore
|
||||
@@ -47,7 +49,7 @@ exec(parent)
|
||||
NoopOcrEngine = locals()['NoopOcrEngine']
|
||||
|
||||
|
||||
class Page4Engine(NoopOcrEngine):
|
||||
class Page4Engine(NoopOcrEngine): # type: ignore
|
||||
def __str__(self):
|
||||
return f"NO-OP Page 4 {NoopOcrEngine.version()}"
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf.helpers import check_pdf
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from subprocess import PIPE, run
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pikepdf
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import img2pdf
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from math import isclose
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
from datetime import timezone
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pikepdf
|
||||
|
||||
+32
-13
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pickle
|
||||
from io import BytesIO
|
||||
from math import isclose
|
||||
@@ -24,20 +26,24 @@ from ocrmypdf.pdfinfo.layout import PDFPage
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
def test_single_page_text(outdir):
|
||||
@pytest.fixture
|
||||
def single_page_text(outdir):
|
||||
filename = outdir / 'text.pdf'
|
||||
pdf = Canvas(str(filename), pagesize=(8 * inch, 6 * inch))
|
||||
text = pdf.beginText()
|
||||
text.setFont('Helvetica', 12)
|
||||
text.setTextOrigin(1 * inch, 3 * inch)
|
||||
text.textLine(
|
||||
"Methink'st thou art a general offence and every" " man should beat thee."
|
||||
"Methink'st thou art a general offence and every man should beat thee."
|
||||
)
|
||||
pdf.drawText(text)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
return filename
|
||||
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
|
||||
def test_single_page_text(single_page_text):
|
||||
info = pdfinfo.PdfInfo(single_page_text)
|
||||
|
||||
assert len(info) == 1
|
||||
page = info[0]
|
||||
@@ -54,7 +60,8 @@ def eight_by_eight():
|
||||
return im
|
||||
|
||||
|
||||
def test_single_page_image(eight_by_eight, outpdf):
|
||||
@pytest.fixture
|
||||
def eight_by_eight_regular_image(eight_by_eight, outpdf):
|
||||
im = eight_by_eight
|
||||
bio = BytesIO()
|
||||
im.save(bio, format='PNG')
|
||||
@@ -71,7 +78,11 @@ def test_single_page_image(eight_by_eight, outpdf):
|
||||
outputstream=f,
|
||||
**IMG2PDF_KWARGS,
|
||||
)
|
||||
info = pdfinfo.PdfInfo(outpdf)
|
||||
return outpdf
|
||||
|
||||
|
||||
def test_single_page_image(eight_by_eight_regular_image):
|
||||
info = pdfinfo.PdfInfo(eight_by_eight_regular_image)
|
||||
|
||||
assert len(info) == 1
|
||||
page = info[0]
|
||||
@@ -88,16 +99,18 @@ def test_single_page_image(eight_by_eight, outpdf):
|
||||
assert isclose(pdfimage.dpi.y, 8)
|
||||
|
||||
|
||||
def test_single_page_inline_image(eight_by_eight, outdir):
|
||||
filename = outdir / 'image-mono-inline.pdf'
|
||||
pdf = Canvas(str(filename), pagesize=(8 * 72, 6 * 72))
|
||||
|
||||
@pytest.fixture
|
||||
def eight_by_eight_inline_image(eight_by_eight, outpdf):
|
||||
pdf = Canvas(str(outpdf), pagesize=(8 * 72, 6 * 72))
|
||||
# Draw image in a 72x72 pt or 1"x1" area
|
||||
pdf.drawInlineImage(eight_by_eight, 0, 0, width=72, height=72)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
return outpdf
|
||||
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
|
||||
def test_single_page_inline_image(eight_by_eight_inline_image):
|
||||
info = pdfinfo.PdfInfo(eight_by_eight_inline_image)
|
||||
print(info)
|
||||
pdfimage = info[0].images[0]
|
||||
assert isclose(pdfimage.dpi.x, 8)
|
||||
@@ -177,7 +190,7 @@ def test_stack_abuse():
|
||||
pdfinfo.info._interpret_contents(stream)
|
||||
|
||||
stream = pikepdf.Stream(p, b'q Q Q Q Q')
|
||||
with pytest.warns(UserWarning, match="underflowed") as record:
|
||||
with pytest.warns(UserWarning, match="underflowed"):
|
||||
pdfinfo.info._interpret_contents(stream)
|
||||
|
||||
stream = pikepdf.Stream(p, b'q ' * 135)
|
||||
@@ -201,7 +214,8 @@ def test_pages_issue700(monkeypatch, resources):
|
||||
)
|
||||
|
||||
|
||||
def test_image_scale0(resources, outpdf):
|
||||
@pytest.fixture
|
||||
def image_scale0(resources, outpdf):
|
||||
with pikepdf.open(resources / 'cmyk.pdf') as cmyk:
|
||||
xobj = cmyk.pages[0].as_form_xobject()
|
||||
|
||||
@@ -215,7 +229,12 @@ def test_image_scale0(resources, outpdf):
|
||||
p, b"q 0 0 0 0 0 0 cm %s Do Q" % bytes(objname)
|
||||
)
|
||||
p.save(outpdf)
|
||||
return outpdf
|
||||
|
||||
pi = pdfinfo.PdfInfo(outpdf, detailed_analysis=True, progbar=False, max_workers=1)
|
||||
|
||||
def test_image_scale0(image_scale0):
|
||||
pi = pdfinfo.PdfInfo(
|
||||
image_scale0, detailed_analysis=True, progbar=False, max_workers=1
|
||||
)
|
||||
assert not pi.pages[0]._images[0].dpi.is_finite
|
||||
assert pi.pages[0].dpi == Resolution(0, 0)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from math import isclose
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf import quality as qual
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import operator
|
||||
from io import BytesIO
|
||||
from math import cos, pi, sin
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user