Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac91fca2a | ||
|
|
6edf756849 | ||
|
|
4fb1bb4de6 | ||
|
|
0544d06c3d | ||
|
|
34c285c9ac | ||
|
|
2f53b27651 | ||
|
|
772677746b | ||
|
|
f0bad87ea6 | ||
|
|
44e71f8c14 | ||
|
|
964b30ca26 | ||
|
|
214a333e2d | ||
|
|
ec6401ab57 | ||
|
|
cbc5e8ce8d | ||
|
|
a1c4cfe8f1 | ||
|
|
3a721e6578 | ||
|
|
e6b716cdde | ||
|
|
02c39998b8 | ||
|
|
0774bc7f14 | ||
|
|
c6a98b3d0b | ||
|
|
981bbf1105 | ||
|
|
2b0c6cfd40 | ||
|
|
59f6bc8306 | ||
|
|
653c4ffb45 | ||
|
|
d947ca258e | ||
|
|
d5ff7f7db9 | ||
|
|
579cef3649 | ||
|
|
cb2f090c60 | ||
|
|
f3d6387bca | ||
|
|
abf9729c61 | ||
|
|
442e9c9f0d | ||
|
|
397fad249d | ||
|
|
9a3c5a3f7c | ||
|
|
950c700274 | ||
|
|
26432c38a9 | ||
|
|
28be50136c | ||
|
|
0c62f2de5d | ||
|
|
5caf654f22 | ||
|
|
205593445e | ||
|
|
f25fb8c63a | ||
|
|
99c78650b6 | ||
|
|
69355886a8 | ||
|
|
08e89e2dbe | ||
|
|
0e013df161 | ||
|
|
9ba4e3ab46 | ||
|
|
5fdcb7602b | ||
|
|
b4db1b741f | ||
|
|
7a8cc21e31 | ||
|
|
0674829d8f | ||
|
|
315aa0474b | ||
|
|
df3451e779 | ||
|
|
3ba42802d1 | ||
|
|
d6342cb8c2 | ||
|
|
065bddbc6c | ||
|
|
067f429dde | ||
|
|
6895c2d70f | ||
|
|
686481982a | ||
|
|
a9e1d19b78 | ||
|
|
f95aa63718 | ||
|
|
855de287b2 | ||
|
|
feeb9f213f | ||
|
|
e7eb8fa805 | ||
|
|
8a747f005a | ||
|
|
16ab4a8b4e | ||
|
|
8d30cff4ef | ||
|
|
59d5b0d1bd | ||
|
|
9ec0745ab8 | ||
|
|
3a3635f7f9 | ||
|
|
6a746a1cbb | ||
|
|
906c130f96 | ||
|
|
4a78458821 | ||
|
|
fddf3ce2f4 | ||
|
|
353b34e695 | ||
|
|
7d63355c3c | ||
|
|
42ff7fc842 | ||
|
|
26470fe16a | ||
|
|
3b9d4b7f0a | ||
|
|
11f53fe9a9 | ||
|
|
123c0c766f | ||
|
|
6a9be2142e | ||
|
|
0bc350f55e | ||
|
|
7a6edf62ba | ||
|
|
07b6f06f11 | ||
|
|
2005f622bb | ||
|
|
cca04fd799 | ||
|
|
75bf8e4ba2 | ||
|
|
daabb5b100 | ||
|
|
035ebea72f | ||
|
|
a499956462 | ||
|
|
74d2a156c4 | ||
|
|
f87fc7b12d | ||
|
|
602f5632cb | ||
|
|
9fbbcf7599 | ||
|
|
9498f01f59 | ||
|
|
2c59aca5a1 | ||
|
|
51301d69c9 | ||
|
|
7e608fd1df | ||
|
|
ecc79315df | ||
|
|
14365d10b8 | ||
|
|
5e5320020f | ||
|
|
103c3e0cd6 | ||
|
|
7a1c89edd9 | ||
|
|
a5ff3d2f42 | ||
|
|
b71d16dd96 | ||
|
|
fd593eb5e9 | ||
|
|
a0b98abb94 | ||
|
|
18353e1e94 | ||
|
|
9adcad84da | ||
|
|
f2714586d8 | ||
|
|
0b6fb62967 | ||
|
|
1db8b0b943 | ||
|
|
f38aebb3d5 | ||
|
|
7162c36d37 | ||
|
|
f4d4ea46c8 | ||
|
|
2fd1a0f178 | ||
|
|
73ed33a086 | ||
|
|
e6095a9949 | ||
|
|
16f05af401 | ||
|
|
1631afc878 | ||
|
|
63d87fc440 | ||
|
|
9489c01259 | ||
|
|
30d92ad83f | ||
|
|
a4987733c4 | ||
|
|
39eee05230 | ||
|
|
5b2f2e6290 | ||
|
|
445617a1a5 | ||
|
|
f6e90a5934 | ||
|
|
43618e6b3f | ||
|
|
e97f89de3b | ||
|
|
11d3e32f1e | ||
|
|
2affa83efe | ||
|
|
c90d5cd84b | ||
|
|
aacaba3d26 | ||
|
|
fec53be841 | ||
|
|
3f7b540f76 | ||
|
|
d217856166 | ||
|
|
e2be457e9b | ||
|
|
4850f486d2 | ||
|
|
729c7febd9 | ||
|
|
6c6aca2f1e | ||
|
|
c69823f496 | ||
|
|
73f8f6aac8 | ||
|
|
d944254e45 | ||
|
|
f7ddffe554 | ||
|
|
8a73ed5d5a | ||
|
|
03669183d7 | ||
|
|
74e101a2fa | ||
|
|
532cf18ad3 | ||
|
|
0b90b697e2 | ||
|
|
6be7c5f7c8 | ||
|
|
db2e5132e6 | ||
|
|
b14f6f778a | ||
|
|
415de77457 | ||
|
|
a9466c4f58 | ||
|
|
d9ae453a63 | ||
|
|
9841e09233 | ||
|
|
0ca314e066 | ||
|
|
d7680cae27 | ||
|
|
491b6bdb1f | ||
|
|
c591f9601a | ||
|
|
8d1e75017e | ||
|
|
94615f7ad4 | ||
|
|
e5df8e1315 | ||
|
|
d739b91aef | ||
|
|
686cfb2539 | ||
|
|
2633716bb7 | ||
|
|
0a07c0a44e | ||
|
|
2ca6e110ca | ||
|
|
334a07c839 | ||
|
|
a57c39358d | ||
|
|
30a0c315fb | ||
|
|
b860f0d94c | ||
|
|
14f4c19f5a | ||
|
|
7ab5c55d46 | ||
|
|
8b6ecd5971 | ||
|
|
7b0871ae4c | ||
|
|
b73af7ce10 | ||
|
|
60645717e2 | ||
|
|
1cbf578538 | ||
|
|
e966c1fceb | ||
|
|
d0133f8641 | ||
|
|
6d30b497dc | ||
|
|
f3b89e66eb | ||
|
|
04154e207c | ||
|
|
9898904be7 | ||
|
|
27d5229842 | ||
|
|
4a9a575ef0 | ||
|
|
52fd9a630d | ||
|
|
a596ccf844 | ||
|
|
e7fa97731f | ||
|
|
290aa28108 |
@@ -64,6 +64,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
img2pdf \
|
||||
libsm6 libxext6 libxrender-dev \
|
||||
pngquant \
|
||||
python-is-python3 \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-chi-sim \
|
||||
tesseract-ocr-deu \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
FROM alpine:3.18 as base
|
||||
FROM alpine:3.19.1 as base
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV TZ=UTC
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: General issues
|
||||
name: Installation, packaging, dependencies
|
||||
description: Installation, packages, dependencies, "nothing works", test suite failures...
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
labels: ["triage"]
|
||||
assignees:
|
||||
- jbarlow83
|
||||
body:
|
||||
@@ -9,6 +9,10 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
If your issue involves using OCRmyPDF on specific file(s) and not getting
|
||||
good results, this is the *wrong* issue template. Please use the recommended
|
||||
template to ensure we have enough information to help.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -20,7 +24,7 @@ body:
|
||||
- type: dropdown
|
||||
id: packaging-system
|
||||
attributes:
|
||||
label: Where are you installing from?
|
||||
label: Where are you installing/running from?
|
||||
multiple: true
|
||||
options:
|
||||
- PyPI (pip, poetry, pipx, etc.)
|
||||
@@ -33,6 +37,11 @@ body:
|
||||
- source build
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OCRmyPDF version
|
||||
description: Paste "ocrmypdf --version" here
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
attributes:
|
||||
@@ -43,6 +52,18 @@ body:
|
||||
- Windows
|
||||
- macOS
|
||||
- BSD
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: Operating system details and version
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Simple sanity checks
|
||||
description: Select all that apply
|
||||
options:
|
||||
- label: Operating system is currently supported by its vendor (not end of life)
|
||||
- label: Python version is compatible with OCRmyPDF
|
||||
- label: This issue is not about a specific input file
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Problem with specific file
|
||||
description: Something went wrong while trying to OCR a specific file
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
labels: ["triage"]
|
||||
assignees:
|
||||
- jbarlow83
|
||||
body:
|
||||
@@ -21,7 +21,7 @@ body:
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Please include steps to reproduce
|
||||
description: Please include steps to reproduce.
|
||||
value: |
|
||||
1. Run ocrmypdf -v1 ...arguments... input.pdf output.pdf
|
||||
2. Open output.pdf
|
||||
@@ -31,8 +31,21 @@ body:
|
||||
id: files
|
||||
attributes:
|
||||
label: Files
|
||||
description: Please attach the input and output files, or any screenshots that may be helpful.
|
||||
placeholder: Drag and drop files here
|
||||
description: |
|
||||
Please attach the input and output files, or any screenshots that may be helpful.
|
||||
|
||||
If you cannot provide a test file, we probably won't be able to help with the issue.
|
||||
PDF is a complex file format, and there may be technical details in the PDF that are
|
||||
causing the issue. There's really no substitute for a test file.
|
||||
|
||||
We understand files may contain personal or sensitive information. Here are some options:
|
||||
- Try reproducing the issue with a file from the OCRmyPDF test suite. (See tests/resources)
|
||||
- Try to create another file in the same way as your private file.
|
||||
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
|
||||
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
|
||||
omits personal information.
|
||||
placeholder: |
|
||||
Drag and drop files here.
|
||||
- type: dropdown
|
||||
id: packaging-system
|
||||
attributes:
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Problem with third party app that uses OCRmyPDF
|
||||
description: |
|
||||
For PDF generation issues with third party software such as Paperless-ngx that
|
||||
uses OCRmyPDF to perform OCR or generate PDFs.
|
||||
title: "[3rdparty]: "
|
||||
labels: ["triage"]
|
||||
assignees:
|
||||
- jbarlow83
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to describe this issue with a particular file
|
||||
and third party app.
|
||||
|
||||
If you are comfortable using OCRmyPDF, please trying to install OCRmyPDF,
|
||||
run it on your file, and see if it works. It's easier for everyone
|
||||
if you can confirm that the issue occurs with OCRmyPDF and not with
|
||||
the third party app.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Simple sanity checks
|
||||
description: Select all that apply
|
||||
options:
|
||||
- label: This is an issue with an app that uses OCRmyPDF for OCR
|
||||
- label: I am using a recent version of the third party app
|
||||
- label: I will include a file that reproduces the issuse
|
||||
- type: input
|
||||
id: thirdparty-app-name-version
|
||||
attributes:
|
||||
label: Third party app name and version
|
||||
description: e.g. Paperless-ngx 2.9.0
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Please include steps to reproduce.
|
||||
value: |
|
||||
1. Import attached file into Paperless-ngx
|
||||
2. Trigger OCR
|
||||
3. Check log file
|
||||
4. ...
|
||||
render: plain text
|
||||
- type: textarea
|
||||
id: files
|
||||
attributes:
|
||||
label: Files
|
||||
description: |
|
||||
Please attach the input and output files, or any screenshots that may be helpful.
|
||||
|
||||
If you cannot provide a test file, we probably won't be able to help with the issue.
|
||||
PDF is a complex file format, and there may be technical details in the PDF that are
|
||||
causing the issue. There's really no substitute for a test file.
|
||||
|
||||
We understand files may contain personal or sensitive information. Here are some options:
|
||||
- Try reproducing the issue with a file from the test suite. (See tests/resources)
|
||||
- Try to create another file in the same way as your private file.
|
||||
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
|
||||
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
|
||||
omits personal information.
|
||||
placeholder: |
|
||||
Drag and drop files here.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: OCRmyPDF version
|
||||
description: Paste "ocrmypdf --version" here
|
||||
placeholder: ocrmypdf --version
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
placeholder: Run OCRmyPDF with verbosity `-v1` to get more detailed logging output.
|
||||
render: plain text
|
||||
+26
-23
@@ -22,20 +22,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
python: "3.9"
|
||||
- os: ubuntu-22.04
|
||||
python: "3.10"
|
||||
- os: ubuntu-22.04
|
||||
python: "3.11"
|
||||
- os: ubuntu-22.04
|
||||
python: "3.9"
|
||||
python: "3.10"
|
||||
tesseract5: true
|
||||
- os: ubuntu-latest
|
||||
python: "3.12"
|
||||
tesseract5: true
|
||||
#- os: ubuntu-latest
|
||||
# python: "pypy3.9"
|
||||
- os: ubuntu-latest
|
||||
python: "pypy3.10"
|
||||
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
@@ -46,7 +44,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
name: Setup Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -63,6 +61,7 @@ jobs:
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ghostscript \
|
||||
jbig2dec \
|
||||
img2pdf \
|
||||
libexempi8 \
|
||||
libffi-dev \
|
||||
@@ -102,17 +101,20 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
|
||||
|
||||
test_macos:
|
||||
name: Test macOS
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64
|
||||
python: ["3.10", "3.11", "3.12"]
|
||||
|
||||
env:
|
||||
@@ -133,11 +135,10 @@ jobs:
|
||||
ghostscript \
|
||||
jbig2enc \
|
||||
openjpeg \
|
||||
openssl \
|
||||
pngquant \
|
||||
tesseract
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
name: Setup Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -160,7 +161,9 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -182,7 +185,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
name: Setup Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
@@ -191,7 +194,7 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: |
|
||||
choco install --yes --no-progress --pre tesseract
|
||||
choco install --yes --no-progress --ignore-checksums ghostscript
|
||||
choco install --yes --no-progress --ignore-checksums ghostscript --version 9.56.1
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
@@ -203,7 +206,9 @@ jobs:
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
@@ -216,10 +221,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
name: Setup Python
|
||||
with:
|
||||
python-version: "3.9"
|
||||
python-version: "3.10"
|
||||
cache: "pip"
|
||||
|
||||
- name: Make wheels and sdist
|
||||
@@ -227,8 +232,9 @@ jobs:
|
||||
python -m pip install --upgrade pip wheel build
|
||||
python -m build --sdist --wheel
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifact
|
||||
path: |
|
||||
./dist/*.whl
|
||||
./dist/*.tar.gz
|
||||
@@ -242,7 +248,7 @@ jobs:
|
||||
id-token: write # mandatory for PyPI publishing
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifact
|
||||
path: dist
|
||||
@@ -259,7 +265,7 @@ jobs:
|
||||
# Required to create a release
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifact
|
||||
path: dist
|
||||
@@ -353,9 +359,6 @@ jobs:
|
||||
username: jbarlow83
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -367,6 +370,6 @@ jobs:
|
||||
run: |
|
||||
docker buildx build \
|
||||
--push \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
|
||||
--file .docker/Dockerfile.alpine .
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ formats:
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.9"
|
||||
python: "3.10"
|
||||
|
||||
python:
|
||||
install:
|
||||
|
||||
@@ -70,6 +70,7 @@ Files: tests/resources/linn.png
|
||||
tests/resources/ccitt.pdf
|
||||
tests/resources/cardinal.pdf
|
||||
tests/resources/jbig2.pdf
|
||||
tests/resources/jbig2_baddevicen.pdf
|
||||
tests/resources/skew.pdf
|
||||
tests/resources/rotated_skew.pdf
|
||||
tests/resources/poster.pdf
|
||||
@@ -123,6 +124,14 @@ Copyright: Kai-Uwe Behrmann <www.behrmann.name>
|
||||
ColorSolutions <www.basICColor.com>
|
||||
License: Zlib
|
||||
|
||||
Files: src/ocrmypdf/data/pdf.ttf
|
||||
Copyright: (C) 2014 Ray Smith
|
||||
(C) 2015 Ken Sharp
|
||||
(C) 2016 James R. Barlow
|
||||
(C) 2016 Jeff Breidenbach
|
||||
(C) 2017 Zdenko Podobný
|
||||
License: Apache-2.0
|
||||
|
||||
Files: tests/resources/3small.pdf
|
||||
Copyright: (C) 2014 Euskaldunaa
|
||||
(C) 2017 James R. Barlow
|
||||
|
||||
@@ -70,6 +70,7 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
|
||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||
| Fedora | ``dnf install ocrmypdf`` |
|
||||
| macOS (Homebrew) | ``brew install ocrmypdf`` |
|
||||
| macOS (MacPorts) | ``port install ocrmypdf`` |
|
||||
| macOS (nix) | ``nix-env -i ocrmypdf`` |
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
| FreeBSD | ``pkg install py-ocrmypdf`` |
|
||||
|
||||
+40
-39
@@ -118,15 +118,16 @@ OCR for huge images
|
||||
-------------------
|
||||
|
||||
Tesseract has internal limits on the size
|
||||
of images it will process. If you issue
|
||||
``--tesseract-downsample-large-images``, OCRmyPDF will downsample images
|
||||
to fit Tesseract limits. (The limits are usually entered only for scanned
|
||||
images of oversized media, such as large maps or blueprints exceeding
|
||||
110 cm or 43 inches in either dimension, and at high DPI.)
|
||||
of images it will process. By default,
|
||||
``--tesseract-downsample-large-images`` is enabled, and OCRmyPDF will
|
||||
downsample images to fit Tesseract limits. (The limits are usually encountered
|
||||
only for scanned images of oversized media, such as large maps or blueprints exceeding
|
||||
110 cm or 43 inches in either dimension, and at high DPI.) This feature can disabled
|
||||
using ``--no-tesseract-downsample-large-images``.
|
||||
|
||||
``--tesseract-downsample-above Npixels`` adjusts the threshold at which images
|
||||
will be downsampled. By default, only images that exceed any of Tesseract's
|
||||
internal limits are downsampled.
|
||||
internal limits are downsampled (32767 pixels on either dimension).
|
||||
|
||||
You will also need to set ``--tesseract-timeout`` high enough to allow
|
||||
for processing.
|
||||
@@ -239,46 +240,46 @@ rendering
|
||||
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The
|
||||
renderer may be selected using ``--pdf-renderer``. The default is
|
||||
``auto`` which lets OCRmyPDF select the renderer to use. Currently,
|
||||
``auto`` always selects ``sandwich``.
|
||||
|
||||
The ``sandwich`` renderer
|
||||
-------------------------
|
||||
|
||||
The ``sandwich`` renderer uses Tesseract's new text-only PDF feature,
|
||||
which produces a PDF page that lays out the OCR in invisible text. This
|
||||
page is then "sandwiched" onto the original PDF page, allowing lossless
|
||||
application of OCR even to PDF pages that contain other vector objects.
|
||||
|
||||
Currently this is the best renderer for most uses, however it is
|
||||
implemented in Tesseract so OCRmyPDF cannot influence it. Currently some
|
||||
problematic PDF viewers like Mozilla PDF.js and macOS Preview have
|
||||
problems with segmenting its text output, and
|
||||
mightrunseveralwordstogether.
|
||||
|
||||
When image preprocessing features like ``--deskew`` are used, the
|
||||
original PDF will be rendered as a full page and the OCR layer will be
|
||||
placed on top.
|
||||
``auto`` always selects ``hocr``.
|
||||
|
||||
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. (You may
|
||||
need to disable PDF/A conversion nad optimization to eliminate all
|
||||
lossy transformations.)
|
||||
.. versionchanged:: 16.0.0
|
||||
|
||||
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
|
||||
looking to customize how OCR is presented should look here. A major
|
||||
disadvantage of this renderer is it not capable of correctly handling
|
||||
text outside the Latin alphabet (specifically, it supports the ISO 8859-1
|
||||
character set). Pull requests to improve the situation are welcome.
|
||||
In both renderers, a text-only layer is rendered and sandwiched (overlaid)
|
||||
on to either the original PDF page, or newly rasterized version of the
|
||||
original PDF page (when ``--force-ocr`` is used). In this way, loss
|
||||
of PDF information is generally avoided. (You may need to disable PDF/A
|
||||
conversion and optimization to eliminate all lossy transformations.)
|
||||
|
||||
Currently, this renderer has the best compatibility with Mozilla's
|
||||
PDF.js viewer.
|
||||
The current approach used by the new hOCR renderer is a re-implementation
|
||||
of Tesseract's PDF renderer, using the same Glyphless font and general
|
||||
ideas, but fixing many technical issues that impeded it. The new hocr
|
||||
provides better text placement accuracy, avoids issues with word
|
||||
segmentation, and provides better positioning of skewed text.
|
||||
|
||||
This works in all versions of Tesseract.
|
||||
Using the experimental API, it is also possible to edit the OCR output
|
||||
from Tesseract, using any tool that is capable of editing hOCR files.
|
||||
|
||||
Older versions of this renderer did not support non-Latin languages, but
|
||||
it is now universal.
|
||||
|
||||
The ``sandwich`` renderer
|
||||
-------------------------
|
||||
|
||||
The ``sandwich`` renderer uses Tesseract's text-only PDF feature,
|
||||
which produces a PDF page that lays out the OCR in invisible text.
|
||||
|
||||
Currently some problematic PDF viewers like Mozilla PDF.js and macOS
|
||||
Preview have problems with segmenting its text output, and
|
||||
mightrunseveralwordstogether. It also does not implement right to left
|
||||
fonts (Arabic, Hebrew, Persian). The output of this renderer cannot
|
||||
be edited. The sandwich renderer is retained for testing.
|
||||
|
||||
When image preprocessing features like ``--deskew`` are used, the
|
||||
original PDF will be rendered as a full page and the OCR layer will be
|
||||
placed on top.
|
||||
|
||||
Rendering and rasterizing options
|
||||
=================================
|
||||
|
||||
+3
-1
@@ -112,7 +112,9 @@ OCRmyPDF is strict about not writing to standard output so that
|
||||
users can safely use it in a pipeline and produce a valid output
|
||||
file. A caller application will have to ensure it does not write to
|
||||
standard output either, if it wants to be compatible with this
|
||||
behavior and support piping to a file.
|
||||
behavior and support piping to a file. Another benefit of running
|
||||
OCRmyPDF in a child process, as recommended above, is that it will
|
||||
not interfere with the parent process's standard output.
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
+16
-2
@@ -187,7 +187,7 @@ might remove desirable content, especially from poor quality scans.
|
||||
background from grayscale or color images. Monochrome images are
|
||||
ignored. This should not be used on documents that contain color
|
||||
photos as it may remove them.
|
||||
- ``--deskew`` will correct pages were scanned at a skewed angle by
|
||||
- ``--deskew`` will correct pages that were scanned at a skewed angle by
|
||||
rotating them back into place.
|
||||
- ``--clean`` uses
|
||||
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
|
||||
@@ -245,6 +245,20 @@ if all you want to is to apply image processing or PDF/A conversion.
|
||||
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
|
||||
for non-OCR operations, if needed.
|
||||
|
||||
Remove all text or OCR from my PDF
|
||||
----------------------------------
|
||||
|
||||
This is getting ridiculous, but OCRmyPDF can complete strip all textual
|
||||
information from a PDF and reconstruct it as a "bag of images" PDF.
|
||||
|
||||
.. code-block::
|
||||
|
||||
ocrmypdf --tesseract-timeout 0 --force-ocr input.pdf output.pdf
|
||||
|
||||
Why would you want to do this? Perhaps you have a PDF where OCR
|
||||
fails to produce useful results, and just want to get rid of all OCR information.
|
||||
This command also removes OCR generated by third party tools.
|
||||
|
||||
Optimize images without performing OCR
|
||||
--------------------------------------
|
||||
|
||||
@@ -393,4 +407,4 @@ any digital signatures will be invalidated.
|
||||
OCRmyPDF cannot open documents that are encrypted with a digital certificate.
|
||||
|
||||
Versions of OCRmyPDF prior to 14.4.0 would invalidate existing digital signatures
|
||||
without warning.
|
||||
without warning.
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ execute the image:
|
||||
- Architecture
|
||||
- Description
|
||||
* - ``jbarlow83/ocrmypdf-alpine``
|
||||
- x86_64 only
|
||||
- x86_64 and arm64
|
||||
- Recommended image, based on Alpine Linux.
|
||||
* - ``jbarlow83/ocrmypdf-ubuntu``
|
||||
- x86_64 and arm64
|
||||
@@ -81,7 +81,7 @@ To start a Docker container (instance of the image):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
docker tag jbarlow83/ocrmypdf-alpine ocrmypdf
|
||||
docker run --rm -i ocrmypdf (... all other arguments here...) - -
|
||||
|
||||
For convenience, create a shell alias to hide the Docker command. It is
|
||||
|
||||
+85
-47
@@ -23,7 +23,9 @@ These platforms have one-liner installs:
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| Fedora | ``dnf install ocrmypdf tesseract-osd`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| macOS | ``brew install ocrmypdf`` |
|
||||
| macOS (Homebrew) | ``brew install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| macOS (MacPorts) | ``port install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
+-------------------------------+-----------------------------------------+
|
||||
@@ -99,12 +101,12 @@ For full details on version availability for your platform, check the
|
||||
Fedora
|
||||
------
|
||||
|
||||
.. |fedora-37| image:: https://repology.org/badge/version-for-repo/fedora_37/ocrmypdf.svg
|
||||
:alt: Fedora 37
|
||||
|
||||
.. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
|
||||
:alt: Fedora 38
|
||||
|
||||
.. |fedora-39| image:: https://repology.org/badge/version-for-repo/fedora_39/ocrmypdf.svg
|
||||
:alt: Fedora 39
|
||||
|
||||
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
|
||||
:alt: Fedore Rawhide
|
||||
|
||||
@@ -113,7 +115,7 @@ Fedora
|
||||
+-----------------------------------------------+
|
||||
| |latest| |
|
||||
+-----------------------------------------------+
|
||||
| |fedora-37| |fedora-38| |fedora-rawhide| |
|
||||
| |fedora-38| |fedora-39| |fedora-rawhide| |
|
||||
+-----------------------------------------------+
|
||||
|
||||
Users of Fedora may simply
|
||||
@@ -123,7 +125,7 @@ Users of Fedora may simply
|
||||
dnf install ocrmypdf tesseract-osd
|
||||
|
||||
For full details on version availability, check the `Fedora Package
|
||||
Tracker <https://apps.fedoraproject.org/packages/ocrmypdf>`__.
|
||||
Tracker <https://packages.fedoraproject.org/pkgs/ocrmypdf/ocrmypdf/>`__.
|
||||
|
||||
If the version available for your platform is out of date, you could opt
|
||||
to install the latest version from source. See `Installing HEAD revision
|
||||
@@ -135,10 +137,36 @@ from sources <#installing-head-revision-from-sources>`__.
|
||||
issues. OCRmyPDF works fine without it but will produce larger output
|
||||
files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later
|
||||
will automatically detect it on the ``PATH``. To add JBIG2 encoding,
|
||||
see `Installing the JBIG2 encoder <jbig2>`__.
|
||||
see :ref:`Installing the JBIG2 encoder <jbig2>`.
|
||||
|
||||
.. _ubuntu-lts-latest:
|
||||
|
||||
RHEL 9
|
||||
------
|
||||
|
||||
Prepare the environment by getting Python 3.11:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
dnf install python3.11 python3.11-pip
|
||||
|
||||
Then, follow `Requirements for pip and HEAD install <#requirements-for-pip-and-head-install>`__ to instal dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
dnf install ghostscript tesseract
|
||||
|
||||
and build ocrmypdf in virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3.11 -m venv .venv
|
||||
|
||||
To add JBIG2 encoding, see :ref:`Installing the JBIG2 encoder <jbig2>`.
|
||||
|
||||
Note Fedora packages for language data haven't been branched for RHEL/EPEL, but you can get traineddata files directly from `tesseract
|
||||
<https://github.com/tesseract-ocr/tessdata/>`__ and place them in ``/usr/share/tesseract/tessdata``.
|
||||
|
||||
Installing the latest version on Ubuntu 22.04 LTS
|
||||
-------------------------------------------------
|
||||
|
||||
@@ -162,37 +190,13 @@ 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:
|
||||
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. The
|
||||
most convenient way to install recent OCRmyPDF on older Ubuntu is to use
|
||||
Homebrew on Linux (Linuxbrew).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install \
|
||||
ghostscript \
|
||||
icc-profiles-free \
|
||||
libxml2 \
|
||||
pngquant \
|
||||
python3-pip \
|
||||
tesseract-ocr \
|
||||
zlib1g
|
||||
|
||||
To install ocrmypdf for the system:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf
|
||||
|
||||
To install for the current user only:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export PATH=$HOME/.local/bin:$PATH
|
||||
pip3 install --user ocrmypdf
|
||||
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
brew install ocrmypdf
|
||||
|
||||
Arch Linux (AUR)
|
||||
----------------
|
||||
@@ -215,7 +219,7 @@ you are using a VM image, such as `the official Vagrant image
|
||||
be completed for you.
|
||||
|
||||
Next you should install the `base-devel package group
|
||||
<https://www.archlinux.org/groups/x86_64/base-devel/>`__. This includes the
|
||||
<https://archlinux.org/packages/core/any/base-devel/>`__. This includes the
|
||||
standard tooling needed to build packages, such as a compiler and binary tools.
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -258,7 +262,7 @@ page.
|
||||
<https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed
|
||||
using the same series of steps as for the installation OCRmyPDF AUR
|
||||
package. Alternatively, it may be built manually from source following the
|
||||
instructions in `Installing the JBIG2 encoder <jbig2>`__. If JBIG2 is
|
||||
instructions in :ref:`Installing the JBIG2 encoder <jbig2>`. If JBIG2 is
|
||||
installed, OCRmyPDF 7.0.0 and later will automatically detect it.
|
||||
|
||||
Alpine Linux
|
||||
@@ -323,6 +327,22 @@ languages you can optionally install them all:
|
||||
|
||||
brew install tesseract-lang # Optional: Install all language packs
|
||||
|
||||
MacPorts
|
||||
--------
|
||||
|
||||
.. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fports.macports.org%2Fapi%2Fv1%2Fports%2Focrmypdf%2F%3Fformat%3Djson&query=version&label=MacPorts
|
||||
:alt: Macports Version Information
|
||||
:target: https://ports.macports.org/port/ocrmypdf
|
||||
|
||||
OCRmyPDF is includes in MacPorts:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo port install ocrmypdf
|
||||
|
||||
Note that while this will install tesseract you will need to install
|
||||
the appropriate tesseract `language ports <https://ports.macports.org/search/?selected_facets=categories_exact%3Atextproc&installed_file=&q=tesseract&name=on>`__.
|
||||
|
||||
Manual installation on macOS
|
||||
----------------------------
|
||||
|
||||
@@ -391,18 +411,20 @@ package manager:
|
||||
|
||||
* ``winget install -e --id Python.Python.3.11``
|
||||
* ``winget install -e --id UB-Mannheim.TesseractOCR``
|
||||
* ``winget install -e --id ArtifexSoftware.GhostScript``
|
||||
|
||||
You will need to install Ghostscript manually, `since it does not support automated
|
||||
installs anymore <https://artifex.com/news/ghostscript-10.01.0-disabling-silent-install-option>`_.
|
||||
|
||||
* `Ghostscript download page <https://ghostscript.com/releases/gsdnld.html>`_.`
|
||||
|
||||
(Or alternately, using the `Chocolatey <https://chocolatey.org/>`_ package manager, install
|
||||
the following when running in an Administrator command prompt):
|
||||
|
||||
* ``choco install python3``
|
||||
* ``choco install --pre tesseract``
|
||||
* ``choco install ghostscript``
|
||||
* ``choco install pngquant`` (optional)
|
||||
|
||||
Either set of commands will install the required software. At the mmoment there is no
|
||||
Either set of commands will install the required software. At the moment there is no
|
||||
single command to install Windows.
|
||||
|
||||
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
|
||||
@@ -452,7 +474,7 @@ Cygwin64
|
||||
|
||||
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
|
||||
|
||||
python39 (or later)
|
||||
python310 (or later)
|
||||
python3?-devel
|
||||
python3?-pip
|
||||
python3?-lxml
|
||||
@@ -538,9 +560,25 @@ try:
|
||||
|
||||
pip install --user ocrmypdf
|
||||
|
||||
(If the message appears ``Requirement already satisfied: ocrmypdf in...``,
|
||||
you will need to use ``pip install --user --upgrade ocrmypdf``.)
|
||||
|
||||
You should then be able to run ``ocrmypdf --version`` and see that the
|
||||
latest version was located.
|
||||
|
||||
Installing with pipx
|
||||
====================
|
||||
|
||||
Some users may prefer pipx. As with the method above, you will need to
|
||||
satisfy all non-Python dependencies. Then if pipx is installed, you
|
||||
can use
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pipx run ocrmypdf
|
||||
|
||||
(If not installed, pipx will install first.)
|
||||
|
||||
Requirements for pip and HEAD install
|
||||
-------------------------------------
|
||||
|
||||
@@ -550,8 +588,8 @@ manager. ``pip`` cannot provide them.
|
||||
|
||||
The following versions are required:
|
||||
|
||||
- Python 3.9 or newer
|
||||
- Ghostscript 9.55 or newer
|
||||
- Python 3.10 or newer
|
||||
- Ghostscript 9.54 or newer
|
||||
- Tesseract 4.1.1 or newer
|
||||
- jbig2enc 0.29 or newer
|
||||
- pngquant 2.5 or newer
|
||||
@@ -586,7 +624,7 @@ unfortunately, the ``pip install`` command cannot satisfy all of them.
|
||||
Installing HEAD revision from sources
|
||||
=====================================
|
||||
|
||||
If you have ``git`` and Python 3.9 or newer installed, you can install
|
||||
If you have ``git`` and Python 3.10 or newer installed, you can install
|
||||
from source. When the ``pip`` installer runs, it will alert you if
|
||||
dependencies are missing.
|
||||
|
||||
@@ -603,7 +641,7 @@ environment:
|
||||
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
|
||||
|
||||
Or, to install in `development
|
||||
mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
|
||||
mode <https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#working-in-development-mode>`__,
|
||||
allowing customization of OCRmyPDF, use the ``-e`` flag:
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -669,7 +707,7 @@ To manually install the ``fish`` completion, copy
|
||||
Note on 32-bit support
|
||||
======================
|
||||
|
||||
Many Python libraries no longer 32-bit binary wheels for Linux. This
|
||||
Many Python libraries no longer provide 32-bit binary wheels for Linux. This
|
||||
includes many of the libraries that OCRmyPDF depends on, such as
|
||||
Pillow. The easiest way to express this to end users is to say we don't
|
||||
support 32-bit Linux.
|
||||
@@ -679,4 +717,4 @@ can still install and use OCRmyPDF. A warning message will appear.
|
||||
In practice, OCRmyPDF may need more than 32-bit memory space to run when
|
||||
large documents are processed, so there are practical limitations to what
|
||||
users can accomplish with it. Still, for the common use case of an 32-bit
|
||||
ARM NAS or Raspberry Pi processing small documents, it should work.
|
||||
ARM NAS or Raspberry Pi processing small documents, it should work.
|
||||
|
||||
+9
-1
@@ -41,7 +41,15 @@ For all other platforms, you would need to build the JBIG2 encoder from source:
|
||||
|
||||
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
|
||||
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
|
||||
as libtool and leptonica-devel.
|
||||
as libtool and leptonica-devel. For this to work, please make sure to install
|
||||
``autotools``, ``automake``, ``libtool`` and ``leptonica`` first if not already
|
||||
installed.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
[sudo] apt install autotools-dev automake libtool libleptonica-dev
|
||||
..
|
||||
|
||||
|
||||
Lossy mode JBIG2
|
||||
================
|
||||
|
||||
+47
-13
@@ -18,16 +18,26 @@ Tesseract's documentation also lists the three-letter code for your language.
|
||||
Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others
|
||||
are not, e.g. German is ``deu`` and French is ``fra``.
|
||||
|
||||
Language packs (strictly speaking, Tesseract "traineddata" files) generally correspond
|
||||
to the language in question, but different language packs are used in certain
|
||||
situations. For German, the "Fraktur" language pack can assist with reading older
|
||||
materials in the Fraktur typeface family (``deu_frak``). Some communities have changed
|
||||
their script from Cyrillic to Latin; the Cyrillic version of Uzbek is available
|
||||
as ``uzb_cyrl`` and the Latin version is ``uzb``.
|
||||
|
||||
After you have installed a language pack, you can use it with ``ocrmypdf -l <language>``,
|
||||
for example ``ocrmypdf -l spa``. For multilingual documents, you can specify
|
||||
all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French.
|
||||
English is assumed by default unless other language(s) are specified.
|
||||
|
||||
For Linux users, you can often find packages that provide language
|
||||
packs:
|
||||
packs.
|
||||
|
||||
Debian and Ubuntu users
|
||||
=======================
|
||||
Platform install steps
|
||||
======================
|
||||
|
||||
Debian and Ubuntu (apt)
|
||||
-----------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -42,8 +52,8 @@ to what languages it should search for. Multiple languages can be
|
||||
requested using either ``-l eng+fra`` (English and French) or
|
||||
``-l eng -l fra``.
|
||||
|
||||
Fedora users
|
||||
============
|
||||
Fedora
|
||||
------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -58,8 +68,24 @@ to what languages it should search for. Multiple languages can be
|
||||
requested using either ``-l eng+fra`` (English and French) or
|
||||
``-l eng -l fra``.
|
||||
|
||||
Gentoo users
|
||||
============
|
||||
Archlinux
|
||||
------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Display a list of all Tesseract language packs
|
||||
pacman -Ss tesseract-data
|
||||
|
||||
# Install German language pack
|
||||
pacman -S tesseract-data-deu
|
||||
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
|
||||
to what languages it should search for. Multiple languages can be
|
||||
requested using either ``-l eng+fra`` (English and French) or
|
||||
``-l eng -l fra``.
|
||||
|
||||
Gentoo
|
||||
------
|
||||
|
||||
On Gentoo the package ``app-text/tessdata_fast``, which ``app-text/tesseract`` depends on, handles Tesseract languages.
|
||||
It accepts USE flags to select what languages should be installed, these can be set in ``/etc/portage/package.use``.
|
||||
@@ -85,23 +111,31 @@ to what languages it should search for. Multiple languages can be
|
||||
requested using either ``-l eng+fra`` (English and French) or
|
||||
``-l eng -l fra``.
|
||||
|
||||
macOS users
|
||||
===========
|
||||
macOS
|
||||
-----
|
||||
|
||||
You can install additional language packs by
|
||||
:ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
|
||||
|
||||
Docker users
|
||||
============
|
||||
Docker
|
||||
------
|
||||
|
||||
Users of the OCRmyPDF Docker image should install language packs into a
|
||||
derived Docker image as
|
||||
:ref:`described in that section <docker-lang-packs>`.
|
||||
|
||||
Windows users
|
||||
=============
|
||||
Windows
|
||||
-------
|
||||
|
||||
The Tesseract installer provided by Chocolatey currently includes only English language.
|
||||
To install other languages, download the respective language pack (``.traineddata`` file)
|
||||
from https://github.com/tesseract-ocr/tessdata/ and place it in
|
||||
``C:\\Program Files\\Tesseract-OCR\\tessdata`` (or wherever Tesseract OCR is installed).
|
||||
|
||||
Custom language packs
|
||||
=====================
|
||||
|
||||
If you have fine-tuned or trained Tesseract and generated custom trained data, you can
|
||||
copy your ``customlang.traineddata`` file into your Tesseract "tessdata" folder, and
|
||||
then use the ``-l customlang`` argument to tell OCRmyPDF to pass that language on to
|
||||
Tesseract.
|
||||
|
||||
+10
-1
@@ -64,4 +64,13 @@ installation documentation.
|
||||
|
||||
If you maintain a Linux distribution that supports 32-bit x86 or ARM, OCRmyPDF
|
||||
should continue to work as long as all of its dependencies continue to be
|
||||
available in 32-bit form. Please note we do not test on 32-bit platforms.
|
||||
available in 32-bit form. Please note we do not test on 32-bit platforms.
|
||||
|
||||
HEIF/HEIC
|
||||
---------
|
||||
|
||||
OCRmyPDF defaults to installing the pi-heif PyPI package, which supports converting
|
||||
HEIF (High Efficiency Image File Format) images to PDF from the command line.
|
||||
If your distribution does not have this library available, you can exclude it and
|
||||
OCRmyPDF will gracefully degrade automatically, losing only support for this
|
||||
feature.
|
||||
@@ -17,7 +17,7 @@ If running OCRmyPDF quickly is your main goal, you can use settings such as:
|
||||
|
||||
* ``--optimize 0`` to disable file size optimization
|
||||
* ``--output-type pdf`` to disable PDF/A generation
|
||||
* ``--fast-web-view 0`` to disable fast web view optimization
|
||||
* ``--fast-web-view 999999`` to disable fast web view optimization
|
||||
* ``--skip-big`` to skip large images, if some pages have large images
|
||||
|
||||
You can also avoid:
|
||||
|
||||
@@ -20,6 +20,8 @@ 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.
|
||||
|
||||
OCRmyPDF typically supports the three most recent Python versions.
|
||||
|
||||
.. note::
|
||||
|
||||
Attention maintainers: these release notes may be updated with information
|
||||
@@ -28,6 +30,160 @@ tagged yet.
|
||||
|
||||
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||
|
||||
v16.4.1
|
||||
=======
|
||||
|
||||
- Fixed calculation of image printed area (used in finding weighted DPI for OCR).
|
||||
:issue:`1334`
|
||||
- Fixed "NotImplementedError: not sure how to get colorspace" error
|
||||
messages in logs which simply records a failure to optimize images with
|
||||
print production colorspaces. :issue:`1315`
|
||||
|
||||
v16.4.0
|
||||
=======
|
||||
|
||||
- Selecting the ``osd`` and ``equ`` pseudo-languages with ``-l/--language`` now
|
||||
exits with an error when using Tesseract OCR, because these are not
|
||||
regular Tesseract languages but implementation details implemented.
|
||||
Using them can cause Tesseract to crash.
|
||||
- The hOCR renderer is more tolerant of extra whitespace in input files.
|
||||
- watcher.py now changes the output file extension to .pdf when the input is not
|
||||
.pdf.
|
||||
- Improved handling of PDFs that contain circularly referenced Form XObjects.
|
||||
:issue:`1321`
|
||||
- Fixed Alpine Docker image for ARM64, which was not building correctly.
|
||||
- Docker images now use pikepdf 9.0.0.
|
||||
- Prevent use of Tesseract OCR 5.4.0, a version with known regressions.
|
||||
- Disabled progressbar for "Linearizing" when ``--no-progress-bar`` set.
|
||||
- Fixed some tests that warn about missing JBIG2 decoding via pikepdf, by
|
||||
installing the necessary libraries during tests.
|
||||
|
||||
v16.3.1
|
||||
=======
|
||||
|
||||
- Fixed a test suite failure with Ghostscript 10.03.0+. :issue:`1316`
|
||||
- Fixed an issue with the presentation of the "OCR" progress bar. :issue:`1313`
|
||||
|
||||
v16.3.0
|
||||
=======
|
||||
|
||||
- Fixed progress bar not displaying for Ghostscript PDF/A conversion. :issue:`1313`
|
||||
- Added progress bar for linearization. :issue:`1313`
|
||||
- If `--rotate-pages-threshold` issued without `--rotate-pages` we now exit with
|
||||
an error since the user likely intended to use `--rotate-pages`. :issue:`1309`
|
||||
- If Tesseract hOCR gives an invalid line box, print an error message instead of
|
||||
exiting with an error. :issue:`1312`
|
||||
|
||||
v16.2.0
|
||||
=======
|
||||
|
||||
- Fixed issue 'NoneType' object has no attribute 'get' when optimizing certain PDFs.
|
||||
:issue:`1293,1271`
|
||||
- Switched formatting from black to ruff.
|
||||
- Added support for sending sidecar output to io.BytesIO.
|
||||
- Added support for converting HEIF/HEIC images (the native image of iPhones and
|
||||
some other devices) to PDFs, when the appropriate pi-hief library is installed.
|
||||
This library is marked as a dependency, but maintainers may opt out if needed.
|
||||
- We now default to downsampling large images that would exceed Tesseract's internal
|
||||
limits, but only if it cause processing to fail. Previously, this behavior only
|
||||
occurred if specifically requested on command line. It can still be configured
|
||||
and disabled. See the --tesseract command line options.
|
||||
- Added Macports install instructions. Thanks @akierig.
|
||||
- Improved logging output when an unexpected error occurs while trying to obtain
|
||||
the version of a third party program.
|
||||
|
||||
v16.1.2
|
||||
=======
|
||||
|
||||
- Fixed test suite failure when using Ghostscript 10.3.
|
||||
- Other minor corrections.
|
||||
|
||||
v16.1.1
|
||||
=======
|
||||
|
||||
- Fixed PyPy 3.10 support.
|
||||
|
||||
v16.1.0
|
||||
=======
|
||||
|
||||
- Improved hOCR renderer is now default for left to right languages.
|
||||
- Improved handling of rotated pages. Previously, OCR text might be missing for
|
||||
pages that were rotated with a /Rotate tag on the page entry.
|
||||
- Improved handling of cropped pages. Previously, in some cases a page with a
|
||||
crop box would not have its OCR applied correctly and misalignment between
|
||||
OCR text and visible text coudl occur.
|
||||
- Documentation improvements, especially installation instructions for less
|
||||
common platforms.
|
||||
|
||||
v16.0.4
|
||||
=======
|
||||
|
||||
- Fixed some issues for left-to-right text with the new hOCR renderer. It is still
|
||||
not default yet but will be made so soon. Right-to-left text is still in progress.
|
||||
- Added an error to prevent use of several versions of Ghostscript that seem
|
||||
corrupt existing text in input PDFs. Newly generated OCR is not affected.
|
||||
For best results, use Ghostscript 10.02.1 or newer, which contains the fix
|
||||
for the issue.
|
||||
|
||||
v16.0.3
|
||||
=======
|
||||
|
||||
- Changed minimum required Ghostscript to 9.54, to support users of RHEL 9 and its
|
||||
derivatives, since that is the latest version available there.
|
||||
- Removed warning message about CVE-2023-43115, on the assumption that most
|
||||
distributions have backported the patch by now.
|
||||
|
||||
v16.0.2
|
||||
=======
|
||||
|
||||
- Temporarily changed PDF text renderer back to sandwich by default to address
|
||||
regressions in macOS Preview.
|
||||
|
||||
v16.0.1
|
||||
=======
|
||||
|
||||
- Fixed text rendering issue with new hOCR text renderer - extraneous byte order
|
||||
marks.
|
||||
- Tightened dependencies.
|
||||
|
||||
v16.0.0
|
||||
=======
|
||||
|
||||
- Added OCR text renderer, combined the best ideas of Tesseract's PDF
|
||||
generator and the older hOCR transformer renderer. The result is a hopefully
|
||||
permanent fix for wordssmushedtogetherwithoutspaces issues in extracted text,
|
||||
better registration/position of text on skewed baselines :issue:`1009`,
|
||||
fixes to character output when the German Fraktur script is used :issue:`1191`,
|
||||
proper rendering of right to left languages (Arabic, Hebrew, Persian) :issue:`1157`.
|
||||
Asian languages may still have excessive word breaks compared to expectations.
|
||||
The new renderer is the default; the old sandwich renderer is still available
|
||||
using ``--pdf-renderer sandwich``; the old hOCR renderer is no more.
|
||||
- The ``ocrmypdf.hocrtransform`` API has changed substantially.
|
||||
- Support for Python 3.9 has been dropped. Python 3.10+ is now required.
|
||||
- pikepdf >= 8.8.0 is now required.
|
||||
|
||||
|
||||
v15.4.4
|
||||
=======
|
||||
|
||||
- Fixed documentation for installing Ghostscript on Windows. :issue:`1198`
|
||||
- Added warning message about security issue in older versions of Ghostscript.
|
||||
|
||||
v15.4.3
|
||||
=======
|
||||
|
||||
- Fixed deprecation warning in pikepdf older than 8.7.1; pikepdf >= 8.7.1 is
|
||||
now required.
|
||||
|
||||
v15.4.2
|
||||
=======
|
||||
|
||||
- We now raise an exception on a certain class of PDFs that likely need an
|
||||
explicit color conversion strategy selected to display correctly
|
||||
for PDF/A conversion.
|
||||
- Fixed an error that occurred while trying to write a log message after the
|
||||
debug log handler was removed.
|
||||
|
||||
v15.4.1
|
||||
=======
|
||||
|
||||
|
||||
+48
-10
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2016 findingorder <https://github.com/findingorder>
|
||||
# SPDX-FileCopyrightText: 2024 nilsro <https://github.com/nilsro>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""Example of using ocrmypdf as a library in a script.
|
||||
@@ -13,7 +14,11 @@ You should edit this script to meet your needs.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import filecmp
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,32 +27,65 @@ import ocrmypdf
|
||||
# pylint: disable=logging-format-interpolation
|
||||
# pylint: disable=logging-not-lazy
|
||||
|
||||
|
||||
def filecompare(a, b):
|
||||
try:
|
||||
return filecmp.cmp(a, b, shallow=True)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
script_dir = Path(__file__).parent
|
||||
# set archive_dir to a path for backup original documents. Leave empty if not required.
|
||||
archive_dir = "/pdfbak"
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = Path(sys.argv[1])
|
||||
else:
|
||||
start_dir = Path('.')
|
||||
start_dir = Path(".")
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
log_file = Path(sys.argv[2])
|
||||
else:
|
||||
log_file = script_dir.with_name('ocr-tree.log')
|
||||
log_file = script_dir.with_name("ocr-tree.log")
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
format="%(asctime)s %(message)s",
|
||||
filename=log_file,
|
||||
filemode='a',
|
||||
filemode="a",
|
||||
)
|
||||
|
||||
logging.info(f"Start directory {start_dir}")
|
||||
|
||||
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
||||
|
||||
for filename in start_dir.glob("**/*.py"):
|
||||
for filename in start_dir.glob("**/*.pdf"):
|
||||
logging.info(f"Processing {filename}")
|
||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||
if result == ocrmypdf.ExitCode.already_done_ocr:
|
||||
logging.error("Skipped document because it already contained text")
|
||||
elif result == ocrmypdf.ExitCode.ok:
|
||||
if ocrmypdf.pdfa.file_claims_pdfa(filename)["pass"]:
|
||||
logging.info("Skipped document because it already contained text")
|
||||
else:
|
||||
archive_filename = archive_dir + str(filename)
|
||||
if len(archive_dir) > 0 and not filecompare(filename, archive_filename):
|
||||
logging.info(f"Archiving document to {archive_filename}")
|
||||
try:
|
||||
shutil.copy2(filename, posixpath.dirname(archive_filename))
|
||||
except OSError:
|
||||
os.makedirs(posixpath.dirname(archive_filename))
|
||||
shutil.copy2(filename, posixpath.dirname(archive_filename))
|
||||
try:
|
||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||
logging.info(result)
|
||||
except ocrmypdf.exceptions.EncryptedPdfError:
|
||||
logging.info("Skipped document because it is encrypted")
|
||||
except ocrmypdf.exceptions.PriorOcrFoundError:
|
||||
logging.info("Skipped document because it already contained text")
|
||||
except ocrmypdf.exceptions.DigitalSignatureError:
|
||||
logging.info("Skipped document because it has a digital signature")
|
||||
except ocrmypdf.exceptions.TaggedPDFError:
|
||||
logging.info(
|
||||
"Skipped document because it does not need ocr as it is tagged"
|
||||
)
|
||||
except Exception:
|
||||
logging.error("Unhandled error occured")
|
||||
logging.info("OCR complete")
|
||||
logging.info(result)
|
||||
|
||||
+4
-3
@@ -53,9 +53,10 @@ for dir_name, _subdirs, file_list in os.walk(start_dir):
|
||||
]
|
||||
logging.info(cmd)
|
||||
full_path_ocr = os.path.join(dir_name, filename_ocr)
|
||||
with open(filename, 'rb') as input_file, open(
|
||||
full_path_ocr, 'wb'
|
||||
) as output_file:
|
||||
with (
|
||||
open(filename, 'rb') as input_file,
|
||||
open(full_path_ocr, 'wb') as output_file,
|
||||
):
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
stdin=input_file,
|
||||
|
||||
+9
-7
@@ -7,7 +7,6 @@
|
||||
|
||||
# Do not enable annotations!
|
||||
# https://github.com/tiangolo/typer/discussions/598
|
||||
# from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
@@ -47,15 +46,18 @@ class LoggingLevelEnum(str, Enum):
|
||||
CRITICAL = "CRITICAL"
|
||||
|
||||
|
||||
def get_output_dir(root: Path, basename: str, output_dir_year_month: bool) -> Path:
|
||||
def get_output_path(root: Path, basename: str, output_dir_year_month: bool) -> Path:
|
||||
assert '/' not in basename, "basename must not contain '/'"
|
||||
if output_dir_year_month:
|
||||
today = datetime.today()
|
||||
output_directory_year_month = root / str(today.year) / f'{today.month:02d}'
|
||||
if not output_directory_year_month.exists():
|
||||
output_directory_year_month.mkdir(parents=True, exist_ok=True)
|
||||
output_path = Path(output_directory_year_month) / basename
|
||||
output_path = Path(output_directory_year_month) / Path(basename).with_suffix(
|
||||
'.pdf'
|
||||
)
|
||||
else:
|
||||
output_path = root / basename
|
||||
output_path = root / Path(basename).with_suffix('.pdf')
|
||||
return output_path
|
||||
|
||||
|
||||
@@ -99,7 +101,7 @@ def execute_ocrmypdf(
|
||||
retries_loading_file: int,
|
||||
output_dir_year_month: bool,
|
||||
):
|
||||
output_path = get_output_dir(output_dir, file_path.name, output_dir_year_month)
|
||||
output_path = get_output_path(output_dir, file_path.name, output_dir_year_month)
|
||||
|
||||
log.info("-" * 20)
|
||||
log.info(f'New file: {file_path}. Waiting until fully written...')
|
||||
@@ -131,7 +133,7 @@ def execute_ocrmypdf(
|
||||
|
||||
|
||||
class HandleObserverEvent(PatternMatchingEventHandler):
|
||||
def __init__(
|
||||
def __init__( # noqa: D107
|
||||
self,
|
||||
patterns=None,
|
||||
ignore_patterns=None,
|
||||
@@ -191,7 +193,7 @@ def main(
|
||||
bool,
|
||||
typer.Option(
|
||||
envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH',
|
||||
help='Create a subdirectory in the output directory for each year and month',
|
||||
help='Create a subdirectory in the output directory for each year/month',
|
||||
),
|
||||
] = False,
|
||||
on_success_delete: Annotated[
|
||||
|
||||
+24
-39
@@ -10,18 +10,17 @@ dynamic = ["version"]
|
||||
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
|
||||
readme = "README.md"
|
||||
license = { text = "MPL-2.0" }
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"Pillow>=10.0.1",
|
||||
"deprecation>=2.1.0",
|
||||
"img2pdf>=0.4.4",
|
||||
"img2pdf>=0.5",
|
||||
"packaging>=20",
|
||||
"pdfminer.six>=20220319",
|
||||
"pikepdf>=8",
|
||||
"pluggy>=0.13.0",
|
||||
"reportlab>=3.6.8",
|
||||
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
|
||||
"pikepdf>=8.10.1",
|
||||
"Pillow>=10.0.1",
|
||||
"pluggy>=1",
|
||||
"rich>=13",
|
||||
"typing-extensions>=4;python_version<'3.10'",
|
||||
]
|
||||
authors = [{ name = "James R. Barlow", email = "james@purplerock.ca" }]
|
||||
classifiers = [
|
||||
@@ -58,10 +57,11 @@ test = [
|
||||
"pytest-cov>=3.0.0",
|
||||
"pytest-xdist>=2.5.0",
|
||||
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
|
||||
"reportlab>=3.6.8",
|
||||
"types-Pillow",
|
||||
"types-humanfriendly",
|
||||
]
|
||||
watcher = ["watchdog>=1.0.2", "typer[all]", "python-dotenv"]
|
||||
watcher = ["watchdog>=1.0.2", "typer-slim[standard]", "python-dotenv"]
|
||||
webservice = ["Flask>=2.0.1"]
|
||||
|
||||
[project.scripts]
|
||||
@@ -77,30 +77,7 @@ namespaces = false
|
||||
[tool.setuptools_scm]
|
||||
|
||||
[tool.distutils.bdist_wheel]
|
||||
python-tag = "py39"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py39", "py310", "py311"]
|
||||
skip-string-normalization = true
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
| docs
|
||||
| misc
|
||||
| \.egg-info
|
||||
)/
|
||||
'''
|
||||
python-tag = "py310"
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
@@ -132,7 +109,11 @@ norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-n auto"
|
||||
markers = ["slow"]
|
||||
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
|
||||
filterwarnings = [
|
||||
"ignore:.*XMLParser.*:DeprecationWarning",
|
||||
"ignore:.*ast.NameConstant.*:DeprecationWarning:reportlab",
|
||||
"ignore:.*distutils.*:DeprecationWarning:libxmp",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
|
||||
@@ -150,7 +131,10 @@ module = [
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
"select" = [
|
||||
"D", # pydocstyle
|
||||
"E", # pycodestyle
|
||||
"W", # pycodestyle
|
||||
@@ -158,17 +142,18 @@ select = [
|
||||
"I001", # isort
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
target-version = "py39"
|
||||
|
||||
[tool.ruff.isort]
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["ocrmypdf"]
|
||||
required-imports = ["from __future__ import annotations"]
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"docs/conf.py" = ["D100", "D101", "D105"]
|
||||
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
|
||||
"misc/*.py" = ["D103", "D101", "D102"]
|
||||
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "preserve"
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ architectures: [amd64]
|
||||
|
||||
environment:
|
||||
TESSDATA_PREFIX: $SNAP/usr/share/tesseract-ocr/4.00/tessdata
|
||||
GS_LIB: $SNAP/usr/share/ghostscript/9.55/Resource/Init
|
||||
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55/Resource/Font
|
||||
GS_LIB: $SNAP/usr/share/ghostscript/9.55.0/Resource/Init
|
||||
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55.0/Resource/Font
|
||||
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
|
||||
|
||||
apps:
|
||||
|
||||
@@ -7,8 +7,8 @@ from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable
|
||||
from typing import Callable, TypeVar
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from ocrmypdf._progressbar import NullProgressBar, ProgressBar
|
||||
|
||||
@@ -19,6 +19,10 @@ def _task_noop(*_args, **_kwargs):
|
||||
return
|
||||
|
||||
|
||||
def _task_finished_noop(_result: Any, pbar: ProgressBar):
|
||||
pbar.update()
|
||||
|
||||
|
||||
class Executor(ABC):
|
||||
"""Abstract concurrent executor."""
|
||||
|
||||
@@ -66,7 +70,7 @@ class Executor(ABC):
|
||||
if not worker_initializer:
|
||||
worker_initializer = _task_noop
|
||||
if not task_finished:
|
||||
task_finished = _task_noop
|
||||
task_finished = _task_finished_noop
|
||||
if not task:
|
||||
task = _task_noop
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2024 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
# Enforce English hegemony
|
||||
DEFAULT_LANGUAGE = 'eng'
|
||||
|
||||
# Default rotation threshold
|
||||
DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import deque
|
||||
from io import BytesIO
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
@@ -16,7 +17,7 @@ from subprocess import PIPE, CalledProcessError
|
||||
from packaging.version import Version
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from ocrmypdf.exceptions import SubprocessOutputError
|
||||
from ocrmypdf.exceptions import ColorConversionNeededError, SubprocessOutputError
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
|
||||
|
||||
@@ -29,38 +30,44 @@ COLOR_CONVERSION_STRATEGIES = frozenset(
|
||||
'UseDeviceIndependentColor',
|
||||
]
|
||||
)
|
||||
# Ghostscript executable - gswin32c is not supported
|
||||
GS = 'gswin64c' if os.name == 'nt' else 'gs'
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DuplicateFilter(logging.Filter):
|
||||
"""Filter out duplicate log messages."""
|
||||
"""Filter out duplicate log messages.
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.last: logging.LogRecord | None = None
|
||||
self.count = 0
|
||||
A context window of default 5 messages is used to determine if a message is a
|
||||
duplicate. This is because some Ghostscript messages are word wrapped.
|
||||
"""
|
||||
|
||||
def __init__(self, logger: logging.Logger, context_window=5):
|
||||
self.window: deque[str] = deque([], maxlen=context_window)
|
||||
self.logger = logger
|
||||
self.levelno = logging.DEBUG
|
||||
self.count = 0
|
||||
|
||||
def filter(self, record):
|
||||
if self.last and record.msg == self.last.msg:
|
||||
if record.msg in self.window:
|
||||
self.count += 1
|
||||
self.levelno = record.levelno
|
||||
return False
|
||||
else:
|
||||
if self.count >= 1:
|
||||
rep_msg = f"(previous message repeated {self.count} times)"
|
||||
rep_msg = f"(suppressed {self.count} repeated lines)"
|
||||
self.count = 0 # Avoid infinite recursion
|
||||
self.logger.log(self.last.levelno, rep_msg)
|
||||
self.last = record
|
||||
self.logger.log(self.levelno, rep_msg)
|
||||
self.window.clear()
|
||||
self.window.append(record.msg)
|
||||
return True
|
||||
|
||||
|
||||
log.addFilter(DuplicateFilter(log))
|
||||
|
||||
|
||||
# Ghostscript executable - gswin32c is not supported
|
||||
GS = 'gswin64c' if os.name == 'nt' else 'gs'
|
||||
|
||||
|
||||
def version() -> Version:
|
||||
return Version(get_version(GS))
|
||||
|
||||
@@ -70,6 +77,20 @@ def _gs_error_reported(stream) -> bool:
|
||||
return bool(match)
|
||||
|
||||
|
||||
def _gs_devicen_reported(stream) -> bool:
|
||||
"""Did Ghostscript warn about a DeviceN with inappropriate alternate?
|
||||
|
||||
If so, we need the user to select a color conversion, or the resulting PDF will
|
||||
not present correctly in some PDF viewers.
|
||||
"""
|
||||
match = re.search(
|
||||
r'DeviceN.*inappropriate alternate',
|
||||
stream,
|
||||
flags=re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
return bool(match)
|
||||
|
||||
|
||||
def rasterize_pdf(
|
||||
input_file: os.PathLike,
|
||||
output_file: os.PathLike,
|
||||
@@ -156,6 +177,17 @@ class GhostscriptFollower:
|
||||
self.progressbar_class = progressbar_class
|
||||
self.progressbar = None
|
||||
|
||||
def __enter__(self):
|
||||
# We can't actually set up the progressbar here, because we don't know
|
||||
# how many pages there are until the first __call__() happens. So we
|
||||
# do it in __call__().
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.progressbar:
|
||||
return self.progressbar.__exit__(exc_type, exc_value, traceback)
|
||||
return False
|
||||
|
||||
def __call__(self, line):
|
||||
if not self.progressbar_class:
|
||||
return
|
||||
@@ -166,7 +198,8 @@ class GhostscriptFollower:
|
||||
self.progressbar = self.progressbar_class(
|
||||
total=self.count, desc="PDF/A conversion", unit='page'
|
||||
)
|
||||
return
|
||||
# Now that we know the count, we can set up the progressbar.
|
||||
self.progressbar.__enter__()
|
||||
else:
|
||||
if self.re_page.match(line.strip()):
|
||||
self.progressbar.update()
|
||||
@@ -243,9 +276,11 @@ def generate_pdfa(
|
||||
]
|
||||
)
|
||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||
|
||||
try:
|
||||
with Path(output_file).open('wb') as output:
|
||||
with (
|
||||
Path(output_file).open('wb') as output,
|
||||
GhostscriptFollower(progressbar_class) as pbar,
|
||||
):
|
||||
p = run_polling_stderr(
|
||||
args_gs,
|
||||
stdout=output,
|
||||
@@ -254,7 +289,7 @@ def generate_pdfa(
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
callback=GhostscriptFollower(progressbar_class),
|
||||
callback=pbar,
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
@@ -272,3 +307,5 @@ def generate_pdfa(
|
||||
# the **** pattern to split the stderr into parts.
|
||||
for part in stderr.split('****'):
|
||||
log.error(part)
|
||||
if _gs_devicen_reported(stderr):
|
||||
raise ColorConversionNeededError()
|
||||
|
||||
@@ -220,7 +220,8 @@ def get_deskew(
|
||||
|
||||
def tesseract_log_output(stream: bytes) -> None:
|
||||
tlog = TesseractLoggerAdapter(
|
||||
log, extra=log.extra if hasattr(log, 'extra') else None # type: ignore
|
||||
log,
|
||||
extra=log.extra if hasattr(log, 'extra') else None, # type: ignore
|
||||
)
|
||||
|
||||
if not stream:
|
||||
|
||||
@@ -8,48 +8,26 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT
|
||||
from typing import Union
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from packaging.version import Version
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from ocrmypdf.exceptions import SubprocessOutputError
|
||||
from ocrmypdf.subprocess import get_version, run
|
||||
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/main/doc/basic-concepts.md
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from tempfile import TemporaryDirectory
|
||||
else:
|
||||
from tempfile import TemporaryDirectory as _TemporaryDirectory
|
||||
|
||||
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)
|
||||
|
||||
del _TemporaryDirectory
|
||||
|
||||
|
||||
UNPAPER_IMAGE_PIXEL_LIMIT = 256 * 1024 * 1024
|
||||
|
||||
DecFloat = Union[Decimal, float]
|
||||
DecFloat = Decimal | float
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
+16
-8
@@ -12,12 +12,12 @@ from pathlib import Path
|
||||
|
||||
from pikepdf import (
|
||||
Dictionary,
|
||||
Matrix,
|
||||
Name,
|
||||
Operator,
|
||||
Page,
|
||||
Pdf,
|
||||
PdfError,
|
||||
PdfMatrix,
|
||||
Stream,
|
||||
parse_content_stream,
|
||||
unparse_content_stream,
|
||||
@@ -268,13 +268,13 @@ class OcrGrafter:
|
||||
mediabox = base_page.mediabox
|
||||
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
|
||||
|
||||
translate = PdfMatrix().translated(-wt / 2, -ht / 2)
|
||||
untranslate = PdfMatrix().translated(wp / 2, hp / 2)
|
||||
corner = PdfMatrix().translated(mediabox[0], mediabox[1])
|
||||
translate = Matrix().translated(-wt / 2, -ht / 2)
|
||||
untranslate = Matrix().translated(wp / 2, hp / 2)
|
||||
corner = Matrix().translated(mediabox[0], mediabox[1])
|
||||
# -rotation because the input is a clockwise angle and this formula
|
||||
# uses CCW
|
||||
text_rotation = -text_rotation % 360
|
||||
rotate = PdfMatrix().rotated(text_rotation)
|
||||
rotate = Matrix().rotated(text_rotation)
|
||||
|
||||
# Because of rounding of DPI, we might get a text layer that is not
|
||||
# identically sized to the target page. Scale to adjust. Normally this
|
||||
@@ -285,12 +285,13 @@ class OcrGrafter:
|
||||
scale_y = hp / ht
|
||||
|
||||
# log.debug('%r', scale_x, scale_y)
|
||||
scale = PdfMatrix().scaled(scale_x, scale_y)
|
||||
scale = Matrix().scaled(scale_x, scale_y)
|
||||
|
||||
# Translate the text so it is centered at (0, 0), rotate it there, adjust
|
||||
# for a size different between initial and text PDF, then untranslate, and
|
||||
# finally move the lower left corner to match the mediabox
|
||||
# finally move the lower left corner to match the mediabox.
|
||||
ctm = translate @ rotate @ scale @ untranslate @ corner
|
||||
log.debug("Grafting with ctm %r", ctm)
|
||||
|
||||
base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
|
||||
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
|
||||
@@ -310,9 +311,16 @@ class OcrGrafter:
|
||||
|
||||
if strip_old_text:
|
||||
strip_invisible_text(self.pdf_base, base_page)
|
||||
|
||||
base_page.contents_coalesce()
|
||||
if self.render_mode == RenderMode.ON_TOP:
|
||||
# Add q/Q to ensure content we append is drawn correctly
|
||||
# Strictly speaking this needs to trace the whole q/Q stack in case
|
||||
# stack is not balanced.
|
||||
original = base_page.Contents.read_bytes()
|
||||
base_page.Contents.write(b'q\n' + original + b'\nQ\n')
|
||||
base_page.contents_add(
|
||||
new_text_layer, prepend=self.render_mode == RenderMode.UNDERNEATH
|
||||
)
|
||||
base_page.contents_coalesce()
|
||||
|
||||
_update_resources(obj=base_page.obj, font=font, font_key=font_key)
|
||||
|
||||
@@ -93,8 +93,8 @@ class PageContext:
|
||||
state = self.__dict__.copy()
|
||||
|
||||
state['options'] = copy(self.options)
|
||||
if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)):
|
||||
if not isinstance(state['options'].input_file, str | bytes | os.PathLike):
|
||||
state['options'].input_file = 'stream'
|
||||
if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)):
|
||||
if not isinstance(state['options'].output_file, str | bytes | os.PathLike):
|
||||
state['options'].output_file = 'stream'
|
||||
return state
|
||||
|
||||
@@ -153,22 +153,52 @@ def _set_language(pdf: Pdf, languages: list[str]):
|
||||
pdf.Root.Lang = iso639_2
|
||||
|
||||
|
||||
class MetadataProgress:
|
||||
def __init__(self, progressbar_class, enable: bool = True):
|
||||
self.progressbar_class = progressbar_class
|
||||
self.progressbar = self.progressbar_class(
|
||||
total=100, desc="Linearizing", unit='%', disable=not enable
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
self.progressbar.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
return self.progressbar.__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
def __call__(self, percent: int):
|
||||
if not self.progressbar_class:
|
||||
return
|
||||
self.progressbar.update(completed=percent)
|
||||
|
||||
|
||||
def metadata_fixup(
|
||||
working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
|
||||
) -> Path:
|
||||
"""Fix certain metadata fields after Ghostscript PDF/A conversion.
|
||||
"""Fix certain metadata fields whether PDF or PDF/A.
|
||||
|
||||
Override some of Ghostscript's metadata choices.
|
||||
|
||||
Also report on metadata in the input file that was not retained during
|
||||
PDF/A conversion.
|
||||
conversion.
|
||||
"""
|
||||
output_file = context.get_path('metafix.pdf')
|
||||
options = context.options
|
||||
|
||||
with Pdf.open(context.origin) as original, Pdf.open(working_file) as pdf:
|
||||
pbar_class = context.plugin_manager.hook.get_progressbar_class()
|
||||
with (
|
||||
Pdf.open(context.origin) as original,
|
||||
Pdf.open(working_file) as pdf,
|
||||
MetadataProgress(pbar_class, options.progress_bar) as pbar,
|
||||
):
|
||||
docinfo = get_docinfo(original, context)
|
||||
with original.open_metadata(
|
||||
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
|
||||
) as meta_original, pdf.open_metadata() as meta_pdf:
|
||||
with (
|
||||
original.open_metadata(
|
||||
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
|
||||
) as meta_original,
|
||||
pdf.open_metadata() as meta_pdf,
|
||||
):
|
||||
meta_pdf.load_from_docinfo(
|
||||
docinfo, delete_missing=False, raise_failure=False
|
||||
)
|
||||
@@ -179,6 +209,6 @@ def metadata_fixup(
|
||||
report_on_metadata(options, meta_missing)
|
||||
|
||||
_set_language(pdf, options.languages)
|
||||
pdf.save(output_file, **pdf_save_settings)
|
||||
pdf.save(output_file, progress=pbar, **pdf_save_settings)
|
||||
|
||||
return output_file
|
||||
|
||||
+101
-23
@@ -12,8 +12,9 @@ import re
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from contextlib import suppress
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj, copystat
|
||||
from shutil import copyfileobj
|
||||
from typing import Any, BinaryIO, TypeVar, cast
|
||||
|
||||
import img2pdf
|
||||
@@ -34,17 +35,29 @@ from ocrmypdf.exceptions import (
|
||||
UnsupportedImageFormatError,
|
||||
)
|
||||
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
|
||||
from ocrmypdf.hocrtransform import HocrTransform
|
||||
from ocrmypdf.hocrtransform import DebugRenderOptions, HocrTransform
|
||||
from ocrmypdf.hocrtransform._font import Courier
|
||||
from ocrmypdf.pdfa import generate_pdfa_ps
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
|
||||
from ocrmypdf.pluginspec import OrientationConfidence
|
||||
|
||||
try:
|
||||
from pi_heif import register_heif_opener
|
||||
except ImportError:
|
||||
|
||||
def register_heif_opener():
|
||||
pass
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
VECTOR_PAGE_DPI = 400
|
||||
|
||||
|
||||
register_heif_opener()
|
||||
|
||||
|
||||
def triage_image_file(input_file: Path, output_file: Path, options) -> None:
|
||||
"""Triage the input image file.
|
||||
|
||||
@@ -129,7 +142,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
|
||||
"""
|
||||
with open(input_file, 'rb') as f:
|
||||
signature = f.read(search_window)
|
||||
m = re.search(br'%PDF-(\d\.\d)', signature)
|
||||
m = re.search(rb'%PDF-(\d\.\d)', signature)
|
||||
if m:
|
||||
return m.group(1).decode('ascii')
|
||||
return ''
|
||||
@@ -462,7 +475,7 @@ def calculate_raster_dpi(page_context: PageContext):
|
||||
page_dpi = get_page_square_dpi(page_context, image_dpi)
|
||||
if dpi_profile and dpi_profile.average_to_max_dpi_ratio < 0.8:
|
||||
log.warning(
|
||||
"Weight average image DPI is %0.1f, max DPI is %0.1f. "
|
||||
"Weighted average image DPI is %0.1f, max DPI is %0.1f. "
|
||||
"The discrepancy may indicate a high detail region on this page, "
|
||||
"but could also indicate a problem with the input PDF file. "
|
||||
"Page image will be rendered at %0.1f DPI.",
|
||||
@@ -712,19 +725,29 @@ def create_pdf_page_from_image(
|
||||
pageinfo = page_context.pageinfo
|
||||
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
|
||||
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
|
||||
if effective_rotation % 180 == 90:
|
||||
swap_axis = effective_rotation % 180 == 90
|
||||
if swap_axis:
|
||||
pagesize = pagesize[1], pagesize[0]
|
||||
|
||||
# This create a single page PDF
|
||||
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
|
||||
# Create a new single page PDF to hold
|
||||
bio = BytesIO()
|
||||
with open(image, 'rb') as imfile:
|
||||
log.debug('convert')
|
||||
|
||||
layout_fun = img2pdf.get_layout_fun(pagesize)
|
||||
img2pdf.convert(
|
||||
imfile, layout_fun=layout_fun, outputstream=pdf, **IMG2PDF_KWARGS
|
||||
imfile,
|
||||
layout_fun=layout_fun,
|
||||
outputstream=bio,
|
||||
engine=img2pdf.Engine.pikepdf,
|
||||
rotation=img2pdf.Rotation.ifvalid,
|
||||
)
|
||||
log.debug('convert done')
|
||||
|
||||
# img2pdf does not generate boxes correctly, so we fix them
|
||||
bio.seek(0)
|
||||
fix_pagepdf_boxes(bio, output_file, page_context, swap_axis=swap_axis)
|
||||
|
||||
output_file = page_context.plugin_manager.hook.filter_pdf_page(
|
||||
page=page_context, image_filename=image, output_pdf=output_file
|
||||
)
|
||||
@@ -741,15 +764,27 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
|
||||
return output_file
|
||||
|
||||
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
|
||||
debug_mode = options.pdf_renderer == 'hocrdebug'
|
||||
|
||||
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.to_scalar()) # square
|
||||
hocrtransform.to_pdf(
|
||||
debug_kwargs = {}
|
||||
if options.pdf_renderer == 'hocrdebug':
|
||||
debug_kwargs = dict(
|
||||
debug_render_options=DebugRenderOptions(
|
||||
render_baseline=True,
|
||||
render_triangle=True,
|
||||
render_line_bbox=False,
|
||||
render_word_bbox=True,
|
||||
render_paragraph_bbox=False,
|
||||
render_space_bbox=False,
|
||||
),
|
||||
font=Courier(),
|
||||
)
|
||||
HocrTransform(
|
||||
hocr_filename=hocr,
|
||||
dpi=dpi.to_scalar(),
|
||||
**debug_kwargs, # square
|
||||
).to_pdf(
|
||||
out_filename=output_file,
|
||||
image_filename=None,
|
||||
show_bounding_boxes=False if not debug_mode else True,
|
||||
invisible_text=True if not debug_mode else False,
|
||||
interword_spaces=True,
|
||||
invisible_text=True if not debug_kwargs else False,
|
||||
)
|
||||
return output_file
|
||||
|
||||
@@ -769,7 +804,57 @@ def ocr_engine_textonly_pdf(
|
||||
output_text=output_text,
|
||||
options=options,
|
||||
)
|
||||
return (output_pdf, output_text)
|
||||
return output_pdf, output_text
|
||||
|
||||
|
||||
def _offset_rect(rect: tuple[float, float, float, float], offset: tuple[float, float]):
|
||||
"""Offset a rectangle by a given amount."""
|
||||
return (
|
||||
rect[0] + offset[0],
|
||||
rect[1] + offset[1],
|
||||
rect[2] + offset[0],
|
||||
rect[3] + offset[1],
|
||||
)
|
||||
|
||||
|
||||
def fix_pagepdf_boxes(
|
||||
infile: Path | BinaryIO,
|
||||
out_file: Path,
|
||||
page_context: PageContext,
|
||||
swap_axis: bool = False,
|
||||
) -> Path:
|
||||
"""Fix the bounding boxes in a single page PDF.
|
||||
|
||||
The single page PDF is created with a normal MediaBox with its lower left corner
|
||||
at (0, 0). infile is the single page PDF. page_context.mediabox has the original
|
||||
file's mediabox, which may have a different origin. We needto adjust the other
|
||||
boxes in the single page PDF to match the effect they had on the original page.
|
||||
|
||||
When correcting page rotation, we create a single page PDF that is correctly
|
||||
rotated instead of an incorrectly rotated and then setting page.Rotate on it.
|
||||
If rotation is either 90 or 270 degrees, then this function can be called
|
||||
with swap_axis to swap the X and Y coordinates of all the boxes.
|
||||
|
||||
We are not concerned with solving degenerate cases where the boxes overlap or
|
||||
or express invalid rectangles. We merely pass the boxes, producing a
|
||||
transformation equivalent to the change made by constructing a new page image.
|
||||
"""
|
||||
with pikepdf.open(infile) as pdf:
|
||||
for page in pdf.pages:
|
||||
# page.BleedBox = page_context.pageinfo.bleedbox
|
||||
# page.ArtBox = page_context.pageinfo.artbox
|
||||
mediabox = page_context.pageinfo.mediabox
|
||||
offset = mediabox[0], mediabox[1]
|
||||
cropbox = _offset_rect(page_context.pageinfo.cropbox, offset)
|
||||
trimbox = _offset_rect(page_context.pageinfo.trimbox, offset)
|
||||
|
||||
if swap_axis:
|
||||
cropbox = cropbox[1], cropbox[0], cropbox[3], cropbox[2]
|
||||
trimbox = trimbox[1], trimbox[0], trimbox[3], trimbox[2]
|
||||
page.CropBox = cropbox
|
||||
page.TrimBox = trimbox
|
||||
pdf.save(out_file)
|
||||
return pdf
|
||||
|
||||
|
||||
def generate_postscript_stub(context: PdfContext) -> Path:
|
||||
@@ -992,10 +1077,3 @@ def copy_final(
|
||||
# get the appropriate umask, ownership, etc.
|
||||
with open(output_file, 'w+b') as output_stream:
|
||||
copyfileobj(input_stream, output_stream)
|
||||
# Attempt to copy file attributes from input to output
|
||||
if original_file:
|
||||
with suppress(OSError):
|
||||
# Copy original file's permissions, ownership, etc. if possible
|
||||
copystat(original_file, output_file)
|
||||
# Set output file's modification time to now
|
||||
Path(output_file).touch(exist_ok=True)
|
||||
|
||||
@@ -11,13 +11,13 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from concurrent.futures.process import BrokenProcessPool
|
||||
from concurrent.futures.thread import BrokenThreadPool
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, NamedTuple, cast
|
||||
from typing import NamedTuple, cast
|
||||
|
||||
import PIL
|
||||
|
||||
@@ -155,9 +155,11 @@ class HOCRResult:
|
||||
|
||||
def configure_debug_logging(
|
||||
log_filename: Path, prefix: str = ''
|
||||
) -> logging.FileHandler:
|
||||
) -> tuple[logging.FileHandler, Callable[[], None]]:
|
||||
"""Create a debug log file at a specified location.
|
||||
|
||||
Returns the log handler, and a function to remove the handler.
|
||||
|
||||
Args:
|
||||
log_filename: Where to the put the log file.
|
||||
prefix: The logging domain prefix that should be sent to the log.
|
||||
@@ -170,7 +172,15 @@ def configure_debug_logging(
|
||||
log_file_handler.setFormatter(formatter)
|
||||
log_file_handler.addFilter(PageNumberFilter())
|
||||
logging.getLogger(prefix).addHandler(log_file_handler)
|
||||
return log_file_handler
|
||||
|
||||
def remover():
|
||||
try:
|
||||
logging.getLogger(prefix).removeHandler(log_file_handler)
|
||||
log_file_handler.close()
|
||||
except OSError as e:
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
return log_file_handler, remover
|
||||
|
||||
|
||||
def worker_init(max_pixels: int) -> None:
|
||||
@@ -188,25 +198,21 @@ def manage_debug_log_handler(
|
||||
options: argparse.Namespace,
|
||||
work_folder: Path,
|
||||
):
|
||||
debug_log_handler = None
|
||||
remover = None
|
||||
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
|
||||
'PYTEST_CURRENT_TEST', ''
|
||||
):
|
||||
# Debug log for command line interface only with verbose output
|
||||
# See https://github.com/pytest-dev/pytest/issues/5502 for why we skip this
|
||||
# when pytest is running
|
||||
debug_log_handler = configure_debug_logging(
|
||||
work_folder / "debug.log"
|
||||
_debug_log_handler, remover = configure_debug_logging(
|
||||
work_folder / "debug.log", prefix=""
|
||||
) # pragma: no cover
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if debug_log_handler:
|
||||
try:
|
||||
debug_log_handler.close()
|
||||
log.removeHandler(debug_log_handler)
|
||||
except OSError as e:
|
||||
print(e, file=sys.stderr)
|
||||
if remover:
|
||||
remover()
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
"""Implements the concurrent and page synchronous parts of the pipeline."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
"""Implements the concurrent and page synchronous parts of the pipeline."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -80,7 +79,6 @@ def _exec_page_sync(page_context: PageContext) -> PageResult:
|
||||
page_context
|
||||
)
|
||||
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
|
||||
|
||||
return PageResult(
|
||||
pageno=page_context.pageno,
|
||||
pdf_page_from_image=pdf_page_from_image_out,
|
||||
@@ -105,14 +103,14 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||
try:
|
||||
set_thread_pageno(result.pageno + 1)
|
||||
sidecars[result.pageno] = result.text
|
||||
pbar.update()
|
||||
pbar.update(0.5)
|
||||
ocrgraft.graft_page(
|
||||
pageno=result.pageno,
|
||||
image=result.pdf_page_from_image,
|
||||
textpdf=result.ocr,
|
||||
autorotate_correction=result.orientation_correction,
|
||||
)
|
||||
pbar.update()
|
||||
pbar.update(0.5)
|
||||
finally:
|
||||
set_thread_pageno(None)
|
||||
|
||||
@@ -120,10 +118,9 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||
use_threads=options.use_threads,
|
||||
max_workers=max_workers,
|
||||
progress_kwargs=dict(
|
||||
total=(2 * len(context.pdfinfo)),
|
||||
total=len(context.pdfinfo),
|
||||
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
|
||||
unit='page',
|
||||
unit_scale=0.5,
|
||||
disable=not options.progress_bar,
|
||||
),
|
||||
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
|
||||
@@ -156,12 +153,13 @@ def _run_pipeline(
|
||||
options: argparse.Namespace,
|
||||
plugin_manager: OcrmypdfPluginManager,
|
||||
) -> ExitCode:
|
||||
with manage_work_folder(
|
||||
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
|
||||
retain=options.keep_temporary_files,
|
||||
print_location=options.keep_temporary_files,
|
||||
) as work_folder, manage_debug_log_handler(
|
||||
options=options, work_folder=work_folder
|
||||
with (
|
||||
manage_work_folder(
|
||||
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
|
||||
retain=options.keep_temporary_files,
|
||||
print_location=options.keep_temporary_files,
|
||||
) as work_folder,
|
||||
manage_debug_log_handler(options=options, work_folder=work_folder),
|
||||
):
|
||||
executor = setup_pipeline(options, plugin_manager)
|
||||
check_requested_output_file(options)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
"""Implements the concurrent and page synchronous parts of the pipeline."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -66,7 +66,7 @@ class ProgressBar(Protocol):
|
||||
def __exit__(self, *args):
|
||||
"""Exit a progress bar context."""
|
||||
|
||||
def update(self, n=1):
|
||||
def update(self, n=1, *, completed=None):
|
||||
"""Update the progress bar by an increment.
|
||||
|
||||
For use within a progress bar context.
|
||||
@@ -85,7 +85,7 @@ class NullProgressBar:
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
return False
|
||||
|
||||
def update(self, _arg=None):
|
||||
def update(self, _arg=None, *, completed=None):
|
||||
return
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ class RichProgressBar:
|
||||
disable: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
self._entered = False
|
||||
self.progress = Progress(
|
||||
TextColumn(
|
||||
"[progress.description]{task.description}",
|
||||
@@ -130,6 +131,7 @@ class RichProgressBar:
|
||||
|
||||
def __enter__(self):
|
||||
self.progress.start()
|
||||
self._entered = True
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
@@ -137,6 +139,10 @@ class RichProgressBar:
|
||||
self.progress.stop()
|
||||
return False
|
||||
|
||||
def update(self, value=None):
|
||||
advance = self.unit_scale if value is None else value
|
||||
self.progress.update(self.progress_bar, advance=advance)
|
||||
def update(self, n=1, *, completed=None):
|
||||
assert self._entered, "Progress bar must be entered before updating"
|
||||
if completed is None:
|
||||
advance = self.unit_scale if n is None else n
|
||||
self.progress.update(self.progress_bar, advance=advance)
|
||||
else:
|
||||
self.progress.update(self.progress_bar, completed=completed)
|
||||
|
||||
@@ -20,6 +20,7 @@ import pikepdf
|
||||
import PIL
|
||||
from pluggy import PluginManager
|
||||
|
||||
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
|
||||
from ocrmypdf._exec import unpaper
|
||||
from ocrmypdf.exceptions import (
|
||||
BadArgsError,
|
||||
@@ -28,20 +29,11 @@ from ocrmypdf.exceptions import (
|
||||
OutputFileAccessError,
|
||||
)
|
||||
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
|
||||
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
|
||||
from ocrmypdf.subprocess import check_external_program
|
||||
|
||||
# -------------
|
||||
# External dependencies
|
||||
|
||||
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------
|
||||
|
||||
|
||||
def check_platform() -> None:
|
||||
if sys.maxsize <= 2**32: # pragma: no cover
|
||||
log.warning(
|
||||
@@ -62,6 +54,7 @@ def check_options_languages(
|
||||
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
|
||||
if not ocr_engine_languages:
|
||||
return
|
||||
|
||||
missing_languages = set(options.languages) - set(ocr_engine_languages)
|
||||
if missing_languages:
|
||||
lang_text = '\n'.join(lang for lang in missing_languages)
|
||||
@@ -83,15 +76,6 @@ def check_options_languages(
|
||||
|
||||
|
||||
def check_options_output(options: Namespace) -> None:
|
||||
is_latin = set(options.languages).issubset(HOCR_OK_LANGS)
|
||||
|
||||
if options.pdf_renderer.startswith('hocr') and not is_latin:
|
||||
log.warning(
|
||||
"The 'hocr' PDF renderer is known to cause problems with one "
|
||||
"or more of the languages in your document. Use "
|
||||
"`--pdf-renderer auto` (the default) to avoid this issue."
|
||||
)
|
||||
|
||||
if options.output_type == 'none' and options.output_file not in (os.devnull, '-'):
|
||||
raise BadArgsError(
|
||||
"Since you specified `--output-type none`, the output file "
|
||||
@@ -140,6 +124,11 @@ def check_options_preprocessing(options: Namespace) -> None:
|
||||
options.clean = True
|
||||
if options.unpaper_args and not options.clean:
|
||||
raise BadArgsError("--clean is required for --unpaper-args")
|
||||
if (
|
||||
options.rotate_pages_threshold != DEFAULT_ROTATE_PAGES_THRESHOLD
|
||||
and not options.rotate_pages
|
||||
):
|
||||
raise BadArgsError("--rotate-pages is required for --rotate-pages-threshold")
|
||||
if options.clean:
|
||||
check_external_program(
|
||||
program='unpaper',
|
||||
|
||||
+13
-8
@@ -14,7 +14,7 @@ from collections.abc import Iterable, Sequence
|
||||
from enum import IntEnum
|
||||
from io import IOBase
|
||||
from pathlib import Path
|
||||
from typing import AnyStr, BinaryIO, Union
|
||||
from typing import AnyStr, BinaryIO
|
||||
from warnings import warn
|
||||
|
||||
import pluggy
|
||||
@@ -28,8 +28,8 @@ from ocrmypdf._validation import check_options
|
||||
from ocrmypdf.cli import ArgumentParser, get_parser
|
||||
from ocrmypdf.helpers import is_iterable_notstr
|
||||
|
||||
StrPath = Union[Path, AnyStr]
|
||||
PathOrIO = Union[BinaryIO, StrPath]
|
||||
StrPath = Path | AnyStr
|
||||
PathOrIO = BinaryIO | StrPath
|
||||
|
||||
# Installing plugins affects the global state of the Python interpreter,
|
||||
# so we need to use a lock to prevent multiple threads from installing
|
||||
@@ -169,7 +169,7 @@ def _kwargs_to_cmdline(
|
||||
|
||||
# We have a parameter
|
||||
cmdline.append(f"--{cmd_style_arg}")
|
||||
if isinstance(val, (int, float)):
|
||||
if isinstance(val, int | float):
|
||||
cmdline.append(str(val))
|
||||
elif isinstance(val, str):
|
||||
cmdline.append(val)
|
||||
@@ -201,14 +201,17 @@ def create_options(
|
||||
defer_kwargs={'progress_bar', 'plugins', 'parser', 'input_file', 'output_file'},
|
||||
**kwargs,
|
||||
)
|
||||
if isinstance(input_file, (BinaryIO, IOBase)):
|
||||
if isinstance(input_file, BinaryIO | IOBase):
|
||||
cmdline.append('stream://input_file')
|
||||
else:
|
||||
cmdline.append(os.fspath(input_file))
|
||||
if isinstance(output_file, (BinaryIO, IOBase)):
|
||||
if isinstance(output_file, BinaryIO | IOBase):
|
||||
cmdline.append('stream://output_file')
|
||||
else:
|
||||
cmdline.append(os.fspath(output_file))
|
||||
if 'sidecar' in kwargs and isinstance(kwargs['sidecar'], BinaryIO | IOBase):
|
||||
cmdline.append('--sidecar')
|
||||
cmdline.append('stream://sidecar')
|
||||
|
||||
parser.enable_api_mode()
|
||||
options = parser.parse_args(cmdline)
|
||||
@@ -219,6 +222,8 @@ def create_options(
|
||||
options.input_file = input_file
|
||||
if options.output_file == 'stream://output_file':
|
||||
options.output_file = output_file
|
||||
if options.sidecar == 'stream://sidecar':
|
||||
options.sidecar = kwargs['sidecar']
|
||||
|
||||
return options
|
||||
|
||||
@@ -230,7 +235,7 @@ def ocr( # noqa: D417
|
||||
language: Iterable[str] | None = None,
|
||||
image_dpi: int | None = None,
|
||||
output_type: str | None = None,
|
||||
sidecar: StrPath | None = None,
|
||||
sidecar: PathOrIO | None = None,
|
||||
jobs: int | None = None,
|
||||
use_threads: bool | None = None,
|
||||
title: str | None = None,
|
||||
@@ -343,7 +348,7 @@ def ocr( # noqa: D417
|
||||
|
||||
if not plugins:
|
||||
plugins = []
|
||||
elif isinstance(plugins, (str, Path)):
|
||||
elif isinstance(plugins, str | Path):
|
||||
plugins = [plugins]
|
||||
else:
|
||||
plugins = list(plugins)
|
||||
|
||||
@@ -12,10 +12,10 @@ import queue
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
||||
from contextlib import suppress
|
||||
from typing import Callable, Union
|
||||
from typing import Union
|
||||
|
||||
from rich.console import Console as RichConsole
|
||||
|
||||
@@ -25,13 +25,13 @@ from ocrmypdf._progressbar import RichProgressBar
|
||||
from ocrmypdf.exceptions import InputFileError
|
||||
from ocrmypdf.helpers import remove_all_log_handlers
|
||||
|
||||
FuturesExecutorClass = Union[type[ThreadPoolExecutor], type[ProcessPoolExecutor]]
|
||||
Queue = Union[multiprocessing.Queue, queue.Queue]
|
||||
FuturesExecutorClass = Union[ # noqa: UP007
|
||||
type[ThreadPoolExecutor], type[ProcessPoolExecutor]
|
||||
]
|
||||
Queue = Union[multiprocessing.Queue, queue.Queue] # noqa: UP007
|
||||
UserInit = Callable[[], None]
|
||||
WorkerInit = Callable[[Queue, UserInit, int], None]
|
||||
|
||||
RichTqdmProgressAdapter = RichProgressBar # Deprecated shim; remove in OCRmyPDF 16
|
||||
|
||||
|
||||
def log_listener(q: Queue):
|
||||
"""Listen to the worker processes and forward the messages to logging.
|
||||
@@ -130,11 +130,14 @@ class StandardExecutor(Executor):
|
||||
listener = threading.Thread(target=log_listener, args=(log_queue,))
|
||||
listener.start()
|
||||
|
||||
with self.pbar_class(**progress_kwargs) as pbar, executor_class(
|
||||
max_workers=max_workers,
|
||||
initializer=initializer,
|
||||
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
|
||||
) as executor:
|
||||
with (
|
||||
self.pbar_class(**progress_kwargs) as pbar,
|
||||
executor_class(
|
||||
max_workers=max_workers,
|
||||
initializer=initializer,
|
||||
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
|
||||
) as executor,
|
||||
):
|
||||
futures = [executor.submit(task, *args) for args in task_arguments]
|
||||
try:
|
||||
for future in as_completed(futures):
|
||||
|
||||
@@ -8,7 +8,5 @@ from ocrmypdf import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
def filter_pdf_page(
|
||||
page, image_filename, output_pdf
|
||||
): # pylint: disable=unused-argument
|
||||
def filter_pdf_page(page, image_filename, output_pdf): # pylint: disable=unused-argument
|
||||
return output_pdf
|
||||
|
||||
@@ -6,6 +6,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
from ocrmypdf._exec import ghostscript
|
||||
from ocrmypdf.exceptions import MissingDependencyError
|
||||
@@ -15,7 +17,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
# Currently all blacklisted versions are lower than 9.55, so none need to
|
||||
# be added here. If a future version is blacklisted, add it here.
|
||||
BLACKLISTED_GS_VERSIONS: frozenset[str] = frozenset()
|
||||
BLACKLISTED_GS_VERSIONS: frozenset[Version] = frozenset()
|
||||
|
||||
|
||||
@hookimpl
|
||||
@@ -52,14 +54,24 @@ def check_options(options):
|
||||
program='gs',
|
||||
package='ghostscript',
|
||||
version_checker=ghostscript.version,
|
||||
need_version='9.55', # Ubuntu 22.04's version
|
||||
need_version='9.54', # RHEL 9's version; Ubuntu 22.04 has 9.55
|
||||
)
|
||||
gs_version = ghostscript.version()
|
||||
if gs_version in BLACKLISTED_GS_VERSIONS:
|
||||
raise MissingDependencyError(
|
||||
f"Ghostscript {gs_version} contains serious regressions and is not "
|
||||
"supported. Please upgrade to a newer version, or downgrade to the "
|
||||
"previous version."
|
||||
"supported. Please upgrade to a newer version."
|
||||
)
|
||||
if Version('10.0.0') <= gs_version < Version('10.02.1') and (
|
||||
options.skip_text or options.redo_ocr
|
||||
):
|
||||
raise MissingDependencyError(
|
||||
f"Ghostscript 10.0.0 through 10.02.0 (your version: {gs_version}) "
|
||||
"contain serious regressions that corrupt PDFs with existing text, "
|
||||
"such as those processed using --skip-text or --redo-ocr. "
|
||||
"Please upgrade to a "
|
||||
"newer version, or use --output-type pdf to avoid Ghostscript, or "
|
||||
"use --force-ocr to discard existing text."
|
||||
)
|
||||
|
||||
if options.output_type == 'pdfa':
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Built-in plugin to implement OCR using Tesseract."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -14,6 +14,7 @@ from ocrmypdf import hookimpl
|
||||
from ocrmypdf._exec import tesseract
|
||||
from ocrmypdf._jobcontext import PageContext
|
||||
from ocrmypdf.cli import numeric, str_to_int
|
||||
from ocrmypdf.exceptions import BadArgsError, MissingDependencyError
|
||||
from ocrmypdf.helpers import clamp
|
||||
from ocrmypdf.imageops import calculate_downsample, downsample_image
|
||||
from ocrmypdf.pluginspec import OcrEngine
|
||||
@@ -94,7 +95,8 @@ def add_options(parser):
|
||||
)
|
||||
tess.add_argument(
|
||||
'--tesseract-downsample-large-images',
|
||||
action='store_true',
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help=(
|
||||
"Downsample large images before OCR. Tesseract has an upper limit on the "
|
||||
"size images it will support. If this argument is given, OCRmyPDF will "
|
||||
@@ -143,10 +145,20 @@ def check_options(options):
|
||||
need_version='4.1.1', # Ubuntu 22.04 version (also 20.04)
|
||||
version_parser=tesseract.TesseractVersion,
|
||||
)
|
||||
tess_version = tesseract.version()
|
||||
if tess_version == tesseract.TesseractVersion('5.4.0'):
|
||||
raise MissingDependencyError(
|
||||
"Tesseract 5.4.0 is not supported due to regressions in this version. "
|
||||
"Please upgrade to a newer or supported older version."
|
||||
)
|
||||
|
||||
# Decide on what renderer to use
|
||||
if options.pdf_renderer == 'auto':
|
||||
options.pdf_renderer = 'sandwich'
|
||||
if {'ara', 'heb', 'fas', 'per'} & set(options.languages):
|
||||
log.info("Using sandwich renderer since there is an RTL language")
|
||||
options.pdf_renderer = 'sandwich'
|
||||
else:
|
||||
options.pdf_renderer = 'hocr'
|
||||
|
||||
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
|
||||
log.warning(
|
||||
@@ -159,6 +171,14 @@ def check_options(options):
|
||||
"The --tesseract-pagesegmode argument you select will disable OCR. "
|
||||
"This may cause processing to fail."
|
||||
)
|
||||
DENIED_LANGUAGES = {'equ', 'osd'}
|
||||
if DENIED_LANGUAGES & set(options.languages):
|
||||
raise BadArgsError(
|
||||
"The following languages for Tesseract's internal use and should not "
|
||||
"be issued explicitly: "
|
||||
f"{', '.join(DENIED_LANGUAGES & set(options.languages))}\n"
|
||||
"Remove them from the -l/--language argument."
|
||||
)
|
||||
|
||||
|
||||
@hookimpl
|
||||
@@ -216,7 +236,7 @@ class TesseractOcrEngine(OcrEngine):
|
||||
|
||||
@staticmethod
|
||||
def creator_tag(options):
|
||||
tag = '-PDF' if options.pdf_renderer == 'sandwich' else ''
|
||||
tag = '-PDF' if options.pdf_renderer == 'sandwich' else '-hOCR'
|
||||
return f"Tesseract OCR{tag} {TesseractOcrEngine.version()}"
|
||||
|
||||
def __str__(self):
|
||||
|
||||
+5
-4
@@ -6,9 +6,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Callable, TypeVar
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
|
||||
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
||||
from ocrmypdf._version import __version__ as _VERSION
|
||||
|
||||
@@ -390,7 +391,7 @@ Online documentation is located at:
|
||||
action='store',
|
||||
type=numeric(float, 0),
|
||||
metavar='MPixels',
|
||||
help="Set maximum number of pixels to unpack before treating an image as a "
|
||||
help="Set maximum number of megapixels to unpack before treating an image as a "
|
||||
"decompression bomb",
|
||||
default=250.0,
|
||||
)
|
||||
@@ -403,7 +404,7 @@ Online documentation is located at:
|
||||
)
|
||||
advanced.add_argument(
|
||||
'--rotate-pages-threshold',
|
||||
default=14.0,
|
||||
default=DEFAULT_ROTATE_PAGES_THRESHOLD,
|
||||
type=numeric(float, 0, 1000),
|
||||
metavar='CONFIDENCE',
|
||||
help="Only rotate pages when confidence is above this value (arbitrary "
|
||||
|
||||
Binary file not shown.
@@ -137,3 +137,16 @@ class TaggedPDFError(InputFileError):
|
||||
override this error.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class ColorConversionNeededError(BadArgsError):
|
||||
"""PDF needs color conversion."""
|
||||
|
||||
message = dedent(
|
||||
"""\
|
||||
The input PDF has an unusual color space. Use
|
||||
--color-conversion-strategy to convert to a common color space
|
||||
such as RGB, or use --output-type pdf to skip PDF/A conversion
|
||||
and retain the original color space.
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -20,13 +20,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
import logging.handlers
|
||||
import signal
|
||||
from collections.abc import Iterable, Iterator
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
from contextlib import suppress
|
||||
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
|
||||
|
||||
from ocrmypdf import Executor, hookimpl
|
||||
from ocrmypdf._concurrent import NullProgressBar
|
||||
|
||||
@@ -10,7 +10,7 @@ import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
import warnings
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from contextlib import suppress
|
||||
from decimal import Decimal
|
||||
from io import StringIO
|
||||
@@ -19,13 +19,13 @@ from pathlib import Path
|
||||
from statistics import harmonic_mean
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
from deprecation import deprecated
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -136,6 +136,7 @@ class Resolution(Generic[T]):
|
||||
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
|
||||
|
||||
|
||||
@deprecated(deprecated_in='15.4.0')
|
||||
class NeverRaise(Exception):
|
||||
"""An exception that is never raised."""
|
||||
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2010 Jonathan Brinley
|
||||
# SPDX-FileCopyrightText: 2013-2014 Julien Pfefferkorn
|
||||
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""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
|
||||
from xml.etree import ElementTree
|
||||
|
||||
with warnings.catch_warnings():
|
||||
# reportlab uses deprecated load_module
|
||||
# shim can be removed when we require reportlab >= 3.7
|
||||
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,
|
||||
# assuming Tesseract has the necessary languages installed. Note that there may
|
||||
# not be language packs for them.
|
||||
HOCR_OK_LANGS = frozenset(
|
||||
[
|
||||
# Languages fully covered by Latin-1:
|
||||
'afr', # Afrikaans
|
||||
'alb', # Albanian
|
||||
'ast', # Leonese
|
||||
'baq', # Basque
|
||||
'bre', # Breton
|
||||
'cos', # Corsican
|
||||
'eng', # English
|
||||
'eus', # Basque
|
||||
'fao', # Faoese
|
||||
'gla', # Scottish Gaelic
|
||||
'glg', # Galician
|
||||
'glv', # Manx
|
||||
'ice', # Icelandic
|
||||
'ind', # Indonesian
|
||||
'isl', # Icelandic
|
||||
'ita', # Italian
|
||||
'ltz', # Luxembourgish
|
||||
'mal', # Malay Rumi
|
||||
'mga', # Irish
|
||||
'nor', # Norwegian
|
||||
'oci', # Occitan
|
||||
'por', # Portugeuse
|
||||
'roh', # Romansh
|
||||
'sco', # Scots
|
||||
'sma', # Sami
|
||||
'spa', # Spanish
|
||||
'sqi', # Albanian
|
||||
'swa', # Swahili
|
||||
'swe', # Swedish
|
||||
'tgl', # Tagalog
|
||||
'wln', # Walloon
|
||||
# Languages supported by Latin-1 except for a few rare characters that OCR
|
||||
# is probably not trained to recognize anyway:
|
||||
'cat', # Catalan
|
||||
'cym', # Welsh
|
||||
'dan', # Danish
|
||||
'deu', # German
|
||||
'dut', # Dutch
|
||||
'est', # Estonian
|
||||
'fin', # Finnish
|
||||
'fra', # French
|
||||
'hun', # Hungarian
|
||||
'kur', # Kurdish
|
||||
'nld', # Dutch
|
||||
'wel', # Welsh
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Element = ElementTree.Element
|
||||
|
||||
|
||||
class Rect(NamedTuple):
|
||||
"""A rectangle for managing PDF coordinates."""
|
||||
|
||||
x1: Any
|
||||
y1: Any
|
||||
x2: Any
|
||||
y2: Any
|
||||
|
||||
|
||||
class HocrTransformError(Exception):
|
||||
"""Error while applying hOCR transform."""
|
||||
|
||||
|
||||
class HocrTransform:
|
||||
"""A class for converting documents from the hOCR format.
|
||||
|
||||
For details of the hOCR format, see:
|
||||
http://kba.cloud/hocr-spec/.
|
||||
"""
|
||||
|
||||
box_pattern = re.compile(r'bbox((\s+\d+){4})')
|
||||
baseline_pattern = re.compile(
|
||||
r'''
|
||||
baseline \s+
|
||||
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
|
||||
([\-\+]?\d+) # +/- int''',
|
||||
re.VERBOSE,
|
||||
)
|
||||
ligatures = str.maketrans(
|
||||
{'ff': 'ff', 'ffi': 'ffi', 'ffl': 'ffl', 'fi': 'fi', 'fl': 'fl'}
|
||||
)
|
||||
|
||||
def __init__(self, *, hocr_filename: str | Path, dpi: float):
|
||||
"""Initialize the HocrTransform object."""
|
||||
self.dpi = dpi
|
||||
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
|
||||
|
||||
# if the hOCR file has a namespace, ElementTree requires its use to
|
||||
# find elements
|
||||
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
|
||||
self.xmlns = ''
|
||||
if matches:
|
||||
self.xmlns = matches.group(1)
|
||||
|
||||
# get dimension in pt (not pixel!!!!) of the OCRed image
|
||||
self.width, self.height = None, None
|
||||
for div in self.hocr.findall(self._child_xpath('div', 'ocr_page')):
|
||||
coords = self.element_coordinates(div)
|
||||
pt_coords = self.pt_from_pixel(coords)
|
||||
self.width = pt_coords.x2 - pt_coords.x1
|
||||
self.height = pt_coords.y2 - pt_coords.y1
|
||||
# there shouldn't be more than one, and if there is, we don't want
|
||||
# it
|
||||
break
|
||||
if self.width is None or self.height is None:
|
||||
raise HocrTransformError("hocr file is missing page dimensions")
|
||||
|
||||
def __str__(self): # pragma: no cover
|
||||
"""Return the textual content of the HTML body."""
|
||||
if self.hocr is None:
|
||||
return ''
|
||||
body = self.hocr.find(self._child_xpath('body'))
|
||||
if body:
|
||||
return self._get_element_text(body)
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _get_element_text(self, element: Element):
|
||||
"""Return the textual content of the element and its children."""
|
||||
text = ''
|
||||
if element.text is not None:
|
||||
text += element.text
|
||||
for child in element:
|
||||
text += self._get_element_text(child)
|
||||
if element.tail is not None:
|
||||
text += element.tail
|
||||
return text
|
||||
|
||||
@classmethod
|
||||
def element_coordinates(cls, element: Element) -> Rect:
|
||||
"""Get coordinates of the bounding box around an element."""
|
||||
out = Rect._make(0 for _ in range(4))
|
||||
if 'title' in element.attrib:
|
||||
matches = cls.box_pattern.search(element.attrib['title'])
|
||||
if matches:
|
||||
coords = matches.group(1).split()
|
||||
out = Rect._make(int(coords[n]) for n in range(4))
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def baseline(cls, element: Element) -> tuple[float, float]:
|
||||
"""Get baseline's slope and intercept."""
|
||||
if 'title' in element.attrib:
|
||||
matches = cls.baseline_pattern.search(element.attrib['title'])
|
||||
if matches:
|
||||
return float(matches.group(1)), int(matches.group(2))
|
||||
return (0.0, 0.0)
|
||||
|
||||
def pt_from_pixel(self, pxl) -> Rect:
|
||||
"""Returns the quantity in PDF units (pt) given quantity in pixels."""
|
||||
return Rect._make((c / self.dpi * inch) for c in pxl)
|
||||
|
||||
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}']"
|
||||
return xpath
|
||||
|
||||
@classmethod
|
||||
def replace_unsupported_chars(cls, s: str) -> str:
|
||||
"""Replaces characters with those available in the Helvetica typeface."""
|
||||
return s.translate(cls.ligatures)
|
||||
|
||||
def to_pdf(
|
||||
self,
|
||||
*,
|
||||
out_filename: Path,
|
||||
image_filename: Path | None = None,
|
||||
show_bounding_boxes: bool = False,
|
||||
fontname: str = "Helvetica",
|
||||
invisible_text: bool = False,
|
||||
interword_spaces: bool = False,
|
||||
) -> None:
|
||||
"""Creates a PDF file with an image superimposed on top of the text.
|
||||
|
||||
Text is positioned according to the bounding box of the lines in
|
||||
the hOCR file.
|
||||
The image need not be identical to the image used to create the hOCR
|
||||
file.
|
||||
It can have a lower resolution, different color mode, etc.
|
||||
|
||||
Arguments:
|
||||
out_filename: Path of PDF to write.
|
||||
image_filename: Image to use for this file. If omitted, the OCR text
|
||||
is shown.
|
||||
show_bounding_boxes: Show bounding boxes around various text regions,
|
||||
for debugging.
|
||||
fontname: Name of font to use.
|
||||
invisible_text: If True, text is rendered invisible so that is
|
||||
selectable but never drawn. If False, text is visible and may
|
||||
be seen if the image is skipped or deleted in Acrobat.
|
||||
interword_spaces: If True, insert spaces between words rather than
|
||||
drawing each word without spaces. Generally this improves text
|
||||
extraction.
|
||||
"""
|
||||
# create the PDF file
|
||||
# page size in points (1/72 in.)
|
||||
pdf = Canvas(
|
||||
os.fspath(out_filename),
|
||||
pagesize=(self.width, self.height),
|
||||
pageCompression=1,
|
||||
)
|
||||
|
||||
# draw bounding box for each paragraph
|
||||
# light blue for bounding box of paragraph
|
||||
pdf.setStrokeColor(cyan)
|
||||
# light blue for bounding box of paragraph
|
||||
pdf.setFillColor(cyan)
|
||||
pdf.setLineWidth(0) # no line for bounding box
|
||||
for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
|
||||
elemtxt = self._get_element_text(elem).rstrip()
|
||||
if len(elemtxt) == 0:
|
||||
continue
|
||||
|
||||
pxl_coords = self.element_coordinates(elem)
|
||||
pt = self.pt_from_pixel(pxl_coords) # pylint: disable=invalid-name
|
||||
|
||||
# draw the bbox border
|
||||
if show_bounding_boxes: # pragma: no cover
|
||||
pdf.rect(
|
||||
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
|
||||
)
|
||||
|
||||
found_lines = False
|
||||
for line in (
|
||||
element
|
||||
for element in self.hocr.iterfind(self._child_xpath('span'))
|
||||
if 'class' in element.attrib
|
||||
and element.attrib['class'] in {'ocr_header', 'ocr_line', 'ocr_textfloat'}
|
||||
):
|
||||
found_lines = True
|
||||
self._do_line(
|
||||
pdf,
|
||||
line,
|
||||
"ocrx_word",
|
||||
fontname,
|
||||
invisible_text,
|
||||
interword_spaces,
|
||||
show_bounding_boxes,
|
||||
)
|
||||
|
||||
if not found_lines:
|
||||
# Tesseract did not report any lines (just words)
|
||||
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
|
||||
self._do_line(
|
||||
pdf,
|
||||
root,
|
||||
"ocrx_word",
|
||||
fontname,
|
||||
invisible_text,
|
||||
interword_spaces,
|
||||
show_bounding_boxes,
|
||||
)
|
||||
# put the image on the page, scaled to fill the page
|
||||
if image_filename is not None:
|
||||
pdf.drawImage(
|
||||
os.fspath(image_filename), 0, 0, width=self.width, height=self.height
|
||||
)
|
||||
|
||||
# finish up the page and save it
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
@classmethod
|
||||
def polyval(cls, poly, x): # pragma: no cover
|
||||
"""Calculate the value of a polynomial at a point."""
|
||||
return x * poly[0] + poly[1]
|
||||
|
||||
def _do_line(
|
||||
self,
|
||||
pdf: Canvas,
|
||||
line: Element | None,
|
||||
elemclass: str,
|
||||
fontname: str,
|
||||
invisible_text: bool,
|
||||
interword_spaces: bool,
|
||||
show_bounding_boxes: bool,
|
||||
):
|
||||
if line is None:
|
||||
return
|
||||
pxl_line_coords = self.element_coordinates(line)
|
||||
line_box = self.pt_from_pixel(pxl_line_coords)
|
||||
line_height = line_box.y2 - line_box.y1
|
||||
|
||||
slope, pxl_intercept = self.baseline(line)
|
||||
if abs(slope) < 0.005:
|
||||
slope = 0.0
|
||||
angle = atan(slope)
|
||||
cos_a, sin_a = cos(angle), sin(angle)
|
||||
|
||||
text = pdf.beginText()
|
||||
intercept = pxl_intercept / self.dpi * inch
|
||||
|
||||
# Don't allow the font to break out of the bounding box. Division by
|
||||
# cos_a accounts for extra clearance between the glyph's vertical axis
|
||||
# on a sloped baseline and the edge of the bounding box.
|
||||
fontsize = (line_height - abs(intercept)) / cos_a
|
||||
text.setFont(fontname, fontsize)
|
||||
if invisible_text:
|
||||
text.setTextRenderMode(3) # Invisible (indicates OCR text)
|
||||
|
||||
# Intercept is normally negative, so this places it above the bottom
|
||||
# of the line box
|
||||
baseline_y2 = self.height - (line_box.y2 + intercept)
|
||||
|
||||
if show_bounding_boxes: # pragma: no cover
|
||||
# draw the baseline in magenta, dashed
|
||||
pdf.setDash()
|
||||
pdf.setStrokeColor(magenta)
|
||||
pdf.setLineWidth(0.5)
|
||||
# negate slope because it is defined as a rise/run in pixel
|
||||
# coordinates and page coordinates have the y axis flipped
|
||||
pdf.line(
|
||||
line_box.x1,
|
||||
baseline_y2,
|
||||
line_box.x2,
|
||||
self.polyval((-slope, baseline_y2), line_box.x2 - line_box.x1),
|
||||
)
|
||||
# light green for bounding box of word/line
|
||||
pdf.setDash(6, 3)
|
||||
pdf.setStrokeColor(red)
|
||||
|
||||
text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2)
|
||||
pdf.setFillColor(black) # text in black
|
||||
|
||||
elements = line.findall(self._child_xpath('span', elemclass))
|
||||
for elem in elements:
|
||||
elemtxt = self._get_element_text(elem).strip()
|
||||
elemtxt = self.replace_unsupported_chars(elemtxt)
|
||||
if elemtxt == '':
|
||||
continue
|
||||
|
||||
pxl_coords = self.element_coordinates(elem)
|
||||
box = self.pt_from_pixel(pxl_coords)
|
||||
if interword_spaces:
|
||||
# if `--interword-spaces` is true, append a space
|
||||
# to the end of each text element to allow simpler PDF viewers
|
||||
# such as PDF.js to better recognize words in search and copy
|
||||
# and paste. Do not remove space from last word in line, even
|
||||
# though it would look better, because it will interfere with
|
||||
# naive text extraction. \n does not work either.
|
||||
elemtxt += ' '
|
||||
box = Rect._make(
|
||||
(
|
||||
box.x1,
|
||||
line_box.y1,
|
||||
box.x2 + pdf.stringWidth(' ', fontname, line_height),
|
||||
line_box.y2,
|
||||
)
|
||||
)
|
||||
box_width = box.x2 - box.x1
|
||||
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
|
||||
|
||||
# draw the bbox border
|
||||
if show_bounding_boxes: # pragma: no cover
|
||||
pdf.rect(
|
||||
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
|
||||
)
|
||||
|
||||
# Adjust relative position of cursor
|
||||
# This is equivalent to:
|
||||
# text.setTextOrigin(pt.x1, self.height - line_box.y2)
|
||||
# but the former generates a full text reposition matrix (Tm) in the
|
||||
# content stream while this issues a "offset" (Td) command.
|
||||
# .moveCursor() is relative to start of the text line, where the
|
||||
# "text line" means whatever reportlab defines it as. Do not use
|
||||
# use .getCursor(), since moveCursor() rather unintuitively plans
|
||||
# its moves relative to .getStartOfLine().
|
||||
# For skewed lines, in the text transform we set up a rotated
|
||||
# coordinate system, so we don't have to account for the
|
||||
# incremental offset. Surprisingly most PDF viewers can handle this.
|
||||
cursor = text.getStartOfLine()
|
||||
dx = box.x1 - cursor[0]
|
||||
dy = baseline_y2 - cursor[1]
|
||||
text.moveCursor(dx, dy)
|
||||
|
||||
# If reportlab tells us this word is 0 units wide, our best seems
|
||||
# to be to suppress this text
|
||||
if font_width > 0:
|
||||
text.setHorizScale(100 * box_width / font_width)
|
||||
text.textOut(elemtxt)
|
||||
pdf.drawText(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
|
||||
parser.add_argument(
|
||||
'-b',
|
||||
'--boundingboxes',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Show bounding boxes borders',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r',
|
||||
'--resolution',
|
||||
type=int,
|
||||
default=300,
|
||||
help='Resolution of the image that was OCRed',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
'--image',
|
||||
default=None,
|
||||
help='Path to the image to be placed above the text',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--interword-spaces',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Add spaces between words',
|
||||
)
|
||||
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
|
||||
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
|
||||
args = parser.parse_args()
|
||||
|
||||
hocr = HocrTransform(hocr_filename=args.hocrfile, dpi=args.resolution)
|
||||
hocr.to_pdf(
|
||||
out_filename=args.outputfile,
|
||||
image_filename=args.image,
|
||||
show_bounding_boxes=args.boundingboxes,
|
||||
interword_spaces=args.interword_spaces,
|
||||
)
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""Transform .hocr and page image to text PDF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ocrmypdf.hocrtransform._hocr import (
|
||||
DebugRenderOptions,
|
||||
HocrTransform,
|
||||
HocrTransformError,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'HocrTransform',
|
||||
'HocrTransformError',
|
||||
'DebugRenderOptions',
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""Simple CLI for testing HOCR."""
|
||||
|
||||
import argparse
|
||||
|
||||
from ocrmypdf.hocrtransform import HocrTransform
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
|
||||
parser.add_argument(
|
||||
'-b',
|
||||
'--boundingboxes',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Show bounding boxes borders',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r',
|
||||
'--resolution',
|
||||
type=int,
|
||||
default=300,
|
||||
help='Resolution of the image that was OCRed',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
'--image',
|
||||
default=None,
|
||||
help='Path to the image to be placed above the text',
|
||||
)
|
||||
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
|
||||
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
|
||||
args = parser.parse_args()
|
||||
|
||||
hocr = HocrTransform(hocr_filename=args.hocrfile, dpi=args.resolution)
|
||||
hocr.to_pdf(
|
||||
out_filename=args.outputfile,
|
||||
image_filename=args.image,
|
||||
)
|
||||
@@ -0,0 +1,141 @@
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import unicodedata
|
||||
import zlib
|
||||
from importlib.resources import files as package_files
|
||||
|
||||
from pikepdf import (
|
||||
Dictionary,
|
||||
Name,
|
||||
Pdf,
|
||||
)
|
||||
from pikepdf.canvas import Font
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EncodableFont(Font):
|
||||
def text_encode(self, text: str) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class GlyphlessFont(EncodableFont):
|
||||
CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536)
|
||||
GLYPHLESS_FONT_NAME = 'pdf.ttf'
|
||||
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
|
||||
CHAR_ASPECT = 2
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def text_width(self, text: str, fontsize: float) -> float:
|
||||
"""Estimate the width of a text string when rendered with the given font."""
|
||||
# NFKC: split ligatures, combine diacritics
|
||||
return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT)
|
||||
|
||||
def text_encode(self, text: str) -> bytes:
|
||||
return text.encode('utf-16be')
|
||||
|
||||
def register(self, pdf: Pdf):
|
||||
"""Register the glyphless font.
|
||||
|
||||
Create several data structures in the Pdf to describe the font. While it create
|
||||
the data, a reference should be set in at least one page's /Resources dictionary
|
||||
to retain the font in the output PDF and ensure it is usable on that page.
|
||||
"""
|
||||
PLACEHOLDER = Name.Placeholder
|
||||
|
||||
basefont = pdf.make_indirect(
|
||||
Dictionary(
|
||||
BaseFont=Name.GlyphLessFont,
|
||||
DescendantFonts=[PLACEHOLDER],
|
||||
Encoding=Name("/Identity-H"),
|
||||
Subtype=Name.Type0,
|
||||
ToUnicode=PLACEHOLDER,
|
||||
Type=Name.Font,
|
||||
)
|
||||
)
|
||||
cid_font_type2 = pdf.make_indirect(
|
||||
Dictionary(
|
||||
BaseFont=Name.GlyphLessFont,
|
||||
CIDToGIDMap=PLACEHOLDER,
|
||||
CIDSystemInfo=Dictionary(
|
||||
Ordering="Identity",
|
||||
Registry="Adobe",
|
||||
Supplement=0,
|
||||
),
|
||||
FontDescriptor=PLACEHOLDER,
|
||||
Subtype=Name.CIDFontType2,
|
||||
Type=Name.Font,
|
||||
DW=1000 // self.CHAR_ASPECT,
|
||||
)
|
||||
)
|
||||
basefont.DescendantFonts = [cid_font_type2]
|
||||
cid_font_type2.CIDToGIDMap = pdf.make_stream(
|
||||
self.CID_TO_GID_DATA, Filter=Name.FlateDecode
|
||||
)
|
||||
basefont.ToUnicode = pdf.make_stream(
|
||||
b"/CIDInit /ProcSet findresource begin\n"
|
||||
b"12 dict begin\n"
|
||||
b"begincmap\n"
|
||||
b"/CIDSystemInfo\n"
|
||||
b"<<\n"
|
||||
b" /Registry (Adobe)\n"
|
||||
b" /Ordering (UCS)\n"
|
||||
b" /Supplement 0\n"
|
||||
b">> def\n"
|
||||
b"/CMapName /Adobe-Identify-UCS def\n"
|
||||
b"/CMapType 2 def\n"
|
||||
b"1 begincodespacerange\n"
|
||||
b"<0000> <FFFF>\n"
|
||||
b"endcodespacerange\n"
|
||||
b"1 beginbfrange\n"
|
||||
b"<0000> <FFFF> <0000>\n"
|
||||
b"endbfrange\n"
|
||||
b"endcmap\n"
|
||||
b"CMapName currentdict /CMap defineresource pop\n"
|
||||
b"end\n"
|
||||
b"end\n"
|
||||
)
|
||||
font_descriptor = pdf.make_indirect(
|
||||
Dictionary(
|
||||
Ascent=1000,
|
||||
CapHeight=1000,
|
||||
Descent=-1,
|
||||
Flags=5, # Fixed pitch and symbolic
|
||||
FontBBox=[0, 0, 1000 // self.CHAR_ASPECT, 1000],
|
||||
FontFile2=PLACEHOLDER,
|
||||
FontName=Name.GlyphLessFont,
|
||||
ItalicAngle=0,
|
||||
StemV=80,
|
||||
Type=Name.FontDescriptor,
|
||||
)
|
||||
)
|
||||
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
|
||||
cid_font_type2.FontDescriptor = font_descriptor
|
||||
return basefont
|
||||
|
||||
|
||||
class Courier(EncodableFont):
|
||||
"""Courier font."""
|
||||
|
||||
def text_width(self, text: str, fontsize: float) -> float:
|
||||
"""Estimate the width of a text string when rendered with the given font."""
|
||||
return len(text) * fontsize
|
||||
|
||||
def text_encode(self, text: str) -> bytes:
|
||||
return text.encode('pdfdoc', errors='ignore')
|
||||
|
||||
def register(self, pdf: Pdf) -> Dictionary:
|
||||
"""Register the font."""
|
||||
return pdf.make_indirect(
|
||||
Dictionary(
|
||||
BaseFont=Name.Courier,
|
||||
Type=Name.Font,
|
||||
Subtype=Name.Type1,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,501 @@
|
||||
# SPDX-FileCopyrightText: 2010 Jonathan Brinley
|
||||
# SPDX-FileCopyrightText: 2013-2014 Julien Pfefferkorn
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""hOCR transform implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass
|
||||
from itertools import pairwise
|
||||
from math import atan, cos, pi
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from pikepdf import Matrix, Name, Rectangle
|
||||
from pikepdf.canvas import (
|
||||
BLACK,
|
||||
BLUE,
|
||||
CYAN,
|
||||
DARKGREEN,
|
||||
GREEN,
|
||||
MAGENTA,
|
||||
RED,
|
||||
Canvas,
|
||||
Text,
|
||||
TextDirection,
|
||||
)
|
||||
|
||||
from ocrmypdf.hocrtransform._font import EncodableFont as Font
|
||||
from ocrmypdf.hocrtransform._font import GlyphlessFont
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INCH = 72.0
|
||||
|
||||
Element = ElementTree.Element
|
||||
|
||||
|
||||
@dataclass
|
||||
class DebugRenderOptions:
|
||||
"""A class for managing rendering options."""
|
||||
|
||||
render_paragraph_bbox: bool = False
|
||||
render_baseline: bool = False
|
||||
render_triangle: bool = False
|
||||
render_line_bbox: bool = False
|
||||
render_word_bbox: bool = False
|
||||
render_space_bbox: bool = False
|
||||
|
||||
|
||||
class HocrTransformError(Exception):
|
||||
"""Error while applying hOCR transform."""
|
||||
|
||||
|
||||
class HocrTransform:
|
||||
"""A class for converting documents from the hOCR format.
|
||||
|
||||
For details of the hOCR format, see:
|
||||
http://kba.github.io/hocr-spec/1.2/.
|
||||
"""
|
||||
|
||||
box_pattern = re.compile(
|
||||
r'''
|
||||
bbox \s+
|
||||
(\d+) \s+ # left: uint
|
||||
(\d+) \s+ # top: uint
|
||||
(\d+) \s+ # right: uint
|
||||
(\d+) # bottom: uint
|
||||
''',
|
||||
re.VERBOSE,
|
||||
)
|
||||
baseline_pattern = re.compile(
|
||||
r'''
|
||||
baseline \s+
|
||||
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
|
||||
([\-\+]?\d+) # +/- int
|
||||
''',
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
hocr_filename: str | Path,
|
||||
dpi: float,
|
||||
debug: bool = False,
|
||||
fontname: Name = Name("/f-0-0"),
|
||||
font: Font = GlyphlessFont(),
|
||||
debug_render_options: DebugRenderOptions | None = None,
|
||||
):
|
||||
"""Initialize the HocrTransform object."""
|
||||
if debug:
|
||||
log.warning("Use debug_render_options instead", DeprecationWarning)
|
||||
self.render_options = DebugRenderOptions(
|
||||
render_baseline=debug,
|
||||
render_triangle=debug,
|
||||
render_line_bbox=False,
|
||||
render_word_bbox=debug,
|
||||
render_paragraph_bbox=False,
|
||||
render_space_bbox=False,
|
||||
)
|
||||
else:
|
||||
self.render_options = debug_render_options or DebugRenderOptions()
|
||||
self.dpi = dpi
|
||||
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
|
||||
self._fontname = fontname
|
||||
self._font = font
|
||||
|
||||
# if the hOCR file has a namespace, ElementTree requires its use to
|
||||
# find elements
|
||||
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
|
||||
self.xmlns = ''
|
||||
if matches:
|
||||
self.xmlns = matches.group(1)
|
||||
|
||||
for div in self.hocr.findall(self._child_xpath('div', 'ocr_page')):
|
||||
coords = self.element_coordinates(div)
|
||||
if not coords:
|
||||
raise HocrTransformError("hocr file is missing page dimensions")
|
||||
self.width = (coords.urx - coords.llx) / (self.dpi / INCH)
|
||||
self.height = (coords.ury - coords.lly) / (self.dpi / INCH)
|
||||
# Stop after first div that has page coordinates
|
||||
break
|
||||
|
||||
def _get_element_text(self, element: Element) -> str:
|
||||
"""Return the textual content of the element and its children."""
|
||||
text = element.text if element.text is not None else ''
|
||||
for child in element:
|
||||
text += self._get_element_text(child)
|
||||
text += element.tail if element.tail is not None else ''
|
||||
return text
|
||||
|
||||
@classmethod
|
||||
def element_coordinates(cls, element: Element) -> Rectangle | None:
|
||||
"""Get coordinates of the bounding box around an element."""
|
||||
matches = cls.box_pattern.search(element.attrib.get('title', ''))
|
||||
if not matches:
|
||||
return None
|
||||
return Rectangle(
|
||||
float(matches.group(1)), # llx = left
|
||||
float(matches.group(2)), # lly = top
|
||||
float(matches.group(3)), # urx = right
|
||||
float(matches.group(4)), # ury = bottom
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def baseline(cls, element: Element) -> tuple[float, float]:
|
||||
"""Get baseline's slope and intercept."""
|
||||
matches = cls.baseline_pattern.search(element.attrib.get('title', ''))
|
||||
if not matches:
|
||||
return (0.0, 0.0)
|
||||
return float(matches.group(1)), int(matches.group(2))
|
||||
|
||||
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}']"
|
||||
return xpath
|
||||
|
||||
@classmethod
|
||||
def normalize_text(cls, s: str) -> str:
|
||||
"""Normalize the given text using the NFKC normalization form."""
|
||||
return unicodedata.normalize("NFKC", s)
|
||||
|
||||
def to_pdf(
|
||||
self,
|
||||
*,
|
||||
out_filename: Path,
|
||||
image_filename: Path | None = None,
|
||||
invisible_text: bool = True,
|
||||
) -> None:
|
||||
"""Creates a PDF file with an image superimposed on top of the text.
|
||||
|
||||
Text is positioned according to the bounding box of the lines in
|
||||
the hOCR file.
|
||||
The image need not be identical to the image used to create the hOCR
|
||||
file.
|
||||
It can have a lower resolution, different color mode, etc.
|
||||
|
||||
Arguments:
|
||||
out_filename: Path of PDF to write.
|
||||
image_filename: Image to use for this file. If omitted, the OCR text
|
||||
is shown.
|
||||
invisible_text: If True, text is rendered invisible so that is
|
||||
selectable but never drawn. If False, text is visible and may
|
||||
be seen if the image is skipped or deleted in Acrobat.
|
||||
"""
|
||||
# create the PDF file
|
||||
# page size in points (1/72 in.)
|
||||
canvas = Canvas(page_size=(self.width, self.height))
|
||||
canvas.add_font(self._fontname, self._font)
|
||||
page_matrix = (
|
||||
Matrix()
|
||||
.translated(0, self.height)
|
||||
.scaled(1, -1)
|
||||
.scaled(INCH / self.dpi, INCH / self.dpi)
|
||||
)
|
||||
log.debug(page_matrix)
|
||||
with canvas.do.save_state(cm=page_matrix):
|
||||
self._debug_draw_paragraph_boxes(canvas)
|
||||
found_lines = False
|
||||
for par in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
|
||||
for line in (
|
||||
element
|
||||
for element in par.iterfind(self._child_xpath('span'))
|
||||
if 'class' in element.attrib
|
||||
and element.attrib['class']
|
||||
in {'ocr_header', 'ocr_line', 'ocr_textfloat'}
|
||||
):
|
||||
found_lines = True
|
||||
direction = self._get_text_direction(par)
|
||||
inject_word_breaks = self._get_inject_word_breaks(par)
|
||||
self._do_line(
|
||||
canvas,
|
||||
line,
|
||||
"ocrx_word",
|
||||
invisible_text,
|
||||
direction,
|
||||
inject_word_breaks,
|
||||
)
|
||||
|
||||
if not found_lines:
|
||||
# Tesseract did not report any lines (just words)
|
||||
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
|
||||
direction = self._get_text_direction(root)
|
||||
self._do_line(
|
||||
canvas,
|
||||
root,
|
||||
"ocrx_word",
|
||||
invisible_text,
|
||||
direction,
|
||||
True,
|
||||
)
|
||||
# put the image on the page, scaled to fill the page
|
||||
if image_filename is not None:
|
||||
canvas.do.draw_image(
|
||||
image_filename, 0, 0, width=self.width, height=self.height
|
||||
)
|
||||
|
||||
# finish up the page and save it
|
||||
canvas.to_pdf().save(out_filename)
|
||||
|
||||
def _get_text_direction(self, par):
|
||||
"""Get the text direction of the paragraph.
|
||||
|
||||
Arabic, Hebrew, Persian, are right-to-left languages.
|
||||
"""
|
||||
return (
|
||||
TextDirection.RTL
|
||||
if par.attrib.get('dir', 'ltr') == 'rtl'
|
||||
else TextDirection.LTR
|
||||
)
|
||||
|
||||
def _get_inject_word_breaks(self, par):
|
||||
"""Determine whether word breaks should be injected.
|
||||
|
||||
In Chinese, Japanese, and Korean, word breaks are not injected, because
|
||||
words are usually one or two characters and separators are usually explicit.
|
||||
In all other languages, we inject word breaks to help word segmentation.
|
||||
"""
|
||||
lang = par.attrib.get('lang', '')
|
||||
log.debug(lang)
|
||||
if lang in {'chi_sim', 'chi_tra', 'jpn', 'kor'}:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def polyval(cls, poly, x): # pragma: no cover
|
||||
"""Calculate the value of a polynomial at a point."""
|
||||
return x * poly[0] + poly[1]
|
||||
|
||||
def _do_line(
|
||||
self,
|
||||
canvas: Canvas,
|
||||
line: Element | None,
|
||||
elemclass: str,
|
||||
invisible_text: bool,
|
||||
text_direction: TextDirection,
|
||||
inject_word_breaks: bool,
|
||||
):
|
||||
"""Render the text for a given line.
|
||||
|
||||
The canvas's coordinate system must be configured so that hOCR pixel
|
||||
coordinates are mapped to PDF coordinates.
|
||||
"""
|
||||
if line is None:
|
||||
return
|
||||
line_box = self.element_coordinates(line)
|
||||
if not line_box:
|
||||
return
|
||||
if line_box.ury <= line_box.lly:
|
||||
log.error(
|
||||
"line box is invalid so we cannot render it: box=%s text=%s",
|
||||
line_box,
|
||||
self._get_element_text(line),
|
||||
)
|
||||
return
|
||||
|
||||
self._debug_draw_line_bbox(canvas, line_box)
|
||||
|
||||
# Baseline is a polynomial (usually straight line) that describes the
|
||||
# text baseline relative to the bottom left corner of the line bounding
|
||||
# box.
|
||||
bottom_left_corner = line_box.llx, line_box.ury
|
||||
slope, intercept = self.baseline(line)
|
||||
if abs(slope) < 0.005:
|
||||
slope = 0.0
|
||||
angle = atan(slope)
|
||||
|
||||
# Setup a new coordinate system on the line box's intercept and rotated by
|
||||
# its slope.
|
||||
line_matrix = (
|
||||
Matrix()
|
||||
.translated(*bottom_left_corner)
|
||||
.translated(0, intercept)
|
||||
.rotated(angle / pi * 180)
|
||||
)
|
||||
log.debug(line_matrix)
|
||||
with canvas.do.save_state(cm=line_matrix):
|
||||
text = Text(direction=text_direction)
|
||||
|
||||
# Don't allow the font to break out of the bounding box. Division by
|
||||
# cos_a accounts for extra clearance between the glyph's vertical axis
|
||||
# on a sloped baseline and the edge of the bounding box.
|
||||
line_box_height = abs(line_box.height) / cos(angle)
|
||||
fontsize = line_box_height + intercept
|
||||
text.font(self._fontname, fontsize)
|
||||
text.render_mode(3 if invisible_text else 0)
|
||||
|
||||
self._debug_draw_baseline(
|
||||
canvas, line_matrix.inverse().transform(line_box), 0
|
||||
)
|
||||
|
||||
canvas.do.fill_color(BLACK) # text in black
|
||||
elements = line.findall(self._child_xpath('span', elemclass))
|
||||
for elem, next_elem in pairwise(elements + [None]):
|
||||
self._do_line_word(
|
||||
canvas,
|
||||
line_matrix,
|
||||
text,
|
||||
fontsize,
|
||||
elem,
|
||||
next_elem,
|
||||
text_direction,
|
||||
inject_word_breaks,
|
||||
)
|
||||
canvas.do.draw_text(text)
|
||||
|
||||
def _do_line_word(
|
||||
self,
|
||||
canvas: Canvas,
|
||||
line_matrix: Matrix,
|
||||
text: Text,
|
||||
fontsize: float,
|
||||
elem: Element,
|
||||
next_elem: Element | None,
|
||||
text_direction: TextDirection,
|
||||
inject_word_breaks: bool,
|
||||
):
|
||||
"""Render the text for a single word."""
|
||||
if elem is None:
|
||||
return
|
||||
elemtxt = self.normalize_text(self._get_element_text(elem).strip())
|
||||
if elemtxt == '':
|
||||
return
|
||||
|
||||
hocr_box = self.element_coordinates(elem)
|
||||
if hocr_box is None:
|
||||
return
|
||||
box = line_matrix.inverse().transform(hocr_box)
|
||||
font_width = self._font.text_width(elemtxt, fontsize)
|
||||
|
||||
# Debug sketches
|
||||
self._debug_draw_word_triangle(canvas, box)
|
||||
self._debug_draw_word_bbox(canvas, box)
|
||||
|
||||
# If this word is 0 units wide, our best bet seems to be to suppress this text
|
||||
if text_direction == TextDirection.RTL:
|
||||
log.info("RTL: %s", elemtxt)
|
||||
if font_width > 0:
|
||||
if text_direction == TextDirection.LTR:
|
||||
text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0))
|
||||
elif text_direction == TextDirection.RTL:
|
||||
text.text_transform(Matrix(-1, 0, 0, -1, box.llx + box.width, 0))
|
||||
text.horiz_scale(100 * box.width / font_width)
|
||||
text.show(self._font.text_encode(elemtxt))
|
||||
|
||||
# Get coordinates of the next word (if there is one)
|
||||
hocr_next_box = (
|
||||
self.element_coordinates(next_elem) if next_elem is not None else None
|
||||
)
|
||||
if hocr_next_box is None:
|
||||
return
|
||||
# Render a space this word and the next word. The explicit space helps
|
||||
# PDF viewers identify the word break, and horizontally scaling it to
|
||||
# occupy the space the between the words helps the PDF viewer
|
||||
# avoid combiningthewordstogether.
|
||||
if not inject_word_breaks:
|
||||
return
|
||||
next_box = line_matrix.inverse().transform(hocr_next_box)
|
||||
if text_direction == TextDirection.LTR:
|
||||
space_box = Rectangle(box.urx, box.lly, next_box.llx, next_box.ury)
|
||||
elif text_direction == TextDirection.RTL:
|
||||
space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury)
|
||||
self._debug_draw_space_bbox(canvas, space_box)
|
||||
space_width = self._font.text_width(' ', fontsize)
|
||||
if space_width > 0:
|
||||
if text_direction == TextDirection.LTR:
|
||||
text.text_transform(Matrix(1, 0, 0, -1, space_box.llx, 0))
|
||||
elif text_direction == TextDirection.RTL:
|
||||
text.text_transform(
|
||||
Matrix(-1, 0, 0, -1, space_box.llx + space_box.width, 0)
|
||||
)
|
||||
text.horiz_scale(100 * space_box.width / space_width)
|
||||
text.show(self._font.text_encode(' '))
|
||||
|
||||
def _debug_draw_paragraph_boxes(self, canvas: Canvas, color=CYAN):
|
||||
"""Draw boxes around paragraphs in the document."""
|
||||
if not self.render_options.render_paragraph_bbox: # pragma: no cover
|
||||
return
|
||||
with canvas.do.save_state():
|
||||
# draw box around paragraph
|
||||
canvas.do.stroke_color(color).line_width(0.1)
|
||||
for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
|
||||
elemtxt = self._get_element_text(elem).strip()
|
||||
if len(elemtxt) == 0:
|
||||
continue
|
||||
ocr_par = self.element_coordinates(elem)
|
||||
if ocr_par is None:
|
||||
continue
|
||||
canvas.do.rect(
|
||||
ocr_par.llx, ocr_par.lly, ocr_par.width, ocr_par.height, fill=0
|
||||
)
|
||||
|
||||
def _debug_draw_line_bbox(self, canvas: Canvas, line_box: Rectangle, color=BLUE):
|
||||
"""Render the bounding box of a text line."""
|
||||
if not self.render_options.render_line_bbox: # pragma: no cover
|
||||
return
|
||||
with canvas.do.save_state():
|
||||
canvas.do.stroke_color(color).line_width(0.15).rect(
|
||||
line_box.llx, line_box.lly, line_box.width, line_box.height, fill=0
|
||||
)
|
||||
|
||||
def _debug_draw_word_triangle(
|
||||
self, canvas: Canvas, box: Rectangle, color=RED, line_width=0.1
|
||||
):
|
||||
"""Render a triangle that conveys word height and drawing direction."""
|
||||
if not self.render_options.render_triangle: # pragma: no cover
|
||||
return
|
||||
with canvas.do.save_state():
|
||||
canvas.do.stroke_color(color).line_width(line_width).line(
|
||||
box.llx, box.lly, box.urx, box.lly
|
||||
).line(box.urx, box.lly, box.llx, box.ury).line(
|
||||
box.llx, box.lly, box.llx, box.ury
|
||||
)
|
||||
|
||||
def _debug_draw_word_bbox(
|
||||
self, canvas: Canvas, box: Rectangle, color=GREEN, line_width=0.1
|
||||
):
|
||||
"""Render a box depicting the word."""
|
||||
if not self.render_options.render_word_bbox: # pragma: no cover
|
||||
return
|
||||
with canvas.do.save_state():
|
||||
canvas.do.stroke_color(color).line_width(line_width).rect(
|
||||
box.llx, box.lly, box.width, box.height, fill=0
|
||||
)
|
||||
|
||||
def _debug_draw_space_bbox(
|
||||
self, canvas: Canvas, box: Rectangle, color=DARKGREEN, line_width=0.1
|
||||
):
|
||||
"""Render a box depicting the space between two words."""
|
||||
if not self.render_options.render_space_bbox: # pragma: no cover
|
||||
return
|
||||
with canvas.do.save_state():
|
||||
canvas.do.fill_color(color).line_width(line_width).rect(
|
||||
box.llx, box.lly, box.width, box.height, fill=1
|
||||
)
|
||||
|
||||
def _debug_draw_baseline(
|
||||
self,
|
||||
canvas: Canvas,
|
||||
line_box: Rectangle,
|
||||
baseline_lly,
|
||||
color=MAGENTA,
|
||||
line_width=0.25,
|
||||
):
|
||||
"""Render the text baseline."""
|
||||
if not self.render_options.render_baseline:
|
||||
return
|
||||
with canvas.do.save_state():
|
||||
canvas.do.stroke_color(color).line_width(line_width).line(
|
||||
line_box.llx,
|
||||
baseline_lly,
|
||||
line_box.urx,
|
||||
baseline_lly,
|
||||
)
|
||||
@@ -7,17 +7,9 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from math import floor, sqrt
|
||||
from typing import Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# While from __future__ import annotations, we use singledispatch here, which
|
||||
# does not support annotations. Disable check about using old-style typing
|
||||
# until Python 3.10, OR when drop singledispatch in ocrmypdf 15.
|
||||
# ruff: noqa: UP006
|
||||
# ruff: noqa: UP007
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -38,9 +30,9 @@ def _calculate_downsample(
|
||||
image_size: tuple[int, int],
|
||||
bytes_per_pixel: int,
|
||||
*,
|
||||
max_size: Optional[tuple[int, int]] = None,
|
||||
max_pixels: Optional[int] = None,
|
||||
max_bytes: Optional[int] = None,
|
||||
max_size: tuple[int, int] | None = None,
|
||||
max_pixels: int | None = None,
|
||||
max_bytes: int | None = None,
|
||||
) -> tuple[int, int]:
|
||||
"""Calculate image size required to downsample an image to fit limits.
|
||||
|
||||
@@ -98,9 +90,9 @@ def _calculate_downsample(
|
||||
def calculate_downsample(
|
||||
image: Image.Image,
|
||||
*,
|
||||
max_size: Optional[tuple[int, int]] = None,
|
||||
max_pixels: Optional[int] = None,
|
||||
max_bytes: Optional[int] = None,
|
||||
max_size: tuple[int, int] | None = None,
|
||||
max_pixels: int | None = None,
|
||||
max_bytes: int | None = None,
|
||||
) -> tuple[int, int]:
|
||||
"""Calculate image size required to downsample an image to fit limits.
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ Derived from
|
||||
https://www.loc.gov/standards/iso639-2/ascii_8bits.html
|
||||
"""
|
||||
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class ISOCodeData(NamedTuple):
|
||||
"""Data for a single ISO 639 code."""
|
||||
|
||||
alt: str
|
||||
alpha_2: str
|
||||
english: str
|
||||
@@ -168,8 +168,10 @@ ISO_639_3 = {
|
||||
'chu': ISOCodeData(
|
||||
'',
|
||||
'cu',
|
||||
('Church Slavic; Old Slavonic; Church Slavonic;'
|
||||
' Old Bulgarian; Old Church Slavonic'),
|
||||
(
|
||||
'Church Slavic; Old Slavonic; Church Slavonic;'
|
||||
' Old Bulgarian; Old Church Slavonic'
|
||||
),
|
||||
"slavon d'église; vieux slave; slavon liturgique; vieux bulgare",
|
||||
),
|
||||
'chv': ISOCodeData('', 'cv', 'Chuvash', 'tchouvache'),
|
||||
|
||||
+17
-14
@@ -3,7 +3,6 @@
|
||||
|
||||
"""Post-processing image optimization of OCR PDFs."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -11,11 +10,10 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator, MutableSet, Sequence
|
||||
from collections.abc import Callable, Iterator, MutableSet, Sequence
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, NamedTuple, NewType
|
||||
from warnings import warn
|
||||
from typing import Any, NamedTuple, NewType
|
||||
from zlib import compress
|
||||
|
||||
import img2pdf
|
||||
@@ -30,6 +28,7 @@ from pikepdf import (
|
||||
Stream,
|
||||
UnsupportedImageTypeError,
|
||||
)
|
||||
from pikepdf.models.image import HifiPrintImageNotTranscodableError
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf._concurrent import Executor, SerialExecutor
|
||||
@@ -71,15 +70,9 @@ def jpg_name(root: Path, xref: Xref) -> Path:
|
||||
|
||||
|
||||
def extract_image_filter(
|
||||
image: Stream, xref: Xref, *args
|
||||
image: Stream, xref: Xref
|
||||
) -> tuple[PdfImage, tuple[Name, Object]] | None:
|
||||
"""Determine if an image is extractable."""
|
||||
if isinstance(image, Pdf):
|
||||
# Support deprecated old function signature
|
||||
# TODO Remove for v16 and drop *args from current function signature
|
||||
image, xref = args[0], args[1]
|
||||
warn("extract_image_filter: pdf, root parameters ignored", DeprecationWarning)
|
||||
|
||||
if image.Subtype != Name.Image:
|
||||
return None
|
||||
if image.Length < 100:
|
||||
@@ -97,6 +90,7 @@ def extract_image_filter(
|
||||
if (
|
||||
len(pim.filter_decodeparms) == 2
|
||||
and first_filtdp[0] == Name.FlateDecode
|
||||
and first_filtdp[1] is not None
|
||||
and first_filtdp[1].get(Name.Predictor, 1) == 1
|
||||
and second_filtdp[0] == Name.DCTDecode
|
||||
and not second_filtdp[1]
|
||||
@@ -161,6 +155,13 @@ def extract_image_jbig2(
|
||||
with imgname.open('wb') as f:
|
||||
ext = pim.extract_to(stream=f)
|
||||
imgname.rename(imgname.with_suffix(ext))
|
||||
except NotImplementedError as e:
|
||||
if '/Decode' in str(e):
|
||||
log.debug(
|
||||
f"xref {xref}: skipping image with unsupported Decode table"
|
||||
)
|
||||
return None
|
||||
raise
|
||||
except UnsupportedImageTypeError:
|
||||
return None
|
||||
finally:
|
||||
@@ -200,7 +201,7 @@ def extract_image_generic(
|
||||
with imgname.open('wb') as f:
|
||||
ext = pim.extract_to(stream=f)
|
||||
imgname.rename(imgname.with_suffix(ext))
|
||||
except UnsupportedImageTypeError:
|
||||
except (UnsupportedImageTypeError, HifiPrintImageNotTranscodableError):
|
||||
return None
|
||||
return XrefExt(xref, ext)
|
||||
elif (
|
||||
@@ -256,6 +257,9 @@ def _find_image_xrefs_container(
|
||||
for _imname, image in dict(xobjs).items():
|
||||
if image.objgen[1] != 0:
|
||||
continue # Ignore images in an incremental PDF
|
||||
xref = Xref(image.objgen[0])
|
||||
if xref in include_xrefs or xref in exclude_xrefs:
|
||||
continue # Already processed
|
||||
if Name.Subtype in image and image.Subtype == Name.Form:
|
||||
# Recurse into Form XObjects
|
||||
log.debug(f"Recursing into Form XObject {_imname} in page {pageno}")
|
||||
@@ -269,7 +273,6 @@ def _find_image_xrefs_container(
|
||||
depth + 1,
|
||||
)
|
||||
continue
|
||||
xref = Xref(image.objgen[0])
|
||||
if Name.SMask in image:
|
||||
# Ignore soft masks
|
||||
smask_xref = Xref(image.SMask.objgen[0])
|
||||
@@ -376,7 +379,7 @@ def _produce_jbig2_images(
|
||||
options.jbig2_threshold,
|
||||
)
|
||||
|
||||
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
|
||||
def jbig2_single_args(root: Path, 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
|
||||
|
||||
@@ -90,7 +90,7 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
|
||||
icc: ICC identifier such as 'sRGB'
|
||||
References:
|
||||
Adobe PDFMARK Reference:
|
||||
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
||||
https://opensource.adobe.com/dc-acrobat-sdk-docs/library/pdfmark/
|
||||
"""
|
||||
if icc != 'sRGB':
|
||||
raise NotImplementedError("Only supporting sRGB")
|
||||
|
||||
@@ -10,9 +10,8 @@ import atexit
|
||||
import logging
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from collections.abc import Container, Iterable, Iterator, Mapping, Sequence
|
||||
from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence
|
||||
from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from enum import Enum, auto
|
||||
@@ -20,18 +19,18 @@ from functools import partial
|
||||
from math import hypot, inf, isclose
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Callable, NamedTuple
|
||||
from typing import NamedTuple
|
||||
from warnings import warn
|
||||
|
||||
from pdfminer.layout import LTPage, LTTextBox
|
||||
from pikepdf import (
|
||||
Matrix,
|
||||
Name,
|
||||
Object,
|
||||
Page,
|
||||
Pdf,
|
||||
PdfImage,
|
||||
PdfInlineImage,
|
||||
PdfMatrix,
|
||||
Stream,
|
||||
UnsupportedImageTypeError,
|
||||
parse_content_stream,
|
||||
@@ -209,7 +208,7 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
||||
CTM unchanged.
|
||||
"""
|
||||
stack = []
|
||||
ctm = PdfMatrix(initial_shorthand)
|
||||
ctm = Matrix(initial_shorthand)
|
||||
xobject_settings: list[XobjectSettings] = []
|
||||
inline_images: list[InlineSettings] = []
|
||||
name_index = defaultdict(lambda: [])
|
||||
@@ -240,7 +239,13 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
|
||||
# to do. Just pretend nothing happened, keep calm and carry on.
|
||||
warn("PDF graphics stack underflowed - PDF may be malformed")
|
||||
elif operator == 'cm':
|
||||
ctm = PdfMatrix(operands) @ ctm
|
||||
try:
|
||||
ctm = Matrix(operands) @ ctm
|
||||
except ValueError:
|
||||
raise InputFileError(
|
||||
"PDF content stream is corrupt - this PDF is malformed. "
|
||||
"Use a PDF editor that is capable of visually inspecting the PDF."
|
||||
)
|
||||
elif operator == 'Do':
|
||||
image_name = operands[0]
|
||||
settings = XobjectSettings(
|
||||
@@ -486,7 +491,7 @@ class ImageInfo:
|
||||
"""Physical area of the image in square inches."""
|
||||
if not self.renderable:
|
||||
return 0.0
|
||||
return float(self.width * self.dpi.x * self.height * self.dpi.y)
|
||||
return float((self.width / self.dpi.x) * (self.height / self.dpi.y))
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the image."""
|
||||
@@ -614,12 +619,12 @@ def _process_content_streams(
|
||||
):
|
||||
# Set the CTM to the state it was when the "Do" operator was
|
||||
# encountered that is drawing this instance of the Form XObject
|
||||
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
|
||||
ctm = Matrix(shorthand) if shorthand else Matrix()
|
||||
|
||||
# A Form XObject may provide its own matrix to map form space into
|
||||
# user space. Get this if one exists
|
||||
form_shorthand = container.get(Name.Matrix, PdfMatrix.identity())
|
||||
form_matrix = PdfMatrix(form_shorthand)
|
||||
form_shorthand = container.get(Name.Matrix, Matrix())
|
||||
form_matrix = Matrix(form_shorthand)
|
||||
|
||||
# Concatenate form matrix with CTM to ensure CTM is correct for
|
||||
# drawing this instance of the XObject
|
||||
@@ -854,6 +859,12 @@ class PageInfo:
|
||||
width_pt = mediabox[2] - mediabox[0]
|
||||
height_pt = mediabox[3] - mediabox[1]
|
||||
|
||||
# self._artbox = [float(d) for d in page.artbox.as_list()]
|
||||
# self._bleedbox = [float(d) for d in page.bleedbox.as_list()]
|
||||
self._cropbox = [float(d) for d in page.cropbox.as_list()]
|
||||
self._mediabox = [float(d) for d in page.mediabox.as_list()]
|
||||
self._trimbox = [float(d) for d in page.trimbox.as_list()]
|
||||
|
||||
check_this_page = pageno in check_pages
|
||||
|
||||
if check_this_page and detailed_analysis:
|
||||
@@ -970,6 +981,21 @@ class PageInfo:
|
||||
else:
|
||||
raise ValueError("rotation must be a cardinal angle")
|
||||
|
||||
@property
|
||||
def cropbox(self) -> FloatRect:
|
||||
"""Return cropbox of page in PDF coordinates."""
|
||||
return self._cropbox
|
||||
|
||||
@property
|
||||
def mediabox(self) -> FloatRect:
|
||||
"""Return mediabox of page in PDF coordinates."""
|
||||
return self._mediabox
|
||||
|
||||
@property
|
||||
def trimbox(self) -> FloatRect:
|
||||
"""Return trimbox of page in PDF coordinates."""
|
||||
return self._trimbox
|
||||
|
||||
@property
|
||||
def images(self) -> list[ImageInfo]:
|
||||
"""Return images."""
|
||||
@@ -1039,12 +1065,7 @@ class PageInfo:
|
||||
|
||||
weights = [area / total_drawn_area for area in image_areas]
|
||||
# Calculate harmonic mean of DPIs weighted by area
|
||||
if sys.version_info >= (3, 10):
|
||||
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
|
||||
else:
|
||||
weighted_dpi = sum(weights) / sum(
|
||||
weight / dpi for weight, dpi in zip(weights, image_dpis)
|
||||
)
|
||||
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
|
||||
max_dpi = max(image_dpis)
|
||||
dpi_average_max_ratio = weighted_dpi / max_dpi
|
||||
|
||||
@@ -1155,7 +1176,7 @@ class PdfInfo:
|
||||
@property
|
||||
def filename(self) -> str | Path:
|
||||
"""Return filename of PDF."""
|
||||
if not isinstance(self._infile, (str, Path)):
|
||||
if not isinstance(self._infile, str | Path):
|
||||
raise NotImplementedError("can't get filename from stream")
|
||||
return self._infile
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Iterator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from math import copysign
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pdfminer
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
"""Utilities to measure OCR quality."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
@@ -8,12 +8,11 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
||||
from subprocess import run as subprocess_run
|
||||
from typing import Callable, Union
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
@@ -23,7 +22,7 @@ from ocrmypdf.exceptions import MissingDependencyError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
Args = Sequence[Union[Path, str]]
|
||||
Args = Sequence[Path | str]
|
||||
OsEnviron = os._Environ # pylint: disable=protected-access
|
||||
|
||||
|
||||
@@ -172,6 +171,7 @@ def get_version(
|
||||
) from e
|
||||
except CalledProcessError as e:
|
||||
if e.returncode != 0:
|
||||
log.exception(e)
|
||||
raise MissingDependencyError(
|
||||
f"Ran program '{program}' but it exited with an error:\n{e.output}"
|
||||
) from e
|
||||
|
||||
@@ -9,18 +9,13 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, TypeVar
|
||||
from typing import Any, TypeAlias, TypeVar
|
||||
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias # pragma: no cover
|
||||
|
||||
if sys.platform == 'win32':
|
||||
# mypy understands 'if sys.platform' better than try/except ModuleNotFoundError
|
||||
import winreg # pylint: disable=import-error
|
||||
@@ -84,7 +79,7 @@ def registry_path_ghostscript(env=None) -> Iterator[Path]:
|
||||
registry_subkeys(k), key=ghostscript_version_key, default=(0, 0, 0)
|
||||
)
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
|
||||
winreg.HKEY_LOCAL_MACHINE, rf"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
|
||||
) as k:
|
||||
for _, gs_path, _ in registry_values(k):
|
||||
yield Path(gs_path) / 'bin'
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||
<meta name='ocr-system' content='tesseract 5.3.2' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.jcdzf4up/000001_ocr.png"; bbox 0 0 4000 2864; ppageno 0; scan_res 2400 2400'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 251 146 2173 237">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 251 146 2173 237">
|
||||
<span class='ocr_line' id='line_1_1' title="bbox 251 146 2173 237; baseline -0.006 5; x_size 99.418808; x_descenders 9.4188042; x_ascenders 32">
|
||||
<span class='ocrx_word' id='word_1_1' title='bbox 251 154 274 176; x_wconf 89'>i</span>
|
||||
<span class='ocrx_word' id='word_1_2' title='bbox 1080 146 1152 237; x_wconf 89'>a</span>
|
||||
<span class='ocrx_word' id='word_1_3' title='bbox 1254 153 1406 235; x_wconf 92'>la</span>
|
||||
<span class='ocrx_word' id='word_1_4' title='bbox 1500 153 2173 235; x_wconf 95'>Waterman</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_separator' id='block_1_2' title="bbox 135 202 2180 295"></div>
|
||||
<div class='ocr_carea' id='block_1_3' title="bbox 145 414 2929 1221">
|
||||
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 145 414 1154 516">
|
||||
<span class='ocr_line' id='line_1_2' title="bbox 145 414 1154 516; baseline -0.005 -4; x_size 122.38636; x_descenders 24.386362; x_ascenders 40">
|
||||
<span class='ocrx_word' id='word_1_5' title='bbox 145 414 211 512; x_wconf 60'>4</span>
|
||||
<span class='ocrx_word' id='word_1_6' title='bbox 318 453 551 516; x_wconf 93'>ons</span>
|
||||
<span class='ocrx_word' id='word_1_7' title='bbox 660 430 1154 512; x_wconf 91'>linzen</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 146 568 1239 667">
|
||||
<span class='ocr_line' id='line_1_3' title="bbox 146 568 1239 667; baseline -0.004 -16; x_size 99; x_descenders 17; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_8' title='bbox 146 569 209 667; x_wconf 94'>3</span>
|
||||
<span class='ocrx_word' id='word_1_9' title='bbox 323 568 729 652; x_wconf 83'>liter</span>
|
||||
<span class='ocrx_word' id='word_1_10' title='bbox 821 569 1239 650; x_wconf 96'>water</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 148 705 645 804">
|
||||
<span class='ocr_line' id='line_1_4' title="bbox 148 705 645 804; baseline -0.016 -14; x_size 103; x_descenders 19; x_ascenders 27">
|
||||
<span class='ocrx_word' id='word_1_11' title='bbox 148 706 211 804; x_wconf 88'>3</span>
|
||||
<span class='ocrx_word' id='word_1_12' title='bbox 311 705 645 789; x_wconf 52'>uien</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 147 832 1154 950">
|
||||
<span class='ocr_line' id='line_1_5' title="bbox 147 832 1154 950; baseline -0.004 -27; x_size 118; x_descenders 28; x_ascenders 30">
|
||||
<span class='ocrx_word' id='word_1_13' title='bbox 147 832 623 950; x_wconf 91'>bloem,</span>
|
||||
<span class='ocrx_word' id='word_1_14' title='bbox 737 843 1154 924; x_wconf 91'>boter</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 148 979 1241 1087">
|
||||
<span class='ocr_line' id='line_1_6' title="bbox 148 979 1241 1087; baseline -0.005 -21; x_size 107; x_descenders 24; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_15' title='bbox 148 983 215 1066; x_wconf 88'>2</span>
|
||||
<span class='ocrx_word' id='word_1_16' title='bbox 312 983 807 1087; x_wconf 88'>kopjes</span>
|
||||
<span class='ocrx_word' id='word_1_17' title='bbox 905 979 1241 1062; x_wconf 92'>melk</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 154 1109 2929 1221">
|
||||
<span class='ocr_line' id='line_1_7' title="bbox 154 1109 2929 1221; baseline -0.003 -21; x_size 112; x_descenders 28; x_ascenders 26">
|
||||
<span class='ocrx_word' id='word_1_18' title='bbox 154 1117 791 1221; x_wconf 92'>laurier,</span>
|
||||
<span class='ocrx_word' id='word_1_19' title='bbox 906 1111 1810 1219; x_wconf 90'>kruidnagel,</span>
|
||||
<span class='ocrx_word' id='word_1_20' title='bbox 1927 1109 2490 1219; x_wconf 90'>kerrie,</span>
|
||||
<span class='ocrx_word' id='word_1_21' title='bbox 2615 1110 2929 1195; x_wconf 91'>zout</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_carea' id='block_1_4' title="bbox 147 1383 3706 2731">
|
||||
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 147 1383 3706 2731">
|
||||
<span class='ocr_line' id='line_1_8' title="bbox 149 1383 3612 1474; baseline -0.003 -2; x_size 107.01524; x_descenders 25.01524; x_ascenders 21">
|
||||
<span class='ocrx_word' id='word_1_22' title='bbox 149 1395 303 1474; x_wconf 93'>De</span>
|
||||
<span class='ocrx_word' id='word_1_23' title='bbox 411 1390 902 1473; x_wconf 80'>linzgen</span>
|
||||
<span class='ocrx_word' id='word_1_24' title='bbox 996 1409 1497 1470; x_wconf 89'>wassen</span>
|
||||
<span class='ocrx_word' id='word_1_25' title='bbox 1595 1409 1752 1470; x_wconf 88'>en</span>
|
||||
<span class='ocrx_word' id='word_1_26' title='bbox 1853 1385 2166 1467; x_wconf 75'>in-l</span>
|
||||
<span class='ocrx_word' id='word_1_27' title='bbox 2275 1383 2684 1466; x_wconf 93'>liter</span>
|
||||
<span class='ocrx_word' id='word_1_28' title='bbox 2775 1383 3278 1464; x_wconf 91'>kokend</span>
|
||||
<span class='ocrx_word' id='word_1_29' title='bbox 3368 1401 3612 1462; x_wconf 90'>wa-</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_9' title="bbox 157 1516 3520 1632; baseline -0.004 -21; x_size 110; x_descenders 25; x_ascenders 25">
|
||||
<span class='ocrx_word' id='word_1_30' title='bbox 157 1531 394 1611; x_wconf 94'>ter</span>
|
||||
<span class='ocrx_word' id='word_1_31' title='bbox 495 1527 558 1609; x_wconf 93'>1</span>
|
||||
<span class='ocrx_word' id='word_1_32' title='bbox 658 1530 903 1632; x_wconf 84'>dag</span>
|
||||
<span class='ocrx_word' id='word_1_33' title='bbox 1002 1527 1415 1609; x_wconf 90'>laten</span>
|
||||
<span class='ocrx_word' id='word_1_34' title='bbox 1505 1525 1979 1611; x_wconf 58'>weken,</span>
|
||||
<span class='ocrx_word' id='word_1_35' title='bbox 2103 1521 2167 1603; x_wconf 96'>2</span>
|
||||
<span class='ocrx_word' id='word_1_36' title='bbox 2275 1518 2683 1603; x_wconf 83'>liter</span>
|
||||
<span class='ocrx_word' id='word_1_37' title='bbox 2777 1519 3194 1601; x_wconf 96'>water</span>
|
||||
<span class='ocrx_word' id='word_1_38' title='bbox 3286 1516 3520 1599; x_wconf 89'>bij</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_10' title="bbox 152 1651 3616 1767; baseline -0.004 -21; x_size 110; x_descenders 27; x_ascenders 23">
|
||||
<span class='ocrx_word' id='word_1_39' title='bbox 152 1668 302 1747; x_wconf 92'>de</span>
|
||||
<span class='ocrx_word' id='word_1_40' title='bbox 407 1662 905 1747; x_wconf 91'>linzen</span>
|
||||
<span class='ocrx_word' id='word_1_41' title='bbox 996 1682 1559 1767; x_wconf 92'>voegen,</span>
|
||||
<span class='ocrx_word' id='word_1_42' title='bbox 1680 1659 2176 1741; x_wconf 96'>zonder</span>
|
||||
<span class='ocrx_word' id='word_1_43' title='bbox 2267 1655 2506 1737; x_wconf 96'>het</span>
|
||||
<span class='ocrx_word' id='word_1_44' title='bbox 2606 1655 3023 1737; x_wconf 92'>water</span>
|
||||
<span class='ocrx_word' id='word_1_45' title='bbox 3116 1651 3616 1735; x_wconf 91'>waarin</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_11' title="bbox 153 1782 3704 1905; baseline -0.004 -20; x_size 117; x_descenders 23; x_ascenders 34">
|
||||
<span class='ocrx_word' id='word_1_46' title='bbox 153 1824 305 1885; x_wconf 91'>ze</span>
|
||||
<span class='ocrx_word' id='word_1_47' title='bbox 407 1800 979 1905; x_wconf 85'>geweekt</span>
|
||||
<span class='ocrx_word' id='word_1_48' title='bbox 1089 1797 1412 1900; x_wconf 96'>zijn</span>
|
||||
<span class='ocrx_word' id='word_1_49' title='bbox 1510 1796 1672 1878; x_wconf 96'>af</span>
|
||||
<span class='ocrx_word' id='word_1_50' title='bbox 1770 1782 1914 1876; x_wconf 93'>te</span>
|
||||
<span class='ocrx_word' id='word_1_51' title='bbox 2019 1792 2576 1899; x_wconf 54'>gieten,</span>
|
||||
<span class='ocrx_word' id='word_1_52' title='bbox 2690 1790 2850 1874; x_wconf 93'>De</span>
|
||||
<span class='ocrx_word' id='word_1_53' title='bbox 2948 1791 3357 1872; x_wconf 89'>helft</span>
|
||||
<span class='ocrx_word' id='word_1_54' title='bbox 3452 1811 3704 1873; x_wconf 96'>van</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_12' title="bbox 151 1928 3593 2035; baseline -0.003 -13; x_size 109; x_descenders 25; x_ascenders 25">
|
||||
<span class='ocrx_word' id='word_1_55' title='bbox 151 1942 305 2024; x_wconf 92'>de</span>
|
||||
<span class='ocrx_word' id='word_1_56' title='bbox 403 1940 735 2021; x_wconf 89'>uien</span>
|
||||
<span class='ocrx_word' id='word_1_57' title='bbox 829 1938 1330 2022; x_wconf 91'>bakken</span>
|
||||
<span class='ocrx_word' id='word_1_58' title='bbox 1419 1939 1659 2018; x_wconf 92'>met</span>
|
||||
<span class='ocrx_word' id='word_1_59' title='bbox 1770 1933 2346 2016; x_wconf 91'>laurier</span>
|
||||
<span class='ocrx_word' id='word_1_60' title='bbox 2447 1953 2603 2014; x_wconf 91'>en</span>
|
||||
<span class='ocrx_word' id='word_1_61' title='bbox 2691 1928 3593 2035; x_wconf 63'>Kruidnagel.</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_13' title="bbox 151 2067 3451 2180; baseline -0.004 -21; x_size 108; x_descenders 25; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_62' title='bbox 151 2076 473 2159; x_wconf 92'>Alle</span>
|
||||
<span class='ocrx_word' id='word_1_63' title='bbox 569 2076 965 2180; x_wconf 88'>uien,</span>
|
||||
<span class='ocrx_word' id='word_1_64' title='bbox 1079 2072 1578 2156; x_wconf 90'>kerrie</span>
|
||||
<span class='ocrx_word' id='word_1_65' title='bbox 1685 2092 1837 2153; x_wconf 93'>en</span>
|
||||
<span class='ocrx_word' id='word_1_66' title='bbox 1938 2072 2254 2153; x_wconf 81'>zgout</span>
|
||||
<span class='ocrx_word' id='word_1_67' title='bbox 2355 2068 2593 2172; x_wconf 43'>bij</span>
|
||||
<span class='ocrx_word' id='word_1_68' title='bbox 2696 2071 2850 2150; x_wconf 91'>de</span>
|
||||
<span class='ocrx_word' id='word_1_69' title='bbox 2957 2067 3451 2150; x_wconf 85'>linzen</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_14' title="bbox 147 2205 3614 2318; baseline -0.003 -23; x_size 106; x_descenders 22; x_ascenders 25">
|
||||
<span class='ocrx_word' id='word_1_70' title='bbox 147 2234 711 2318; x_wconf 88'>voegen,</span>
|
||||
<span class='ocrx_word' id='word_1_71' title='bbox 826 2210 1234 2295; x_wconf 95'>Alles</span>
|
||||
<span class='ocrx_word' id='word_1_72' title='bbox 1342 2232 1588 2295; x_wconf 95'>aan</span>
|
||||
<span class='ocrx_word' id='word_1_73' title='bbox 1679 2212 1831 2291; x_wconf 96'>de</span>
|
||||
<span class='ocrx_word' id='word_1_74' title='bbox 1928 2208 2263 2290; x_wconf 93'>kook</span>
|
||||
<span class='ocrx_word' id='word_1_75' title='bbox 2355 2206 3000 2308; x_wconf 54'>brengen,</span>
|
||||
<span class='ocrx_word' id='word_1_76' title='bbox 3117 2205 3367 2285; x_wconf 95'>Van</span>
|
||||
<span class='ocrx_word' id='word_1_77' title='bbox 3462 2206 3614 2287; x_wconf 95'>de</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_15' title="bbox 152 2341 3706 2447; baseline -0.003 -18; x_size 107; x_descenders 24; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_78' title='bbox 152 2352 565 2434; x_wconf 91'>bloem</span>
|
||||
<span class='ocrx_word' id='word_1_79' title='bbox 655 2351 896 2431; x_wconf 92'>met</span>
|
||||
<span class='ocrx_word' id='word_1_80' title='bbox 997 2349 1669 2431; x_wconf 90'>boter</span>
|
||||
<span class='ocrx_word' id='word_1_81' title='bbox 1514 2337 1683 2455; x_wconf 91'>en</span>
|
||||
<span class='ocrx_word' id='word_1_82' title='bbox 1757 2343 2095 2426; x_wconf 88'>melk</span>
|
||||
<span class='ocrx_word' id='word_1_83' title='bbox 2193 2364 2432 2427; x_wconf 93'>een</span>
|
||||
<span class='ocrx_word' id='word_1_84' title='bbox 2527 2341 2935 2447; x_wconf 90'>papje</span>
|
||||
<span class='ocrx_word' id='word_1_85' title='bbox 3029 2341 3453 2422; x_wconf 96'>maken</span>
|
||||
<span class='ocrx_word' id='word_1_86' title='bbox 3549 2362 3706 2422; x_wconf 95'>en</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_16' title="bbox 149 2477 3619 2586; baseline -0.003 -16; x_size 107; x_descenders 24; x_ascenders 23">
|
||||
<span class='ocrx_word' id='word_1_87' title='bbox 149 2489 650 2571; x_wconf 86'>verder</span>
|
||||
<span class='ocrx_word' id='word_1_88' title='bbox 750 2486 1330 2570; x_wconf 90'>afmaken</span>
|
||||
<span class='ocrx_word' id='word_1_89' title='bbox 1420 2485 1660 2567; x_wconf 96'>met</span>
|
||||
<span class='ocrx_word' id='word_1_90' title='bbox 1765 2485 1917 2566; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_91' title='bbox 2025 2505 2409 2586; x_wconf 86'>soep,</span>
|
||||
<span class='ocrx_word' id='word_1_92' title='bbox 2521 2479 2764 2564; x_wconf 96'>Als</span>
|
||||
<span class='ocrx_word' id='word_1_93' title='bbox 2868 2480 3021 2561; x_wconf 92'>de</span>
|
||||
<span class='ocrx_word' id='word_1_94' title='bbox 3127 2477 3619 2562; x_wconf 91'>linzen</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_17' title="bbox 155 2619 2412 2731; baseline -0.005 -20; x_size 98; x_descenders 15; x_ascenders 23">
|
||||
<span class='ocrx_word' id='word_1_95' title='bbox 155 2647 481 2731; x_wconf 35'>gfgaar</span>
|
||||
<span class='ocrx_word' id='word_1_96' title='bbox 581 2624 909 2728; x_wconf 87'>Zijn</span>
|
||||
<span class='ocrx_word' id='word_1_97' title='bbox 1005 2623 1153 2707; x_wconf 95'>is</span>
|
||||
<span class='ocrx_word' id='word_1_98' title='bbox 1255 2624 1409 2706; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_99' title='bbox 1522 2642 1840 2726; x_wconf 91'>soep</span>
|
||||
<span class='ocrx_word' id='word_1_100' title='bbox 1929 2619 2412 2709; x_wconf 89'>klaar.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
i a la Waterman
|
||||
|
||||
4 ons linzen
|
||||
|
||||
3 liter water
|
||||
|
||||
3 uien
|
||||
|
||||
bloem, boter
|
||||
|
||||
2 kopjes melk
|
||||
|
||||
laurier, kruidnagel, kerrie, zout
|
||||
|
||||
De linzgen wassen en in-l liter kokend wa-
|
||||
ter 1 dag laten weken, 2 liter water bij
|
||||
de linzen voegen, zonder het water waarin
|
||||
ze geweekt zijn af te gieten, De helft van
|
||||
de uien bakken met laurier en Kruidnagel.
|
||||
Alle uien, kerrie en zgout bij de linzen
|
||||
voegen, Alles aan de kook brengen, Van de
|
||||
bloem met boter en melk een papje maken en
|
||||
verder afmaken met de soep, Als de linzen
|
||||
gfgaar Zijn is de soep klaar.
|
||||
BIN
Binary file not shown.
+89
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||
<meta name='ocr-system' content='tesseract 5.3.2' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.vh2to5iv/000001_ocr.png"; bbox 0 0 640 682; ppageno 0; scan_res 230 230'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 365 15 429 29">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 365 15 429 29">
|
||||
<span class='ocr_line' id='line_1_1' title="bbox 365 15 429 29; baseline 0 0; x_size 19.238094; x_descenders 5.2380953; x_ascenders 4">
|
||||
<span class='ocrx_word' id='word_1_1' title='bbox 365 15 429 29; x_wconf 92'>Tarnose</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_photo' id='block_1_2' title="bbox 255 38 616 72"></div>
|
||||
<div class='ocr_photo' id='block_1_3' title="bbox 273 38 349 57"></div>
|
||||
<div class='ocr_carea' id='block_1_4' title="bbox 186 20 244 49">
|
||||
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 186 20 345 49">
|
||||
<span class='ocr_line' id='line_1_2' title="bbox 186 20 244 49; baseline -0.006 -10; x_size 20.454546; x_descenders 5.4545455; x_ascenders 5">
|
||||
<span class='ocrx_word' id='word_1_2' title='bbox 186 20 244 49; x_wconf 92'>Bokale</span>
|
||||
<span class='ocrx_word' id='word_1_3' title='bbox 299 28 345 46; x_wconf 42'>oa</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_photo' id='block_1_5' title="bbox 64 38 186 72"></div>
|
||||
<div class='ocr_carea' id='block_1_6' title="bbox 537 276 612 291">
|
||||
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 537 276 612 291">
|
||||
<span class='ocr_line' id='line_1_3' title="bbox 537 276 612 291; baseline 0 0; x_size 20.238094; x_descenders 5.2380953; x_ascenders 5">
|
||||
<span class='ocrx_word' id='word_1_4' title='bbox 537 276 612 291; x_wconf 92'>Lehuntze</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_photo' id='block_1_7' title="bbox 20 269 616 314"></div>
|
||||
<div class='ocr_carea' id='block_1_8' title="bbox 480 323 550 341">
|
||||
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 480 323 550 341">
|
||||
<span class='ocr_line' id='line_1_4' title="bbox 480 323 550 341; baseline 0 -4; x_size 18; x_descenders 4; x_ascenders 4">
|
||||
<span class='ocrx_word' id='word_1_5' title='bbox 480 323 550 341; x_wconf 91'>Mugerre</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_photo' id='block_1_9' title="bbox 44 574 197 623"></div>
|
||||
<div class='ocr_carea' id='block_1_10' title="bbox 204 542 603 574">
|
||||
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 204 542 603 574">
|
||||
<span class='ocr_line' id='line_1_5' title="bbox 204 542 603 574; baseline 0.018 -11; x_size 23.26087; x_descenders 5.2608695; x_ascenders 7">
|
||||
<span class='ocrx_word' id='word_1_6' title='bbox 204 542 295 574; x_wconf 91'>Milafranga</span>
|
||||
<span class='ocrx_word' id='word_1_7' title='bbox 439 552 603 569; x_wconf 90'>Komunikabideak</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_carea' id='block_1_11' title="bbox 220 584 616 619">
|
||||
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 220 584 616 625">
|
||||
<span class='ocr_line' id='line_1_6' title="bbox 220 584 616 619; baseline -0.005 -1; x_size 43.666668; x_descenders 10.916667; x_ascenders 10.916667">
|
||||
<span class='ocrx_word' id='word_1_8' title='bbox 220 585 404 619; x_wconf 2'>BAIONA</span>
|
||||
<span class='ocrx_word' id='word_1_9' title='bbox 468 584 576 625; x_wconf 0'> zeiteninsiie</span>
|
||||
<span class='ocrx_word' id='word_1_10' title='bbox 585 588 616 610; x_wconf 86'>—</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_carea' id='block_1_12' title="bbox 393 623 598 634">
|
||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 393 623 598 634">
|
||||
<span class='ocr_line' id='line_1_7' title="bbox 393 623 598 634; baseline 0 0; x_size 22.75; x_descenders 5.5; x_ascenders 5.75">
|
||||
<span class='ocrx_word' id='word_1_11' title='bbox 393 629 396 631; x_wconf 54'>7</span>
|
||||
<span class='ocrx_word' id='word_1_12' title='bbox 470 623 539 634; x_wconf 24'>Trenbideak</span>
|
||||
<span class='ocrx_word' id='word_1_13' title='bbox 550 628 598 630; x_wconf 24'>-----</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_carea' id='block_1_13' title="bbox 81 633 613 667">
|
||||
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 81 633 613 667">
|
||||
<span class='ocr_line' id='line_1_8' title="bbox 81 633 613 667; baseline -0.002 -13; x_size 21; x_descenders 4; x_ascenders 7">
|
||||
<span class='ocrx_word' id='word_1_14' title='bbox 81 637 111 653; x_wconf 0'>t\</span>
|
||||
<span class='ocrx_word' id='word_1_15' title='bbox 123 633 201 663; x_wconf 22'>Basusarri</span>
|
||||
<span class='ocrx_word' id='word_1_16' title='bbox 214 633 222 663; x_wconf 0'>—</span>
|
||||
<span class='ocrx_word' id='word_1_17' title='bbox 230 647 325 656; x_wconf 0'>spmeans:20141004</span>
|
||||
<span class='ocrx_word' id='word_1_18' title='bbox 373 633 415 667; x_wconf 42'>ae:</span>
|
||||
<span class='ocrx_word' id='word_1_19' title='bbox 441 650 444 653; x_wconf 25'>.</span>
|
||||
<span class='ocrx_word' id='word_1_20' title='bbox 521 649 544 657; x_wconf 17'>_</span>
|
||||
<span class='ocrx_word' id='word_1_21' title='bbox 595 650 613 659; x_wconf 7'>~</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
Tarnose
|
||||
|
||||
Bokale oa
|
||||
|
||||
Lehuntze
|
||||
|
||||
Mugerre
|
||||
|
||||
Milafranga Komunikabideak
|
||||
|
||||
BAIONA zeiteninsiie —
|
||||
|
||||
7 Trenbideak -----
|
||||
|
||||
t\ Basusarri — spmeans:20141004 ae: . _ ~
|
||||
BIN
Binary file not shown.
+27
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||
<meta name='ocr-system' content='tesseract 5.3.2' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.vh2to5iv/000002_ocr.png"; bbox 0 0 400 50; ppageno 0; scan_res 200 200'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 54 16 344 33">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 54 16 344 33">
|
||||
<span class='ocr_line' id='line_1_1' title="bbox 54 16 344 33; baseline 0 -4; x_size 25.75; x_descenders 5.5; x_ascenders 6.75">
|
||||
<span class='ocrx_word' id='word_1_1' title='bbox 54 16 110 29; x_wconf 84'>Covfefe</span>
|
||||
<span class='ocrx_word' id='word_1_2' title='bbox 115 17 127 29; x_wconf 95'>is</span>
|
||||
<span class='ocrx_word' id='word_1_3' title='bbox 132 20 140 29; x_wconf 95'>a</span>
|
||||
<span class='ocrx_word' id='word_1_4' title='bbox 145 16 212 33; x_wconf 85'>perfectly</span>
|
||||
<span class='ocrx_word' id='word_1_5' title='bbox 217 16 296 29; x_wconf 85'>cromulent</span>
|
||||
<span class='ocrx_word' id='word_1_6' title='bbox 300 16 344 29; x_wconf 95'>word.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
Covfefe is a perfectly cromulent word.
|
||||
BIN
Binary file not shown.
+177
@@ -0,0 +1,177 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||
<meta name='ocr-system' content='tesseract 5.3.2' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.vh2to5iv/000003_ocr.png"; bbox 0 0 4000 2864; ppageno 0; scan_res 1440 1440'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 142 146 2173 258">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 142 146 2173 258">
|
||||
<span class='ocr_line' id='line_1_1' title="bbox 142 146 2173 258; baseline -0.004 -19; x_size 113; x_descenders 23; x_ascenders 32">
|
||||
<span class='ocrx_word' id='word_1_1' title='bbox 142 154 982 258; x_wconf 90'>Linzensoep</span>
|
||||
<span class='ocrx_word' id='word_1_2' title='bbox 1080 146 1152 237; x_wconf 96'>a</span>
|
||||
<span class='ocrx_word' id='word_1_3' title='bbox 1254 153 1406 235; x_wconf 96'>la</span>
|
||||
<span class='ocrx_word' id='word_1_4' title='bbox 1500 153 2173 235; x_wconf 96'>Waterman</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_separator' id='block_1_2' title="bbox 136 253 2180 275"></div>
|
||||
<div class='ocr_carea' id='block_1_3' title="bbox 145 414 2929 1221">
|
||||
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 145 414 1154 516">
|
||||
<span class='ocr_line' id='line_1_2' title="bbox 145 414 1154 516; baseline -0.005 -4; x_size 122.38636; x_descenders 24.386362; x_ascenders 40">
|
||||
<span class='ocrx_word' id='word_1_5' title='bbox 145 414 211 512; x_wconf 60'>4</span>
|
||||
<span class='ocrx_word' id='word_1_6' title='bbox 318 453 551 516; x_wconf 93'>ons</span>
|
||||
<span class='ocrx_word' id='word_1_7' title='bbox 660 430 1154 512; x_wconf 91'>linzen</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 146 568 1239 667">
|
||||
<span class='ocr_line' id='line_1_3' title="bbox 146 568 1239 667; baseline -0.004 -16; x_size 99; x_descenders 17; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_8' title='bbox 146 569 209 667; x_wconf 94'>3</span>
|
||||
<span class='ocrx_word' id='word_1_9' title='bbox 323 568 729 652; x_wconf 83'>liter</span>
|
||||
<span class='ocrx_word' id='word_1_10' title='bbox 821 569 1239 650; x_wconf 96'>water</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 148 705 645 804">
|
||||
<span class='ocr_line' id='line_1_4' title="bbox 148 705 645 804; baseline -0.016 -14; x_size 103; x_descenders 19; x_ascenders 27">
|
||||
<span class='ocrx_word' id='word_1_11' title='bbox 148 706 211 804; x_wconf 88'>3</span>
|
||||
<span class='ocrx_word' id='word_1_12' title='bbox 311 705 645 789; x_wconf 52'>uien</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 147 832 1154 950">
|
||||
<span class='ocr_line' id='line_1_5' title="bbox 147 832 1154 950; baseline -0.004 -27; x_size 118; x_descenders 28; x_ascenders 30">
|
||||
<span class='ocrx_word' id='word_1_13' title='bbox 147 832 623 950; x_wconf 91'>bloem,</span>
|
||||
<span class='ocrx_word' id='word_1_14' title='bbox 737 843 1154 924; x_wconf 91'>boter</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 148 979 1241 1087">
|
||||
<span class='ocr_line' id='line_1_6' title="bbox 148 979 1241 1087; baseline -0.005 -21; x_size 107; x_descenders 24; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_15' title='bbox 148 983 215 1066; x_wconf 88'>2</span>
|
||||
<span class='ocrx_word' id='word_1_16' title='bbox 312 983 807 1087; x_wconf 88'>kopjes</span>
|
||||
<span class='ocrx_word' id='word_1_17' title='bbox 905 979 1241 1062; x_wconf 92'>melk</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 154 1109 2929 1221">
|
||||
<span class='ocr_line' id='line_1_7' title="bbox 154 1109 2929 1221; baseline -0.003 -21; x_size 112; x_descenders 28; x_ascenders 26">
|
||||
<span class='ocrx_word' id='word_1_18' title='bbox 154 1117 791 1221; x_wconf 92'>laurier,</span>
|
||||
<span class='ocrx_word' id='word_1_19' title='bbox 906 1111 1810 1219; x_wconf 90'>kruidnagel,</span>
|
||||
<span class='ocrx_word' id='word_1_20' title='bbox 1927 1109 2490 1219; x_wconf 90'>kerrie,</span>
|
||||
<span class='ocrx_word' id='word_1_21' title='bbox 2615 1110 2929 1195; x_wconf 91'>zout</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='ocr_carea' id='block_1_4' title="bbox 147 1383 3706 2731">
|
||||
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 147 1383 3706 2731">
|
||||
<span class='ocr_line' id='line_1_8' title="bbox 149 1383 3612 1474; baseline -0.003 -2; x_size 107.01524; x_descenders 25.01524; x_ascenders 21">
|
||||
<span class='ocrx_word' id='word_1_22' title='bbox 149 1395 303 1474; x_wconf 93'>De</span>
|
||||
<span class='ocrx_word' id='word_1_23' title='bbox 411 1390 902 1473; x_wconf 80'>linzgen</span>
|
||||
<span class='ocrx_word' id='word_1_24' title='bbox 996 1409 1497 1470; x_wconf 89'>wassen</span>
|
||||
<span class='ocrx_word' id='word_1_25' title='bbox 1595 1409 1752 1470; x_wconf 88'>en</span>
|
||||
<span class='ocrx_word' id='word_1_26' title='bbox 1853 1385 2166 1467; x_wconf 75'>in-l</span>
|
||||
<span class='ocrx_word' id='word_1_27' title='bbox 2275 1383 2684 1466; x_wconf 93'>liter</span>
|
||||
<span class='ocrx_word' id='word_1_28' title='bbox 2775 1383 3278 1464; x_wconf 91'>kokend</span>
|
||||
<span class='ocrx_word' id='word_1_29' title='bbox 3368 1401 3612 1462; x_wconf 90'>wa-</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_9' title="bbox 157 1516 3521 1632; baseline -0.004 -21; x_size 110; x_descenders 25; x_ascenders 25">
|
||||
<span class='ocrx_word' id='word_1_30' title='bbox 157 1531 394 1611; x_wconf 93'>ter</span>
|
||||
<span class='ocrx_word' id='word_1_31' title='bbox 495 1527 558 1609; x_wconf 94'>1</span>
|
||||
<span class='ocrx_word' id='word_1_32' title='bbox 658 1530 903 1632; x_wconf 75'>dag</span>
|
||||
<span class='ocrx_word' id='word_1_33' title='bbox 1002 1527 1415 1609; x_wconf 85'>laten</span>
|
||||
<span class='ocrx_word' id='word_1_34' title='bbox 1505 1525 1979 1611; x_wconf 86'>weken,</span>
|
||||
<span class='ocrx_word' id='word_1_35' title='bbox 2103 1521 2167 1603; x_wconf 96'>2</span>
|
||||
<span class='ocrx_word' id='word_1_36' title='bbox 2275 1518 2683 1603; x_wconf 93'>liter</span>
|
||||
<span class='ocrx_word' id='word_1_37' title='bbox 2777 1519 3194 1601; x_wconf 96'>water</span>
|
||||
<span class='ocrx_word' id='word_1_38' title='bbox 3286 1516 3521 1620; x_wconf 93'>bij</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_10' title="bbox 152 1651 3616 1767; baseline -0.004 -21; x_size 110; x_descenders 27; x_ascenders 23">
|
||||
<span class='ocrx_word' id='word_1_39' title='bbox 152 1668 302 1747; x_wconf 92'>de</span>
|
||||
<span class='ocrx_word' id='word_1_40' title='bbox 407 1662 905 1747; x_wconf 91'>linzen</span>
|
||||
<span class='ocrx_word' id='word_1_41' title='bbox 996 1682 1559 1767; x_wconf 92'>voegen,</span>
|
||||
<span class='ocrx_word' id='word_1_42' title='bbox 1680 1659 2176 1741; x_wconf 96'>zonder</span>
|
||||
<span class='ocrx_word' id='word_1_43' title='bbox 2267 1655 2506 1737; x_wconf 96'>het</span>
|
||||
<span class='ocrx_word' id='word_1_44' title='bbox 2606 1655 3023 1737; x_wconf 92'>water</span>
|
||||
<span class='ocrx_word' id='word_1_45' title='bbox 3116 1651 3616 1735; x_wconf 91'>waarin</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_11' title="bbox 153 1782 3704 1905; baseline -0.004 -20; x_size 117; x_descenders 23; x_ascenders 34">
|
||||
<span class='ocrx_word' id='word_1_46' title='bbox 153 1824 305 1885; x_wconf 91'>ze</span>
|
||||
<span class='ocrx_word' id='word_1_47' title='bbox 407 1800 979 1905; x_wconf 85'>geweekt</span>
|
||||
<span class='ocrx_word' id='word_1_48' title='bbox 1089 1797 1412 1900; x_wconf 96'>zijn</span>
|
||||
<span class='ocrx_word' id='word_1_49' title='bbox 1510 1796 1672 1878; x_wconf 96'>af</span>
|
||||
<span class='ocrx_word' id='word_1_50' title='bbox 1770 1782 1914 1876; x_wconf 93'>te</span>
|
||||
<span class='ocrx_word' id='word_1_51' title='bbox 2019 1792 2576 1899; x_wconf 54'>gieten,</span>
|
||||
<span class='ocrx_word' id='word_1_52' title='bbox 2690 1790 2850 1874; x_wconf 93'>De</span>
|
||||
<span class='ocrx_word' id='word_1_53' title='bbox 2948 1791 3357 1872; x_wconf 89'>helft</span>
|
||||
<span class='ocrx_word' id='word_1_54' title='bbox 3452 1811 3704 1873; x_wconf 96'>van</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_12' title="bbox 151 1928 3593 2035; baseline -0.003 -13; x_size 109; x_descenders 25; x_ascenders 25">
|
||||
<span class='ocrx_word' id='word_1_55' title='bbox 151 1942 305 2024; x_wconf 92'>de</span>
|
||||
<span class='ocrx_word' id='word_1_56' title='bbox 403 1940 735 2021; x_wconf 89'>uien</span>
|
||||
<span class='ocrx_word' id='word_1_57' title='bbox 829 1938 1330 2022; x_wconf 91'>bakken</span>
|
||||
<span class='ocrx_word' id='word_1_58' title='bbox 1419 1939 1659 2018; x_wconf 92'>met</span>
|
||||
<span class='ocrx_word' id='word_1_59' title='bbox 1770 1933 2346 2016; x_wconf 91'>laurier</span>
|
||||
<span class='ocrx_word' id='word_1_60' title='bbox 2447 1953 2603 2014; x_wconf 91'>en</span>
|
||||
<span class='ocrx_word' id='word_1_61' title='bbox 2691 1928 3593 2035; x_wconf 63'>Kruidnagel.</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_13' title="bbox 151 2067 3451 2180; baseline -0.004 -21; x_size 108; x_descenders 25; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_62' title='bbox 151 2076 473 2159; x_wconf 92'>Alle</span>
|
||||
<span class='ocrx_word' id='word_1_63' title='bbox 569 2076 965 2180; x_wconf 88'>uien,</span>
|
||||
<span class='ocrx_word' id='word_1_64' title='bbox 1079 2072 1578 2156; x_wconf 90'>kerrie</span>
|
||||
<span class='ocrx_word' id='word_1_65' title='bbox 1685 2092 1837 2153; x_wconf 93'>en</span>
|
||||
<span class='ocrx_word' id='word_1_66' title='bbox 1938 2072 2254 2153; x_wconf 81'>zgout</span>
|
||||
<span class='ocrx_word' id='word_1_67' title='bbox 2355 2068 2593 2172; x_wconf 43'>bij</span>
|
||||
<span class='ocrx_word' id='word_1_68' title='bbox 2696 2071 2850 2150; x_wconf 91'>de</span>
|
||||
<span class='ocrx_word' id='word_1_69' title='bbox 2957 2067 3451 2150; x_wconf 85'>linzen</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_14' title="bbox 147 2205 3614 2318; baseline -0.003 -23; x_size 106; x_descenders 22; x_ascenders 25">
|
||||
<span class='ocrx_word' id='word_1_70' title='bbox 147 2234 711 2318; x_wconf 88'>voegen,</span>
|
||||
<span class='ocrx_word' id='word_1_71' title='bbox 826 2210 1234 2295; x_wconf 95'>Alles</span>
|
||||
<span class='ocrx_word' id='word_1_72' title='bbox 1342 2232 1588 2295; x_wconf 95'>aan</span>
|
||||
<span class='ocrx_word' id='word_1_73' title='bbox 1679 2212 1831 2291; x_wconf 96'>de</span>
|
||||
<span class='ocrx_word' id='word_1_74' title='bbox 1928 2208 2263 2290; x_wconf 93'>kook</span>
|
||||
<span class='ocrx_word' id='word_1_75' title='bbox 2355 2206 3000 2308; x_wconf 54'>brengen,</span>
|
||||
<span class='ocrx_word' id='word_1_76' title='bbox 3117 2205 3367 2285; x_wconf 95'>Van</span>
|
||||
<span class='ocrx_word' id='word_1_77' title='bbox 3462 2206 3614 2287; x_wconf 95'>de</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_15' title="bbox 152 2341 3706 2447; baseline -0.003 -18; x_size 107; x_descenders 24; x_ascenders 24">
|
||||
<span class='ocrx_word' id='word_1_78' title='bbox 152 2352 565 2434; x_wconf 91'>bloem</span>
|
||||
<span class='ocrx_word' id='word_1_79' title='bbox 655 2351 896 2431; x_wconf 92'>met</span>
|
||||
<span class='ocrx_word' id='word_1_80' title='bbox 997 2349 1669 2431; x_wconf 90'>boter</span>
|
||||
<span class='ocrx_word' id='word_1_81' title='bbox 1514 2337 1683 2455; x_wconf 91'>en</span>
|
||||
<span class='ocrx_word' id='word_1_82' title='bbox 1757 2343 2095 2426; x_wconf 88'>melk</span>
|
||||
<span class='ocrx_word' id='word_1_83' title='bbox 2193 2364 2432 2427; x_wconf 93'>een</span>
|
||||
<span class='ocrx_word' id='word_1_84' title='bbox 2527 2341 2935 2447; x_wconf 90'>papje</span>
|
||||
<span class='ocrx_word' id='word_1_85' title='bbox 3029 2341 3453 2422; x_wconf 96'>maken</span>
|
||||
<span class='ocrx_word' id='word_1_86' title='bbox 3549 2362 3706 2422; x_wconf 95'>en</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_16' title="bbox 149 2477 3619 2586; baseline -0.003 -16; x_size 107; x_descenders 24; x_ascenders 23">
|
||||
<span class='ocrx_word' id='word_1_87' title='bbox 149 2489 650 2571; x_wconf 86'>verder</span>
|
||||
<span class='ocrx_word' id='word_1_88' title='bbox 750 2486 1330 2570; x_wconf 90'>afmaken</span>
|
||||
<span class='ocrx_word' id='word_1_89' title='bbox 1420 2485 1660 2567; x_wconf 96'>met</span>
|
||||
<span class='ocrx_word' id='word_1_90' title='bbox 1765 2485 1917 2566; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_91' title='bbox 2025 2505 2409 2586; x_wconf 86'>soep,</span>
|
||||
<span class='ocrx_word' id='word_1_92' title='bbox 2521 2479 2764 2564; x_wconf 96'>Als</span>
|
||||
<span class='ocrx_word' id='word_1_93' title='bbox 2868 2480 3021 2561; x_wconf 92'>de</span>
|
||||
<span class='ocrx_word' id='word_1_94' title='bbox 3127 2477 3619 2562; x_wconf 91'>linzen</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_17' title="bbox 155 2619 2412 2731; baseline -0.005 -20; x_size 98; x_descenders 15; x_ascenders 23">
|
||||
<span class='ocrx_word' id='word_1_95' title='bbox 155 2647 481 2731; x_wconf 35'>gfgaar</span>
|
||||
<span class='ocrx_word' id='word_1_96' title='bbox 581 2624 909 2728; x_wconf 87'>Zijn</span>
|
||||
<span class='ocrx_word' id='word_1_97' title='bbox 1005 2623 1153 2707; x_wconf 95'>is</span>
|
||||
<span class='ocrx_word' id='word_1_98' title='bbox 1255 2624 1409 2706; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_99' title='bbox 1522 2642 1840 2726; x_wconf 91'>soep</span>
|
||||
<span class='ocrx_word' id='word_1_100' title='bbox 1929 2619 2412 2709; x_wconf 89'>klaar.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Linzensoep a la Waterman
|
||||
|
||||
4 ons linzen
|
||||
|
||||
3 liter water
|
||||
|
||||
3 uien
|
||||
|
||||
bloem, boter
|
||||
|
||||
2 kopjes melk
|
||||
|
||||
laurier, kruidnagel, kerrie, zout
|
||||
|
||||
De linzgen wassen en in-l liter kokend wa-
|
||||
ter 1 dag laten weken, 2 liter water bij
|
||||
de linzen voegen, zonder het water waarin
|
||||
ze geweekt zijn af te gieten, De helft van
|
||||
de uien bakken met laurier en Kruidnagel.
|
||||
Alle uien, kerrie en zgout bij de linzen
|
||||
voegen, Alles aan de kook brengen, Van de
|
||||
bloem met boter en melk een papje maken en
|
||||
verder afmaken met de soep, Als de linzen
|
||||
gfgaar Zijn is de soep klaar.
|
||||
BIN
Binary file not shown.
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.9_hn68nz/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0; scan_res 200 200'>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.su6l_waz/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0; scan_res 200 200'>
|
||||
<div class='ocr_photo' id='block_1_1' title="bbox 296 96 704 504"></div>
|
||||
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 150 592 841 622">
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.g296cpyi/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
|
||||
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.91dzclia/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
|
||||
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.91dzclia/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
|
||||
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.91dzclia/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
|
||||
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.zopxh6db/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.bgz1p10r/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
|
||||
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
|
||||
|
||||
BIN
Binary file not shown.
+104
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||
<meta name='ocr-system' content='tesseract 5.3.2' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.9pz7mnez/000001_ocr.png"; bbox 0 0 900 900; ppageno 0; scan_res 300 300'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 180 159 760 740">
|
||||
<p class='ocr_par' id='par_1_1' lang='deu' title="bbox 180 159 760 740">
|
||||
<span class='ocr_line' id='line_1_1' title="bbox 182 159 721 198; baseline 0 -8; x_size 39; x_descenders 8; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_1' title='bbox 182 160 292 190; x_wconf 92'>Portez</span>
|
||||
<span class='ocrx_word' id='word_1_2' title='bbox 307 169 344 190; x_wconf 93'>ce</span>
|
||||
<span class='ocrx_word' id='word_1_3' title='bbox 357 160 449 190; x_wconf 92'>vieux</span>
|
||||
<span class='ocrx_word' id='word_1_4' title='bbox 462 159 583 197; x_wconf 91'>whisky</span>
|
||||
<span class='ocrx_word' id='word_1_5' title='bbox 595 169 633 190; x_wconf 92'>au</span>
|
||||
<span class='ocrx_word' id='word_1_6' title='bbox 646 160 721 198; x_wconf 87'>juge</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_2' title="bbox 181 209 640 247; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_7' title='bbox 181 209 276 240; x_wconf 95'>blond</span>
|
||||
<span class='ocrx_word' id='word_1_8' title='bbox 293 210 343 247; x_wconf 93'>qui</span>
|
||||
<span class='ocrx_word' id='word_1_9' title='bbox 358 210 448 240; x_wconf 91'>fume</span>
|
||||
<span class='ocrx_word' id='word_1_10' title='bbox 463 219 516 240; x_wconf 96'>sur</span>
|
||||
<span class='ocrx_word' id='word_1_11' title='bbox 529 219 587 240; x_wconf 96'>son</span>
|
||||
<span class='ocrx_word' id='word_1_12' title='bbox 602 209 640 240; x_wconf 89'>Ile</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_3' title="bbox 181 259 676 295; baseline 0 -5; x_size 36; x_descenders 5; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_13' title='bbox 181 260 357 295; x_wconf 89'>interieure,</span>
|
||||
<span class='ocrx_word' id='word_1_14' title='bbox 372 260 387 290; x_wconf 83'>a</span>
|
||||
<span class='ocrx_word' id='word_1_15' title='bbox 404 260 478 290; x_wconf 61'>cöte</span>
|
||||
<span class='ocrx_word' id='word_1_16' title='bbox 493 259 534 290; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_17' title='bbox 551 259 676 290; x_wconf 81'>l'alcöve</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_4' title="bbox 180 309 613 345; baseline 0 -5; x_size 36; x_descenders 5; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_18' title='bbox 180 309 305 345; x_wconf 92'>ovoide,</span>
|
||||
<span class='ocrx_word' id='word_1_19' title='bbox 321 310 362 340; x_wconf 38'>oU</span>
|
||||
<span class='ocrx_word' id='word_1_20' title='bbox 380 309 423 340; x_wconf 92'>les</span>
|
||||
<span class='ocrx_word' id='word_1_21' title='bbox 440 309 562 340; x_wconf 92'>büches</span>
|
||||
<span class='ocrx_word' id='word_1_22' title='bbox 577 319 613 340; x_wconf 96'>se</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_5' title="bbox 180 359 693 397; baseline 0 -7; x_size 36; x_descenders 5; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_23' title='bbox 180 362 375 390; x_wconf 92'>consument</span>
|
||||
<span class='ocrx_word' id='word_1_24' title='bbox 389 359 469 390; x_wconf 92'>dans</span>
|
||||
<span class='ocrx_word' id='word_1_25' title='bbox 485 359 575 395; x_wconf 86'>l'ätre,</span>
|
||||
<span class='ocrx_word' id='word_1_26' title='bbox 591 369 628 390; x_wconf 96'>ce</span>
|
||||
<span class='ocrx_word' id='word_1_27' title='bbox 643 360 693 397; x_wconf 96'>qui</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_6' title="bbox 181 409 617 447; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_28' title='bbox 181 409 217 440; x_wconf 49'>ui</span>
|
||||
<span class='ocrx_word' id='word_1_29' title='bbox 234 412 359 447; x_wconf 91'>permet</span>
|
||||
<span class='ocrx_word' id='word_1_30' title='bbox 373 409 414 440; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_31' title='bbox 430 419 548 447; x_wconf 90'>penser</span>
|
||||
<span class='ocrx_word' id='word_1_32' title='bbox 556 405 568 451; x_wconf 73'>&</span>
|
||||
<span class='ocrx_word' id='word_1_33' title='bbox 594 409 617 440; x_wconf 96'>la</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_7' title="bbox 180 459 680 498; baseline 0 -8; x_size 39; x_descenders 8; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_34' title='bbox 180 460 402 498; x_wconf 79'>caenogenese</span>
|
||||
<span class='ocrx_word' id='word_1_35' title='bbox 417 459 458 490; x_wconf 93'>de</span>
|
||||
<span class='ocrx_word' id='word_1_36' title='bbox 475 459 557 490; x_wconf 42'>|'etre</span>
|
||||
<span class='ocrx_word' id='word_1_37' title='bbox 572 459 653 490; x_wconf 96'>dont</span>
|
||||
<span class='ocrx_word' id='word_1_38' title='bbox 668 459 680 490; x_wconf 94'>il</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_8' title="bbox 180 509 646 547; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_39' title='bbox 180 512 231 540; x_wconf 96'>est</span>
|
||||
<span class='ocrx_word' id='word_1_40' title='bbox 245 510 396 547; x_wconf 96'>question</span>
|
||||
<span class='ocrx_word' id='word_1_41' title='bbox 412 509 492 540; x_wconf 96'>dans</span>
|
||||
<span class='ocrx_word' id='word_1_42' title='bbox 509 509 531 540; x_wconf 95'>la</span>
|
||||
<span class='ocrx_word' id='word_1_43' title='bbox 548 519 646 540; x_wconf 96'>cause</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_9' title="bbox 180 559 736 598; baseline 0 -8; x_size 36; x_descenders 5; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_44' title='bbox 180 559 328 598; x_wconf 87'>ambigu&</span>
|
||||
<span class='ocrx_word' id='word_1_45' title='bbox 343 559 510 590; x_wconf 91'>entendue</span>
|
||||
<span class='ocrx_word' id='word_1_46' title='bbox 525 560 541 590; x_wconf 93'>a</span>
|
||||
<span class='ocrx_word' id='word_1_47' title='bbox 559 560 641 597; x_wconf 61'>MoY,</span>
|
||||
<span class='ocrx_word' id='word_1_48' title='bbox 657 559 736 590; x_wconf 96'>dans</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_10' title="bbox 181 609 719 647; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_49' title='bbox 181 619 221 640; x_wconf 93'>un</span>
|
||||
<span class='ocrx_word' id='word_1_50' title='bbox 237 609 452 647; x_wconf 92'>capharnaüm</span>
|
||||
<span class='ocrx_word' id='word_1_51' title='bbox 469 610 528 647; x_wconf 93'>qui,</span>
|
||||
<span class='ocrx_word' id='word_1_52' title='bbox 545 609 719 647; x_wconf 91'>pense-t-il,</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_11' title="bbox 180 659 760 697; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
|
||||
<span class='ocrx_word' id='word_1_53' title='bbox 180 659 319 690; x_wconf 90'>diminue</span>
|
||||
<span class='ocrx_word' id='word_1_54' title='bbox 334 660 369 697; x_wconf 95'>ca</span>
|
||||
<span class='ocrx_word' id='word_1_55' title='bbox 385 662 418 690; x_wconf 95'>et</span>
|
||||
<span class='ocrx_word' id='word_1_56' title='bbox 434 659 457 690; x_wconf 94'>la</span>
|
||||
<span class='ocrx_word' id='word_1_57' title='bbox 474 659 497 690; x_wconf 92'>la</span>
|
||||
<span class='ocrx_word' id='word_1_58' title='bbox 514 659 630 697; x_wconf 75'>qualit&</span>
|
||||
<span class='ocrx_word' id='word_1_59' title='bbox 645 659 686 690; x_wconf 96'>de</span>
|
||||
<span class='ocrx_word' id='word_1_60' title='bbox 701 669 760 690; x_wconf 96'>son</span>
|
||||
</span>
|
||||
<span class='ocr_line' id='line_1_12' title="bbox 180 719 300 740; baseline 0 0; x_size 39.328766; x_descenders 7.3287673; x_ascenders 10.5">
|
||||
<span class='ocrx_word' id='word_1_61' title='bbox 180 719 300 740; x_wconf 66'>ceuvre.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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
|
||||
ui 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 qualit& de son
|
||||
ceuvre.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user