Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6567f2ae4 | ||
|
|
e860c56b75 | ||
|
|
2e15d52895 | ||
|
|
ce97af5a79 | ||
|
|
3831c4cd4d | ||
|
|
61a2674317 | ||
|
|
9ad8cbf1f6 | ||
|
|
123fde174d | ||
|
|
fd991a2380 | ||
|
|
6f5d77d930 | ||
|
|
5169ac633b | ||
|
|
5b6ab1e003 | ||
|
|
8f984bf958 | ||
|
|
9c5f0d0ec6 | ||
|
|
32041c43e1 | ||
|
|
599028bebb | ||
|
|
6faa8f7221 | ||
|
|
a4dc5e365f | ||
|
|
e2a563cc76 | ||
|
|
1037d73efb | ||
|
|
aeb7b142a9 | ||
|
|
422ea9777e | ||
|
|
2f1c743227 | ||
|
|
96ee21aee9 | ||
|
|
4b759af6ff | ||
|
|
25d2b0cda4 | ||
|
|
16dd8b54a8 | ||
|
|
c4dc5269d2 | ||
|
|
c36e9950ae | ||
|
|
0c0d53b10f | ||
|
|
63de7e1677 | ||
|
|
b0e92760a2 | ||
|
|
054c0773a3 | ||
|
|
89aa78b724 | ||
|
|
708113a514 | ||
|
|
95ef5410c2 | ||
|
|
868b3b4abd | ||
|
|
045bdff95a | ||
|
|
d12b27ac1d | ||
|
|
e4e00de79f | ||
|
|
a53a3937c2 | ||
|
|
343424b4d2 | ||
|
|
c5edff2c2f | ||
|
|
8c5f8b8ddd | ||
|
|
39da931a56 | ||
|
|
9fe354359b |
+3
-3
@@ -1,5 +1,3 @@
|
|||||||
# Coverage isn't really compatible with subprocesses so results are unreliable
|
|
||||||
|
|
||||||
[paths]
|
[paths]
|
||||||
source =
|
source =
|
||||||
src
|
src
|
||||||
@@ -8,9 +6,11 @@ source =
|
|||||||
[run]
|
[run]
|
||||||
branch = true
|
branch = true
|
||||||
parallel = true
|
parallel = true
|
||||||
|
concurrency =
|
||||||
|
thread
|
||||||
|
multiprocessing
|
||||||
source =
|
source =
|
||||||
src/ocrmypdf
|
src/ocrmypdf
|
||||||
tests
|
|
||||||
omit =
|
omit =
|
||||||
tests/spoof/*
|
tests/spoof/*
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ RUN pip3 install --no-cache-dir \
|
|||||||
-r requirements/main.txt \
|
-r requirements/main.txt \
|
||||||
-r requirements/webservice.txt \
|
-r requirements/webservice.txt \
|
||||||
-r requirements/test.txt \
|
-r requirements/test.txt \
|
||||||
|
-r requirements/watcher.txt \
|
||||||
.
|
.
|
||||||
|
|
||||||
FROM base
|
FROM base
|
||||||
@@ -69,6 +70,7 @@ COPY --from=builder /usr/local/lib/ /usr/local/lib/
|
|||||||
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
||||||
|
|
||||||
COPY --from=builder /app/misc/webservice.py /app/
|
COPY --from=builder /app/misc/webservice.py /app/
|
||||||
|
COPY --from=builder /app/misc/watcher.py /app/
|
||||||
|
|
||||||
# Copy minimal project files to get the test suite.
|
# Copy minimal project files to get the test suite.
|
||||||
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
|
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
|
||||||
|
|||||||
+14
-8
@@ -1,10 +1,4 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: stable
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
language_version: python3.7
|
|
||||||
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.4.0
|
rev: v2.4.0
|
||||||
hooks:
|
hooks:
|
||||||
@@ -13,5 +7,17 @@ repos:
|
|||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
- id: name-tests-test
|
- repo: https://github.com/asottile/seed-isort-config
|
||||||
args: ["--django"]
|
rev: v1.9.3
|
||||||
|
hooks:
|
||||||
|
- id: seed-isort-config
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
|
rev: v4.3.21 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: stable
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
language_version: python3.7
|
||||||
|
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ and users of Fedora 29 or later may simply
|
|||||||
dnf install ocrmypdf
|
dnf install ocrmypdf
|
||||||
```
|
```
|
||||||
|
|
||||||
and macOS users with Homebrew may simply
|
and Homebrew users (macOS, Linux, Windows Subsystem for Linux) may simply
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install ocrmypdf
|
brew install ocrmypdf
|
||||||
@@ -113,18 +113,12 @@ ocrmypdf --help
|
|||||||
|
|
||||||
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
|
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
|
||||||
|
|
||||||
If you detect an issue, please:
|
Please report issues on our [GitHub issues](https://github.com/jbarlow83/OCRmyPDF/issues) page, and follow the issue template for quick response.
|
||||||
|
|
||||||
- Check whether your issue is already known
|
|
||||||
- If no problem report exists on github, please create one here: <https://github.com/jbarlow83/OCRmyPDF/issues>
|
|
||||||
- Describe your problem thoroughly
|
|
||||||
- Append the console output of the script when running the debug mode (`-v 1` option)
|
|
||||||
- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox)
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
|
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD.
|
||||||
|
|
||||||
Press & Media
|
Press & Media
|
||||||
-------------
|
-------------
|
||||||
|
|||||||
+4
-18
@@ -34,27 +34,20 @@ stages:
|
|||||||
displayName: "Install system packages"
|
displayName: "Install system packages"
|
||||||
- pwsh: |
|
- pwsh: |
|
||||||
refreshenv
|
refreshenv
|
||||||
$env:path = "C:\Program Files\Tesseract-OCR;C:\Program Files\gs\gs9.50\bin;" + $env:path
|
python -m pip install --upgrade pip wheel
|
||||||
pip install --upgrade pip wheel
|
python -m pip install -r requirements/main.txt -r requirements/test.txt .
|
||||||
pip install -r requirements/main.txt -r requirements/test.txt .
|
|
||||||
tesseract --version
|
|
||||||
displayName: "Install Python packages"
|
displayName: "Install Python packages"
|
||||||
- pwsh: |
|
- pwsh: |
|
||||||
refreshenv
|
refreshenv
|
||||||
$env:path = "C:\Program Files\Tesseract-OCR;C:\Program Files\gs\gs9.50\bin;" + $env:path
|
|
||||||
$env:pathext += ';.py'
|
$env:pathext += ';.py'
|
||||||
# -n auto helps Windows
|
# -n auto helps Windows
|
||||||
pytest -n auto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
|
python -m pytest -n auto --junitxml=test.xml --cov=ocrmypdf --cov-report=xml
|
||||||
displayName: "Test"
|
displayName: "Test"
|
||||||
- task: PublishTestResults@2
|
- task: PublishTestResults@2
|
||||||
inputs:
|
inputs:
|
||||||
testResultsFiles: "test.xml"
|
testResultsFiles: "test.xml"
|
||||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||||
condition: succeededOrFailed()
|
condition: succeededOrFailed()
|
||||||
- task: PublishCodeCoverageResults@1
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: Cobertura
|
|
||||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
|
||||||
- job: "Ubuntu_1804"
|
- job: "Ubuntu_1804"
|
||||||
pool:
|
pool:
|
||||||
vmImage: "ubuntu-18.04"
|
vmImage: "ubuntu-18.04"
|
||||||
@@ -105,10 +98,6 @@ stages:
|
|||||||
testResultsFiles: "test.xml"
|
testResultsFiles: "test.xml"
|
||||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||||
condition: succeededOrFailed()
|
condition: succeededOrFailed()
|
||||||
- task: PublishCodeCoverageResults@1
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: Cobertura
|
|
||||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
|
||||||
- job: "Ubuntu_1604"
|
- job: "Ubuntu_1604"
|
||||||
pool:
|
pool:
|
||||||
vmImage: "ubuntu-16.04"
|
vmImage: "ubuntu-16.04"
|
||||||
@@ -157,10 +146,6 @@ stages:
|
|||||||
testResultsFiles: "test.xml"
|
testResultsFiles: "test.xml"
|
||||||
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
testRunTitle: "$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)"
|
||||||
condition: succeededOrFailed()
|
condition: succeededOrFailed()
|
||||||
- task: PublishCodeCoverageResults@1
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: Cobertura
|
|
||||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
|
||||||
- job: "macOS_Mojave"
|
- job: "macOS_Mojave"
|
||||||
pool:
|
pool:
|
||||||
vmImage: "macos-10.14"
|
vmImage: "macos-10.14"
|
||||||
@@ -176,6 +161,7 @@ stages:
|
|||||||
versionSpec: "$(python.version)"
|
versionSpec: "$(python.version)"
|
||||||
- bash: |
|
- bash: |
|
||||||
brew update
|
brew update
|
||||||
|
brew unlink python@2
|
||||||
brew install \
|
brew install \
|
||||||
exempi \
|
exempi \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
|
|||||||
+52
-4
@@ -198,6 +198,54 @@ and all inquiries are appreciated.
|
|||||||
Hot (watched) folders
|
Hot (watched) folders
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
Watched folders with Docker
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
The OCRmyPDF Docker image includes a watcher service. This service can
|
||||||
|
be launched as follows:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
docker run \
|
||||||
|
-v <path to files to convert>:/input \
|
||||||
|
-v <path to store results>:/output \
|
||||||
|
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=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:
|
||||||
|
|
||||||
|
.. csv-table:: watcher.py parameters for Docker
|
||||||
|
:header: "Parameter", "Description"
|
||||||
|
:widths: 50, 50
|
||||||
|
|
||||||
|
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||||
|
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||||
|
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "This will place files in the output in {output}/{year}/{month}/{filename}"
|
||||||
|
|
||||||
|
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
|
||||||
|
slow network.
|
||||||
|
|
||||||
|
Watched folders with watcher.py
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
The watcher service may also be run natively.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip3 install -r reqs/watcher.txt
|
||||||
|
|
||||||
|
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
|
||||||
|
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
|
||||||
|
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||||
|
python3 watcher.py
|
||||||
|
|
||||||
|
Watched folders with CLI
|
||||||
|
------------------------
|
||||||
|
|
||||||
To set up a "hot folder" that will trigger OCR for every file inserted,
|
To set up a "hot folder" that will trigger OCR for every file inserted,
|
||||||
use a program like Python
|
use a program like Python
|
||||||
`watchdog <https://pypi.python.org/pypi/watchdog>`__ (supports all major
|
`watchdog <https://pypi.python.org/pypi/watchdog>`__ (supports all major
|
||||||
@@ -225,12 +273,12 @@ told to run ``ocrmypdf`` on any .pdf added to the current directory
|
|||||||
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
||||||
. # don't forget the final dot
|
. # don't forget the final dot
|
||||||
|
|
||||||
For more complex behavior you can write a Python script around to use
|
|
||||||
the watchdog API.
|
|
||||||
|
|
||||||
On file servers, you could configure watchmedo as a system service so it
|
On file servers, you could configure watchmedo as a system service so it
|
||||||
will run all the time.
|
will run all the time.
|
||||||
|
|
||||||
|
For more complex behavior you can write a Python script around to use
|
||||||
|
the watchdog API. You can refer to the watcher.py script as an example.
|
||||||
|
|
||||||
Caveats
|
Caveats
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -250,7 +298,7 @@ Caveats
|
|||||||
Alternatives
|
Alternatives
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- `systemd user services <https://wiki.archlinux.org/index.php/Systemd/User>`__
|
- On Linux, `systemd user services <https://wiki.archlinux.org/index.php/Systemd/User>`__
|
||||||
can be configured to automatically perform OCR on a collection of files.
|
can be configured to automatically perform OCR on a collection of files.
|
||||||
|
|
||||||
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
|
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
|
||||||
|
|||||||
+20
-1
@@ -15,7 +15,10 @@ Code style
|
|||||||
==========
|
==========
|
||||||
|
|
||||||
We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The
|
We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The
|
||||||
settings for programs are in ``pyproject.toml`` and ``setup.cfg``.
|
settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull
|
||||||
|
requests should follow the style guide. One difference we use from "black" style
|
||||||
|
is that strings shown to the user are always in double quotes (``"``) and strings
|
||||||
|
for internal uses are in single quotes (``'``).
|
||||||
|
|
||||||
Tests
|
Tests
|
||||||
=====
|
=====
|
||||||
@@ -36,3 +39,19 @@ New non-Python dependencies
|
|||||||
|
|
||||||
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
|
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
|
||||||
its functionality. In general we prefer to avoid adding new external programs.
|
its functionality. In general we prefer to avoid adding new external programs.
|
||||||
|
|
||||||
|
Style guide: Is it OCRmyPDF or ocrmypdf?
|
||||||
|
========================================
|
||||||
|
|
||||||
|
The program/project is OCRmyPDF and the name of the executable or library is ocrmypdf.
|
||||||
|
|
||||||
|
Known ports/packagers
|
||||||
|
=====================
|
||||||
|
|
||||||
|
OCRmyPDF has been ported to many platforms already. If you are interesting in
|
||||||
|
porting to a new platform, check with
|
||||||
|
`Repology <https://repology.org/projects/?search=ocrmypdf>`__ to see the status
|
||||||
|
of that platform.
|
||||||
|
|
||||||
|
Packager maintainers, please ensure that the command line completion scripts in
|
||||||
|
``misc/`` are installed.
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ This produces a file named "output.pdf" and a companion text file named
|
|||||||
|
|
||||||
ocrmypdf --sidecar output.txt input.pdf output.pdf
|
ocrmypdf --sidecar output.txt input.pdf output.pdf
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The sidecar file contains the **OCR text** found by OCRmyPDF. If the document
|
||||||
|
contains pages that already have text, that text will not appear in the
|
||||||
|
sidecar. If the option ``--pages`` is used, only those pages on which OCR
|
||||||
|
was performed will be included in the sidecar. If certain pages were skipped
|
||||||
|
because of options like ``--skip-big`` or ``--tesseract-timeout``, those pages
|
||||||
|
will not be in the sidecar.
|
||||||
|
|
||||||
|
To extract all text from a PDF, whether generated from OCR or otherwise,
|
||||||
|
use a program like Poppler's ``pdftotext`` or ``pdfgrep``.
|
||||||
|
|
||||||
OCR images, not PDFs
|
OCR images, not PDFs
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|||||||
+38
-14
@@ -8,11 +8,20 @@ Installing OCRmyPDF
|
|||||||
|latest|
|
|latest|
|
||||||
|
|
||||||
The easiest way to install OCRmyPDF is to follow the steps for your operating
|
The easiest way to install OCRmyPDF is to follow the steps for your operating
|
||||||
system/platform, although sometimes this version may be out of date.
|
system/platform, although sometimes this version may be out of date. This
|
||||||
|
installation guide provides information allowing you to compare the current
|
||||||
|
version to the one provided by your platform.
|
||||||
|
|
||||||
If you want to use the latest version of OCRmyPDF, your best bet is to install
|
If you want to use the latest version of OCRmyPDF and all of its optional
|
||||||
the most recent version your platform provides, and then upgrade that version by
|
dependencies, the easiest way to get that is install the Homebrew package. Homebrew
|
||||||
installing the Python binary wheels.
|
is best known as a macOS package manger, but also works for
|
||||||
|
`Linux and Windows Subsystem for Linux <https://docs.brew.sh/Homebrew-on-Linux>`__.
|
||||||
|
After Homebrew is installed, simply run ``brew install ocrmypdf``.
|
||||||
|
|
||||||
|
You can also use the more detailed procedures here to manually install OCRmyPDF
|
||||||
|
from source or with the ``pip`` package manager for binary wheels. The reason
|
||||||
|
for these varied steps is that OCRmyPDF requires third-party executables that are
|
||||||
|
not part of Python.
|
||||||
|
|
||||||
.. contents:: Platform-specific steps
|
.. contents:: Platform-specific steps
|
||||||
:depth: 2
|
:depth: 2
|
||||||
@@ -55,8 +64,8 @@ Debian and Ubuntu 18.04 or newer
|
|||||||
| |ubu-1804| |ubu-1810| |ubu-1904| |ubu-1910| |
|
| |ubu-1804| |ubu-1810| |ubu-1904| |ubu-1910| |
|
||||||
+-----------------------------------------------+
|
+-----------------------------------------------+
|
||||||
|
|
||||||
Users of Debian 9 ("stretch") or later or Ubuntu 18.04 or later may
|
Users of Debian 9 ("stretch") or later or Ubuntu 18.04 or later, including users
|
||||||
simply
|
of Windows Subsystem for Linux, may simply
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -303,6 +312,19 @@ the following command.
|
|||||||
If you have any difficulties with installation, check the repository
|
If you have any difficulties with installation, check the repository
|
||||||
package page.
|
package page.
|
||||||
|
|
||||||
|
Alpine Linux
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. image:: https://repology.org/badge/version-for-repo/alpine_edge/ocrmypdf.svg
|
||||||
|
:alt: Alpine Linux
|
||||||
|
:target: https://repology.org/metapackage/ocrmypdf
|
||||||
|
|
||||||
|
To install OCRmyPDF for Alpine Linux:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
apk add ocrmypdf
|
||||||
|
|
||||||
Other Linux packages
|
Other Linux packages
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -380,7 +402,7 @@ packs. If you need other languages you can optionally install them all:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
brew install tesseract --with-all-languages # Option 2: for all language packs
|
brew install tesseract-lang # Option 2: for all language packs
|
||||||
|
|
||||||
Update the homebrew pip:
|
Update the homebrew pip:
|
||||||
|
|
||||||
@@ -427,8 +449,7 @@ Installing the Docker image
|
|||||||
===========================
|
===========================
|
||||||
|
|
||||||
For some users, installing the Docker image will be easier than
|
For some users, installing the Docker image will be easier than
|
||||||
installing all of OCRmyPDF's dependencies. For Windows, it is the only
|
installing all of OCRmyPDF's dependencies.
|
||||||
option.
|
|
||||||
|
|
||||||
See `OCRmyPDF Docker Image <docker>`__ for more information.
|
See `OCRmyPDF Docker Image <docker>`__ for more information.
|
||||||
|
|
||||||
@@ -448,7 +469,7 @@ Installing on Windows
|
|||||||
|
|
||||||
You must install the following for Windows:
|
You must install the following for Windows:
|
||||||
|
|
||||||
* Python 3.7 (64-bit recommended)
|
* Python 3.7 (64-bit)
|
||||||
* Tesseract 4.0 or later
|
* Tesseract 4.0 or later
|
||||||
* Ghostscript 9.50 or later
|
* Ghostscript 9.50 or later
|
||||||
|
|
||||||
@@ -463,11 +484,14 @@ Also consider adding:
|
|||||||
* ``choco install pngquant``
|
* ``choco install pngquant``
|
||||||
|
|
||||||
Windows 10 64-bit and 64-bit versions of applications are recommended. Earlier
|
Windows 10 64-bit and 64-bit versions of applications are recommended. Earlier
|
||||||
versions of Windows and 32-bit versions of these programs are not tested.
|
versions of Windows and 32-bit versions of these programs are not tested, and not
|
||||||
|
supported at this time.
|
||||||
|
|
||||||
Modify your ``PATH`` environment variable so that Tesseract and Ghostscript, and
|
OCRmyPDF will check for Tesseract-OCR and Ghostscript in your Program Files folder.
|
||||||
any optional executables can be found. You can enter it in the command line
|
If they are in some other location, you may need to modify the ``PATH``
|
||||||
or `follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
|
environment variable so Tesseract, Ghostscript, and other any optional executables can
|
||||||
|
be found. You can enter it in the command line or
|
||||||
|
`follow these directions <https://www.computerhope.com/issues/ch000549.htm#dospath>`_
|
||||||
to make the change persistent and system-wide.
|
to make the change persistent and system-wide.
|
||||||
|
|
||||||
You may then use pip to install ocrmypdf:
|
You may then use pip to install ocrmypdf:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
|
OCRmyPDF is a Python 3 application and library that adds OCR layers to PDFs.
|
||||||
|
|
||||||
About OCR
|
About OCR
|
||||||
=========
|
=========
|
||||||
|
|||||||
@@ -13,6 +13,37 @@ Note that it is licensed under GPLv3, so scripts that
|
|||||||
``import ocrmypdf`` and are released publicly should probably also be
|
``import ocrmypdf`` and are released publicly should probably also be
|
||||||
licensed under GPLv3.
|
licensed under GPLv3.
|
||||||
|
|
||||||
|
v9.5.0
|
||||||
|
======
|
||||||
|
|
||||||
|
- Added API functions to measure OCR quality.
|
||||||
|
- Modest improvements to handling PDFs with difficult/non compliant metadata.
|
||||||
|
|
||||||
|
v9.4.0
|
||||||
|
======
|
||||||
|
|
||||||
|
- Updated recommended dependency versions.
|
||||||
|
- Improvements to test coverage and changes to facilitate better measurement of
|
||||||
|
test coverage, such as when tests run in subprocesses.
|
||||||
|
- Improvements to error messages when Leptonica is not installed correctly.
|
||||||
|
- Fixed use of pytest "session scope" that may have caused some intermittent
|
||||||
|
CI failures.
|
||||||
|
- When the argument ``--keep-temporary-files`` or verbosity is set to ``-v1``,
|
||||||
|
a debug log file is generated in the working temporary folder.
|
||||||
|
|
||||||
|
v9.3.0
|
||||||
|
======
|
||||||
|
|
||||||
|
- Improved native Windows support: we now check in the obvious places in
|
||||||
|
the "Program Files" folders installations of Tesseract and Ghostscript,
|
||||||
|
rather than relying on the user to edit ``PATH`` to specify their location.
|
||||||
|
The ``PATH`` environment variable can still be used to differentiate when
|
||||||
|
multiple installations are present or the programs are installed to non-
|
||||||
|
standard locations.
|
||||||
|
- Fixed an exception on parsing Ghostscript error messages.
|
||||||
|
- Added an improved example demonstrating how to set up a watched folder
|
||||||
|
for automated OCR processing (thanks to @ianalexander for the contribution).
|
||||||
|
|
||||||
v9.2.0
|
v9.2.0
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ _ocrmypdf()
|
|||||||
COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) )
|
COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) )
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages)
|
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages|--fast-web-view)
|
||||||
# argument required but no completions available
|
# argument required but no completions available
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
@@ -76,7 +76,8 @@ _ocrmypdf()
|
|||||||
--max-image-mpixels --tesseract-config --tesseract-pagesegmode
|
--max-image-mpixels --tesseract-config --tesseract-pagesegmode
|
||||||
--help --tesseract-oem --pdf-renderer --tesseract-timeout
|
--help --tesseract-oem --pdf-renderer --tesseract-timeout
|
||||||
--rotate-pages-threshold --pdfa-image-compression --user-words
|
--rotate-pages-threshold --pdfa-image-compression --user-words
|
||||||
--user-patterns --keep-temporary-files --output-type' \
|
--user-patterns --keep-temporary-files --output-type
|
||||||
|
--no-progress-bar --pages --fast-web-view' \
|
||||||
-- "$cur" ) )
|
-- "$cur" ) )
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ function __fish_ocrmypdf_verbose
|
|||||||
end
|
end
|
||||||
complete -c ocrmypdf -x -s v -l verbose -a '(__fish_ocrmypdf_verbose)' -d "set verbosity level"
|
complete -c ocrmypdf -x -s v -l verbose -a '(__fish_ocrmypdf_verbose)' -d "set verbosity level"
|
||||||
|
|
||||||
|
complete -c ocrmypdf -x -l no-progress-bar -d "disable the progress bar"
|
||||||
|
|
||||||
function __fish_ocrmypdf_pdfa_compression
|
function __fish_ocrmypdf_pdfa_compression
|
||||||
echo -e "auto\t"(_ "let Ghostscript decide how to compress images")
|
echo -e "auto\t"(_ "let Ghostscript decide how to compress images")
|
||||||
echo -e "jpeg\t"(_ "convert color and grayscale images to JPEG")
|
echo -e "jpeg\t"(_ "convert color and grayscale images to JPEG")
|
||||||
@@ -111,5 +113,6 @@ complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence"
|
|||||||
|
|
||||||
complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
|
complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
|
||||||
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
|
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
|
||||||
|
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
|
||||||
|
|
||||||
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
|
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf)"
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from watchdog.events import PatternMatchingEventHandler
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
import ocrmypdf
|
||||||
|
|
||||||
|
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||||
|
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||||
|
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', False))
|
||||||
|
PATTERNS = ['*.pdf']
|
||||||
|
|
||||||
|
|
||||||
|
def execute_ocrmypdf(file_path):
|
||||||
|
filename = Path(file_path).name
|
||||||
|
if OUTPUT_DIRECTORY_YEAR_MONTH:
|
||||||
|
today = datetime.today()
|
||||||
|
output_directory_year_month = Path(
|
||||||
|
f'{OUTPUT_DIRECTORY}/{today.year}/{today.month}'
|
||||||
|
)
|
||||||
|
if not output_directory_year_month.exists():
|
||||||
|
output_directory_year_month.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = Path(output_directory_year_month) / filename
|
||||||
|
else:
|
||||||
|
output_path = Path(OUTPUT_DIRECTORY) / filename
|
||||||
|
print(f'New file: {file_path}.\nAttempting to OCRmyPDF to: {output_path}')
|
||||||
|
ocrmypdf.ocr(file_path, output_path)
|
||||||
|
|
||||||
|
|
||||||
|
class HandleObserverEvent(PatternMatchingEventHandler):
|
||||||
|
def on_any_event(self, event):
|
||||||
|
if event.event_type in ['created', 'modified']:
|
||||||
|
execute_ocrmypdf(event.src_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
handler = HandleObserverEvent(patterns=PATTERNS)
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
||||||
|
observer.start()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
+2
-1
@@ -10,7 +10,8 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = ["py36", "py37", "py38"]
|
target-version = ["py36",
|
||||||
|
"py37", "py38"]
|
||||||
skip-string-normalization = true
|
skip-string-normalization = true
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
exclude = '''
|
exclude = '''
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# installation
|
# installation
|
||||||
cffi == 1.13.2
|
cffi == 1.13.2
|
||||||
img2pdf == 0.3.3
|
img2pdf == 0.3.3
|
||||||
pdfminer.six == 20191110
|
pdfminer.six == 20200104
|
||||||
pikepdf == 1.8.1
|
pikepdf == 1.8.2
|
||||||
Pillow >= 6.2.0
|
Pillow == 7.0.0
|
||||||
reportlab == 3.5.32
|
reportlab == 3.5.32
|
||||||
tqdm == 4.37.0
|
tqdm == 4.41.1
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
pytest >= 5.0.0
|
pytest >= 5.0.0
|
||||||
pytest-helpers-namespace >= 2019.1.8
|
pytest-helpers-namespace >= 2019.1.8
|
||||||
pytest-xdist >= 1.29.0 # For DumpError fix
|
pytest-xdist >= 1.31.0
|
||||||
pytest-cov >= 2.6.1
|
pytest-cov >= 2.8.0
|
||||||
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
|
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
|
||||||
# or brew install exempi
|
# or brew install exempi
|
||||||
PyPDF2 >= 1.26.0
|
|
||||||
#PyMuPDF == 1.13.4 # optional
|
#PyMuPDF == 1.13.4 # optional
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
watchdog >= 0.8.2, < 1.0
|
||||||
@@ -22,6 +22,8 @@ include_trailing_comma=True
|
|||||||
force_grid_wrap=0
|
force_grid_wrap=0
|
||||||
use_parentheses=True
|
use_parentheses=True
|
||||||
line_length=88
|
line_length=88
|
||||||
|
known_first_party = ocrmypdf
|
||||||
|
known_third_party = PIL,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
license_file = LICENSE
|
license_file = LICENSE
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ from __future__ import print_function, unicode_literals
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
if sys.version_info < (3, 6):
|
if sys.version_info < (3, 6):
|
||||||
print("Python 3.6 or newer is required", file=sys.stderr)
|
print("Python 3.6 or newer is required", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
# pylint: disable=w0613
|
# pylint: disable=w0613
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ setup(
|
|||||||
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
|
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
|
||||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||||
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
||||||
'pdfminer.six >= 20181108, <= 20191110',
|
'pdfminer.six >= 20181108, <= 20200104',
|
||||||
'pikepdf >= 1.8.1, < 2',
|
'pikepdf >= 1.8.1, < 2',
|
||||||
'Pillow >= 6.2.0',
|
'Pillow >= 6.2.0',
|
||||||
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from .exceptions import (
|
|||||||
DpiError,
|
DpiError,
|
||||||
EncryptedPdfError,
|
EncryptedPdfError,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
|
ExitCodeException,
|
||||||
InputFileError,
|
InputFileError,
|
||||||
MissingDependencyError,
|
MissingDependencyError,
|
||||||
OutputFileAccessError,
|
OutputFileAccessError,
|
||||||
|
|||||||
+53
-45
@@ -19,6 +19,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
@@ -41,7 +42,7 @@ from .helpers import safe_symlink
|
|||||||
from .hocrtransform import HocrTransform
|
from .hocrtransform import HocrTransform
|
||||||
from .optimize import optimize
|
from .optimize import optimize
|
||||||
from .pdfa import generate_pdfa_ps
|
from .pdfa import generate_pdfa_ps
|
||||||
from .pdfinfo import Colorspace, PdfInfo, Encoding
|
from .pdfinfo import Colorspace, Encoding, PdfInfo
|
||||||
|
|
||||||
VECTOR_PAGE_DPI = 400
|
VECTOR_PAGE_DPI = 400
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ def _pdf_guess_version(input_file, search_window=1024):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def triage(input_file, output_file, options, log):
|
def triage(original_filename, input_file, output_file, options, log):
|
||||||
try:
|
try:
|
||||||
if _pdf_guess_version(input_file):
|
if _pdf_guess_version(input_file):
|
||||||
if options.image_dpi:
|
if options.image_dpi:
|
||||||
@@ -135,8 +136,9 @@ def triage(input_file, output_file, options, log):
|
|||||||
safe_symlink(input_file, output_file)
|
safe_symlink(input_file, output_file)
|
||||||
return output_file
|
return output_file
|
||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
log.error(e)
|
log.debug(f"Temporary file was at: {input_file}")
|
||||||
raise InputFileError() from e
|
msg = str(e).replace(input_file, original_filename)
|
||||||
|
raise InputFileError(msg) from e
|
||||||
|
|
||||||
triage_image_file(input_file, output_file, options, log)
|
triage_image_file(input_file, output_file, options, log)
|
||||||
return output_file
|
return output_file
|
||||||
@@ -181,7 +183,7 @@ def validate_pdfinfo_options(context):
|
|||||||
)
|
)
|
||||||
raise InputFileError()
|
raise InputFileError()
|
||||||
else:
|
else:
|
||||||
log.warn(
|
log.warning(
|
||||||
"This PDF has a fillable form. "
|
"This PDF has a fillable form. "
|
||||||
"Chances are it is a pure digital "
|
"Chances are it is a pure digital "
|
||||||
"document that does not need OCR."
|
"document that does not need OCR."
|
||||||
@@ -725,47 +727,53 @@ def should_linearize(working_file, context):
|
|||||||
def metadata_fixup(working_file, context):
|
def metadata_fixup(working_file, context):
|
||||||
output_file = context.get_path('metafix.pdf')
|
output_file = context.get_path('metafix.pdf')
|
||||||
options = context.options
|
options = context.options
|
||||||
original = pikepdf.open(context.origin)
|
|
||||||
docinfo = get_docinfo(original, options)
|
|
||||||
pdf = pikepdf.open(working_file)
|
|
||||||
with pdf.open_metadata() as meta:
|
|
||||||
meta.load_from_docinfo(docinfo, delete_missing=False)
|
|
||||||
# If xmp:CreateDate is missing, set it to the modify date to
|
|
||||||
# match Ghostscript, for consistency
|
|
||||||
if 'xmp:CreateDate' not in meta:
|
|
||||||
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
|
|
||||||
|
|
||||||
meta_original = original.open_metadata()
|
def report_on_metadata(missing):
|
||||||
not_copied = set(meta_original.keys()) - set(meta.keys())
|
if not missing:
|
||||||
if not_copied:
|
return
|
||||||
if options.output_type.startswith('pdfa'):
|
if options.output_type.startswith('pdfa'):
|
||||||
context.log.warning(
|
context.log.warning(
|
||||||
"Some input metadata could not be copied because it is not "
|
"Some input metadata could not be copied because it is not "
|
||||||
"permitted in PDF/A. You may wish to examine the output "
|
"permitted in PDF/A. You may wish to examine the output "
|
||||||
"PDF's XMP metadata."
|
"PDF's XMP metadata."
|
||||||
)
|
)
|
||||||
context.log.debug(
|
context.log.debug(
|
||||||
"The following metadata fields were not copied: %r", not_copied
|
"The following metadata fields were not copied: %r", missing
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
context.log.error(
|
context.log.error(
|
||||||
"Some input metadata could not be copied."
|
"Some input metadata could not be copied."
|
||||||
"You may wish to examine the output PDF's XMP metadata."
|
"You may wish to examine the output PDF's XMP metadata."
|
||||||
)
|
)
|
||||||
context.log.info(
|
context.log.info(
|
||||||
"The following metadata fields were not copied: %r", not_copied
|
"The following metadata fields were not copied: %r", missing
|
||||||
)
|
)
|
||||||
pdf.save(
|
|
||||||
output_file,
|
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
||||||
compress_streams=True,
|
docinfo = get_docinfo(original, options)
|
||||||
preserve_pdfa=True,
|
with pdf.open_metadata() as meta:
|
||||||
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
|
||||||
linearize=( # Don't linearize if optimize() will be linearizing too
|
# If xmp:CreateDate is missing, set it to the modify date to
|
||||||
should_linearize(working_file, context) if options.optimize == 0 else False
|
# match Ghostscript, for consistency
|
||||||
),
|
if 'xmp:CreateDate' not in meta:
|
||||||
)
|
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
|
||||||
original.close()
|
|
||||||
pdf.close()
|
meta_original = original.open_metadata()
|
||||||
|
missing = set(meta_original.keys()) - set(meta.keys())
|
||||||
|
report_on_metadata(missing)
|
||||||
|
|
||||||
|
pdf.save(
|
||||||
|
output_file,
|
||||||
|
compress_streams=True,
|
||||||
|
preserve_pdfa=True,
|
||||||
|
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
||||||
|
linearize=( # Don't linearize if optimize() will be linearizing too
|
||||||
|
should_linearize(working_file, context)
|
||||||
|
if options.optimize == 0
|
||||||
|
else False
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return output_file
|
return output_file
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+60
-23
@@ -23,10 +23,11 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from pathlib import Path
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
import PIL
|
import PIL
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from ._graft import OcrGrafter
|
from ._graft import OcrGrafter
|
||||||
from ._jobcontext import PDFContext, cleanup_working_files, make_logger
|
from ._jobcontext import PDFContext, cleanup_working_files, make_logger
|
||||||
@@ -267,28 +268,43 @@ def exec_concurrent(context):
|
|||||||
unit='page',
|
unit='page',
|
||||||
unit_scale=0.5,
|
unit_scale=0.5,
|
||||||
disable=not context.options.progress_bar,
|
disable=not context.options.progress_bar,
|
||||||
) as pbar, Pool(
|
) as pbar:
|
||||||
processes=max_workers,
|
pool = Pool(
|
||||||
initializer=initializer,
|
processes=max_workers,
|
||||||
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
|
initializer=initializer,
|
||||||
) as pool:
|
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
|
||||||
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
)
|
||||||
while True:
|
try:
|
||||||
try:
|
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
||||||
page_result = results.next()
|
while True:
|
||||||
sidecars[page_result.pageno] = page_result.text
|
try:
|
||||||
pbar.update()
|
page_result = results.next()
|
||||||
ocrgraft.graft_page(page_result)
|
sidecars[page_result.pageno] = page_result.text
|
||||||
pbar.update()
|
pbar.update()
|
||||||
except StopIteration:
|
ocrgraft.graft_page(page_result)
|
||||||
break
|
pbar.update()
|
||||||
except (Exception, KeyboardInterrupt):
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Terminate pool so we exit instantly
|
||||||
|
pool.terminate()
|
||||||
|
# Don't try listener.join() here, will deadlock
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
||||||
|
# Unless inside pytest, exit immediately because no one wants
|
||||||
|
# to wait for child processes to finalize results that will be
|
||||||
|
# thrown away. Inside pytest, we want child processes to exit
|
||||||
|
# cleanly so that they output an error messages or coverage data
|
||||||
|
# we need from them.
|
||||||
pool.terminate()
|
pool.terminate()
|
||||||
log_queue.put_nowait(None) # Terminate log listener
|
raise
|
||||||
# Don't try listener.join() here, will deadlock
|
finally:
|
||||||
raise
|
# Terminate log listener
|
||||||
|
log_queue.put_nowait(None)
|
||||||
|
pool.close()
|
||||||
|
pool.join()
|
||||||
|
|
||||||
log_queue.put_nowait(None)
|
|
||||||
listener.join()
|
listener.join()
|
||||||
|
|
||||||
# Output sidecar text
|
# Output sidecar text
|
||||||
@@ -320,6 +336,17 @@ def samefile(f1, f2):
|
|||||||
return os.path.samefile(f1, f2)
|
return os.path.samefile(f1, f2)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_debug_logging(log_filename, prefix=''):
|
||||||
|
log_file_handler = logging.FileHandler(log_filename, delay=True)
|
||||||
|
log_file_handler.setLevel(logging.DEBUG)
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'[%(asctime)s] - %(name)s - %(levelname)7s - %(message)s'
|
||||||
|
)
|
||||||
|
log_file_handler.setFormatter(formatter)
|
||||||
|
logging.getLogger(prefix).addHandler(log_file_handler)
|
||||||
|
return log_file_handler
|
||||||
|
|
||||||
|
|
||||||
def run_pipeline(options, api=False):
|
def run_pipeline(options, api=False):
|
||||||
log = make_logger(options, __name__)
|
log = make_logger(options, __name__)
|
||||||
|
|
||||||
@@ -330,13 +357,22 @@ def run_pipeline(options, api=False):
|
|||||||
options.jobs = available_cpu_count()
|
options.jobs = available_cpu_count()
|
||||||
|
|
||||||
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
||||||
|
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
|
||||||
|
'PYTEST_CURRENT_TEST', ''
|
||||||
|
):
|
||||||
|
configure_debug_logging(Path(work_folder) / "debug.log")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_requested_output_file(options)
|
check_requested_output_file(options)
|
||||||
start_input_file = create_input_file(options, work_folder)
|
start_input_file, original_filename = create_input_file(options, work_folder)
|
||||||
|
|
||||||
# Triage image or pdf
|
# Triage image or pdf
|
||||||
origin_pdf = triage(
|
origin_pdf = triage(
|
||||||
start_input_file, os.path.join(work_folder, 'origin.pdf'), options, log
|
original_filename,
|
||||||
|
start_input_file,
|
||||||
|
os.path.join(work_folder, 'origin.pdf'),
|
||||||
|
options,
|
||||||
|
log,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gather pdfinfo and create context
|
# Gather pdfinfo and create context
|
||||||
@@ -345,6 +381,7 @@ def run_pipeline(options, api=False):
|
|||||||
detailed_page_analysis=options.redo_ocr,
|
detailed_page_analysis=options.redo_ocr,
|
||||||
progbar=options.progress_bar,
|
progbar=options.progress_bar,
|
||||||
)
|
)
|
||||||
|
|
||||||
context = PDFContext(options, work_folder, origin_pdf, pdfinfo)
|
context = PDFContext(options, work_folder, origin_pdf, pdfinfo)
|
||||||
|
|
||||||
# Validate options are okay for this pdf
|
# Validate options are okay for this pdf
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ log = logging.getLogger(__name__)
|
|||||||
verify_python3_env()
|
verify_python3_env()
|
||||||
|
|
||||||
|
|
||||||
|
def check_platform():
|
||||||
|
if os.name == 'nt' and sys.maxsize <= 2 ** 32: # pragma: no cover
|
||||||
|
# 32-bit interpreter on Windows
|
||||||
|
log.error(
|
||||||
|
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
|
||||||
|
"Please use a 64-bit (x86-64) version of Python."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_options_languages(options):
|
def check_options_languages(options):
|
||||||
if not options.language:
|
if not options.language:
|
||||||
options.language = [DEFAULT_LANGUAGE]
|
options.language = [DEFAULT_LANGUAGE]
|
||||||
@@ -292,6 +301,7 @@ def check_options_pillow(options):
|
|||||||
|
|
||||||
|
|
||||||
def check_options(options):
|
def check_options(options):
|
||||||
|
check_platform()
|
||||||
check_options_languages(options)
|
check_options_languages(options)
|
||||||
check_options_metadata(options)
|
check_options_metadata(options)
|
||||||
check_options_output(options)
|
check_options_output(options)
|
||||||
@@ -304,7 +314,7 @@ def check_options(options):
|
|||||||
check_dependency_versions(options)
|
check_dependency_versions(options)
|
||||||
|
|
||||||
|
|
||||||
def check_closed_streams(options):
|
def check_closed_streams(options): # pragma: no cover
|
||||||
"""Work around Python issue with multiprocessing forking on closed streams
|
"""Work around Python issue with multiprocessing forking on closed streams
|
||||||
|
|
||||||
https://bugs.python.org/issue28326
|
https://bugs.python.org/issue28326
|
||||||
@@ -370,12 +380,12 @@ def create_input_file(options, work_folder):
|
|||||||
target = os.path.join(work_folder, 'stdin')
|
target = os.path.join(work_folder, 'stdin')
|
||||||
with open(target, 'wb') as stream_buffer:
|
with open(target, 'wb') as stream_buffer:
|
||||||
copyfileobj(sys.stdin.buffer, stream_buffer)
|
copyfileobj(sys.stdin.buffer, stream_buffer)
|
||||||
return target
|
return target, "<stdin>"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
target = os.path.join(work_folder, 'origin')
|
target = os.path.join(work_folder, 'origin')
|
||||||
safe_symlink(options.input_file, target)
|
safe_symlink(options.input_file, target)
|
||||||
return target
|
return target, os.fspath(options.input_file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise InputFileError(f"File not found - {options.input_file}")
|
raise InputFileError(f"File not found - {options.input_file}")
|
||||||
|
|
||||||
|
|||||||
+18
-7
@@ -18,10 +18,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
from contextlib import suppress
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
@@ -31,7 +31,15 @@ from .cli import parser
|
|||||||
|
|
||||||
|
|
||||||
class TqdmConsole:
|
class TqdmConsole:
|
||||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar"""
|
"""Wrapper to log messages in a way that is compatible with tqdm progress bar
|
||||||
|
|
||||||
|
This routes log messages through tqdm so that it can print them above the
|
||||||
|
progress bar, and then refresh the progress bar, rather than overwriting
|
||||||
|
it which looks messy.
|
||||||
|
|
||||||
|
For some reason Python 3.6 prints extra empty messages from time to time,
|
||||||
|
so we suppress those.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
self.file = file
|
self.file = file
|
||||||
@@ -46,7 +54,7 @@ class TqdmConsole:
|
|||||||
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
if hasattr(self.file, "flush"):
|
with suppress(AttributeError):
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
|
|
||||||
@@ -80,11 +88,14 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
|||||||
overwrite the progress bar
|
overwrite the progress bar
|
||||||
manage_root_logger (bool): Configure the process's root logger, to ensure
|
manage_root_logger (bool): Configure the process's root logger, to ensure
|
||||||
all log output is sent through
|
all log output is sent through
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The toplevel logger for ocrmypdf (or the root logger, if we are managing it).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prefix = '' if manage_root_logger else 'ocrmypdf'
|
prefix = '' if manage_root_logger else 'ocrmypdf'
|
||||||
log = logging.getLogger(prefix)
|
log = logging.getLogger(prefix)
|
||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
if progress_bar_friendly:
|
if progress_bar_friendly:
|
||||||
console = logging.StreamHandler(stream=TqdmConsole(sys.stderr))
|
console = logging.StreamHandler(stream=TqdmConsole(sys.stderr))
|
||||||
@@ -99,8 +110,6 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
|||||||
console.setLevel(logging.INFO)
|
console.setLevel(logging.INFO)
|
||||||
|
|
||||||
formatter = logging.Formatter('%(levelname)7s - %(message)s')
|
formatter = logging.Formatter('%(levelname)7s - %(message)s')
|
||||||
if verbosity >= 1:
|
|
||||||
log.setLevel(logging.DEBUG)
|
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
formatter = logging.Formatter('%(name)s - %(levelname)7s - %(message)s')
|
formatter = logging.Formatter('%(name)s - %(levelname)7s - %(message)s')
|
||||||
|
|
||||||
@@ -116,6 +125,8 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
|||||||
if manage_root_logger:
|
if manage_root_logger:
|
||||||
logging.captureWarnings(True)
|
logging.captureWarnings(True)
|
||||||
|
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
def create_options(*, input_file, output_file, **kwargs):
|
def create_options(*, input_file, output_file, **kwargs):
|
||||||
cmdline = []
|
cmdline = []
|
||||||
|
|||||||
@@ -20,14 +20,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from subprocess import PIPE, STDOUT, CalledProcessError, run as subprocess_run
|
from functools import lru_cache
|
||||||
|
from subprocess import PIPE, STDOUT, CalledProcessError
|
||||||
|
from subprocess import run as subprocess_run
|
||||||
|
|
||||||
from ..exceptions import ExitCode, MissingDependencyError
|
from ..exceptions import ExitCode, MissingDependencyError
|
||||||
|
|
||||||
log = logging.Logger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_program(args, env=None):
|
def _get_program(args, env=None):
|
||||||
@@ -39,23 +41,61 @@ def _get_program(args, env=None):
|
|||||||
|
|
||||||
|
|
||||||
def run(args, *, env=None, **kwargs):
|
def run(args, *, env=None, **kwargs):
|
||||||
|
"""Wrapper around subprocess.run()
|
||||||
|
|
||||||
|
The main purpose of this wrapper is to allow us to substitute the main program
|
||||||
|
for a spoof in the test suite. The hidden variable _OCRMYPDF_TEST_PATH replaces
|
||||||
|
the main PATH as a location to check for programs to run.
|
||||||
|
|
||||||
|
Secondly we have to account for behavioral differences in Windows in particular.
|
||||||
|
Creating symbolic links in Windows requires administrator privileges and
|
||||||
|
may not work if for some reason we're using a FAT file system or the temporary
|
||||||
|
folder is on a different drive from the working folder. The test suite
|
||||||
|
works around this by creating shim Python scripts that perform the same function
|
||||||
|
as a symbolic link, but those shims require support on this side, to ensure
|
||||||
|
we call them with Python.
|
||||||
|
|
||||||
|
"""
|
||||||
if not env:
|
if not env:
|
||||||
env = os.environ
|
env = os.environ
|
||||||
|
|
||||||
|
# Search in spoof path if necessary
|
||||||
program = _get_program(args, env)
|
program = _get_program(args, env)
|
||||||
|
|
||||||
|
# If we are running a .py on Windows, ensure we call it with this Python
|
||||||
|
# (to support test suite shims)
|
||||||
if os.name == 'nt' and program.lower().endswith('.py'):
|
if os.name == 'nt' and program.lower().endswith('.py'):
|
||||||
args = [sys.executable, program] + args[1:]
|
args = [sys.executable, program] + args[1:]
|
||||||
else:
|
else:
|
||||||
args = [program] + args[1:]
|
args = [program] + args[1:]
|
||||||
log.debug(args)
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
paths = os.pathsep.join(os.get_exec_path(env))
|
||||||
|
if not shutil.which(args[0], path=paths):
|
||||||
|
shimmed_path = shim_paths_with_program_files(env)
|
||||||
|
new_args0 = shutil.which(args[0], path=shimmed_path)
|
||||||
|
if new_args0:
|
||||||
|
args[0] = new_args0
|
||||||
|
|
||||||
|
process_log = log.getChild(os.path.basename(program))
|
||||||
|
process_log.debug("Running: %s", args)
|
||||||
if sys.version_info < (3, 7) and os.name == 'nt':
|
if sys.version_info < (3, 7) and os.name == 'nt':
|
||||||
# Can't use close_fds=True on Windows with Python 3.6 or older
|
# Can't use close_fds=True on Windows with Python 3.6 or older
|
||||||
# https://bugs.python.org/issue19575, etc.
|
# https://bugs.python.org/issue19575, etc.
|
||||||
kwargs['close_fds'] = False
|
kwargs['close_fds'] = False
|
||||||
return subprocess_run(args, env=env, **kwargs)
|
proc = subprocess_run(args, env=env, **kwargs)
|
||||||
|
if process_log.isEnabledFor(logging.DEBUG):
|
||||||
|
try:
|
||||||
|
stderr = proc.stderr.decode('utf-8', 'replace')
|
||||||
|
except AttributeError:
|
||||||
|
stderr = proc.stderr
|
||||||
|
if stderr:
|
||||||
|
process_log.debug("stderr = %s", stderr)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
|
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
|
||||||
"Get the version of the specified program"
|
"""Get the version of the specified program"""
|
||||||
args_prog = [program, version_arg]
|
args_prog = [program, version_arg]
|
||||||
try:
|
try:
|
||||||
proc = run(
|
proc = run(
|
||||||
@@ -91,6 +131,32 @@ def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def shim_paths_with_program_files(env=None):
|
||||||
|
if not env:
|
||||||
|
env = os.environ
|
||||||
|
program_files = env.get('PROGRAMFILES', '')
|
||||||
|
if not program_files:
|
||||||
|
return env.get('PATH', '')
|
||||||
|
paths = []
|
||||||
|
try:
|
||||||
|
for dirname in os.listdir(program_files):
|
||||||
|
if dirname.lower() == 'tesseract-ocr':
|
||||||
|
paths.append(os.path.join(program_files, dirname))
|
||||||
|
elif dirname.lower() == 'gs':
|
||||||
|
try:
|
||||||
|
latest_gs = max(
|
||||||
|
os.listdir(os.path.join(program_files, dirname)),
|
||||||
|
key=lambda d: float(d[2:]),
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
|
continue
|
||||||
|
paths.append(os.path.join(program_files, dirname, latest_gs, 'bin'))
|
||||||
|
except EnvironmentError:
|
||||||
|
pass
|
||||||
|
paths.extend(path for path in os.get_exec_path(env) if path not in set(paths))
|
||||||
|
return os.pathsep.join(paths)
|
||||||
|
|
||||||
|
|
||||||
missing_program = '''
|
missing_program = '''
|
||||||
The program '{program}' could not be executed or was not found on your
|
The program '{program}' could not be executed or was not found on your
|
||||||
system PATH.
|
system PATH.
|
||||||
|
|||||||
@@ -18,20 +18,20 @@
|
|||||||
"""Interface to Ghostscript executable"""
|
"""Interface to Ghostscript executable"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, CalledProcessError
|
|
||||||
from shutil import which
|
from shutil import which
|
||||||
|
from subprocess import PIPE, CalledProcessError
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..exceptions import SubprocessOutputError, MissingDependencyError
|
from ..exceptions import MissingDependencyError, SubprocessOutputError
|
||||||
from . import get_version, run
|
from . import get_version, run
|
||||||
|
|
||||||
gslog = logging.getLogger()
|
gslog = logging.getLogger()
|
||||||
@@ -195,8 +195,6 @@ def rasterize_pdf(
|
|||||||
try:
|
try:
|
||||||
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
|
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
with suppress(OSError):
|
|
||||||
Path(output_file).unlink() # no unfinished files
|
|
||||||
log.error(e.stderr.decode(errors='replace'))
|
log.error(e.stderr.decode(errors='replace'))
|
||||||
raise SubprocessOutputError('Ghostscript rasterizing failed')
|
raise SubprocessOutputError('Ghostscript rasterizing failed')
|
||||||
else:
|
else:
|
||||||
@@ -313,15 +311,12 @@ def generate_pdfa(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||||
log.debug(args_gs)
|
|
||||||
try:
|
try:
|
||||||
with Path(output_file).open('wb') as output:
|
with Path(output_file).open('wb') as output:
|
||||||
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
|
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
# Ghostscript does not change return code when it fails to create
|
# Ghostscript does not change return code when it fails to create
|
||||||
# PDF/A - check PDF/A status elsewhere
|
# PDF/A - check PDF/A status elsewhere
|
||||||
with suppress(OSError):
|
|
||||||
Path(output_file).unlink()
|
|
||||||
log.error(e.stderr.decode(errors='replace'))
|
log.error(e.stderr.decode(errors='replace'))
|
||||||
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
|
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
|
||||||
else:
|
else:
|
||||||
@@ -329,7 +324,7 @@ def generate_pdfa(
|
|||||||
if _gs_error_reported(stderr):
|
if _gs_error_reported(stderr):
|
||||||
last_part = None
|
last_part = None
|
||||||
repcount = 0
|
repcount = 0
|
||||||
for part in p.stdout.split('****'):
|
for part in stderr.split('****'):
|
||||||
if part != last_part:
|
if part != last_part:
|
||||||
if repcount > 1:
|
if repcount > 1:
|
||||||
log.error(f"(previous error message repeated {repcount} times)")
|
log.error(f"(previous error message repeated {repcount} times)")
|
||||||
@@ -346,5 +341,3 @@ def generate_pdfa(
|
|||||||
"Ghostscript had to remove PDF 'overprinting' from the "
|
"Ghostscript had to remove PDF 'overprinting' from the "
|
||||||
"input file to complete PDF/A conversion. "
|
"input file to complete PDF/A conversion. "
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
log.debug(stderr)
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
"""Interface to Tesseract executable"""
|
"""Interface to Tesseract executable"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
|
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
|
||||||
|
|
||||||
@@ -260,8 +260,8 @@ def generate_hocr(
|
|||||||
log,
|
log,
|
||||||
):
|
):
|
||||||
|
|
||||||
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
|
output_hocr = next(o for o in output_files if fspath(o).endswith('.hocr'))
|
||||||
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
|
output_sidecar = next(o for o in output_files if fspath(o).endswith('.txt'))
|
||||||
prefix = os.path.splitext(output_hocr)[0]
|
prefix = os.path.splitext(output_hocr)[0]
|
||||||
|
|
||||||
args_tesseract = tess_base_args(language, engine_mode)
|
args_tesseract = tess_base_args(language, engine_mode)
|
||||||
@@ -275,14 +275,10 @@ def generate_hocr(
|
|||||||
if user_patterns:
|
if user_patterns:
|
||||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||||
|
|
||||||
if user_words or user_patterns:
|
|
||||||
args_tesseract.extend(['-c', 'lstm_use_matrix=1'])
|
|
||||||
|
|
||||||
# Reminder: test suite tesseract spoofers will break after any changes
|
# Reminder: test suite tesseract spoofers will break after any changes
|
||||||
# to the number of order parameters here
|
# to the number of order parameters here
|
||||||
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
|
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
|
||||||
try:
|
try:
|
||||||
log.debug(args_tesseract)
|
|
||||||
p = run(
|
p = run(
|
||||||
args_tesseract,
|
args_tesseract,
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
@@ -381,7 +377,6 @@ def generate_pdf(
|
|||||||
|
|
||||||
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
|
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
|
||||||
try:
|
try:
|
||||||
log.debug(args_tesseract)
|
|
||||||
p = run(
|
p = run(
|
||||||
args_tesseract,
|
args_tesseract,
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ from tempfile import TemporaryDirectory
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..exceptions import MissingDependencyError, SubprocessOutputError
|
from ..exceptions import MissingDependencyError, SubprocessOutputError
|
||||||
from . import get_version, run as external_run
|
from . import get_version
|
||||||
|
from . import run as external_run
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
|
|||||||
+29
-21
@@ -104,28 +104,36 @@ def is_file_writable(test_file):
|
|||||||
can replace it atomically. Before doing the OCR work, make sure
|
can replace it atomically. Before doing the OCR work, make sure
|
||||||
the location is writable.
|
the location is writable.
|
||||||
"""
|
"""
|
||||||
p = Path(test_file)
|
try:
|
||||||
|
if not isinstance(test_file, Path):
|
||||||
if p.is_symlink():
|
p = Path(test_file)
|
||||||
p = p.resolve(strict=False)
|
|
||||||
|
|
||||||
# p.is_file() throws an exception in some cases
|
|
||||||
if p.exists() and p.is_file():
|
|
||||||
return os.access(
|
|
||||||
os.fspath(p),
|
|
||||||
os.W_OK,
|
|
||||||
effective_ids=(os.access in os.supports_effective_ids),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
fp = p.open('wb')
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
fp.close()
|
p = test_file
|
||||||
with suppress(OSError):
|
|
||||||
p.unlink()
|
if p.is_symlink():
|
||||||
return True
|
p = p.resolve(strict=False)
|
||||||
|
|
||||||
|
# p.is_file() throws an exception in some cases
|
||||||
|
if p.exists() and p.is_file():
|
||||||
|
return os.access(
|
||||||
|
os.fspath(p),
|
||||||
|
os.W_OK,
|
||||||
|
effective_ids=(os.access in os.supports_effective_ids),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
fp = p.open('wb')
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
fp.close()
|
||||||
|
with suppress(OSError):
|
||||||
|
p.unlink()
|
||||||
|
return True
|
||||||
|
except (EnvironmentError, RuntimeError) as e:
|
||||||
|
log.debug(e)
|
||||||
|
log.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def deprecated(func):
|
def deprecated(func):
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class HocrTransform:
|
|||||||
if self.width is None or self.height is None:
|
if self.width is None or self.height is None:
|
||||||
raise HocrTransformError("hocr file is missing page dimensions")
|
raise HocrTransformError("hocr file is missing page dimensions")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self): # pragma: no cover
|
||||||
"""
|
"""
|
||||||
Return the textual content of the HTML body
|
Return the textual content of the HTML body
|
||||||
"""
|
"""
|
||||||
@@ -190,7 +190,7 @@ class HocrTransform:
|
|||||||
pt = self.pt_from_pixel(pxl_coords)
|
pt = self.pt_from_pixel(pxl_coords)
|
||||||
|
|
||||||
# draw the bbox border
|
# draw the bbox border
|
||||||
if showBoundingboxes:
|
if showBoundingboxes: # pragma: no cover
|
||||||
pdf.rect(
|
pdf.rect(
|
||||||
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
|
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
|
||||||
)
|
)
|
||||||
@@ -231,7 +231,7 @@ class HocrTransform:
|
|||||||
pdf.save()
|
pdf.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def polyval(cls, poly, x):
|
def polyval(cls, poly, x): # pragma: no cover
|
||||||
return x * poly[0] + poly[1]
|
return x * poly[0] + poly[1]
|
||||||
|
|
||||||
def _do_line(
|
def _do_line(
|
||||||
@@ -269,7 +269,7 @@ class HocrTransform:
|
|||||||
# of the line box
|
# of the line box
|
||||||
baseline_y2 = self.height - (line_box.y2 + intercept)
|
baseline_y2 = self.height - (line_box.y2 + intercept)
|
||||||
|
|
||||||
if showBoundingboxes:
|
if showBoundingboxes: # pragma: no cover
|
||||||
# draw the baseline in magenta, dashed
|
# draw the baseline in magenta, dashed
|
||||||
pdf.setDash()
|
pdf.setDash()
|
||||||
pdf.setStrokeColorRGB(0.95, 0.65, 0.95)
|
pdf.setStrokeColorRGB(0.95, 0.65, 0.95)
|
||||||
@@ -318,7 +318,7 @@ class HocrTransform:
|
|||||||
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
|
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
|
||||||
|
|
||||||
# draw the bbox border
|
# draw the bbox border
|
||||||
if showBoundingboxes:
|
if showBoundingboxes: # pragma: no cover
|
||||||
pdf.rect(
|
pdf.rect(
|
||||||
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
|
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ from io import BytesIO
|
|||||||
from os import fspath
|
from os import fspath
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
|
|
||||||
from .lib._leptonica import ffi
|
|
||||||
from .exceptions import MissingDependencyError
|
from .exceptions import MissingDependencyError
|
||||||
|
from .exec import shim_paths_with_program_files
|
||||||
|
from .lib._leptonica import ffi
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
@@ -42,24 +43,36 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
libname = 'liblept-5'
|
libname = 'liblept-5'
|
||||||
|
os.environ['PATH'] = shim_paths_with_program_files()
|
||||||
else:
|
else:
|
||||||
libname = 'lept'
|
libname = 'lept'
|
||||||
_libpath = find_library(libname)
|
_libpath = find_library(libname)
|
||||||
if not _libpath and os.name == 'nt':
|
if not _libpath:
|
||||||
raise MissingDependencyError(
|
raise MissingDependencyError(
|
||||||
"""
|
"""
|
||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
This error normally occurs when ocrmypdf can't find a file named
|
This error normally occurs when ocrmypdf can't find the Leptonica
|
||||||
liblept-5.dll (Leptonica). Please ensure Tesseract-OCR is installed
|
library, which is usually installed with Tesseract OCR. It could be that
|
||||||
and its location is added to the system PATH environment variable.
|
Tesseract is not installed properly, we can't find the installation
|
||||||
|
on your system PATH environment variable.
|
||||||
|
|
||||||
For details see:
|
The library we are looking for is usually called:
|
||||||
|
liblept-5.dll (Windows)
|
||||||
|
liblept*.dylib (macOS)
|
||||||
|
liblept*.so (Linux/BSD)
|
||||||
|
|
||||||
|
Please review our installation procedures to find a solution:
|
||||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
lept = ffi.dlopen(_libpath)
|
try:
|
||||||
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
lept = ffi.dlopen(_libpath)
|
||||||
|
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
||||||
|
except ffi.error as e:
|
||||||
|
raise MissingDependencyError(
|
||||||
|
f"Leptonica library found at {_libpath}, but we could not access it"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
class _LeptonicaErrorTrap:
|
class _LeptonicaErrorTrap:
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||||
|
#
|
||||||
|
# This file is part of OCRmyPDF.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
"""Utilities to measure OCR quality"""
|
||||||
|
|
||||||
|
|
||||||
|
class OcrQualityDictionary:
|
||||||
|
"""Manages a dictionary for simple OCR quality checks."""
|
||||||
|
|
||||||
|
def __init__(self, *, wordlist: Iterable[str] = []):
|
||||||
|
"""Construct a dictionary from a list of words.
|
||||||
|
|
||||||
|
Words for which capitalization is important should be capitalized in the
|
||||||
|
dictionary. Words that contain spaces or other punctuation will never match.
|
||||||
|
"""
|
||||||
|
self.dictionary = set()
|
||||||
|
self.dictionary.update(w for w in wordlist)
|
||||||
|
|
||||||
|
def measure_words_matched(self, ocr_text: str) -> float:
|
||||||
|
"""Check how many unique words in the OCR text match a dictionary.
|
||||||
|
|
||||||
|
Words with mixed capitalized are only considered a match if the test word
|
||||||
|
matches that capitalization.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
number of words that match / number
|
||||||
|
"""
|
||||||
|
text = re.sub(r"[0-9_]+", ' ', ocr_text)
|
||||||
|
text = re.sub(r'\W+', ' ', text)
|
||||||
|
text_words_list = re.split(r'\s+', text)
|
||||||
|
text_words = {w for w in text_words_list if len(w) >= 3}
|
||||||
|
|
||||||
|
matches = 0
|
||||||
|
for w in text_words:
|
||||||
|
if w in self.dictionary or (
|
||||||
|
w != w.lower() and w.lower() in self.dictionary
|
||||||
|
):
|
||||||
|
matches += 1
|
||||||
|
if matches > 0:
|
||||||
|
hit_ratio = matches / len(text_words)
|
||||||
|
else:
|
||||||
|
hit_ratio = 0.0
|
||||||
|
return hit_ratio
|
||||||
+21
-11
@@ -21,18 +21,13 @@ import platform
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, run
|
from subprocess import PIPE, run
|
||||||
from ocrmypdf import api, cli
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from ocrmypdf import api, cli
|
||||||
|
|
||||||
pytest_plugins = ['helpers_namespace']
|
pytest_plugins = ['helpers_namespace']
|
||||||
|
|
||||||
try:
|
|
||||||
from pytest_cov.embed import cleanup_on_sigterm
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
cleanup_on_sigterm()
|
|
||||||
|
|
||||||
# pylint: disable=E1101
|
# pylint: disable=E1101
|
||||||
# pytest.helpers is dynamic so it confuses pylint
|
# pytest.helpers is dynamic so it confuses pylint
|
||||||
@@ -136,12 +131,12 @@ def spoof(tmp_path_factory, **kwargs):
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_tesseract_noop(tmp_path_factory):
|
def spoof_tesseract_noop(tmp_path_factory):
|
||||||
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
|
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_tesseract_cache(tmp_path_factory):
|
def spoof_tesseract_cache(tmp_path_factory):
|
||||||
if running_in_docker():
|
if running_in_docker():
|
||||||
return os.environ.copy()
|
return os.environ.copy()
|
||||||
@@ -200,7 +195,10 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
|
|||||||
|
|
||||||
@pytest.helpers.register
|
@pytest.helpers.register
|
||||||
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
||||||
"Run ocrmypdf and let caller deal with results"
|
"""Run ocrmypdf via API and let caller deal with results
|
||||||
|
|
||||||
|
Does not currently have a way to manipulate the PATH except for Tesseract.
|
||||||
|
"""
|
||||||
|
|
||||||
options = cli.parser.parse_args(
|
options = cli.parser.parse_args(
|
||||||
[str(input_file), str(output_file)]
|
[str(input_file), str(output_file)]
|
||||||
@@ -210,6 +208,10 @@ def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
|||||||
if env:
|
if env:
|
||||||
options.tesseract_env = env.copy()
|
options.tesseract_env = env.copy()
|
||||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
||||||
|
first_path = env.get('_OCRMYPDF_TEST_PATH', '').split(os.pathsep)[0]
|
||||||
|
if 'spoof' in first_path:
|
||||||
|
assert 'gs' not in first_path, "use run_ocrmypdf() for gs"
|
||||||
|
assert 'tesseract' in first_path
|
||||||
if options.tesseract_env:
|
if options.tesseract_env:
|
||||||
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
|
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
|
||||||
|
|
||||||
@@ -221,13 +223,21 @@ def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=Tr
|
|||||||
"Run ocrmypdf and let caller deal with results"
|
"Run ocrmypdf and let caller deal with results"
|
||||||
|
|
||||||
if env is None:
|
if env is None:
|
||||||
env = os.environ
|
env = os.environ.copy()
|
||||||
|
|
||||||
p_args = (
|
p_args = (
|
||||||
OCRMYPDF
|
OCRMYPDF
|
||||||
+ [str(arg) for arg in args if arg is not None]
|
+ [str(arg) for arg in args if arg is not None]
|
||||||
+ [str(input_file), str(output_file)]
|
+ [str(input_file), str(output_file)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Tell subprocess where to find coverage.py configuration
|
||||||
|
# This has no effect except when coverage is running
|
||||||
|
# Details: https://coverage.readthedocs.io/en/coverage-5.0/subprocess.html
|
||||||
|
coverage_rc = Path(__file__).parent.parent / '.coveragerc'
|
||||||
|
assert coverage_rc.exists()
|
||||||
|
env['COVERAGE_PROCESS_START'] = os.fspath(coverage_rc)
|
||||||
|
|
||||||
p = run(
|
p = run(
|
||||||
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
|
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
@@ -25,14 +25,12 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
|
|
||||||
|
from gs import real_ghostscript
|
||||||
|
|
||||||
"""Replicate one type of Ghostscript feature elision warning during
|
"""Replicate one type of Ghostscript feature elision warning during
|
||||||
PDF/A creation."""
|
PDF/A creation."""
|
||||||
|
|
||||||
|
|
||||||
from gs import real_ghostscript
|
|
||||||
|
|
||||||
|
|
||||||
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
|
elision_warning = """GPL Ghostscript 9.20: Setting Overprint Mode to 1
|
||||||
not permitted in PDF/A-2, overprint mode not set"""
|
not permitted in PDF/A-2, overprint mode not set"""
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from gs import real_ghostscript
|
||||||
|
|
||||||
|
|
||||||
"""Replicate Ghostscript PDF/A conversion failure by suppressing some
|
"""Replicate Ghostscript PDF/A conversion failure by suppressing some
|
||||||
arguments"""
|
arguments"""
|
||||||
|
|
||||||
from gs import real_ghostscript
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if '--version' in sys.argv:
|
if '--version' in sys.argv:
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
"""Replicate Ghostscript raster failure while allowing rendering"""
|
|
||||||
|
|
||||||
|
|
||||||
from gs import real_ghostscript
|
from gs import real_ghostscript
|
||||||
|
|
||||||
|
"""Replicate Ghostscript raster failure while allowing rendering"""
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if '--version' in sys.argv:
|
if '--version' in sys.argv:
|
||||||
@@ -36,13 +35,13 @@ def main():
|
|||||||
print('SPOOFED: ' + os.path.basename(__file__))
|
print('SPOOFED: ' + os.path.basename(__file__))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# For any rendering calls (device == pdfwrite) call real ghostscript
|
# For non-image rastering calls, use real ghostscript
|
||||||
if '-sDEVICE=pdfwrite' in sys.argv:
|
if '-sDEVICE=pdfwrite' in sys.argv or '-sDEVICE=txtwrite' in sys.argv:
|
||||||
real_ghostscript(sys.argv)
|
real_ghostscript(sys.argv)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fail
|
# Fail
|
||||||
print("ERROR: Ghost story archive not found")
|
print("ERROR: Ghost story archive not found", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
from gs import real_ghostscript
|
from gs import real_ghostscript
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Fail
|
# Fail
|
||||||
print("ERROR: Casper is not a friendly ghost")
|
print("ERROR: Casper is not a friendly ghost", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
"""Tesseract bad utf8 spoof
|
"""Tesseract bad utf8 spoof
|
||||||
|
|
||||||
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
|
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
__version__ = subprocess.check_output(
|
__version__ = subprocess.check_output(
|
||||||
['tesseract', '--version'], stderr=subprocess.STDOUT
|
['tesseract', '--version'], stderr=subprocess.STDOUT
|
||||||
).decode()
|
).decode()
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ In orientation check mode, report the orientation is upright.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import PyPDF2 as pypdf
|
import pikepdf
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
VERSION_STRING = '''tesseract 4.0.0
|
VERSION_STRING = '''tesseract 4.0.0
|
||||||
@@ -99,12 +100,10 @@ def main():
|
|||||||
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
|
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
|
||||||
ptsize = pagesize[0] * 72, pagesize[1] * 72
|
ptsize = pagesize[0] * 72, pagesize[1] * 72
|
||||||
|
|
||||||
pdf_out = pypdf.PdfFileWriter()
|
pdf_out = pikepdf.new()
|
||||||
pdf_out.addBlankPage(ptsize[0], ptsize[1])
|
pdf_out.add_blank_page(page_size=ptsize)
|
||||||
with open(output + '.pdf', 'wb') as f:
|
pdf_out.save(Path(output).with_suffix('.pdf'), static_id=True)
|
||||||
pdf_out.write(f)
|
Path(output).with_suffix('.txt').write_text('')
|
||||||
with open(output + '.txt', 'w') as f:
|
|
||||||
f.write('')
|
|
||||||
else:
|
else:
|
||||||
inputf = sys.argv[-4]
|
inputf = sys.argv[-4]
|
||||||
output = sys.argv[-3]
|
output = sys.argv[-3]
|
||||||
|
|||||||
+10
-2
@@ -15,11 +15,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
|
|
||||||
|
|
||||||
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
check_ocrmypdf = pytest.helpers.check_ocrmypdf
|
||||||
|
|
||||||
|
|
||||||
@@ -31,4 +32,11 @@ def acroform(resources):
|
|||||||
def test_acroform_and_redo(acroform, caplog, no_outpdf):
|
def test_acroform_and_redo(acroform, caplog, no_outpdf):
|
||||||
with pytest.raises(ocrmypdf.exceptions.InputFileError):
|
with pytest.raises(ocrmypdf.exceptions.InputFileError):
|
||||||
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
|
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
|
||||||
assert '--redo-ocr is not currently possible' in caplog.text
|
assert '--redo-ocr is not currently possible' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_acroform_message(acroform, caplog, spoof_tesseract_noop, outpdf):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
check_ocrmypdf(acroform, outpdf, env=spoof_tesseract_noop)
|
||||||
|
assert 'fillable form' in caplog.text
|
||||||
|
assert '--force-ocr' in caplog.text
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||||
|
#
|
||||||
|
# This file is part of OCRmyPDF.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
import ocrmypdf
|
||||||
|
|
||||||
|
|
||||||
|
def test_raw_console():
|
||||||
|
bio = StringIO()
|
||||||
|
tqconsole = ocrmypdf.api.TqdmConsole(file=bio)
|
||||||
|
tqconsole.write("Test")
|
||||||
|
tqconsole.flush()
|
||||||
|
assert "Test" in bio.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def test_tqdm_console():
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
formatter = logging.Formatter('%(message)s')
|
||||||
|
|
||||||
|
bio = StringIO()
|
||||||
|
console = logging.StreamHandler(ocrmypdf.api.TqdmConsole(file=bio))
|
||||||
|
console.setFormatter(formatter)
|
||||||
|
|
||||||
|
log.addHandler(console)
|
||||||
|
|
||||||
|
def before_pbar(message):
|
||||||
|
# Ensure that log messages appear before the progress bar, even when
|
||||||
|
# printed after the progress bar updates.
|
||||||
|
v = bio.getvalue()
|
||||||
|
pbar_start_marker = '|#'
|
||||||
|
return v.index(message) < v.index(pbar_start_marker)
|
||||||
|
|
||||||
|
with tqdm(total=2, file=bio, disable=False) as pbar:
|
||||||
|
pbar.update()
|
||||||
|
msg = "1/2 above progress bar"
|
||||||
|
log.info(msg)
|
||||||
|
assert before_pbar(msg)
|
||||||
|
|
||||||
|
log.info("done")
|
||||||
|
assert not before_pbar("done")
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from subprocess import run, PIPE
|
from subprocess import PIPE, run
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|||||||
+23
-24
@@ -18,7 +18,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -32,28 +31,28 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
|||||||
spoof = pytest.helpers.spoof
|
spoof = pytest.helpers.spoof
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_no_tess_gs_render_fail(tmp_path_factory):
|
def spoof_no_tess_gs_render_fail(tmp_path_factory):
|
||||||
return spoof(
|
return spoof(
|
||||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
|
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
|
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
|
||||||
return spoof(
|
return spoof(
|
||||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
|
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_no_tess_no_pdfa(tmp_path_factory):
|
def spoof_no_tess_no_pdfa(tmp_path_factory):
|
||||||
return spoof(
|
return spoof(
|
||||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
|
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
||||||
return spoof(
|
return spoof(
|
||||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
|
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
|
||||||
@@ -61,18 +60,18 @@ def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def linn(resources):
|
def francais(resources):
|
||||||
path = resources / 'linn.pdf'
|
path = resources / 'francais.pdf'
|
||||||
return path, pikepdf.open(path)
|
return path, pikepdf.open(path)
|
||||||
|
|
||||||
|
|
||||||
def test_rasterize_size(linn, outdir, caplog):
|
def test_rasterize_size(francais, outdir, caplog):
|
||||||
path, pdf = linn
|
path, pdf = francais
|
||||||
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
|
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
|
||||||
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
|
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
|
||||||
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
|
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
|
||||||
target_size = Decimal('200.0'), Decimal('150.0')
|
target_size = Decimal('50.0'), Decimal('30.0')
|
||||||
target_dpi = 42.0, 4242.0
|
forced_dpi = 42.0, 4242.0
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
rasterize_pdf(
|
rasterize_pdf(
|
||||||
@@ -82,21 +81,21 @@ def test_rasterize_size(linn, outdir, caplog):
|
|||||||
target_size[1] / page_size[1],
|
target_size[1] / page_size[1],
|
||||||
raster_device='pngmono',
|
raster_device='pngmono',
|
||||||
log=log,
|
log=log,
|
||||||
page_dpi=target_dpi,
|
page_dpi=forced_dpi,
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(outdir / 'out.png') as im:
|
with Image.open(outdir / 'out.png') as im:
|
||||||
assert im.size == target_size
|
assert im.size == target_size
|
||||||
assert im.info['dpi'] == target_dpi
|
assert im.info['dpi'] == forced_dpi
|
||||||
|
|
||||||
|
|
||||||
def test_rasterize_rotated(linn, outdir, caplog):
|
def test_rasterize_rotated(francais, outdir, caplog):
|
||||||
path, pdf = linn
|
path, pdf = francais
|
||||||
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
|
page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3])
|
||||||
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
|
assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0
|
||||||
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
|
page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72))
|
||||||
target_size = Decimal('200.0'), Decimal('150.0')
|
target_size = Decimal('50.0'), Decimal('30.0')
|
||||||
target_dpi = 42.0, 4242.0
|
forced_dpi = 42.0, 4242.0
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
@@ -107,34 +106,34 @@ def test_rasterize_rotated(linn, outdir, caplog):
|
|||||||
target_size[1] / page_size[1],
|
target_size[1] / page_size[1],
|
||||||
raster_device='pngmono',
|
raster_device='pngmono',
|
||||||
log=log,
|
log=log,
|
||||||
page_dpi=target_dpi,
|
page_dpi=forced_dpi,
|
||||||
rotation=90,
|
rotation=90,
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(outdir / 'out.png') as im:
|
with Image.open(outdir / 'out.png') as im:
|
||||||
assert im.size == (target_size[1], target_size[0])
|
assert im.size == (target_size[1], target_size[0])
|
||||||
assert im.info['dpi'] == (target_dpi[1], target_dpi[0])
|
assert im.info['dpi'] == (forced_dpi[1], forced_dpi[0])
|
||||||
|
|
||||||
|
|
||||||
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
|
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
|
||||||
p, out, err = run_ocrmypdf(
|
p, out, err = run_ocrmypdf(
|
||||||
resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail
|
resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail
|
||||||
)
|
)
|
||||||
print(err)
|
assert 'Casper is not a friendly ghost' in err
|
||||||
assert p.returncode == ExitCode.child_process_error
|
assert p.returncode == ExitCode.child_process_error
|
||||||
|
|
||||||
|
|
||||||
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
|
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
|
||||||
p, out, err = run_ocrmypdf(
|
p, out, err = run_ocrmypdf(
|
||||||
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
|
resources / 'francais.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
|
||||||
)
|
)
|
||||||
print(err)
|
assert 'Ghost story archive not found' in err
|
||||||
assert p.returncode == ExitCode.child_process_error
|
assert p.returncode == ExitCode.child_process_error
|
||||||
|
|
||||||
|
|
||||||
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
||||||
p, out, err = run_ocrmypdf(
|
p, out, err = run_ocrmypdf(
|
||||||
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa
|
resources / 'francais.pdf', outpdf, env=spoof_no_tess_no_pdfa
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
p.returncode == ExitCode.pdfa_conversion_failed
|
p.returncode == ExitCode.pdfa_conversion_failed
|
||||||
@@ -142,4 +141,4 @@ def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
|||||||
|
|
||||||
|
|
||||||
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
|
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
|
||||||
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
|
check_ocrmypdf(resources / 'francais.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
|
||||||
|
|||||||
+1
-1
@@ -18,10 +18,10 @@
|
|||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
import pikepdf
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_glyphless_graft(resources, outdir):
|
def test_no_glyphless_graft(resources, outdir):
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# © 2019 James R. Barlow: github.com/jbarlow83
|
||||||
|
#
|
||||||
|
# This file is part of OCRmyPDF.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import ocrmypdf.helpers as helpers
|
||||||
|
|
||||||
|
|
||||||
|
class TestSafeSymlink:
|
||||||
|
def test_safe_symlink_link_self(self, tmp_path, caplog):
|
||||||
|
helpers.safe_symlink(tmp_path / 'self', tmp_path / 'self')
|
||||||
|
assert caplog.record_tuples[0][1] == logging.WARNING
|
||||||
|
|
||||||
|
def test_safe_symlink_overwrite(self, tmp_path):
|
||||||
|
(tmp_path / 'regular_file').touch()
|
||||||
|
with pytest.raises(FileExistsError):
|
||||||
|
helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file')
|
||||||
|
|
||||||
|
def test_safe_symlink_relink(self, tmp_path):
|
||||||
|
(tmp_path / 'regular_file_a').touch()
|
||||||
|
(tmp_path / 'regular_file_b').write_bytes(b'ABC')
|
||||||
|
(tmp_path / 'link').symlink_to(tmp_path / 'regular_file_a')
|
||||||
|
helpers.safe_symlink(tmp_path / 'regular_file_b', tmp_path / 'link')
|
||||||
|
assert (tmp_path / 'link').samefile(tmp_path / 'regular_file_b') or (
|
||||||
|
tmp_path / 'link'
|
||||||
|
).read_bytes() == b'ABC'
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_cpu_count(monkeypatch):
|
||||||
|
def cpu_count_raises():
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
monkeypatch.setattr(multiprocessing, 'cpu_count', cpu_count_raises)
|
||||||
|
with pytest.warns(expected_warning=UserWarning):
|
||||||
|
assert helpers.available_cpu_count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated():
|
||||||
|
@helpers.deprecated
|
||||||
|
def old_function():
|
||||||
|
return 42
|
||||||
|
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
assert old_function() == 42
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileIsWritable:
|
||||||
|
@pytest.fixture
|
||||||
|
def non_existent(self, tmp_path):
|
||||||
|
return tmp_path / 'nofile'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def basic_file(self, tmp_path):
|
||||||
|
basic = tmp_path / 'basic'
|
||||||
|
basic.touch()
|
||||||
|
return basic
|
||||||
|
|
||||||
|
def test_plain(self, non_existent):
|
||||||
|
assert helpers.is_file_writable(non_existent)
|
||||||
|
|
||||||
|
def test_symlink_loop(self, tmp_path):
|
||||||
|
loop = tmp_path / 'loop'
|
||||||
|
loop.symlink_to(loop)
|
||||||
|
assert not helpers.is_file_writable(loop)
|
||||||
|
|
||||||
|
def test_chmod(self, basic_file):
|
||||||
|
assert helpers.is_file_writable(basic_file)
|
||||||
|
basic_file.chmod(0o400)
|
||||||
|
assert not helpers.is_file_writable(basic_file)
|
||||||
|
basic_file.chmod(0o000)
|
||||||
|
assert not helpers.is_file_writable(basic_file)
|
||||||
|
|
||||||
|
def test_permission_error(self, basic_file):
|
||||||
|
pathmock = MagicMock(spec_set=basic_file)
|
||||||
|
pathmock.is_symlink.return_value = False
|
||||||
|
pathmock.exists.return_value = True
|
||||||
|
pathmock.is_file.side_effect = PermissionError
|
||||||
|
assert not helpers.is_file_writable(pathmock)
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PIL import Image
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
import pytest
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,8 +16,8 @@
|
|||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from os import fspath
|
|
||||||
import os
|
import os
|
||||||
|
from os import fspath
|
||||||
from pickle import dumps, loads
|
from pickle import dumps, loads
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -45,12 +45,12 @@ spoof = pytest.helpers.spoof
|
|||||||
RENDERERS = ['hocr', 'sandwich']
|
RENDERERS = ['hocr', 'sandwich']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_tesseract_crash(tmp_path_factory):
|
def spoof_tesseract_crash(tmp_path_factory):
|
||||||
return spoof(tmp_path_factory, tesseract='tesseract_crash.py')
|
return spoof(tmp_path_factory, tesseract='tesseract_crash.py')
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_tesseract_big_image_error(tmp_path_factory):
|
def spoof_tesseract_big_image_error(tmp_path_factory):
|
||||||
return spoof(tmp_path_factory, tesseract='tesseract_big_image_error.py')
|
return spoof(tmp_path_factory, tesseract='tesseract_big_image_error.py')
|
||||||
|
|
||||||
@@ -270,6 +270,16 @@ def test_input_file_not_found(caplog, no_outpdf):
|
|||||||
assert input_file in caplog.text
|
assert input_file in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(os.name == 'nt', reason="chmod")
|
||||||
|
def test_input_file_not_readable(caplog, resources, outdir, no_outpdf):
|
||||||
|
input_file = outdir / 'trivial.pdf'
|
||||||
|
shutil.copy(resources / 'trivial.pdf', input_file)
|
||||||
|
input_file.chmod(0o000)
|
||||||
|
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||||
|
assert result == ExitCode.input_file
|
||||||
|
assert str(input_file) in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_input_file_not_a_pdf(caplog, no_outpdf):
|
def test_input_file_not_a_pdf(caplog, no_outpdf):
|
||||||
input_file = __file__ # Try to OCR this file
|
input_file = __file__ # Try to OCR this file
|
||||||
result = run_ocrmypdf_api(input_file, no_outpdf)
|
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||||
|
|||||||
@@ -17,22 +17,22 @@
|
|||||||
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from datetime import timezone
|
|
||||||
import logging
|
import logging
|
||||||
import mmap
|
import mmap
|
||||||
from os import fspath
|
|
||||||
import os
|
import os
|
||||||
|
from datetime import timezone
|
||||||
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfile, move
|
from shutil import copyfile, move
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
import pytest
|
||||||
|
from pikepdf.models.metadata import decode_pdf_date
|
||||||
|
|
||||||
from ocrmypdf._jobcontext import PDFContext
|
from ocrmypdf._jobcontext import PDFContext
|
||||||
from ocrmypdf.exceptions import ExitCode
|
from ocrmypdf.exceptions import ExitCode
|
||||||
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
|
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
|
||||||
from pikepdf.models.metadata import decode_pdf_date
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import fitz
|
import fitz
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import logging
|
|||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
import pikepdf
|
|
||||||
from ocrmypdf import optimize as opt
|
from ocrmypdf import optimize as opt
|
||||||
from ocrmypdf.exec import jbig2enc, pngquant
|
from ocrmypdf.exec import jbig2enc, pngquant
|
||||||
from ocrmypdf.exec.ghostscript import rasterize_pdf
|
from ocrmypdf.exec.ghostscript import rasterize_pdf
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import pytest
|
|||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
from ocrmypdf._validation import _pages_from_ranges
|
from ocrmypdf._validation import _pages_from_ranges
|
||||||
from ocrmypdf.pdfinfo import PdfInfo
|
|
||||||
from ocrmypdf.exceptions import BadArgsError
|
from ocrmypdf.exceptions import BadArgsError
|
||||||
|
from ocrmypdf.pdfinfo import PdfInfo
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ from math import isclose
|
|||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from reportlab.pdfgen.canvas import Canvas
|
from reportlab.pdfgen.canvas import Canvas
|
||||||
|
|
||||||
import pikepdf
|
|
||||||
from ocrmypdf import pdfinfo
|
from ocrmypdf import pdfinfo
|
||||||
from ocrmypdf.pdfinfo import Colorspace, Encoding
|
from ocrmypdf.pdfinfo import Colorspace, Encoding
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||||
|
#
|
||||||
|
# This file is part of OCRmyPDF.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# OCRmyPDF is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import ocrmypdf.quality as qual
|
||||||
|
|
||||||
|
|
||||||
|
def test_quality_measurement():
|
||||||
|
oqd = qual.OcrQualityDictionary(
|
||||||
|
wordlist=["words", "words", "quick", "brown", "fox", "dog", "lazy"]
|
||||||
|
)
|
||||||
|
assert len(oqd.dictionary) == 6 # 6 unique
|
||||||
|
|
||||||
|
assert (
|
||||||
|
oqd.measure_words_matched("The quick brown fox jumps quickly over the lazy dog")
|
||||||
|
== 0.5
|
||||||
|
)
|
||||||
|
assert oqd.measure_words_matched("12345 10% _f 7fox -brown | words") == 1.0
|
||||||
|
|
||||||
|
assert oqd.measure_words_matched("quick quick quick") == 1.0
|
||||||
@@ -21,10 +21,10 @@ from os import fspath
|
|||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
import pikepdf
|
|
||||||
from ocrmypdf import leptonica
|
from ocrmypdf import leptonica
|
||||||
from ocrmypdf.exec import ghostscript, tesseract
|
from ocrmypdf.exec import ghostscript, tesseract
|
||||||
from ocrmypdf.pdfinfo import PdfInfo
|
from ocrmypdf.pdfinfo import PdfInfo
|
||||||
|
|||||||
+8
-2
@@ -18,7 +18,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import DEVNULL, PIPE, run, Popen, CalledProcessError
|
from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, run
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf
|
|||||||
spoof = pytest.helpers.spoof
|
spoof = pytest.helpers.spoof
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture
|
||||||
def spoof_tess_bad_utf8(tmp_path_factory):
|
def spoof_tess_bad_utf8(tmp_path_factory):
|
||||||
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
|
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
|
||||||
|
|
||||||
@@ -56,6 +56,9 @@ def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
|||||||
|
|
||||||
|
|
||||||
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||||
|
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
|
||||||
|
pytest.skip(msg="Coverage uses stdout")
|
||||||
|
|
||||||
input_file = str(resources / 'francais.pdf')
|
input_file = str(resources / 'francais.pdf')
|
||||||
output_file = str(outpdf)
|
output_file = str(outpdf)
|
||||||
|
|
||||||
@@ -121,6 +124,9 @@ def test_bad_locale():
|
|||||||
reason="Windows does not like this; not sure how to fix",
|
reason="Windows does not like this; not sure how to fix",
|
||||||
)
|
)
|
||||||
def test_dev_null(spoof_tesseract_noop, resources):
|
def test_dev_null(spoof_tesseract_noop, resources):
|
||||||
|
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
|
||||||
|
pytest.skip(msg="Coverage uses stdout")
|
||||||
|
|
||||||
p, out, err = run_ocrmypdf(
|
p, out, err = run_ocrmypdf(
|
||||||
resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop
|
resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -81,3 +83,95 @@ def test_no_languages(tmp_path):
|
|||||||
|
|
||||||
with pytest.raises(MissingDependencyError):
|
with pytest.raises(MissingDependencyError):
|
||||||
tesseract.languages(tesseract_env=env)
|
tesseract.languages(tesseract_env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_too_large_hocr(monkeypatch, resources, outdir):
|
||||||
|
log = logging.getLogger('test_image_too_large_hocr')
|
||||||
|
|
||||||
|
def dummy_run(args, *, env=None, **kwargs):
|
||||||
|
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
|
||||||
|
|
||||||
|
monkeypatch.setattr(tesseract, 'run', dummy_run)
|
||||||
|
tesseract.generate_hocr(
|
||||||
|
input_file=resources / 'crom.png',
|
||||||
|
output_files=[outdir / 'out.hocr', outdir / 'out.txt'],
|
||||||
|
language=['eng'],
|
||||||
|
engine_mode=None,
|
||||||
|
tessconfig=[],
|
||||||
|
timeout=180.0,
|
||||||
|
pagesegmode=None,
|
||||||
|
log=log,
|
||||||
|
user_words=None,
|
||||||
|
user_patterns=None,
|
||||||
|
tesseract_env=None,
|
||||||
|
)
|
||||||
|
assert "name='ocr-capabilities'" in Path(outdir / 'out.hocr').read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_too_large_pdf(monkeypatch, resources, outdir):
|
||||||
|
log = logging.getLogger('test_image_too_large_pdf')
|
||||||
|
|
||||||
|
def dummy_run(args, *, env=None, **kwargs):
|
||||||
|
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
|
||||||
|
|
||||||
|
monkeypatch.setattr(tesseract, 'run', dummy_run)
|
||||||
|
tesseract.generate_pdf(
|
||||||
|
input_image=resources / 'crom.png',
|
||||||
|
skip_pdf=resources / 'blank.pdf',
|
||||||
|
output_pdf=outdir / 'pdf.pdf',
|
||||||
|
output_text=outdir / 'txt.txt',
|
||||||
|
language=['eng'],
|
||||||
|
engine_mode=None,
|
||||||
|
text_only=False,
|
||||||
|
tessconfig=[],
|
||||||
|
timeout=180.0,
|
||||||
|
pagesegmode=None,
|
||||||
|
log=log,
|
||||||
|
user_words=None,
|
||||||
|
user_patterns=None,
|
||||||
|
tesseract_env=None,
|
||||||
|
)
|
||||||
|
assert Path(outdir / 'txt.txt').read_text() == '[skipped page]'
|
||||||
|
if os.name != 'nt': # different semantics
|
||||||
|
assert Path(outdir / 'pdf.pdf').samefile(resources / 'blank.pdf')
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeout(caplog):
|
||||||
|
log = logging.getLogger('test_timeout')
|
||||||
|
tesseract.page_timedout(log, '123456.png', 5)
|
||||||
|
assert "123456" in caplog.text
|
||||||
|
assert "took too long" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'in_, logged',
|
||||||
|
[
|
||||||
|
(b'Tesseract Open Source', ''),
|
||||||
|
(b'lots of diacritics blah blah', 'diacritics'),
|
||||||
|
(b'Warning in pixReadMem', ''),
|
||||||
|
(b'OSD: Weak margin', 'unsure about page orientation'),
|
||||||
|
(b'Error in pixScanForForeground', ''),
|
||||||
|
(b'Error in boxClipToRectangle', ''),
|
||||||
|
(b'an unexpected error', 'an unexpected error'),
|
||||||
|
(b'a dire warning', 'a dire warning'),
|
||||||
|
(b'read_params_file something', 'read_params_file'),
|
||||||
|
(b'an innocent message', 'innocent'),
|
||||||
|
(b'\x7f\x7f\x80innocent unicode failure', 'innocent'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_tesseract_log_output(caplog, in_, logged):
|
||||||
|
log = logging.getLogger('tesseract_log_output')
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
tesseract.tesseract_log_output(log, in_, 'dummy')
|
||||||
|
if logged == '':
|
||||||
|
assert caplog.text == ''
|
||||||
|
else:
|
||||||
|
assert logged in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_tesseract_log_output_raises(caplog):
|
||||||
|
log = logging.getLogger('tesseract_log_output')
|
||||||
|
with pytest.raises(tesseract.TesseractConfigError):
|
||||||
|
tesseract.tesseract_log_output(log, b'parameter not found: moo', 'dummy')
|
||||||
|
assert 'not found' in caplog.text
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ocrmypdf.cli import parser
|
|
||||||
from ocrmypdf._validation import check_options
|
from ocrmypdf._validation import check_options
|
||||||
|
from ocrmypdf.cli import parser
|
||||||
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
|
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
|
||||||
from ocrmypdf.exec import unpaper
|
from ocrmypdf.exec import unpaper
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ def have_unpaper():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture
|
||||||
def spoof_unpaper_oldversion(tmp_path_factory):
|
def spoof_unpaper_oldversion(tmp_path_factory):
|
||||||
return spoof(tmp_path_factory, unpaper="unpaper_oldversion.py")
|
return spoof(tmp_path_factory, unpaper="unpaper_oldversion.py")
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import pytest
|
|||||||
|
|
||||||
import ocrmypdf._validation as vd
|
import ocrmypdf._validation as vd
|
||||||
from ocrmypdf.api import create_options
|
from ocrmypdf.api import create_options
|
||||||
from ocrmypdf.exceptions import MissingDependencyError, BadArgsError
|
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
|
||||||
from ocrmypdf.pdfinfo import PdfInfo
|
from ocrmypdf.pdfinfo import PdfInfo
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user