Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21fb6c82ca | ||
|
|
27f7b9f255 | ||
|
|
6f31a92ffb | ||
|
|
da2276788c | ||
|
|
dc6f1a266a | ||
|
|
9c8ddd853d | ||
|
|
014d0302f2 | ||
|
|
65568b3dbc | ||
|
|
05e2b6698d | ||
|
|
2f8e0f7d95 | ||
|
|
7e7553fc6b | ||
|
|
6b425aaebe | ||
|
|
725af43bc3 | ||
|
|
5c60309609 | ||
|
|
7d5cd55909 | ||
|
|
ec4a06fad2 | ||
|
|
48c6e2318e | ||
|
|
2eafa5e070 | ||
|
|
6aa04d7569 | ||
|
|
b9bffa97ba | ||
|
|
777ba99ccc | ||
|
|
001b3324f1 | ||
|
|
b1f2d257e2 | ||
|
|
24a08e5170 | ||
|
|
adf97fd82c | ||
|
|
a60ea72517 | ||
|
|
59f967cdcd | ||
|
|
6e439ee89e | ||
|
|
da38e1b035 | ||
|
|
28c60c4f82 | ||
|
|
a5efc4af9b | ||
|
|
1141235c42 | ||
|
|
2b6b7a4975 | ||
|
|
b062c9e8c0 | ||
|
|
ed632ae366 | ||
|
|
af742229e7 | ||
|
|
e2d998245d | ||
|
|
8c58e95c3a | ||
|
|
61600111d3 | ||
|
|
e4c45e3d3b | ||
|
|
7cabbb125f | ||
|
|
d8753dc790 | ||
|
|
ef43d7e016 | ||
|
|
17a5b8b43c | ||
|
|
13d11e76e5 | ||
|
|
61069660a2 | ||
|
|
685a06c93d | ||
|
|
6cdf68363a | ||
|
|
522ff3c21a | ||
|
|
10245dc954 | ||
|
|
3d4f80639d | ||
|
|
db9a22c9dd | ||
|
|
31683530f8 | ||
|
|
0e550a1c6d | ||
|
|
b17fb61389 | ||
|
|
d640c2ded3 | ||
|
|
a0ac448d52 | ||
|
|
e3ba13e365 | ||
|
|
0cd04abc4e | ||
|
|
ee81f3968f |
+4
-1
@@ -1 +1,4 @@
|
||||
ref-names: $Format:%D$
|
||||
node: $Format:%H$
|
||||
node-date: $Format:%cI$
|
||||
describe-name: $Format:%(describe:tags=true)$
|
||||
ref-names: $Format:%D$
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
+17
-17
@@ -41,11 +41,11 @@ jobs:
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -129,11 +129,11 @@ jobs:
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -184,11 +184,11 @@ jobs:
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -217,11 +217,11 @@ jobs:
|
||||
name: Build sdist and wheels
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: "3.7"
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
python setup.py sdist
|
||||
python setup.py bdist_wheel
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
./dist/*.whl
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: dist
|
||||
@@ -274,22 +274,22 @@ jobs:
|
||||
- name: Set image name
|
||||
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: jbarlow83
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Print image tag
|
||||
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
@@ -11,23 +11,23 @@ repos:
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
args: ["--profile", "black", "-a", "from __future__ import annotations"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.1
|
||||
rev: v1.20.2
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.32.1
|
||||
rev: v2.37.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py37-plus"]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.950
|
||||
rev: v0.971
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
|
||||
@@ -62,7 +62,8 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
|
||||
| Debian, Ubuntu | ``apt install ocrmypdf`` |
|
||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||
| Fedora | ``dnf install ocrmypdf`` |
|
||||
| macOS | ``brew install ocrmypdf`` |
|
||||
| macOS (Homebrew) | ``brew install ocrmypdf`` |
|
||||
| macOS (nix) | ``nix-env -i ocrmypdf`` |
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
| FreeBSD | ``pkg install py37-ocrmypdf`` |
|
||||
| Conda | ``conda install ocrmypdf`` |
|
||||
|
||||
+3
-1
@@ -223,7 +223,9 @@ The ``hocr`` renderer
|
||||
The ``hocr`` renderer works with older versions of Tesseract. The image
|
||||
layer is copied from the original PDF page if possible, avoiding
|
||||
potentially lossy transcoding or loss of other PDF information. If
|
||||
preprocessing is specified, then the image layer is a new PDF.
|
||||
preprocessing is specified, then the image layer is a new PDF. (You may
|
||||
need to disable PDF/A conversion nad optimization to eliminate all
|
||||
lossy transformations.)
|
||||
|
||||
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
|
||||
looking to customize how OCR is presented should look here. A major
|
||||
|
||||
+20
-12
@@ -36,18 +36,21 @@ Directory trees
|
||||
===============
|
||||
|
||||
This will walk through a directory tree and run OCR on all files in
|
||||
place, printing the output in a way that makes
|
||||
place, and printing each filename in between runs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||
find . -printf '%p\n' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||
|
||||
Alternatively, with a docker container (mounts a volume to the container
|
||||
where the PDFs are stored):
|
||||
Alternatively, with a Docker container and streaming the file through
|
||||
standard input and output:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf '<container dir>/{}' '<container dir>/{}' \;
|
||||
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
|
||||
pdfout=$(mktemp)
|
||||
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
|
||||
done
|
||||
|
||||
This only runs one ``ocrmypdf`` process at a time. This variation uses
|
||||
``find`` to create a directory list and ``parallel`` to parallelize runs
|
||||
@@ -124,7 +127,9 @@ Users may need to customize the script to meet their requirements.
|
||||
|
||||
"OCR_INPUT_DIRECTORY", "Set input directory to monitor (recursive)"
|
||||
"OCR_OUTPUT_DIRECTORY", "Set output directory (should not be under input)"
|
||||
"OCR_ARCHIVE_DIRECTORY", "Set archive directory for processed originals (should not be under input, requires ``OCR_ON_SUCCESS_ARCHIVE`` to be set)"
|
||||
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
|
||||
"OCR_ON_SUCCESS_ARCHIVE", "This will move the processed orignal file to ``OCR_ARCHIVE_DIRECTORY`` if the exit code is 0 (OK). Note that ``OCR_ON_SUCCESS_DELETE`` takes precedence over this option, i.e. if both options are set, the input file will be deleted."
|
||||
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||
"OCR_DESKEW", "Apply deskew to crooked input PDFs"
|
||||
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true}'``."
|
||||
@@ -144,16 +149,18 @@ The watcher service is included in the OCRmyPDF Docker image. To run it:
|
||||
docker run \
|
||||
-v <path to files to convert>:/input \
|
||||
-v <path to store results>:/output \
|
||||
-v <path to store processed originals>:/archive \
|
||||
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
-e OCR_ON_SUCCESS_DELETE=1 \
|
||||
-e OCR_ON_SUCCESS_ARCHIVE=1 \
|
||||
-e OCR_DESKEW=1 \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-it --entrypoint python3 \
|
||||
jbarlow83/ocrmypdf \
|
||||
watcher.py
|
||||
|
||||
This service will watch for a file that matches ``/input/\*.pdf`` and will
|
||||
convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
This service will watch for a file that matches ``/input/\*.pdf``,
|
||||
convert it to a OCRed PDF in ``/output/``, and move the processed
|
||||
original to ``/archive``. The parameters to this image are:
|
||||
|
||||
.. csv-table:: watcher.py parameters for Docker
|
||||
:header: "Parameter", "Description"
|
||||
@@ -161,10 +168,11 @@ convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
|
||||
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1"
|
||||
"``-e OCR_ON_SUCCESS_DELETE=1``", "Define environment variable"
|
||||
"``-e OCR_DESKEW=1``", "Define environment variable"
|
||||
"``-e PYTHONBUFFERED=1``", "This will force STDOUT to be unbuffered and allow you to see messages in docker logs"
|
||||
"``-v <path to store processed originals>:/archive``", "Archive processed originals here"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable ``OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1`` to place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||
"``-e OCR_ON_SUCCESS_ARCHIVE=1``", "Define environment variable ``OCR_ON_SUCCESS_ARCHIVE`` to move processed originals"
|
||||
"``-e OCR_DESKEW=1``", "Define environment variable ``OCR_DESKEW`` to apply deskew to crooked input PDFs"
|
||||
"``-e PYTHONBUFFERED=1``", "This will force ``STDOUT`` to be unbuffered and allow you to see messages in docker logs"
|
||||
|
||||
This service relies on polling to check for changes to the filesystem. It
|
||||
may not be suitable for some environments, such as filesystems shared on a
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ master_doc = 'index'
|
||||
# General information about the project.
|
||||
project = 'ocrmypdf'
|
||||
copyright = (
|
||||
'2021, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
||||
'2022, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
||||
)
|
||||
author = 'James R. Barlow'
|
||||
|
||||
|
||||
+9
-1
@@ -200,7 +200,7 @@ might remove desirable content, especially from poor quality scans.
|
||||
|
||||
.. warning::
|
||||
|
||||
``--clean-final`` and ``-remove-background`` may leave undesirable
|
||||
``--clean-final`` and ``--remove-background`` may leave undesirable
|
||||
visual artifacts in some images where their algorithms have
|
||||
shortcomings. Files should be visually reviewed after using these
|
||||
options.
|
||||
@@ -340,6 +340,9 @@ levels in the GCC compiler.
|
||||
- Enables lossless optimizations, such as transcoding images to more
|
||||
efficient formats. Also compress other uncompressed objects in the
|
||||
PDF and enables the more efficient "object streams" within the PDF.
|
||||
(If ``--jbig2-lossy`` is issued, then lossy JBIG2 optimization is used.
|
||||
The decision to use lossy JBIG2 is separate from standard optimization
|
||||
settings.)
|
||||
* - ``--optimize 2``
|
||||
- All of the above, and enables lossy optimizations and color quantization.
|
||||
* - ``--optimize 3``
|
||||
@@ -360,3 +363,8 @@ fo a PDF.
|
||||
ocrmypdf --optimize 3 in.pdf out.pdf # Make it small
|
||||
|
||||
Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
|
||||
|
||||
.. note::
|
||||
|
||||
Image processing and PDF/A conversion can also introduce lossy transformations
|
||||
to your PDF images, even when ``--optimize 1`` is in use.
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 256 256.00001"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"
|
||||
id="svg270"
|
||||
sodipodi:docname="logo-square-256.svg"
|
||||
inkscape:export-filename="/home/jb/src/ocrmypdf/docs/images/logo-square.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:serif="http://www.serif.com/"><metadata
|
||||
id="metadata276"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs274" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2396"
|
||||
inkscape:window-height="1691"
|
||||
id="namedview272"
|
||||
showgrid="false"
|
||||
lock-margins="false"
|
||||
inkscape:zoom="2.0079523"
|
||||
inkscape:cx="189.74554"
|
||||
inkscape:cy="54.533168"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg270"
|
||||
inkscape:pagecheckerboard="0"
|
||||
width="256px"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<g
|
||||
id="svg"
|
||||
transform="matrix(0.48534351,0,0,0.4057699,1.8106874,71.192214)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="520"
|
||||
height="280"
|
||||
style="fill:#ffffff"
|
||||
id="rect188" />
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,-69.7528,-83.422)"
|
||||
id="g267">
|
||||
<g
|
||||
transform="translate(243.977,20.0703)"
|
||||
id="g218">
|
||||
<g
|
||||
id="Page">
|
||||
<g
|
||||
transform="matrix(0.961773,0,0,1.05962,6.19811,-3.01071)"
|
||||
id="g192">
|
||||
<path
|
||||
d="m 328.5,97.682 c 0,-1.217 -0.517,-2.386 -1.444,-3.264 -7.03,-6.66 -37.614,-35.638 -44.828,-42.474 -0.977,-0.925 -2.327,-1.448 -3.738,-1.448 -13.997,0 -90.407,0 -111.151,0 -2.871,0 -5.198,2.113 -5.198,4.718 0,27.837 0,170.351 0,198.186 0,2.605 2.327,4.717 5.197,4.717 24.904,0 131.821,0 156.2,0 2.74,0 4.962,-2.016 4.962,-4.504 0,-24.345 0,-139.717 0,-155.931 z"
|
||||
style="fill:#fdfdfd;stroke:#333333;stroke-width:3.95px"
|
||||
id="path190" />
|
||||
</g>
|
||||
<g
|
||||
id="Dog-ear"
|
||||
serif:id="Dog ear"
|
||||
transform="translate(-4,2)">
|
||||
<path
|
||||
d="m 277.072,48.496 v 45.352 c 0,1.324 0.526,2.593 1.462,3.529 0.936,0.936 2.205,1.462 3.529,1.462 12.485,0 44.078,0 44.078,0"
|
||||
style="fill:#f5f5f5;stroke:#333333;stroke-width:4px"
|
||||
id="path194" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-29.6816,-0.395178)"
|
||||
id="g216">
|
||||
<g
|
||||
transform="matrix(1.00243,0,0,1.11818,-144.72,-8.80181)"
|
||||
id="g200">
|
||||
<path
|
||||
d="m 465.73,119.654 c 0,-2.049 -1.856,-3.713 -4.142,-3.713 H 310.259 c -2.286,0 -4.142,1.664 -4.142,3.713 v 63.454 c 0,2.049 1.856,3.713 4.142,3.713 h 151.329 c 2.286,0 4.142,-1.664 4.142,-3.713 z"
|
||||
style="fill:#f80000;stroke:#ffffff;stroke-width:3.77px"
|
||||
id="path198" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.24571,0,0,1.35864,116.812,84.3924)"
|
||||
id="g214">
|
||||
<g
|
||||
transform="matrix(64,0,0,64,42.1437,77.6203)"
|
||||
id="g204">
|
||||
<path
|
||||
d="m 0.084,0 v -0.68 h 0.213 c 0.074,0 0.137,0.017 0.19,0.05 0.053,0.034 0.079,0.09 0.079,0.168 0,0.077 -0.028,0.134 -0.085,0.17 -0.057,0.037 -0.121,0.055 -0.193,0.055 H 0.213 V 0 Z m 0.209,-0.572 h -0.08 v 0.228 h 0.082 c 0.039,0 0.07,-0.009 0.094,-0.027 0.024,-0.017 0.037,-0.045 0.04,-0.083 0,-0.044 -0.012,-0.075 -0.036,-0.092 -0.024,-0.017 -0.057,-0.026 -0.1,-0.026 z"
|
||||
style="fill:#ffffff;fill-rule:nonzero"
|
||||
id="path202" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(64,0,0,64,79.7117,77.6203)"
|
||||
id="g208">
|
||||
<path
|
||||
d="M 0.332,0 H 0.084 v -0.68 h 0.252 c 0.105,0 0.182,0.032 0.233,0.095 0.051,0.063 0.076,0.144 0.076,0.241 0,0.105 -0.027,0.189 -0.082,0.251 C 0.508,-0.031 0.431,0 0.332,0 Z M 0.337,-0.57 H 0.213 v 0.461 H 0.33 c 0.055,0 0.099,-0.018 0.132,-0.054 C 0.495,-0.199 0.511,-0.259 0.511,-0.344 0.511,-0.415 0.497,-0.47 0.469,-0.51 0.441,-0.55 0.397,-0.57 0.337,-0.57 Z"
|
||||
style="fill:#ffffff;fill-rule:nonzero"
|
||||
id="path206" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(64,0,0,64,123.424,77.6203)"
|
||||
id="g212">
|
||||
<path
|
||||
d="M 0.405,-0.288 H 0.213 V 0 H 0.084 v -0.68 h 0.385 l 0.02,0.102 H 0.213 v 0.189 h 0.173 z"
|
||||
style="fill:#ffffff;fill-rule:nonzero"
|
||||
id="path210" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1,0,0,1.52217,67.3796,10.7507)"
|
||||
id="g222">
|
||||
<rect
|
||||
x="23.500999"
|
||||
y="81.300003"
|
||||
width="162.30499"
|
||||
height="61.77"
|
||||
style="fill:#b4d5ff"
|
||||
id="rect220" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.967536,0,0,0.961535,5.90498,47.9703)"
|
||||
id="g236">
|
||||
<g
|
||||
transform="matrix(90.4804,0,0,90.4804,82.6698,167.705)"
|
||||
id="g226">
|
||||
<path
|
||||
d="m 0.057,-0.337 c 0,-0.105 0.027,-0.19 0.082,-0.257 0.055,-0.066 0.132,-0.1 0.231,-0.102 0.107,0 0.186,0.034 0.237,0.103 0.051,0.069 0.077,0.152 0.077,0.249 0,0.105 -0.027,0.191 -0.082,0.258 -0.055,0.067 -0.133,0.1 -0.232,0.1 C 0.264,0.014 0.185,-0.02 0.134,-0.089 0.083,-0.157 0.057,-0.24 0.057,-0.337 Z m 0.135,-0.001 c 0,0.071 0.014,0.13 0.043,0.175 0.029,0.045 0.073,0.068 0.134,0.068 0.055,0 0.098,-0.02 0.131,-0.061 0.033,-0.041 0.049,-0.103 0.049,-0.188 0,-0.071 -0.014,-0.129 -0.043,-0.174 -0.029,-0.045 -0.073,-0.068 -0.134,-0.068 -0.053,0 -0.097,0.022 -0.13,0.067 -0.033,0.045 -0.05,0.105 -0.05,0.181 z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path224" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(90.4804,0,0,90.4804,147.906,167.705)"
|
||||
id="g230">
|
||||
<path
|
||||
d="M 0.505,-0.557 C 0.473,-0.567 0.448,-0.574 0.429,-0.579 0.41,-0.583 0.388,-0.585 0.361,-0.585 c -0.054,0 -0.096,0.022 -0.125,0.066 -0.029,0.044 -0.044,0.104 -0.044,0.181 0,0.066 0.012,0.123 0.037,0.171 0.025,0.048 0.066,0.072 0.124,0.072 0.029,0 0.056,-0.003 0.081,-0.009 0.025,-0.006 0.047,-0.013 0.068,-0.022 L 0.551,-0.03 C 0.525,-0.017 0.494,-0.006 0.457,0.002 0.42,0.01 0.388,0.014 0.36,0.014 0.254,0.014 0.177,-0.02 0.129,-0.088 0.081,-0.156 0.057,-0.239 0.057,-0.337 c 0,-0.105 0.027,-0.19 0.08,-0.257 0.053,-0.067 0.129,-0.1 0.228,-0.1 0.02,0 0.048,0.003 0.083,0.01 0.035,0.007 0.068,0.018 0.097,0.034 z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path228" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(90.4804,0,0,90.4804,199.751,167.705)"
|
||||
id="g234">
|
||||
<path
|
||||
d="m 0.293,-0.572 h -0.08 v 0.208 h 0.082 c 0.039,0 0.071,-0.008 0.096,-0.024 0.025,-0.015 0.038,-0.041 0.038,-0.077 0,-0.038 -0.012,-0.065 -0.036,-0.082 -0.024,-0.017 -0.057,-0.025 -0.1,-0.025 z M 0.479,0 0.335,-0.26 C 0.328,-0.259 0.32,-0.259 0.312,-0.259 0.304,-0.258 0.296,-0.258 0.288,-0.258 H 0.213 V 0 H 0.084 v -0.68 h 0.213 c 0.074,0 0.137,0.017 0.19,0.051 0.053,0.034 0.079,0.087 0.079,0.158 0,0.042 -0.011,0.078 -0.032,0.108 -0.022,0.031 -0.05,0.054 -0.084,0.071 L 0.617,0 Z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path232" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.916882,0,0,1,121.475,-32.6535)"
|
||||
id="g246">
|
||||
<g
|
||||
transform="matrix(86.953,0,0,86.953,152.996,241.878)"
|
||||
id="g240">
|
||||
<path
|
||||
d="M 0.479,-0.428 C 0.5,-0.451 0.527,-0.47 0.562,-0.484 c 0.034,-0.013 0.065,-0.02 0.092,-0.02 0.066,0 0.113,0.019 0.141,0.058 0.027,0.039 0.041,0.086 0.041,0.142 V 0 H 0.705 v -0.298 c 0,-0.031 -0.007,-0.054 -0.022,-0.071 -0.015,-0.016 -0.036,-0.024 -0.064,-0.024 -0.019,0 -0.038,0.005 -0.059,0.015 -0.021,0.01 -0.039,0.021 -0.056,0.034 0.001,0.007 0.001,0.013 0.002,0.02 0.001,0.007 0.001,0.013 0.001,0.02 V 0 H 0.376 v -0.298 c 0,-0.031 -0.007,-0.054 -0.022,-0.071 -0.015,-0.016 -0.036,-0.024 -0.063,-0.024 -0.017,0 -0.033,0.003 -0.05,0.01 -0.017,0.007 -0.034,0.016 -0.049,0.027 V 0 H 0.062 V -0.485 H 0.13 l 0.032,0.044 c 0.022,-0.02 0.049,-0.035 0.08,-0.047 0.031,-0.011 0.058,-0.016 0.083,-0.016 0.038,0 0.07,0.007 0.095,0.02 0.025,0.014 0.045,0.033 0.059,0.056 z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path238" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(86.953,0,0,86.953,228.906,241.878)"
|
||||
id="g244">
|
||||
<path
|
||||
d="M 0.156,0.023 0.179,-0.034 0.006,-0.467 0.14,-0.485 0.252,-0.191 0.358,-0.485 H 0.495 L 0.278,0.064 C 0.263,0.103 0.236,0.137 0.197,0.165 0.158,0.193 0.118,0.212 0.075,0.222 L 0.029,0.115 C 0.052,0.105 0.077,0.093 0.104,0.079 0.13,0.064 0.147,0.046 0.156,0.023 Z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path242" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="Selectors"
|
||||
transform="matrix(0.965977,0,0,0.807602,67.3796,67.3718)">
|
||||
<g
|
||||
id="Right-selector"
|
||||
serif:id="Right selector">
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,2.07044,0)"
|
||||
id="g250">
|
||||
<path
|
||||
d="M 185.806,161.156 V 67.132"
|
||||
style="fill:none;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="path248" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,161.788,169.469)"
|
||||
id="g254">
|
||||
<circle
|
||||
cx="31.523001"
|
||||
cy="34.313999"
|
||||
r="10.021"
|
||||
style="fill:#4c9fff;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="circle252" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="Left-selector"
|
||||
serif:id="Left selector">
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,-170.092,0)"
|
||||
id="g259">
|
||||
<path
|
||||
d="M 185.806,161.156 V 67.132"
|
||||
style="fill:none;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="path257" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,-10.3742,28.2274)"
|
||||
id="g263">
|
||||
<circle
|
||||
cx="31.523001"
|
||||
cy="34.313999"
|
||||
r="10.021"
|
||||
style="fill:#4c9fff;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="circle261" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
+71
-108
@@ -12,21 +12,23 @@ system/platform. This version may be out of date, however.
|
||||
|
||||
These platforms have one-liner installs:
|
||||
|
||||
+-------------------------------+-------------------------------+
|
||||
| Debian, Ubuntu | ``apt install ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
| Fedora | ``dnf install ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
| macOS | ``brew install ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
| FreeBSD | ``pkg install py38-ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
| Conda (WSL, macOS, Linux) | ``conda install ocrmypdf`` |
|
||||
+-------------------------------+-------------------------------+
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| Debian, Ubuntu | ``apt install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| Fedora | ``dnf install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| macOS | ``brew install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| FreeBSD | ``pkg install textproc/py-ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| Conda (WSL, macOS, Linux) | ``conda install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| Snap (snapcraft packaging) | ``snap install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
|
||||
More detailed procedures are outlined below. If you want to do a manual
|
||||
install, or install a more recent version than your platform provides, read on.
|
||||
@@ -41,11 +43,11 @@ Installing on Linux
|
||||
Debian and Ubuntu 18.04 or newer
|
||||
--------------------------------
|
||||
|
||||
.. |deb-stable| image:: https://repology.org/badge/version-for-repo/debian_stable/ocrmypdf.svg
|
||||
:alt: Debian 9 stable ("stretch")
|
||||
.. |deb-11| image:: https://repology.org/badge/version-for-repo/debian_11/ocrmypdf.svg
|
||||
:alt: Debian 11
|
||||
|
||||
.. |deb-testing| image:: https://repology.org/badge/version-for-repo/debian_testing/ocrmypdf.svg
|
||||
:alt: Debian 10 testing ("buster")
|
||||
.. |deb-12| image:: https://repology.org/badge/version-for-repo/debian_12/ocrmypdf.svg
|
||||
:alt: Debian 12
|
||||
|
||||
.. |deb-unstable| image:: https://repology.org/badge/version-for-repo/debian_unstable/ocrmypdf.svg
|
||||
:alt: Debian unstable
|
||||
@@ -56,17 +58,17 @@ Debian and Ubuntu 18.04 or newer
|
||||
.. |ubu-2004| image:: https://repology.org/badge/version-for-repo/ubuntu_20_04/ocrmypdf.svg
|
||||
:alt: Ubuntu 20.04 LTS
|
||||
|
||||
.. |ubu-2110| image:: https://repology.org/badge/version-for-repo/ubuntu_21_10/ocrmypdf.svg
|
||||
:alt: Ubuntu 21.10
|
||||
.. |ubu-2204| image:: https://repology.org/badge/version-for-repo/ubuntu_22_04/ocrmypdf.svg
|
||||
:alt: Ubuntu 22.04 LTS
|
||||
|
||||
+-----------------------------------------------+
|
||||
| **OCRmyPDF versions in Debian & Ubuntu** |
|
||||
+-----------------------------------------------+
|
||||
| |latest| |
|
||||
+-----------------------------------------------+
|
||||
| |deb-stable| |deb-testing| |deb-unstable| |
|
||||
| |deb-11| |deb-12| |deb-unstable| |
|
||||
+-----------------------------------------------+
|
||||
| |ubu-1804| |ubu-2004| |ubu-2110| |
|
||||
| |ubu-1804| |ubu-2004| |ubu-2204| |
|
||||
+-----------------------------------------------+
|
||||
|
||||
Users of Debian 9 ("stretch") or later, or Ubuntu 18.04 or later, including users
|
||||
@@ -80,8 +82,7 @@ As indicated in the table above, Debian and Ubuntu releases may lag
|
||||
behind the latest version. If the version available for your platform is
|
||||
out of date, you could opt to install the latest version from source.
|
||||
See `Installing HEAD revision from
|
||||
sources <#installing-head-revision-from-sources>`__. Ubuntu 16.10 to 17.10
|
||||
inclusive also had ocrmypdf, but these versions are end of life.
|
||||
sources <#installing-head-revision-from-sources>`__.
|
||||
|
||||
For full details on version availability for your platform, check the
|
||||
`Debian Package Tracker <https://tracker.debian.org/pkg/ocrmypdf>`__ or
|
||||
@@ -91,19 +92,19 @@ For full details on version availability for your platform, check the
|
||||
|
||||
OCRmyPDF for Debian and Ubuntu currently omit the JBIG2 encoder.
|
||||
OCRmyPDF works fine without it but will produce larger output files.
|
||||
If you build jbig2enc from source, ocrmypdf 7.0.0 and later will
|
||||
If you build jbig2enc from source, ocrmypdf will
|
||||
automatically detect it (specifically the ``jbig2`` binary) on the
|
||||
``PATH``. To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
Fedora
|
||||
------
|
||||
|
||||
.. |fedora-34| image:: https://repology.org/badge/version-for-repo/fedora_34/ocrmypdf.svg
|
||||
:alt: Fedora 34
|
||||
|
||||
.. |fedora-35| image:: https://repology.org/badge/version-for-repo/fedora_35/ocrmypdf.svg
|
||||
:alt: Fedora 35
|
||||
|
||||
.. |fedora-36| image:: https://repology.org/badge/version-for-repo/fedora_36/ocrmypdf.svg
|
||||
:alt: Fedora 36
|
||||
|
||||
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
|
||||
:alt: Fedore Rawhide
|
||||
|
||||
@@ -112,7 +113,7 @@ Fedora
|
||||
+-----------------------------------------------+
|
||||
| |latest| |
|
||||
+-----------------------------------------------+
|
||||
| |fedora-34| |fedora-35| |fedora-rawhide| |
|
||||
| |fedora-35| |fedora-36| |fedora-rawhide| |
|
||||
+-----------------------------------------------+
|
||||
|
||||
Users of Fedora 29 or later may simply
|
||||
@@ -138,9 +139,29 @@ from sources <#installing-head-revision-from-sources>`__.
|
||||
|
||||
.. _ubuntu-lts-latest:
|
||||
|
||||
Installing the latest version on Ubuntu 20.04 LTS
|
||||
Installing the latest version on Ubuntu 22.04 LTS
|
||||
-------------------------------------------------
|
||||
|
||||
Ubuntu 22.04 includes ocrmypdf 13.4.0 - you can install that with
|
||||
``apt install ocrmypdf``. To install a more recent version for the current
|
||||
user, follow these steps:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install ocrmypdf python3-pip
|
||||
|
||||
pip install --user --upgrade ocrmypdf
|
||||
|
||||
If you get the message ``WARNING: The script ocrmypdf is installed in
|
||||
'/home/$USER/.local/bin' which is not on PATH.``, you may need to re-login
|
||||
or open a new shell, or manually add this to your user's PATH.
|
||||
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
Ubuntu 20.04 LTS
|
||||
----------------
|
||||
|
||||
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To
|
||||
install a more recent version, uninstall the system-provided version of
|
||||
ocrmypdf, and install the following dependencies:
|
||||
@@ -171,6 +192,8 @@ To install for the current user only:
|
||||
export PATH=$HOME/.local/bin:$PATH
|
||||
pip3 install --user ocrmypdf
|
||||
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
Ubuntu 18.04 LTS
|
||||
----------------
|
||||
|
||||
@@ -291,46 +314,6 @@ To install OCRmyPDF for Alpine Linux:
|
||||
|
||||
apk add ocrmypdf
|
||||
|
||||
Mageia 7
|
||||
--------
|
||||
|
||||
There is no OS-level packaging available for Mageia, so you must install the
|
||||
dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# As root user
|
||||
urpmi.update -a
|
||||
urpmi \
|
||||
ghostscript \
|
||||
icc-profiles-openicc \
|
||||
jbig2dec \
|
||||
pngquant \
|
||||
python3-pip \
|
||||
python3-distutils-extra \
|
||||
python3-pkg-resources \
|
||||
python3-reportlab \
|
||||
qpdf \
|
||||
tesseract \
|
||||
tesseract-osd \
|
||||
tesseract-eng \
|
||||
tesseract-fra
|
||||
|
||||
To install ocrmypdf for the system:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# As root user
|
||||
pip3 install ocrmypdf
|
||||
ldconfig
|
||||
|
||||
Or, to install for the current user only:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export PATH=$HOME/.local/bin:$PATH
|
||||
pip3 install --user ocrmypdf
|
||||
|
||||
Other Linux packages
|
||||
--------------------
|
||||
|
||||
@@ -365,19 +348,6 @@ languages you can optionally install them all:
|
||||
|
||||
brew install tesseract-lang # Optional: Install all language packs
|
||||
|
||||
.. note::
|
||||
|
||||
Users who previously installed OCRmyPDF on macOS using
|
||||
``pip install ocrmypdf`` should remove the pip version
|
||||
(``pip3 uninstall ocrmypdf``) before switching to the Homebrew
|
||||
version.
|
||||
|
||||
.. note::
|
||||
|
||||
Users who previously installed OCRmyPDF from the private tap should
|
||||
switch to the mainline version (``brew untap ocrmypdf/OCRmyPDF``)
|
||||
and install from there.
|
||||
|
||||
Manual installation on macOS
|
||||
----------------------------
|
||||
|
||||
@@ -411,19 +381,19 @@ Update the homebrew pip:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --upgrade pip
|
||||
pip install --upgrade pip
|
||||
|
||||
You can then install OCRmyPDF from PyPI, for the current user:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --user ocrmypdf
|
||||
pip install --user ocrmypdf
|
||||
|
||||
or system-wide:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
pip install ocrmypdf
|
||||
|
||||
The command line program should now be available:
|
||||
|
||||
@@ -485,8 +455,8 @@ to change the PATH.
|
||||
Windows Subsystem for Linux
|
||||
---------------------------
|
||||
|
||||
#. Install Ubuntu 20.04 for Windows Subsystem for Linux, if not already installed.
|
||||
#. Follow the procedure to install :ref:`OCRmyPDF on Ubuntu 20.04 <ubuntu-lts-latest>`.
|
||||
#. Install Ubuntu 22.04 for Windows Subsystem for Linux, if not already installed.
|
||||
#. Follow the procedure to install :ref:`OCRmyPDF on Ubuntu 22.04 <ubuntu-lts-latest>`.
|
||||
#. Open the Windows command prompt and create a symlink:
|
||||
|
||||
.. code-block:: powershell
|
||||
@@ -558,16 +528,13 @@ your command prompt can run the docker "hello world" container.
|
||||
Installing on FreeBSD
|
||||
=====================
|
||||
|
||||
.. image:: https://repology.org/badge/version-for-repo/freebsd/python:ocrmypdf.svg
|
||||
.. image:: https://repology.org/badge/version-for-repo/freebsd/ocrmypdf.svg
|
||||
:alt: FreeBSD
|
||||
:target: https://repology.org/project/python:ocrmypdf/versions
|
||||
|
||||
FreeBSD 11.3, 12.0, 12.1-RELEASE and 13.0-CURRENT are supported. Other
|
||||
versions likely work but have not been tested.
|
||||
:target: https://repology.org/project/ocrmypdf/versions
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pkg install py38-ocrmypdf
|
||||
pkg install textproc/py-ocrmypdf
|
||||
|
||||
To install a more recent version, you could attempt to first install the system
|
||||
version with ``pkg``, then use ``pip install --user ocrmypdf``.
|
||||
@@ -616,18 +583,18 @@ try:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install --user ocrmypdf
|
||||
pip install --user ocrmypdf
|
||||
|
||||
You should then be able to run ``ocrmypdf --version`` and see that the
|
||||
latest version was located.
|
||||
|
||||
Since ``pip3 install --user`` does not work correctly on some platforms,
|
||||
Since ``pip install --user`` does not work correctly on some platforms,
|
||||
notably Ubuntu 16.04 and older, and the Homebrew version of Python,
|
||||
instead use this for a system wide installation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
pip install ocrmypdf
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -643,13 +610,9 @@ OCRmyPDF currently requires these external programs and libraries to be
|
||||
installed, and must be satisfied using the operating system package
|
||||
manager. ``pip`` cannot provide them.
|
||||
|
||||
The following versions are required:
|
||||
|
||||
- Python 3.7 or newer
|
||||
- Ghostscript 9.15 or newer
|
||||
- Tesseract 4.0.0-beta or newer
|
||||
|
||||
As of ocrmypdf 7.2.1, the following versions are recommended:
|
||||
|
||||
- Python 3.9 or newer
|
||||
- Ghostscript 9.23 or newer
|
||||
- Tesseract 4.0.0 or newer
|
||||
- jbig2enc 0.29 or newer
|
||||
@@ -696,7 +659,7 @@ environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install git+https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development
|
||||
mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
|
||||
@@ -704,7 +667,7 @@ allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -e git+https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
pip install -e git+https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
|
||||
You may find it easiest to install in a virtual environment, rather than
|
||||
system-wide:
|
||||
@@ -715,7 +678,7 @@ system-wide:
|
||||
python3 -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip3 install .
|
||||
pip install .
|
||||
|
||||
However, ``ocrmypdf`` will only be accessible on the system PATH when
|
||||
you activate the virtual environment.
|
||||
@@ -739,7 +702,7 @@ To install all of the development and test requirements:
|
||||
.. code-block:: bash
|
||||
|
||||
git clone -b master https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
python3 -m venv
|
||||
python -m venv
|
||||
source venv/bin/activate
|
||||
cd OCRmyPDF
|
||||
pip install -e .[test]
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ For all other Linux, you must build a JBIG2 encoder from source:
|
||||
|
||||
.. _jbig2-lossy:
|
||||
|
||||
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
|
||||
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
|
||||
are packaged as libtool and libleptonica-dev.
|
||||
|
||||
Lossy mode JBIG2
|
||||
|
||||
+13
-1
@@ -162,6 +162,11 @@ Examples
|
||||
management system.
|
||||
|
||||
|
||||
Suppressing or overriding other plugins
|
||||
---------------------------------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.initialize
|
||||
|
||||
Custom command line arguments
|
||||
-----------------------------
|
||||
|
||||
@@ -172,7 +177,7 @@ Custom command line arguments
|
||||
Execution and progress reporting
|
||||
--------------------------------
|
||||
|
||||
.. autoclass: ocrmypdf.pluginspec.Executor
|
||||
.. autoclass:: ocrmypdf.pluginspec.Executor
|
||||
:members:
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.get_logging_console
|
||||
@@ -216,3 +221,10 @@ PDF/A production
|
||||
----------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.generate_pdfa
|
||||
|
||||
PDF optimization
|
||||
----------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.optimize_pdf
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.is_optimization_enabled
|
||||
@@ -16,8 +16,49 @@ The most recent release of OCRmyPDF is |OCRmyPDF PyPI|. Any newer versions
|
||||
referred to in these notes may exist the main branch but have not been
|
||||
tagged yet.
|
||||
|
||||
.. note::
|
||||
|
||||
Attention maintainers: that these release notes may be updated with information
|
||||
about a forthcoming release that has not been tagged yet. A release is only
|
||||
official when it's tagged and posted to PyPI.
|
||||
|
||||
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||
|
||||
v13.6.2
|
||||
=======
|
||||
|
||||
- Added a shim to prevent an "error during error handling" for Python 3.7 and 3.8.
|
||||
- Modernized some type annotations.
|
||||
- Improved annotations on our _windows module to help IDEs and mypy figure out what
|
||||
we're doing.
|
||||
|
||||
v13.6.1
|
||||
=======
|
||||
|
||||
- Require setuptools-scm 7.0.5 to avoid possible issues with source distributions in
|
||||
earlier versions of setuptools-scm.
|
||||
- Suppress a spurious warning, improve tests, improve typing and other miscellany.
|
||||
|
||||
v13.6.0
|
||||
=======
|
||||
|
||||
- Added a new ``initialize`` plugin hook, making it possible to suppress built-in
|
||||
plugins more easily, among other possibilities.
|
||||
- Fixed an issue where unpaper would exit with a "wrong stream" error, probably
|
||||
related to images with an odd integer width. :issue:`887, 665`
|
||||
|
||||
v13.5.0
|
||||
=======
|
||||
|
||||
- Added a new ``optimize_pdf`` plugin hook, making it possible to create plugins that
|
||||
replace or enhance OCRmyPDF's PDF optimizer.
|
||||
- Removed all max version restrictions. Our new policy is to blacklist known-bad releases
|
||||
and only block known-bad versions of dependencies.
|
||||
- The naming schema for object that holds all OCR text that OCRmyPDF inserts has
|
||||
changed. This has always been an implementation detail (and remains so), but possibly,
|
||||
someone was relying on it and would appreciate the heads-up.
|
||||
- Cleanup.
|
||||
|
||||
v13.4.7
|
||||
=======
|
||||
|
||||
|
||||
+2
-1
@@ -19,8 +19,9 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
from __future__ import annotations
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -37,6 +37,8 @@ To use this as an API:
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PIL import Image
|
||||
|
||||
+2
-1
@@ -19,8 +19,9 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
from __future__ import annotations
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
+16
-4
@@ -20,9 +20,12 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
@@ -44,8 +47,10 @@ def getenv_bool(name: str, default: str = 'False'):
|
||||
|
||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||
ARCHIVE_DIRECTORY = os.getenv('OCR_ARCHIVE_DIRECTORY', '/processed')
|
||||
OUTPUT_DIRECTORY_YEAR_MONTH = getenv_bool('OCR_OUTPUT_DIRECTORY_YEAR_MONTH')
|
||||
ON_SUCCESS_DELETE = getenv_bool('OCR_ON_SUCCESS_DELETE')
|
||||
ON_SUCCESS_ARCHIVE = getenv_bool('OCR_ON_SUCCESS_ARCHIVE')
|
||||
DESKEW = getenv_bool('OCR_DESKEW')
|
||||
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
||||
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
|
||||
@@ -108,9 +113,13 @@ def execute_ocrmypdf(file_path):
|
||||
deskew=DESKEW,
|
||||
**OCR_JSON_SETTINGS,
|
||||
)
|
||||
if exit_code == 0 and ON_SUCCESS_DELETE:
|
||||
log.info(f'OCR is done. Deleting: {file_path}')
|
||||
file_path.unlink()
|
||||
if exit_code == 0:
|
||||
if ON_SUCCESS_DELETE:
|
||||
log.info(f'OCR is done. Deleting: {file_path}')
|
||||
file_path.unlink()
|
||||
elif ON_SUCCESS_ARCHIVE:
|
||||
log.info(f'OCR is done. Archiving {file_path.name} to {ARCHIVE_DIRECTORY}')
|
||||
shutil.move(file_path, f'{ARCHIVE_DIRECTORY}/{file_path.name}')
|
||||
else:
|
||||
log.info('OCR is done')
|
||||
|
||||
@@ -135,13 +144,16 @@ def main():
|
||||
f"Starting OCRmyPDF watcher with config:\n"
|
||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
||||
f"Output Directory: {OUTPUT_DIRECTORY}\n"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||
f"Archive Directory: {ARCHIVE_DIRECTORY}"
|
||||
)
|
||||
log.debug(
|
||||
f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||
f"ARCHIVE_DIRECTORY: {ARCHIVE_DIRECTORY}\n"
|
||||
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
||||
f"ON_SUCCESS_ARCHIVE: {ON_SUCCESS_ARCHIVE}\n"
|
||||
f"DESKEW: {DESKEW}\n"
|
||||
f"ARGS: {OCR_JSON_SETTINGS}\n"
|
||||
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
||||
|
||||
@@ -24,6 +24,8 @@ to emphasize that SaaS deployments should make sure they comply with
|
||||
Ghostscript's license as well as OCRmyPDF's.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
from subprocess import PIPE, run
|
||||
|
||||
+10
-6
@@ -1,14 +1,12 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools >= 30.3.0",
|
||||
"wheel",
|
||||
"setuptools_scm[toml] >= 3.4",
|
||||
"setuptools_scm_git_archive"
|
||||
"setuptools >= 52",
|
||||
"setuptools_scm[toml] >= 7.0.5",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
version_scheme = "post-release"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
@@ -96,6 +94,12 @@ module = [
|
||||
'pdfminer.*',
|
||||
'reportlab.*',
|
||||
'fitz',
|
||||
'libxmp.utils'
|
||||
'libxmp.utils',
|
||||
'importlib_metadata'
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pylint.basic]
|
||||
good-names = ["i", "j", "k", "ex", "Run", "_", "e", "p", "im", "w", "h", "m", "x", "y", "a", "b", "fp", "n", "f", "s", "v", "q", "dx", "dy"]
|
||||
logging-format-style = "old"
|
||||
disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "logging-fstring-interpolation", "missing-function-docstring", "too-few-public-methods"]
|
||||
|
||||
@@ -47,15 +47,16 @@ packages = find:
|
||||
install_requires =
|
||||
Pillow>=8.2.0
|
||||
coloredlogs>=14.0 # strictly optional
|
||||
img2pdf>=0.3.0,<0.5 # pure Python
|
||||
img2pdf>=0.3.0 # pure Python
|
||||
packaging>=20
|
||||
pdfminer.six!=20200720,>=20191110
|
||||
pikepdf!=5.0.0,>=4.0.0
|
||||
pluggy>=0.13.0,<2
|
||||
pluggy>=0.13.0
|
||||
reportlab>=3.5.66
|
||||
tqdm>=4
|
||||
importlib-metadata>=4;python_version<'3.8' # until Python 3.8
|
||||
importlib-resources>=5;python_version<'3.9' # until Python 3.9
|
||||
typing-extensions>=4;python_version<'3.10'
|
||||
python_requires = >=3.7
|
||||
include_package_data = True
|
||||
package_dir =
|
||||
@@ -86,10 +87,12 @@ test =
|
||||
pytest-cov>=2.11.1
|
||||
pytest-xdist>=2.2.0
|
||||
python-xmp-toolkit==2.0.1 # also requires apt-get install libexempi3
|
||||
types-Pillow
|
||||
types-humanfriendly
|
||||
watcher =
|
||||
watchdog>=1.0.2,<3
|
||||
watchdog>=1.0.2
|
||||
webservice =
|
||||
Flask>=1,<3
|
||||
Flask>=1
|
||||
|
||||
[options.package_data]
|
||||
ocrmypdf =
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""setup.py to support older setuptools and pip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# Minimal setup to support older setuptools/setuptools_scm
|
||||
setup(
|
||||
setup_requires=[ # can be removed whenever we can drop pip 9 support
|
||||
'setuptools_scm', # so that version will work
|
||||
'setuptools_scm_git_archive', # enable version from github tarballs
|
||||
],
|
||||
use_scm_version={'version_scheme': 'post-release'},
|
||||
)
|
||||
setup()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
name: ocrmypdf
|
||||
title: OCRmyPDF
|
||||
base: core20
|
||||
version: git
|
||||
summary: OCRmyPDF adds optical character recognition (OCR) to PDFs
|
||||
description: OCRmyPDF packaged for snap
|
||||
grade: stable
|
||||
confinement: strict
|
||||
icon: docs/images/logo-square-256.svg
|
||||
license: MPL-2.0
|
||||
|
||||
architectures: [amd64]
|
||||
|
||||
environment:
|
||||
TESSDATA_PREFIX: $SNAP/usr/share/tesseract-ocr/4.00/tessdata
|
||||
GS_LIB: $SNAP/usr/share/ghostscript/9.50/Resource/Init
|
||||
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.50/Resource/Font
|
||||
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
|
||||
|
||||
apps:
|
||||
ocrmypdf:
|
||||
command: usr/bin/snapcraft-preload python3 -m ocrmypdf
|
||||
plugs:
|
||||
- desktop
|
||||
- desktop-legacy
|
||||
- wayland
|
||||
- x11
|
||||
- home
|
||||
- removable-media
|
||||
|
||||
parts:
|
||||
snapcraft-preload:
|
||||
source: https://github.com/sergiusens/snapcraft-preload.git
|
||||
plugin: cmake
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr -DLIBPATH=/usr/lib
|
||||
build-packages:
|
||||
- on amd64:
|
||||
- gcc-multilib
|
||||
- g++-multilib
|
||||
stage-packages:
|
||||
- lib32stdc++6
|
||||
|
||||
ocrmypdf:
|
||||
plugin: python
|
||||
source: https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
|
||||
stage-packages:
|
||||
- ghostscript
|
||||
- icc-profiles-free
|
||||
- liblept5
|
||||
- libxml2
|
||||
- pngquant
|
||||
- tesseract-ocr-all
|
||||
- unpaper
|
||||
- qpdf
|
||||
- zlib1g
|
||||
|
||||
python-packages:
|
||||
- cffi
|
||||
- pdfminer.six
|
||||
- pikepdf
|
||||
- Pillow
|
||||
- pluggy
|
||||
- reportlab
|
||||
- setuptools
|
||||
- tqdm
|
||||
- pipe
|
||||
|
||||
override-build: |
|
||||
snapcraftctl build
|
||||
ln -sf ../usr/lib/libsnapcraft-preload.so $SNAPCRAFT_PART_INSTALL/lib/libsnapcraft-preload.so
|
||||
@@ -4,6 +4,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Adds OCR layer to PDFs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pluggy import HookimplMarker as _HookimplMarker
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""ocrmypdf command line entrypoint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -63,7 +66,7 @@ def run(args=None):
|
||||
log.error(e)
|
||||
return ExitCode.missing_dependency
|
||||
|
||||
if hasattr(signal, 'SIGBUS'):
|
||||
with suppress(AttributeError, OSError):
|
||||
signal.signal(signal.SIGBUS, sigbus)
|
||||
|
||||
result = run_pipeline(options=options, plugin_manager=plugin_manager)
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""OCRmyPDF concurrency abstractions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Iterable, Optional
|
||||
from typing import Callable, Iterable
|
||||
|
||||
|
||||
def _task_noop(*_args, **_kwargs):
|
||||
@@ -14,6 +18,8 @@ def _task_noop(*_args, **_kwargs):
|
||||
|
||||
|
||||
class NullProgressBar:
|
||||
"""Progress bar API that takes no actions."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
@@ -28,6 +34,8 @@ class NullProgressBar:
|
||||
|
||||
|
||||
class Executor(ABC):
|
||||
"""Abstract concurrent executor."""
|
||||
|
||||
pool_lock = threading.Lock()
|
||||
pbar_class = NullProgressBar
|
||||
|
||||
@@ -41,10 +49,10 @@ class Executor(ABC):
|
||||
use_threads: bool,
|
||||
max_workers: int,
|
||||
tqdm_kwargs: dict,
|
||||
worker_initializer: Optional[Callable] = None,
|
||||
task: Optional[Callable] = None,
|
||||
task_arguments: Optional[Iterable] = None,
|
||||
task_finished: Optional[Callable] = None,
|
||||
worker_initializer: Callable | None = None,
|
||||
task: Callable | None = None,
|
||||
task_arguments: Iterable | None = None,
|
||||
task_finished: Callable | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set up parallel execution and progress reporting.
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
|
||||
|
||||
"""Manage third party executables"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Interface to Ghostscript executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -14,13 +16,11 @@ import sys
|
||||
from io import BytesIO
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import PIPE, CalledProcessError
|
||||
from typing import Optional
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from ocrmypdf.exceptions import SubprocessOutputError
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
|
||||
|
||||
@@ -33,29 +33,18 @@ except AttributeError:
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
missing_gs_error = """
|
||||
---------------------------------------------------------------------
|
||||
This error normally occurs when ocrmypdf find can't Ghostscript.
|
||||
Please ensure Ghostscript is installed and its location is added to
|
||||
the system PATH environment variable.
|
||||
|
||||
For details see:
|
||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||
---------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
# Most reliable what to get the bitness of Python interpreter, according to Python docs
|
||||
_is_64bit = sys.maxsize > 2 ** 32
|
||||
_IS_64BIT = sys.maxsize > 2**32
|
||||
|
||||
_gswin = None
|
||||
_GSWIN = None
|
||||
if os.name == 'nt':
|
||||
if _is_64bit:
|
||||
_gswin = 'gswin64c'
|
||||
if _IS_64BIT:
|
||||
_GSWIN = 'gswin64c'
|
||||
else:
|
||||
_gswin = 'gswin32c'
|
||||
_GSWIN = 'gswin32c'
|
||||
|
||||
GS = _gswin if _gswin else 'gs'
|
||||
del _gswin
|
||||
GS = _GSWIN if _GSWIN else 'gs'
|
||||
del _GSWIN
|
||||
|
||||
|
||||
def version():
|
||||
@@ -89,8 +78,8 @@ def rasterize_pdf(
|
||||
raster_device: str,
|
||||
raster_dpi: Resolution,
|
||||
pageno: int = 1,
|
||||
page_dpi: Optional[Resolution] = None,
|
||||
rotation: Optional[int] = None,
|
||||
page_dpi: Resolution | None = None,
|
||||
rotation: int | None = None,
|
||||
filter_vector: bool = False,
|
||||
):
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units."""
|
||||
@@ -115,7 +104,7 @@ def rasterize_pdf(
|
||||
+ [
|
||||
'-o',
|
||||
'-',
|
||||
'-sstdout=%stderr',
|
||||
'-sstdout=%stderr', # Literal %s, not string interpolation
|
||||
'-dAutoRotatePages=/None', # Probably has no effect on raster
|
||||
'-f',
|
||||
fspath(input_file),
|
||||
@@ -126,7 +115,7 @@ def rasterize_pdf(
|
||||
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
|
||||
except CalledProcessError as e:
|
||||
log.error(e.stderr.decode(errors='replace'))
|
||||
raise SubprocessOutputError('Ghostscript rasterizing failed')
|
||||
raise SubprocessOutputError('Ghostscript rasterizing failed') from e
|
||||
else:
|
||||
stderr = p.stderr.decode(errors='replace')
|
||||
if _gs_error_reported(stderr):
|
||||
@@ -156,6 +145,8 @@ def rasterize_pdf(
|
||||
|
||||
|
||||
class GhostscriptFollower:
|
||||
"""Parses the output of Ghostscript and uses it to update the progress bar."""
|
||||
|
||||
re_process = re.compile(r"Processing pages \d+ through (\d+).")
|
||||
re_page = re.compile(r"Page (\d+)")
|
||||
|
||||
@@ -251,7 +242,7 @@ def generate_pdfa(
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-o",
|
||||
"-",
|
||||
"-sstdout=%stderr",
|
||||
"-sstdout=%stderr", # Literal %s, not string interpolation
|
||||
]
|
||||
)
|
||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Interface to jbig2 executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from subprocess import PIPE
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"""Interface to pngquant executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
"""Interface to Tesseract executable"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from math import pi
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
|
||||
from packaging.version import Version
|
||||
from PIL import Image
|
||||
@@ -46,7 +47,7 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
</html>
|
||||
"""
|
||||
|
||||
TESSERACT_THRESHOLDING_METHODS: Dict[str, int] = {
|
||||
TESSERACT_THRESHOLDING_METHODS: dict[str, int] = {
|
||||
'auto': 0,
|
||||
'otsu': 0,
|
||||
'adaptive-otsu': 1,
|
||||
@@ -55,9 +56,11 @@ TESSERACT_THRESHOLDING_METHODS: Dict[str, int] = {
|
||||
|
||||
|
||||
class TesseractLoggerAdapter(logging.LoggerAdapter):
|
||||
"Prepend [tesseract] to messages emitted from tesseract"
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
kwargs['extra'] = self.extra
|
||||
return '[tesseract] %s' % (msg), kwargs
|
||||
return f'[tesseract] {msg}', kwargs
|
||||
|
||||
|
||||
TESSERACT_VERSION_PATTERN = r"""
|
||||
@@ -105,6 +108,7 @@ TESSERACT_VERSION_PATTERN = r"""
|
||||
|
||||
|
||||
class TesseractVersion(Version):
|
||||
"Modify standard packaging.Version regex to support Tesseract idiosyncracies."
|
||||
_regex = re.compile(
|
||||
r"^\s*" + TESSERACT_VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE
|
||||
)
|
||||
@@ -159,7 +163,7 @@ def get_languages():
|
||||
return {lang.strip() for lang in rest}
|
||||
|
||||
|
||||
def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
||||
def tess_base_args(langs: list[str], engine_mode: int | None) -> list[str]:
|
||||
args = ['tesseract']
|
||||
if langs:
|
||||
args.extend(['-l', '+'.join(langs)])
|
||||
@@ -168,19 +172,19 @@ def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
||||
return args
|
||||
|
||||
|
||||
def _parse_tesseract_output(binary_output: bytes) -> Dict[str, str]:
|
||||
def g():
|
||||
def _parse_tesseract_output(binary_output: bytes) -> dict[str, str]:
|
||||
def gen():
|
||||
for line in binary_output.decode().splitlines():
|
||||
line = line.strip()
|
||||
parts = line.split(':', maxsplit=2)
|
||||
if len(parts) == 2:
|
||||
yield parts[0].strip(), parts[1].strip()
|
||||
|
||||
return {k: v for k, v in g()}
|
||||
return dict(gen())
|
||||
|
||||
|
||||
def get_orientation(
|
||||
input_file: Path, engine_mode: Optional[int], timeout: float
|
||||
input_file: Path, engine_mode: int | None, timeout: float
|
||||
) -> OrientationConfidence:
|
||||
args_tesseract = tess_base_args(['osd'], engine_mode) + [
|
||||
'--psm',
|
||||
@@ -205,14 +209,14 @@ def get_orientation(
|
||||
|
||||
osd = _parse_tesseract_output(p.stdout)
|
||||
angle = int(osd.get('Orientation in degrees', 0))
|
||||
oc = OrientationConfidence(
|
||||
orient_conf = OrientationConfidence(
|
||||
angle=angle, confidence=float(osd.get('Orientation confidence', 0))
|
||||
)
|
||||
return oc
|
||||
return orient_conf
|
||||
|
||||
|
||||
def get_deskew(
|
||||
input_file: Path, languages: List[str], engine_mode: Optional[int], timeout: float
|
||||
input_file: Path, languages: list[str], engine_mode: int | None, timeout: float
|
||||
) -> float:
|
||||
"""Gets angle to deskew this page, in degrees."""
|
||||
args_tesseract = tess_base_args(languages, engine_mode) + [
|
||||
@@ -303,9 +307,9 @@ def generate_hocr(
|
||||
input_file: Path,
|
||||
output_hocr: Path,
|
||||
output_text: Path,
|
||||
languages: List[str],
|
||||
languages: list[str],
|
||||
engine_mode: int,
|
||||
tessconfig: List[str],
|
||||
tessconfig: list[str],
|
||||
timeout: float,
|
||||
pagesegmode: int,
|
||||
thresholding: int,
|
||||
@@ -369,9 +373,9 @@ def generate_pdf(
|
||||
input_file: Path,
|
||||
output_pdf: Path,
|
||||
output_text: Path,
|
||||
languages: List[str],
|
||||
languages: list[str],
|
||||
engine_mode: int,
|
||||
tessconfig: List[str],
|
||||
tessconfig: list[str],
|
||||
timeout: float,
|
||||
pagesegmode: int,
|
||||
thresholding: int,
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
"""Interface to unpaper executable"""
|
||||
|
||||
import logging
|
||||
@@ -18,7 +19,7 @@ from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT
|
||||
from typing import Iterator, List, Optional, Tuple, Union
|
||||
from typing import Iterator, Union
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@@ -30,12 +31,16 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
from tempfile import TemporaryDirectory as _TemporaryDirectory
|
||||
|
||||
# Consume the ignore_cleanup_errors kwarg in Python 3.9 and older, without acting
|
||||
# on this keyword. Users who need this issue full resolved should upgrade to Python
|
||||
# 3.10.
|
||||
# See: https://github.com/python/cpython/pull/24793
|
||||
|
||||
class TemporaryDirectory(_TemporaryDirectory):
|
||||
"""Shim to consume ignore_cleanup_errors kwarg on Python 3.9 and older.
|
||||
|
||||
The argument is consumed without action. If users are getting errors related
|
||||
to temporary file cleanup, they should upgrade to Python 3.10 which properly
|
||||
cleans up temporary directories on Windows.
|
||||
|
||||
See: https://github.com/python/cpython/pull/24793
|
||||
"""
|
||||
|
||||
def __init__(self, ignore_cleanup_errors=False, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -50,6 +55,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnpaperImageTooLargeError(Exception):
|
||||
"""To capture details when an image is too large for unpaper."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
w,
|
||||
@@ -66,11 +73,13 @@ def version() -> str:
|
||||
return get_version('unpaper')
|
||||
|
||||
|
||||
def _convert_image(im: Image.Image) -> Tuple[Image.Image, bool, str]:
|
||||
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
|
||||
SUPPORTED_MODES = {'1', 'L', 'RGB'}
|
||||
|
||||
|
||||
def _convert_image(im: Image.Image) -> tuple[Image.Image, bool]:
|
||||
im_modified = False
|
||||
|
||||
if im.mode not in SUFFIXES:
|
||||
if im.mode not in SUPPORTED_MODES:
|
||||
log.info("Converting image to other colorspace")
|
||||
try:
|
||||
if im.mode == 'P' and len(im.getcolors()) == 2:
|
||||
@@ -83,41 +92,41 @@ def _convert_image(im: Image.Image) -> Tuple[Image.Image, bool, str]:
|
||||
) from e
|
||||
else:
|
||||
im_modified = True
|
||||
try:
|
||||
suffix = SUFFIXES[im.mode]
|
||||
except KeyError:
|
||||
raise MissingDependencyError(
|
||||
"Failed to convert image to a supported format."
|
||||
) from None
|
||||
return im, im_modified, suffix
|
||||
if im.mode not in SUPPORTED_MODES:
|
||||
raise MissingDependencyError(
|
||||
"Failed to convert image to a supported format."
|
||||
) from None
|
||||
return im, im_modified
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _setup_unpaper_io(input_file: Path) -> Iterator[Tuple[Path, Path, Path]]:
|
||||
def _setup_unpaper_io(input_file: Path) -> Iterator[tuple[Path, Path, Path]]:
|
||||
with Image.open(input_file) as im:
|
||||
if im.width * im.height >= UNPAPER_IMAGE_PIXEL_LIMIT:
|
||||
raise UnpaperImageTooLargeError(w=im.width, h=im.height)
|
||||
im, im_modified, suffix = _convert_image(im)
|
||||
im, im_modified = _convert_image(im)
|
||||
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
|
||||
tmppath = Path(tmpdir)
|
||||
if im_modified or input_file.suffix != '.pnm':
|
||||
input_pnm = tmppath / 'input.pnm'
|
||||
im.save(input_pnm, format='PPM')
|
||||
if im_modified or input_file.suffix != '.png':
|
||||
input_png = tmppath / 'input.png'
|
||||
im.save(input_png, format='PNG')
|
||||
else:
|
||||
# No changes, PNG input, just use the file we already have
|
||||
input_pnm = input_file
|
||||
input_png = input_file
|
||||
|
||||
output_pnm = tmppath / f'output{suffix}'
|
||||
yield input_pnm, output_pnm, tmppath
|
||||
# unpaper can write .png too, but it seems to write them slowly
|
||||
# adds a few seconds to test suite - so just use pnm
|
||||
output_pnm = tmppath / 'output.pnm'
|
||||
yield input_png, output_pnm, tmppath
|
||||
|
||||
|
||||
def run_unpaper(
|
||||
input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: List[str]
|
||||
input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: list[str]
|
||||
) -> None:
|
||||
args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args
|
||||
|
||||
with _setup_unpaper_io(input_file) as (input_pnm, output_pnm, tmpdir):
|
||||
with _setup_unpaper_io(input_file) as (input_png, output_pnm, tmpdir):
|
||||
# To prevent any shenanigans from accepting arbitrary parameters in
|
||||
# --unpaper-args, we:
|
||||
# 1) run with cwd set to a tmpdir with only unpaper's files
|
||||
@@ -125,7 +134,7 @@ def run_unpaper(
|
||||
# 3) append absolute paths for the input and output file
|
||||
# This should ensure that a user cannot clobber some other file with
|
||||
# their unpaper arguments (whether intentionally or otherwise)
|
||||
args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)])
|
||||
args_unpaper.extend([os.fspath(input_png), os.fspath(output_pnm)])
|
||||
run(
|
||||
args_unpaper,
|
||||
close_fds=True,
|
||||
@@ -146,7 +155,7 @@ def run_unpaper(
|
||||
) from e
|
||||
|
||||
|
||||
def validate_custom_args(args: str) -> List[str]:
|
||||
def validate_custom_args(args: str) -> list[str]:
|
||||
unpaper_args = shlex.split(args)
|
||||
if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args):
|
||||
raise ValueError('No filenames allowed in --unpaper-args')
|
||||
@@ -158,7 +167,7 @@ def clean(
|
||||
output_file: Path,
|
||||
*,
|
||||
dpi: DecFloat,
|
||||
unpaper_args: Optional[List[str]] = None,
|
||||
unpaper_args: list[str] | None = None,
|
||||
) -> Path:
|
||||
default_args = [
|
||||
'--layout',
|
||||
|
||||
+10
-6
@@ -4,19 +4,19 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""For grafting text-only PDF pages onto freeform PDF pages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pikepdf import (
|
||||
Dictionary,
|
||||
Name,
|
||||
Object,
|
||||
Operator,
|
||||
Page,
|
||||
Pdf,
|
||||
PdfError,
|
||||
PdfMatrix,
|
||||
@@ -81,6 +81,8 @@ def strip_invisible_text(pdf, page):
|
||||
|
||||
|
||||
class OcrGrafter:
|
||||
"""Manages grafting text-only PDFs onto regular PDFs."""
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.path_base = context.origin
|
||||
@@ -102,8 +104,8 @@ class OcrGrafter:
|
||||
self,
|
||||
*,
|
||||
pageno: int,
|
||||
image: Optional[Path],
|
||||
textpdf: Optional[Path],
|
||||
image: Path | None,
|
||||
textpdf: Path | None,
|
||||
autorotate_correction: int,
|
||||
):
|
||||
if textpdf and not self.font:
|
||||
@@ -236,6 +238,8 @@ class OcrGrafter:
|
||||
):
|
||||
"""Insert the text layer from text page 0 on to pdf_base at page_num"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
log.debug("Grafting")
|
||||
if Path(textpdf).stat().st_size == 0:
|
||||
return
|
||||
@@ -282,7 +286,7 @@ class OcrGrafter:
|
||||
|
||||
base_resources = _ensure_dictionary(base_page, Name.Resources)
|
||||
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
|
||||
text_xobj_name = Name('/' + str(uuid.uuid4()))
|
||||
text_xobj_name = Name.random(prefix="OCR-")
|
||||
xobj = self.pdf_base.make_stream(pdf_text_contents)
|
||||
base_xobjs[text_xobj_name] = xobj
|
||||
xobj.Type = Name.XObject
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Defines context objects that are passed to child processes/threads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
@@ -49,7 +52,7 @@ class PdfContext:
|
||||
"""
|
||||
return self.work_folder / name
|
||||
|
||||
def get_page_contexts(self) -> Iterator['PageContext']:
|
||||
def get_page_contexts(self) -> Iterator[PageContext]:
|
||||
"""Get all ``PageContext`` for this PDF."""
|
||||
npages = len(self.pdfinfo)
|
||||
for n in range(npages):
|
||||
@@ -83,7 +86,7 @@ class PageContext:
|
||||
The path will be based in a common temporary folder and have a prefix based
|
||||
on the page number.
|
||||
"""
|
||||
return self.work_folder / ("%06d_%s" % (self.pageno + 1, name))
|
||||
return self.work_folder / f"{(self.pageno + 1):06d}_{name}"
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Logging support classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class PageNumberFilter(logging.Filter):
|
||||
"""Insert PDF page number that emitted log message to log record."""
|
||||
|
||||
def filter(self, record):
|
||||
pageno = getattr(record, 'pageno', None)
|
||||
if isinstance(pageno, int):
|
||||
|
||||
+46
-33
@@ -4,6 +4,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""OCRmyPDF page processing pipeline functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -13,7 +16,7 @@ from contextlib import suppress
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import Dict, Iterable, Optional
|
||||
from typing import Iterable
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
@@ -34,14 +37,13 @@ from ocrmypdf.exceptions import (
|
||||
)
|
||||
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
|
||||
from ocrmypdf.hocrtransform import HocrTransform
|
||||
from ocrmypdf.optimize import optimize
|
||||
from ocrmypdf.pdfa import generate_pdfa_ps
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
|
||||
|
||||
# Remove this workaround when we require Pillow >= 10
|
||||
try:
|
||||
BICUBIC = Image.Resampling.BICUBIC # type: ignore
|
||||
except AttributeError:
|
||||
except AttributeError: # pragma: no cover
|
||||
# Pillow 9 shim
|
||||
BICUBIC = Image.BICUBIC # type: ignore
|
||||
|
||||
@@ -332,7 +334,8 @@ def is_ocr_required(page_context: PageContext):
|
||||
ocr_required = False
|
||||
log.warning(
|
||||
"page too big, skipping OCR "
|
||||
f"({(pixel_count / 1_000_000):.1f} MPixels > {options.skip_big:.1f} MPixels --skip-big)"
|
||||
f"({(pixel_count / 1_000_000):.1f} MPixels > "
|
||||
f"{options.skip_big:.1f} MPixels --skip-big)"
|
||||
)
|
||||
return ocr_required
|
||||
|
||||
@@ -430,8 +433,8 @@ def rasterize(
|
||||
output_file = page_context.get_path(f'rasterize{output_tag}.png')
|
||||
pageinfo = page_context.pageinfo
|
||||
|
||||
def at_least(cs):
|
||||
return max(device_idx, colorspaces.index(cs))
|
||||
def at_least(colorspace):
|
||||
return max(device_idx, colorspaces.index(colorspace))
|
||||
|
||||
for image in pageinfo.images:
|
||||
if image.type_ != 'image':
|
||||
@@ -471,10 +474,10 @@ def rasterize(
|
||||
|
||||
def preprocess_remove_background(input_file: Path, page_context: PageContext):
|
||||
if any(image.bpc > 1 for image in page_context.pageinfo.images):
|
||||
output_file = page_context.get_path('pp_rm_bg.png')
|
||||
# leptonica.remove_background(input_file, output_file)
|
||||
raise NotImplementedError("--remove-background is temporarily not implemented")
|
||||
return output_file
|
||||
# output_file = page_context.get_path('pp_rm_bg.png')
|
||||
# leptonica.remove_background(input_file, output_file)
|
||||
# return output_file
|
||||
else:
|
||||
log.info("background removal skipped on mono page")
|
||||
return input_file
|
||||
@@ -493,7 +496,7 @@ def preprocess_deskew(input_file: Path, page_context: PageContext):
|
||||
deskewed = im.rotate(
|
||||
deskew_angle_degrees,
|
||||
resample=BICUBIC,
|
||||
fillcolor=ImageColor.getcolor('white', mode=im.mode),
|
||||
fillcolor=ImageColor.getcolor('white', mode=im.mode), # type: ignore
|
||||
)
|
||||
deskewed.save(output_file, dpi=dpi)
|
||||
|
||||
@@ -664,7 +667,7 @@ def ocr_engine_textonly_pdf(input_image: Path, page_context: PageContext):
|
||||
return (output_pdf, output_text)
|
||||
|
||||
|
||||
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]:
|
||||
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
|
||||
options = context.options
|
||||
|
||||
def from_document_info(key):
|
||||
@@ -678,15 +681,14 @@ def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]:
|
||||
k: from_document_info(k)
|
||||
for k in ('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')
|
||||
}
|
||||
if options is not None:
|
||||
if options.title:
|
||||
pdfmark['/Title'] = options.title
|
||||
if options.author:
|
||||
pdfmark['/Author'] = options.author
|
||||
if options.keywords:
|
||||
pdfmark['/Keywords'] = options.keywords
|
||||
if options.subject:
|
||||
pdfmark['/Subject'] = options.subject
|
||||
if options.title:
|
||||
pdfmark['/Title'] = options.title
|
||||
if options.author:
|
||||
pdfmark['/Author'] = options.author
|
||||
if options.keywords:
|
||||
pdfmark['/Keywords'] = options.keywords
|
||||
if options.subject:
|
||||
pdfmark['/Subject'] = options.subject
|
||||
|
||||
creator_tag = context.plugin_manager.hook.get_ocr_engine().creator_tag(options)
|
||||
|
||||
@@ -816,13 +818,14 @@ def metadata_fixup(working_file: Path, context: PdfContext):
|
||||
missing = set(meta_original.keys()) - set(meta.keys())
|
||||
report_on_metadata(missing)
|
||||
|
||||
optimizing = context.plugin_manager.hook.is_optimization_enabled(
|
||||
context=context
|
||||
)
|
||||
pdf.save(
|
||||
output_file,
|
||||
**get_pdf_save_settings(options.output_type),
|
||||
linearize=( # Don't linearize if optimize() will be linearizing too
|
||||
should_linearize(working_file, context)
|
||||
if options.optimize == 0
|
||||
else False
|
||||
not optimizing and should_linearize(working_file, context)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -831,12 +834,22 @@ def metadata_fixup(working_file: Path, context: PdfContext):
|
||||
|
||||
def optimize_pdf(input_file: Path, context: PdfContext, executor: Executor):
|
||||
output_file = context.get_path('optimize.pdf')
|
||||
save_settings = dict(
|
||||
output_pdf, messages = context.plugin_manager.hook.optimize_pdf(
|
||||
input_pdf=input_file,
|
||||
output_pdf=output_file,
|
||||
context=context,
|
||||
executor=executor,
|
||||
linearize=should_linearize(input_file, context),
|
||||
**get_pdf_save_settings(context.options.output_type),
|
||||
)
|
||||
optimize(input_file, output_file, context, save_settings, executor)
|
||||
return output_file
|
||||
|
||||
input_size = input_file.stat().st_size
|
||||
output_size = output_file.stat().st_size
|
||||
if output_size > 0:
|
||||
ratio = input_size / output_size
|
||||
savings = 1 - output_size / input_size
|
||||
log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
|
||||
|
||||
return output_pdf, messages
|
||||
|
||||
|
||||
def enumerate_compress_ranges(iterable):
|
||||
@@ -855,11 +868,11 @@ def enumerate_compress_ranges(iterable):
|
||||
yield (skipped_from, index), None
|
||||
|
||||
|
||||
def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
|
||||
def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext):
|
||||
output_file = context.get_path('sidecar.txt')
|
||||
with open(output_file, 'w', encoding="utf-8") as stream:
|
||||
for (frm, to), txt_file in enumerate_compress_ranges(txt_files):
|
||||
if frm != 1:
|
||||
for (from_, to_), txt_file in enumerate_compress_ranges(txt_files):
|
||||
if from_ != 1:
|
||||
stream.write('\f') # Form feed between pages
|
||||
if txt_file:
|
||||
with open(txt_file, encoding="utf-8") as in_:
|
||||
@@ -872,10 +885,10 @@ def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
|
||||
else:
|
||||
stream.write(txt)
|
||||
else:
|
||||
if frm != to:
|
||||
pages = f'{frm}-{to}'
|
||||
if from_ != to_:
|
||||
pages = f'{from_}-{to_}'
|
||||
else:
|
||||
pages = f'{frm}'
|
||||
pages = f'{from_}'
|
||||
stream.write(f'[OCR skipped on page(s) {pages}]')
|
||||
return output_file
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Plugin manager using pluggy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
@@ -11,7 +14,7 @@ import importlib.util
|
||||
import pkgutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Sequence, Tuple, Union
|
||||
from typing import Sequence
|
||||
|
||||
import pluggy
|
||||
|
||||
@@ -33,7 +36,7 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
plugins: List[Union[str, Path]],
|
||||
plugins: list[str | Path],
|
||||
builtins: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -100,23 +103,28 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
|
||||
self.register(module)
|
||||
|
||||
|
||||
def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
|
||||
pm = OcrmypdfPluginManager(
|
||||
def get_plugin_manager(plugins: list[str | Path], builtins=True):
|
||||
return OcrmypdfPluginManager(
|
||||
project_name='ocrmypdf',
|
||||
plugins=plugins,
|
||||
builtins=builtins,
|
||||
)
|
||||
return pm
|
||||
|
||||
|
||||
def get_parser_options_plugins(
|
||||
args: Sequence[str],
|
||||
) -> Tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]:
|
||||
) -> tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]:
|
||||
pre_options, _unused = plugins_only_parser.parse_known_args(args=args)
|
||||
plugin_manager = get_plugin_manager(pre_options.plugins)
|
||||
|
||||
parser = get_parser()
|
||||
plugin_manager.hook.initialize( # pylint: disable=no-member
|
||||
plugin_manager=plugin_manager
|
||||
)
|
||||
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
|
||||
|
||||
options = parser.parse_args(args=args)
|
||||
return parser, options, plugin_manager
|
||||
|
||||
|
||||
__all__ = ['OcrmypdfPluginManager', 'get_plugin_manager', 'get_parser_options_plugins']
|
||||
|
||||
+27
-15
@@ -4,6 +4,10 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Implements the concurrent and page synchronous parts of the pipeline."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
@@ -16,7 +20,7 @@ from concurrent.futures.thread import BrokenThreadPool
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from typing import List, NamedTuple, Optional, Tuple, cast
|
||||
from typing import NamedTuple, Sequence, cast
|
||||
|
||||
import PIL
|
||||
|
||||
@@ -68,11 +72,13 @@ from ocrmypdf.pdfa import file_claims_pdfa
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PageResult(NamedTuple): # pylint: disable=inherit-non-class
|
||||
class PageResult(NamedTuple):
|
||||
"""Result when a page is finished processing."""
|
||||
|
||||
pageno: int
|
||||
pdf_page_from_image: Optional[Path]
|
||||
ocr: Optional[Path]
|
||||
text: Optional[Path]
|
||||
pdf_page_from_image: Path | None
|
||||
ocr: Path | None
|
||||
text: Path | None
|
||||
orientation_correction: int
|
||||
|
||||
|
||||
@@ -111,7 +117,7 @@ def preprocess(
|
||||
|
||||
def make_intermediate_images(
|
||||
page_context: PageContext, orientation_correction: int
|
||||
) -> Tuple[Path, Optional[Path]]:
|
||||
) -> tuple[Path, Path | None]:
|
||||
options = page_context.options
|
||||
|
||||
ocr_image = preprocess_out = None
|
||||
@@ -226,7 +232,9 @@ def exec_page_sync(page_context: PageContext) -> PageResult:
|
||||
)
|
||||
|
||||
|
||||
def post_process(pdf_file: Path, context: PdfContext, executor: Executor) -> Path:
|
||||
def post_process(
|
||||
pdf_file: Path, context: PdfContext, executor: Executor
|
||||
) -> tuple[Path, Sequence[str]]:
|
||||
pdf_out = pdf_file
|
||||
if context.options.output_type.startswith('pdfa'):
|
||||
ps_stub_out = generate_postscript_stub(context)
|
||||
@@ -244,7 +252,7 @@ def worker_init(max_pixels: int) -> None:
|
||||
pikepdf_enable_mmap()
|
||||
|
||||
|
||||
def exec_concurrent(context: PdfContext, executor: Executor) -> None:
|
||||
def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||
"""Execute the pipeline concurrently"""
|
||||
|
||||
# Run exec_page_sync on every page context
|
||||
@@ -253,7 +261,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> None:
|
||||
if max_workers > 1:
|
||||
log.info("Start processing %d pages concurrently", max_workers)
|
||||
|
||||
sidecars: List[Optional[Path]] = [None] * len(context.pdfinfo)
|
||||
sidecars: list[Path | None] = [None] * len(context.pdfinfo)
|
||||
ocrgraft = OcrGrafter(context)
|
||||
|
||||
def update_page(result: PageResult, pbar):
|
||||
@@ -296,13 +304,15 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> None:
|
||||
# Merge layers to one single pdf
|
||||
pdf = ocrgraft.finalize()
|
||||
|
||||
messages: Sequence[str] = []
|
||||
if options.output_type != 'none':
|
||||
# PDF/A and metadata
|
||||
log.info("Postprocessing...")
|
||||
pdf = post_process(pdf, context, executor)
|
||||
pdf, messages = post_process(pdf, context, executor)
|
||||
|
||||
# Copy PDF file to destination
|
||||
copy_final(pdf, options.output_file, context)
|
||||
return messages
|
||||
|
||||
|
||||
def configure_debug_logging(
|
||||
@@ -329,7 +339,7 @@ def configure_debug_logging(
|
||||
def run_pipeline(
|
||||
options: argparse.Namespace,
|
||||
*,
|
||||
plugin_manager: Optional[OcrmypdfPluginManager],
|
||||
plugin_manager: OcrmypdfPluginManager | None,
|
||||
api: bool = False,
|
||||
) -> ExitCode:
|
||||
# Any changes to options will not take effect for options that are already
|
||||
@@ -382,7 +392,7 @@ def run_pipeline(
|
||||
validate_pdfinfo_options(context)
|
||||
|
||||
# Execute the pipeline
|
||||
exec_concurrent(context, executor)
|
||||
optimize_messages = exec_concurrent(context, executor)
|
||||
|
||||
if options.output_file == '-':
|
||||
log.info("Output sent to stdout")
|
||||
@@ -408,7 +418,9 @@ def run_pipeline(
|
||||
if not check_pdf(options.output_file):
|
||||
log.warning('Output file: The generated PDF is INVALID')
|
||||
return ExitCode.invalid_output_pdf
|
||||
report_output_file_size(options, start_input_file, options.output_file)
|
||||
report_output_file_size(
|
||||
options, start_input_file, options.output_file, optimize_messages
|
||||
)
|
||||
|
||||
except (KeyboardInterrupt if not api else NeverRaise):
|
||||
if options.verbose >= 1:
|
||||
@@ -425,7 +437,7 @@ def run_pipeline(
|
||||
else:
|
||||
log.error(type(e).__name__)
|
||||
return e.exit_code
|
||||
except (PIL.Image.DecompressionBombError if not api else NeverRaise) as e:
|
||||
except (PIL.Image.DecompressionBombError if not api else NeverRaise):
|
||||
log.exception(
|
||||
"A decompression bomb error was encountered while executing the "
|
||||
"pipeline. Use the argument --max-image-mpixels to raise the maximum "
|
||||
@@ -435,7 +447,7 @@ def run_pipeline(
|
||||
except (
|
||||
BrokenProcessPool if not api else NeverRaise,
|
||||
BrokenThreadPool if not api else NeverRaise,
|
||||
) as e:
|
||||
):
|
||||
log.exception(
|
||||
"A worker process was terminated unexpectedly. This is known to occur if "
|
||||
"processing your file takes all available swap space and RAM. It may "
|
||||
|
||||
+34
-67
@@ -5,6 +5,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Validate a work order from API or command line."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import locale
|
||||
import logging
|
||||
@@ -13,19 +16,19 @@ import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import List, Set, Tuple
|
||||
from typing import Sequence
|
||||
|
||||
import pikepdf
|
||||
import PIL
|
||||
|
||||
from ocrmypdf._exec import jbig2enc, pngquant, unpaper
|
||||
from ocrmypdf._exec import unpaper
|
||||
from ocrmypdf.exceptions import (
|
||||
BadArgsError,
|
||||
InputFileError,
|
||||
MissingDependencyError,
|
||||
OutputFileAccessError,
|
||||
)
|
||||
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink, samefile
|
||||
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
|
||||
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
|
||||
from ocrmypdf.subprocess import check_external_program
|
||||
|
||||
@@ -143,16 +146,16 @@ def check_options_preprocessing(options):
|
||||
raise BadArgsError("--unpaper-args: " + str(e)) from e
|
||||
|
||||
|
||||
def _pages_from_ranges(ranges: str) -> Set[int]:
|
||||
pages: List[int] = []
|
||||
def _pages_from_ranges(ranges: str) -> set[int]:
|
||||
pages: list[int] = []
|
||||
page_groups = ranges.replace(' ', '').split(',')
|
||||
for g in page_groups:
|
||||
if not g:
|
||||
for group in page_groups:
|
||||
if not group:
|
||||
continue
|
||||
try:
|
||||
start, end = g.split('-')
|
||||
start, end = group.split('-')
|
||||
except ValueError:
|
||||
pages.append(int(g) - 1)
|
||||
pages.append(int(group) - 1)
|
||||
else:
|
||||
try:
|
||||
new_pages = list(range(int(start) - 1, int(end)))
|
||||
@@ -162,7 +165,7 @@ def _pages_from_ranges(ranges: str) -> Set[int]:
|
||||
) from None
|
||||
pages.extend(new_pages)
|
||||
except ValueError:
|
||||
raise BadArgsError(f"invalid page subrange '{g}'") from None
|
||||
raise BadArgsError(f"invalid page subrange '{group}'") from None
|
||||
|
||||
if not pages:
|
||||
raise BadArgsError(
|
||||
@@ -193,37 +196,6 @@ def check_options_ocr_behavior(options):
|
||||
options.pages = _pages_from_ranges(options.pages)
|
||||
|
||||
|
||||
def check_options_optimizing(options):
|
||||
if options.optimize >= 2:
|
||||
check_external_program(
|
||||
program='pngquant',
|
||||
package='pngquant',
|
||||
version_checker=pngquant.version,
|
||||
need_version='2.0.1',
|
||||
required_for='--optimize {2,3}',
|
||||
)
|
||||
|
||||
if options.optimize >= 2:
|
||||
# Although we use JBIG2 for optimize=1, don't nag about it unless the
|
||||
# user is asking for more optimization
|
||||
check_external_program(
|
||||
program='jbig2',
|
||||
package='jbig2enc',
|
||||
version_checker=jbig2enc.version,
|
||||
need_version='0.28',
|
||||
required_for='--optimize {2,3} | --jbig2-lossy',
|
||||
recommended=True if not options.jbig2_lossy else False,
|
||||
)
|
||||
|
||||
if options.optimize == 0 and any(
|
||||
[options.jbig2_lossy, options.png_quality, options.jpeg_quality]
|
||||
):
|
||||
log.warning(
|
||||
"The arguments --jbig2-lossy, --png-quality, and --jpeg-quality "
|
||||
"will be ignored because --optimize=0."
|
||||
)
|
||||
|
||||
|
||||
def check_options_advanced(options):
|
||||
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
|
||||
'pdfa'
|
||||
@@ -237,13 +209,13 @@ def check_options_advanced(options):
|
||||
def check_options_metadata(options):
|
||||
docinfo = [options.title, options.author, options.keywords, options.subject]
|
||||
for s in (m for m in docinfo if m):
|
||||
for c in s:
|
||||
if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000:
|
||||
for char in s:
|
||||
if unicodedata.category(char) == 'Co' or ord(char) >= 0x10000:
|
||||
hexchar = hex(ord(char))[2:].upper()
|
||||
raise ValueError(
|
||||
"One of the metadata strings contains "
|
||||
"an unsupported Unicode character: '{}' (U+{})".format(
|
||||
c, hex(ord(c))[2:].upper()
|
||||
)
|
||||
"an unsupported Unicode character: "
|
||||
f"{char} (U+{hexchar})"
|
||||
)
|
||||
|
||||
|
||||
@@ -261,7 +233,6 @@ def _check_options(options, plugin_manager, ocr_engine_languages):
|
||||
check_options_sidecar(options)
|
||||
check_options_preprocessing(options)
|
||||
check_options_ocr_behavior(options)
|
||||
check_options_optimizing(options)
|
||||
check_options_advanced(options)
|
||||
check_options_pillow(options)
|
||||
plugin_manager.hook.check_options(options=options)
|
||||
@@ -272,7 +243,7 @@ def check_options(options, plugin_manager):
|
||||
_check_options(options, plugin_manager, ocr_engine_languages)
|
||||
|
||||
|
||||
def create_input_file(options, work_folder: Path) -> Tuple[Path, str]:
|
||||
def create_input_file(options, work_folder: Path) -> tuple[Path, str]:
|
||||
if options.input_file == '-':
|
||||
# stdin
|
||||
log.info('reading file from standard input')
|
||||
@@ -293,7 +264,7 @@ def create_input_file(options, work_folder: Path) -> Tuple[Path, str]:
|
||||
target = work_folder / 'origin'
|
||||
safe_symlink(options.input_file, target)
|
||||
return target, os.fspath(options.input_file)
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError as e:
|
||||
msg = f"File not found - {options.input_file}"
|
||||
if Path('/.dockerenv').exists(): # pragma: no cover
|
||||
msg += (
|
||||
@@ -304,7 +275,7 @@ def create_input_file(options, work_folder: Path) -> Tuple[Path, str]:
|
||||
"\n"
|
||||
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf\n"
|
||||
)
|
||||
raise InputFileError(msg)
|
||||
raise InputFileError(msg) from e
|
||||
|
||||
|
||||
def check_requested_output_file(options):
|
||||
@@ -324,7 +295,16 @@ def check_requested_output_file(options):
|
||||
)
|
||||
|
||||
|
||||
def report_output_file_size(options, input_file, output_file):
|
||||
def report_output_file_size(
|
||||
options,
|
||||
input_file: Path,
|
||||
output_file: Path,
|
||||
optimize_messages: Sequence[str] | None = None,
|
||||
file_overhead: int = 4000,
|
||||
page_overhead: int = 3000,
|
||||
):
|
||||
if optimize_messages is None:
|
||||
optimize_messages = []
|
||||
try:
|
||||
output_size = Path(output_file).stat().st_size
|
||||
input_size = Path(input_file).stat().st_size
|
||||
@@ -333,9 +313,7 @@ def report_output_file_size(options, input_file, output_file):
|
||||
with pikepdf.open(output_file) as p:
|
||||
# Overhead constants obtained by estimating amount of data added by OCR
|
||||
# PDF/A conversion, and possible XMP metadata addition, with compression
|
||||
FILE_OVERHEAD = 4000
|
||||
OCR_PER_PAGE_OVERHEAD = 3000
|
||||
reasonable_overhead = FILE_OVERHEAD + OCR_PER_PAGE_OVERHEAD * len(p.pages)
|
||||
reasonable_overhead = file_overhead + page_overhead * len(p.pages)
|
||||
ratio = output_size / input_size
|
||||
reasonable_ratio = output_size / (input_size + reasonable_overhead)
|
||||
if reasonable_ratio < 1.35 or input_size < 25000:
|
||||
@@ -355,19 +333,8 @@ def report_output_file_size(options, input_file, output_file):
|
||||
f"The argument --{arg.replace('_', '-')} was issued, causing transcoding."
|
||||
)
|
||||
|
||||
if options.optimize == 0:
|
||||
reasons.append("Optimization was disabled.")
|
||||
else:
|
||||
image_optimizers = {
|
||||
'jbig2': jbig2enc.available(),
|
||||
'pngquant': pngquant.available(),
|
||||
}
|
||||
for name, available in image_optimizers.items():
|
||||
if not available:
|
||||
reasons.append(
|
||||
f"The optional dependency '{name}' was not found, so some image "
|
||||
f"optimizations could not be attempted."
|
||||
)
|
||||
reasons.extend(optimize_messages)
|
||||
|
||||
if options.output_type.startswith('pdfa'):
|
||||
reasons.append("PDF/A conversion was enabled. (Try `--output-type pdf`.)")
|
||||
if options.plugins:
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Get version by introspecting package information.
|
||||
|
||||
OCRmyPDF uses setuptools_scm to derive version from git tags.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
from importlib_metadata import version as _package_version
|
||||
except ImportError:
|
||||
from importlib.metadata import version as _package_version
|
||||
except ImportError:
|
||||
from importlib_metadata import version as _package_version # type: ignore
|
||||
|
||||
PROGRAM_NAME = 'ocrmypdf'
|
||||
|
||||
|
||||
+18
-12
@@ -4,6 +4,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Functions for using ocrmypdf as an API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -12,7 +15,7 @@ import threading
|
||||
from enum import IntEnum
|
||||
from io import IOBase
|
||||
from pathlib import Path
|
||||
from typing import AnyStr, BinaryIO, Iterable, Optional, Union
|
||||
from typing import AnyStr, BinaryIO, Iterable, Union
|
||||
from warnings import warn
|
||||
|
||||
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
|
||||
@@ -25,7 +28,10 @@ from ocrmypdf.helpers import is_iterable_notstr
|
||||
try:
|
||||
import coloredlogs
|
||||
except ModuleNotFoundError:
|
||||
coloredlogs = None
|
||||
coloredlogs = None # pylint: disable=invalid-name
|
||||
|
||||
if coloredlogs:
|
||||
from humanfriendly.terminal import enable_ansi_support
|
||||
|
||||
|
||||
StrPath = Union[Path, AnyStr]
|
||||
@@ -37,6 +43,7 @@ _api_lock = threading.Lock()
|
||||
class Verbosity(IntEnum):
|
||||
"""Verbosity level for configure_logging."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
quiet = -1 #: Suppress most messages
|
||||
default = 0 #: Default level of logging
|
||||
debug = 1 #: Output ocrmypdf debug messages
|
||||
@@ -116,16 +123,15 @@ def configure_logging(
|
||||
fmt = '%(pageno)s%(message)s'
|
||||
|
||||
use_colors = progress_bar_friendly
|
||||
if not coloredlogs:
|
||||
use_colors = False
|
||||
if use_colors:
|
||||
if os.name == 'nt':
|
||||
use_colors = coloredlogs.enable_ansi_support()
|
||||
formatter = None
|
||||
if coloredlogs and use_colors:
|
||||
use_colors = enable_ansi_support()
|
||||
if use_colors:
|
||||
use_colors = coloredlogs.terminal_supports_colors()
|
||||
if use_colors:
|
||||
formatter = coloredlogs.ColoredFormatter(fmt=fmt)
|
||||
else:
|
||||
if use_colors:
|
||||
formatter = coloredlogs.ColoredFormatter(fmt=fmt)
|
||||
|
||||
if not formatter:
|
||||
formatter = logging.Formatter(fmt=fmt)
|
||||
|
||||
console.setFormatter(formatter)
|
||||
@@ -193,7 +199,7 @@ def create_options(
|
||||
else:
|
||||
cmdline.append(os.fspath(output_file))
|
||||
|
||||
parser._api_mode = True
|
||||
parser.enable_api_mode()
|
||||
options = parser.parse_args(cmdline)
|
||||
for keyword, val in deferred:
|
||||
setattr(options, keyword, val)
|
||||
@@ -213,7 +219,7 @@ def ocr( # pylint: disable=unused-argument
|
||||
language: Iterable[str] = None,
|
||||
image_dpi: int = None,
|
||||
output_type=None,
|
||||
sidecar: Optional[StrPath] = None,
|
||||
sidecar: StrPath | None = None,
|
||||
jobs: int = None,
|
||||
use_threads: bool = None,
|
||||
title: str = None,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# This file exists only mark builtin_plugins as a package.
|
||||
# The plugin manager will not load it, so anything defined here may not be
|
||||
# processed as a module.
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""OCRmyPDF's multiprocessing/multithreading abstraction layer."""
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
@@ -21,7 +24,6 @@ import sys
|
||||
import threading
|
||||
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
||||
from contextlib import suppress
|
||||
from multiprocessing.pool import Pool, ThreadPool
|
||||
from typing import Callable, Iterable, Type, Union
|
||||
|
||||
from tqdm import tqdm
|
||||
@@ -44,7 +46,8 @@ def log_listener(q: Queue):
|
||||
should actually write to sys.stderr or whatever we're using, so if this is
|
||||
made into a process the main application needs to be directed to it.
|
||||
|
||||
See https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
|
||||
See:
|
||||
https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes
|
||||
"""
|
||||
|
||||
while True:
|
||||
@@ -89,6 +92,8 @@ def process_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
||||
|
||||
|
||||
def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
||||
del q # unused but required argument
|
||||
del loglevel # unused but required argument
|
||||
# As a thread, block SIGBUS so the main thread deals with it...
|
||||
with suppress(AttributeError):
|
||||
signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGBUS})
|
||||
@@ -98,6 +103,17 @@ def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
||||
|
||||
|
||||
class StandardExecutor(Executor):
|
||||
"""Standard OCRmyPDF concurrent task executor."""
|
||||
|
||||
def _cancel_futures_kwargs(self):
|
||||
"""Shim older Pythons that do not have Executor.shutdown(...cancel_futures=).
|
||||
|
||||
Remove this code when support for Python 3.8 is dropped.
|
||||
"""
|
||||
if sys.version_info[:2] < (3, 9):
|
||||
return {}
|
||||
return dict(cancel_futures=True)
|
||||
|
||||
def _execute(
|
||||
self,
|
||||
*,
|
||||
@@ -136,7 +152,7 @@ class StandardExecutor(Executor):
|
||||
task_finished(result, pbar)
|
||||
except KeyboardInterrupt:
|
||||
# Terminate pool so we exit instantly
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
|
||||
raise
|
||||
except Exception:
|
||||
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
||||
@@ -145,7 +161,7 @@ class StandardExecutor(Executor):
|
||||
# results will be discard. But if the condition above is True,
|
||||
# then we are running in pytest, and we want everything to exit
|
||||
# as cleanly as possible so that we get good error messages.
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
|
||||
raise
|
||||
finally:
|
||||
# Terminate log listener
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""OCRmyPDF automatically installs these filters as plugins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Built-in plugin to implement PDF page rasterization and PDF/A production."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# © 2022 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Built-in plugin to implement PDF page optimization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
from ocrmypdf import Executor, PdfContext, hookimpl
|
||||
from ocrmypdf._exec import jbig2enc, pngquant
|
||||
from ocrmypdf._pipeline import get_pdf_save_settings
|
||||
from ocrmypdf.cli import numeric
|
||||
from ocrmypdf.optimize import optimize
|
||||
from ocrmypdf.subprocess import check_external_program
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
optimizing = parser.add_argument_group(
|
||||
"Optimization options", "Control how the PDF is optimized after OCR"
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'-O',
|
||||
'--optimize',
|
||||
type=int,
|
||||
choices=range(0, 4),
|
||||
default=1,
|
||||
help=(
|
||||
"Control how PDF is optimized after processing:"
|
||||
"0 - do not optimize; "
|
||||
"1 - do safe, lossless optimizations (default); "
|
||||
"2 - do lossy JPEG and JPEG2000 optimizations; "
|
||||
"3 - do more aggressive lossy JPEG and JPEG2000 optimizations. "
|
||||
"To enable lossy JBIG2, see --jbig2-lossy."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpeg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust JPEG quality level for JPEG optimization. "
|
||||
"100 is best quality and largest output size; "
|
||||
"1 is lowest quality and smallest output; "
|
||||
"0 uses the default."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
dest='jpeg_quality',
|
||||
help=argparse.SUPPRESS, # Alias for --jpeg-quality
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--png-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust PNG quality level to use when quantizing PNGs. "
|
||||
"Values have same meaning as with --jpeg-quality"
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-lossy',
|
||||
action='store_true',
|
||||
help=(
|
||||
"Enable JBIG2 lossy mode (better compression, not suitable for some "
|
||||
"use cases - see documentation). Only takes effect if --optimize 1 or "
|
||||
"higher is also enabled."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-page-group-size',
|
||||
type=numeric(int, 1, 10000),
|
||||
default=0,
|
||||
metavar='N',
|
||||
# Adjust number of pages to consider at once for JBIG2 compression
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
|
||||
@hookimpl
|
||||
def check_options(options):
|
||||
if options.optimize >= 2:
|
||||
check_external_program(
|
||||
program='pngquant',
|
||||
package='pngquant',
|
||||
version_checker=pngquant.version,
|
||||
need_version='2.0.1',
|
||||
required_for='--optimize {2,3}',
|
||||
)
|
||||
|
||||
if options.optimize >= 2:
|
||||
# Although we use JBIG2 for optimize=1, don't nag about it unless the
|
||||
# user is asking for more optimization
|
||||
check_external_program(
|
||||
program='jbig2',
|
||||
package='jbig2enc',
|
||||
version_checker=jbig2enc.version,
|
||||
need_version='0.28',
|
||||
required_for='--optimize {2,3} | --jbig2-lossy',
|
||||
recommended=True if not options.jbig2_lossy else False,
|
||||
)
|
||||
|
||||
if options.optimize == 0 and any(
|
||||
[options.jbig2_lossy, options.png_quality, options.jpeg_quality]
|
||||
):
|
||||
log.warning(
|
||||
"The arguments --jbig2-lossy, --png-quality, and --jpeg-quality "
|
||||
"will be ignored because --optimize=0."
|
||||
)
|
||||
|
||||
|
||||
@hookimpl
|
||||
def optimize_pdf(
|
||||
input_pdf: Path,
|
||||
output_pdf: Path,
|
||||
context: PdfContext,
|
||||
executor: Executor,
|
||||
linearize: bool,
|
||||
) -> tuple[Path, Sequence[str]]:
|
||||
save_settings = dict(
|
||||
linearize=linearize,
|
||||
**get_pdf_save_settings(context.options.output_type),
|
||||
)
|
||||
result_path = optimize(input_pdf, output_pdf, context, save_settings, executor)
|
||||
messages = []
|
||||
if context.options.optimize == 0:
|
||||
messages.append("Optimization was disabled.")
|
||||
else:
|
||||
image_optimizers = {
|
||||
'jbig2': jbig2enc.available(),
|
||||
'pngquant': pngquant.available(),
|
||||
}
|
||||
for name, available in image_optimizers.items():
|
||||
if not available:
|
||||
messages.append(
|
||||
f"The optional dependency '{name}' was not found, so some image "
|
||||
f"optimizations could not be attempted."
|
||||
)
|
||||
return result_path, messages
|
||||
|
||||
|
||||
@hookimpl
|
||||
def is_optimization_enabled(context: PdfContext) -> bool:
|
||||
return context.options.optimize != 0
|
||||
@@ -4,6 +4,10 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Built-in plugin to implement OCR using Tesseract."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -138,6 +142,8 @@ def validate(pdfinfo, options):
|
||||
|
||||
|
||||
class TesseractOcrEngine(OcrEngine):
|
||||
"""Implements OCR with Tesseract."""
|
||||
|
||||
@staticmethod
|
||||
def version():
|
||||
return tesseract.version()
|
||||
|
||||
+16
-71
@@ -4,9 +4,12 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Command line interface customization and validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Any, Callable, Mapping, Optional, TypeVar
|
||||
from typing import Any, Callable, Mapping, TypeVar
|
||||
|
||||
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
||||
from ocrmypdf._version import __version__ as _VERSION
|
||||
@@ -14,9 +17,7 @@ from ocrmypdf._version import __version__ as _VERSION
|
||||
T = TypeVar('T', int, float)
|
||||
|
||||
|
||||
def numeric(
|
||||
basetype: Callable[[Any], T], min_: Optional[T] = None, max_: Optional[T] = None
|
||||
):
|
||||
def numeric(basetype: Callable[[Any], T], min_: T | None = None, max_: T | None = None):
|
||||
"""Validator for numeric params"""
|
||||
min_ = basetype(min_) if min_ is not None else None
|
||||
max_ = basetype(max_) if max_ is not None else None
|
||||
@@ -42,7 +43,7 @@ def str_to_int(mapping: Mapping[str, int]):
|
||||
except KeyError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"{s!r} must be one of: {', '.join(mapping.keys())}"
|
||||
)
|
||||
) from None
|
||||
|
||||
return _str_to_int
|
||||
|
||||
@@ -51,12 +52,20 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
"""Override parser's default behavior of calling sys.exit()
|
||||
|
||||
https://stackoverflow.com/questions/5943249/python-argparse-and-controlling-overriding-the-exit-status-code
|
||||
|
||||
OCRmyPDF began as a CLI but eventually acquired an API. The API works inside out,
|
||||
by synthesizing a command line argument. So we subclass the standard parser with
|
||||
one that doesn't call sys.exit(). Obviously this is not the ideal way to do things
|
||||
but it works for us.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._api_mode = False
|
||||
|
||||
def enable_api_mode(self):
|
||||
self._api_mode = True
|
||||
|
||||
def error(self, message):
|
||||
if not self._api_mode:
|
||||
super().error(message)
|
||||
@@ -65,6 +74,8 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
|
||||
class LanguageSetAction(argparse.Action):
|
||||
"""Manages a list of languages."""
|
||||
|
||||
def __init__(self, option_strings, dest, default=None, **kwargs):
|
||||
if default is None:
|
||||
default = set()
|
||||
@@ -341,72 +352,6 @@ Online documentation is located at:
|
||||
"but include skipped pages in final output",
|
||||
)
|
||||
|
||||
optimizing = parser.add_argument_group(
|
||||
"Optimization options", "Control how the PDF is optimized after OCR"
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'-O',
|
||||
'--optimize',
|
||||
type=int,
|
||||
choices=range(0, 4),
|
||||
default=1,
|
||||
help=(
|
||||
"Control how PDF is optimized after processing:"
|
||||
"0 - do not optimize; "
|
||||
"1 - do safe, lossless optimizations (default); "
|
||||
"2 - do lossy JPEG and JPEG2000 optimizations; "
|
||||
"3 - do more aggressive lossy JPEG and JPEG2000 optimizations. "
|
||||
"To enable lossy JBIG2, see --jbig2-lossy."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpeg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust JPEG quality level for JPEG optimization. "
|
||||
"100 is best quality and largest output size; "
|
||||
"1 is lowest quality and smallest output; "
|
||||
"0 uses the default."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jpg-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
dest='jpeg_quality',
|
||||
help=argparse.SUPPRESS, # Alias for --jpeg-quality
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--png-quality',
|
||||
type=numeric(int, 0, 100),
|
||||
default=0,
|
||||
metavar='Q',
|
||||
help=(
|
||||
"Adjust PNG quality level to use when quantizing PNGs. "
|
||||
"Values have same meaning as with --jpeg-quality"
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-lossy',
|
||||
action='store_true',
|
||||
help=(
|
||||
"Enable JBIG2 lossy mode (better compression, not suitable for some "
|
||||
"use cases - see documentation). Only takes effect if --optimize 1 or "
|
||||
"higher is also enabled."
|
||||
),
|
||||
)
|
||||
optimizing.add_argument(
|
||||
'--jbig2-page-group-size',
|
||||
type=numeric(int, 1, 10000),
|
||||
default=0,
|
||||
metavar='N',
|
||||
# Adjust number of pages to consider at once for JBIG2 compression
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
advanced = parser.add_argument_group(
|
||||
"Advanced", "Advanced options to control OCRmyPDF"
|
||||
)
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
|
||||
|
||||
"""Data files used to generate certain PDFs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""OCRmyPDF's exceptions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
class ExitCode(IntEnum):
|
||||
"""OCRmyPDF's exit codes."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
ok = 0
|
||||
bad_args = 1
|
||||
input_file = 2
|
||||
@@ -26,6 +32,8 @@ class ExitCode(IntEnum):
|
||||
|
||||
|
||||
class ExitCodeException(Exception):
|
||||
"""An exception which should return an exit code with sys.exit()."""
|
||||
|
||||
exit_code = ExitCode.other_error
|
||||
message = ""
|
||||
|
||||
@@ -37,17 +45,24 @@ class ExitCodeException(Exception):
|
||||
|
||||
|
||||
class BadArgsError(ExitCodeException):
|
||||
"""Invalid arguments on the command line or API."""
|
||||
|
||||
exit_code = ExitCode.bad_args
|
||||
|
||||
|
||||
class PdfMergeFailedError(ExitCodeException):
|
||||
class PdfMergeFailedError(ExitCodeException): # deprecated
|
||||
"""An intermediate PDF can't be merged.
|
||||
|
||||
No longer in use.
|
||||
"""
|
||||
|
||||
exit_code = ExitCode.input_file
|
||||
message = dedent(
|
||||
'''\
|
||||
Failed to merge PDF image layer with OCR layer
|
||||
|
||||
Usually this happens because the input PDF file is malformed and
|
||||
ocrmypdf cannot automatically correct the problem on its own.
|
||||
ocrmypdf cannot correct the problem on its own.
|
||||
|
||||
Try using
|
||||
ocrmypdf --pdf-renderer sandwich [..other args..]
|
||||
@@ -56,34 +71,50 @@ class PdfMergeFailedError(ExitCodeException):
|
||||
|
||||
|
||||
class MissingDependencyError(ExitCodeException):
|
||||
"""A third-party dependency is missing."""
|
||||
|
||||
exit_code = ExitCode.missing_dependency
|
||||
|
||||
|
||||
class UnsupportedImageFormatError(ExitCodeException):
|
||||
"""The image format is not supported."""
|
||||
|
||||
exit_code = ExitCode.input_file
|
||||
|
||||
|
||||
class DpiError(ExitCodeException):
|
||||
"""Missing information about input image DPI."""
|
||||
|
||||
exit_code = ExitCode.input_file
|
||||
|
||||
|
||||
class OutputFileAccessError(ExitCodeException):
|
||||
"""Cannot access the intended output file path."""
|
||||
|
||||
exit_code = ExitCode.file_access_error
|
||||
|
||||
|
||||
class PriorOcrFoundError(ExitCodeException):
|
||||
"""This file already has OCR."""
|
||||
|
||||
exit_code = ExitCode.already_done_ocr
|
||||
|
||||
|
||||
class InputFileError(ExitCodeException):
|
||||
"""Something is wrong with the input file."""
|
||||
|
||||
exit_code = ExitCode.input_file
|
||||
|
||||
|
||||
class SubprocessOutputError(ExitCodeException):
|
||||
"""A subprocess returned an unexpected error."""
|
||||
|
||||
exit_code = ExitCode.child_process_error
|
||||
|
||||
|
||||
class EncryptedPdfError(ExitCodeException):
|
||||
"""Input PDF is encrypted."""
|
||||
|
||||
exit_code = ExitCode.encrypted_pdf
|
||||
message = dedent(
|
||||
'''\
|
||||
@@ -100,5 +131,7 @@ class EncryptedPdfError(ExitCodeException):
|
||||
|
||||
|
||||
class TesseractConfigError(ExitCodeException):
|
||||
"""Tesseract config can't be parsed."""
|
||||
|
||||
exit_code = ExitCode.invalid_config
|
||||
message = "Error occurred while parsing a Tesseract configuration file"
|
||||
|
||||
@@ -20,6 +20,8 @@ be guaranteed, some workers may end up with too much work while others are idle.
|
||||
It is less efficient than the standard implementation, so not th edefault.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import signal
|
||||
@@ -28,7 +30,7 @@ from enum import Enum, auto
|
||||
from itertools import islice, repeat, takewhile, zip_longest
|
||||
from multiprocessing import Pipe, Process
|
||||
from multiprocessing.connection import Connection, wait
|
||||
from typing import Callable, Iterable, Iterator, List
|
||||
from typing import Callable, Iterable, Iterator
|
||||
|
||||
from ocrmypdf import Executor, hookimpl
|
||||
from ocrmypdf._concurrent import NullProgressBar
|
||||
@@ -37,9 +39,11 @@ from ocrmypdf.helpers import remove_all_log_handlers
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
exception = auto()
|
||||
result = auto()
|
||||
complete = auto()
|
||||
"""Implement basic IPC messaging."""
|
||||
|
||||
exception = auto() # pylint: disable=invalid-name
|
||||
result = auto() # pylint: disable=invalid-name
|
||||
complete = auto() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def split_every(n: int, iterable: Iterable) -> Iterator:
|
||||
@@ -59,6 +63,8 @@ def process_sigbus(*args):
|
||||
|
||||
|
||||
class ConnectionLogHandler(logging.handlers.QueueHandler):
|
||||
"""Handler used by child processes to forward log messages to parent."""
|
||||
|
||||
def __init__(self, conn: Connection) -> None:
|
||||
# sets the parent's queue to None - parent only touches queue
|
||||
# in enqueue() which we override
|
||||
@@ -91,7 +97,7 @@ def process_loop(
|
||||
for args in task_args:
|
||||
try:
|
||||
result = task(args)
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
conn.send((MessageType.exception, e))
|
||||
break
|
||||
else:
|
||||
@@ -103,6 +109,8 @@ def process_loop(
|
||||
|
||||
|
||||
class LambdaExecutor(Executor):
|
||||
"""Executor for AWS Lambda or similar environments that lack semaphores."""
|
||||
|
||||
def _execute(
|
||||
self,
|
||||
*,
|
||||
@@ -128,8 +136,8 @@ class LambdaExecutor(Executor):
|
||||
if not grouped_args:
|
||||
return
|
||||
|
||||
processes: List[Process] = []
|
||||
connections: List[Connection] = []
|
||||
processes: list[Process] = []
|
||||
connections: list[Connection] = []
|
||||
for chunk in grouped_args:
|
||||
parent_conn, child_conn = Pipe()
|
||||
|
||||
@@ -153,13 +161,13 @@ class LambdaExecutor(Executor):
|
||||
|
||||
with self.pbar_class(**tqdm_kwargs) as pbar:
|
||||
while connections:
|
||||
for r in wait(connections):
|
||||
if not isinstance(r, Connection):
|
||||
for result in wait(connections):
|
||||
if not isinstance(result, Connection):
|
||||
raise NotImplementedError("We only support Connection()")
|
||||
try:
|
||||
msg_type, msg = r.recv()
|
||||
msg_type, msg = result.recv()
|
||||
except EOFError:
|
||||
connections.remove(r)
|
||||
connections.remove(result)
|
||||
continue
|
||||
|
||||
if msg_type == MessageType.result:
|
||||
@@ -170,7 +178,7 @@ class LambdaExecutor(Executor):
|
||||
logger = logging.getLogger(record.name)
|
||||
logger.handle(record)
|
||||
elif msg_type == MessageType.complete:
|
||||
connections.remove(r)
|
||||
connections.remove(result)
|
||||
elif msg_type == MessageType.exception:
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
|
||||
+18
-15
@@ -4,6 +4,9 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Support functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
@@ -137,11 +140,11 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
|
||||
os.symlink(os.path.abspath(input_file), soft_link_name)
|
||||
|
||||
|
||||
def samefile(f1: os.PathLike, f2: os.PathLike):
|
||||
def samefile(file1: os.PathLike, file2: os.PathLike):
|
||||
if os.name == 'nt':
|
||||
return f1 == f2
|
||||
return file1 == file2
|
||||
else:
|
||||
return os.path.samefile(f1, f2)
|
||||
return os.path.samefile(file1, file2)
|
||||
|
||||
|
||||
def is_iterable_notstr(thing: Any) -> bool:
|
||||
@@ -149,9 +152,9 @@ def is_iterable_notstr(thing: Any) -> bool:
|
||||
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
||||
|
||||
|
||||
def monotonic(L: Sequence) -> bool:
|
||||
def monotonic(seq: Sequence) -> bool:
|
||||
"""Does this sequence increase monotonically?"""
|
||||
return all(b > a for a, b in zip(L, L[1:]))
|
||||
return all(b > a for a, b in zip(seq, seq[1:]))
|
||||
|
||||
|
||||
def page_number(input_file: os.PathLike) -> int:
|
||||
@@ -166,7 +169,7 @@ def available_cpu_count() -> int:
|
||||
except NotImplementedError:
|
||||
pass
|
||||
warnings.warn(
|
||||
"Could not get CPU count. Assuming one (1) CPU." "Use -j N to set manually."
|
||||
"Could not get CPU count. Assuming one (1) CPU. Use -j N to set manually."
|
||||
)
|
||||
return 1
|
||||
|
||||
@@ -190,16 +193,16 @@ def is_file_writable(test_file: os.PathLike) -> bool:
|
||||
os.W_OK,
|
||||
effective_ids=(os.access in os.supports_effective_ids),
|
||||
)
|
||||
|
||||
try:
|
||||
fp = p.open('wb')
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
fp = p.open('wb')
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
fp.close()
|
||||
with suppress(OSError):
|
||||
p.unlink()
|
||||
return True
|
||||
fp.close()
|
||||
with suppress(OSError):
|
||||
p.unlink()
|
||||
return True
|
||||
except (OSError, RuntimeError) as e:
|
||||
log.debug(e)
|
||||
log.error(str(e))
|
||||
|
||||
@@ -28,17 +28,26 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""Transform .hocr and page image to text PDF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from math import atan, cos, sin
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple, Optional, Tuple, Union
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from reportlab.lib.colors import black, cyan, magenta, red
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
'ignore', category=DeprecationWarning, message=r".*load_module.*"
|
||||
)
|
||||
from reportlab.lib.colors import black, cyan, magenta, red
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
|
||||
# According to Wikipedia these languages are supported in the ISO-8859-1 character
|
||||
# set, meaning reportlab can generate them and they are compatible with hocr,
|
||||
@@ -99,7 +108,7 @@ HOCR_OK_LANGS = frozenset(
|
||||
Element = ElementTree.Element
|
||||
|
||||
|
||||
class Rect(NamedTuple): # pylint: disable=inherit-non-class
|
||||
class Rect(NamedTuple):
|
||||
"""A rectangle for managing PDF coordinates."""
|
||||
|
||||
x1: Any
|
||||
@@ -109,7 +118,7 @@ class Rect(NamedTuple): # pylint: disable=inherit-non-class
|
||||
|
||||
|
||||
class HocrTransformError(Exception):
|
||||
pass
|
||||
"""Error while applying hOCR transform."""
|
||||
|
||||
|
||||
class HocrTransform:
|
||||
@@ -132,7 +141,7 @@ class HocrTransform:
|
||||
{'ff': 'ff', 'ffi': 'ffi', 'ffl': 'ffl', 'fi': 'fi', 'fl': 'fl'}
|
||||
)
|
||||
|
||||
def __init__(self, *, hocr_filename: Union[str, Path], dpi: float):
|
||||
def __init__(self, *, hocr_filename: str | Path, dpi: float):
|
||||
self.dpi = dpi
|
||||
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
|
||||
|
||||
@@ -196,7 +205,7 @@ class HocrTransform:
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def baseline(cls, element: Element) -> Tuple[float, float]:
|
||||
def baseline(cls, element: Element) -> tuple[float, float]:
|
||||
"""
|
||||
Returns a tuple containing the baseline slope and intercept.
|
||||
"""
|
||||
@@ -212,7 +221,7 @@ class HocrTransform:
|
||||
"""
|
||||
return Rect._make((c / self.dpi * inch) for c in pxl)
|
||||
|
||||
def _child_xpath(self, html_tag: str, html_class: Optional[str] = None) -> str:
|
||||
def _child_xpath(self, html_tag: str, html_class: str | None = None) -> str:
|
||||
xpath = f".//{self.xmlns}{html_tag}"
|
||||
if html_class:
|
||||
xpath += f"[@class='{html_class}']"
|
||||
@@ -239,7 +248,7 @@ class HocrTransform:
|
||||
self,
|
||||
*,
|
||||
out_filename: Path,
|
||||
image_filename: Optional[Path] = None,
|
||||
image_filename: Path | None = None,
|
||||
show_bounding_boxes: bool = False,
|
||||
fontname: str = "Helvetica",
|
||||
invisible_text: bool = False,
|
||||
@@ -287,7 +296,7 @@ class HocrTransform:
|
||||
continue
|
||||
|
||||
pxl_coords = self.element_coordinates(elem)
|
||||
pt = self.pt_from_pixel(pxl_coords)
|
||||
pt = self.pt_from_pixel(pxl_coords) # pylint: disable=invalid-name
|
||||
|
||||
# draw the bbox border
|
||||
if show_bounding_boxes: # pragma: no cover
|
||||
@@ -342,7 +351,7 @@ class HocrTransform:
|
||||
def _do_line(
|
||||
self,
|
||||
pdf: Canvas,
|
||||
line: Optional[Element],
|
||||
line: Element | None,
|
||||
elemclass: str,
|
||||
fontname: str,
|
||||
invisible_text: bool,
|
||||
|
||||
+34
-39
@@ -4,6 +4,10 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""Post-processing image optimization of OCR PDFs."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
@@ -12,18 +16,7 @@ import threading
|
||||
from collections import defaultdict
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
MutableSet,
|
||||
NamedTuple,
|
||||
NewType,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
from typing import Callable, Iterator, MutableSet, NamedTuple, NewType, Sequence
|
||||
from zlib import compress
|
||||
|
||||
import img2pdf
|
||||
@@ -55,7 +48,9 @@ DEFAULT_PNG_QUALITY = 70
|
||||
Xref = NewType('Xref', int)
|
||||
|
||||
|
||||
class XrefExt(NamedTuple): # pylint: disable=inherit-non-class
|
||||
class XrefExt(NamedTuple):
|
||||
"""A PDF xref and image extension pair."""
|
||||
|
||||
xref: Xref
|
||||
ext: str
|
||||
|
||||
@@ -74,7 +69,7 @@ def jpg_name(root: Path, xref: Xref) -> Path:
|
||||
|
||||
def extract_image_filter(
|
||||
pike: Pdf, root: Path, image: Stream, xref: Xref
|
||||
) -> Optional[Tuple[PdfImage, Tuple[Name, Object]]]:
|
||||
) -> tuple[PdfImage, tuple[Name, Object]] | None:
|
||||
del pike # unused args
|
||||
del root
|
||||
|
||||
@@ -131,7 +126,7 @@ def extract_image_filter(
|
||||
|
||||
def extract_image_jbig2(
|
||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||
) -> Optional[XrefExt]:
|
||||
) -> XrefExt | None:
|
||||
del options # unused arg
|
||||
|
||||
result = extract_image_filter(pike, root, image, xref)
|
||||
@@ -172,7 +167,7 @@ def extract_image_jbig2(
|
||||
|
||||
def extract_image_generic(
|
||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||
) -> Optional[XrefExt]:
|
||||
) -> XrefExt | None:
|
||||
result = extract_image_filter(pike, root, image, xref)
|
||||
if result is None:
|
||||
return None
|
||||
@@ -236,8 +231,8 @@ def extract_images(
|
||||
pike: Pdf,
|
||||
root: Path,
|
||||
options,
|
||||
extract_fn: Callable[..., Optional[XrefExt]],
|
||||
) -> Iterator[Tuple[int, XrefExt]]:
|
||||
extract_fn: Callable[..., XrefExt | None],
|
||||
) -> Iterator[tuple[int, XrefExt]]:
|
||||
"""Extract image using extract_fn
|
||||
|
||||
Enumerate images on each page, lookup their xref/ID number in the PDF.
|
||||
@@ -266,7 +261,7 @@ def extract_images(
|
||||
if image.objgen[1] != 0:
|
||||
continue # Ignore images in an incremental PDF
|
||||
xref = Xref(image.objgen[0])
|
||||
if hasattr(image, 'SMask'):
|
||||
if Name.SMask in image:
|
||||
# Ignore soft masks
|
||||
smask_xref = Xref(image.SMask.objgen[0])
|
||||
exclude_xrefs.add(smask_xref)
|
||||
@@ -296,7 +291,7 @@ def extract_images(
|
||||
|
||||
def extract_images_generic(
|
||||
pike: Pdf, root: Path, options
|
||||
) -> Tuple[List[Xref], List[Xref]]:
|
||||
) -> tuple[list[Xref], list[Xref]]:
|
||||
"""Extract any >=2bpp image we think we can improve"""
|
||||
|
||||
jpegs = []
|
||||
@@ -311,7 +306,7 @@ def extract_images_generic(
|
||||
return jpegs, pngs
|
||||
|
||||
|
||||
def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefExt]]:
|
||||
def extract_images_jbig2(pike: Pdf, root: Path, options) -> dict[int, list[XrefExt]]:
|
||||
"""Extract any bitonal image that we think we can improve as JBIG2"""
|
||||
|
||||
jbig2_groups = defaultdict(list)
|
||||
@@ -324,11 +319,11 @@ def extract_images_jbig2(pike: Pdf, root: Path, options) -> Dict[int, List[XrefE
|
||||
|
||||
|
||||
def _produce_jbig2_images(
|
||||
jbig2_groups: Dict[int, List[XrefExt]], root: Path, options, executor: Executor
|
||||
jbig2_groups: dict[int, list[XrefExt]], root: Path, options, executor: Executor
|
||||
) -> None:
|
||||
"""Produce JBIG2 images from their groups"""
|
||||
|
||||
def jbig2_group_args(root: Path, groups: Dict[int, List[XrefExt]]):
|
||||
def jbig2_group_args(root: Path, groups: dict[int, list[XrefExt]]):
|
||||
for group, xref_exts in groups.items():
|
||||
prefix = f'group{group:08d}'
|
||||
yield (
|
||||
@@ -337,7 +332,7 @@ def _produce_jbig2_images(
|
||||
prefix, # =out_prefix
|
||||
)
|
||||
|
||||
def jbig2_single_args(root, groups: Dict[int, List[XrefExt]]):
|
||||
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
|
||||
for group, xref_exts in groups.items():
|
||||
prefix = f'group{group:08d}'
|
||||
# Second loop is to ensure multiple images per page are unpacked
|
||||
@@ -372,7 +367,7 @@ def _produce_jbig2_images(
|
||||
|
||||
def convert_to_jbig2(
|
||||
pike: Pdf,
|
||||
jbig2_groups: Dict[int, List[XrefExt]],
|
||||
jbig2_groups: dict[int, list[XrefExt]],
|
||||
root: Path,
|
||||
options,
|
||||
executor: Executor,
|
||||
@@ -389,7 +384,7 @@ def convert_to_jbig2(
|
||||
When the JBIG2 symbolic coder is not used, each JBIG2 stands on its own
|
||||
and needs no dictionary. Currently this must be lossless JBIG2.
|
||||
"""
|
||||
jbig2_globals_dict: Optional[Dictionary]
|
||||
jbig2_globals_dict: Dictionary | None
|
||||
|
||||
_produce_jbig2_images(jbig2_groups, root, options, executor)
|
||||
|
||||
@@ -415,7 +410,7 @@ def convert_to_jbig2(
|
||||
)
|
||||
|
||||
|
||||
def _optimize_jpeg(args: Tuple[Xref, Path, Path, int]) -> Tuple[Xref, Optional[Path]]:
|
||||
def _optimize_jpeg(args: tuple[Xref, Path, Path, int]) -> tuple[Xref, Path | None]:
|
||||
xref, in_jpg, opt_jpg, jpeg_quality = args
|
||||
|
||||
with Image.open(in_jpg) as im:
|
||||
@@ -431,13 +426,13 @@ def _optimize_jpeg(args: Tuple[Xref, Path, Path, int]) -> Tuple[Xref, Optional[P
|
||||
def transcode_jpegs(
|
||||
pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor
|
||||
) -> None:
|
||||
def jpeg_args() -> Iterator[Tuple[Xref, Path, Path, int]]:
|
||||
def jpeg_args() -> Iterator[tuple[Xref, Path, Path, int]]:
|
||||
for xref in jpegs:
|
||||
in_jpg = jpg_name(root, xref)
|
||||
opt_jpg = in_jpg.with_suffix('.opt.jpg')
|
||||
yield xref, in_jpg, opt_jpg, options.jpeg_quality
|
||||
|
||||
def finish_jpeg(result: Tuple[Xref, Optional[Path]], pbar):
|
||||
def finish_jpeg(result: tuple[Xref, Path | None], pbar):
|
||||
xref, opt_jpg = result
|
||||
if opt_jpg:
|
||||
compdata = opt_jpg.read_bytes() # JPEG can inserted into PDF as is
|
||||
@@ -462,11 +457,11 @@ def transcode_jpegs(
|
||||
|
||||
def _find_deflatable_jpeg(
|
||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||
) -> Optional[XrefExt]:
|
||||
) -> XrefExt | None:
|
||||
result = extract_image_filter(pike, root, image, xref)
|
||||
if result is None:
|
||||
return None
|
||||
pim, filtdp = result
|
||||
_pim, filtdp = result
|
||||
|
||||
if filtdp[0] == Name.DCTDecode and not filtdp[1] and options.optimize >= 1:
|
||||
return XrefExt(xref, '.memory')
|
||||
@@ -474,7 +469,7 @@ def _find_deflatable_jpeg(
|
||||
return None
|
||||
|
||||
|
||||
def _deflate_jpeg(args: Tuple[Pdf, threading.Lock, Xref, int]) -> Tuple[Xref, bytes]:
|
||||
def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, bytes]:
|
||||
pike, lock, xref, complevel = args
|
||||
with lock:
|
||||
xobj = pike.get_object(xref, 0)
|
||||
@@ -621,11 +616,11 @@ def optimize(
|
||||
context,
|
||||
save_settings,
|
||||
executor: Executor = DEFAULT_EXECUTOR,
|
||||
) -> None:
|
||||
) -> Path:
|
||||
options = context.options
|
||||
if options.optimize == 0:
|
||||
safe_symlink(input_file, output_file)
|
||||
return
|
||||
return output_file
|
||||
|
||||
if options.jpeg_quality == 0:
|
||||
options.jpeg_quality = DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
|
||||
@@ -660,9 +655,7 @@ def optimize(
|
||||
f"Output file not created after optimizing. We probably ran "
|
||||
f"out of disk space in the temporary folder: {tempfile.gettempdir()}."
|
||||
)
|
||||
ratio = input_size / output_size
|
||||
savings = 1 - output_size / input_size
|
||||
log.info(f"Optimize ratio: {ratio:.2f} savings: {(savings):.1%}")
|
||||
|
||||
if savings < 0:
|
||||
log.info(
|
||||
@@ -676,6 +669,8 @@ def optimize(
|
||||
else:
|
||||
safe_symlink(target_file, output_file)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def main(infile, outfile, level, jobs=1):
|
||||
from shutil import copy # pylint: disable=import-outside-toplevel
|
||||
@@ -707,9 +702,9 @@ def main(infile, outfile, level, jobs=1):
|
||||
jb2lossy=False,
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as td:
|
||||
context = PdfContext(options, td, infile, None, None)
|
||||
tmpout = Path(td) / 'out.pdf'
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
context = PdfContext(options, tmpdir, infile, None, None)
|
||||
tmpout = Path(tmpdir) / 'out.pdf'
|
||||
optimize(
|
||||
infile,
|
||||
tmpout,
|
||||
|
||||
+11
-8
@@ -9,14 +9,16 @@
|
||||
Utilities for PDF/A production and confirmation with Ghostspcript.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, Union
|
||||
from typing import Iterator
|
||||
|
||||
try:
|
||||
from importlib_resources import files as package_files
|
||||
except ImportError:
|
||||
from importlib.resources import files as package_files
|
||||
except ImportError:
|
||||
from importlib_resources import files as package_files # type: ignore
|
||||
|
||||
import pikepdf
|
||||
|
||||
@@ -25,7 +27,7 @@ SRGB_ICC_PROFILE_NAME = 'sRGB.icc'
|
||||
|
||||
def _postscript_objdef(
|
||||
alias: str,
|
||||
dictionary: Dict[str, str],
|
||||
dictionary: dict[str, str],
|
||||
*,
|
||||
stream_name: str = None,
|
||||
stream_data: bytes = None,
|
||||
@@ -97,7 +99,8 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
|
||||
target_filename: filename to save
|
||||
icc: ICC identifier such as 'sRGB'
|
||||
References:
|
||||
Adobe PDFMARK Reference: https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
||||
Adobe PDFMARK Reference:
|
||||
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
||||
"""
|
||||
if icc != 'sRGB':
|
||||
raise NotImplementedError("Only supporting sRGB")
|
||||
@@ -105,11 +108,11 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
|
||||
bytes_icc_profile = (
|
||||
package_files('ocrmypdf.data') / SRGB_ICC_PROFILE_NAME
|
||||
).read_bytes()
|
||||
ps = '\n'.join(_make_postscript(icc, bytes_icc_profile, 3))
|
||||
postscript = '\n'.join(_make_postscript(icc, bytes_icc_profile, 3))
|
||||
|
||||
# We should have encoded everything to pure ASCII by this point, and
|
||||
# to be safe, only allow ASCII in PostScript
|
||||
Path(target_filename).write_text(ps, encoding='ascii')
|
||||
Path(target_filename).write_text(postscript, encoding='ascii')
|
||||
return target_filename
|
||||
|
||||
|
||||
@@ -130,7 +133,7 @@ def file_claims_pdfa(filename: Path):
|
||||
}
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||
pdfa_dict: Dict[str, Union[str, bool]] = {}
|
||||
pdfa_dict: dict[str, str | bool] = {}
|
||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
|
||||
@@ -6,4 +6,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""For extracting information about PDFs prior to OCR."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ocrmypdf.pdfinfo.info import Colorspace, Encoding, PdfInfo
|
||||
|
||||
@@ -6,28 +6,30 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Extract information about the content of a PDF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from contextlib import ExitStack
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from enum import Enum, auto
|
||||
from functools import partial
|
||||
from math import hypot, inf, isclose
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Container,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
from warnings import warn
|
||||
|
||||
@@ -48,13 +50,41 @@ from ocrmypdf.pdfinfo.layout import get_page_analysis, get_text_boxes
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
|
||||
|
||||
Encoding = Enum(
|
||||
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate runlength'
|
||||
)
|
||||
class Colorspace(Enum):
|
||||
"""Description of common image colorspaces in a PDF."""
|
||||
|
||||
FRIENDLY_COLORSPACE: Dict[str, Colorspace] = {
|
||||
# pylint: disable=invalid-name
|
||||
gray = auto()
|
||||
rgb = auto()
|
||||
cmyk = auto()
|
||||
lab = auto()
|
||||
icc = auto()
|
||||
index = auto()
|
||||
sep = auto()
|
||||
devn = auto()
|
||||
pattern = auto()
|
||||
jpeg2000 = auto()
|
||||
|
||||
|
||||
class Encoding(Enum):
|
||||
"""Description of common image encodings in a PDF."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
ccitt = auto()
|
||||
jpeg = auto()
|
||||
jpeg2000 = auto()
|
||||
jbig2 = auto()
|
||||
asciihex = auto()
|
||||
ascii85 = auto()
|
||||
lzw = auto()
|
||||
flate = auto()
|
||||
runlength = auto()
|
||||
|
||||
|
||||
FloatRect = Tuple[float, float, float, float]
|
||||
|
||||
FRIENDLY_COLORSPACE: dict[str, Colorspace] = {
|
||||
'/DeviceGray': Colorspace.gray,
|
||||
'/CalGray': Colorspace.gray,
|
||||
'/DeviceRGB': Colorspace.rgb,
|
||||
@@ -72,7 +102,7 @@ FRIENDLY_COLORSPACE: Dict[str, Colorspace] = {
|
||||
'/I': Colorspace.index,
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING: Dict[str, Encoding] = {
|
||||
FRIENDLY_ENCODING: dict[str, Encoding] = {
|
||||
'/CCITTFaxDecode': Encoding.ccitt,
|
||||
'/DCTDecode': Encoding.jpeg,
|
||||
'/JPXDecode': Encoding.jpeg2000,
|
||||
@@ -86,7 +116,7 @@ FRIENDLY_ENCODING: Dict[str, Encoding] = {
|
||||
'/RL': Encoding.runlength,
|
||||
}
|
||||
|
||||
FRIENDLY_COMP: Dict[Colorspace, int] = {
|
||||
FRIENDLY_COMP: dict[Colorspace, int] = {
|
||||
Colorspace.gray: 1,
|
||||
Colorspace.rgb: 3,
|
||||
Colorspace.cmyk: 4,
|
||||
@@ -105,37 +135,45 @@ def _is_unit_square(shorthand):
|
||||
|
||||
|
||||
class XobjectSettings(NamedTuple):
|
||||
"""Info about an XObject found in a PDF."""
|
||||
|
||||
name: str
|
||||
shorthand: Tuple[float, float, float, float, float, float]
|
||||
shorthand: tuple[float, float, float, float, float, float]
|
||||
stack_depth: int
|
||||
|
||||
|
||||
class InlineSettings(NamedTuple):
|
||||
"""Info about an inline image found in a PDF."""
|
||||
|
||||
iimage: PdfInlineImage
|
||||
shorthand: Tuple[float, float, float, float, float, float]
|
||||
shorthand: tuple[float, float, float, float, float, float]
|
||||
stack_depth: int
|
||||
|
||||
|
||||
class ContentsInfo(NamedTuple):
|
||||
xobject_settings: List[XobjectSettings]
|
||||
inline_images: List[InlineSettings]
|
||||
"""Info about various objects found in a PDF."""
|
||||
|
||||
xobject_settings: list[XobjectSettings]
|
||||
inline_images: list[InlineSettings]
|
||||
found_vector: bool
|
||||
found_text: bool
|
||||
name_index: Mapping[str, List[XobjectSettings]]
|
||||
name_index: Mapping[str, list[XobjectSettings]]
|
||||
|
||||
|
||||
class TextboxInfo(NamedTuple):
|
||||
bbox: Tuple[float, float, float, float]
|
||||
"""Info about a text box found in a PDF."""
|
||||
|
||||
bbox: tuple[float, float, float, float]
|
||||
is_visible: bool
|
||||
is_corrupt: bool
|
||||
|
||||
|
||||
class VectorMarker:
|
||||
pass
|
||||
"""Sentinel indicating vector drawing operations were found on a page."""
|
||||
|
||||
|
||||
class TextMarker:
|
||||
pass
|
||||
"""Sentinel indicating text drawing operations were found on a page."""
|
||||
|
||||
|
||||
def _normalize_stack(graphobjs):
|
||||
@@ -178,8 +216,8 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
||||
|
||||
stack = []
|
||||
ctm = PdfMatrix(initial_shorthand)
|
||||
xobject_settings: List[XobjectSettings] = []
|
||||
inline_images: List[InlineSettings] = []
|
||||
xobject_settings: list[XobjectSettings] = []
|
||||
inline_images: list[InlineSettings] = []
|
||||
name_index = defaultdict(lambda: [])
|
||||
found_vector = False
|
||||
found_text = False
|
||||
@@ -197,7 +235,7 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
||||
if len(stack) > 32: # See docstring
|
||||
if len(stack) > 128:
|
||||
raise RuntimeError(
|
||||
"PDF graphics stack overflowed hard limit, operator %i" % n
|
||||
f"PDF graphics stack overflowed hard limit at operator {n}"
|
||||
)
|
||||
warn("PDF graphics stack overflowed spec limit")
|
||||
elif operator == 'Q':
|
||||
@@ -283,7 +321,7 @@ def _get_dpi(ctm_shorthand, image_size) -> Resolution:
|
||||
|
||||
"""
|
||||
|
||||
a, b, c, d, _, _ = ctm_shorthand
|
||||
a, b, c, d, _, _ = ctm_shorthand # pylint: disable=invalid-name
|
||||
|
||||
# Calculate the width and height of the image in PDF units
|
||||
image_drawn = hypot(a, b), hypot(c, d)
|
||||
@@ -299,23 +337,25 @@ def _get_dpi(ctm_shorthand, image_size) -> Resolution:
|
||||
|
||||
|
||||
class ImageInfo:
|
||||
"""Information about an image found in a PDF."""
|
||||
|
||||
DPI_PREC = Decimal('1.000')
|
||||
|
||||
_comp: Optional[int]
|
||||
_comp: int | None
|
||||
_name: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name='',
|
||||
pdfimage: Optional[Object] = None,
|
||||
inline: Optional[PdfInlineImage] = None,
|
||||
pdfimage: Object | None = None,
|
||||
inline: PdfInlineImage | None = None,
|
||||
shorthand=None,
|
||||
):
|
||||
self._name = str(name)
|
||||
self._shorthand = shorthand
|
||||
|
||||
pim: Union[PdfInlineImage, PdfImage]
|
||||
pim: PdfInlineImage | PdfImage
|
||||
|
||||
if inline is not None:
|
||||
self._origin = 'inline'
|
||||
@@ -359,7 +399,7 @@ class ImageInfo:
|
||||
self._comp = 4
|
||||
else:
|
||||
self._comp = 3
|
||||
except UnsupportedImageTypeError as ex:
|
||||
except (AttributeError, UnsupportedImageTypeError) as ex:
|
||||
self._comp = None
|
||||
logger.warning(
|
||||
f"An image with a corrupt or unreadable ICC profile was found. "
|
||||
@@ -428,11 +468,11 @@ def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]:
|
||||
|
||||
for n, inline in enumerate(contentsinfo.inline_images):
|
||||
yield ImageInfo(
|
||||
name='inline-%02d' % n, shorthand=inline.shorthand, inline=inline.iimage
|
||||
name=f'inline-{n:02d}', shorthand=inline.shorthand, inline=inline.iimage
|
||||
)
|
||||
|
||||
|
||||
def _image_xobjects(container) -> Iterator[Tuple[Object, str]]:
|
||||
def _image_xobjects(container) -> Iterator[tuple[Object, str]]:
|
||||
"""Search for all XObject-based images in the container
|
||||
|
||||
Usually the container is a page, but it could also be a Form XObject
|
||||
@@ -520,7 +560,7 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content
|
||||
|
||||
def _process_content_streams(
|
||||
*, pdf: Pdf, container: Object, shorthand=None
|
||||
) -> Iterator[Union[VectorMarker, TextMarker, ImageInfo]]:
|
||||
) -> Iterator[VectorMarker | TextMarker | ImageInfo]:
|
||||
"""Find all individual instances of images drawn in the container
|
||||
|
||||
Usually the container is a page, but it may also be a Form XObject.
|
||||
@@ -569,10 +609,10 @@ def _process_content_streams(
|
||||
yield from _find_form_xobject_images(pdf, container, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(text_blocks, page_width, page_height) -> bool:
|
||||
def _page_has_text(text_blocks: Iterable[FloatRect], page_width, page_height) -> bool:
|
||||
"""Smarter text detection that ignores text in margins"""
|
||||
|
||||
pw, ph = float(page_width), float(page_height)
|
||||
pw, ph = float(page_width), float(page_height) # pylint: disable=invalid-name
|
||||
|
||||
margin_ratio = 0.125
|
||||
interior_bbox = (
|
||||
@@ -582,7 +622,7 @@ def _page_has_text(text_blocks, page_width, page_height) -> bool:
|
||||
margin_ratio * ph, # bottom (first quadrant: bottom < top)
|
||||
)
|
||||
|
||||
def rects_intersect(a, b) -> bool:
|
||||
def rects_intersect(a: FloatRect, b: FloatRect) -> bool:
|
||||
"""
|
||||
Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3)
|
||||
https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
|
||||
@@ -604,19 +644,19 @@ def simplify_textboxes(miner, textbox_getter) -> Iterator[TextboxInfo]:
|
||||
We do this to save memory and ensure that our objects are pickleable.
|
||||
"""
|
||||
for box in textbox_getter(miner):
|
||||
first_line = box._objs[0]
|
||||
first_char = first_line._objs[0]
|
||||
first_line = box._objs[0] # pylint: disable=protected-access
|
||||
first_char = first_line._objs[0] # pylint: disable=protected-access
|
||||
|
||||
visible = first_char.rendermode != 3
|
||||
corrupt = first_char.get_text() == '\ufffd'
|
||||
yield TextboxInfo(box.bbox, visible, corrupt)
|
||||
|
||||
|
||||
worker_pdf = None
|
||||
worker_pdf = None # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _pdf_pageinfo_sync_init(pdf: Pdf, infile: Path, pdfminer_loglevel):
|
||||
global worker_pdf # pylint: disable=global-statement
|
||||
global worker_pdf # pylint: disable=global-statement,invalid-name
|
||||
pikepdf_enable_mmap()
|
||||
|
||||
logging.getLogger('pdfminer').setLevel(pdfminer_loglevel)
|
||||
@@ -650,8 +690,8 @@ def _pdf_pageinfo_concurrent(
|
||||
max_workers,
|
||||
check_pages,
|
||||
detailed_analysis=False,
|
||||
) -> Sequence[Optional['PageInfo']]:
|
||||
pages: Sequence[Optional['PageInfo']] = [None] * len(pdf.pages)
|
||||
) -> Sequence[PageInfo | None]:
|
||||
pages: Sequence[PageInfo | None] = [None] * len(pdf.pages)
|
||||
|
||||
def update_pageinfo(result, pbar):
|
||||
page = result
|
||||
@@ -701,9 +741,11 @@ def _pdf_pageinfo_concurrent(
|
||||
|
||||
|
||||
class PageInfo:
|
||||
_has_text: Optional[bool]
|
||||
_has_vector: Optional[bool]
|
||||
_images: List[ImageInfo]
|
||||
"""Information about type of contents on each page in a PDF."""
|
||||
|
||||
_has_text: bool | None
|
||||
_has_vector: bool | None
|
||||
_images: list[ImageInfo]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -762,15 +804,15 @@ class PageInfo:
|
||||
self._has_vector = False
|
||||
self._has_text = False
|
||||
self._images = []
|
||||
for ci in _process_content_streams(
|
||||
for info in _process_content_streams(
|
||||
pdf=pdf, container=page, shorthand=userunit_shorthand
|
||||
):
|
||||
if isinstance(ci, VectorMarker):
|
||||
if isinstance(info, VectorMarker):
|
||||
self._has_vector = True
|
||||
elif isinstance(ci, TextMarker):
|
||||
elif isinstance(info, TextMarker):
|
||||
self._has_text = True
|
||||
elif isinstance(ci, ImageInfo):
|
||||
self._images.append(ci)
|
||||
elif isinstance(info, ImageInfo):
|
||||
self._images.append(info)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
@@ -836,9 +878,7 @@ class PageInfo:
|
||||
def images(self):
|
||||
return self._images
|
||||
|
||||
def get_textareas(
|
||||
self, visible: Optional[bool] = None, corrupt: Optional[bool] = None
|
||||
):
|
||||
def get_textareas(self, visible: bool | None = None, corrupt: bool | None = None):
|
||||
def predicate(obj, want_visible, want_corrupt):
|
||||
result = True
|
||||
if want_visible is not None:
|
||||
@@ -922,7 +962,7 @@ class PdfInfo:
|
||||
self._has_acroform = True
|
||||
|
||||
@property
|
||||
def pages(self) -> Sequence[Optional[PageInfo]]:
|
||||
def pages(self) -> Sequence[PageInfo | None]:
|
||||
return self._pages
|
||||
|
||||
@property
|
||||
@@ -939,7 +979,7 @@ class PdfInfo:
|
||||
return self._has_acroform
|
||||
|
||||
@property
|
||||
def filename(self) -> Union[str, Path]:
|
||||
def filename(self) -> str | Path:
|
||||
if not isinstance(self._infile, (str, Path)):
|
||||
raise NotImplementedError("can't get filename from stream")
|
||||
return self._infile
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from math import copysign
|
||||
from pathlib import Path
|
||||
|
||||
+99
-11
@@ -4,16 +4,19 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""OCRmyPDF pluggy plugin specification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from logging import Handler
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, AbstractSet, List, NamedTuple, Optional
|
||||
from typing import TYPE_CHECKING, AbstractSet, NamedTuple, Sequence
|
||||
|
||||
import pluggy
|
||||
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf import Executor, PdfContext
|
||||
from ocrmypdf.helpers import Resolution
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -42,6 +45,33 @@ def get_logging_console() -> Handler:
|
||||
"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def initialize(plugin_manager: pluggy.PluginManager):
|
||||
"""Called when this plugin is first loaded into OCRmyPDF.
|
||||
|
||||
The primary intended use of this is for plugins to check compatibility with other
|
||||
plugins and possibly block other blocks, a plugin that wishes to block ocrmypdf's
|
||||
built-in optimize plugin could do:
|
||||
|
||||
.. code-block::
|
||||
|
||||
plugin_manager.set_blocked('ocrmypdf.builtin_plugins.optimize')
|
||||
|
||||
It would also be reasonable for an plugin implementation to check if it is unable
|
||||
to proceed, for example, because a required dependency is missing. (If the plugin's
|
||||
ability to proceed depends on options and arguments, use ``validate`` instead.)
|
||||
|
||||
Raises:
|
||||
ocrmypdf.exceptions.ExitCodeException: If options are not acceptable
|
||||
and the application should terminate gracefully with an informative
|
||||
message and error code.
|
||||
|
||||
Note:
|
||||
This hook will be called from the main process, and may modify global state
|
||||
before child worker processes are forked.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def add_options(parser: ArgumentParser) -> None:
|
||||
"""Allows the plugin to add its own command line and API arguments.
|
||||
@@ -141,7 +171,7 @@ def get_progressbar_class():
|
||||
|
||||
|
||||
@hookspec
|
||||
def validate(pdfinfo: 'PdfInfo', options: Namespace) -> None:
|
||||
def validate(pdfinfo: PdfInfo, options: Namespace) -> None:
|
||||
"""Called to give a plugin an opportunity to review *options* and *pdfinfo*.
|
||||
|
||||
*options* contains the "work order" to process a particular file. *pdfinfo*
|
||||
@@ -167,8 +197,8 @@ def rasterize_pdf_page(
|
||||
raster_device: str,
|
||||
raster_dpi: Resolution,
|
||||
pageno: int,
|
||||
page_dpi: Optional[Resolution],
|
||||
rotation: Optional[int],
|
||||
page_dpi: Resolution | None,
|
||||
rotation: int | None,
|
||||
filter_vector: bool,
|
||||
) -> Path:
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
|
||||
@@ -197,7 +227,7 @@ def rasterize_pdf_page(
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def filter_ocr_image(page: 'PageContext', image: 'Image.Image') -> 'Image.Image':
|
||||
def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
|
||||
"""Called to filter the image before it is sent to OCR.
|
||||
|
||||
This is the image that OCR sees, not what the user sees when they view the
|
||||
@@ -224,7 +254,7 @@ def filter_ocr_image(page: 'PageContext', image: 'Image.Image') -> 'Image.Image'
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
|
||||
def filter_page_image(page: PageContext, image_filename: Path) -> Path:
|
||||
"""Called to filter the whole page before it is inserted into the PDF.
|
||||
|
||||
A whole page image is only produced when preprocessing command line arguments
|
||||
@@ -264,9 +294,7 @@ def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def filter_pdf_page(
|
||||
page: 'PageContext', image_filename: Path, output_pdf: Path
|
||||
) -> Path:
|
||||
def filter_pdf_page(page: PageContext, image_filename: Path, output_pdf: Path) -> Path:
|
||||
"""Called to convert a filtered whole page image into a PDF.
|
||||
|
||||
A whole page image is only produced when preprocessing command line arguments
|
||||
@@ -409,7 +437,7 @@ def get_ocr_engine() -> OcrEngine:
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def generate_pdfa(
|
||||
pdf_pages: List[Path],
|
||||
pdf_pages: list[Path],
|
||||
pdfmark: Path,
|
||||
output_file: Path,
|
||||
compression: str,
|
||||
@@ -456,3 +484,63 @@ def generate_pdfa(
|
||||
See also:
|
||||
https://github.com/tqdm/tqdm
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def optimize_pdf(
|
||||
input_pdf: Path,
|
||||
output_pdf: Path,
|
||||
context: PdfContext,
|
||||
executor: Executor,
|
||||
linearize: bool,
|
||||
) -> tuple[Path, Sequence[str]]:
|
||||
"""Optimize a PDF after image, OCR and metadata processing.
|
||||
|
||||
If the input_pdf is a PDF/A, the plugin should modify input_pdf in a way
|
||||
that preserves the PDF/A status, or report to the user when this is not possible.
|
||||
|
||||
If the implementation fails to produce a smaller file than the input file, it
|
||||
should return input_pdf instead.
|
||||
|
||||
A plugin that implements a new optimizer may need to suppress the built-in
|
||||
optimizer by implementing an ``initialize`` hook.
|
||||
|
||||
Arguments:
|
||||
input_pdf: The input PDF, which has OCR added.
|
||||
output_pdf: The requested filename of the output PDF which should be created
|
||||
by this optimization hook.
|
||||
context: The current context.
|
||||
executor: An initialized executor which may be used during optimization,
|
||||
to distribute optimization tasks.
|
||||
linearize: If True, OCRmyPDF requires ``optimize_pdf`` to return a linearized,
|
||||
also known as fast web view PDF.
|
||||
|
||||
Returns:
|
||||
Path: If optimization is successful, the hook should return ``output_file``.
|
||||
If optimization does not produce a smaller file, the hook should return
|
||||
``input_file``.
|
||||
Sequence[str]: Any comments that the plugin wishes to report to the user,
|
||||
especially reasons it was not able to further optimize the file. For
|
||||
example, the plugin could report that a required third party was not
|
||||
installed, so a specific optimization was not attempted.
|
||||
|
||||
Note:
|
||||
This is a :ref:`firstresult hook<firstresult>`.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def is_optimization_enabled(context: PdfContext) -> bool:
|
||||
"""For a given PdfContext, OCRmyPDF asks the plugin if optimization is enabled.
|
||||
|
||||
An optimization plugin might be installed and active but could be disabled by
|
||||
user settings.
|
||||
|
||||
If this returns False, OCRmyPDF will take certain actions to finalize the PDF.
|
||||
|
||||
Returns:
|
||||
True if the plugin's optimization is enabled.
|
||||
|
||||
Note:
|
||||
This is a :ref:`firstresult hook<firstresult>`.
|
||||
"""
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"""Utilities to measure OCR quality"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
|
||||
"""Wrappers to manage subprocess calls"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
||||
from subprocess import run as subprocess_run
|
||||
from typing import Callable, Optional, Type, Union
|
||||
from typing import Callable, Mapping, Sequence, Union
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
@@ -26,9 +28,17 @@ from ocrmypdf.exceptions import MissingDependencyError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
Args = Sequence[Union[Path, str]]
|
||||
OsEnviron = os._Environ # pylint: disable=protected-access
|
||||
|
||||
|
||||
def run(
|
||||
args, *, env=None, logs_errors_to_stdout: bool = False, **kwargs
|
||||
args: Args,
|
||||
*,
|
||||
env: OsEnviron | None = None,
|
||||
logs_errors_to_stdout: bool = False,
|
||||
check: bool = False,
|
||||
**kwargs,
|
||||
) -> CompletedProcess:
|
||||
"""Wrapper around :py:func:`subprocess.run`
|
||||
|
||||
@@ -50,7 +60,7 @@ def run(
|
||||
stderr = None
|
||||
stderr_name = 'stderr' if not logs_errors_to_stdout else 'stdout'
|
||||
try:
|
||||
proc = subprocess_run(args, env=env, **kwargs)
|
||||
proc = subprocess_run(args, env=env, check=check, **kwargs)
|
||||
except CalledProcessError as e:
|
||||
stderr = getattr(e, stderr_name, None)
|
||||
raise
|
||||
@@ -68,7 +78,12 @@ def run(
|
||||
|
||||
|
||||
def run_polling_stderr(
|
||||
args, *, callback: Callable[[str], None], check: bool = False, env=None, **kwargs
|
||||
args: Args,
|
||||
*,
|
||||
callback: Callable[[str], None],
|
||||
check: bool = False,
|
||||
env: OsEnviron | None = None,
|
||||
**kwargs,
|
||||
) -> CompletedProcess:
|
||||
"""Run a process like ``ocrmypdf.subprocess.run``, and poll stderr.
|
||||
|
||||
@@ -101,7 +116,9 @@ def run_polling_stderr(
|
||||
return CompletedProcess(args, proc.returncode, None, stderr=stderr)
|
||||
|
||||
|
||||
def _fix_process_args(args, env, kwargs):
|
||||
def _fix_process_args(
|
||||
args: Args, env: OsEnviron | None, kwargs
|
||||
) -> tuple[Args, OsEnviron, logging.Logger, bool]:
|
||||
assert 'universal_newlines' not in kwargs, "Use text= instead of universal_newlines"
|
||||
|
||||
if not env:
|
||||
@@ -110,21 +127,26 @@ def _fix_process_args(args, env, kwargs):
|
||||
# Search in spoof path if necessary
|
||||
program = str(args[0])
|
||||
|
||||
if os.name == 'nt':
|
||||
if sys.platform == 'win32':
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from ocrmypdf.subprocess._windows import fix_windows_args
|
||||
|
||||
args = fix_windows_args(program, args, env)
|
||||
|
||||
log.debug("Running: %s", args)
|
||||
process_log = log.getChild(os.path.basename(program))
|
||||
text = kwargs.get('text', False)
|
||||
text = bool(kwargs.get('text', False))
|
||||
|
||||
return args, env, process_log, text
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_version(
|
||||
program: str, *, version_arg: str = '--version', regex=r'(\d+(\.\d+)*)', env=None
|
||||
program: str,
|
||||
*,
|
||||
version_arg: str = '--version',
|
||||
regex=r'(\d+(\.\d+)*)',
|
||||
env: OsEnviron | None = None,
|
||||
) -> str:
|
||||
"""Get the version of the specified program
|
||||
|
||||
@@ -171,42 +193,42 @@ def get_version(
|
||||
return version
|
||||
|
||||
|
||||
missing_program = '''
|
||||
MISSING_PROGRAM = '''
|
||||
The program '{program}' could not be executed or was not found on your
|
||||
system PATH.
|
||||
'''
|
||||
|
||||
missing_optional_program = '''
|
||||
MISSING_OPTIONAL_PROGRAM = '''
|
||||
The program '{program}' could not be executed or was not found on your
|
||||
system PATH. This program is required when you use the
|
||||
{required_for} arguments. You could try omitting these arguments, or install
|
||||
the package.
|
||||
'''
|
||||
|
||||
missing_recommend_program = '''
|
||||
MISSING_RECOMMEND_PROGRAM = '''
|
||||
The program '{program}' could not be executed or was not found on your
|
||||
system PATH. This program is recommended when using the {required_for} arguments,
|
||||
but not required, so we will proceed. For best results, install the program.
|
||||
'''
|
||||
|
||||
old_version = '''
|
||||
OLD_VERSION = '''
|
||||
OCRmyPDF requires '{program}' {need_version} or higher. Your system appears
|
||||
to have {found_version}. Please update this program.
|
||||
'''
|
||||
|
||||
old_version_required_for = '''
|
||||
OLD_VERSION_REQUIRED_FOR = '''
|
||||
OCRmyPDF requires '{program}' {need_version} or higher when run with the
|
||||
{required_for} arguments. If you omit these arguments, OCRmyPDF may be able to
|
||||
proceed. For best results, install the program.
|
||||
'''
|
||||
|
||||
osx_install_advice = '''
|
||||
OSX_INSTALL_ADVICE = '''
|
||||
If you have homebrew installed, try these command to install the missing
|
||||
package:
|
||||
brew install {package}
|
||||
'''
|
||||
|
||||
linux_install_advice = '''
|
||||
LINUX_INSTALL_ADVICE = '''
|
||||
On systems with the aptitude package manager (Debian, Ubuntu), try these
|
||||
commands:
|
||||
sudo apt-get update
|
||||
@@ -216,14 +238,14 @@ On RPM-based systems (Red Hat, Fedora), search for instructions on
|
||||
installing the RPM for {program}.
|
||||
'''
|
||||
|
||||
windows_install_advice = '''
|
||||
WINDOWS_INSTALL_ADVICE = '''
|
||||
If not already installed, install the Chocolatey package manager. Then use
|
||||
a command prompt to install the missing package:
|
||||
choco install {package}
|
||||
'''
|
||||
|
||||
|
||||
def _get_platform():
|
||||
def _get_platform() -> str:
|
||||
if sys.platform.startswith('freebsd'):
|
||||
return 'freebsd'
|
||||
elif sys.platform.startswith('linux'):
|
||||
@@ -233,46 +255,66 @@ def _get_platform():
|
||||
return sys.platform
|
||||
|
||||
|
||||
def _error_trailer(program, package, **kwargs):
|
||||
def _error_trailer(program: str, package: str | Mapping[str, str], **kwargs) -> None:
|
||||
del kwargs
|
||||
if isinstance(package, Mapping):
|
||||
package = package.get(_get_platform(), program)
|
||||
|
||||
if _get_platform() == 'darwin':
|
||||
log.info(osx_install_advice.format(**locals()))
|
||||
log.info(OSX_INSTALL_ADVICE.format(**locals()))
|
||||
elif _get_platform() == 'linux':
|
||||
log.info(linux_install_advice.format(**locals()))
|
||||
log.info(LINUX_INSTALL_ADVICE.format(**locals()))
|
||||
elif _get_platform() == 'windows':
|
||||
log.info(windows_install_advice.format(**locals()))
|
||||
log.info(WINDOWS_INSTALL_ADVICE.format(**locals()))
|
||||
|
||||
|
||||
def _error_missing_program(program, package, required_for, recommended):
|
||||
def _error_missing_program(
|
||||
program: str, package: str, required_for: str | None, recommended: bool
|
||||
) -> None:
|
||||
# pylint: disable=unused-argument
|
||||
if recommended:
|
||||
log.warning(missing_recommend_program.format(**locals()))
|
||||
log.warning(MISSING_RECOMMEND_PROGRAM.format(**locals()))
|
||||
elif required_for:
|
||||
log.error(missing_optional_program.format(**locals()))
|
||||
log.error(MISSING_OPTIONAL_PROGRAM.format(**locals()))
|
||||
else:
|
||||
log.error(missing_program.format(**locals()))
|
||||
log.error(MISSING_PROGRAM.format(**locals()))
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def _error_old_version(program, package, need_version, found_version, required_for):
|
||||
def _error_old_version(
|
||||
program: str,
|
||||
package: str,
|
||||
need_version: str,
|
||||
found_version: str,
|
||||
required_for: str | None,
|
||||
) -> None:
|
||||
# pylint: disable=unused-argument
|
||||
if required_for:
|
||||
log.error(old_version_required_for.format(**locals()))
|
||||
log.error(OLD_VERSION_REQUIRED_FOR.format(**locals()))
|
||||
else:
|
||||
log.error(old_version.format(**locals()))
|
||||
log.error(OLD_VERSION.format(**locals()))
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def _remove_leading_v(s: str) -> str:
|
||||
if sys.version_info >= (3, 9):
|
||||
return s.removeprefix('v')
|
||||
|
||||
if s.startswith('v'):
|
||||
return s[1:]
|
||||
return s
|
||||
|
||||
|
||||
def check_external_program(
|
||||
*,
|
||||
program: str,
|
||||
package: str,
|
||||
version_checker: Callable,
|
||||
version_checker: Callable[[], str],
|
||||
need_version: str,
|
||||
required_for: Optional[str] = None,
|
||||
recommended=False,
|
||||
version_parser: Type[Version] = Version,
|
||||
):
|
||||
required_for: str | None = None,
|
||||
recommended: bool = False,
|
||||
version_parser: type[Version] = Version,
|
||||
) -> None:
|
||||
"""Check for required version of external program and raise exception if not.
|
||||
|
||||
Args:
|
||||
@@ -294,19 +336,19 @@ def check_external_program(
|
||||
found_version = version_checker()
|
||||
else: # deprecated
|
||||
found_version = version_checker
|
||||
except (CalledProcessError, FileNotFoundError, MissingDependencyError):
|
||||
except (CalledProcessError, FileNotFoundError) as e:
|
||||
_error_missing_program(program, package, required_for, recommended)
|
||||
if not recommended:
|
||||
raise MissingDependencyError(program)
|
||||
raise MissingDependencyError(program) from e
|
||||
return
|
||||
except MissingDependencyError:
|
||||
_error_missing_program(program, package, required_for, recommended)
|
||||
if not recommended:
|
||||
raise
|
||||
return
|
||||
|
||||
def remove_leading_v(s):
|
||||
if s.startswith('v'):
|
||||
return s[1:]
|
||||
return s
|
||||
|
||||
found_version = remove_leading_v(found_version)
|
||||
need_version = remove_leading_v(need_version)
|
||||
found_version = _remove_leading_v(found_version)
|
||||
need_version = _remove_leading_v(need_version)
|
||||
|
||||
if found_version and version_parser(found_version) < version_parser(need_version):
|
||||
_error_old_version(program, package, need_version, found_version, required_for)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
"""Find Tesseract and Ghostscript binaries on Windows using the registry."""
|
||||
|
||||
# type: ignore
|
||||
# Non-Windows mypy now breaks when trying to typecheck winreg
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -13,12 +13,26 @@ import shutil
|
||||
import sys
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, Iterator, Set, Tuple, TypeVar
|
||||
from typing import Any, Callable, Iterable, Iterator, TypeVar
|
||||
|
||||
try:
|
||||
import winreg
|
||||
except ModuleNotFoundError as e:
|
||||
raise ModuleNotFoundError("This module is for Windows only") from e
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias # pragma: no cover
|
||||
|
||||
if sys.platform == 'win32':
|
||||
# mypy understands 'if sys.platform' better than try/except ModuleNotFoundError
|
||||
import winreg # pylint: disable=import-error
|
||||
|
||||
HKEYType: TypeAlias = winreg.HKEYType
|
||||
else:
|
||||
from unittest.mock import Mock
|
||||
|
||||
winreg = Mock(
|
||||
spec=['HKEYType', 'EnumKey', 'EnumValue', 'HKEY_LOCAL_MACHINE', 'OpenKey']
|
||||
)
|
||||
# mypy does not understand winreg.HKeyType where winreg is a Mock (fair enough!)
|
||||
HKEYType: TypeAlias = Any
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -26,7 +40,7 @@ log = logging.getLogger(__name__)
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def ghostscript_version_key(s: str) -> Tuple[int, int, int]:
|
||||
def ghostscript_version_key(s: str) -> tuple[int, int, int]:
|
||||
"""Compare Ghostscript version numbers."""
|
||||
try:
|
||||
release = [int(elem) for elem in s.split('.', maxsplit=3)]
|
||||
@@ -37,30 +51,29 @@ def ghostscript_version_key(s: str) -> Tuple[int, int, int]:
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def registry_enum(
|
||||
key: winreg.HKEYType, enum_fn: Callable[[winreg.HKEYType, int], T]
|
||||
) -> Iterator[T]:
|
||||
LIMIT = 999
|
||||
def registry_enum(key: HKEYType, enum_fn: Callable[[HKEYType, int], T]) -> Iterator[T]:
|
||||
limit = 999
|
||||
n = 0
|
||||
while n < LIMIT:
|
||||
while n < limit:
|
||||
try:
|
||||
yield enum_fn(key, n)
|
||||
n += 1
|
||||
except OSError:
|
||||
break
|
||||
if n == LIMIT:
|
||||
if n == limit:
|
||||
raise ValueError(f"Too many registry keys under {key}")
|
||||
|
||||
|
||||
def registry_subkeys(key: winreg.HKEYType) -> Iterator[str]:
|
||||
def registry_subkeys(key: HKEYType) -> Iterator[str]:
|
||||
return registry_enum(key, winreg.EnumKey)
|
||||
|
||||
|
||||
def registry_values(key: winreg.HKEYType) -> Iterator[Tuple[str, Any, int]]:
|
||||
def registry_values(key: HKEYType) -> Iterator[tuple[str, Any, int]]:
|
||||
return registry_enum(key, winreg.EnumValue)
|
||||
|
||||
|
||||
def registry_path_ghostscript(env=None) -> Iterator[Path]:
|
||||
del env # unused (but needed for protocol)
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Artifex\GPL Ghostscript"
|
||||
@@ -71,13 +84,14 @@ def registry_path_ghostscript(env=None) -> Iterator[Path]:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
|
||||
) as k:
|
||||
_, gs_path, _ = next(registry_values(k))
|
||||
yield Path(gs_path) / 'bin'
|
||||
for _, gs_path, _ in registry_values(k):
|
||||
yield Path(gs_path) / 'bin'
|
||||
except OSError as e:
|
||||
log.warning(e)
|
||||
|
||||
|
||||
def registry_path_tesseract(env=None) -> Iterator[Path]:
|
||||
del env # unused (but needed for protocol)
|
||||
try:
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Tesseract-OCR") as k:
|
||||
for subkey, val, _valtype in registry_values(k):
|
||||
@@ -157,7 +171,7 @@ def unique_everseen(iterable: Iterable[T], key: Callable[[T], T]) -> Iterator[T]
|
||||
"List unique elements, preserving order."
|
||||
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||
seen: Set[T] = set()
|
||||
seen: set[T] = set()
|
||||
seen_add = seen.add
|
||||
for element in iterable:
|
||||
k = key(element)
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Empty __init__.py file
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
Tesseract Open Source OCR Engine v4.1.1 with Leptonica
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Portez ce vieux whisky au juge
|
||||
blond qui fume sur son Ile
|
||||
interieure, a cöte de l'alcöve
|
||||
ovoide, oU les büches se
|
||||
consument dans l'ätre, ce qui
|
||||
lui permet de penser & la
|
||||
caenogenese de |'etre dont il
|
||||
est question dans la cause
|
||||
ambigu& entendue a MoY, dans
|
||||
un capharnaüm qui, pense-t-il,
|
||||
diminue ca et la la qualite de son
|
||||
ceuvre.
|
||||
|
||||
Vendored
+1
@@ -82,3 +82,4 @@
|
||||
{"tesseract_version": "4.1.1", "system": "Linux", "python": "3.9.5", "argv_slug": "__-l__eng__--oem__1__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "--oem", "1", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "5.0.0", "system": "Linux", "python": "3.9.5", "argv_slug": "__-l__eng__thresholding_method=1__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "-c", "thresholding_method=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "5.0.0", "system": "Linux", "python": "3.9.5", "argv_slug": "__-l__eng__thresholding_method=2__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/trivial.pdf", "args": ["-l", "eng", "-c", "textonly_pdf=1", "-c", "thresholding_method=2", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
{"tesseract_version": "4.1.1", "system": "Linux", "python": "3.10.4", "argv_slug": "__-l__deu__000001_ocr.png__000001_ocr_tess__pdf__txt", "sourcefile": "resources/francais.pdf", "args": ["-l", "deu", "-c", "textonly_pdf=1", "$TMPDIR/000001_ocr.png", "$TMPDIR/000001_ocr_tess", "pdf", "txt"]}
|
||||
|
||||
+3
-1
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
@@ -52,7 +54,7 @@ def resources() -> Path:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ocrmypdf_exec() -> List[str]:
|
||||
def ocrmypdf_exec() -> list[str]:
|
||||
return [sys.executable, '-m', 'ocrmypdf']
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ that is not UTF-8 compatible, so we are forced to check that we can convert it
|
||||
and present it to the user.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -44,6 +44,8 @@ Assumes Tesseract 4.0.0-alpha or higher.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
@@ -31,6 +31,8 @@ In 'pdf' mode, convert the image to PDF using another program.
|
||||
In orientation check mode, report 0, 90, 180, 270... based on page number.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ In 'pdf' mode, convert the image to PDF using another program.
|
||||
In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# type: ignore
|
||||
|
||||
"""Tesseract no-op plugin that simulates the OOM killer on page 4.
|
||||
|
||||
OCRmyPDF can use a lot of memory, even that it might trigger the
|
||||
@@ -30,14 +28,18 @@ ensure we fail with an error rather than deadlock in such cases.
|
||||
Page 4 was chosen because of this number's association with bad luck
|
||||
in many East Asian cultures.
|
||||
"""
|
||||
# type: ignore
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
# type: ignore
|
||||
|
||||
|
||||
# Ugly hack that let us use the NoopOcrEngine without setting up packaging for our
|
||||
# tests.
|
||||
# This hack also requires us to set type: ignore
|
||||
@@ -47,7 +49,7 @@ exec(parent)
|
||||
NoopOcrEngine = locals()['NoopOcrEngine']
|
||||
|
||||
|
||||
class Page4Engine(NoopOcrEngine):
|
||||
class Page4Engine(NoopOcrEngine): # type: ignore
|
||||
def __str__(self):
|
||||
return f"NO-OP Page 4 {NoopOcrEngine.version()}"
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf.helpers import check_pdf
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from subprocess import PIPE, run
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pikepdf
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import img2pdf
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from math import isclose
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
from datetime import timezone
|
||||
from os import fspath
|
||||
from shutil import copyfile
|
||||
|
||||
import pikepdf
|
||||
@@ -17,7 +18,7 @@ from pikepdf.models.metadata import decode_pdf_date
|
||||
|
||||
from ocrmypdf._jobcontext import PdfContext
|
||||
from ocrmypdf._pipeline import convert_to_pdfa, metadata_fixup
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf._plugin_manager import get_parser_options_plugins, get_plugin_manager
|
||||
from ocrmypdf.cli import get_parser
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf.pdfa import file_claims_pdfa, generate_pdfa_ps
|
||||
@@ -300,8 +301,8 @@ def test_kodak_toc(resources, outpdf):
|
||||
|
||||
|
||||
def test_metadata_fixup_warning(resources, outdir, caplog):
|
||||
options = get_parser().parse_args(
|
||||
args=['--output-type', 'pdfa-2', 'graph.pdf', 'out.pdf']
|
||||
_parser, options, _pm = get_parser_options_plugins(
|
||||
['--output-type', 'pdfa-2', 'graph.pdf', 'out.pdf']
|
||||
)
|
||||
|
||||
copyfile(resources / 'graph.pdf', outdir / 'graph.pdf')
|
||||
@@ -326,6 +327,9 @@ def test_metadata_fixup_warning(resources, outdir, caplog):
|
||||
assert any(record.levelname == 'WARNING' for record in caplog.records)
|
||||
|
||||
|
||||
XMP_MAGIC = b'W5M0MpCehiHzreSzNTczkc9d'
|
||||
|
||||
|
||||
def test_prevent_gs_invalid_xml(resources, outdir):
|
||||
generate_pdfa_ps(outdir / 'pdfa.ps')
|
||||
copyfile(resources / 'trivial.pdf', outdir / 'layers.rendered.pdf')
|
||||
@@ -352,7 +356,7 @@ def test_prevent_gs_invalid_xml(resources, outdir):
|
||||
contents = (outdir / 'pdfa.pdf').read_bytes()
|
||||
# Since the XML may be invalid, we scan instead of actually feeding it
|
||||
# to a parser.
|
||||
XMP_MAGIC = b'W5M0MpCehiHzreSzNTczkc9d'
|
||||
|
||||
xmp_start = contents.find(XMP_MAGIC)
|
||||
xmp_end = contents.rfind(b'<?xpacket end', xmp_start)
|
||||
assert 0 < xmp_start < xmp_end
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pikepdf
|
||||
|
||||
+32
-13
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pickle
|
||||
from io import BytesIO
|
||||
from math import isclose
|
||||
@@ -24,20 +26,24 @@ from ocrmypdf.pdfinfo.layout import PDFPage
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
def test_single_page_text(outdir):
|
||||
@pytest.fixture
|
||||
def single_page_text(outdir):
|
||||
filename = outdir / 'text.pdf'
|
||||
pdf = Canvas(str(filename), pagesize=(8 * inch, 6 * inch))
|
||||
text = pdf.beginText()
|
||||
text.setFont('Helvetica', 12)
|
||||
text.setTextOrigin(1 * inch, 3 * inch)
|
||||
text.textLine(
|
||||
"Methink'st thou art a general offence and every" " man should beat thee."
|
||||
"Methink'st thou art a general offence and every man should beat thee."
|
||||
)
|
||||
pdf.drawText(text)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
return filename
|
||||
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
|
||||
def test_single_page_text(single_page_text):
|
||||
info = pdfinfo.PdfInfo(single_page_text)
|
||||
|
||||
assert len(info) == 1
|
||||
page = info[0]
|
||||
@@ -54,7 +60,8 @@ def eight_by_eight():
|
||||
return im
|
||||
|
||||
|
||||
def test_single_page_image(eight_by_eight, outpdf):
|
||||
@pytest.fixture
|
||||
def eight_by_eight_regular_image(eight_by_eight, outpdf):
|
||||
im = eight_by_eight
|
||||
bio = BytesIO()
|
||||
im.save(bio, format='PNG')
|
||||
@@ -71,7 +78,11 @@ def test_single_page_image(eight_by_eight, outpdf):
|
||||
outputstream=f,
|
||||
**IMG2PDF_KWARGS,
|
||||
)
|
||||
info = pdfinfo.PdfInfo(outpdf)
|
||||
return outpdf
|
||||
|
||||
|
||||
def test_single_page_image(eight_by_eight_regular_image):
|
||||
info = pdfinfo.PdfInfo(eight_by_eight_regular_image)
|
||||
|
||||
assert len(info) == 1
|
||||
page = info[0]
|
||||
@@ -88,16 +99,18 @@ def test_single_page_image(eight_by_eight, outpdf):
|
||||
assert isclose(pdfimage.dpi.y, 8)
|
||||
|
||||
|
||||
def test_single_page_inline_image(eight_by_eight, outdir):
|
||||
filename = outdir / 'image-mono-inline.pdf'
|
||||
pdf = Canvas(str(filename), pagesize=(8 * 72, 6 * 72))
|
||||
|
||||
@pytest.fixture
|
||||
def eight_by_eight_inline_image(eight_by_eight, outpdf):
|
||||
pdf = Canvas(str(outpdf), pagesize=(8 * 72, 6 * 72))
|
||||
# Draw image in a 72x72 pt or 1"x1" area
|
||||
pdf.drawInlineImage(eight_by_eight, 0, 0, width=72, height=72)
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
return outpdf
|
||||
|
||||
info = pdfinfo.PdfInfo(filename)
|
||||
|
||||
def test_single_page_inline_image(eight_by_eight_inline_image):
|
||||
info = pdfinfo.PdfInfo(eight_by_eight_inline_image)
|
||||
print(info)
|
||||
pdfimage = info[0].images[0]
|
||||
assert isclose(pdfimage.dpi.x, 8)
|
||||
@@ -177,7 +190,7 @@ def test_stack_abuse():
|
||||
pdfinfo.info._interpret_contents(stream)
|
||||
|
||||
stream = pikepdf.Stream(p, b'q Q Q Q Q')
|
||||
with pytest.warns(UserWarning, match="underflowed") as record:
|
||||
with pytest.warns(UserWarning, match="underflowed"):
|
||||
pdfinfo.info._interpret_contents(stream)
|
||||
|
||||
stream = pikepdf.Stream(p, b'q ' * 135)
|
||||
@@ -201,7 +214,8 @@ def test_pages_issue700(monkeypatch, resources):
|
||||
)
|
||||
|
||||
|
||||
def test_image_scale0(resources, outpdf):
|
||||
@pytest.fixture
|
||||
def image_scale0(resources, outpdf):
|
||||
with pikepdf.open(resources / 'cmyk.pdf') as cmyk:
|
||||
xobj = cmyk.pages[0].as_form_xobject()
|
||||
|
||||
@@ -215,7 +229,12 @@ def test_image_scale0(resources, outpdf):
|
||||
p, b"q 0 0 0 0 0 0 cm %s Do Q" % bytes(objname)
|
||||
)
|
||||
p.save(outpdf)
|
||||
return outpdf
|
||||
|
||||
pi = pdfinfo.PdfInfo(outpdf, detailed_analysis=True, progbar=False, max_workers=1)
|
||||
|
||||
def test_image_scale0(image_scale0):
|
||||
pi = pdfinfo.PdfInfo(
|
||||
image_scale0, detailed_analysis=True, progbar=False, max_workers=1
|
||||
)
|
||||
assert not pi.pages[0]._images[0].dpi.is_finite
|
||||
assert pi.pages[0].dpi == Resolution(0, 0)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from math import isclose
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf import quality as qual
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user