Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae123fd209 | ||
|
|
454ad0acc5 | ||
|
|
0c306ac328 | ||
|
|
52d99732b1 | ||
|
|
aac913c666 | ||
|
|
b5e73ac4e4 | ||
|
|
9e98c90891 | ||
|
|
c6b5332699 | ||
|
|
68610046c6 | ||
|
|
880326868d | ||
|
|
c6be3ba076 | ||
|
|
0565cb0b10 | ||
|
|
dc49906704 | ||
|
|
93fda0dd00 | ||
|
|
d4110e78cb | ||
|
|
5285d68fcc | ||
|
|
2b0e149809 | ||
|
|
b7ce5b0d7d | ||
|
|
ffd6a64ce9 | ||
|
|
5727f1e081 | ||
|
|
b75a7eca2a | ||
|
|
2b01676434 | ||
|
|
e11c386c58 | ||
|
|
9346d1f970 | ||
|
|
012cbef865 | ||
|
|
0687568e1b | ||
|
|
3086cfc3d9 | ||
|
|
91a14660b3 | ||
|
|
539f0ee0ce | ||
|
|
7172817cd6 | ||
|
|
d9cc759142 | ||
|
|
364799fc3e | ||
|
|
f4c211fa2d | ||
|
|
113a6b45bd | ||
|
|
e9419d2c40 | ||
|
|
fb006ef39f | ||
|
|
890b994403 | ||
|
|
01bbf7d144 | ||
|
|
468de5324a | ||
|
|
072db75fa3 | ||
|
|
8519b3f625 | ||
|
|
dd7c4f3eaa | ||
|
|
c8e6f20f8d | ||
|
|
10530a8698 | ||
|
|
207866abf5 | ||
|
|
3829af16fb | ||
|
|
24db31b4c5 | ||
|
|
8132a4ae10 | ||
|
|
d5128c5cf5 | ||
|
|
270e31fa67 | ||
|
|
85e31d0a19 | ||
|
|
ea36aedb5f | ||
|
|
bd4d44e182 | ||
|
|
8fcf358934 | ||
|
|
47b0f28564 | ||
|
|
7018e2b247 | ||
|
|
8d12ecb798 | ||
|
|
0ab29ec0ba | ||
|
|
179714770a | ||
|
|
f04f45545c | ||
|
|
a3a083c125 | ||
|
|
d855f63985 | ||
|
|
d4863cbf0f | ||
|
|
7b8f081fbf | ||
|
|
7d33039bcd | ||
|
|
fde886baf4 | ||
|
|
146da79c00 | ||
|
|
2fc3b0d973 | ||
|
|
5667424530 | ||
|
|
8add531ffd | ||
|
|
0388c23ae7 | ||
|
|
9b77daae7c | ||
|
|
3e1b3ec98d | ||
|
|
0f0ca6f517 | ||
|
|
c93349c350 | ||
|
|
0c287929c2 | ||
|
|
23a37fc35c | ||
|
|
162a47f98e | ||
|
|
0239f69912 | ||
|
|
2ad8961d0b | ||
|
|
eec8a2b574 | ||
|
|
6c78076bea | ||
|
|
2637e84691 | ||
|
|
e8c82ee4b6 | ||
|
|
de2bb5ce8c | ||
|
|
ec1c377532 | ||
|
|
173428e81a | ||
|
|
67ed29dcea | ||
|
|
3454c050ed | ||
|
|
5ee99b26e7 | ||
|
|
ac3aa67d8a | ||
|
|
1768a1eda9 | ||
|
|
5902fe45c1 | ||
|
|
78981641f0 | ||
|
|
c77ae4b34c | ||
|
|
b2cbbf0099 | ||
|
|
6b6c34af01 | ||
|
|
be12f7a728 | ||
|
|
e3c813fc67 | ||
|
|
35a1eaf62a | ||
|
|
d393d18c13 | ||
|
|
54e622ad10 | ||
|
|
330352aeed | ||
|
|
ac2fc49208 | ||
|
|
4bee7355e9 | ||
|
|
86f2b1f9a7 | ||
|
|
3002409e49 | ||
|
|
0cf6828c20 | ||
|
|
331c829b6e | ||
|
|
06a5e0c3f6 | ||
|
|
811f23381a | ||
|
|
a371655052 | ||
|
|
a6ce35b13a | ||
|
|
45added738 | ||
|
|
6e20439c91 | ||
|
|
72e056436c | ||
|
|
e02ba19097 | ||
|
|
d3b858f994 | ||
|
|
19045c4f21 | ||
|
|
f4d89fe6cc | ||
|
|
ab85c0f5a9 | ||
|
|
32693b683d | ||
|
|
b5dc276ba1 |
@@ -0,0 +1,83 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
FROM alpine:3.18 as base
|
||||||
|
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
ENV TZ=UTC
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
python3 \
|
||||||
|
zlib
|
||||||
|
|
||||||
|
FROM base as builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
python3-dev \
|
||||||
|
py3-pip
|
||||||
|
|
||||||
|
# On arm64, we need to build cffi from source.
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
RUN if [ "${TARGETPLATFORM}" == "linux/arm64" ]; then \
|
||||||
|
apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
zlib-dev \
|
||||||
|
libffi-dev \
|
||||||
|
cairo-dev \
|
||||||
|
pkgconfig \
|
||||||
|
; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN python3 -m venv .venv
|
||||||
|
|
||||||
|
RUN source .venv/bin/activate \
|
||||||
|
&& python3 -m pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& python3 -m pip install --no-cache-dir wheel \
|
||||||
|
&& python3 -m pip install --no-cache-dir .[test,webservice,watcher]
|
||||||
|
|
||||||
|
FROM base
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ghostscript \
|
||||||
|
jbig2dec \
|
||||||
|
jbig2enc \
|
||||||
|
pngquant \
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-data-chi_sim \
|
||||||
|
tesseract-ocr-data-deu \
|
||||||
|
tesseract-ocr-data-eng \
|
||||||
|
tesseract-ocr-data-fra \
|
||||||
|
tesseract-ocr-data-osd \
|
||||||
|
tesseract-ocr-data-por \
|
||||||
|
tesseract-ocr-data-spa \
|
||||||
|
ttf-droid \
|
||||||
|
unpaper \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local/lib/ /usr/local/lib/
|
||||||
|
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
||||||
|
|
||||||
|
COPY --from=builder /app/.venv/ /app/.venv/
|
||||||
|
|
||||||
|
COPY --from=builder /app/misc/webservice.py /app/
|
||||||
|
COPY --from=builder /app/misc/watcher.py /app/
|
||||||
|
|
||||||
|
# Copy minimal project files to get the test suite.
|
||||||
|
COPY --from=builder /app/pyproject.toml /app/README.md /app/
|
||||||
|
COPY --from=builder /app/tests /app/tests
|
||||||
|
|
||||||
|
ENV PATH="/app/.venv/bin:${PATH}"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
|
||||||
+111
-42
@@ -22,39 +22,35 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-20.04
|
- os: ubuntu-22.04
|
||||||
python: "3.8"
|
|
||||||
- os: ubuntu-20.04
|
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
- os: ubuntu-20.04
|
- os: ubuntu-22.04
|
||||||
python: "3.10"
|
python: "3.10"
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-22.04
|
||||||
python: "3.9"
|
|
||||||
- os: ubuntu-latest
|
|
||||||
python: "3.10"
|
|
||||||
- os: ubuntu-latest
|
|
||||||
python: "3.11"
|
python: "3.11"
|
||||||
# - os: ubuntu-latest
|
- os: ubuntu-22.04
|
||||||
# python: "pypy3.8"
|
|
||||||
#- os: ubuntu-latest
|
|
||||||
# python: "pypy3.9"
|
|
||||||
- os: ubuntu-latest
|
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
tesseract5: true
|
tesseract5: true
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: "3.12"
|
||||||
|
tesseract5: true
|
||||||
|
#- os: ubuntu-latest
|
||||||
|
# python: "pypy3.9"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
PYTHON: ${{ matrix.python }}
|
PYTHON: ${{ matrix.python }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
name: Install Python
|
name: Setup Python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
- name: Install Tesseract 5
|
- name: Install Tesseract 5
|
||||||
if: matrix.tesseract5
|
if: matrix.tesseract5
|
||||||
@@ -68,6 +64,7 @@ jobs:
|
|||||||
curl \
|
curl \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
img2pdf \
|
img2pdf \
|
||||||
|
libexempi8 \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
libsm6 libxext6 libxrender-dev \
|
libsm6 libxext6 libxrender-dev \
|
||||||
pngquant \
|
pngquant \
|
||||||
@@ -79,12 +76,6 @@ jobs:
|
|||||||
unpaper \
|
unpaper \
|
||||||
zlib1g
|
zlib1g
|
||||||
|
|
||||||
- name: Install Ubuntu 20.04 packages
|
|
||||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
libexempi8
|
|
||||||
|
|
||||||
- name: Install Ubuntu packages for PyPy
|
- name: Install Ubuntu packages for PyPy
|
||||||
if: startsWith(matrix.python, 'pypy')
|
if: startsWith(matrix.python, 'pypy')
|
||||||
run: |
|
run: |
|
||||||
@@ -122,14 +113,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-latest]
|
||||||
python: ["3.10", "3.11"]
|
python: ["3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
PYTHON: ${{ matrix.python }}
|
PYTHON: ${{ matrix.python }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||||
|
|
||||||
@@ -142,13 +133,15 @@ jobs:
|
|||||||
ghostscript \
|
ghostscript \
|
||||||
jbig2enc \
|
jbig2enc \
|
||||||
openjpeg \
|
openjpeg \
|
||||||
|
openssl \
|
||||||
pngquant \
|
pngquant \
|
||||||
tesseract
|
tesseract
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
name: Install Python
|
name: Setup Python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
- name: Install Python packages
|
- name: Install Python packages
|
||||||
run: |
|
run: |
|
||||||
@@ -178,21 +171,22 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest]
|
os: [windows-latest]
|
||||||
python: ["3.10", "3.11"]
|
python: ["3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
OS: ${{ matrix.os }}
|
OS: ${{ matrix.os }}
|
||||||
PYTHON: ${{ matrix.python }}
|
PYTHON: ${{ matrix.python }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
name: Install Python
|
name: Setup Python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: |
|
run: |
|
||||||
@@ -218,14 +212,15 @@ jobs:
|
|||||||
name: Build sdist and wheels
|
name: Build sdist and wheels
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
name: Install Python
|
name: Setup Python
|
||||||
with:
|
with:
|
||||||
python-version: "3.7"
|
python-version: "3.9"
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
- name: Make wheels and sdist
|
- name: Make wheels and sdist
|
||||||
run: |
|
run: |
|
||||||
@@ -242,6 +237,9 @@ jobs:
|
|||||||
name: Deploy artifacts to PyPI
|
name: Deploy artifacts to PyPI
|
||||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: release
|
||||||
|
permissions:
|
||||||
|
id-token: write # mandatory for PyPI publishing
|
||||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
@@ -249,14 +247,37 @@ jobs:
|
|||||||
name: artifact
|
name: artifact
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
- name: Publish to PyPI
|
||||||
with:
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.TOKEN_PYPI }}
|
|
||||||
# repository_url: https://test.pypi.org/legacy/
|
|
||||||
|
|
||||||
docker:
|
create_release:
|
||||||
name: Build Docker images
|
name: Create GitHub release
|
||||||
|
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
||||||
|
permissions:
|
||||||
|
# Required to create a release
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create-release
|
||||||
|
uses: shogo82148/actions-create-release@v1
|
||||||
|
|
||||||
|
- name: Upload Assets
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||||
|
asset_path: |
|
||||||
|
./dist/*.whl
|
||||||
|
./dist/*.tar.gz
|
||||||
|
|
||||||
|
docker_ubuntu:
|
||||||
|
name: Build Ubuntu-based Docker image
|
||||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
@@ -274,22 +295,22 @@ jobs:
|
|||||||
- name: Set image name
|
- name: Set image name
|
||||||
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: jbarlow83
|
username: jbarlow83
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Print image tag
|
- name: Print image tag
|
||||||
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
|
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
|
||||||
@@ -300,4 +321,52 @@ jobs:
|
|||||||
--push \
|
--push \
|
||||||
--platform linux/arm64/v8,linux/amd64 \
|
--platform linux/arm64/v8,linux/amd64 \
|
||||||
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
|
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
|
||||||
|
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}-ubuntu:${DOCKER_IMAGE_TAG}" \
|
||||||
--file .docker/Dockerfile .
|
--file .docker/Dockerfile .
|
||||||
|
|
||||||
|
docker_alpine:
|
||||||
|
name: Build Alpine-based Docker images
|
||||||
|
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Set image tag to release or branch
|
||||||
|
run: echo "DOCKER_IMAGE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: If main, set to latest
|
||||||
|
run: echo 'DOCKER_IMAGE_TAG=latest' >> $GITHUB_ENV
|
||||||
|
if: env.DOCKER_IMAGE_TAG == 'main'
|
||||||
|
|
||||||
|
- name: Set Docker Hub repository to username
|
||||||
|
run: echo "DOCKER_REPOSITORY=jbarlow83" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set image name
|
||||||
|
run: echo "DOCKER_IMAGE_NAME=ocrmypdf-alpine" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Print image tag
|
||||||
|
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--push \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
|
||||||
|
--file .docker/Dockerfile.alpine .
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ venv*/
|
|||||||
*.traineddata
|
*.traineddata
|
||||||
/private
|
/private
|
||||||
/coverage.xml
|
/coverage.xml
|
||||||
|
/issuepdf
|
||||||
|
|
||||||
# Package building
|
# Package building
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|||||||
+5
-1
@@ -16,8 +16,12 @@ formats:
|
|||||||
- pdf
|
- pdf
|
||||||
|
|
||||||
# Optionally set the version of Python and requirements required to build your docs
|
# Optionally set the version of Python and requirements required to build your docs
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.9"
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: "3.8"
|
|
||||||
install:
|
install:
|
||||||
- method: pip
|
- method: pip
|
||||||
path: .
|
path: .
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ ocrmypdf # it's a scriptable command line program
|
|||||||
- Distributes work across all available CPU cores
|
- Distributes work across all available CPU cores
|
||||||
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata)
|
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata)
|
||||||
- Keeps your private data private.
|
- Keeps your private data private.
|
||||||
- Scales properly to handle files with thousands of pages
|
- Scales properly to handle files with thousands of pages.
|
||||||
- Battle-tested on millions of PDFs
|
- Battle-tested on millions of PDFs.
|
||||||
|
|
||||||
<img src="misc/screencast/demo.svg" alt="Demo of OCRmyPDF in a terminal session">
|
<img src="misc/screencast/demo.svg" alt="Demo of OCRmyPDF in a terminal session">
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
|
|||||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||||
| Fedora | ``dnf install ocrmypdf`` |
|
| Fedora | ``dnf install ocrmypdf`` |
|
||||||
| macOS (Homebrew) | ``brew install ocrmypdf`` |
|
| macOS (Homebrew) | ``brew install ocrmypdf`` |
|
||||||
| macOS (nix) | ``nix-env -i ocrmypdf`` |
|
| macOS (nix) | ``nix-env -i ocrmypdf`` |
|
||||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||||
| FreeBSD | ``pkg install py-ocrmypdf`` |
|
| FreeBSD | ``pkg install py-ocrmypdf`` |
|
||||||
| Conda | ``conda install ocrmypdf`` |
|
| Conda | ``conda install ocrmypdf`` |
|
||||||
|
|||||||
+72
-13
@@ -47,8 +47,8 @@ and clean up the margins of both.
|
|||||||
|
|
||||||
Some ``unpaper`` features cause multiple input or output files to be
|
Some ``unpaper`` features cause multiple input or output files to be
|
||||||
consumed or produced. OCRmyPDF requires ``unpaper`` to consume one
|
consumed or produced. OCRmyPDF requires ``unpaper`` to consume one
|
||||||
file and produce one file. An deviation from that condition will
|
file and produce one file; errors will result if this assumption is not
|
||||||
result in errors.
|
met.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -82,14 +82,17 @@ is stripped out. Then an image of each page is created with visible text
|
|||||||
masked out. The page image is sent for OCR, and any additional text is
|
masked out. The page image is sent for OCR, and any additional text is
|
||||||
inserted as OCR. If a file contains a mix of text and bitmap images that
|
inserted as OCR. If a file contains a mix of text and bitmap images that
|
||||||
contain text, OCRmyPDF will locate the additional text in images without
|
contain text, OCRmyPDF will locate the additional text in images without
|
||||||
disrupting the existing text.
|
disrupting the existing text. Some PDF OCR solutions render text as
|
||||||
|
technically printable or visible in some way, perhaps by drawing it and
|
||||||
|
then painting over it. OCRmyPDF cannot distinguish this type of OCR
|
||||||
|
text from real text, so it will not be "redone".
|
||||||
|
|
||||||
If ``--force-ocr`` is issued, then all pages will be rasterized to
|
If ``--force-ocr`` is issued, then all pages will be rasterized to
|
||||||
images, discarding any hidden OCR text, and rasterizing any printable
|
images, discarding any hidden OCR text, rasterizing any printable
|
||||||
text. This is useful for redoing OCR, for fixing OCR text with a damaged
|
text, and flattening form fields or interactive objects into their visual
|
||||||
character map (text is selectable but not searchable), and destroying
|
representation. This is useful for redoing OCR, for fixing OCR text
|
||||||
redacted information. Any forms and vector graphics will be rasterized
|
with a damaged character map (text is selectable but not searchable),
|
||||||
as well.
|
and destroying redacted information.
|
||||||
|
|
||||||
Time and image size limits
|
Time and image size limits
|
||||||
--------------------------
|
--------------------------
|
||||||
@@ -111,6 +114,41 @@ exceed a certain number of megapixels with ``--skip-big``. (A 300 DPI,
|
|||||||
# Allow 300 seconds for OCR; skip any page larger than 50 megapixels
|
# Allow 300 seconds for OCR; skip any page larger than 50 megapixels
|
||||||
ocrmypdf --tesseract-timeout 300 --skip-big 50 bigfile.pdf output.pdf
|
ocrmypdf --tesseract-timeout 300 --skip-big 50 bigfile.pdf output.pdf
|
||||||
|
|
||||||
|
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.)
|
||||||
|
|
||||||
|
``--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.
|
||||||
|
|
||||||
|
You will also need to set ``--tesseract-timeout`` high enough to allow
|
||||||
|
for processing.
|
||||||
|
|
||||||
|
Only the image sent for OCR is downsampled. The original image is
|
||||||
|
preserved.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Allow 600 seconds for OCR on huge images
|
||||||
|
ocrmypdf --tesseract-timeout 600 \
|
||||||
|
--tesseract-downsample-large-images \
|
||||||
|
bigfile.pdf output.pdf
|
||||||
|
|
||||||
|
# Downsample images above 5000 pixels on the longest dimension to
|
||||||
|
# 5000 pixels
|
||||||
|
ocrmypdf --tesseract-timeout 120 \
|
||||||
|
--tesseract-downsample-large-images \
|
||||||
|
--tesseract-downsample-above 5000 \
|
||||||
|
bigfile.pdf output_downsampled_ocr.pdf
|
||||||
|
|
||||||
|
|
||||||
Overriding default tesseract
|
Overriding default tesseract
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
@@ -154,12 +192,13 @@ In addition to tesseract, OCRmyPDF uses the following external binaries:
|
|||||||
- ``jbig2``
|
- ``jbig2``
|
||||||
|
|
||||||
In each case OCRmyPDF will search the ``PATH`` environment variable to
|
In each case OCRmyPDF will search the ``PATH`` environment variable to
|
||||||
locate the binaries.
|
locate the binaries. By modifying the ``PATH`` environment variable, you
|
||||||
|
can override the binaries that OCRmyPDF uses.
|
||||||
|
|
||||||
Changing tesseract configuration variables
|
Changing Tesseract configuration variables
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
You can override tesseract's default `control
|
You can override Tesseract's default `control
|
||||||
parameters <https://tesseract-ocr.github.io/tessdoc/tess3/ControlParams.html>`__
|
parameters <https://tesseract-ocr.github.io/tessdoc/tess3/ControlParams.html>`__
|
||||||
with a configuration file.
|
with a configuration file.
|
||||||
|
|
||||||
@@ -234,7 +273,7 @@ Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
|
|||||||
looking to customize how OCR is presented should look here. A major
|
looking to customize how OCR is presented should look here. A major
|
||||||
disadvantage of this renderer is it not capable of correctly handling
|
disadvantage of this renderer is it not capable of correctly handling
|
||||||
text outside the Latin alphabet (specifically, it supports the ISO 8859-1
|
text outside the Latin alphabet (specifically, it supports the ISO 8859-1
|
||||||
character). Pull requests to improve the situation are welcome.
|
character set). Pull requests to improve the situation are welcome.
|
||||||
|
|
||||||
Currently, this renderer has the best compatibility with Mozilla's
|
Currently, this renderer has the best compatibility with Mozilla's
|
||||||
PDF.js viewer.
|
PDF.js viewer.
|
||||||
@@ -247,11 +286,31 @@ Rendering and rasterizing options
|
|||||||
.. versionadded:: 14.3.0
|
.. versionadded:: 14.3.0
|
||||||
|
|
||||||
The ``--continue-on-soft-render-error`` option allows OCRmyPDF to
|
The ``--continue-on-soft-render-error`` option allows OCRmyPDF to
|
||||||
proceed if a page cannot be rasterized rendered. This is useful if you are
|
proceed if a page cannot be rasterized/rendered. This is useful if you are
|
||||||
trying to get the best possible OCR from a PDF that is not well-formed,
|
trying to get the best possible OCR from a PDF that is not well-formed,
|
||||||
and you are willing to accept some pages that may not visually match the
|
and you are willing to accept some pages that may not visually match the
|
||||||
input, and that may not OCR well.
|
input, and that may not OCR well.
|
||||||
|
|
||||||
|
Color conversion strategy
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. versionadded:: 15.0.0
|
||||||
|
|
||||||
|
OCRmyPDF uses Ghostscript to convert PDF to PDF/A. In some cases, this
|
||||||
|
conversion requires color conversion. The default strategy is to convert
|
||||||
|
using the ``LeaveColorUnchanged`` strategy, which preserves the original
|
||||||
|
color space wherever possible (some rare color spaces might still be
|
||||||
|
converted).
|
||||||
|
|
||||||
|
Usually document scanners produce PDFs in the sRGB color space, and do
|
||||||
|
not need to be converted, so the default strategy is appropriate.
|
||||||
|
|
||||||
|
Suppose that you have a document that was prepared for professional
|
||||||
|
printing in a Separation or CMYK color space, and text was converted to
|
||||||
|
curves. In this case, you may want to use a different color conversion
|
||||||
|
strategy. The ``--color-conversion-strategy`` option allows you to select a
|
||||||
|
different strategy, such as ``RGB``.
|
||||||
|
|
||||||
Return code policy
|
Return code policy
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|||||||
+24
-8
@@ -39,9 +39,7 @@ Parent process requirements
|
|||||||
The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line
|
The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line
|
||||||
execution. To do this, it will:
|
execution. To do this, it will:
|
||||||
|
|
||||||
- create a monitoring thread
|
- create worker processes or threads
|
||||||
- create worker processes (on Linux, forking itself; on Windows and macOS, by
|
|
||||||
spawning)
|
|
||||||
- manage the signal flags of its worker processes
|
- manage the signal flags of its worker processes
|
||||||
- execute other subprocesses (forking and executing other programs)
|
- execute other subprocesses (forking and executing other programs)
|
||||||
|
|
||||||
@@ -54,7 +52,19 @@ processes.
|
|||||||
|
|
||||||
Creating a child process to call :func:`ocrmypdf.ocr()` is suggested. That
|
Creating a child process to call :func:`ocrmypdf.ocr()` is suggested. That
|
||||||
way your application will survive and remain interactive even if
|
way your application will survive and remain interactive even if
|
||||||
OCRmyPDF fails for any reason.
|
OCRmyPDF fails for any reason. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from multiprocessing import Process
|
||||||
|
|
||||||
|
def ocrmypdf_process():
|
||||||
|
ocrmypdf.ocr('input.pdf', 'output.pdf')
|
||||||
|
|
||||||
|
def call_ocrmypdf_from_my_app():
|
||||||
|
p = Process(target=ocrmypdf_process)
|
||||||
|
p.start()
|
||||||
|
p.join()
|
||||||
|
|
||||||
Programs that call :func:`ocrmypdf.ocr()` should also install a SIGBUS signal
|
Programs that call :func:`ocrmypdf.ocr()` should also install a SIGBUS signal
|
||||||
handler (except on Windows), to raise an exception if access to a memory
|
handler (except on Windows), to raise an exception if access to a memory
|
||||||
@@ -89,12 +99,21 @@ your use case.
|
|||||||
Progress monitoring
|
Progress monitoring
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
OCRmyPDF uses the ``tqdm`` package to implement its progress bars.
|
OCRmyPDF uses the ``rich`` package to implement its progress bars.
|
||||||
:func:`ocrmypdf.configure_logging` will set up logging output to
|
:func:`ocrmypdf.configure_logging` will set up logging output to
|
||||||
``sys.stderr`` in a way that is compatible with the display of the
|
``sys.stderr`` in a way that is compatible with the display of the
|
||||||
progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable
|
progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable
|
||||||
the progress bar.
|
the progress bar.
|
||||||
|
|
||||||
|
Standard output
|
||||||
|
---------------
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Exceptions
|
Exceptions
|
||||||
----------
|
----------
|
||||||
|
|
||||||
@@ -104,9 +123,6 @@ exceptions, some exceptions related to multiprocessing, and
|
|||||||
handler. OCRmyPDF will clean up its temporary files and worker processes
|
handler. OCRmyPDF will clean up its temporary files and worker processes
|
||||||
automatically when an exception occurs.
|
automatically when an exception occurs.
|
||||||
|
|
||||||
Programs that call OCRmyPDF should consider trapping KeyboardInterrupt
|
|
||||||
so that they allow OCR to terminate with the whole program terminating.
|
|
||||||
|
|
||||||
When OCRmyPDF succeeds conditionally, it returns an integer exit code.
|
When OCRmyPDF succeeds conditionally, it returns an integer exit code.
|
||||||
|
|
||||||
Reference
|
Reference
|
||||||
|
|||||||
+15
-16
@@ -46,16 +46,6 @@ place, and printing each filename in between runs:
|
|||||||
|
|
||||||
find . -printf '%p\n' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
find . -printf '%p\n' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||||
|
|
||||||
Alternatively, with a Docker container and streaming the file through
|
|
||||||
standard input and output:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
|
|
||||||
pdfout=$(mktemp)
|
|
||||||
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
|
|
||||||
done
|
|
||||||
|
|
||||||
This only runs one ``ocrmypdf`` process at a time. This variation uses
|
This only runs one ``ocrmypdf`` process at a time. This variation uses
|
||||||
``find`` to create a directory list and ``parallel`` to parallelize runs
|
``find`` to create a directory list and ``parallel`` to parallelize runs
|
||||||
of ``ocrmypdf``, again updating files in place.
|
of ``ocrmypdf``, again updating files in place.
|
||||||
@@ -70,6 +60,15 @@ In a Windows batch file, use
|
|||||||
|
|
||||||
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
|
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
|
||||||
|
|
||||||
|
With a Docker container, you will need to stream through standard input and output:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
find . -name '*.pdf' -print0 | xargs -0 | while read pdf; do
|
||||||
|
pdfout=$(mktemp)
|
||||||
|
docker run --rm -i jbarlow83/ocrmypdf - - <$pdf >$pdfout && cp $pdfout $pdf
|
||||||
|
done
|
||||||
|
|
||||||
Sample script
|
Sample script
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -88,9 +87,9 @@ package <https://www.synology.com/en-global/dsm/packages/Docker>`__ is
|
|||||||
installed. Attached is a script to address particular quirks of using
|
installed. Attached is a script to address particular quirks of using
|
||||||
OCRmyPDF on one of these devices.
|
OCRmyPDF on one of these devices.
|
||||||
|
|
||||||
This is only possible for x86-based Synology products. Some Synology
|
At the time this script was written, it only worked for x86-based Synology
|
||||||
products use ARM or Power processors and do not support Docker. Further
|
products. It is not known if it will work on ARM-based Synology products.
|
||||||
adjustments might be needed to deal with the Synology's relatively
|
Further adjustments might be needed to deal with the Synology's relatively
|
||||||
limited CPU and RAM.
|
limited CPU and RAM.
|
||||||
|
|
||||||
.. literalinclude:: ../misc/synology.py
|
.. literalinclude:: ../misc/synology.py
|
||||||
@@ -153,7 +152,7 @@ The watcher service is included in the OCRmyPDF Docker image. To run it:
|
|||||||
docker run \
|
docker run \
|
||||||
--volume <path to files to convert>:/input \
|
--volume <path to files to convert>:/input \
|
||||||
--volume <path to store results>:/output \
|
--volume <path to store results>:/output \
|
||||||
--volume <path to store processed originals>:/archive \
|
--volume <path to store processed originals>:/processed \
|
||||||
--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||||
--env OCR_ON_SUCCESS_ARCHIVE=1 \
|
--env OCR_ON_SUCCESS_ARCHIVE=1 \
|
||||||
--env OCR_DESKEW=1 \
|
--env OCR_DESKEW=1 \
|
||||||
@@ -164,7 +163,7 @@ The watcher service is included in the OCRmyPDF Docker image. To run it:
|
|||||||
|
|
||||||
This service will watch for a file that matches ``/input/\*.pdf``,
|
This service will watch for a file that matches ``/input/\*.pdf``,
|
||||||
convert it to a OCRed PDF in ``/output/``, and move the processed
|
convert it to a OCRed PDF in ``/output/``, and move the processed
|
||||||
original to ``/archive``. The parameters to this image are:
|
original to ``/processed``. The parameters to this image are:
|
||||||
|
|
||||||
.. csv-table:: watcher.py parameters for Docker
|
.. csv-table:: watcher.py parameters for Docker
|
||||||
:header: "Parameter", "Description"
|
:header: "Parameter", "Description"
|
||||||
@@ -172,7 +171,7 @@ original to ``/archive``. The parameters to this image are:
|
|||||||
|
|
||||||
"``--volume <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
"``--volume <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||||
"``--volume <path to store results>:/output``", "This is where OCRed files will be stored"
|
"``--volume <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||||
"``--volume <path to store processed originals>:/archive``", "Archive processed originals here"
|
"``--volume <path to store processed originals>:/processed``", "Archive processed originals here"
|
||||||
"``--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable ``OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1`` to place files in the output in ``{output}/{year}/{month}/{filename}``"
|
"``--env OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable ``OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1`` to place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||||
"``--env OCR_ON_SUCCESS_ARCHIVE=1``", "Define environment variable ``OCR_ON_SUCCESS_ARCHIVE`` to move processed originals"
|
"``--env OCR_ON_SUCCESS_ARCHIVE=1``", "Define environment variable ``OCR_ON_SUCCESS_ARCHIVE`` to move processed originals"
|
||||||
"``--env OCR_DESKEW=1``", "Define environment variable ``OCR_DESKEW`` to apply deskew to crooked input PDFs"
|
"``--env OCR_DESKEW=1``", "Define environment variable ``OCR_DESKEW`` to apply deskew to crooked input PDFs"
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ master_doc = 'index'
|
|||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'ocrmypdf'
|
project = 'ocrmypdf'
|
||||||
copyright = (
|
copyright = (
|
||||||
'2022, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
'2023, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
||||||
)
|
)
|
||||||
author = 'James R. Barlow'
|
author = 'James R. Barlow'
|
||||||
|
|
||||||
|
|||||||
+25
-8
@@ -29,12 +29,17 @@ Tests
|
|||||||
|
|
||||||
New features should come with tests that confirm their correctness.
|
New features should come with tests that confirm their correctness.
|
||||||
|
|
||||||
New Python dependencies
|
New dependencies
|
||||||
=======================
|
================
|
||||||
|
|
||||||
If you are proposing a change that will require a new Python dependency, we
|
If you are proposing a change that will require a new dependency, we
|
||||||
prefer dependencies that are already packaged by Debian or Red Hat. This makes
|
prefer dependencies that are already packaged by Debian or Red Hat. This makes
|
||||||
life much easier for our downstream package maintainers.
|
life much easier for our downstream package maintainers. A package that is only
|
||||||
|
available on PyPI or GitHub, and not more widely packaged, may not be accepted.
|
||||||
|
|
||||||
|
We are unlikely to accept a dependency on CUDA or other GPU-based libraries,
|
||||||
|
because these are still difficult to package and install on many systems.
|
||||||
|
We recommend implementing these changes as plugins.
|
||||||
|
|
||||||
Python dependencies must also be license-compatible. GPLv3 or AGPLv3 are likely
|
Python dependencies must also be license-compatible. GPLv3 or AGPLv3 are likely
|
||||||
incompatible with the project's license, but LGPLv3 is compatible.
|
incompatible with the project's license, but LGPLv3 is compatible.
|
||||||
@@ -43,7 +48,19 @@ New non-Python dependencies
|
|||||||
===========================
|
===========================
|
||||||
|
|
||||||
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
|
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
|
||||||
its functionality. In general we prefer to avoid adding new external programs.
|
its functionality. In general we prefer to avoid adding new external programs,
|
||||||
|
and if we are to add external programs, we prefer those that are already
|
||||||
|
packaged by Debian or Red Hat.
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
=======
|
||||||
|
|
||||||
|
Some new features may be a good fit for a plugin. Plugins are a way to add
|
||||||
|
features to OCRmyPDF without adding them to the core program. Plugins are
|
||||||
|
installed separately from OCRmyPDF. They are written in Python and can be
|
||||||
|
installed from PyPI. See the `plugin documentation <https://ocrmypdf.readthedocs.io/en/latest/plugins.html>`_.
|
||||||
|
|
||||||
|
We are happy to link users to your plugin from the documentation.
|
||||||
|
|
||||||
Style guide: Is it OCRmyPDF or ocrmypdf?
|
Style guide: Is it OCRmyPDF or ocrmypdf?
|
||||||
========================================
|
========================================
|
||||||
@@ -53,8 +70,8 @@ The program/project is OCRmyPDF and the name of the executable or library is ocr
|
|||||||
Copyright and license
|
Copyright and license
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
For contributions over 10 lines of code, please include your name to list of
|
For contributions over 10 lines of code, please add your name to list of
|
||||||
copyright holders for that file. The core program is licensed under MPL-2.0,
|
copyright holders for that file. The core program is licensed under MPL-2.0,
|
||||||
test files and documentation under CC-BY-SA 4.0, and miscellaneous files under
|
test files and documentation under CC-BY-SA 4.0, and miscellaneous files under
|
||||||
MIT. Please contribute code only that you wrote and you have the permission to
|
MIT, with a few minor exceptions. Please contribute only content that you own
|
||||||
contribute or license to us.
|
or have the right to contribute under these licenses.
|
||||||
|
|||||||
+26
-4
@@ -231,13 +231,20 @@ Don't actually OCR my PDF
|
|||||||
=========================
|
=========================
|
||||||
|
|
||||||
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image
|
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image
|
||||||
processing without performing OCR, if all you want to is to apply image
|
processing without performing OCR (by causing OCR to time out). This works
|
||||||
processing or PDF/A conversion.
|
if all you want to is to apply image processing or PDF/A conversion.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
|
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
|
||||||
|
|
||||||
|
.. versionchanged:: v14.1.0
|
||||||
|
|
||||||
|
Prior to this version, ``--tesseract-timeout 0`` would prevent other
|
||||||
|
uses of Tesseract, such as deskewing, from working. This is no longer
|
||||||
|
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
|
||||||
|
for non-OCR operations, if needed.
|
||||||
|
|
||||||
Optimize images without performing OCR
|
Optimize images without performing OCR
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
@@ -261,9 +268,10 @@ Hyphens denote a range of pages and commas separate page numbers. If you prefer
|
|||||||
to use spaces, quote all of the page numbers: ``--pages '2, 3, 5, 7'``.
|
to use spaces, quote all of the page numbers: ``--pages '2, 3, 5, 7'``.
|
||||||
|
|
||||||
OCRmyPDF will warn if your list of page numbers contains duplicates or
|
OCRmyPDF will warn if your list of page numbers contains duplicates or
|
||||||
overlap pages. OCRmyPDF does not currently account for document page numbers,
|
overlapping pages. OCRmyPDF does not currently account for document page numbers,
|
||||||
such as an introduction section of a book that uses Roman numerals. It simply
|
such as an introduction section of a book that uses Roman numerals. It simply
|
||||||
counts the number of virtual pieces of paper since the start.
|
counts the number of virtual pieces of paper since the start. If your list of
|
||||||
|
pages is out of numerical order, OCRmyPDF will sort it for you.
|
||||||
|
|
||||||
Regardless of the argument to ``--pages``, OCRmyPDF will optimize all pages/images
|
Regardless of the argument to ``--pages``, OCRmyPDF will optimize all pages/images
|
||||||
in the file and convert it to PDF/A, unless you disable those options. Both of these
|
in the file and convert it to PDF/A, unless you disable those options. Both of these
|
||||||
@@ -372,3 +380,17 @@ Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
|
|||||||
|
|
||||||
Image processing and PDF/A conversion can also introduce lossy transformations
|
Image processing and PDF/A conversion can also introduce lossy transformations
|
||||||
to your PDF images, even when ``--optimize 1`` is in use.
|
to your PDF images, even when ``--optimize 1`` is in use.
|
||||||
|
|
||||||
|
|
||||||
|
Digitally signed PDFs
|
||||||
|
=====================
|
||||||
|
|
||||||
|
OCRmyPDF cannot preserve digital signatures in PDFs and also add to OCR to them.
|
||||||
|
By default, it will refuse to modify a signed PDF regardless of other settings. You can
|
||||||
|
override this behavior with ``--invalidate-digital-signatures``; as the name suggests,
|
||||||
|
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.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.. SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
============
|
||||||
|
Design notes
|
||||||
|
============
|
||||||
|
|
||||||
|
Why doesn't OCRmyPDF use PyTesseract?
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
PyTesseract is a Python wrapper around the Tesseract OCR engine. When OCRmyPDF was
|
||||||
|
first written, PyTesseract used ABI bindings to call the Tesseract library. This
|
||||||
|
was not a good fit for OCRmyPDF because ABI bindings can be fragile.
|
||||||
|
|
||||||
|
PyTesseract has since evolved calling the Tesseract executable, abandoning the ABI
|
||||||
|
approach and using the CLI instead, just like OCRmyPDF does. If it were written from
|
||||||
|
scratch today, OCRmyPDF might use PyTesseract.
|
||||||
|
|
||||||
|
PyTesseract has more features don't particularly need PDF output, but less features
|
||||||
|
than OCRmyPDF's API for creating PDFs.
|
||||||
|
|
||||||
|
What is ``executor()``?
|
||||||
|
=======================
|
||||||
|
|
||||||
|
OCRmyPDF uses a custom concurrent executor which can support either threads or
|
||||||
|
processes with the same interface. This is useful because OCRmyPDF can use
|
||||||
|
either threads or processes to parallelize work, whichever is more appropriate
|
||||||
|
for the task at hand.
|
||||||
|
|
||||||
|
The interface is currently private and subject to change. In particular, if
|
||||||
|
experiments with asyncio and anyio are successful, the interface will change.
|
||||||
|
|
||||||
+60
-29
@@ -8,17 +8,16 @@
|
|||||||
OCRmyPDF Docker image
|
OCRmyPDF Docker image
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
OCRmyPDF is also available in a Docker image that packages recent
|
OCRmyPDF is also available in Docker images that packages recent
|
||||||
versions of all dependencies.
|
versions of all dependencies.
|
||||||
|
|
||||||
For users who already have Docker installed this may be an easy and
|
For users who already have Docker installed this may be an easy and
|
||||||
convenient option. However, it is less performant than a system
|
convenient option.
|
||||||
installation and may require Docker engine configuration.
|
|
||||||
|
|
||||||
OCRmyPDF needs a generous amount of RAM, CPU cores, temporary storage
|
On platforms other than Linux, Docker runs in a virtual machine, and so may
|
||||||
space, whether running in a Docker container or on its own. It may be
|
be less performant. You may also want to adjust the Docker virtual machine's
|
||||||
necessary to ensure the container is provisioned with additional
|
memory and CPU allocation. On Linux, the Docker image runs natively and
|
||||||
resources.
|
performance is comparable to a system installation.
|
||||||
|
|
||||||
.. _docker-install:
|
.. _docker-install:
|
||||||
|
|
||||||
@@ -35,28 +34,37 @@ execute the image:
|
|||||||
|
|
||||||
docker run hello-world
|
docker run hello-world
|
||||||
|
|
||||||
The recommended OCRmyPDF Docker image is currently named ``ocrmypdf``:
|
.. list-table:: Docker images
|
||||||
|
:width: 30 20 50
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Image
|
||||||
|
- Architecture
|
||||||
|
- Description
|
||||||
|
* - ``jbarlow83/ocrmypdf-alpine``
|
||||||
|
- x86_64 only
|
||||||
|
- Recommended image, based on Alpine Linux.
|
||||||
|
* - ``jbarlow83/ocrmypdf-ubuntu``
|
||||||
|
- x86_64 and arm64
|
||||||
|
- Alternate image, based on Ubuntu. When the Alpine image is considered
|
||||||
|
stable and available for arm64, this image will be deprecated.
|
||||||
|
* - ``jbarlow83/ocrmypdf``
|
||||||
|
- x86_64 and arm64
|
||||||
|
- Currently an alias for ocrmypdf-ubuntu. When the Alpine image is
|
||||||
|
considered stable and available for arm64, this name point to the
|
||||||
|
Alpine image. If you don't about the difference between Alpine and
|
||||||
|
Ubuntu, use this image.
|
||||||
|
|
||||||
|
To install:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker pull jbarlow83/ocrmypdf
|
docker pull jbarlow83/ocrmypdf-alpine
|
||||||
|
|
||||||
|
The ``ocrmypdf`` image is also available, but is deprecated and will be removed
|
||||||
|
in the future.
|
||||||
|
|
||||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox
|
OCRmyPDF will use all available CPU cores. See the Docker documentation for
|
||||||
machine instance on Windows and macOS has only a single CPU core
|
|
||||||
enabled. Use the VirtualBox Manager to determine the name of your Docker
|
|
||||||
engine host, and then follow these optional steps to enable multiple
|
|
||||||
CPUs:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# Optional step for Mac OS X users
|
|
||||||
docker-machine stop "yourVM"
|
|
||||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
|
||||||
docker-machine start "yourVM"
|
|
||||||
eval $(docker-machine env "yourVM")
|
|
||||||
|
|
||||||
See the Docker documentation for
|
|
||||||
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__.
|
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__.
|
||||||
|
|
||||||
Using the Docker image on the command line
|
Using the Docker image on the command line
|
||||||
@@ -66,6 +74,8 @@ Using the Docker image on the command line
|
|||||||
container is ephemeral – it runs for one OCR job and terminates, just like a
|
container is ephemeral – it runs for one OCR job and terminates, just like a
|
||||||
command line program. We are using Docker to deliver an application (as opposed
|
command line program. We are using Docker to deliver an application (as opposed
|
||||||
to the more conventional case, where a Docker container runs as a server).
|
to the more conventional case, where a Docker container runs as a server).
|
||||||
|
For that reason we usually use the ``--rm`` argument to delete the container
|
||||||
|
when it exits.
|
||||||
|
|
||||||
To start a Docker container (instance of the image):
|
To start a Docker container (instance of the image):
|
||||||
|
|
||||||
@@ -132,17 +142,35 @@ You can then add new data with either a Dockerfile:
|
|||||||
|
|
||||||
.. code-block:: dockerfile
|
.. code-block:: dockerfile
|
||||||
|
|
||||||
FROM jbarlow83/ocrmypdf
|
FROM jbarlow83/ocrmypdf:{TAG}
|
||||||
|
|
||||||
# Example: add a tessdata_best file
|
# Example: add a tessdata_best file
|
||||||
COPY chi_tra_vert.traineddata /usr/share/tesseract-ocr/<data version>/tessdata/
|
COPY chi_tra_vert.traineddata /usr/share/tesseract-ocr/<data version>/tessdata/
|
||||||
|
|
||||||
|
When creating your own image, you should always pin a specific version of the
|
||||||
|
OCRmyPDF Docker image. This ensures that your image will not break when a new
|
||||||
|
version of OCRmyPDF is released.
|
||||||
|
|
||||||
Alternately, you can copy training data into a Docker container as follows:
|
Alternately, you can copy training data into a Docker container as follows:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker cp mycustomtraining.traineddata name_of_container:/usr/share/tesseract-ocr/<tesseract version>/tessdata/
|
docker cp mycustomtraining.traineddata name_of_container:/usr/share/tesseract-ocr/<tesseract version>/tessdata/
|
||||||
|
|
||||||
|
Extending the Docker image
|
||||||
|
==========================
|
||||||
|
|
||||||
|
You can extend the Docker image with your own customizations, similar to the way
|
||||||
|
it is extended to add language packs.
|
||||||
|
|
||||||
|
Note that the Docker image is subject to change at any time. For example, the base
|
||||||
|
image may be updated to a newer version of Ubuntu or Debian. Such changes will be
|
||||||
|
noted in the release notes but might occur at minor versions releases, unless the
|
||||||
|
way a "casual" user of the Docker image is affected.
|
||||||
|
|
||||||
|
If you extend the Docker image, you should pin a specific version of the OCRmyPDF
|
||||||
|
Docker image.
|
||||||
|
|
||||||
Executing the test suite
|
Executing the test suite
|
||||||
========================
|
========================
|
||||||
|
|
||||||
@@ -150,16 +178,16 @@ The OCRmyPDF test suite is installed with image. To run it:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run --entrypoint python3 jbarlow83/ocrmypdf -m pytest
|
docker run --rm --entrypoint python jbarlow83/ocrmypdf -m pytest
|
||||||
|
|
||||||
Accessing the shell
|
Accessing the shell
|
||||||
===================
|
===================
|
||||||
|
|
||||||
To use the bash shell in the Docker image:
|
To use the shell in the Docker image:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run -it --entrypoint bash jbarlow83/ocrmypdf
|
docker run -it --entrypoint sh jbarlow83/ocrmypdf
|
||||||
|
|
||||||
Using the OCRmyPDF web service wrapper
|
Using the OCRmyPDF web service wrapper
|
||||||
======================================
|
======================================
|
||||||
@@ -169,7 +197,10 @@ service. The webservice may be launched as follows:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf webservice.py
|
docker run --entrypoint python -p 5000:5000 jbarlow83/ocrmypdf webservice.py
|
||||||
|
|
||||||
|
We omit the ``--rm`` parameter so that the container will not be
|
||||||
|
automatically deleted when it exits.
|
||||||
|
|
||||||
This will configure the machine to listen on port 5000. On Linux machines
|
This will configure the machine to listen on port 5000. On Linux machines
|
||||||
this is port 5000 of localhost. On macOS or Windows machines running
|
this is port 5000 of localhost. On macOS or Windows machines running
|
||||||
|
|||||||
+2
-1
@@ -12,7 +12,7 @@ files, allowing them to be searched.
|
|||||||
|
|
||||||
PDF is the best format for storing and exchanging scanned documents.
|
PDF is the best format for storing and exchanging scanned documents.
|
||||||
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
||||||
image processing and OCR to existing PDFs.
|
image processing and OCR (recognized, searchable text) to existing PDFs.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
@@ -44,6 +44,7 @@ image processing and OCR to existing PDFs.
|
|||||||
api
|
api
|
||||||
plugins
|
plugins
|
||||||
apiref
|
apiref
|
||||||
|
design_notes
|
||||||
contributing
|
contributing
|
||||||
maintainers
|
maintainers
|
||||||
|
|
||||||
|
|||||||
+69
-76
@@ -72,8 +72,7 @@ Debian and Ubuntu 20.04 or newer
|
|||||||
| |ubu-2004| |ubu-2204| |
|
| |ubu-2004| |ubu-2204| |
|
||||||
+-----------------------------------------------+
|
+-----------------------------------------------+
|
||||||
|
|
||||||
Users of Debian 9 ("stretch") or later, or Ubuntu 18.04 or later, including users
|
Users of Debian or Ubuntu may simply
|
||||||
of Windows Subsystem for Linux, may simply
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -100,11 +99,11 @@ For full details on version availability for your platform, check the
|
|||||||
Fedora
|
Fedora
|
||||||
------
|
------
|
||||||
|
|
||||||
.. |fedora-35| image:: https://repology.org/badge/version-for-repo/fedora_35/ocrmypdf.svg
|
.. |fedora-37| image:: https://repology.org/badge/version-for-repo/fedora_37/ocrmypdf.svg
|
||||||
:alt: Fedora 35
|
:alt: Fedora 37
|
||||||
|
|
||||||
.. |fedora-36| image:: https://repology.org/badge/version-for-repo/fedora_36/ocrmypdf.svg
|
.. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
|
||||||
:alt: Fedora 36
|
:alt: Fedora 38
|
||||||
|
|
||||||
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
|
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
|
||||||
:alt: Fedore Rawhide
|
:alt: Fedore Rawhide
|
||||||
@@ -114,10 +113,10 @@ Fedora
|
|||||||
+-----------------------------------------------+
|
+-----------------------------------------------+
|
||||||
| |latest| |
|
| |latest| |
|
||||||
+-----------------------------------------------+
|
+-----------------------------------------------+
|
||||||
| |fedora-35| |fedora-36| |fedora-rawhide| |
|
| |fedora-37| |fedora-38| |fedora-rawhide| |
|
||||||
+-----------------------------------------------+
|
+-----------------------------------------------+
|
||||||
|
|
||||||
Users of Fedora 29 or later may simply
|
Users of Fedora may simply
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ user, follow these steps:
|
|||||||
|
|
||||||
If you get the message ``WARNING: The script ocrmypdf is installed in
|
If you get the message ``WARNING: The script ocrmypdf is installed in
|
||||||
'/home/$USER/.local/bin' which is not on PATH.``, you may need to re-login
|
'/home/$USER/.local/bin' which is not on PATH.``, you may need to re-login
|
||||||
or open a new shell, or manually add this to your user's PATH.
|
or open a new shell, or manually adjust your PATH.
|
||||||
|
|
||||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||||
|
|
||||||
@@ -275,6 +274,21 @@ To install OCRmyPDF for Alpine Linux:
|
|||||||
|
|
||||||
apk add ocrmypdf
|
apk add ocrmypdf
|
||||||
|
|
||||||
|
Gentoo Linux
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. image:: https://repology.org/badge/version-for-repo/gentoo_ovl_guru/ocrmypdf.svg
|
||||||
|
:alt: Gentoo Linux
|
||||||
|
:target: https://repology.org/metapackage/ocrmypdf
|
||||||
|
|
||||||
|
To install OCRmyPDF on Gentoo Linux, use the following commands:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
eselect repository enable guru
|
||||||
|
emaint sync --repo guru
|
||||||
|
emerge --ask app-text/OCRmyPDF
|
||||||
|
|
||||||
Other Linux packages
|
Other Linux packages
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -344,18 +358,12 @@ Update the homebrew pip:
|
|||||||
|
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
|
|
||||||
You can then install OCRmyPDF from PyPI, for the current user:
|
You can then install OCRmyPDF from PyPI for the current user:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install --user ocrmypdf
|
pip install --user ocrmypdf
|
||||||
|
|
||||||
or system-wide:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
pip install ocrmypdf
|
|
||||||
|
|
||||||
The command line program should now be available:
|
The command line program should now be available:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
@@ -374,29 +382,33 @@ Native Windows
|
|||||||
|
|
||||||
You must install the following for Windows:
|
You must install the following for Windows:
|
||||||
|
|
||||||
* Python 3.8 (64-bit) or later
|
* Python 64-bit
|
||||||
* Tesseract 4.1.1 (64-bit) or later
|
* Tesseract 64-bit
|
||||||
* Ghostscript 9.50 (64-bit) or later
|
* Ghostscript 64-bit
|
||||||
|
|
||||||
Using the `Chocolatey <https://chocolatey.org/>`_ package manager, install the
|
Using the `winget <https://docs.microsoft.com/en-us/windows/package-manager/winget/>`_
|
||||||
following when running in an Administrator command prompt:
|
package manager:
|
||||||
|
|
||||||
|
* ``winget install -e --id Python.Python.3.11``
|
||||||
|
* ``winget install -e --id UB-Mannheim.TesseractOCR``
|
||||||
|
* ``winget install -e --id ArtifexSoftware.GhostScript``
|
||||||
|
|
||||||
|
|
||||||
|
(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 python3``
|
||||||
* ``choco install --pre tesseract``
|
* ``choco install --pre tesseract``
|
||||||
* ``choco install ghostscript``
|
* ``choco install ghostscript``
|
||||||
* ``choco install pngquant`` (optional)
|
* ``choco install pngquant`` (optional)
|
||||||
|
|
||||||
The commands above will install Python 3.x (latest version), Tesseract, Ghostscript
|
Either set of commands will install the required software. At the mmoment there is no
|
||||||
and pngquant. Chocolatey may also need to install the Windows Visual C++ Runtime
|
single command to install Windows.
|
||||||
DLLs or other Windows patches, and may require a reboot.
|
|
||||||
|
|
||||||
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
|
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
|
||||||
Administrator.):
|
Administrator.):
|
||||||
|
|
||||||
* ``pip install ocrmypdf``
|
* ``python3 -m pip install ocrmypdf``
|
||||||
|
|
||||||
Chocolatey automatically selects appropriate versions of these applications. Please make sure
|
|
||||||
you are installing the 64-bit versions.
|
|
||||||
|
|
||||||
OCRmyPDF will check the Windows Registry and standard locations in your Program Files
|
OCRmyPDF will check the Windows Registry and standard locations in your Program Files
|
||||||
for third party software it needs (specifically, Tesseract and Ghostscript). To
|
for third party software it needs (specifically, Tesseract and Ghostscript). To
|
||||||
@@ -408,12 +420,12 @@ to change the PATH.
|
|||||||
|
|
||||||
As of early 2021, users have reported problems with the Microsoft Store version of
|
As of early 2021, users have reported problems with the Microsoft Store version of
|
||||||
Python and OCRmyPDF. These issues affect many other third party Python packages.
|
Python and OCRmyPDF. These issues affect many other third party Python packages.
|
||||||
Please download Python from Python.org or Chocolatey instead, and do not use the
|
Please download Python from Python.org or a package manager instead of the
|
||||||
Microsoft Store version.
|
Microsoft Store version.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
32-bit Windows might work, but is not supported.
|
32-bit Windows is not supported.
|
||||||
|
|
||||||
Windows Subsystem for Linux
|
Windows Subsystem for Linux
|
||||||
---------------------------
|
---------------------------
|
||||||
@@ -440,7 +452,7 @@ Cygwin64
|
|||||||
|
|
||||||
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
|
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
|
||||||
|
|
||||||
python38 (or later)
|
python39 (or later)
|
||||||
python3?-devel
|
python3?-devel
|
||||||
python3?-pip
|
python3?-pip
|
||||||
python3?-lxml
|
python3?-lxml
|
||||||
@@ -449,7 +461,7 @@ First install the the following prerequisite Cygwin packages using ``setup-x86_6
|
|||||||
(where 3? means match the version of python3 you installed)
|
(where 3? means match the version of python3 you installed)
|
||||||
|
|
||||||
gcc-g++
|
gcc-g++
|
||||||
ghostscript (<=9.50 or >=9.52-2 see note below)
|
ghostscript
|
||||||
libexempi3
|
libexempi3
|
||||||
libexempi-devel
|
libexempi-devel
|
||||||
libffi6
|
libffi6
|
||||||
@@ -460,13 +472,6 @@ First install the the following prerequisite Cygwin packages using ``setup-x86_6
|
|||||||
tesseract-ocr
|
tesseract-ocr
|
||||||
tesseract-ocr-devel
|
tesseract-ocr-devel
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The Cygwin package for Ghostscript in versions 9.52 and
|
|
||||||
9.52-1 contained a bug that caused an exception to occur when
|
|
||||||
ocrmypdf invoked gs. Make sure you have either 9.50 (or earlier)
|
|
||||||
or 9.52-2 (or later).
|
|
||||||
|
|
||||||
Then open a Cygwin terminal (i.e. ``mintty``), run the following commands. Note
|
Then open a Cygwin terminal (i.e. ``mintty``), run the following commands. Note
|
||||||
that if you are using the version of ``pip`` that was installed with the Cygwin
|
that if you are using the version of ``pip`` that was installed with the Cygwin
|
||||||
Python package, the command name will be ``pip3``. If you have since updated
|
Python package, the command name will be ``pip3``. If you have since updated
|
||||||
@@ -518,21 +523,6 @@ the latest version. However, PyPI and ``pip`` cannot address the fact
|
|||||||
that ``ocrmypdf`` depends on certain non-Python system libraries and
|
that ``ocrmypdf`` depends on certain non-Python system libraries and
|
||||||
programs being installed.
|
programs being installed.
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Debian and Ubuntu users: unfortunately, Debian and Ubuntu customize
|
|
||||||
Python in non-standard ways, and the nature of these customizations
|
|
||||||
varies from release to release. This can make for a frustrating
|
|
||||||
user experience. The instructions below work on almost all platforms that
|
|
||||||
have Python installed, except for Debian and Ubuntu, where you may need
|
|
||||||
to take additional steps. For best results on Debian and Ubuntu, use the
|
|
||||||
``apt`` packages; or if these are too old, run
|
|
||||||
``apt install python3-pip python3-venv``, create a virtual environment,
|
|
||||||
and install OCRmyPDF in that environment.
|
|
||||||
|
|
||||||
`See here for more information on Debian-Python issues
|
|
||||||
<https://gist.github.com/tiran/2dec9e03c6f901814f6d1e8dad09528e>`__.
|
|
||||||
|
|
||||||
For best results, first install `your platform's
|
For best results, first install `your platform's
|
||||||
version <https://repology.org/metapackage/ocrmypdf/versions>`__ of
|
version <https://repology.org/metapackage/ocrmypdf/versions>`__ of
|
||||||
``ocrmypdf``, using the instructions elsewhere in this document. Then
|
``ocrmypdf``, using the instructions elsewhere in this document. Then
|
||||||
@@ -551,21 +541,6 @@ try:
|
|||||||
You should then be able to run ``ocrmypdf --version`` and see that the
|
You should then be able to run ``ocrmypdf --version`` and see that the
|
||||||
latest version was located.
|
latest version was located.
|
||||||
|
|
||||||
Since ``pip install --user`` does not work correctly on some platforms,
|
|
||||||
notably Ubuntu 16.04 and older, and the Homebrew version of Python,
|
|
||||||
instead use this for a system wide installation:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
pip install ocrmypdf
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
AArch64 (ARM64) users: this process will be difficult because most
|
|
||||||
Python packages are not available as binary wheels for your platform.
|
|
||||||
You're probably better off using a platform install on Debian, Ubuntu,
|
|
||||||
or Fedora.
|
|
||||||
|
|
||||||
Requirements for pip and HEAD install
|
Requirements for pip and HEAD install
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
@@ -575,13 +550,16 @@ manager. ``pip`` cannot provide them.
|
|||||||
|
|
||||||
The following versions are required:
|
The following versions are required:
|
||||||
|
|
||||||
- Python 3.8 or newer
|
- Python 3.9 or newer
|
||||||
- Ghostscript 9.50 or newer
|
- Ghostscript 9.55 or newer
|
||||||
- Tesseract 4.1.1 or newer
|
- Tesseract 4.1.1 or newer
|
||||||
- jbig2enc 0.29 or newer
|
- jbig2enc 0.29 or newer
|
||||||
- pngquant 2.5 or newer
|
- pngquant 2.5 or newer
|
||||||
- unpaper 6.1
|
- unpaper 6.1
|
||||||
|
|
||||||
|
We recommend 64-bit versions of all software. (32-bit versions are not
|
||||||
|
supported, although on Linux, they may still work.)
|
||||||
|
|
||||||
jbig2enc, pngquant, and unpaper are optional. If missing certain
|
jbig2enc, pngquant, and unpaper are optional. If missing certain
|
||||||
features are disabled. OCRmyPDF will discover them as soon as they are
|
features are disabled. OCRmyPDF will discover them as soon as they are
|
||||||
available.
|
available.
|
||||||
@@ -608,7 +586,7 @@ unfortunately, the ``pip install`` command cannot satisfy all of them.
|
|||||||
Installing HEAD revision from sources
|
Installing HEAD revision from sources
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
If you have ``git`` and Python 3.8 or newer installed, you can install
|
If you have ``git`` and Python 3.9 or newer installed, you can install
|
||||||
from source. When the ``pip`` installer runs, it will alert you if
|
from source. When the ``pip`` installer runs, it will alert you if
|
||||||
dependencies are missing.
|
dependencies are missing.
|
||||||
|
|
||||||
@@ -638,8 +616,8 @@ system-wide:
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
|
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
|
||||||
python3 -m venv venv
|
python3 -m venv .venv
|
||||||
source venv/bin/activate
|
source .venv/bin/activate
|
||||||
cd OCRmyPDF
|
cd OCRmyPDF
|
||||||
pip install .
|
pip install .
|
||||||
|
|
||||||
@@ -665,8 +643,8 @@ To install all of the development and test requirements:
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
|
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
|
||||||
python -m venv
|
python -m .venv
|
||||||
source venv/bin/activate
|
source .venv/bin/activate
|
||||||
cd OCRmyPDF
|
cd OCRmyPDF
|
||||||
pip install -e .[test]
|
pip install -e .[test]
|
||||||
|
|
||||||
@@ -687,3 +665,18 @@ To manually install the ``bash`` completion, copy
|
|||||||
To manually install the ``fish`` completion, copy
|
To manually install the ``fish`` completion, copy
|
||||||
``misc/completion/ocrmypdf.fish`` to
|
``misc/completion/ocrmypdf.fish`` to
|
||||||
``~/.config/fish/completions/ocrmypdf.fish``.
|
``~/.config/fish/completions/ocrmypdf.fish``.
|
||||||
|
|
||||||
|
Note on 32-bit support
|
||||||
|
======================
|
||||||
|
|
||||||
|
Many Python libraries no longer 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.
|
||||||
|
|
||||||
|
However, if your Linux distribution still supports 32-bit binaries, you
|
||||||
|
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.
|
||||||
@@ -59,5 +59,9 @@ To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``.
|
|||||||
also required. Also, a JBIG2 encoder must be installed as described in
|
also required. Also, a JBIG2 encoder must be installed as described in
|
||||||
the previous section.
|
the previous section.
|
||||||
|
|
||||||
|
You can adjust the threshold for JBIG2 compression with the
|
||||||
|
``--jbig2-threshold``. The default is 0.85, meaning that if two symbols
|
||||||
|
are 85% similar, they will be compressed together.
|
||||||
|
|
||||||
*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by
|
*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by
|
||||||
default.*
|
default.*
|
||||||
|
|||||||
@@ -45,11 +45,6 @@ to indicate that your distribution modifies OCRmyPDF in some way.
|
|||||||
You can patch the ``__version__`` variable in ``src/ocrmypdf/_version.py`` if
|
You can patch the ``__version__`` variable in ``src/ocrmypdf/_version.py`` if
|
||||||
necessary.
|
necessary.
|
||||||
|
|
||||||
OCRmyPDF uses setuptools-scm-git-archive to ensure that tarballs downloaded from
|
|
||||||
GitHub contain version information. Unfortunately, these tarballs are not always
|
|
||||||
deterministic. See this
|
|
||||||
`issue <https://github.com/ocrmypdf/OCRmyPDF/issues/841#issuecomment-936562696>`_.
|
|
||||||
|
|
||||||
jbig2enc
|
jbig2enc
|
||||||
--------
|
--------
|
||||||
|
|
||||||
@@ -61,4 +56,12 @@ improve OCRmyPDF's compression.
|
|||||||
Command line completions
|
Command line completions
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
Please ensure that command line completions are installed.
|
Please ensure that command line completions are installed, as described in the
|
||||||
|
installation documentation.
|
||||||
|
|
||||||
|
32-bit Linux support
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
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.
|
||||||
+2
-2
@@ -45,8 +45,8 @@ Optimizations that always occurs
|
|||||||
================================
|
================================
|
||||||
|
|
||||||
OCRmyPDF will automatically replace obsolete or inferior compression schemes
|
OCRmyPDF will automatically replace obsolete or inferior compression schemes
|
||||||
such as RLE or LZW with superior schemes such as Deflate and converting
|
such as RLE or LZW with superior schemes such as Deflate, and convert
|
||||||
monochrome images to CCITT G4. Since this is harmless it always occurs and there
|
monochrome images to CCITT G4. Since this is lossless, it always occurs and there
|
||||||
is no way to disable it. Other non-image compressed objects are compressed as
|
is no way to disable it. Other non-image compressed objects are compressed as
|
||||||
well.
|
well.
|
||||||
|
|
||||||
|
|||||||
+55
-14
@@ -54,31 +54,72 @@ into the existing PDF or it may essentially reconstruct ("re-fry") a
|
|||||||
visually identical PDF that may be quite different at the binary level.
|
visually identical PDF that may be quite different at the binary level.
|
||||||
That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
|
That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
|
||||||
|
|
||||||
Password protection, digital signatures and certification
|
Password protected PDFs
|
||||||
=========================================================
|
=======================
|
||||||
|
|
||||||
Password protected PDFs usually have two passwords, and owner and user
|
Password protected PDFs usually have two passwords, and owner and user
|
||||||
password. When the user password is set to empty, PDF readers will open
|
password. When the user password is set to empty, PDF readers will open
|
||||||
the file automatically and marked it as "(SECURED)". While not as
|
the file automatically and mark it as "(SECURED)". Password security can
|
||||||
reliable as a digital signature, this indicates that whoever set the
|
also request certain restrictions on the PDF, but anyone can remove these
|
||||||
password approved of the file at that time. When the user password is
|
restrictions if they have either the owner *or* user password. Passwords
|
||||||
set, the document cannot be viewed without the password.
|
mainly present a barrier for casual users.
|
||||||
|
|
||||||
Either way, OCRmyPDF does not remove passwords from PDFs and exits with
|
OCRmyPDF cannot remove passwords from PDFs. If you want to remove a
|
||||||
an error on encountering them.
|
password from a PDF, you must use other software, such as ``qpdf``.
|
||||||
|
|
||||||
``qpdf`` can remove passwords. If the owner and user password are set, a
|
If the owner and user password are set, a
|
||||||
password is required for ``qpdf``. If only the owner password is set, then the
|
password is required for ``qpdf``. If only the owner password is set, then the
|
||||||
password can be stripped, even if one does not have the owner password.
|
password can be stripped, even if one does not have the owner password. To
|
||||||
|
remove the password from a using QPDF, use:
|
||||||
|
|
||||||
After OCR is applied, password protection is not permitted on PDF/A
|
.. code-block:: bash
|
||||||
documents but the file can be converted to regular PDF.
|
|
||||||
|
qpdf --decrypt --password='abc123' input.pdf no_password.pdf
|
||||||
|
|
||||||
|
Then you can run OCRmyPDF on the file.
|
||||||
|
|
||||||
|
In its default mode, OCRmyPDF generates PDF/A. Passwords may not be set on PDF/A
|
||||||
|
documents. If you want to set a password on the output PDF, you must
|
||||||
|
specify ``--output-type pdf``.
|
||||||
|
|
||||||
|
Signature images
|
||||||
|
================
|
||||||
|
|
||||||
Many programs exist which are capable of inserting an image of someone's
|
Many programs exist which are capable of inserting an image of someone's
|
||||||
signature. On its own, this offers no security guarantees. It is trivial
|
signature. On its own, this offers no security guarantees. It is trivial
|
||||||
to remove the signature image and apply it to other files. This practice
|
to remove the signature image and apply it to other files. This practice
|
||||||
offers no real security.
|
offers no real security.
|
||||||
|
|
||||||
|
Digital signatures
|
||||||
|
==================
|
||||||
|
|
||||||
Important documents can be digitally signed and certified to attest to
|
Important documents can be digitally signed and certified to attest to
|
||||||
their authorship. OCRmyPDF cannot do this. Open source tools such as
|
their authorship, approval or execution of a legal agreement. OCRmyPDF
|
||||||
pdfbox (Java) have this capability as does Adobe Acrobat.
|
will detect signed PDFs and will not modify them, unless the
|
||||||
|
``--invalidate-digital-signatures`` option is used, which will
|
||||||
|
invalidate any signatures. (The signature may still be present in the PDF
|
||||||
|
if opened, but PDF readers will not validate it.)
|
||||||
|
|
||||||
|
A digital signature adds a cryptographic hash of the document to the
|
||||||
|
document, so tamper protection is provided. That also precludes OCRmyPDF
|
||||||
|
from modifying the document and preserving the signature.
|
||||||
|
|
||||||
|
Digital signatures are not the same as a signature image. A digital
|
||||||
|
signature is a cryptographic hash of the document that is encrypted with
|
||||||
|
the author's private key. The signature is decrypted with the author's
|
||||||
|
public key. The public key is usually distributed by a certificate
|
||||||
|
authority. The signature is then verified by the PDF reader. If the
|
||||||
|
document is modified, the signature will be invalidated.
|
||||||
|
|
||||||
|
Certificate-encrypted PDFs
|
||||||
|
==========================
|
||||||
|
|
||||||
|
PDFs can be encrypted with a certificate. This is a more secure form of
|
||||||
|
encryption than a password. The certificate is usually issued by a
|
||||||
|
certificate authority. A certificate is used to encrypt the document using
|
||||||
|
the public key for the benefit of a specific recipient who possesses
|
||||||
|
the private key.
|
||||||
|
|
||||||
|
OCRmyPDF cannot open certificate-encrypted PDFs. If you have the
|
||||||
|
certificate, you can use other PDF software, such as Acrobat, to
|
||||||
|
decrypt the PDF.
|
||||||
+96
-1
@@ -28,6 +28,101 @@ tagged yet.
|
|||||||
|
|
||||||
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
|
||||||
|
|
||||||
|
v15.3.1
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Fixed an issue with logging settings for misc/watcher.py introduced in the
|
||||||
|
previous release. :issue:`1180`
|
||||||
|
- Updated documentation on Docker performance concerns.
|
||||||
|
|
||||||
|
v15.3.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Update misc/watcher.py to improve command line interface using Typer, and
|
||||||
|
support ``.env`` specification of environment variables. Improved error
|
||||||
|
messages. Thanks to @mflagg2814 for the PR that prompted this improvement.
|
||||||
|
- Improved error message when a file cannot be read because we are running in
|
||||||
|
a snap container.
|
||||||
|
|
||||||
|
v15.2.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Added a Docker image based on Alpine Linux. This image is smaller than the
|
||||||
|
Ubuntu-based image and may be useful in some situations. Currently hosted at
|
||||||
|
jbarlow83/ocrmypdf-alpine. Currently not available in ARM flavor.
|
||||||
|
- The Ubuntu Docker is now aliased to jbarlow83/ocrmypdf-ubuntu.
|
||||||
|
- Updated Docker documentation.
|
||||||
|
|
||||||
|
v15.1.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- We now require Pillow 10.0.1, due a serious security vulnerability in all earlier
|
||||||
|
versions of that dependency. The vulnerability concerns WebP images and could
|
||||||
|
be triggered in OCRmyPDF when creating a PDF from a malicious WebP image.
|
||||||
|
- Added some keyword arguments to ``ocrmypdf.ocr`` that were previously accepted
|
||||||
|
but undocumented.
|
||||||
|
- Documentation updates and typing improvements.
|
||||||
|
|
||||||
|
v15.0.2
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Added Python 3.12 to test matrix.
|
||||||
|
- Updated documentation for notes on Python 3.12, 32-bit support and some new
|
||||||
|
features in v15.
|
||||||
|
|
||||||
|
v15.0.1
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Wheels Python tag changed to py39.
|
||||||
|
- Marked as a expected fail a test that fails on recent Ghostscript versions.
|
||||||
|
- Clarified documentation and release notes around the extent of 32-bit support.
|
||||||
|
- Updated installation documentation to changes in v15.
|
||||||
|
|
||||||
|
v15.0.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Dropped support for Python 3.8.
|
||||||
|
- Dropped support some older dependencies, specifically ``coloredlogs`` and
|
||||||
|
``tqdm`` in favor of rich - see ``pyproject.toml`` for details.
|
||||||
|
Generally speaking, Ubuntu 22.04 is our new baseline system.
|
||||||
|
- Tightened version requirements for some dependencies.
|
||||||
|
- Dropped support for 32-bit Linux wheels. We strongly recommend a 64-bit operating
|
||||||
|
system, and 64-bit versions of Python, Tesseract and Ghostscript to use OCRmyPDF.
|
||||||
|
Many of our dependencies are dropping 32-bit builds (e.g. Pillow), and we are
|
||||||
|
following suit. (Maintainers may still build 32-bit versions from source.)
|
||||||
|
- Changed to trusted release for PyPI publishing.
|
||||||
|
- pikepdf memory mapping is enabled again for improved performance, now that an
|
||||||
|
issue with feature in pikepdf is fixed.
|
||||||
|
- ``ocrmypdf.helpers.calculate_downsample`` previously had two variants, one
|
||||||
|
that took a ``PIL.Image`` and one that took a ``tuple[int, int]``. The latter
|
||||||
|
was removed.
|
||||||
|
- The snap version of ocrmypdf is now based on Ubuntu core22.
|
||||||
|
- We now account for situations where a small portion of an image on a page is drawn
|
||||||
|
at high DPI (resolution). Previously, the entire page would be rasterized at the
|
||||||
|
highest resolution of any feature, which caused performance problems. Now,
|
||||||
|
the page is rasterized
|
||||||
|
at a resolution based on the average DPI of the page, weighted by the area that
|
||||||
|
each feature occupies. Typically, small areas of high resolution in PDFs are
|
||||||
|
errors or quirks from the repeated use of assets and high resolution is not
|
||||||
|
beneficial. :issue:`1010,1104,1004,1079,1010`
|
||||||
|
- Ghostscript color conversion strategy is now configurable using
|
||||||
|
``--color-conversion-strategy``. :issue:`1143`
|
||||||
|
- JBIG2 threshold for optimization is now configurable using
|
||||||
|
``--jbig2-threshold``. :issue:`1133`
|
||||||
|
|
||||||
|
v14.4.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Digitally signed PDFs are now detected. If the PDF is signed, OCRmyPDF will
|
||||||
|
refuse to modify it. Previously, only encrypted PDFs were detected, not
|
||||||
|
those that were signed but not encrypted. :issue:`1040`
|
||||||
|
- In addition, ``--invalidate-digital-signatures`` can be used to override the
|
||||||
|
above behavior and modify the PDF anyway. :issue:`1040`
|
||||||
|
- tqdm progress bars replaced with "rich" progress bars. The rich library is
|
||||||
|
a new dependency. Certain APIs that used tqdm are now deprecated and will
|
||||||
|
be removed in the next major release.
|
||||||
|
- Improved integration with GitHub Releases. Thanks to @stumpylog.
|
||||||
|
|
||||||
v14.3.0
|
v14.3.0
|
||||||
=======
|
=======
|
||||||
|
|
||||||
@@ -54,7 +149,7 @@ v14.2.1
|
|||||||
v14.2.0
|
v14.2.0
|
||||||
=======
|
=======
|
||||||
|
|
||||||
- Added `--tesseract-downsample-above` to downsample larger images even when
|
- Added ``--tesseract-downsample-above`` to downsample larger images even when
|
||||||
they do not exceed Tesseract's internal limits. This can be used to speed
|
they do not exceed Tesseract's internal limits. This can be used to speed
|
||||||
up OCR, possibly sacrificing accuracy.
|
up OCR, possibly sacrificing accuracy.
|
||||||
- Fixed resampling AttributeError on older Pillow. :issue:`1096`
|
- Fixed resampling AttributeError on older Pillow. :issue:`1096`
|
||||||
|
|||||||
@@ -6,52 +6,56 @@ set -o errexit
|
|||||||
|
|
||||||
__ocrmypdf_arguments()
|
__ocrmypdf_arguments()
|
||||||
{
|
{
|
||||||
local arguments="--help (show help message)
|
local arguments="\
|
||||||
--language (language(s) of the file to be OCRed)
|
--help (show help message)
|
||||||
--image-dpi (assume this DPI if input image DPI is unknown)
|
--language (language(s) of the file to be OCRed)
|
||||||
--output-type (select PDF output options)
|
--image-dpi (assume this DPI if input image DPI is unknown)
|
||||||
--sidecar (write OCR to text file)
|
--output-type (select PDF output options)
|
||||||
--version (print program version and exit)
|
--sidecar (write OCR to text file)
|
||||||
--jobs (how many worker processes to use)
|
--version (print program version and exit)
|
||||||
--quiet (suppress INFO messages)
|
--jobs (how many worker processes to use)
|
||||||
--verbose (set verbosity level)
|
--quiet (suppress INFO messages)
|
||||||
--title (set metadata)
|
--verbose (set verbosity level)
|
||||||
--author (set metadata)
|
--title (set metadata)
|
||||||
--subject (set metadata)
|
--author (set metadata)
|
||||||
--keywords (set metadata)
|
--subject (set metadata)
|
||||||
--rotate-pages (rotate pages to correct orientation)
|
--keywords (set metadata)
|
||||||
--remove-background (attempt to remove background from pages)
|
--rotate-pages (rotate pages to correct orientation)
|
||||||
--deskew (fix small horizontal alignment skew)
|
--remove-background (attempt to remove background from pages)
|
||||||
--clean (clean document images before OCR)
|
--deskew (fix small horizontal alignment skew)
|
||||||
--clean-final (clean document images and keep result)
|
--clean (clean document images before OCR)
|
||||||
--unpaper-args (a quoted string of arguments to pass to unpaper)
|
--clean-final (clean document images and keep result)
|
||||||
--oversample (oversample images to this DPI)
|
--unpaper-args (a quoted string of arguments to pass to unpaper)
|
||||||
--remove-vectors (don\'t send vector objects to OCR)
|
--oversample (oversample images to this DPI)
|
||||||
--threshold (threshold images before OCR)
|
--remove-vectors (don\'t send vector objects to OCR)
|
||||||
--force-ocr (OCR documents that already have printable text)
|
--threshold (threshold images before OCR)
|
||||||
--skip-text (skip OCR on any pages that already contain text)
|
--force-ocr (OCR documents that already have printable text)
|
||||||
--redo-ocr (redo OCR on any pages that seem to have OCR already)
|
--skip-text (skip OCR on any pages that already contain text)
|
||||||
--skip-big (skip OCR on pages larger than this many MPixels)
|
--redo-ocr (redo OCR on any pages that seem to have OCR already)
|
||||||
--optimize (select optimization level)
|
--invalidate-digital-signatures (remove digital signatures from PDF)
|
||||||
--jpeg-quality (JPEG quality [0..100])
|
--skip-big (skip OCR on pages larger than this many MPixels)
|
||||||
--png-quality (PNG quality [0..100])
|
--optimize (select optimization level)
|
||||||
--jbig2-lossy (enable lossy JBIG2 (see docs))
|
--jpeg-quality (JPEG quality [0..100])
|
||||||
--pages (apply OCR to only the specified pages)
|
--png-quality (PNG quality [0..100])
|
||||||
--max-image-mpixels (image decompression bomb threshold)
|
--jbig2-lossy (enable lossy JBIG2 (see docs))
|
||||||
--pdf-renderer (select PDF renderer options)
|
--jbig2-threshold (set JBIG2 threshold (see docs))
|
||||||
--rotate-pages-threshold (page rotation confidence)
|
--pages (apply OCR to only the specified pages)
|
||||||
--pdfa-image-compression (set PDF/A image compression options)
|
--max-image-mpixels (image decompression bomb threshold)
|
||||||
--fast-web-view (if file size if above this amount in MB linearize PDF)
|
--pdf-renderer (select PDF renderer options)
|
||||||
--plugin (name of plugin to import)
|
--rotate-pages-threshold (page rotation confidence)
|
||||||
--keep-temporary-files (keep temporary files (debug)
|
--pdfa-image-compression (set PDF/A image compression options)
|
||||||
--tesseract-config (set custom tesseract config file)
|
--fast-web-view (if file size if above this amount in MB linearize PDF)
|
||||||
--tesseract-pagesegmode (set tesseract --psm)
|
--plugin (name of plugin to import)
|
||||||
--tesseract-oem (set tesseract --oem)
|
--keep-temporary-files (keep temporary files (debug)
|
||||||
--tesseract-thresholding (set tesseract image thresholding)
|
--tesseract-config (set custom tesseract config file)
|
||||||
--tesseract-timeout (maximum number of seconds to wait for OCR)
|
--tesseract-pagesegmode (set tesseract --psm)
|
||||||
--user-words (specify location of user words file)
|
--tesseract-oem (set tesseract --oem)
|
||||||
--user-patterns (specify location of user patterns file)
|
--tesseract-thresholding (set tesseract image thresholding)
|
||||||
--no-progress-bar (disable the progress bar)
|
--tesseract-timeout (maximum number of seconds to wait for OCR)
|
||||||
|
--user-words (specify location of user words file)
|
||||||
|
--user-patterns (specify location of user patterns file)
|
||||||
|
--no-progress-bar (disable the progress bar)
|
||||||
|
--color-conversion-strategy (select color conversion strategy)
|
||||||
"
|
"
|
||||||
|
|
||||||
COMPREPLY=( $( compgen -W "$arguments" -- "$cur") )
|
COMPREPLY=( $( compgen -W "$arguments" -- "$cur") )
|
||||||
@@ -191,6 +195,20 @@ sauvola (use Sauvola thresholding)"
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__ocrmypdf_color-conversion-strategy()
|
||||||
|
{
|
||||||
|
local choices="LeaveColorUnchanged (default)
|
||||||
|
CMYK (convert to CMYK)
|
||||||
|
Gray (convert to grayscale)
|
||||||
|
RGB (convert to RGB)
|
||||||
|
UseDeviceIndependentColor (convert with device independent color)"
|
||||||
|
|
||||||
|
COMPREPLY=( $( compgen -W "$choices" -- "$cur") )
|
||||||
|
# Remove description if only one completion exists
|
||||||
|
if [[ ${#COMPREPLY[*]} -eq 1 ]]; then
|
||||||
|
COMPREPLY=( ${COMPREPLY[0]%% *} )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
__ocrmypdf_check_previous()
|
__ocrmypdf_check_previous()
|
||||||
{
|
{
|
||||||
@@ -250,6 +268,10 @@ __ocrmypdf_check_previous()
|
|||||||
_filedir
|
_filedir
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--color-conversion-strategy)
|
||||||
|
__ocrmypdf_color-conversion-strategy
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ complete -c ocrmypdf -s i -l clean-final -d "clean document images and keep resu
|
|||||||
complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
|
complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
|
||||||
|
|
||||||
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
|
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
|
||||||
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
|
complete -c ocrmypdf -s s -l skip-text -d "skip OCR on any pages that already contain text"
|
||||||
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
|
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
|
||||||
|
complete -c ocrmypdf -l invalidate-digital-signatures -d "invalidate digital signatures and allow OCR to proceed"
|
||||||
|
|
||||||
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
|
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ complete -c ocrmypdf -x -l skip-big -d "skip OCR on pages larger than this many
|
|||||||
complete -c ocrmypdf -x -l jpeg-quality -d "JPEG quality [0..100]"
|
complete -c ocrmypdf -x -l jpeg-quality -d "JPEG quality [0..100]"
|
||||||
complete -c ocrmypdf -x -l png-quality -d "PNG quality [0..100]"
|
complete -c ocrmypdf -x -l png-quality -d "PNG quality [0..100]"
|
||||||
complete -c ocrmypdf -x -l jbig2-lossy -d "enable lossy JBIG2 (see docs)"
|
complete -c ocrmypdf -x -l jbig2-lossy -d "enable lossy JBIG2 (see docs)"
|
||||||
|
complete -c ocrmypdf -x -l jbig2-threshold -d "JBIG2 compression threshold (see docs)"
|
||||||
complete -c ocrmypdf -x -l max-image-mpixels -d "image decompression bomb threshold"
|
complete -c ocrmypdf -x -l max-image-mpixels -d "image decompression bomb threshold"
|
||||||
complete -c ocrmypdf -x -l pages -d "apply OCR to only the specified pages"
|
complete -c ocrmypdf -x -l pages -d "apply OCR to only the specified pages"
|
||||||
complete -c ocrmypdf -x -l tesseract-config -d "set custom tesseract config file"
|
complete -c ocrmypdf -x -l tesseract-config -d "set custom tesseract config file"
|
||||||
@@ -128,4 +130,27 @@ complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
|
|||||||
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
|
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
|
||||||
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
|
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
|
||||||
|
|
||||||
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf; __fish_complete_suffix .PDF; __fish_complete_suffix .jpg; __fish_complete_suffix .png)"
|
function __fish_ocrmypdf_color_conversion_strategy
|
||||||
|
echo -e "LeaveColorUnchanged\t"(_ "do not convert color spaces (default)")
|
||||||
|
echo -e "CMYK\t"(_ "convert all color spaces to CMYK")
|
||||||
|
echo -e "Gray\t"(_ "convert all color spaces to grayscale")
|
||||||
|
echo -e "RGB\t"(_ "convert all color spaces to RGB")
|
||||||
|
echo -e "UseDeviceIndependentColor\t"(_ "convert all color spaces to ICC-based color spaces")
|
||||||
|
end
|
||||||
|
|
||||||
|
complete -c ocrmypdf -x -l color-conversion-strategy -a '(__fish_ocrmypdf_color_conversion_strategy)' -d "set color conversion strategy"
|
||||||
|
|
||||||
|
function __fish_ocrmypdf_input_file_given
|
||||||
|
set -l tokens (commandline -opc)
|
||||||
|
for token in $tokens
|
||||||
|
if string match -q -r '^-' -- $token
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
if test -f "$token"
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
complete -c ocrmypdf -x -n 'not __fish_ocrmypdf_input_file_given' -a "(__fish_complete_suffix .pdf)" -d "input file"
|
||||||
|
|||||||
+231
-75
@@ -5,107 +5,122 @@
|
|||||||
|
|
||||||
"""Watch a directory for new PDFs and OCR them."""
|
"""Watch a directory for new PDFs and OCR them."""
|
||||||
|
|
||||||
from __future__ import annotations
|
# Do not enable annotations!
|
||||||
|
# https://github.com/tiangolo/typer/discussions/598
|
||||||
|
# from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
import typer
|
||||||
|
from dotenv import load_dotenv
|
||||||
from watchdog.events import PatternMatchingEventHandler
|
from watchdog.events import PatternMatchingEventHandler
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.observers.polling import PollingObserver
|
from watchdog.observers.polling import PollingObserver
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=logging-format-interpolation
|
# pylint: disable=logging-format-interpolation
|
||||||
|
app = typer.Typer(name="ocrmypdf-watcher")
|
||||||
|
|
||||||
def getenv_bool(name: str, default: str = 'False'):
|
|
||||||
return os.getenv(name, default).lower() in ('true', 'yes', 'y', '1')
|
|
||||||
|
|
||||||
|
|
||||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
|
||||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
|
||||||
ARCHIVE_DIRECTORY = os.getenv('OCR_ARCHIVE_DIRECTORY', '/processed')
|
|
||||||
OUTPUT_DIRECTORY_YEAR_MONTH = getenv_bool('OCR_OUTPUT_DIRECTORY_YEAR_MONTH')
|
|
||||||
ON_SUCCESS_DELETE = getenv_bool('OCR_ON_SUCCESS_DELETE')
|
|
||||||
ON_SUCCESS_ARCHIVE = getenv_bool('OCR_ON_SUCCESS_ARCHIVE')
|
|
||||||
DESKEW = getenv_bool('OCR_DESKEW')
|
|
||||||
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
|
||||||
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
|
|
||||||
USE_POLLING = getenv_bool('OCR_USE_POLLING')
|
|
||||||
RETRIES_LOADING_FILE = int(os.getenv('OCR_RETRIES_LOADING_FILE', '5'))
|
|
||||||
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO')
|
|
||||||
PATTERNS = ['*.pdf', '*.PDF']
|
|
||||||
|
|
||||||
log = logging.getLogger('ocrmypdf-watcher')
|
log = logging.getLogger('ocrmypdf-watcher')
|
||||||
|
|
||||||
|
|
||||||
def get_output_dir(root, basename):
|
class LoggingLevelEnum(str, Enum):
|
||||||
if OUTPUT_DIRECTORY_YEAR_MONTH:
|
"""Enum for logging levels."""
|
||||||
|
|
||||||
|
DEBUG = "DEBUG"
|
||||||
|
INFO = "INFO"
|
||||||
|
WARNING = "WARNING"
|
||||||
|
ERROR = "ERROR"
|
||||||
|
CRITICAL = "CRITICAL"
|
||||||
|
|
||||||
|
|
||||||
|
def get_output_dir(root: Path, basename: str, output_dir_year_month: bool) -> Path:
|
||||||
|
if output_dir_year_month:
|
||||||
today = datetime.today()
|
today = datetime.today()
|
||||||
output_directory_year_month = (
|
output_directory_year_month = root / str(today.year) / f'{today.month:02d}'
|
||||||
Path(root) / str(today.year) / f'{today.month:02d}'
|
|
||||||
)
|
|
||||||
if not output_directory_year_month.exists():
|
if not output_directory_year_month.exists():
|
||||||
output_directory_year_month.mkdir(parents=True, exist_ok=True)
|
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) / basename
|
||||||
else:
|
else:
|
||||||
output_path = Path(OUTPUT_DIRECTORY) / basename
|
output_path = root / basename
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
def wait_for_file_ready(file_path):
|
def wait_for_file_ready(
|
||||||
|
file_path: Path, poll_new_file_seconds: int, retries_loading_file: int
|
||||||
|
):
|
||||||
# This loop waits to make sure that the file is completely loaded on
|
# This loop waits to make sure that the file is completely loaded on
|
||||||
# disk before attempting to read. Docker sometimes will publish the
|
# disk before attempting to read. Docker sometimes will publish the
|
||||||
# watchdog event before the file is actually fully on disk, causing
|
# watchdog event before the file is actually fully on disk, causing
|
||||||
# pikepdf to fail.
|
# pikepdf to fail.
|
||||||
|
|
||||||
retries = RETRIES_LOADING_FILE
|
tries = retries_loading_file + 1
|
||||||
while retries:
|
while tries:
|
||||||
try:
|
try:
|
||||||
pdf = pikepdf.open(file_path)
|
with pikepdf.Pdf.open(file_path) as pdf:
|
||||||
except (FileNotFoundError, pikepdf.PdfError) as e:
|
log.debug(f"{file_path} ready with {pdf.pages} pages")
|
||||||
|
return True
|
||||||
|
except (FileNotFoundError, OSError) as e:
|
||||||
log.info(f"File {file_path} is not ready yet")
|
log.info(f"File {file_path} is not ready yet")
|
||||||
log.debug("Exception was", exc_info=e)
|
log.debug("Exception was", exc_info=e)
|
||||||
time.sleep(POLL_NEW_FILE_SECONDS)
|
time.sleep(poll_new_file_seconds)
|
||||||
retries -= 1
|
tries -= 1
|
||||||
else:
|
except pikepdf.PdfError as e:
|
||||||
pdf.close()
|
log.info(f"File {file_path} is not full written yet")
|
||||||
return True
|
log.debug("Exception was", exc_info=e)
|
||||||
|
time.sleep(poll_new_file_seconds)
|
||||||
|
tries -= 1
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def execute_ocrmypdf(file_path):
|
def execute_ocrmypdf(
|
||||||
file_path = Path(file_path)
|
*,
|
||||||
output_path = get_output_dir(OUTPUT_DIRECTORY, file_path.name)
|
file_path: Path,
|
||||||
|
archive_dir: Path,
|
||||||
|
output_dir: Path,
|
||||||
|
deskew: bool,
|
||||||
|
ocrmypdf_kwargs: dict[str, Any],
|
||||||
|
on_success_delete: bool,
|
||||||
|
on_success_archive: bool,
|
||||||
|
poll_new_file_seconds: int,
|
||||||
|
retries_loading_file: int,
|
||||||
|
output_dir_year_month: bool,
|
||||||
|
):
|
||||||
|
output_path = get_output_dir(output_dir, file_path.name, output_dir_year_month)
|
||||||
|
|
||||||
log.info("-" * 20)
|
log.info("-" * 20)
|
||||||
log.info(f'New file: {file_path}. Waiting until fully loaded...')
|
log.info(f'New file: {file_path}. Waiting until fully written...')
|
||||||
if not wait_for_file_ready(file_path):
|
if not wait_for_file_ready(file_path, poll_new_file_seconds, retries_loading_file):
|
||||||
log.info(f"Gave up waiting for {file_path} to become ready")
|
log.info(f"Gave up waiting for {file_path} to become ready")
|
||||||
return
|
return
|
||||||
log.info(f'Attempting to OCRmyPDF to: {output_path}')
|
log.info(f'Attempting to OCRmyPDF to: {output_path}')
|
||||||
exit_code = ocrmypdf.ocr(
|
exit_code = ocrmypdf.ocr(
|
||||||
input_file=file_path,
|
input_file=file_path,
|
||||||
output_file=output_path,
|
output_file=output_path,
|
||||||
deskew=DESKEW,
|
deskew=deskew,
|
||||||
**OCR_JSON_SETTINGS,
|
**ocrmypdf_kwargs,
|
||||||
)
|
)
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
if ON_SUCCESS_DELETE:
|
if on_success_delete:
|
||||||
log.info(f'OCR is done. Deleting: {file_path}')
|
log.info(f'OCR is done. Deleting: {file_path}')
|
||||||
file_path.unlink()
|
file_path.unlink()
|
||||||
elif ON_SUCCESS_ARCHIVE:
|
elif on_success_archive:
|
||||||
log.info(f'OCR is done. Archiving {file_path.name} to {ARCHIVE_DIRECTORY}')
|
log.info(f'OCR is done. Archiving {file_path.name} to {archive_dir}')
|
||||||
shutil.move(file_path, f'{ARCHIVE_DIRECTORY}/{file_path.name}')
|
shutil.move(file_path, f'{archive_dir}/{file_path.name}')
|
||||||
else:
|
else:
|
||||||
log.info('OCR is done')
|
log.info('OCR is done')
|
||||||
else:
|
else:
|
||||||
@@ -113,61 +128,202 @@ def execute_ocrmypdf(file_path):
|
|||||||
|
|
||||||
|
|
||||||
class HandleObserverEvent(PatternMatchingEventHandler):
|
class HandleObserverEvent(PatternMatchingEventHandler):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
patterns=None,
|
||||||
|
ignore_patterns=None,
|
||||||
|
ignore_directories=False,
|
||||||
|
case_sensitive=False,
|
||||||
|
settings={},
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
patterns=patterns,
|
||||||
|
ignore_patterns=ignore_patterns,
|
||||||
|
ignore_directories=ignore_directories,
|
||||||
|
case_sensitive=case_sensitive,
|
||||||
|
)
|
||||||
|
self._settings = settings
|
||||||
|
|
||||||
def on_any_event(self, event):
|
def on_any_event(self, event):
|
||||||
if event.event_type in ['created']:
|
if event.event_type in ['created']:
|
||||||
execute_ocrmypdf(event.src_path)
|
execute_ocrmypdf(event.src_path, **self._settings)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
@app.command()
|
||||||
|
def main(
|
||||||
|
input_dir: Annotated[
|
||||||
|
Path,
|
||||||
|
typer.Argument(
|
||||||
|
envvar='OCR_INPUT_DIRECTORY',
|
||||||
|
exists=True,
|
||||||
|
file_okay=False,
|
||||||
|
dir_okay=True,
|
||||||
|
readable=True,
|
||||||
|
resolve_path=True,
|
||||||
|
),
|
||||||
|
] = '/input',
|
||||||
|
output_dir: Annotated[
|
||||||
|
Path,
|
||||||
|
typer.Argument(
|
||||||
|
envvar='OCR_OUTPUT_DIRECTORY',
|
||||||
|
exists=True,
|
||||||
|
file_okay=False,
|
||||||
|
dir_okay=True,
|
||||||
|
writable=True,
|
||||||
|
resolve_path=True,
|
||||||
|
),
|
||||||
|
] = '/output',
|
||||||
|
archive_dir: Annotated[
|
||||||
|
Path,
|
||||||
|
typer.Argument(
|
||||||
|
envvar='OCR_ARCHIVE_DIRECTORY',
|
||||||
|
exists=True,
|
||||||
|
file_okay=False,
|
||||||
|
dir_okay=True,
|
||||||
|
writable=True,
|
||||||
|
resolve_path=True,
|
||||||
|
),
|
||||||
|
] = '/processed',
|
||||||
|
output_dir_year_month: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH',
|
||||||
|
help='Create a subdirectory in the output directory for each year and month',
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
on_success_delete: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_ON_SUCCESS_DELETE',
|
||||||
|
help='Delete the input file after successful OCR',
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
on_success_archive: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_ON_SUCCESS_ARCHIVE',
|
||||||
|
help='Archive the input file after successful OCR',
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
deskew: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_DESKEW',
|
||||||
|
help='Deskew the input file before OCR',
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
ocr_json_settings: Annotated[
|
||||||
|
typer.FileText,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_JSON_SETTINGS',
|
||||||
|
help='JSON settings to pass to OCRmyPDF',
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
poll_new_file_seconds: Annotated[
|
||||||
|
int,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_POLL_NEW_FILE_SECONDS',
|
||||||
|
help='Seconds to wait before polling a new file',
|
||||||
|
min=0,
|
||||||
|
),
|
||||||
|
] = 1,
|
||||||
|
use_polling: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_USE_POLLING',
|
||||||
|
help='Use polling instead of filesystem events',
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
retries_loading_file: Annotated[
|
||||||
|
int,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_RETRIES_LOADING_FILE',
|
||||||
|
help='Number of times to retry loading a file before giving up',
|
||||||
|
min=0,
|
||||||
|
),
|
||||||
|
] = 5,
|
||||||
|
loglevel: Annotated[
|
||||||
|
LoggingLevelEnum,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_LOGLEVEL',
|
||||||
|
help='Logging level',
|
||||||
|
),
|
||||||
|
] = LoggingLevelEnum.INFO,
|
||||||
|
patterns: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option(
|
||||||
|
envvar='OCR_PATTERNS',
|
||||||
|
help='File patterns to watch',
|
||||||
|
),
|
||||||
|
] = '*.pdf,*.PDF',
|
||||||
|
):
|
||||||
ocrmypdf.configure_logging(
|
ocrmypdf.configure_logging(
|
||||||
verbosity=(
|
verbosity=(
|
||||||
ocrmypdf.Verbosity.default
|
ocrmypdf.Verbosity.default
|
||||||
if LOGLEVEL != 'DEBUG'
|
if loglevel != LoggingLevelEnum.DEBUG
|
||||||
else ocrmypdf.Verbosity.debug
|
else ocrmypdf.Verbosity.debug
|
||||||
),
|
),
|
||||||
manage_root_logger=True,
|
manage_root_logger=True,
|
||||||
)
|
)
|
||||||
log.setLevel(LOGLEVEL)
|
log.setLevel(loglevel.value)
|
||||||
log.info(
|
log.info(
|
||||||
f"Starting OCRmyPDF watcher with config:\n"
|
f"Starting OCRmyPDF watcher with config:\n"
|
||||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
f"Input Directory: {input_dir}\n"
|
||||||
f"Output Directory: {OUTPUT_DIRECTORY}\n"
|
f"Output Directory: {output_dir}\n"
|
||||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
f"Output Directory Year & Month: {output_dir_year_month}\n"
|
||||||
f"Archive Directory: {ARCHIVE_DIRECTORY}"
|
f"Archive Directory: {archive_dir}"
|
||||||
)
|
)
|
||||||
log.debug(
|
log.debug(
|
||||||
f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n"
|
f"INPUT_DIRECTORY: {input_dir}\n"
|
||||||
f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n"
|
f"OUTPUT_DIRECTORY: {output_dir}\n"
|
||||||
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
f"OUTPUT_DIRECTORY_YEAR_MONTH: {output_dir_year_month}\n"
|
||||||
f"ARCHIVE_DIRECTORY: {ARCHIVE_DIRECTORY}\n"
|
f"ARCHIVE_DIRECTORY: {archive_dir}\n"
|
||||||
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
f"ON_SUCCESS_DELETE: {on_success_delete}\n"
|
||||||
f"ON_SUCCESS_ARCHIVE: {ON_SUCCESS_ARCHIVE}\n"
|
f"ON_SUCCESS_ARCHIVE: {on_success_archive}\n"
|
||||||
f"DESKEW: {DESKEW}\n"
|
f"DESKEW: {deskew}\n"
|
||||||
f"ARGS: {OCR_JSON_SETTINGS}\n"
|
f"ARGS: {ocr_json_settings}\n"
|
||||||
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
f"POLL_NEW_FILE_SECONDS: {poll_new_file_seconds}\n"
|
||||||
f"RETRIES_LOADING_FILE: {RETRIES_LOADING_FILE}\n"
|
f"RETRIES_LOADING_FILE: {retries_loading_file}\n"
|
||||||
f"USE_POLLING: {USE_POLLING}\n"
|
f"USE_POLLING: {use_polling}\n"
|
||||||
f"LOGLEVEL: {LOGLEVEL}"
|
f"LOGLEVEL: {loglevel.value}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'input_file' in OCR_JSON_SETTINGS or 'output_file' in OCR_JSON_SETTINGS:
|
json_settings = json.loads(ocr_json_settings.read() if ocr_json_settings else '{}')
|
||||||
log.error('OCR_JSON_SETTINGS should not specify input file or output file')
|
|
||||||
|
if 'input_file' in json_settings or 'output_file' in json_settings:
|
||||||
|
log.error(
|
||||||
|
'OCR_JSON_SETTINGS (--ocr-json-settings) may not specify input/output file'
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
handler = HandleObserverEvent(patterns=PATTERNS)
|
handler = HandleObserverEvent(
|
||||||
if USE_POLLING:
|
patterns=patterns.split(','),
|
||||||
|
settings={
|
||||||
|
'archive_dir': archive_dir,
|
||||||
|
'output_dir': output_dir,
|
||||||
|
'deskew': deskew,
|
||||||
|
'ocrmypdf_kwargs': json_settings,
|
||||||
|
'on_success_delete': on_success_delete,
|
||||||
|
'on_success_archive': on_success_archive,
|
||||||
|
'poll_new_file_seconds': poll_new_file_seconds,
|
||||||
|
'retries_loading_file': retries_loading_file,
|
||||||
|
'output_dir_year_month': output_dir_year_month,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if use_polling:
|
||||||
observer = PollingObserver()
|
observer = PollingObserver()
|
||||||
else:
|
else:
|
||||||
observer = Observer()
|
observer = Observer()
|
||||||
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
observer.schedule(handler, input_dir, recursive=True)
|
||||||
observer.start()
|
observer.start()
|
||||||
|
typer.echo(f"Watching {input_dir} for new PDFs. Press Ctrl+C to exit.")
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(30)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
observer.stop()
|
observer.stop()
|
||||||
observer.join()
|
observer.join()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
app()
|
||||||
|
|||||||
+29
-41
@@ -1,11 +1,7 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = ["setuptools >= 61", "setuptools_scm[toml] >= 7.0.5", "wheel"]
|
||||||
"setuptools >= 61",
|
|
||||||
"setuptools_scm[toml] >= 7.0.5",
|
|
||||||
"wheel"
|
|
||||||
]
|
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
@@ -13,23 +9,21 @@ name = "ocrmypdf"
|
|||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
|
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MPL-2.0"}
|
license = { text = "MPL-2.0" }
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Pillow>=8.2.0",
|
"Pillow>=10.0.1",
|
||||||
"coloredlogs>=14.0",
|
|
||||||
"deprecation>=2.1.0",
|
"deprecation>=2.1.0",
|
||||||
"img2pdf>=0.3.0", # pure Python
|
"img2pdf>=0.4.4",
|
||||||
"packaging>=20",
|
"packaging>=20",
|
||||||
"pdfminer.six>=20201018",
|
"pdfminer.six>=20220319",
|
||||||
"pikepdf>=5.0.1",
|
"pikepdf>=8",
|
||||||
"pluggy>=0.13.0",
|
"pluggy>=0.13.0",
|
||||||
"reportlab>=3.5.66",
|
"reportlab>=3.6.8",
|
||||||
"tqdm>=4",
|
"rich>=13",
|
||||||
"importlib-resources>=5;python_version<'3.9'", # until Python 3.9
|
|
||||||
"typing-extensions>=4;python_version<'3.10'",
|
"typing-extensions>=4;python_version<'3.10'",
|
||||||
]
|
]
|
||||||
authors = [{name = "James R. Barlow", email="james@purplerock.ca"}]
|
authors = [{ name = "James R. Barlow", email = "james@purplerock.ca" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
@@ -47,13 +41,7 @@ classifiers = [
|
|||||||
"Topic :: Text Processing :: Indexing",
|
"Topic :: Text Processing :: Indexing",
|
||||||
"Topic :: Text Processing :: Linguistic",
|
"Topic :: Text Processing :: Linguistic",
|
||||||
]
|
]
|
||||||
keywords = [
|
keywords = ["PDF", "OCR", "optical character recognition", "PDF/A", "scanning"]
|
||||||
"PDF",
|
|
||||||
"OCR",
|
|
||||||
"optical character recognition",
|
|
||||||
"PDF/A",
|
|
||||||
"scanning",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Documentation = "https://ocrmypdf.readthedocs.io/"
|
Documentation = "https://ocrmypdf.readthedocs.io/"
|
||||||
@@ -64,17 +52,17 @@ Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues"
|
|||||||
docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"]
|
docs = ["sphinx", "sphinx-issues", "sphinx-rtd-theme"]
|
||||||
extended_test = ["PyMuPDF==1.19.1"]
|
extended_test = ["PyMuPDF==1.19.1"]
|
||||||
test = [
|
test = [
|
||||||
"coverage[toml]>=5",
|
"coverage[toml]>=6.2",
|
||||||
"hypothesis>=6.0.0",
|
"hypothesis>=6.36.0",
|
||||||
"pytest>=6.0.0",
|
"pytest>=6.2.5",
|
||||||
"pytest-cov>=2.11.1",
|
"pytest-cov>=3.0.0",
|
||||||
"pytest-xdist>=2.2.0",
|
"pytest-xdist>=2.5.0",
|
||||||
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
|
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
|
||||||
"types-Pillow",
|
"types-Pillow",
|
||||||
"types-humanfriendly",
|
"types-humanfriendly",
|
||||||
]
|
]
|
||||||
watcher = ["watchdog>=1.0.2"]
|
watcher = ["watchdog>=1.0.2", "typer[all]", "python-dotenv"]
|
||||||
webservice = ["Flask>=1"]
|
webservice = ["Flask>=2.0.1"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
ocrmypdf = "ocrmypdf.__main__:run"
|
ocrmypdf = "ocrmypdf.__main__:run"
|
||||||
@@ -89,11 +77,11 @@ namespaces = false
|
|||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
|
|
||||||
[tool.distutils.bdist_wheel]
|
[tool.distutils.bdist_wheel]
|
||||||
python-tag = "py38"
|
python-tag = "py39"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = ["py38", "py39", "py310", "py311"]
|
target-version = ["py39", "py310", "py311"]
|
||||||
skip-string-normalization = true
|
skip-string-normalization = true
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
exclude = '''
|
exclude = '''
|
||||||
@@ -134,7 +122,7 @@ exclude_lines = [
|
|||||||
"if 0:",
|
"if 0:",
|
||||||
"if False:",
|
"if False:",
|
||||||
"if __name__ == .__main__.:",
|
"if __name__ == .__main__.:",
|
||||||
"if TYPE_CHECKING:"
|
"if TYPE_CHECKING:",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
@@ -156,20 +144,20 @@ module = [
|
|||||||
'pdfminer.*',
|
'pdfminer.*',
|
||||||
'reportlab.*',
|
'reportlab.*',
|
||||||
'fitz',
|
'fitz',
|
||||||
'libxmp.utils'
|
'libxmp.utils',
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = [
|
select = [
|
||||||
"D", # pydocstyle
|
"D", # pydocstyle
|
||||||
"E", # pycodestyle
|
"E", # pycodestyle
|
||||||
"W", # pycodestyle
|
"W", # pycodestyle
|
||||||
"F", # pyflakes
|
"F", # pyflakes
|
||||||
"I001", # isort
|
"I001", # isort
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
]
|
]
|
||||||
target-version = "py38"
|
target-version = "py39"
|
||||||
|
|
||||||
[tool.ruff.isort]
|
[tool.ruff.isort]
|
||||||
known-first-party = ["ocrmypdf"]
|
known-first-party = ["ocrmypdf"]
|
||||||
|
|||||||
+5
-5
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
name: ocrmypdf
|
name: ocrmypdf
|
||||||
title: OCRmyPDF
|
title: OCRmyPDF
|
||||||
base: core20
|
base: core22
|
||||||
version: git
|
version: git
|
||||||
summary: OCRmyPDF adds optical character recognition (OCR) to PDFs
|
summary: OCRmyPDF adds a searchable text layer to scanned PDF files
|
||||||
description: OCRmyPDF packaged for snap
|
description: OCRmyPDF packaged for snap
|
||||||
grade: stable
|
grade: stable
|
||||||
confinement: strict
|
confinement: strict
|
||||||
@@ -18,8 +18,8 @@ architectures: [amd64]
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
TESSDATA_PREFIX: $SNAP/usr/share/tesseract-ocr/4.00/tessdata
|
TESSDATA_PREFIX: $SNAP/usr/share/tesseract-ocr/4.00/tessdata
|
||||||
GS_LIB: $SNAP/usr/share/ghostscript/9.50/Resource/Init
|
GS_LIB: $SNAP/usr/share/ghostscript/9.55/Resource/Init
|
||||||
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.50/Resource/Font
|
GS_FONTPATH: $SNAP/usr/share/ghostscript/9.55/Resource/Font
|
||||||
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
|
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
|
||||||
|
|
||||||
apps:
|
apps:
|
||||||
@@ -49,7 +49,7 @@ parts:
|
|||||||
jbig2enc:
|
jbig2enc:
|
||||||
plugin: autotools
|
plugin: autotools
|
||||||
source: https://github.com/agl/jbig2enc.git
|
source: https://github.com/agl/jbig2enc.git
|
||||||
source-tag: '0.29'
|
source-tag: "0.29"
|
||||||
build-packages:
|
build-packages:
|
||||||
- libleptonica-dev
|
- libleptonica-dev
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Callable, Iterable
|
from collections.abc import Iterable
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
def _task_noop(*_args, **_kwargs):
|
def _task_noop(*_args, **_kwargs):
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -21,31 +20,49 @@ from ocrmypdf.exceptions import SubprocessOutputError
|
|||||||
from ocrmypdf.helpers import Resolution
|
from ocrmypdf.helpers import Resolution
|
||||||
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
|
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
|
||||||
|
|
||||||
# Remove this workaround when we require Pillow >= 10
|
COLOR_CONVERSION_STRATEGIES = frozenset(
|
||||||
try:
|
[
|
||||||
Transpose = Image.Transpose # type: ignore
|
'CMYK',
|
||||||
except AttributeError:
|
'Gray',
|
||||||
# Pillow 9 shim
|
'LeaveColorUnchanged',
|
||||||
Transpose = Image # type: ignore
|
'RGB',
|
||||||
|
'UseDeviceIndependentColor',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Most reliable way to get the bitness of Python interpreter, according to Python docs
|
|
||||||
_IS_64BIT = sys.maxsize > 2**32
|
|
||||||
|
|
||||||
_GSWIN = None
|
class DuplicateFilter(logging.Filter):
|
||||||
if os.name == 'nt':
|
"""Filter out duplicate log messages."""
|
||||||
if _IS_64BIT:
|
|
||||||
_GSWIN = 'gswin64c'
|
|
||||||
else:
|
|
||||||
_GSWIN = 'gswin32c'
|
|
||||||
|
|
||||||
GS = _GSWIN if _GSWIN else 'gs'
|
def __init__(self, logger: logging.Logger):
|
||||||
del _GSWIN
|
self.last: logging.LogRecord | None = None
|
||||||
|
self.count = 0
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def filter(self, record):
|
||||||
|
if self.last and record.msg == self.last.msg:
|
||||||
|
self.count += 1
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if self.count >= 1:
|
||||||
|
rep_msg = f"(previous message repeated {self.count} times)"
|
||||||
|
self.count = 0 # Avoid infinite recursion
|
||||||
|
self.logger.log(self.last.levelno, rep_msg)
|
||||||
|
self.last = record
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def version():
|
log.addFilter(DuplicateFilter(log))
|
||||||
return get_version(GS)
|
|
||||||
|
|
||||||
|
# Ghostscript executable - gswin32c is not supported
|
||||||
|
GS = 'gswin64c' if os.name == 'nt' else 'gs'
|
||||||
|
|
||||||
|
|
||||||
|
def version() -> Version:
|
||||||
|
return Version(get_version(GS))
|
||||||
|
|
||||||
|
|
||||||
def _gs_error_reported(stream) -> bool:
|
def _gs_error_reported(stream) -> bool:
|
||||||
@@ -112,11 +129,11 @@ def rasterize_pdf(
|
|||||||
# rotation is a clockwise angle and Image.ROTATE_* is
|
# rotation is a clockwise angle and Image.ROTATE_* is
|
||||||
# counterclockwise so this cancels out the rotation
|
# counterclockwise so this cancels out the rotation
|
||||||
if rotation == 90:
|
if rotation == 90:
|
||||||
im = im.transpose(Transpose.ROTATE_90)
|
im = im.transpose(Image.Transpose.ROTATE_90)
|
||||||
elif rotation == 180:
|
elif rotation == 180:
|
||||||
im = im.transpose(Transpose.ROTATE_180)
|
im = im.transpose(Image.Transpose.ROTATE_180)
|
||||||
elif rotation == 270:
|
elif rotation == 270:
|
||||||
im = im.transpose(Transpose.ROTATE_270)
|
im = im.transpose(Image.Transpose.ROTATE_270)
|
||||||
if rotation % 180 == 90:
|
if rotation % 180 == 90:
|
||||||
page_dpi = page_dpi.flip_axis()
|
page_dpi = page_dpi.flip_axis()
|
||||||
im.save(fspath(output_file), dpi=page_dpi)
|
im.save(fspath(output_file), dpi=page_dpi)
|
||||||
@@ -160,6 +177,7 @@ def generate_pdfa(
|
|||||||
output_file: os.PathLike,
|
output_file: os.PathLike,
|
||||||
*,
|
*,
|
||||||
compression: str,
|
compression: str,
|
||||||
|
color_conversion_strategy: str,
|
||||||
pdf_version: str = '1.5',
|
pdf_version: str = '1.5',
|
||||||
pdfa_part: str = '2',
|
pdfa_part: str = '2',
|
||||||
progressbar_class=None,
|
progressbar_class=None,
|
||||||
@@ -189,8 +207,7 @@ def generate_pdfa(
|
|||||||
"-dAutoFilterGrayImages=true",
|
"-dAutoFilterGrayImages=true",
|
||||||
]
|
]
|
||||||
|
|
||||||
strategy = 'LeaveColorUnchanged'
|
gs_version = version()
|
||||||
gs_version = Version(version())
|
|
||||||
if gs_version == Version('9.56.0'):
|
if gs_version == Version('9.56.0'):
|
||||||
# 9.56.0 breaks our OCR, should be fixed in 9.56.1
|
# 9.56.0 breaks our OCR, should be fixed in 9.56.1
|
||||||
# https://bugs.ghostscript.com/show_bug.cgi?id=705187
|
# https://bugs.ghostscript.com/show_bug.cgi?id=705187
|
||||||
@@ -209,16 +226,16 @@ def generate_pdfa(
|
|||||||
"-dBATCH",
|
"-dBATCH",
|
||||||
"-dNOPAUSE",
|
"-dNOPAUSE",
|
||||||
"-dSAFER",
|
"-dSAFER",
|
||||||
"-dCompatibilityLevel=" + str(pdf_version),
|
f"-dCompatibilityLevel={str(pdf_version)}",
|
||||||
"-sDEVICE=pdfwrite",
|
"-sDEVICE=pdfwrite",
|
||||||
"-dAutoRotatePages=/None",
|
"-dAutoRotatePages=/None",
|
||||||
"-sColorConversionStrategy=" + strategy,
|
f"-sColorConversionStrategy={color_conversion_strategy}",
|
||||||
]
|
]
|
||||||
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
|
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
|
||||||
+ compression_args
|
+ compression_args
|
||||||
+ [
|
+ [
|
||||||
"-dJPEGQ=95",
|
"-dJPEGQ=95",
|
||||||
"-dPDFA=" + pdfa_part,
|
f"-dPDFA={pdfa_part}",
|
||||||
"-dPDFACompatibilityPolicy=1",
|
"-dPDFACompatibilityPolicy=1",
|
||||||
"-o",
|
"-o",
|
||||||
"-",
|
"-",
|
||||||
@@ -249,14 +266,9 @@ def generate_pdfa(
|
|||||||
# If there is an error we log the whole stderr, except for filtering
|
# If there is an error we log the whole stderr, except for filtering
|
||||||
# duplicates.
|
# duplicates.
|
||||||
if _gs_error_reported(stderr):
|
if _gs_error_reported(stderr):
|
||||||
last_part = None
|
# Ghostscript outputs the pattern **** Error: .... frequently.
|
||||||
repcount = 0
|
# Occasionally the error message is spammed many times. We filter
|
||||||
|
# out duplicates of this message using the filter above. We use
|
||||||
|
# the **** pattern to split the stderr into parts.
|
||||||
for part in stderr.split('****'):
|
for part in stderr.split('****'):
|
||||||
if part != last_part:
|
log.error(part)
|
||||||
if repcount > 1:
|
|
||||||
log.error(f"(previous error message repeated {repcount} times)")
|
|
||||||
repcount = 0
|
|
||||||
log.error(part)
|
|
||||||
else:
|
|
||||||
repcount += 1
|
|
||||||
last_part = part
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from subprocess import PIPE
|
from subprocess import PIPE
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
from ocrmypdf.exceptions import MissingDependencyError
|
from ocrmypdf.exceptions import MissingDependencyError
|
||||||
from ocrmypdf.subprocess import get_version, run
|
from ocrmypdf.subprocess import get_version, run
|
||||||
|
|
||||||
|
|
||||||
def version():
|
def version() -> Version:
|
||||||
return get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*')
|
return Version(get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*'))
|
||||||
|
|
||||||
|
|
||||||
def available():
|
def available():
|
||||||
@@ -23,15 +25,17 @@ def available():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def convert_group(*, cwd, infiles, out_prefix):
|
def convert_group(*, cwd, infiles, out_prefix, threshold):
|
||||||
args = [
|
args = [
|
||||||
'jbig2',
|
'jbig2',
|
||||||
'-b',
|
'-b',
|
||||||
out_prefix,
|
out_prefix,
|
||||||
'-s', # symbol mode (lossy)
|
'--symbol-mode', # symbol mode (lossy)
|
||||||
|
'-t',
|
||||||
|
str(threshold), # threshold
|
||||||
# '-r', # refinement mode (lossless symbol mode, currently disabled in
|
# '-r', # refinement mode (lossless symbol mode, currently disabled in
|
||||||
# jbig2)
|
# jbig2)
|
||||||
'-p',
|
'--pdf',
|
||||||
]
|
]
|
||||||
args.extend(infiles)
|
args.extend(infiles)
|
||||||
proc = run(args, cwd=cwd, stdout=PIPE, stderr=PIPE)
|
proc = run(args, cwd=cwd, stdout=PIPE, stderr=PIPE)
|
||||||
@@ -40,11 +44,13 @@ def convert_group(*, cwd, infiles, out_prefix):
|
|||||||
|
|
||||||
|
|
||||||
def convert_group_mp(args):
|
def convert_group_mp(args):
|
||||||
return convert_group(cwd=args[0], infiles=args[1], out_prefix=args[2])
|
return convert_group(
|
||||||
|
cwd=args[0], infiles=args[1], out_prefix=args[2], threshold=args[3]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_single(*, cwd, infile, outfile):
|
def convert_single(*, cwd, infile, outfile, threshold):
|
||||||
args = ['jbig2', '-p', infile]
|
args = ['jbig2', '--pdf', '-t', str(threshold), infile]
|
||||||
with open(outfile, 'wb') as fstdout:
|
with open(outfile, 'wb') as fstdout:
|
||||||
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
|
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
|
||||||
proc.check_returncode()
|
proc.check_returncode()
|
||||||
@@ -52,4 +58,6 @@ def convert_single(*, cwd, infile, outfile):
|
|||||||
|
|
||||||
|
|
||||||
def convert_single_mp(args):
|
def convert_single_mp(args):
|
||||||
return convert_single(cwd=args[0], infile=args[1], outfile=args[2])
|
return convert_single(
|
||||||
|
cwd=args[0], infile=args[1], outfile=args[2], threshold=args[3]
|
||||||
|
)
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ from io import BytesIO
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE
|
from subprocess import PIPE
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ocrmypdf.exceptions import MissingDependencyError
|
from ocrmypdf.exceptions import MissingDependencyError
|
||||||
from ocrmypdf.subprocess import get_version, run
|
from ocrmypdf.subprocess import get_version, run
|
||||||
|
|
||||||
|
|
||||||
def version():
|
def version() -> Version:
|
||||||
return get_version('pngquant', regex=r'(\d+(\.\d+)*).*')
|
return Version(get_version('pngquant', regex=r'(\d+(\.\d+)*).*'))
|
||||||
|
|
||||||
|
|
||||||
def available():
|
def available():
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from contextlib import suppress
|
||||||
from math import pi
|
from math import pi
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -113,13 +114,13 @@ class TesseractVersion(Version):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def version() -> str:
|
def version() -> Version:
|
||||||
return get_version('tesseract', regex=r'tesseract\s(.+)')
|
return TesseractVersion(get_version('tesseract', regex=r'tesseract\s(.+)'))
|
||||||
|
|
||||||
|
|
||||||
def has_thresholding() -> bool:
|
def has_thresholding() -> bool:
|
||||||
"""Does Tesseract have -c thresholding method capability?"""
|
"""Does Tesseract have -c thresholding method capability?"""
|
||||||
return version() >= '5.0'
|
return version() >= Version('5.0')
|
||||||
|
|
||||||
|
|
||||||
def get_languages() -> set[str]:
|
def get_languages() -> set[str]:
|
||||||
@@ -350,7 +351,7 @@ def generate_hocr(
|
|||||||
tesseract_log_output(stdout)
|
tesseract_log_output(stdout)
|
||||||
# The sidecar text file will get the suffix .txt; rename it to
|
# The sidecar text file will get the suffix .txt; rename it to
|
||||||
# whatever caller wants it named
|
# whatever caller wants it named
|
||||||
if prefix.with_suffix('.txt').exists():
|
with suppress(FileNotFoundError):
|
||||||
prefix.with_suffix('.txt').replace(output_text)
|
prefix.with_suffix('.txt').replace(output_text)
|
||||||
|
|
||||||
|
|
||||||
@@ -406,7 +407,7 @@ def generate_pdf(
|
|||||||
try:
|
try:
|
||||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||||
stdout = p.stdout
|
stdout = p.stdout
|
||||||
if prefix.with_suffix('.txt').exists():
|
with suppress(FileNotFoundError):
|
||||||
prefix.with_suffix('.txt').replace(output_text)
|
prefix.with_suffix('.txt').replace(output_text)
|
||||||
except TimeoutExpired:
|
except TimeoutExpired:
|
||||||
page_timedout(timeout)
|
page_timedout(timeout)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, STDOUT
|
from subprocess import PIPE, STDOUT
|
||||||
from typing import Iterator, Union
|
from typing import Union
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
|
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
|
||||||
@@ -67,8 +69,8 @@ class UnpaperImageTooLargeError(Exception):
|
|||||||
super().__init__(self.message)
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
def version() -> str:
|
def version() -> Version:
|
||||||
return get_version('unpaper')
|
return Version(get_version('unpaper'))
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_MODES = {'1', 'L', 'RGB'}
|
SUPPORTED_MODES = {'1', 'L', 'RGB'}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from collections.abc import Iterator
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator
|
|
||||||
|
|
||||||
from pluggy import PluginManager
|
from pluggy import PluginManager
|
||||||
|
|
||||||
|
|||||||
+65
-16
@@ -6,9 +6,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
from rich.console import Console
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
from rich.progress import (
|
||||||
|
BarColumn,
|
||||||
|
MofNCompleteColumn,
|
||||||
|
Progress,
|
||||||
|
TaskProgressColumn,
|
||||||
|
TextColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
)
|
||||||
|
from rich.table import Column
|
||||||
|
|
||||||
|
|
||||||
class PageNumberFilter(logging.Filter):
|
class PageNumberFilter(logging.Filter):
|
||||||
@@ -23,21 +32,61 @@ class PageNumberFilter(logging.Filter):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TqdmConsole:
|
class RichLoggingHandler(RichHandler):
|
||||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar.
|
def __init__(self, console: Console, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
console=console, show_level=False, show_time=False, markup=True, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
This routes log messages through tqdm so that it can print them above the
|
|
||||||
progress bar, and then refresh the progress bar, rather than overwriting
|
|
||||||
it which looks messy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, file):
|
class RichTqdmProgressAdapter:
|
||||||
self.file = file
|
"""Adapt tqdm API to rich progress bar."""
|
||||||
|
|
||||||
def write(self, msg):
|
def __init__(
|
||||||
# When no progress bar is active, tqdm.write() routes to print()
|
self,
|
||||||
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
*,
|
||||||
|
console: Console,
|
||||||
|
desc: str,
|
||||||
|
total: float | None = None,
|
||||||
|
unit: str | None = None,
|
||||||
|
unit_scale: float | None = 1.0,
|
||||||
|
disable: bool = False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.progress = Progress(
|
||||||
|
TextColumn(
|
||||||
|
"[progress.description]{task.description}",
|
||||||
|
table_column=Column(min_width=20),
|
||||||
|
),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
MofNCompleteColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
console=console,
|
||||||
|
auto_refresh=True,
|
||||||
|
redirect_stderr=True,
|
||||||
|
redirect_stdout=False,
|
||||||
|
disable=disable,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
self.unit_scale = unit_scale
|
||||||
|
self.progress_bar = self.progress.add_task(
|
||||||
|
desc,
|
||||||
|
total=total * self.unit_scale
|
||||||
|
if total is not None and self.unit_scale is not None
|
||||||
|
else None,
|
||||||
|
unit=unit,
|
||||||
|
)
|
||||||
|
|
||||||
def flush(self):
|
def __enter__(self):
|
||||||
with suppress(AttributeError):
|
self.progress.start()
|
||||||
self.file.flush()
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.progress.refresh()
|
||||||
|
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)
|
||||||
|
|||||||
+243
-116
@@ -10,11 +10,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj, copystat
|
||||||
from typing import Any, BinaryIO, Iterable, Sequence, cast
|
from typing import Any, BinaryIO, TypeVar, cast
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pikepdf
|
import pikepdf
|
||||||
@@ -27,10 +28,12 @@ from ocrmypdf._jobcontext import PageContext, PdfContext
|
|||||||
from ocrmypdf._version import PROGRAM_NAME
|
from ocrmypdf._version import PROGRAM_NAME
|
||||||
from ocrmypdf._version import __version__ as VERSION
|
from ocrmypdf._version import __version__ as VERSION
|
||||||
from ocrmypdf.exceptions import (
|
from ocrmypdf.exceptions import (
|
||||||
|
DigitalSignatureError,
|
||||||
DpiError,
|
DpiError,
|
||||||
EncryptedPdfError,
|
EncryptedPdfError,
|
||||||
InputFileError,
|
InputFileError,
|
||||||
PriorOcrFoundError,
|
PriorOcrFoundError,
|
||||||
|
TaggedPDFError,
|
||||||
UnsupportedImageFormatError,
|
UnsupportedImageFormatError,
|
||||||
)
|
)
|
||||||
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
|
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
|
||||||
@@ -39,13 +42,7 @@ from ocrmypdf.pdfa import generate_pdfa_ps
|
|||||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
|
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
|
||||||
from ocrmypdf.pluginspec import OrientationConfidence
|
from ocrmypdf.pluginspec import OrientationConfidence
|
||||||
|
|
||||||
# Remove this workaround when we require Pillow >= 10
|
T = TypeVar("T")
|
||||||
try:
|
|
||||||
BICUBIC = Image.Resampling.BICUBIC # type: ignore
|
|
||||||
except AttributeError: # pragma: no cover
|
|
||||||
# Pillow 9 shim
|
|
||||||
BICUBIC = Image.BICUBIC # type: ignore
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
VECTOR_PAGE_DPI = 400
|
VECTOR_PAGE_DPI = 400
|
||||||
@@ -80,34 +77,32 @@ def triage_image_file(input_file: Path, output_file: Path, options) -> None:
|
|||||||
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
|
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
|
||||||
log.info("Image size: (%d, %d)", *im.size)
|
log.info("Image size: (%d, %d)", *im.size)
|
||||||
log.info("Image resolution: (%d, %d)", *im.info['dpi'])
|
log.info("Image resolution: (%d, %d)", *im.info['dpi'])
|
||||||
log.error(
|
raise DpiError(
|
||||||
"Input file is an image, but the resolution (DPI) is "
|
"Input file is an image, but the resolution (DPI) is "
|
||||||
"not credible. Estimate the resolution at which the "
|
"not credible. Estimate the resolution at which the "
|
||||||
"image was scanned and specify it using --image-dpi."
|
"image was scanned and specify it using --image-dpi."
|
||||||
)
|
)
|
||||||
raise DpiError()
|
|
||||||
elif not options.image_dpi:
|
elif not options.image_dpi:
|
||||||
log.info("Image size: (%d, %d)", *im.size)
|
log.info("Image size: (%d, %d)", *im.size)
|
||||||
log.error(
|
raise DpiError(
|
||||||
"Input file is an image, but has no resolution (DPI) "
|
"Input file is an image, but has no resolution (DPI) "
|
||||||
"in its metadata. Estimate the resolution at which "
|
"in its metadata. Estimate the resolution at which "
|
||||||
"image was scanned and specify it using --image-dpi."
|
"image was scanned and specify it using --image-dpi."
|
||||||
)
|
)
|
||||||
raise DpiError()
|
|
||||||
|
|
||||||
if im.mode in ('RGBA', 'LA'):
|
if im.mode in ('RGBA', 'LA'):
|
||||||
log.error(
|
raise UnsupportedImageFormatError(
|
||||||
"The input image has an alpha channel. Remove the alpha "
|
"The input image has an alpha channel. Remove the alpha "
|
||||||
"channel first."
|
"channel first."
|
||||||
)
|
)
|
||||||
raise UnsupportedImageFormatError()
|
|
||||||
|
|
||||||
if 'iccprofile' not in im.info:
|
if 'iccprofile' not in im.info:
|
||||||
if im.mode == 'RGB':
|
if im.mode == 'RGB':
|
||||||
log.info("Input image has no ICC profile, assuming sRGB")
|
log.info("Input image has no ICC profile, assuming sRGB")
|
||||||
elif im.mode == 'CMYK':
|
elif im.mode == 'CMYK':
|
||||||
log.error("Input CMYK image has no ICC profile, not usable")
|
raise UnsupportedImageFormatError(
|
||||||
raise UnsupportedImageFormatError()
|
"Input CMYK image has no ICC profile, not usable"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log.info("Image seems valid. Try converting to PDF...")
|
log.info("Image seems valid. Try converting to PDF...")
|
||||||
@@ -125,7 +120,6 @@ def triage_image_file(input_file: Path, output_file: Path, options) -> None:
|
|||||||
)
|
)
|
||||||
log.info("Successfully converted to PDF, processing...")
|
log.info("Successfully converted to PDF, processing...")
|
||||||
except img2pdf.ImageOpenError as e:
|
except img2pdf.ImageOpenError as e:
|
||||||
log.error(e)
|
|
||||||
raise UnsupportedImageFormatError() from e
|
raise UnsupportedImageFormatError() from e
|
||||||
|
|
||||||
|
|
||||||
@@ -147,6 +141,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
|
|||||||
def triage(
|
def triage(
|
||||||
original_filename: str, input_file: Path, output_file: Path, options
|
original_filename: str, input_file: Path, output_file: Path, options
|
||||||
) -> Path:
|
) -> Path:
|
||||||
|
"""Triage the input file. We can handle PDFs and images."""
|
||||||
try:
|
try:
|
||||||
if _pdf_guess_version(input_file):
|
if _pdf_guess_version(input_file):
|
||||||
if options.image_dpi:
|
if options.image_dpi:
|
||||||
@@ -175,6 +170,7 @@ def get_pdfinfo(
|
|||||||
max_workers: int | None = None,
|
max_workers: int | None = None,
|
||||||
check_pages=None,
|
check_pages=None,
|
||||||
) -> PdfInfo:
|
) -> PdfInfo:
|
||||||
|
"""Get the PDF info."""
|
||||||
try:
|
try:
|
||||||
return PdfInfo(
|
return PdfInfo(
|
||||||
input_file,
|
input_file,
|
||||||
@@ -191,22 +187,26 @@ def get_pdfinfo(
|
|||||||
|
|
||||||
|
|
||||||
def validate_pdfinfo_options(context: PdfContext) -> None:
|
def validate_pdfinfo_options(context: PdfContext) -> None:
|
||||||
|
"""Validate the PDF info options."""
|
||||||
pdfinfo = context.pdfinfo
|
pdfinfo = context.pdfinfo
|
||||||
options = context.options
|
options = context.options
|
||||||
|
|
||||||
if pdfinfo.needs_rendering:
|
if pdfinfo.needs_rendering:
|
||||||
log.error(
|
raise InputFileError(
|
||||||
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
|
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
|
||||||
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
|
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
|
||||||
)
|
)
|
||||||
raise InputFileError()
|
if pdfinfo.has_signature:
|
||||||
|
if options.invalidate_digital_signatures:
|
||||||
|
log.warning("All digital signatures will be invalidated")
|
||||||
|
else:
|
||||||
|
raise DigitalSignatureError()
|
||||||
if pdfinfo.has_acroform:
|
if pdfinfo.has_acroform:
|
||||||
if options.redo_ocr:
|
if options.redo_ocr:
|
||||||
log.error(
|
raise InputFileError(
|
||||||
"This PDF has a user fillable form. --redo-ocr is not "
|
"This PDF has a user fillable form. --redo-ocr is not "
|
||||||
"currently possible on such files."
|
"currently possible on such files."
|
||||||
)
|
)
|
||||||
raise InputFileError()
|
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
"This PDF has a fillable form. "
|
"This PDF has a fillable form. "
|
||||||
@@ -219,6 +219,16 @@ def validate_pdfinfo_options(context: PdfContext) -> None:
|
|||||||
"form and all filled form fields. The output PDF will be "
|
"form and all filled form fields. The output PDF will be "
|
||||||
"'flattened' and will no longer be fillable."
|
"'flattened' and will no longer be fillable."
|
||||||
)
|
)
|
||||||
|
if pdfinfo.is_tagged:
|
||||||
|
if options.force_ocr or options.skip_text or options.redo_ocr:
|
||||||
|
log.warning(
|
||||||
|
"This PDF is marked as a Tagged PDF. This often indicates "
|
||||||
|
"that the PDF was generated from an office document and does "
|
||||||
|
"not need OCR. PDF pages processed by OCRmyPDF may not be "
|
||||||
|
"tagged correctly."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise TaggedPDFError()
|
||||||
context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options)
|
context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options)
|
||||||
|
|
||||||
|
|
||||||
@@ -227,28 +237,19 @@ def _vector_page_dpi(pageinfo: PageInfo) -> int:
|
|||||||
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0
|
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0
|
||||||
|
|
||||||
|
|
||||||
def get_page_dpi(pageinfo: PageInfo, options) -> Resolution:
|
def get_page_square_dpi(
|
||||||
"""Get the DPI when nonsquare DPI is tolerable."""
|
page_context: PageContext, image_dpi: Resolution | None = None
|
||||||
xres = max(
|
) -> Resolution:
|
||||||
pageinfo.dpi.x or VECTOR_PAGE_DPI,
|
|
||||||
options.oversample or 0.0,
|
|
||||||
_vector_page_dpi(pageinfo),
|
|
||||||
)
|
|
||||||
yres = max(
|
|
||||||
pageinfo.dpi.y or VECTOR_PAGE_DPI,
|
|
||||||
options.oversample or 0,
|
|
||||||
_vector_page_dpi(pageinfo),
|
|
||||||
)
|
|
||||||
return Resolution(float(xres), float(yres))
|
|
||||||
|
|
||||||
|
|
||||||
def get_page_square_dpi(pageinfo: PageInfo, options) -> Resolution:
|
|
||||||
"""Get the DPI when we require xres == yres, scaled to physical units.
|
"""Get the DPI when we require xres == yres, scaled to physical units.
|
||||||
|
|
||||||
Page DPI includes UserUnit scaling.
|
Page DPI includes UserUnit scaling.
|
||||||
"""
|
"""
|
||||||
xres = pageinfo.dpi.x or 0.0
|
pageinfo = page_context.pageinfo
|
||||||
yres = pageinfo.dpi.y or 0.0
|
options = page_context.options
|
||||||
|
if not image_dpi:
|
||||||
|
image_dpi = pageinfo.dpi
|
||||||
|
xres = image_dpi.x or 0.0
|
||||||
|
yres = image_dpi.y or 0.0
|
||||||
userunit = float(pageinfo.userunit) or 1.0
|
userunit = float(pageinfo.userunit) or 1.0
|
||||||
units = float(
|
units = float(
|
||||||
max(
|
max(
|
||||||
@@ -261,17 +262,23 @@ def get_page_square_dpi(pageinfo: PageInfo, options) -> Resolution:
|
|||||||
return Resolution(units, units)
|
return Resolution(units, units)
|
||||||
|
|
||||||
|
|
||||||
def get_canvas_square_dpi(pageinfo: PageInfo, options) -> Resolution:
|
def get_canvas_square_dpi(
|
||||||
|
page_context: PageContext, image_dpi: Resolution | None = None
|
||||||
|
) -> Resolution:
|
||||||
"""Get the DPI when we require xres == yres, in Postscript units.
|
"""Get the DPI when we require xres == yres, in Postscript units.
|
||||||
|
|
||||||
Canvas DPI is independent of PDF UserUnit scaling, which is
|
Canvas DPI is independent of PDF UserUnit scaling, which is
|
||||||
used to describe situations where the PDF user space is not 1:1 with
|
used to describe situations where the PDF user space is not 1:1 with
|
||||||
the physical units of the page.
|
the physical units of the page.
|
||||||
"""
|
"""
|
||||||
|
pageinfo = page_context.pageinfo
|
||||||
|
options = page_context.options
|
||||||
|
if not image_dpi:
|
||||||
|
image_dpi = pageinfo.dpi
|
||||||
units = float(
|
units = float(
|
||||||
max(
|
max(
|
||||||
(pageinfo.dpi.x) or VECTOR_PAGE_DPI,
|
image_dpi.x or VECTOR_PAGE_DPI,
|
||||||
(pageinfo.dpi.y) or VECTOR_PAGE_DPI,
|
image_dpi.y or VECTOR_PAGE_DPI,
|
||||||
_vector_page_dpi(pageinfo),
|
_vector_page_dpi(pageinfo),
|
||||||
options.oversample or 0.0,
|
options.oversample or 0.0,
|
||||||
)
|
)
|
||||||
@@ -358,8 +365,10 @@ def is_ocr_required(page_context: PageContext) -> bool:
|
|||||||
def rasterize_preview(input_file: Path, page_context: PageContext) -> Path:
|
def rasterize_preview(input_file: Path, page_context: PageContext) -> Path:
|
||||||
"""Generate a lower quality preview image."""
|
"""Generate a lower quality preview image."""
|
||||||
output_file = page_context.get_path('rasterize_preview.jpg')
|
output_file = page_context.get_path('rasterize_preview.jpg')
|
||||||
canvas_dpi = get_canvas_square_dpi(page_context.pageinfo, page_context.options)
|
canvas_dpi = Resolution(300.0, 300.0).take_min(
|
||||||
page_dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
[get_canvas_square_dpi(page_context)]
|
||||||
|
)
|
||||||
|
page_dpi = Resolution(300.0, 300.0).take_min([get_page_square_dpi(page_context)])
|
||||||
page_context.plugin_manager.hook.rasterize_pdf_page(
|
page_context.plugin_manager.hook.rasterize_pdf_page(
|
||||||
input_file=input_file,
|
input_file=input_file,
|
||||||
output_file=output_file,
|
output_file=output_file,
|
||||||
@@ -433,6 +442,38 @@ def get_orientation_correction(preview: Path, page_context: PageContext) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_image_dpi(page_context: PageContext) -> Resolution:
|
||||||
|
"""Calculate the DPI for the page image."""
|
||||||
|
pageinfo = page_context.pageinfo
|
||||||
|
dpi_profile = pageinfo.page_dpi_profile()
|
||||||
|
if dpi_profile and dpi_profile.average_to_max_dpi_ratio < 0.8:
|
||||||
|
image_dpi = Resolution(dpi_profile.weighted_dpi, dpi_profile.weighted_dpi)
|
||||||
|
else:
|
||||||
|
image_dpi = pageinfo.dpi
|
||||||
|
return image_dpi
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_raster_dpi(page_context: PageContext):
|
||||||
|
"""Calculate the DPI for rasterization."""
|
||||||
|
# Produce the page image with square resolution or else deskew and OCR
|
||||||
|
# will not work properly.
|
||||||
|
image_dpi = calculate_image_dpi(page_context)
|
||||||
|
dpi_profile = page_context.pageinfo.page_dpi_profile()
|
||||||
|
canvas_dpi = get_canvas_square_dpi(page_context, image_dpi)
|
||||||
|
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. "
|
||||||
|
"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.",
|
||||||
|
dpi_profile.weighted_dpi,
|
||||||
|
dpi_profile.max_dpi,
|
||||||
|
canvas_dpi.to_scalar(),
|
||||||
|
)
|
||||||
|
return canvas_dpi, page_dpi
|
||||||
|
|
||||||
|
|
||||||
def rasterize(
|
def rasterize(
|
||||||
input_file: Path,
|
input_file: Path,
|
||||||
page_context: PageContext,
|
page_context: PageContext,
|
||||||
@@ -484,10 +525,7 @@ def rasterize(
|
|||||||
|
|
||||||
log.debug(f"Rasterize with {device}, rotation {correction}")
|
log.debug(f"Rasterize with {device}, rotation {correction}")
|
||||||
|
|
||||||
# Produce the page image with square resolution or else deskew and OCR
|
canvas_dpi, page_dpi = calculate_raster_dpi(page_context)
|
||||||
# will not work properly.
|
|
||||||
canvas_dpi = get_canvas_square_dpi(pageinfo, page_context.options)
|
|
||||||
page_dpi = get_page_square_dpi(pageinfo, page_context.options)
|
|
||||||
|
|
||||||
page_context.plugin_manager.hook.rasterize_pdf_page(
|
page_context.plugin_manager.hook.rasterize_pdf_page(
|
||||||
input_file=input_file,
|
input_file=input_file,
|
||||||
@@ -504,6 +542,7 @@ def rasterize(
|
|||||||
|
|
||||||
|
|
||||||
def preprocess_remove_background(input_file: Path, page_context: PageContext) -> Path:
|
def preprocess_remove_background(input_file: Path, page_context: PageContext) -> Path:
|
||||||
|
"""Remove the background from the input image (temporarily disabled)."""
|
||||||
if any(image.bpc > 1 for image in page_context.pageinfo.images):
|
if any(image.bpc > 1 for image in page_context.pageinfo.images):
|
||||||
raise NotImplementedError("--remove-background is temporarily not implemented")
|
raise NotImplementedError("--remove-background is temporarily not implemented")
|
||||||
# output_file = page_context.get_path('pp_rm_bg.png')
|
# output_file = page_context.get_path('pp_rm_bg.png')
|
||||||
@@ -524,7 +563,7 @@ def preprocess_deskew(input_file: Path, page_context: PageContext) -> Path:
|
|||||||
Path: The path to the deskewed image file.
|
Path: The path to the deskewed image file.
|
||||||
"""
|
"""
|
||||||
output_file = page_context.get_path('pp_deskew.png')
|
output_file = page_context.get_path('pp_deskew.png')
|
||||||
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
|
||||||
|
|
||||||
ocr_engine = page_context.plugin_manager.hook.get_ocr_engine()
|
ocr_engine = page_context.plugin_manager.hook.get_ocr_engine()
|
||||||
deskew_angle_degrees = ocr_engine.get_deskew(input_file, page_context.options)
|
deskew_angle_degrees = ocr_engine.get_deskew(input_file, page_context.options)
|
||||||
@@ -534,7 +573,7 @@ def preprocess_deskew(input_file: Path, page_context: PageContext) -> Path:
|
|||||||
# resampling if image is mode '1' or 'P'
|
# resampling if image is mode '1' or 'P'
|
||||||
deskewed = im.rotate(
|
deskewed = im.rotate(
|
||||||
deskew_angle_degrees,
|
deskew_angle_degrees,
|
||||||
resample=BICUBIC,
|
resample=Image.Resampling.BICUBIC,
|
||||||
fillcolor=ImageColor.getcolor('white', mode=im.mode), # type: ignore
|
fillcolor=ImageColor.getcolor('white', mode=im.mode), # type: ignore
|
||||||
)
|
)
|
||||||
deskewed.save(output_file, dpi=dpi)
|
deskewed.save(output_file, dpi=dpi)
|
||||||
@@ -543,12 +582,13 @@ def preprocess_deskew(input_file: Path, page_context: PageContext) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def preprocess_clean(input_file: Path, page_context: PageContext) -> Path:
|
def preprocess_clean(input_file: Path, page_context: PageContext) -> Path:
|
||||||
|
"""Clean the input image using unpaper."""
|
||||||
output_file = page_context.get_path('pp_clean.png')
|
output_file = page_context.get_path('pp_clean.png')
|
||||||
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
|
||||||
return unpaper.clean(
|
return unpaper.clean(
|
||||||
input_file,
|
input_file,
|
||||||
output_file,
|
output_file,
|
||||||
dpi=dpi.x,
|
dpi=dpi.to_scalar(),
|
||||||
unpaper_args=page_context.options.unpaper_args,
|
unpaper_args=page_context.options.unpaper_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -603,6 +643,7 @@ def create_ocr_image(image: Path, page_context: PageContext) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def ocr_engine_hocr(input_file: Path, page_context: PageContext) -> tuple[Path, Path]:
|
def ocr_engine_hocr(input_file: Path, page_context: PageContext) -> tuple[Path, Path]:
|
||||||
|
"""Run the OCR engine and generate hOCR output."""
|
||||||
hocr_out = page_context.get_path('ocr_hocr.hocr')
|
hocr_out = page_context.get_path('ocr_hocr.hocr')
|
||||||
hocr_text_out = page_context.get_path('ocr_hocr.txt')
|
hocr_text_out = page_context.get_path('ocr_hocr.txt')
|
||||||
options = page_context.options
|
options = page_context.options
|
||||||
@@ -634,6 +675,10 @@ def should_visible_page_image_use_jpg(pageinfo: PageInfo) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
|
def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
|
||||||
|
"""Create a visible page image in JPEG format.
|
||||||
|
|
||||||
|
This is intended to be used when all images on the page were originally JPEGs.
|
||||||
|
"""
|
||||||
output_file = page_context.get_path('visible.jpg')
|
output_file = page_context.get_path('visible.jpg')
|
||||||
with Image.open(image) as im:
|
with Image.open(image) as im:
|
||||||
# At this point the image should be a .png, but deskew, unpaper
|
# At this point the image should be a .png, but deskew, unpaper
|
||||||
@@ -645,7 +690,7 @@ def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
|
|||||||
dpi = Resolution(*im.info['dpi'])
|
dpi = Resolution(*im.info['dpi'])
|
||||||
else:
|
else:
|
||||||
# Fallback to page-implied DPI
|
# Fallback to page-implied DPI
|
||||||
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
|
||||||
|
|
||||||
# Pillow requires integer DPI
|
# Pillow requires integer DPI
|
||||||
im.save(output_file, format='JPEG', dpi=dpi.to_int())
|
im.save(output_file, format='JPEG', dpi=dpi.to_int())
|
||||||
@@ -655,6 +700,7 @@ def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
|
|||||||
def create_pdf_page_from_image(
|
def create_pdf_page_from_image(
|
||||||
image: Path, page_context: PageContext, orientation_correction: int
|
image: Path, page_context: PageContext, orientation_correction: int
|
||||||
) -> Path:
|
) -> Path:
|
||||||
|
"""Create a PDF page from a page image."""
|
||||||
# We rasterize a square DPI version of each page because most image
|
# We rasterize a square DPI version of each page because most image
|
||||||
# processing tools don't support rectangular DPI. Use the square DPI as it
|
# processing tools don't support rectangular DPI. Use the square DPI as it
|
||||||
# accurately describes the image. It would be possible to resample the image
|
# accurately describes the image. It would be possible to resample the image
|
||||||
@@ -686,9 +732,10 @@ def create_pdf_page_from_image(
|
|||||||
|
|
||||||
|
|
||||||
def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
|
def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
|
||||||
|
"""Render the hOCR page to a PDF."""
|
||||||
options = page_context.options
|
options = page_context.options
|
||||||
output_file = page_context.get_path('ocr_hocr.pdf')
|
output_file = page_context.get_path('ocr_hocr.pdf')
|
||||||
dpi = get_page_square_dpi(page_context.pageinfo, options)
|
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
|
||||||
debug_mode = options.pdf_renderer == 'hocrdebug'
|
debug_mode = options.pdf_renderer == 'hocrdebug'
|
||||||
|
|
||||||
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.x) # square
|
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.x) # square
|
||||||
@@ -705,6 +752,7 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
|
|||||||
def ocr_engine_textonly_pdf(
|
def ocr_engine_textonly_pdf(
|
||||||
input_image: Path, page_context: PageContext
|
input_image: Path, page_context: PageContext
|
||||||
) -> tuple[Path, Path]:
|
) -> tuple[Path, Path]:
|
||||||
|
"""Run the OCR engine and generate a text-only PDF (will look blank)."""
|
||||||
output_pdf = page_context.get_path('ocr_tess.pdf')
|
output_pdf = page_context.get_path('ocr_tess.pdf')
|
||||||
output_text = page_context.get_path('ocr_tess.txt')
|
output_text = page_context.get_path('ocr_tess.txt')
|
||||||
options = page_context.options
|
options = page_context.options
|
||||||
@@ -720,6 +768,7 @@ def ocr_engine_textonly_pdf(
|
|||||||
|
|
||||||
|
|
||||||
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
|
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
|
||||||
|
"""Read the document info and store it in a dictionary."""
|
||||||
options = context.options
|
options = context.options
|
||||||
|
|
||||||
def from_document_info(key):
|
def from_document_info(key):
|
||||||
@@ -751,12 +800,28 @@ def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def generate_postscript_stub(context: PdfContext) -> Path:
|
def generate_postscript_stub(context: PdfContext) -> Path:
|
||||||
|
"""Generates a PostScript file stub for the given PDF context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: The PDF context to generate the PostScript file stub for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: The path to the generated PostScript file stub.
|
||||||
|
"""
|
||||||
output_file = context.get_path('pdfa.ps')
|
output_file = context.get_path('pdfa.ps')
|
||||||
generate_pdfa_ps(output_file)
|
generate_pdfa_ps(output_file)
|
||||||
return output_file
|
return output_file
|
||||||
|
|
||||||
|
|
||||||
def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -> Path:
|
def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -> Path:
|
||||||
|
"""Converts the given PDF to PDF/A.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_pdf: The input PDF file path (presumably not PDF/A).
|
||||||
|
input_ps_stub: The input PostScript file path, containing instructions
|
||||||
|
for the PDF/A generator to use.
|
||||||
|
context: The PDF context.
|
||||||
|
"""
|
||||||
options = context.options
|
options = context.options
|
||||||
input_pdfinfo = context.pdfinfo
|
input_pdfinfo = context.pdfinfo
|
||||||
fix_docinfo_file = context.get_path('fix_docinfo.pdf')
|
fix_docinfo_file = context.get_path('fix_docinfo.pdf')
|
||||||
@@ -767,21 +832,8 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -
|
|||||||
# NULs in DocumentInfo seem to be common since older Acrobats included them.
|
# NULs in DocumentInfo seem to be common since older Acrobats included them.
|
||||||
# pikepdf can deal with this, but we make the world a better place by
|
# pikepdf can deal with this, but we make the world a better place by
|
||||||
# stamping them out as soon as possible.
|
# stamping them out as soon as possible.
|
||||||
modified = False
|
|
||||||
with pikepdf.open(input_pdf) as pdf_file:
|
with pikepdf.open(input_pdf) as pdf_file:
|
||||||
try:
|
if _repair_docinfo_nuls(pdf_file):
|
||||||
len(pdf_file.docinfo)
|
|
||||||
except TypeError:
|
|
||||||
log.error(
|
|
||||||
"File contains a malformed DocumentInfo block - continuing anyway"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if pdf_file.docinfo:
|
|
||||||
for k, v in pdf_file.docinfo.items():
|
|
||||||
if b'\x00' in bytes(v):
|
|
||||||
pdf_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
|
||||||
modified = True
|
|
||||||
if modified:
|
|
||||||
pdf_file.save(fix_docinfo_file)
|
pdf_file.save(fix_docinfo_file)
|
||||||
else:
|
else:
|
||||||
safe_symlink(input_pdf, fix_docinfo_file)
|
safe_symlink(input_pdf, fix_docinfo_file)
|
||||||
@@ -791,7 +843,7 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -
|
|||||||
pdf_pages=[fix_docinfo_file],
|
pdf_pages=[fix_docinfo_file],
|
||||||
pdfmark=input_ps_stub,
|
pdfmark=input_ps_stub,
|
||||||
output_file=output_file,
|
output_file=output_file,
|
||||||
compression=options.pdfa_image_compression,
|
context=context,
|
||||||
pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3
|
pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3
|
||||||
progressbar_class=(
|
progressbar_class=(
|
||||||
context.plugin_manager.hook.get_progressbar_class()
|
context.plugin_manager.hook.get_progressbar_class()
|
||||||
@@ -804,7 +856,30 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -
|
|||||||
return output_file
|
return output_file
|
||||||
|
|
||||||
|
|
||||||
|
def _repair_docinfo_nuls(pdf):
|
||||||
|
"""If the DocumentInfo block contains NUL characters, remove them.
|
||||||
|
|
||||||
|
If the DocumentInfo block is malformed, log an error and continue.
|
||||||
|
"""
|
||||||
|
modified = False
|
||||||
|
try:
|
||||||
|
if not isinstance(pdf.docinfo, pikepdf.Dictionary):
|
||||||
|
raise TypeError("DocumentInfo is not a dictionary")
|
||||||
|
for k, v in pdf.docinfo.items():
|
||||||
|
if isinstance(v, str) and b'\x00' in bytes(v):
|
||||||
|
pdf.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||||
|
modified = True
|
||||||
|
except TypeError:
|
||||||
|
# TypeError can also be raised if dictionary items are unexpected types
|
||||||
|
log.error("File contains a malformed DocumentInfo block - continuing anyway.")
|
||||||
|
return modified
|
||||||
|
|
||||||
|
|
||||||
def should_linearize(working_file: Path, context: PdfContext) -> bool:
|
def should_linearize(working_file: Path, context: PdfContext) -> bool:
|
||||||
|
"""Determine whether the PDF should be linearized.
|
||||||
|
|
||||||
|
For smaller files, linearization is not worth the effort.
|
||||||
|
"""
|
||||||
filesize = os.stat(working_file).st_size
|
filesize = os.stat(working_file).st_size
|
||||||
if filesize > (context.options.fast_web_view * 1_000_000):
|
if filesize > (context.options.fast_web_view * 1_000_000):
|
||||||
return True
|
return True
|
||||||
@@ -812,6 +887,11 @@ def should_linearize(working_file: Path, context: PdfContext) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def get_pdf_save_settings(output_type: str) -> dict[str, Any]:
|
def get_pdf_save_settings(output_type: str) -> dict[str, Any]:
|
||||||
|
"""Get pikepdf.Pdf.save settings for the given output type.
|
||||||
|
|
||||||
|
Essentially, don't use features that are incompatible with a given
|
||||||
|
PDF/A specification.
|
||||||
|
"""
|
||||||
if output_type == 'pdfa-1':
|
if output_type == 'pdfa-1':
|
||||||
# Trigger recompression to ensure object streams are removed, because
|
# Trigger recompression to ensure object streams are removed, because
|
||||||
# Acrobat complains about them in PDF/A-1b validation.
|
# Acrobat complains about them in PDF/A-1b validation.
|
||||||
@@ -830,6 +910,11 @@ def get_pdf_save_settings(output_type: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
|
def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
|
||||||
|
"""Fix certain metadata fields after Ghostscript PDF/A conversion.
|
||||||
|
|
||||||
|
Also report on metadata in the input file that was not retained during
|
||||||
|
PDF/A conversion.
|
||||||
|
"""
|
||||||
output_file = context.get_path('metafix.pdf')
|
output_file = context.get_path('metafix.pdf')
|
||||||
options = context.options
|
options = context.options
|
||||||
|
|
||||||
@@ -852,43 +937,43 @@ def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
|
|||||||
|
|
||||||
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
||||||
docinfo = get_docinfo(original, context)
|
docinfo = get_docinfo(original, context)
|
||||||
with pdf.open_metadata() as meta_pdf:
|
with original.open_metadata(
|
||||||
meta_pdf.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
|
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
|
||||||
|
)
|
||||||
# If xmp:CreateDate is missing, set it to the modify date to
|
# If xmp:CreateDate is missing, set it to the modify date to
|
||||||
# ensure consistency with Ghostscript.
|
# ensure consistency with Ghostscript.
|
||||||
if 'xmp:CreateDate' not in meta_pdf:
|
if 'xmp:CreateDate' not in meta_pdf:
|
||||||
meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '')
|
meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '')
|
||||||
|
if meta_pdf.get('dc:title') == 'Untitled':
|
||||||
with original.open_metadata(
|
# Ghostscript likes to set title to Untitled if omitted from input.
|
||||||
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
|
# Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
|
||||||
) as meta_original:
|
# and the XMP Spec do not make this recommendation.
|
||||||
if meta_pdf.get('dc:title') == 'Untitled':
|
if 'dc:title' not in meta_original:
|
||||||
# Ghostscript likes to set title to Untitled if omitted from input.
|
del meta_pdf['dc:title']
|
||||||
# Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
|
# If the user explicitly specified an empty string for any of the
|
||||||
# and the XMP Spec do not make this recommendation.
|
# following, they should be unset and not reported as missing in
|
||||||
if 'dc:title' not in meta_original:
|
# the output pdf. Note that some metadata fields use differing names
|
||||||
del meta_pdf['dc:title']
|
# between PDF-A and PDF.
|
||||||
# If the user explicitly specified an empty string for any of the
|
for meta in [meta_pdf, meta_original]:
|
||||||
# following, they should be unset and not reported as missing in
|
if options.title == '' and 'dc:title' in meta:
|
||||||
# the output pdf. Note that some metadata fields use differing names
|
del meta['dc:title'] # PDF-A and PDF
|
||||||
# between PDF-A and PDF.
|
if options.author == '':
|
||||||
for meta in [meta_pdf, meta_original]:
|
if 'dc:creator' in meta:
|
||||||
if options.title == '' and 'dc:title' in meta:
|
del meta['dc:creator'] # PDF-A (Not xmp:CreatorTool)
|
||||||
del meta['dc:title'] # PDF-A and PDF
|
if 'pdf:Author' in meta:
|
||||||
if options.author == '':
|
del meta['pdf:Author'] # PDF
|
||||||
if 'dc:creator' in meta:
|
if options.subject == '':
|
||||||
del meta['dc:creator'] # PDF-A (Not xmp:CreatorTool)
|
if 'dc:description' in meta:
|
||||||
if 'pdf:Author' in meta:
|
del meta['dc:description'] # PDF-A
|
||||||
del meta['pdf:Author'] # PDF
|
if 'dc:subject' in meta:
|
||||||
if options.subject == '':
|
del meta['dc:subject'] # PDF
|
||||||
if 'dc:description' in meta:
|
if options.keywords == '' and 'pdf:Keywords' in meta:
|
||||||
del meta['dc:description'] # PDF-A
|
del meta['pdf:Keywords'] # PDF-A and PDF
|
||||||
if 'dc:subject' in meta:
|
meta_missing = set(meta_original.keys()) - set(meta_pdf.keys())
|
||||||
del meta['dc:subject'] # PDF
|
report_on_metadata(meta_missing)
|
||||||
if options.keywords == '' and 'pdf:Keywords' in meta:
|
|
||||||
del meta['pdf:Keywords'] # PDF-A and PDF
|
|
||||||
meta_missing = set(meta_original.keys()) - set(meta_pdf.keys())
|
|
||||||
report_on_metadata(meta_missing)
|
|
||||||
|
|
||||||
optimizing = context.plugin_manager.hook.is_optimization_enabled(
|
optimizing = context.plugin_manager.hook.is_optimization_enabled(
|
||||||
context=context
|
context=context
|
||||||
@@ -907,6 +992,17 @@ def metadata_fixup(working_file: Path, context: PdfContext) -> Path:
|
|||||||
def _file_size_ratio(
|
def _file_size_ratio(
|
||||||
input_file: Path, output_file: Path
|
input_file: Path, output_file: Path
|
||||||
) -> tuple[float | None, float | None]:
|
) -> tuple[float | None, float | None]:
|
||||||
|
"""Calculate ratio of input to output file sizes and percentage savings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_file (Path): The path to the input file.
|
||||||
|
output_file (Path): The path to the output file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[float | None, float | None]: A tuple containing the file size
|
||||||
|
ratio and the percentage savings achieved by the output file size
|
||||||
|
compared to the input file size.
|
||||||
|
"""
|
||||||
input_size = input_file.stat().st_size
|
input_size = input_file.stat().st_size
|
||||||
output_size = output_file.stat().st_size
|
output_size = output_file.stat().st_size
|
||||||
if output_size == 0:
|
if output_size == 0:
|
||||||
@@ -919,6 +1015,7 @@ def _file_size_ratio(
|
|||||||
def optimize_pdf(
|
def optimize_pdf(
|
||||||
input_file: Path, context: PdfContext, executor: Executor
|
input_file: Path, context: PdfContext, executor: Executor
|
||||||
) -> tuple[Path, Sequence[str]]:
|
) -> tuple[Path, Sequence[str]]:
|
||||||
|
"""Optimize the given PDF file."""
|
||||||
output_file = context.get_path('optimize.pdf')
|
output_file = context.get_path('optimize.pdf')
|
||||||
output_pdf, messages = context.plugin_manager.hook.optimize_pdf(
|
output_pdf, messages = context.plugin_manager.hook.optimize_pdf(
|
||||||
input_pdf=input_file,
|
input_pdf=input_file,
|
||||||
@@ -937,7 +1034,20 @@ def optimize_pdf(
|
|||||||
return output_pdf, messages
|
return output_pdf, messages
|
||||||
|
|
||||||
|
|
||||||
def enumerate_compress_ranges(iterable):
|
def enumerate_compress_ranges(
|
||||||
|
iterable: Iterable[T],
|
||||||
|
) -> Iterator[tuple[tuple[int, int], T]]:
|
||||||
|
"""Enumerate the ranges of non-empty elements in an iterable.
|
||||||
|
|
||||||
|
Compresses consecutive ranges of length 1 into single elements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
iterable: An iterable of elements to enumerate.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
A tuple containing a range of indices and the corresponding element.
|
||||||
|
If the element is None, the range represents a skipped range of indices.
|
||||||
|
"""
|
||||||
skipped_from, index = None, None
|
skipped_from, index = None, None
|
||||||
for index, txt_file in enumerate(iterable):
|
for index, txt_file in enumerate(iterable):
|
||||||
index += 1
|
index += 1
|
||||||
@@ -954,21 +1064,22 @@ def enumerate_compress_ranges(iterable):
|
|||||||
|
|
||||||
|
|
||||||
def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Path:
|
def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Path:
|
||||||
|
"""Merge the page sidecar files into a single file.
|
||||||
|
|
||||||
|
Sidecar files are created by the OCR engine and contain the text for each
|
||||||
|
page in the PDF. This function merges the sidecar files into a single file
|
||||||
|
and returns the path to the merged file.
|
||||||
|
"""
|
||||||
output_file = context.get_path('sidecar.txt')
|
output_file = context.get_path('sidecar.txt')
|
||||||
with open(output_file, 'w', encoding="utf-8") as stream:
|
with open(output_file, 'w', encoding="utf-8") as stream:
|
||||||
for (from_, to_), txt_file in enumerate_compress_ranges(txt_files):
|
for (from_, to_), txt_file in enumerate_compress_ranges(txt_files):
|
||||||
if from_ != 1:
|
if from_ != 1:
|
||||||
stream.write('\f') # Form feed between pages
|
stream.write('\f') # Form feed between pages for all pages after first
|
||||||
if txt_file:
|
if txt_file:
|
||||||
with open(txt_file, encoding="utf-8") as in_:
|
txt = txt_file.read_text(encoding="utf-8")
|
||||||
txt = in_.read()
|
# Some versions of Tesseract add a form feed at the end and
|
||||||
# Some OCR engines (e.g. Tesseract v4 alpha) add form feeds
|
# others don't. Remove it if it exists, since we add one manually.
|
||||||
# between pages, and some do not. For consistency, we ignore
|
stream.write(txt.removesuffix('\f'))
|
||||||
# any added by the OCR engine and them on our own.
|
|
||||||
if txt.endswith('\f'):
|
|
||||||
stream.write(txt[:-1])
|
|
||||||
else:
|
|
||||||
stream.write(txt)
|
|
||||||
else:
|
else:
|
||||||
if from_ != to_:
|
if from_ != to_:
|
||||||
pages = f'{from_}-{to_}'
|
pages = f'{from_}-{to_}'
|
||||||
@@ -979,8 +1090,18 @@ def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Pat
|
|||||||
|
|
||||||
|
|
||||||
def copy_final(
|
def copy_final(
|
||||||
input_file: Path, output_file: str | Path | BinaryIO, _context: PdfContext
|
input_file: Path, output_file: str | Path | BinaryIO, context: PdfContext
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Copy the final temporary file to the output destination.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_file (Path): The intermediate input file to copy.
|
||||||
|
output_file (str | Path | BinaryIO): The output file to copy to.
|
||||||
|
context (PdfContext): The PDF context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
log.debug('%s -> %s', input_file, output_file)
|
log.debug('%s -> %s', input_file, output_file)
|
||||||
with input_file.open('rb') as input_stream:
|
with input_file.open('rb') as input_stream:
|
||||||
if output_file == '-':
|
if output_file == '-':
|
||||||
@@ -995,5 +1116,11 @@ def copy_final(
|
|||||||
# At this point we overwrite the output_file specified by the user
|
# At this point we overwrite the output_file specified by the user
|
||||||
# use copyfileobj because then we use open() to create the file and
|
# use copyfileobj because then we use open() to create the file and
|
||||||
# get the appropriate umask, ownership, etc.
|
# get the appropriate umask, ownership, etc.
|
||||||
with open(output_file, 'wb') as output_stream:
|
with open(output_file, 'w+b') as output_stream:
|
||||||
copyfileobj(input_stream, output_stream)
|
copyfileobj(input_stream, output_stream)
|
||||||
|
# Attempt to copy file attributes from input to output
|
||||||
|
with suppress(OSError):
|
||||||
|
# Copy original file's permissions, ownership, etc. if possible
|
||||||
|
copystat(context.options.input_file, output_file)
|
||||||
|
# Set output file's modification time to now
|
||||||
|
Path(output_file).touch(exist_ok=True)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import importlib
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
@@ -100,7 +100,9 @@ class OcrmypdfPluginManager(pluggy.PluginManager):
|
|||||||
self.register(module)
|
self.register(module)
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_manager(plugins: list[str | Path], builtins=True):
|
def get_plugin_manager(
|
||||||
|
plugins: list[str | Path], builtins=True
|
||||||
|
) -> OcrmypdfPluginManager:
|
||||||
return OcrmypdfPluginManager(
|
return OcrmypdfPluginManager(
|
||||||
project_name='ocrmypdf',
|
project_name='ocrmypdf',
|
||||||
plugins=plugins,
|
plugins=plugins,
|
||||||
|
|||||||
+22
-4
@@ -13,12 +13,13 @@ import logging.handlers
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
from collections.abc import Sequence
|
||||||
from concurrent.futures.process import BrokenProcessPool
|
from concurrent.futures.process import BrokenProcessPool
|
||||||
from concurrent.futures.thread import BrokenThreadPool
|
from concurrent.futures.thread import BrokenThreadPool
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from typing import NamedTuple, Sequence, cast
|
from typing import NamedTuple, cast
|
||||||
|
|
||||||
import PIL
|
import PIL
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ def preprocess(
|
|||||||
deskew: bool,
|
deskew: bool,
|
||||||
clean: bool,
|
clean: bool,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
|
"""Preprocess an image."""
|
||||||
if remove_background:
|
if remove_background:
|
||||||
image = preprocess_remove_background(image, page_context)
|
image = preprocess_remove_background(image, page_context)
|
||||||
if deskew:
|
if deskew:
|
||||||
@@ -116,6 +118,7 @@ def preprocess(
|
|||||||
def make_intermediate_images(
|
def make_intermediate_images(
|
||||||
page_context: PageContext, orientation_correction: int
|
page_context: PageContext, orientation_correction: int
|
||||||
) -> tuple[Path, Path | None]:
|
) -> tuple[Path, Path | None]:
|
||||||
|
"""Create intermediate and preprocessed images for OCR."""
|
||||||
options = page_context.options
|
options = page_context.options
|
||||||
|
|
||||||
ocr_image = preprocess_out = None
|
ocr_image = preprocess_out = None
|
||||||
@@ -173,6 +176,7 @@ def make_intermediate_images(
|
|||||||
|
|
||||||
|
|
||||||
def exec_page_sync(page_context: PageContext) -> PageResult:
|
def exec_page_sync(page_context: PageContext) -> PageResult:
|
||||||
|
"""Execute a pipeline for a single page synchronously."""
|
||||||
options = page_context.options
|
options = page_context.options
|
||||||
tls.pageno = page_context.pageno + 1
|
tls.pageno = page_context.pageno + 1
|
||||||
|
|
||||||
@@ -233,6 +237,7 @@ def exec_page_sync(page_context: PageContext) -> PageResult:
|
|||||||
def post_process(
|
def post_process(
|
||||||
pdf_file: Path, context: PdfContext, executor: Executor
|
pdf_file: Path, context: PdfContext, executor: Executor
|
||||||
) -> tuple[Path, Sequence[str]]:
|
) -> tuple[Path, Sequence[str]]:
|
||||||
|
"""Postprocess the PDF file."""
|
||||||
pdf_out = pdf_file
|
pdf_out = pdf_file
|
||||||
if context.options.output_type.startswith('pdfa'):
|
if context.options.output_type.startswith('pdfa'):
|
||||||
ps_stub_out = generate_postscript_stub(context)
|
ps_stub_out = generate_postscript_stub(context)
|
||||||
@@ -243,6 +248,7 @@ def post_process(
|
|||||||
|
|
||||||
|
|
||||||
def worker_init(max_pixels: int) -> None:
|
def worker_init(max_pixels: int) -> None:
|
||||||
|
"""Initialize a worker thread or process."""
|
||||||
# In Windows, child process will not inherit our change to this value in
|
# In Windows, child process will not inherit our change to this value in
|
||||||
# the parent process, so ensure workers get it set. Not needed when running
|
# the parent process, so ensure workers get it set. Not needed when running
|
||||||
# threaded, but harmless to set again.
|
# threaded, but harmless to set again.
|
||||||
@@ -251,8 +257,8 @@ def worker_init(max_pixels: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
||||||
"""Execute the pipeline concurrently."""
|
"""Execute the OCR pipeline concurrently."""
|
||||||
# Run exec_page_sync on every page context
|
# Run exec_page_sync on every page
|
||||||
options = context.options
|
options = context.options
|
||||||
max_workers = min(len(context.pdfinfo), options.jobs)
|
max_workers = min(len(context.pdfinfo), options.jobs)
|
||||||
if max_workers > 1:
|
if max_workers > 1:
|
||||||
@@ -262,6 +268,7 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
|
|||||||
ocrgraft = OcrGrafter(context)
|
ocrgraft = OcrGrafter(context)
|
||||||
|
|
||||||
def update_page(result: PageResult, pbar):
|
def update_page(result: PageResult, pbar):
|
||||||
|
"""After OCR is complete for a page, update the PDF."""
|
||||||
try:
|
try:
|
||||||
tls.pageno = result.pageno + 1
|
tls.pageno = result.pageno + 1
|
||||||
sidecars[result.pageno] = result.text
|
sidecars[result.pageno] = result.text
|
||||||
@@ -317,7 +324,7 @@ def configure_debug_logging(
|
|||||||
) -> logging.FileHandler:
|
) -> logging.FileHandler:
|
||||||
"""Create a debug log file at a specified location.
|
"""Create a debug log file at a specified location.
|
||||||
|
|
||||||
Arguments:
|
Args:
|
||||||
log_filename: Where to the put the log file.
|
log_filename: Where to the put the log file.
|
||||||
prefix: The logging domain prefix that should be sent to the log.
|
prefix: The logging domain prefix that should be sent to the log.
|
||||||
"""
|
"""
|
||||||
@@ -338,6 +345,17 @@ def run_pipeline(
|
|||||||
plugin_manager: OcrmypdfPluginManager | None,
|
plugin_manager: OcrmypdfPluginManager | None,
|
||||||
api: bool = False,
|
api: bool = False,
|
||||||
) -> ExitCode:
|
) -> ExitCode:
|
||||||
|
"""Run the OCR pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: The parsed command line options.
|
||||||
|
plugin_manager: The plugin manager to use. If not provided, one will be
|
||||||
|
created.
|
||||||
|
api: If ``True``, the pipeline is being run from the API. This is used
|
||||||
|
to manage exceptions in a way appropriate for API or CLI usage.
|
||||||
|
For CLI (``api=False``), exceptions are printed and described;
|
||||||
|
for API use, they are propagated to the caller.
|
||||||
|
"""
|
||||||
# Any changes to options will not take effect for options that are already
|
# Any changes to options will not take effect for options that are already
|
||||||
# bound to function parameters in the pipeline. (For example
|
# bound to function parameters in the pipeline. (For example
|
||||||
# options.input_file, options.pdf_renderer are already bound.)
|
# options.input_file, options.pdf_renderer are already bound.)
|
||||||
|
|||||||
+28
-17
@@ -12,9 +12,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
import PIL
|
import PIL
|
||||||
@@ -43,10 +43,11 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def check_platform() -> None:
|
def check_platform() -> None:
|
||||||
if os.name == 'nt' and sys.maxsize <= 2**32: # pragma: no cover
|
if sys.maxsize <= 2**32: # pragma: no cover
|
||||||
# 32-bit interpreter on Windows
|
log.warning(
|
||||||
log.error(
|
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter. "
|
||||||
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
|
"This is not supported. 32-bit does not have enough address space "
|
||||||
|
"to process large files. "
|
||||||
"Please use a 64-bit (x86-64) version of Python."
|
"Please use a 64-bit (x86-64) version of Python."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -204,16 +205,6 @@ def check_options_ocr_behavior(options: Namespace) -> None:
|
|||||||
options.pages = _pages_from_ranges(options.pages)
|
options.pages = _pages_from_ranges(options.pages)
|
||||||
|
|
||||||
|
|
||||||
def check_options_advanced(options: Namespace) -> None:
|
|
||||||
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
|
|
||||||
'pdfa'
|
|
||||||
):
|
|
||||||
log.warning(
|
|
||||||
"--pdfa-image-compression argument only applies when "
|
|
||||||
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_options_metadata(options: Namespace) -> None:
|
def check_options_metadata(options: Namespace) -> None:
|
||||||
docinfo = [options.title, options.author, options.keywords, options.subject]
|
docinfo = [options.title, options.author, options.keywords, options.subject]
|
||||||
for s in (m for m in docinfo if m):
|
for s in (m for m in docinfo if m):
|
||||||
@@ -240,7 +231,6 @@ def _check_plugin_invariant_options(options: Namespace) -> None:
|
|||||||
check_options_sidecar(options)
|
check_options_sidecar(options)
|
||||||
check_options_preprocessing(options)
|
check_options_preprocessing(options)
|
||||||
check_options_ocr_behavior(options)
|
check_options_ocr_behavior(options)
|
||||||
check_options_advanced(options)
|
|
||||||
check_options_pillow(options)
|
check_options_pillow(options)
|
||||||
|
|
||||||
|
|
||||||
@@ -255,6 +245,18 @@ def check_options(options: Namespace, plugin_manager: PluginManager) -> None:
|
|||||||
_check_plugin_options(options, plugin_manager)
|
_check_plugin_options(options, plugin_manager)
|
||||||
|
|
||||||
|
|
||||||
|
def _in_docker():
|
||||||
|
return Path('/.dockerenv').exists()
|
||||||
|
|
||||||
|
|
||||||
|
def _in_snap():
|
||||||
|
try:
|
||||||
|
cgroup_text = Path('/proc/self/cgroup').read_text()
|
||||||
|
return 'snap.ocrmypdf' in cgroup_text
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]:
|
def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]:
|
||||||
if options.input_file == '-':
|
if options.input_file == '-':
|
||||||
# stdin
|
# stdin
|
||||||
@@ -278,7 +280,7 @@ def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]
|
|||||||
return target, os.fspath(options.input_file)
|
return target, os.fspath(options.input_file)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
msg = f"File not found - {options.input_file}"
|
msg = f"File not found - {options.input_file}"
|
||||||
if Path('/.dockerenv').exists(): # pragma: no cover
|
if _in_docker(): # pragma: no cover
|
||||||
msg += (
|
msg += (
|
||||||
"\nDocker cannot your working directory unless you "
|
"\nDocker cannot your working directory unless you "
|
||||||
"explicitly share it with the Docker container and set up"
|
"explicitly share it with the Docker container and set up"
|
||||||
@@ -288,6 +290,15 @@ def create_input_file(options: Namespace, work_folder: Path) -> tuple[Path, str]
|
|||||||
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf"
|
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf"
|
||||||
"\n"
|
"\n"
|
||||||
)
|
)
|
||||||
|
elif _in_snap(): # pragma: no cover
|
||||||
|
msg += (
|
||||||
|
"\nSnap applications cannot access files outside of "
|
||||||
|
"your home directory unless you explicitly allow it. "
|
||||||
|
"You may find it easier to use stdin/stdout:"
|
||||||
|
"\n"
|
||||||
|
"\tsnap run ocrmypdf - - <input.pdf >output.pdf"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
raise InputFileError(msg) from e
|
raise InputFileError(msg) from e
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+27
-16
@@ -9,16 +9,17 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
from argparse import Namespace
|
||||||
|
from collections.abc import Iterable
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from io import IOBase
|
from io import IOBase
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AnyStr, BinaryIO, Iterable, Union
|
from typing import AnyStr, BinaryIO, Union
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
import coloredlogs
|
import pluggy
|
||||||
from humanfriendly.terminal import enable_ansi_support
|
|
||||||
|
|
||||||
from ocrmypdf._logging import PageNumberFilter, TqdmConsole
|
from ocrmypdf._logging import PageNumberFilter
|
||||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||||
from ocrmypdf._sync import run_pipeline
|
from ocrmypdf._sync import run_pipeline
|
||||||
from ocrmypdf._validation import check_options
|
from ocrmypdf._validation import check_options
|
||||||
@@ -46,7 +47,7 @@ def configure_logging(
|
|||||||
*,
|
*,
|
||||||
progress_bar_friendly: bool = True,
|
progress_bar_friendly: bool = True,
|
||||||
manage_root_logger: bool = False,
|
manage_root_logger: bool = False,
|
||||||
plugin_manager=None,
|
plugin_manager: pluggy.PluginManager | None = None,
|
||||||
):
|
):
|
||||||
"""Set up logging.
|
"""Set up logging.
|
||||||
|
|
||||||
@@ -112,14 +113,7 @@ def configure_logging(
|
|||||||
else:
|
else:
|
||||||
fmt = '%(pageno)s%(message)s'
|
fmt = '%(pageno)s%(message)s'
|
||||||
|
|
||||||
use_colors = progress_bar_friendly
|
|
||||||
formatter = None
|
formatter = None
|
||||||
if use_colors:
|
|
||||||
use_colors = enable_ansi_support()
|
|
||||||
if use_colors:
|
|
||||||
use_colors = coloredlogs.terminal_supports_colors()
|
|
||||||
if use_colors:
|
|
||||||
formatter = coloredlogs.ColoredFormatter(fmt=fmt)
|
|
||||||
|
|
||||||
if not formatter:
|
if not formatter:
|
||||||
formatter = logging.Formatter(fmt=fmt)
|
formatter = logging.Formatter(fmt=fmt)
|
||||||
@@ -141,8 +135,21 @@ def configure_logging(
|
|||||||
|
|
||||||
def create_options(
|
def create_options(
|
||||||
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
|
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
|
||||||
):
|
) -> Namespace:
|
||||||
"""Construct an options object from the input/output files and keyword arguments."""
|
"""Construct an options object from the input/output files and keyword arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_file: Input file path or file object.
|
||||||
|
output_file: Output file path or file object.
|
||||||
|
parser: ArgumentParser object.
|
||||||
|
**kwargs: Keyword arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.Namespace: A Namespace object containing the parsed arguments.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If the type of a keyword argument is not supported.
|
||||||
|
"""
|
||||||
cmdline = []
|
cmdline = []
|
||||||
deferred = []
|
deferred = []
|
||||||
|
|
||||||
@@ -203,7 +210,7 @@ def create_options(
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def ocr( # noqa: ruff: disable=D417
|
def ocr( # noqa: D417
|
||||||
input_file: PathOrIO,
|
input_file: PathOrIO,
|
||||||
output_file: PathOrIO,
|
output_file: PathOrIO,
|
||||||
*,
|
*,
|
||||||
@@ -234,6 +241,7 @@ def ocr( # noqa: ruff: disable=D417
|
|||||||
png_quality: int | None = None,
|
png_quality: int | None = None,
|
||||||
jbig2_lossy: bool | None = None,
|
jbig2_lossy: bool | None = None,
|
||||||
jbig2_page_group_size: int | None = None,
|
jbig2_page_group_size: int | None = None,
|
||||||
|
jbig2_threshold: float | None = None,
|
||||||
pages: str | None = None,
|
pages: str | None = None,
|
||||||
max_image_mpixels: float | None = None,
|
max_image_mpixels: float | None = None,
|
||||||
tesseract_config: Iterable[str] | None = None,
|
tesseract_config: Iterable[str] | None = None,
|
||||||
@@ -243,12 +251,16 @@ def ocr( # noqa: ruff: disable=D417
|
|||||||
pdf_renderer: str | None = None,
|
pdf_renderer: str | None = None,
|
||||||
tesseract_timeout: float | None = None,
|
tesseract_timeout: float | None = None,
|
||||||
tesseract_non_ocr_timeout: float | None = None,
|
tesseract_non_ocr_timeout: float | None = None,
|
||||||
|
tesseract_downsample_above: int | None = None,
|
||||||
|
tesseract_downsample_large_images: bool | None = None,
|
||||||
rotate_pages_threshold: float | None = None,
|
rotate_pages_threshold: float | None = None,
|
||||||
pdfa_image_compression: str | None = None,
|
pdfa_image_compression: str | None = None,
|
||||||
|
color_conversion_strategy: str | None = None,
|
||||||
user_words: os.PathLike | None = None,
|
user_words: os.PathLike | None = None,
|
||||||
user_patterns: os.PathLike | None = None,
|
user_patterns: os.PathLike | None = None,
|
||||||
fast_web_view: float | None = None,
|
fast_web_view: float | None = None,
|
||||||
continue_on_soft_render_error: bool | None = None,
|
continue_on_soft_render_error: bool | None = None,
|
||||||
|
invalidate_digital_signatures: bool | None = None,
|
||||||
plugins: Iterable[StrPath] | None = None,
|
plugins: Iterable[StrPath] | None = None,
|
||||||
plugin_manager=None,
|
plugin_manager=None,
|
||||||
keep_temporary_files: bool | None = None,
|
keep_temporary_files: bool | None = None,
|
||||||
@@ -349,7 +361,6 @@ def ocr( # noqa: ruff: disable=D417
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'PageNumberFilter',
|
'PageNumberFilter',
|
||||||
'TqdmConsole',
|
|
||||||
'Verbosity',
|
'Verbosity',
|
||||||
'check_options',
|
'check_options',
|
||||||
'configure_logging',
|
'configure_logging',
|
||||||
|
|||||||
@@ -12,18 +12,19 @@ import queue
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
from collections.abc import Iterable
|
||||||
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Callable, Iterable, Type, Union
|
from typing import Callable, Union
|
||||||
|
|
||||||
from tqdm import tqdm
|
from rich.console import Console as RichConsole
|
||||||
|
|
||||||
from ocrmypdf import Executor, hookimpl
|
from ocrmypdf import Executor, hookimpl
|
||||||
from ocrmypdf._logging import TqdmConsole
|
from ocrmypdf._logging import RichLoggingHandler, RichTqdmProgressAdapter
|
||||||
from ocrmypdf.exceptions import InputFileError
|
from ocrmypdf.exceptions import InputFileError
|
||||||
from ocrmypdf.helpers import remove_all_log_handlers
|
from ocrmypdf.helpers import remove_all_log_handlers
|
||||||
|
|
||||||
FuturesExecutorClass = Union[Type[ThreadPoolExecutor], Type[ProcessPoolExecutor]]
|
FuturesExecutorClass = Union[type[ThreadPoolExecutor], type[ProcessPoolExecutor]]
|
||||||
Queue = Union[multiprocessing.Queue, queue.Queue]
|
Queue = Union[multiprocessing.Queue, queue.Queue]
|
||||||
UserInit = Callable[[], None]
|
UserInit = Callable[[], None]
|
||||||
WorkerInit = Callable[[Queue, UserInit, int], None]
|
WorkerInit = Callable[[Queue, UserInit, int], None]
|
||||||
@@ -68,7 +69,7 @@ def process_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
|||||||
# Windows and Cygwin do not have pthread_sigmask or SIGBUS
|
# Windows and Cygwin do not have pthread_sigmask or SIGBUS
|
||||||
signal.signal(signal.SIGBUS, process_sigbus)
|
signal.signal(signal.SIGBUS, process_sigbus)
|
||||||
|
|
||||||
# Remove any log handlers that belong to the parent process
|
# Remove any log handlers inherited from the parent process
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
remove_all_log_handlers(root)
|
remove_all_log_handlers(root)
|
||||||
|
|
||||||
@@ -95,15 +96,6 @@ def thread_init(q: Queue, user_init: UserInit, loglevel) -> None:
|
|||||||
class StandardExecutor(Executor):
|
class StandardExecutor(Executor):
|
||||||
"""Standard OCRmyPDF concurrent task executor."""
|
"""Standard OCRmyPDF concurrent task executor."""
|
||||||
|
|
||||||
def _cancel_futures_kwargs(self):
|
|
||||||
"""Shim older Pythons that do not have Executor.shutdown(...cancel_futures=).
|
|
||||||
|
|
||||||
Remove this code when support for Python 3.8 is dropped.
|
|
||||||
"""
|
|
||||||
if sys.version_info[:2] < (3, 9):
|
|
||||||
return {}
|
|
||||||
return dict(cancel_futures=True)
|
|
||||||
|
|
||||||
def _execute(
|
def _execute(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -127,6 +119,11 @@ class StandardExecutor(Executor):
|
|||||||
# Regardless of whether we use_threads for worker processes, the log_listener
|
# Regardless of whether we use_threads for worker processes, the log_listener
|
||||||
# must be a thread. Make sure we create the listener after the worker pool,
|
# must be a thread. Make sure we create the listener after the worker pool,
|
||||||
# so that it does not get forked into the workers.
|
# so that it does not get forked into the workers.
|
||||||
|
# If use_threads is False, we are currently guilty of creating a thread before
|
||||||
|
# forking on Linux, which is not recommended. However, we take a big
|
||||||
|
# performance hit in pdfinfo if we can't fork. Long term solution is to
|
||||||
|
# replace most of this with an asyncio implementation, and probably to
|
||||||
|
# migrate some of pdfinfo into C++ or Rust.
|
||||||
listener = threading.Thread(target=log_listener, args=(log_queue,))
|
listener = threading.Thread(target=log_listener, args=(log_queue,))
|
||||||
listener.start()
|
listener.start()
|
||||||
|
|
||||||
@@ -142,7 +139,7 @@ class StandardExecutor(Executor):
|
|||||||
task_finished(result, pbar)
|
task_finished(result, pbar)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# Terminate pool so we exit instantly
|
# Terminate pool so we exit instantly
|
||||||
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
|
executor.shutdown(wait=False, cancel_futures=True)
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
||||||
@@ -151,7 +148,7 @@ class StandardExecutor(Executor):
|
|||||||
# results will be discard. But if the condition above is True,
|
# results will be discard. But if the condition above is True,
|
||||||
# then we are running in pytest, and we want everything to exit
|
# then we are running in pytest, and we want everything to exit
|
||||||
# as cleanly as possible so that we get good error messages.
|
# as cleanly as possible so that we get good error messages.
|
||||||
executor.shutdown(wait=False, **self._cancel_futures_kwargs())
|
executor.shutdown(wait=False, cancel_futures=True)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# Terminate log listener
|
# Terminate log listener
|
||||||
@@ -168,13 +165,20 @@ def get_executor(progressbar_class):
|
|||||||
return StandardExecutor(pbar_class=progressbar_class)
|
return StandardExecutor(pbar_class=progressbar_class)
|
||||||
|
|
||||||
|
|
||||||
|
RICH_CONSOLE = RichConsole(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def get_progressbar_class():
|
def get_progressbar_class():
|
||||||
"""Return the default progress bar class."""
|
"""Return the default progress bar class."""
|
||||||
return tqdm
|
|
||||||
|
def partial_RichTqdmProgressAdapter(*args, **kwargs):
|
||||||
|
return RichTqdmProgressAdapter(*args, **kwargs, console=RICH_CONSOLE)
|
||||||
|
|
||||||
|
return partial_RichTqdmProgressAdapter
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def get_logging_console():
|
def get_logging_console():
|
||||||
"""Return the default logging console handler."""
|
"""Return the default logging console handler."""
|
||||||
return logging.StreamHandler(stream=TqdmConsole(sys.stderr))
|
return RichLoggingHandler(console=RICH_CONSOLE)
|
||||||
|
|||||||
@@ -13,6 +13,37 @@ from ocrmypdf.subprocess import check_external_program
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def add_options(parser):
|
||||||
|
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
|
||||||
|
gs.add_argument(
|
||||||
|
'--color-conversion-strategy',
|
||||||
|
action='store',
|
||||||
|
type=str,
|
||||||
|
metavar='STRATEGY',
|
||||||
|
choices=ghostscript.COLOR_CONVERSION_STRATEGIES,
|
||||||
|
default='LeaveColorUnchanged',
|
||||||
|
help="Set Ghostscript color conversion strategy",
|
||||||
|
)
|
||||||
|
gs.add_argument(
|
||||||
|
'--pdfa-image-compression',
|
||||||
|
choices=['auto', 'jpeg', 'lossless'],
|
||||||
|
default='auto',
|
||||||
|
help="Specify how to compress images in the output PDF/A. 'auto' lets "
|
||||||
|
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
|
||||||
|
"JPEG compression. 'lossless' uses PNG-style lossless compression "
|
||||||
|
"for all images. Monochrome images are always compressed using a "
|
||||||
|
"lossless codec. Compression settings "
|
||||||
|
"are applied to all pages, including those for which OCR was "
|
||||||
|
"skipped. Not supported for --output-type=pdf ; that setting "
|
||||||
|
"preserves the original compression of all images.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def check_options(options):
|
def check_options(options):
|
||||||
@@ -21,10 +52,10 @@ def check_options(options):
|
|||||||
program='gs',
|
program='gs',
|
||||||
package='ghostscript',
|
package='ghostscript',
|
||||||
version_checker=ghostscript.version,
|
version_checker=ghostscript.version,
|
||||||
need_version='9.50', # Ubuntu 20.04's version
|
need_version='9.55', # Ubuntu 22.04's version
|
||||||
)
|
)
|
||||||
gs_version = ghostscript.version()
|
gs_version = ghostscript.version()
|
||||||
if gs_version in ('9.51',):
|
if gs_version in BLACKLISTED_GS_VERSIONS:
|
||||||
raise MissingDependencyError(
|
raise MissingDependencyError(
|
||||||
f"Ghostscript {gs_version} contains serious regressions and is not "
|
f"Ghostscript {gs_version} contains serious regressions and is not "
|
||||||
"supported. Please upgrade to a newer version, or downgrade to the "
|
"supported. Please upgrade to a newer version, or downgrade to the "
|
||||||
@@ -33,6 +64,17 @@ def check_options(options):
|
|||||||
|
|
||||||
if options.output_type == 'pdfa':
|
if options.output_type == 'pdfa':
|
||||||
options.output_type = 'pdfa-2'
|
options.output_type = 'pdfa-2'
|
||||||
|
if options.color_conversion_strategy not in ghostscript.COLOR_CONVERSION_STRATEGIES:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid color conversion strategy: {options.color_conversion_strategy}"
|
||||||
|
)
|
||||||
|
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
|
||||||
|
'pdfa'
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
"--pdfa-image-compression argument only applies when "
|
||||||
|
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
@@ -67,7 +109,7 @@ def generate_pdfa(
|
|||||||
pdf_pages,
|
pdf_pages,
|
||||||
pdfmark,
|
pdfmark,
|
||||||
output_file,
|
output_file,
|
||||||
compression,
|
context,
|
||||||
pdf_version,
|
pdf_version,
|
||||||
pdfa_part,
|
pdfa_part,
|
||||||
progressbar_class,
|
progressbar_class,
|
||||||
@@ -77,7 +119,8 @@ def generate_pdfa(
|
|||||||
ghostscript.generate_pdfa(
|
ghostscript.generate_pdfa(
|
||||||
pdf_pages=[*pdf_pages, pdfmark],
|
pdf_pages=[*pdf_pages, pdfmark],
|
||||||
output_file=output_file,
|
output_file=output_file,
|
||||||
compression=compression,
|
compression=context.options.pdfa_image_compression,
|
||||||
|
color_conversion_strategy=context.options.color_conversion_strategy,
|
||||||
pdf_version=pdf_version,
|
pdf_version=pdf_version,
|
||||||
pdfa_part=pdfa_part,
|
pdfa_part=pdfa_part,
|
||||||
progressbar_class=progressbar_class,
|
progressbar_class=progressbar_class,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from ocrmypdf import Executor, PdfContext, hookimpl
|
from ocrmypdf import Executor, PdfContext, hookimpl
|
||||||
from ocrmypdf._exec import jbig2enc, pngquant
|
from ocrmypdf._exec import jbig2enc, pngquant
|
||||||
@@ -86,6 +86,16 @@ def add_options(parser):
|
|||||||
# Adjust number of pages to consider at once for JBIG2 compression
|
# Adjust number of pages to consider at once for JBIG2 compression
|
||||||
help=argparse.SUPPRESS,
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
|
optimizing.add_argument(
|
||||||
|
'--jbig2-threshold',
|
||||||
|
type=numeric(float, 0.4, 0.9),
|
||||||
|
default=0.85,
|
||||||
|
metavar='T',
|
||||||
|
help=(
|
||||||
|
"Adjust JBIG2 symbol code classification threshold "
|
||||||
|
"(default 0.85), range 0.4 to 0.9."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
@@ -95,7 +105,7 @@ def check_options(options):
|
|||||||
program='pngquant',
|
program='pngquant',
|
||||||
package='pngquant',
|
package='pngquant',
|
||||||
version_checker=pngquant.version,
|
version_checker=pngquant.version,
|
||||||
need_version='2.0.1',
|
need_version='2.12.2',
|
||||||
required_for='--optimize {2,3}',
|
required_for='--optimize {2,3}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def add_options(parser):
|
|||||||
action='append',
|
action='append',
|
||||||
metavar='CFG',
|
metavar='CFG',
|
||||||
default=[],
|
default=[],
|
||||||
help="Additional Tesseract configuration files -- see documentation",
|
help="Additional Tesseract configuration files -- see documentation.",
|
||||||
)
|
)
|
||||||
tess.add_argument(
|
tess.add_argument(
|
||||||
'--tesseract-pagesegmode',
|
'--tesseract-pagesegmode',
|
||||||
@@ -38,7 +38,7 @@ def add_options(parser):
|
|||||||
type=int,
|
type=int,
|
||||||
metavar='PSM',
|
metavar='PSM',
|
||||||
choices=range(0, 14),
|
choices=range(0, 14),
|
||||||
help="Set Tesseract page segmentation mode (see tesseract --help)",
|
help="Set Tesseract page segmentation mode (see tesseract --help).",
|
||||||
)
|
)
|
||||||
tess.add_argument(
|
tess.add_argument(
|
||||||
'--tesseract-oem',
|
'--tesseract-oem',
|
||||||
@@ -75,7 +75,10 @@ def add_options(parser):
|
|||||||
metavar='SECONDS',
|
metavar='SECONDS',
|
||||||
help=(
|
help=(
|
||||||
"Give up on OCR after the timeout, but copy the preprocessed page "
|
"Give up on OCR after the timeout, but copy the preprocessed page "
|
||||||
"into the final output."
|
"into the final output. This timeout is only used when using Tesseract "
|
||||||
|
"for OCR. When Tesseract is used for other operations such as "
|
||||||
|
"deskewing and orientation, the timeout is controlled by "
|
||||||
|
"--tesseract-non-ocr-timeout."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
tess.add_argument(
|
tess.add_argument(
|
||||||
@@ -137,7 +140,7 @@ def check_options(options):
|
|||||||
program='tesseract',
|
program='tesseract',
|
||||||
package={'linux': 'tesseract-ocr'},
|
package={'linux': 'tesseract-ocr'},
|
||||||
version_checker=tesseract.version,
|
version_checker=tesseract.version,
|
||||||
need_version='4.1.1', # Ubuntu 20.04 version
|
need_version='4.1.1', # Ubuntu 22.04 version (also 20.04)
|
||||||
version_parser=tesseract.TesseractVersion,
|
version_parser=tesseract.TesseractVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,6 +178,15 @@ def validate(pdfinfo, options):
|
|||||||
tess_threads = int(os.environ['OMP_THREAD_LIMIT'])
|
tess_threads = int(os.environ['OMP_THREAD_LIMIT'])
|
||||||
log.debug("Using Tesseract OpenMP thread limit %d", tess_threads)
|
log.debug("Using Tesseract OpenMP thread limit %d", tess_threads)
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.tesseract_downsample_above != 32767
|
||||||
|
and not options.tesseract_downsample_large_images
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
"The --tesseract-downsample-above argument will have no effect unless "
|
||||||
|
"--tesseract-downsample-large-images is also given."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
|
def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
|
||||||
@@ -200,7 +212,7 @@ class TesseractOcrEngine(OcrEngine):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def version():
|
def version():
|
||||||
return tesseract.version()
|
return str(tesseract.version())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def creator_tag(options):
|
def creator_tag(options):
|
||||||
|
|||||||
+17
-16
@@ -6,7 +6,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from typing import Any, Callable, Mapping, TypeVar
|
from collections.abc import Mapping
|
||||||
|
from typing import Any, Callable, TypeVar
|
||||||
|
|
||||||
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
|
||||||
from ocrmypdf._version import __version__ as _VERSION
|
from ocrmypdf._version import __version__ as _VERSION
|
||||||
@@ -15,7 +16,11 @@ T = TypeVar('T', int, float)
|
|||||||
|
|
||||||
|
|
||||||
def numeric(basetype: Callable[[Any], T], min_: T | None = None, max_: T | None = None):
|
def numeric(basetype: Callable[[Any], T], min_: T | None = None, max_: T | None = None):
|
||||||
"""Validator for numeric params."""
|
"""Validator for numeric command line parameters.
|
||||||
|
|
||||||
|
Stipulates that the value must be of type basetype (typically int or float), and
|
||||||
|
optionally, within the range [min_, max_].
|
||||||
|
"""
|
||||||
min_ = basetype(min_) if min_ is not None else None
|
min_ = basetype(min_) if min_ is not None else None
|
||||||
max_ = basetype(max_) if max_ is not None else None
|
max_ = basetype(max_) if max_ is not None else None
|
||||||
|
|
||||||
@@ -177,7 +182,9 @@ Online documentation is located at:
|
|||||||
'--image-dpi',
|
'--image-dpi',
|
||||||
metavar='DPI',
|
metavar='DPI',
|
||||||
type=int,
|
type=int,
|
||||||
help="For input image instead of PDF, use this DPI instead of file's.",
|
help="When the input file is an image, not a PDF, use this DPI instead "
|
||||||
|
"of the DPI claimed by the input file. If the input does not claim a "
|
||||||
|
"sensible DPI, this option will be required.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--output-type',
|
'--output-type',
|
||||||
@@ -359,6 +366,13 @@ Online documentation is located at:
|
|||||||
help="Skip OCR on pages larger than the specified amount of megapixels, "
|
help="Skip OCR on pages larger than the specified amount of megapixels, "
|
||||||
"but include skipped pages in final output",
|
"but include skipped pages in final output",
|
||||||
)
|
)
|
||||||
|
ocrsettings.add_argument(
|
||||||
|
'--invalidate-digital-signatures',
|
||||||
|
action='store_true',
|
||||||
|
help="Normally, OCRmyPDF will refuse to OCR a PDF that has a digital "
|
||||||
|
"signature. This option allows OCR to proceed, but the digital signature "
|
||||||
|
"will be invalidated.",
|
||||||
|
)
|
||||||
|
|
||||||
advanced = parser.add_argument_group(
|
advanced = parser.add_argument_group(
|
||||||
"Advanced", "Advanced options to control OCRmyPDF"
|
"Advanced", "Advanced options to control OCRmyPDF"
|
||||||
@@ -395,19 +409,6 @@ Online documentation is located at:
|
|||||||
help="Only rotate pages when confidence is above this value (arbitrary "
|
help="Only rotate pages when confidence is above this value (arbitrary "
|
||||||
"units reported by tesseract)",
|
"units reported by tesseract)",
|
||||||
)
|
)
|
||||||
advanced.add_argument(
|
|
||||||
'--pdfa-image-compression',
|
|
||||||
choices=['auto', 'jpeg', 'lossless'],
|
|
||||||
default='auto',
|
|
||||||
help="Specify how to compress images in the output PDF/A. 'auto' lets "
|
|
||||||
"OCRmyPDF decide. 'jpeg' changes all grayscale and color images to "
|
|
||||||
"JPEG compression. 'lossless' uses PNG-style lossless compression "
|
|
||||||
"for all images. Monochrome images are always compressed using a "
|
|
||||||
"lossless codec. Compression settings "
|
|
||||||
"are applied to all pages, including those for which OCR was "
|
|
||||||
"skipped. Not supported for --output-type=pdf ; that setting "
|
|
||||||
"preserves the original compression of all images.",
|
|
||||||
)
|
|
||||||
advanced.add_argument(
|
advanced.add_argument(
|
||||||
'--fast-web-view',
|
'--fast-web-view',
|
||||||
type=numeric(float, 0),
|
type=numeric(float, 0),
|
||||||
|
|||||||
@@ -113,3 +113,27 @@ class TesseractConfigError(ExitCodeException):
|
|||||||
|
|
||||||
exit_code = ExitCode.invalid_config
|
exit_code = ExitCode.invalid_config
|
||||||
message = "Error occurred while parsing a Tesseract configuration file"
|
message = "Error occurred while parsing a Tesseract configuration file"
|
||||||
|
|
||||||
|
|
||||||
|
class DigitalSignatureError(InputFileError):
|
||||||
|
"""PDF has a digital signature."""
|
||||||
|
|
||||||
|
message = dedent(
|
||||||
|
"""\
|
||||||
|
Input PDF has a digital signature. OCR would alter the document,
|
||||||
|
invalidating the signature.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedPDFError(InputFileError):
|
||||||
|
"""PDF is tagged."""
|
||||||
|
|
||||||
|
message = dedent(
|
||||||
|
"""\
|
||||||
|
This PDF is marked as a Tagged PDF. This often indicates
|
||||||
|
that the PDF was generated from an office document and does
|
||||||
|
not need OCR. Use --force-ocr, --skip-text or --redo-ocr to
|
||||||
|
override this error.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ worker communicates only with the main process.
|
|||||||
|
|
||||||
This is not without drawbacks. If the tasks are not "even" in size, which cannot
|
This is not without drawbacks. If the tasks are not "even" in size, which cannot
|
||||||
be guaranteed, some workers may end up with too much work while others are idle.
|
be guaranteed, some workers may end up with too much work while others are idle.
|
||||||
It is less efficient than the standard implementation, so not th edefault.
|
It is less efficient than the standard implementation, so not the default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -20,12 +20,13 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import signal
|
import signal
|
||||||
|
from collections.abc import Iterable, Iterator
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from itertools import islice, repeat, takewhile, zip_longest
|
from itertools import islice, repeat, takewhile, zip_longest
|
||||||
from multiprocessing import Pipe, Process
|
from multiprocessing import Pipe, Process
|
||||||
from multiprocessing.connection import Connection, wait
|
from multiprocessing.connection import Connection, wait
|
||||||
from typing import Callable, Iterable, Iterator
|
from typing import Callable
|
||||||
|
|
||||||
from ocrmypdf import Executor, hookimpl
|
from ocrmypdf import Executor, hookimpl
|
||||||
from ocrmypdf._concurrent import NullProgressBar
|
from ocrmypdf._concurrent import NullProgressBar
|
||||||
|
|||||||
+56
-40
@@ -10,30 +10,29 @@ import multiprocessing
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from math import isclose, isfinite
|
from math import isclose, isfinite
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Generic, Sequence, SupportsFloat, SupportsRound, TypeVar
|
from statistics import harmonic_mean
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Generic,
|
||||||
|
TypeVar,
|
||||||
|
)
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
import pikepdf
|
import pikepdf
|
||||||
from packaging.version import Version
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
if Version(img2pdf.__version__) < Version('0.4.0'):
|
IMG2PDF_KWARGS = dict(engine=img2pdf.Engine.pikepdf, rotation=img2pdf.Rotation.ifvalid)
|
||||||
IMG2PDF_KWARGS = dict(without_pdfw=True)
|
|
||||||
elif Version(img2pdf.__version__) < Version('0.4.3'):
|
|
||||||
IMG2PDF_KWARGS = dict(engine=img2pdf.Engine.pikepdf)
|
|
||||||
else:
|
|
||||||
IMG2PDF_KWARGS = dict(
|
|
||||||
engine=img2pdf.Engine.pikepdf, rotation=img2pdf.Rotation.ifvalid
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T', bound=SupportsRound[Any])
|
T = TypeVar('T', float, int, Decimal)
|
||||||
|
|
||||||
|
|
||||||
class Resolution(Generic[T]):
|
class Resolution(Generic[T]):
|
||||||
@@ -77,21 +76,40 @@ class Resolution(Generic[T]):
|
|||||||
@property
|
@property
|
||||||
def is_finite(self) -> bool:
|
def is_finite(self) -> bool:
|
||||||
"""True if both x and y are finite numbers."""
|
"""True if both x and y are finite numbers."""
|
||||||
if isinstance(self.x, SupportsFloat) and isinstance(self.y, SupportsFloat):
|
return isfinite(self.x) and isfinite(self.y)
|
||||||
return isfinite(self.x) and isfinite(self.y)
|
|
||||||
return True
|
def to_scalar(self) -> float:
|
||||||
|
"""Return the harmonic mean of x and y as a 1D approximation.
|
||||||
|
|
||||||
|
In most cases, Resolution is 2D, but typically it is "square" (x == y) and
|
||||||
|
can be approximated as a single number. When not square, the harmonic mean
|
||||||
|
is used to approximate the 2D resolution as a single number.
|
||||||
|
"""
|
||||||
|
return harmonic_mean([float(self.x), float(self.y)])
|
||||||
|
|
||||||
|
def _take_minmax(
|
||||||
|
self, vals: Iterable[Any], yvals: Iterable[Any] | None, cmp: Callable
|
||||||
|
) -> Resolution:
|
||||||
|
"""Return a new Resolution object with the maximum resolution of inputs."""
|
||||||
|
if yvals is not None:
|
||||||
|
return Resolution(cmp(self.x, *vals), cmp(self.y, *yvals))
|
||||||
|
cmp_x, cmp_y = self.x, self.y
|
||||||
|
for x, y in vals:
|
||||||
|
cmp_x = cmp(x, cmp_x)
|
||||||
|
cmp_y = cmp(y, cmp_y)
|
||||||
|
return Resolution(cmp_x, cmp_y)
|
||||||
|
|
||||||
def take_max(
|
def take_max(
|
||||||
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
|
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
|
||||||
) -> Resolution:
|
) -> Resolution:
|
||||||
"""Return a new Resolution object with the maximum resolution of inputs."""
|
"""Return a new Resolution object with the maximum resolution of inputs."""
|
||||||
if yvals is not None:
|
return self._take_minmax(vals, yvals, max)
|
||||||
return Resolution(max(self.x, *vals), max(self.y, *yvals))
|
|
||||||
max_x, max_y = self.x, self.y
|
def take_min(
|
||||||
for x, y in vals:
|
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
|
||||||
max_x = max(x, max_x)
|
) -> Resolution:
|
||||||
max_y = max(y, max_y)
|
"""Return a new Resolution object with the minimum resolution of inputs."""
|
||||||
return Resolution(max_x, max_y)
|
return self._take_minmax(vals, yvals, min)
|
||||||
|
|
||||||
def flip_axis(self) -> Resolution[T]:
|
def flip_axis(self) -> Resolution[T]:
|
||||||
"""Return a new Resolution object with x and y swapped."""
|
"""Return a new Resolution object with x and y swapped."""
|
||||||
@@ -103,11 +121,11 @@ class Resolution(Generic[T]):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return a string representation of the resolution."""
|
"""Return a string representation of the resolution."""
|
||||||
return f"{self.x:f}x{self.y:f}"
|
return f"{self.x:f}×{self.y:f}"
|
||||||
|
|
||||||
def __repr__(self): # pragma: no cover
|
def __repr__(self): # pragma: no cover
|
||||||
"""Return a repr() of the resolution."""
|
"""Return a repr() of the resolution."""
|
||||||
return f"Resolution({self.x}x{self.y} dpi)"
|
return f"Resolution({self.x!r}, {self.y!r})"
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Return True if the resolution is equal to another resolution."""
|
"""Return True if the resolution is equal to another resolution."""
|
||||||
@@ -122,7 +140,7 @@ class NeverRaise(Exception):
|
|||||||
"""An exception that is never raised."""
|
"""An exception that is never raised."""
|
||||||
|
|
||||||
|
|
||||||
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
|
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike) -> None:
|
||||||
"""Create a symbolic link at ``soft_link_name``, which references ``input_file``.
|
"""Create a symbolic link at ``soft_link_name``, which references ``input_file``.
|
||||||
|
|
||||||
Think of this as copying ``input_file`` to ``soft_link_name`` with less overhead.
|
Think of this as copying ``input_file`` to ``soft_link_name`` with less overhead.
|
||||||
@@ -137,7 +155,7 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
|
|||||||
# Guard against soft linking to oneself
|
# Guard against soft linking to oneself
|
||||||
if input_file == soft_link_name:
|
if input_file == soft_link_name:
|
||||||
log.warning(
|
log.warning(
|
||||||
"No symbolic link created. You are using the original data directory "
|
"No symbolic link created. You are using the original data directory "
|
||||||
"as the working directory."
|
"as the working directory."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -163,7 +181,7 @@ def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike):
|
|||||||
os.symlink(os.path.abspath(input_file), soft_link_name)
|
os.symlink(os.path.abspath(input_file), soft_link_name)
|
||||||
|
|
||||||
|
|
||||||
def samefile(file1: os.PathLike, file2: os.PathLike):
|
def samefile(file1: os.PathLike, file2: os.PathLike) -> bool:
|
||||||
"""Return True if two files are the same file.
|
"""Return True if two files are the same file.
|
||||||
|
|
||||||
Attempts to account for different relative paths to the same file.
|
Attempts to account for different relative paths to the same file.
|
||||||
@@ -282,12 +300,12 @@ def check_pdf(input_file: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def clamp(n, smallest, largest): # mypy doesn't understand types for this
|
def clamp(n: T, smallest: T, largest: T) -> T:
|
||||||
"""Clamps the value of ``n`` to between ``smallest`` and ``largest``."""
|
"""Clamps the value of ``n`` to between ``smallest`` and ``largest``."""
|
||||||
return max(smallest, min(n, largest))
|
return max(smallest, min(n, largest))
|
||||||
|
|
||||||
|
|
||||||
def remove_all_log_handlers(logger):
|
def remove_all_log_handlers(logger: logging.Logger) -> None:
|
||||||
"""Remove all log handlers, usually used in a child process.
|
"""Remove all log handlers, usually used in a child process.
|
||||||
|
|
||||||
The child process inherits the log handlers from the parent process when
|
The child process inherits the log handlers from the parent process when
|
||||||
@@ -300,15 +318,13 @@ def remove_all_log_handlers(logger):
|
|||||||
handler.close() # To ensure handlers with opened resources are released
|
handler.close() # To ensure handlers with opened resources are released
|
||||||
|
|
||||||
|
|
||||||
def pikepdf_enable_mmap():
|
def pikepdf_enable_mmap() -> None:
|
||||||
"""Enable pikepdf mmap."""
|
"""Enable pikepdf mmap."""
|
||||||
# try:
|
try:
|
||||||
# if pikepdf._qpdf.set_access_default_mmap(True):
|
pikepdf._core.set_access_default_mmap(True)
|
||||||
# log.debug("pikepdf mmap enabled")
|
log.debug(
|
||||||
# except AttributeError:
|
"pikepdf mmap "
|
||||||
# log.debug("pikepdf mmap not available")
|
+ ('enabled' if pikepdf._core.get_access_default_mmap() else 'disabled')
|
||||||
# We found a race condition probably related to pybind issue #2252 that can
|
)
|
||||||
# cause a crash. For now, disable pikepdf mmap to be on the safe side.
|
except AttributeError:
|
||||||
# Fix is not in pybind11 2.6.0
|
log.debug("pikepdf mmap not available")
|
||||||
# log.debug("pikepdf mmap disabled")
|
|
||||||
return
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from xml.etree import ElementTree
|
|||||||
|
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
# reportlab uses deprecated load_module
|
# reportlab uses deprecated load_module
|
||||||
|
# shim can be removed when we require reportlab >= 3.7
|
||||||
warnings.filterwarnings(
|
warnings.filterwarnings(
|
||||||
'ignore', category=DeprecationWarning, message=r".*load_module.*"
|
'ignore', category=DeprecationWarning, message=r".*load_module.*"
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-22
@@ -6,20 +6,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from functools import singledispatch
|
|
||||||
from math import floor, sqrt
|
from math import floor, sqrt
|
||||||
from typing import Optional, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
# Remove this workaround when we require Pillow >= 9.1.0
|
|
||||||
try:
|
|
||||||
Resampling = Image.Resampling # type: ignore
|
|
||||||
except AttributeError:
|
|
||||||
# Pillow 9 shim
|
|
||||||
Resampling = Image # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# While from __future__ import annotations, we use singledispatch here, which
|
# While from __future__ import annotations, we use singledispatch here, which
|
||||||
# does not support annotations. Disable check about using old-style typing
|
# does not support annotations. Disable check about using old-style typing
|
||||||
# until Python 3.10, OR when drop singledispatch in ocrmypdf 15.
|
# until Python 3.10, OR when drop singledispatch in ocrmypdf 15.
|
||||||
@@ -43,15 +34,14 @@ def bytes_per_pixel(mode: str) -> int:
|
|||||||
return 4
|
return 4
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
def _calculate_downsample(
|
||||||
def calculate_downsample(
|
image_size: tuple[int, int],
|
||||||
image_size: Tuple[int, int],
|
|
||||||
bytes_per_pixel: int,
|
bytes_per_pixel: int,
|
||||||
*,
|
*,
|
||||||
max_size: Optional[Tuple[int, int]] = None,
|
max_size: Optional[tuple[int, int]] = None,
|
||||||
max_pixels: Optional[int] = None,
|
max_pixels: Optional[int] = None,
|
||||||
max_bytes: Optional[int] = None,
|
max_bytes: Optional[int] = None,
|
||||||
) -> Tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""Calculate image size required to downsample an image to fit limits.
|
"""Calculate image size required to downsample an image to fit limits.
|
||||||
|
|
||||||
If no limit is exceeded, the input image's size is returned.
|
If no limit is exceeded, the input image's size is returned.
|
||||||
@@ -105,15 +95,13 @@ def calculate_downsample(
|
|||||||
return size
|
return size
|
||||||
|
|
||||||
|
|
||||||
@calculate_downsample.register
|
def calculate_downsample(
|
||||||
def _(
|
|
||||||
image: Image.Image,
|
image: Image.Image,
|
||||||
arg: None = None,
|
|
||||||
*,
|
*,
|
||||||
max_size: Optional[Tuple[int, int]] = None,
|
max_size: Optional[tuple[int, int]] = None,
|
||||||
max_pixels: Optional[int] = None,
|
max_pixels: Optional[int] = None,
|
||||||
max_bytes: Optional[int] = None,
|
max_bytes: Optional[int] = None,
|
||||||
) -> Tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""Calculate image size required to downsample an image to fit limits.
|
"""Calculate image size required to downsample an image to fit limits.
|
||||||
|
|
||||||
If no limit is exceeded, the input image's size is returned.
|
If no limit is exceeded, the input image's size is returned.
|
||||||
@@ -126,7 +114,7 @@ def _(
|
|||||||
max_bytes: The maximum number of bytes in the image. RGB is counted as 4
|
max_bytes: The maximum number of bytes in the image. RGB is counted as 4
|
||||||
bytes; all other modes are counted as 1 byte.
|
bytes; all other modes are counted as 1 byte.
|
||||||
"""
|
"""
|
||||||
return calculate_downsample(
|
return _calculate_downsample(
|
||||||
image.size,
|
image.size,
|
||||||
bytes_per_pixel(image.mode),
|
bytes_per_pixel(image.mode),
|
||||||
max_size=max_size,
|
max_size=max_size,
|
||||||
@@ -139,7 +127,7 @@ def downsample_image(
|
|||||||
image: Image.Image,
|
image: Image.Image,
|
||||||
new_size: tuple[int, int],
|
new_size: tuple[int, int],
|
||||||
*,
|
*,
|
||||||
resample_mode: Image.Resampling = Resampling.BICUBIC,
|
resample_mode: Image.Resampling = Image.Resampling.BICUBIC,
|
||||||
reducing_gap: int = 3,
|
reducing_gap: int = 3,
|
||||||
) -> Image.Image:
|
) -> Image.Image:
|
||||||
"""Downsample an image to fit within the given limits.
|
"""Downsample an image to fit within the given limits.
|
||||||
|
|||||||
+51
-47
@@ -11,9 +11,10 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Iterator, MutableSet, Sequence
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Iterator, MutableSet, NamedTuple, NewType, Sequence
|
from typing import Callable, NamedTuple, NewType
|
||||||
from zlib import compress
|
from zlib import compress
|
||||||
|
|
||||||
import img2pdf
|
import img2pdf
|
||||||
@@ -68,10 +69,10 @@ def jpg_name(root: Path, xref: Xref) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def extract_image_filter(
|
def extract_image_filter(
|
||||||
pike: Pdf, root: Path, image: Stream, xref: Xref
|
pdf: Pdf, root: Path, image: Stream, xref: Xref
|
||||||
) -> tuple[PdfImage, tuple[Name, Object]] | None:
|
) -> tuple[PdfImage, tuple[Name, Object]] | None:
|
||||||
"""Determine if an image is extractable."""
|
"""Determine if an image is extractable."""
|
||||||
del pike # unused args
|
del pdf # unused args
|
||||||
del root
|
del root
|
||||||
|
|
||||||
if image.Subtype != Name.Image:
|
if image.Subtype != Name.Image:
|
||||||
@@ -126,12 +127,12 @@ def extract_image_filter(
|
|||||||
|
|
||||||
|
|
||||||
def extract_image_jbig2(
|
def extract_image_jbig2(
|
||||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
*, pdf: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||||
) -> XrefExt | None:
|
) -> XrefExt | None:
|
||||||
"""Extract an image, saving it as a JBIG2 file."""
|
"""Extract an image, saving it as a JBIG2 file."""
|
||||||
del options # unused arg
|
del options # unused arg
|
||||||
|
|
||||||
result = extract_image_filter(pike, root, image, xref)
|
result = extract_image_filter(pdf, root, image, xref)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
pim, filtdp = result
|
pim, filtdp = result
|
||||||
@@ -168,10 +169,10 @@ def extract_image_jbig2(
|
|||||||
|
|
||||||
|
|
||||||
def extract_image_generic(
|
def extract_image_generic(
|
||||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
*, pdf: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||||
) -> XrefExt | None:
|
) -> XrefExt | None:
|
||||||
"""Generic image extraction."""
|
"""Generic image extraction."""
|
||||||
result = extract_image_filter(pike, root, image, xref)
|
result = extract_image_filter(pdf, root, image, xref)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
pim, filtdp = result
|
pim, filtdp = result
|
||||||
@@ -239,7 +240,7 @@ def _find_image_xrefs_container(
|
|||||||
pageno_for_xref: dict[Xref, int],
|
pageno_for_xref: dict[Xref, int],
|
||||||
depth: int = 0,
|
depth: int = 0,
|
||||||
):
|
):
|
||||||
"""Find all image XRefs in a page or Form XObject and add to the include/exclude sets."""
|
"""Find all image XRefs or Form XObject and add to the include/exclude sets."""
|
||||||
if depth > 10:
|
if depth > 10:
|
||||||
log.warning("Recursion depth exceeded in _find_image_xrefs_page")
|
log.warning("Recursion depth exceeded in _find_image_xrefs_page")
|
||||||
return
|
return
|
||||||
@@ -290,7 +291,7 @@ def _find_image_xrefs(pdf: Pdf):
|
|||||||
|
|
||||||
|
|
||||||
def extract_images(
|
def extract_images(
|
||||||
pike: Pdf,
|
pdf: Pdf,
|
||||||
root: Path,
|
root: Path,
|
||||||
options,
|
options,
|
||||||
extract_fn: Callable[..., XrefExt | None],
|
extract_fn: Callable[..., XrefExt | None],
|
||||||
@@ -310,12 +311,12 @@ def extract_images(
|
|||||||
extension. extract_fn must also extract the file it finds interesting.
|
extension. extract_fn must also extract the file it finds interesting.
|
||||||
"""
|
"""
|
||||||
errors = 0
|
errors = 0
|
||||||
working_xrefs, pageno_for_xref = _find_image_xrefs(pike)
|
working_xrefs, pageno_for_xref = _find_image_xrefs(pdf)
|
||||||
for xref in working_xrefs:
|
for xref in working_xrefs:
|
||||||
image = pike.get_object((xref, 0))
|
image = pdf.get_object((xref, 0))
|
||||||
try:
|
try:
|
||||||
result = extract_fn(
|
result = extract_fn(
|
||||||
pike=pike, root=root, image=image, xref=xref, options=options
|
pdf=pdf, root=root, image=image, xref=xref, options=options
|
||||||
)
|
)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
log.exception(
|
log.exception(
|
||||||
@@ -329,12 +330,12 @@ def extract_images(
|
|||||||
|
|
||||||
|
|
||||||
def extract_images_generic(
|
def extract_images_generic(
|
||||||
pike: Pdf, root: Path, options
|
pdf: Pdf, root: Path, options
|
||||||
) -> tuple[list[Xref], list[Xref]]:
|
) -> tuple[list[Xref], list[Xref]]:
|
||||||
"""Extract any >=2bpp image we think we can improve."""
|
"""Extract any >=2bpp image we think we can improve."""
|
||||||
jpegs = []
|
jpegs = []
|
||||||
pngs = []
|
pngs = []
|
||||||
for _, xref_ext in extract_images(pike, root, options, extract_image_generic):
|
for _, xref_ext in extract_images(pdf, root, options, extract_image_generic):
|
||||||
log.debug('%s', xref_ext)
|
log.debug('%s', xref_ext)
|
||||||
if xref_ext.ext == '.png':
|
if xref_ext.ext == '.png':
|
||||||
pngs.append(xref_ext.xref)
|
pngs.append(xref_ext.xref)
|
||||||
@@ -344,10 +345,10 @@ def extract_images_generic(
|
|||||||
return jpegs, pngs
|
return jpegs, pngs
|
||||||
|
|
||||||
|
|
||||||
def extract_images_jbig2(pike: Pdf, root: Path, options) -> dict[int, list[XrefExt]]:
|
def extract_images_jbig2(pdf: Pdf, root: Path, options) -> dict[int, list[XrefExt]]:
|
||||||
"""Extract any bitonal image that we think we can improve as JBIG2."""
|
"""Extract any bitonal image that we think we can improve as JBIG2."""
|
||||||
jbig2_groups = defaultdict(list)
|
jbig2_groups = defaultdict(list)
|
||||||
for pageno, xref_ext in extract_images(pike, root, options, extract_image_jbig2):
|
for pageno, xref_ext in extract_images(pdf, root, options, extract_image_jbig2):
|
||||||
group = pageno // options.jbig2_page_group_size
|
group = pageno // options.jbig2_page_group_size
|
||||||
jbig2_groups[group].append(xref_ext)
|
jbig2_groups[group].append(xref_ext)
|
||||||
|
|
||||||
@@ -367,6 +368,7 @@ def _produce_jbig2_images(
|
|||||||
fspath(root), # =cwd
|
fspath(root), # =cwd
|
||||||
(img_name(root, xref, ext) for xref, ext in xref_exts), # =infiles
|
(img_name(root, xref, ext) for xref, ext in xref_exts), # =infiles
|
||||||
prefix, # =out_prefix
|
prefix, # =out_prefix
|
||||||
|
options.jbig2_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
|
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
|
||||||
@@ -379,6 +381,7 @@ def _produce_jbig2_images(
|
|||||||
fspath(root),
|
fspath(root),
|
||||||
img_name(root, xref, ext),
|
img_name(root, xref, ext),
|
||||||
root / f'{prefix}.{n:04d}',
|
root / f'{prefix}.{n:04d}',
|
||||||
|
options.jbig2_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
if options.jbig2_page_group_size > 1:
|
if options.jbig2_page_group_size > 1:
|
||||||
@@ -403,7 +406,7 @@ def _produce_jbig2_images(
|
|||||||
|
|
||||||
|
|
||||||
def convert_to_jbig2(
|
def convert_to_jbig2(
|
||||||
pike: Pdf,
|
pdf: Pdf,
|
||||||
jbig2_groups: dict[int, list[XrefExt]],
|
jbig2_groups: dict[int, list[XrefExt]],
|
||||||
root: Path,
|
root: Path,
|
||||||
options,
|
options,
|
||||||
@@ -430,7 +433,7 @@ def convert_to_jbig2(
|
|||||||
jbig2_symfile = root / (prefix + '.sym')
|
jbig2_symfile = root / (prefix + '.sym')
|
||||||
if jbig2_symfile.exists():
|
if jbig2_symfile.exists():
|
||||||
jbig2_globals_data = jbig2_symfile.read_bytes()
|
jbig2_globals_data = jbig2_symfile.read_bytes()
|
||||||
jbig2_globals = Stream(pike, jbig2_globals_data)
|
jbig2_globals = Stream(pdf, jbig2_globals_data)
|
||||||
jbig2_globals_dict = Dictionary(JBIG2Globals=jbig2_globals)
|
jbig2_globals_dict = Dictionary(JBIG2Globals=jbig2_globals)
|
||||||
elif options.jbig2_page_group_size == 1:
|
elif options.jbig2_page_group_size == 1:
|
||||||
jbig2_globals_dict = None
|
jbig2_globals_dict = None
|
||||||
@@ -441,7 +444,7 @@ def convert_to_jbig2(
|
|||||||
xref, _ = xref_ext
|
xref, _ = xref_ext
|
||||||
jbig2_im_file = root / (prefix + f'.{n:04d}')
|
jbig2_im_file = root / (prefix + f'.{n:04d}')
|
||||||
jbig2_im_data = jbig2_im_file.read_bytes()
|
jbig2_im_data = jbig2_im_file.read_bytes()
|
||||||
im_obj = pike.get_object(xref, 0)
|
im_obj = pdf.get_object(xref, 0)
|
||||||
im_obj.write(
|
im_obj.write(
|
||||||
jbig2_im_data, filter=Name.JBIG2Decode, decode_parms=jbig2_globals_dict
|
jbig2_im_data, filter=Name.JBIG2Decode, decode_parms=jbig2_globals_dict
|
||||||
)
|
)
|
||||||
@@ -461,7 +464,7 @@ def _optimize_jpeg(args: tuple[Xref, Path, Path, int]) -> tuple[Xref, Path | Non
|
|||||||
|
|
||||||
|
|
||||||
def transcode_jpegs(
|
def transcode_jpegs(
|
||||||
pike: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor
|
pdf: Pdf, jpegs: Sequence[Xref], root: Path, options, executor: Executor
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Optimize JPEGs according to optimization settings."""
|
"""Optimize JPEGs according to optimization settings."""
|
||||||
|
|
||||||
@@ -475,7 +478,7 @@ def transcode_jpegs(
|
|||||||
xref, opt_jpg = result
|
xref, opt_jpg = result
|
||||||
if opt_jpg:
|
if opt_jpg:
|
||||||
compdata = opt_jpg.read_bytes() # JPEG can inserted into PDF as is
|
compdata = opt_jpg.read_bytes() # JPEG can inserted into PDF as is
|
||||||
im_obj = pike.get_object(xref, 0)
|
im_obj = pdf.get_object(xref, 0)
|
||||||
im_obj.write(compdata, filter=Name.DCTDecode)
|
im_obj.write(compdata, filter=Name.DCTDecode)
|
||||||
pbar.update()
|
pbar.update()
|
||||||
|
|
||||||
@@ -495,9 +498,9 @@ def transcode_jpegs(
|
|||||||
|
|
||||||
|
|
||||||
def _find_deflatable_jpeg(
|
def _find_deflatable_jpeg(
|
||||||
*, pike: Pdf, root: Path, image: Stream, xref: Xref, options
|
*, pdf: Pdf, root: Path, image: Stream, xref: Xref, options
|
||||||
) -> XrefExt | None:
|
) -> XrefExt | None:
|
||||||
result = extract_image_filter(pike, root, image, xref)
|
result = extract_image_filter(pdf, root, image, xref)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
_pim, filtdp = result
|
_pim, filtdp = result
|
||||||
@@ -509,9 +512,9 @@ def _find_deflatable_jpeg(
|
|||||||
|
|
||||||
|
|
||||||
def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, bytes]:
|
def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, bytes]:
|
||||||
pike, lock, xref, complevel = args
|
pdf, lock, xref, complevel = args
|
||||||
with lock:
|
with lock:
|
||||||
xobj = pike.get_object(xref, 0)
|
xobj = pdf.get_object(xref, 0)
|
||||||
try:
|
try:
|
||||||
data = xobj.read_raw_bytes()
|
data = xobj.read_raw_bytes()
|
||||||
except PdfError:
|
except PdfError:
|
||||||
@@ -522,7 +525,7 @@ def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, by
|
|||||||
return xref, compdata
|
return xref, compdata
|
||||||
|
|
||||||
|
|
||||||
def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
|
def deflate_jpegs(pdf: Pdf, root: Path, options, executor: Executor) -> None:
|
||||||
"""Apply FlateDecode to JPEGs.
|
"""Apply FlateDecode to JPEGs.
|
||||||
|
|
||||||
This is a lossless compression method that is supported by all PDF viewers,
|
This is a lossless compression method that is supported by all PDF viewers,
|
||||||
@@ -530,7 +533,7 @@ def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
|
|||||||
images.
|
images.
|
||||||
"""
|
"""
|
||||||
jpegs = []
|
jpegs = []
|
||||||
for _pageno, xref_ext in extract_images(pike, root, options, _find_deflatable_jpeg):
|
for _pageno, xref_ext in extract_images(pdf, root, options, _find_deflatable_jpeg):
|
||||||
xref = xref_ext.xref
|
xref = xref_ext.xref
|
||||||
log.debug(f'xref {xref}: marking this JPEG as deflatable')
|
log.debug(f'xref {xref}: marking this JPEG as deflatable')
|
||||||
jpegs.append(xref)
|
jpegs.append(xref)
|
||||||
@@ -542,13 +545,13 @@ def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
|
|||||||
|
|
||||||
def deflate_args() -> Iterator:
|
def deflate_args() -> Iterator:
|
||||||
for xref in jpegs:
|
for xref in jpegs:
|
||||||
yield pike, lock, xref, complevel
|
yield pdf, lock, xref, complevel
|
||||||
|
|
||||||
def finish(result, pbar):
|
def finish(result, pbar):
|
||||||
xref, compdata = result
|
xref, compdata = result
|
||||||
if len(compdata) > 0:
|
if len(compdata) > 0:
|
||||||
with lock:
|
with lock:
|
||||||
xobj = pike.get_object(xref, 0)
|
xobj = pdf.get_object(xref, 0)
|
||||||
xobj.write(compdata, filter=[Name.FlateDecode, Name.DCTDecode])
|
xobj.write(compdata, filter=[Name.FlateDecode, Name.DCTDecode])
|
||||||
pbar.update()
|
pbar.update()
|
||||||
|
|
||||||
@@ -567,16 +570,16 @@ def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
|
def _transcode_png(pdf: Pdf, filename: Path, xref: Xref) -> bool:
|
||||||
output = filename.with_suffix('.png.pdf')
|
output = filename.with_suffix('.png.pdf')
|
||||||
with output.open('wb') as f:
|
with output.open('wb') as f:
|
||||||
img2pdf.convert(fspath(filename), outputstream=f, **IMG2PDF_KWARGS)
|
img2pdf.convert(fspath(filename), outputstream=f, **IMG2PDF_KWARGS)
|
||||||
|
|
||||||
with Pdf.open(output) as pdf_image:
|
with Pdf.open(output) as pdf_image:
|
||||||
foreign_image = next(iter(pdf_image.pages[0].images.values()))
|
foreign_image = next(iter(pdf_image.pages[0].images.values()))
|
||||||
local_image = pike.copy_foreign(foreign_image)
|
local_image = pdf.copy_foreign(foreign_image)
|
||||||
|
|
||||||
im_obj = pike.get_object(xref, 0)
|
im_obj = pdf.get_object(xref, 0)
|
||||||
im_obj.write(
|
im_obj.write(
|
||||||
local_image.read_raw_bytes(),
|
local_image.read_raw_bytes(),
|
||||||
filter=local_image.Filter,
|
filter=local_image.Filter,
|
||||||
@@ -609,7 +612,7 @@ def _transcode_png(pike: Pdf, filename: Path, xref: Xref) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def transcode_pngs(
|
def transcode_pngs(
|
||||||
pike: Pdf,
|
pdf: Pdf,
|
||||||
images: Sequence[Xref],
|
images: Sequence[Xref],
|
||||||
image_name_fn: Callable[[Path, Xref], Path],
|
image_name_fn: Callable[[Path, Xref], Path],
|
||||||
root: Path,
|
root: Path,
|
||||||
@@ -650,7 +653,7 @@ def transcode_pngs(
|
|||||||
|
|
||||||
for xref in modified:
|
for xref in modified:
|
||||||
filename = png_name(root, xref)
|
filename = png_name(root, xref)
|
||||||
_transcode_png(pike, filename, xref)
|
_transcode_png(pdf, filename, xref)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_EXECUTOR = SerialExecutor()
|
DEFAULT_EXECUTOR = SerialExecutor()
|
||||||
@@ -676,24 +679,24 @@ def optimize(
|
|||||||
if options.jbig2_page_group_size == 0:
|
if options.jbig2_page_group_size == 0:
|
||||||
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
|
options.jbig2_page_group_size = 10 if options.jbig2_lossy else 1
|
||||||
|
|
||||||
with Pdf.open(input_file) as pike:
|
with Pdf.open(input_file) as pdf:
|
||||||
root = output_file.parent / 'images'
|
root = output_file.parent / 'images'
|
||||||
root.mkdir(exist_ok=True)
|
root.mkdir(exist_ok=True)
|
||||||
|
|
||||||
jpegs, pngs = extract_images_generic(pike, root, options)
|
jpegs, pngs = extract_images_generic(pdf, root, options)
|
||||||
transcode_jpegs(pike, jpegs, root, options, executor)
|
transcode_jpegs(pdf, jpegs, root, options, executor)
|
||||||
deflate_jpegs(pike, root, options, executor)
|
deflate_jpegs(pdf, root, options, executor)
|
||||||
# if options.optimize >= 2:
|
# if options.optimize >= 2:
|
||||||
# Try pngifying the jpegs
|
# Try pngifying the jpegs
|
||||||
# transcode_pngs(pike, jpegs, jpg_name, root, options)
|
# transcode_pngs(pdf, jpegs, jpg_name, root, options)
|
||||||
transcode_pngs(pike, pngs, png_name, root, options, executor)
|
transcode_pngs(pdf, pngs, png_name, root, options, executor)
|
||||||
|
|
||||||
jbig2_groups = extract_images_jbig2(pike, root, options)
|
jbig2_groups = extract_images_jbig2(pdf, root, options)
|
||||||
convert_to_jbig2(pike, jbig2_groups, root, options, executor)
|
convert_to_jbig2(pdf, jbig2_groups, root, options, executor)
|
||||||
|
|
||||||
target_file = output_file.with_suffix('.opt.pdf')
|
target_file = output_file.with_suffix('.opt.pdf')
|
||||||
pike.remove_unreferenced_resources()
|
pdf.remove_unreferenced_resources()
|
||||||
pike.save(target_file, **save_settings)
|
pdf.save(target_file, **save_settings)
|
||||||
|
|
||||||
input_size = input_file.stat().st_size
|
input_size = input_file.stat().st_size
|
||||||
output_size = target_file.stat().st_size
|
output_size = target_file.stat().st_size
|
||||||
@@ -710,9 +713,9 @@ def optimize(
|
|||||||
"optimizations will not be used"
|
"optimizations will not be used"
|
||||||
)
|
)
|
||||||
# We still need to save the file
|
# We still need to save the file
|
||||||
with Pdf.open(input_file) as pike:
|
with Pdf.open(input_file) as pdf:
|
||||||
pike.remove_unreferenced_resources()
|
pdf.remove_unreferenced_resources()
|
||||||
pike.save(output_file, **save_settings)
|
pdf.save(output_file, **save_settings)
|
||||||
else:
|
else:
|
||||||
safe_symlink(target_file, output_file)
|
safe_symlink(target_file, output_file)
|
||||||
|
|
||||||
@@ -737,6 +740,7 @@ def main(infile, outfile, level, jobs=1):
|
|||||||
self.png_quality = png_quality
|
self.png_quality = png_quality
|
||||||
self.jbig2_page_group_size = 0
|
self.jbig2_page_group_size = 0
|
||||||
self.jbig2_lossy = jb2lossy
|
self.jbig2_lossy = jb2lossy
|
||||||
|
self.jbig2_threshold = 0.85
|
||||||
self.quiet = True
|
self.quiet = True
|
||||||
self.progress_bar = False
|
self.progress_bar = False
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from importlib.resources import files as package_files
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator
|
|
||||||
|
|
||||||
try:
|
|
||||||
from importlib.resources import files as package_files
|
|
||||||
except ImportError:
|
|
||||||
from importlib_resources import files as package_files # type: ignore
|
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
|
||||||
|
|||||||
+149
-41
@@ -9,7 +9,10 @@ from __future__ import annotations
|
|||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Container, Iterable, Iterator, Mapping, Sequence
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
@@ -17,11 +20,14 @@ from functools import partial
|
|||||||
from math import hypot, inf, isclose
|
from math import hypot, inf, isclose
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Container, Iterable, Iterator, Mapping, NamedTuple, Sequence, Tuple
|
from typing import Callable, NamedTuple
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
|
from pdfminer.layout import LTPage, LTTextBox
|
||||||
from pikepdf import (
|
from pikepdf import (
|
||||||
|
Name,
|
||||||
Object,
|
Object,
|
||||||
|
Page,
|
||||||
Pdf,
|
Pdf,
|
||||||
PdfImage,
|
PdfImage,
|
||||||
PdfInlineImage,
|
PdfInlineImage,
|
||||||
@@ -33,7 +39,7 @@ from pikepdf import (
|
|||||||
from ocrmypdf._concurrent import Executor, SerialExecutor
|
from ocrmypdf._concurrent import Executor, SerialExecutor
|
||||||
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
||||||
from ocrmypdf.helpers import Resolution, available_cpu_count, pikepdf_enable_mmap
|
from ocrmypdf.helpers import Resolution, available_cpu_count, pikepdf_enable_mmap
|
||||||
from ocrmypdf.pdfinfo.layout import get_page_analysis, get_text_boxes
|
from ocrmypdf.pdfinfo.layout import LTStateAwareChar, get_page_analysis, get_text_boxes
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -69,7 +75,7 @@ class Encoding(Enum):
|
|||||||
runlength = auto()
|
runlength = auto()
|
||||||
|
|
||||||
|
|
||||||
FloatRect = Tuple[float, float, float, float]
|
FloatRect = tuple[float, float, float, float]
|
||||||
|
|
||||||
FRIENDLY_COLORSPACE: dict[str, Colorspace] = {
|
FRIENDLY_COLORSPACE: dict[str, Colorspace] = {
|
||||||
'/DeviceGray': Colorspace.gray,
|
'/DeviceGray': Colorspace.gray,
|
||||||
@@ -418,12 +424,12 @@ class ImageInfo:
|
|||||||
return self._type
|
return self._type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width(self):
|
def width(self) -> int:
|
||||||
"""Width of the image in pixels."""
|
"""Width of the image in pixels."""
|
||||||
return self._width
|
return self._width
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def height(self):
|
def height(self) -> int:
|
||||||
"""Height of the image in pixels."""
|
"""Height of the image in pixels."""
|
||||||
return self._height
|
return self._height
|
||||||
|
|
||||||
@@ -456,17 +462,24 @@ class ImageInfo:
|
|||||||
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
|
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dpi(self):
|
def dpi(self) -> Resolution:
|
||||||
"""Dots per inch of the image.
|
"""Dots per inch of the image.
|
||||||
|
|
||||||
Calculated based on where and how the image is drawn in the PDF.
|
Calculated based on where and how the image is drawn in the PDF.
|
||||||
"""
|
"""
|
||||||
return _get_dpi(self._shorthand, (self._width, self._height))
|
return _get_dpi(self._shorthand, (self._width, self._height))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def printed_area(self) -> float:
|
||||||
|
"""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)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Return a string representation of the image."""
|
"""Return a string representation of the image."""
|
||||||
return (
|
return (
|
||||||
f"<ImageInfo '{self.name}' {self.type_} {self.width}x{self.height} "
|
f"<ImageInfo '{self.name}' {self.type_} {self.width}×{self.height} "
|
||||||
f"{self.color} {self.comp} {self.bpc} {self.enc} {self.dpi}>"
|
f"{self.color} {self.comp} {self.bpc} {self.enc} {self.dpi}>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -491,15 +504,15 @@ def _image_xobjects(container) -> Iterator[tuple[Object, str]]:
|
|||||||
since the object does not know its own name.
|
since the object does not know its own name.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if '/Resources' not in container:
|
if Name.Resources not in container:
|
||||||
return
|
return
|
||||||
resources = container['/Resources']
|
resources = container[Name.Resources]
|
||||||
if '/XObject' not in resources:
|
if Name.XObject not in resources:
|
||||||
return
|
return
|
||||||
for key, candidate in resources['/XObject'].items():
|
for key, candidate in resources[Name.XObject].items():
|
||||||
if candidate is None or '/Subtype' not in candidate:
|
if candidate is None or Name.Subtype not in candidate:
|
||||||
continue
|
continue
|
||||||
if candidate['/Subtype'] == '/Image':
|
if candidate[Name.Subtype] == Name.Image:
|
||||||
pdfimage = candidate
|
pdfimage = candidate
|
||||||
yield (pdfimage, key)
|
yield (pdfimage, key)
|
||||||
|
|
||||||
@@ -535,15 +548,15 @@ def _find_form_xobject_images(pdf: Pdf, container: Object, contentsinfo: Content
|
|||||||
The container may be a page, or a parent Form XObject.
|
The container may be a page, or a parent Form XObject.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if '/Resources' not in container:
|
if Name.Resources not in container:
|
||||||
return
|
return
|
||||||
resources = container['/Resources']
|
resources = container[Name.Resources]
|
||||||
if '/XObject' not in resources:
|
if Name.XObject not in resources:
|
||||||
return
|
return
|
||||||
xobjs = resources['/XObject'].as_dict()
|
xobjs = resources[Name.XObject].as_dict()
|
||||||
for xobj in xobjs:
|
for xobj in xobjs:
|
||||||
candidate = xobjs[xobj]
|
candidate = xobjs[xobj]
|
||||||
if candidate is None or candidate['/Subtype'] != '/Form':
|
if candidate is None or candidate[Name.Subtype] != Name.Form:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
form_xobject = candidate
|
form_xobject = candidate
|
||||||
@@ -581,16 +594,19 @@ def _process_content_streams(
|
|||||||
downsampling.
|
downsampling.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if container.get('/Type') == '/Page' and '/Contents' in container:
|
if container.get(Name.Type) == Name.Page and Name.Contents in container:
|
||||||
initial_shorthand = shorthand or UNIT_SQUARE
|
initial_shorthand = shorthand or UNIT_SQUARE
|
||||||
elif container.get('/Type') == '/XObject' and container['/Subtype'] == '/Form':
|
elif (
|
||||||
|
container.get(Name.Type) == Name.XObject
|
||||||
|
and container[Name.Subtype] == Name.Form
|
||||||
|
):
|
||||||
# Set the CTM to the state it was when the "Do" operator was
|
# Set the CTM to the state it was when the "Do" operator was
|
||||||
# encountered that is drawing this instance of the Form XObject
|
# encountered that is drawing this instance of the Form XObject
|
||||||
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
|
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
|
||||||
|
|
||||||
# A Form XObject may provide its own matrix to map form space into
|
# A Form XObject may provide its own matrix to map form space into
|
||||||
# user space. Get this if one exists
|
# user space. Get this if one exists
|
||||||
form_shorthand = container.get('/Matrix', PdfMatrix.identity())
|
form_shorthand = container.get(Name.Matrix, PdfMatrix.identity())
|
||||||
form_matrix = PdfMatrix(form_shorthand)
|
form_matrix = PdfMatrix(form_shorthand)
|
||||||
|
|
||||||
# Concatenate form matrix with CTM to ensure CTM is correct for
|
# Concatenate form matrix with CTM to ensure CTM is correct for
|
||||||
@@ -640,7 +656,9 @@ def _page_has_text(text_blocks: Iterable[FloatRect], page_width, page_height) ->
|
|||||||
return has_text
|
return has_text
|
||||||
|
|
||||||
|
|
||||||
def simplify_textboxes(miner, textbox_getter) -> Iterator[TextboxInfo]:
|
def simplify_textboxes(
|
||||||
|
miner: LTPage, textbox_getter: Callable[[LTPage], Iterator[LTTextBox]]
|
||||||
|
) -> Iterator[TextboxInfo]:
|
||||||
"""Extract only limited content from text boxes.
|
"""Extract only limited content from text boxes.
|
||||||
|
|
||||||
We do this to save memory and ensure that our objects are pickleable.
|
We do this to save memory and ensure that our objects are pickleable.
|
||||||
@@ -648,7 +666,8 @@ def simplify_textboxes(miner, textbox_getter) -> Iterator[TextboxInfo]:
|
|||||||
for box in textbox_getter(miner):
|
for box in textbox_getter(miner):
|
||||||
first_line = box._objs[0] # pylint: disable=protected-access
|
first_line = box._objs[0] # pylint: disable=protected-access
|
||||||
first_char = first_line._objs[0] # pylint: disable=protected-access
|
first_char = first_line._objs[0] # pylint: disable=protected-access
|
||||||
|
if not isinstance(first_char, LTStateAwareChar):
|
||||||
|
continue
|
||||||
visible = first_char.rendermode != 3
|
visible = first_char.rendermode != 3
|
||||||
corrupt = first_char.get_text() == '\ufffd'
|
corrupt = first_char.get_text() == '\ufffd'
|
||||||
yield TextboxInfo(box.bbox, visible, corrupt)
|
yield TextboxInfo(box.bbox, visible, corrupt)
|
||||||
@@ -742,12 +761,38 @@ def _pdf_pageinfo_concurrent(
|
|||||||
return pages
|
return pages
|
||||||
|
|
||||||
|
|
||||||
|
class PageResolutionProfile(NamedTuple):
|
||||||
|
"""Information about the resolutions of a page."""
|
||||||
|
|
||||||
|
weighted_dpi: float
|
||||||
|
"""The weighted average DPI of the page, weighted by the area of each image."""
|
||||||
|
|
||||||
|
max_dpi: float
|
||||||
|
"""The maximum DPI of an image on the page."""
|
||||||
|
|
||||||
|
average_to_max_dpi_ratio: float
|
||||||
|
"""The average DPI of the page divided by the maximum DPI of the page.
|
||||||
|
|
||||||
|
This indicates the intensity of the resolution variation on the page.
|
||||||
|
|
||||||
|
If the average is 1.0 or close to 1.0, has all of its content at a uniform
|
||||||
|
resolution. If the average is much lower than 1.0, some content is at a
|
||||||
|
higher resolution than the rest of the page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
area_ratio: float
|
||||||
|
"""The maximum-DPI area of the page divided by the total drawn area.
|
||||||
|
|
||||||
|
This indicates the prevalence of high-resolution content on the page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PageInfo:
|
class PageInfo:
|
||||||
"""Information about type of contents on each page in a PDF."""
|
"""Information about type of contents on each page in a PDF."""
|
||||||
|
|
||||||
_has_text: bool | None
|
_has_text: bool | None
|
||||||
_has_vector: bool | None
|
_has_vector: bool | None
|
||||||
_images: list[ImageInfo]
|
_images: list[ImageInfo] = []
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -771,7 +816,7 @@ class PageInfo:
|
|||||||
check_pages: Container[int],
|
check_pages: Container[int],
|
||||||
detailed_analysis: bool,
|
detailed_analysis: bool,
|
||||||
):
|
):
|
||||||
page = pdf.pages[pageno]
|
page: Page = pdf.pages[pageno]
|
||||||
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
|
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
|
||||||
width_pt = mediabox[2] - mediabox[0]
|
width_pt = mediabox[2] - mediabox[0]
|
||||||
height_pt = mediabox[3] - mediabox[1]
|
height_pt = mediabox[3] - mediabox[1]
|
||||||
@@ -779,9 +824,12 @@ class PageInfo:
|
|||||||
check_this_page = pageno in check_pages
|
check_this_page = pageno in check_pages
|
||||||
|
|
||||||
if check_this_page and detailed_analysis:
|
if check_this_page and detailed_analysis:
|
||||||
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
|
pscript5_mode = str(pdf.docinfo.get(Name.Creator)).startswith('PScript5')
|
||||||
miner = get_page_analysis(infile, pageno, pscript5_mode)
|
miner = get_page_analysis(infile, pageno, pscript5_mode)
|
||||||
self._textboxes = list(simplify_textboxes(miner, get_text_boxes))
|
if miner is not None:
|
||||||
|
self._textboxes = list(simplify_textboxes(miner, get_text_boxes))
|
||||||
|
else:
|
||||||
|
self._textboxes = []
|
||||||
bboxes = (box.bbox for box in self._textboxes)
|
bboxes = (box.bbox for box in self._textboxes)
|
||||||
|
|
||||||
self._has_text = _page_has_text(bboxes, width_pt, height_pt)
|
self._has_text = _page_has_text(bboxes, width_pt, height_pt)
|
||||||
@@ -789,17 +837,13 @@ class PageInfo:
|
|||||||
self._textboxes = []
|
self._textboxes = []
|
||||||
self._has_text = None # i.e. "no information"
|
self._has_text = None # i.e. "no information"
|
||||||
|
|
||||||
userunit = page.get('/UserUnit', Decimal(1.0))
|
userunit = page.get(Name.UserUnit, Decimal(1.0))
|
||||||
if not isinstance(userunit, Decimal):
|
if not isinstance(userunit, Decimal):
|
||||||
userunit = Decimal(userunit)
|
userunit = Decimal(userunit)
|
||||||
self._userunit = userunit
|
self._userunit = userunit
|
||||||
self._width_inches = width_pt * userunit / Decimal(72.0)
|
self._width_inches = width_pt * userunit / Decimal(72.0)
|
||||||
self._height_inches = height_pt * userunit / Decimal(72.0)
|
self._height_inches = height_pt * userunit / Decimal(72.0)
|
||||||
|
self._rotate = int(getattr(page.obj, 'Rotate', 0))
|
||||||
try:
|
|
||||||
self._rotate = int(page['/Rotate'])
|
|
||||||
except KeyError:
|
|
||||||
self._rotate = 0
|
|
||||||
|
|
||||||
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
|
userunit_shorthand = (userunit, 0, 0, userunit, 0, 0)
|
||||||
|
|
||||||
@@ -901,7 +945,9 @@ class PageInfo:
|
|||||||
def get_textareas(self, visible: bool | None = None, corrupt: bool | None = None):
|
def get_textareas(self, visible: bool | None = None, corrupt: bool | None = None):
|
||||||
"""Return textareas bounding boxes in PDF coordinates on the page."""
|
"""Return textareas bounding boxes in PDF coordinates on the page."""
|
||||||
|
|
||||||
def predicate(obj, want_visible, want_corrupt):
|
def predicate(
|
||||||
|
obj: TextboxInfo, want_visible: bool | None, want_corrupt: bool | None
|
||||||
|
) -> bool:
|
||||||
result = True
|
result = True
|
||||||
if want_visible is not None:
|
if want_visible is not None:
|
||||||
if obj.is_visible != want_visible:
|
if obj.is_visible != want_visible:
|
||||||
@@ -938,6 +984,47 @@ class PageInfo:
|
|||||||
else:
|
else:
|
||||||
return '1.5'
|
return '1.5'
|
||||||
|
|
||||||
|
def page_dpi_profile(self) -> PageResolutionProfile | None:
|
||||||
|
"""Return information about the DPIs of the page.
|
||||||
|
|
||||||
|
This is useful to detect pages with a small proportion of high-resolution
|
||||||
|
content that is forcing us to use a high DPI for the whole page. The ratio
|
||||||
|
is weighted by the area of each image. If images overlap, the overlapped
|
||||||
|
area counts.
|
||||||
|
|
||||||
|
Vector graphics and text are ignored.
|
||||||
|
|
||||||
|
Returns None if there is no meaningful DPI for the page.
|
||||||
|
"""
|
||||||
|
image_dpis = [
|
||||||
|
image.dpi.to_scalar() for image in self._images if image.renderable
|
||||||
|
]
|
||||||
|
image_areas = [image.printed_area for image in self._images if image.renderable]
|
||||||
|
total_drawn_area = sum(image_areas)
|
||||||
|
if total_drawn_area == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
max_dpi = max(image_dpis)
|
||||||
|
dpi_average_max_ratio = weighted_dpi / max_dpi
|
||||||
|
|
||||||
|
arg_max_dpi = image_dpis.index(max_dpi)
|
||||||
|
max_area_ratio = image_areas[arg_max_dpi] / total_drawn_area
|
||||||
|
|
||||||
|
return PageResolutionProfile(
|
||||||
|
weighted_dpi,
|
||||||
|
max_dpi,
|
||||||
|
dpi_average_max_ratio,
|
||||||
|
max_area_ratio,
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Return string representation."""
|
"""Return string representation."""
|
||||||
return (
|
return (
|
||||||
@@ -951,7 +1038,15 @@ DEFAULT_EXECUTOR = SerialExecutor()
|
|||||||
|
|
||||||
|
|
||||||
class PdfInfo:
|
class PdfInfo:
|
||||||
"""Get summary information about a PDF."""
|
"""Extract summary information about a PDF without retaining the PDF itself.
|
||||||
|
|
||||||
|
Crucially this lets us get the information in a pure Python format so that
|
||||||
|
it can be pickled and passed to a worker process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_has_acroform: bool = False
|
||||||
|
_has_signature: bool = False
|
||||||
|
_needs_rendering: bool = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -980,13 +1075,16 @@ class PdfInfo:
|
|||||||
check_pages=check_pages,
|
check_pages=check_pages,
|
||||||
detailed_analysis=detailed_analysis,
|
detailed_analysis=detailed_analysis,
|
||||||
)
|
)
|
||||||
self._needs_rendering = pdf.Root.get('/NeedsRendering', False)
|
self._needs_rendering = pdf.Root.get(Name.NeedsRendering, False)
|
||||||
self._has_acroform = False
|
if Name.AcroForm in pdf.Root:
|
||||||
if '/AcroForm' in pdf.Root:
|
if len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0:
|
||||||
if len(pdf.Root.AcroForm.get('/Fields', [])) > 0:
|
|
||||||
self._has_acroform = True
|
self._has_acroform = True
|
||||||
elif '/XFA' in pdf.Root.AcroForm:
|
elif Name.XFA in pdf.Root.AcroForm:
|
||||||
self._has_acroform = True
|
self._has_acroform = True
|
||||||
|
self._has_signature = bool(pdf.Root.AcroForm.get(Name.SigFlags, 0) & 1)
|
||||||
|
self._is_tagged = bool(
|
||||||
|
pdf.Root.get(Name.MarkInfo, {}).get(Name.Marked, False)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pages(self) -> Sequence[PageInfo | None]:
|
def pages(self) -> Sequence[PageInfo | None]:
|
||||||
@@ -1006,9 +1104,19 @@ class PdfInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_acroform(self) -> bool:
|
def has_acroform(self) -> bool:
|
||||||
"""Return True if any page has an AcroForm."""
|
"""Return True if the document catalog has an AcroForm."""
|
||||||
return self._has_acroform
|
return self._has_acroform
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_signature(self) -> bool:
|
||||||
|
"""Return True if the document annotations has a digital signature."""
|
||||||
|
return self._has_signature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_tagged(self) -> bool:
|
||||||
|
"""Return True if the document catalog indicates this is a Tagged PDF."""
|
||||||
|
return self._is_tagged
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self) -> str | Path:
|
def filename(self) -> str | Path:
|
||||||
"""Return filename of PDF."""
|
"""Return filename of PDF."""
|
||||||
|
|||||||
+106
-71
@@ -5,8 +5,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from contextlib import contextmanager
|
||||||
from math import copysign
|
from math import copysign
|
||||||
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterator
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pdfminer
|
import pdfminer
|
||||||
@@ -15,10 +19,13 @@ import pdfminer.pdfdevice
|
|||||||
import pdfminer.pdfinterp
|
import pdfminer.pdfinterp
|
||||||
from pdfminer.converter import PDFLayoutAnalyzer
|
from pdfminer.converter import PDFLayoutAnalyzer
|
||||||
from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox
|
from pdfminer.layout import LAParams, LTChar, LTPage, LTTextBox
|
||||||
|
from pdfminer.pdfcolor import PDFColorSpace
|
||||||
|
from pdfminer.pdfdevice import PDFTextSeq
|
||||||
from pdfminer.pdfdocument import PDFTextExtractionNotAllowed
|
from pdfminer.pdfdocument import PDFTextExtractionNotAllowed
|
||||||
from pdfminer.pdffont import PDFSimpleFont, PDFUnicodeNotDefined
|
from pdfminer.pdffont import FontWidthDict, PDFFont, PDFSimpleFont, PDFUnicodeNotDefined
|
||||||
|
from pdfminer.pdfinterp import PDFGraphicState, PDFResourceManager, PDFTextState
|
||||||
from pdfminer.pdfpage import PDFPage
|
from pdfminer.pdfpage import PDFPage
|
||||||
from pdfminer.utils import bbox2str, matrix2str
|
from pdfminer.utils import Matrix, bbox2str, matrix2str
|
||||||
|
|
||||||
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
from ocrmypdf.exceptions import EncryptedPdfError, InputFileError
|
||||||
|
|
||||||
@@ -28,7 +35,12 @@ STRIP_NAME = re.compile(r'[0-9]+')
|
|||||||
original_pdfsimplefont_init = PDFSimpleFont.__init__
|
original_pdfsimplefont_init = PDFSimpleFont.__init__
|
||||||
|
|
||||||
|
|
||||||
def pdfsimplefont__init__(self, descriptor, widths, spec):
|
def pdfsimplefont__init__(
|
||||||
|
self,
|
||||||
|
descriptor: Mapping[str, Any],
|
||||||
|
widths: FontWidthDict,
|
||||||
|
spec: Mapping[str, Any],
|
||||||
|
) -> None:
|
||||||
"""Monkeypatch pdfminer.six PDFSimpleFont.__init__.
|
"""Monkeypatch pdfminer.six PDFSimpleFont.__init__.
|
||||||
|
|
||||||
If there is no ToUnicode and no Encoding, pdfminer.six assumes that Unicode
|
If there is no ToUnicode and no Encoding, pdfminer.six assumes that Unicode
|
||||||
@@ -44,7 +56,7 @@ def pdfsimplefont__init__(self, descriptor, widths, spec):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
PDFSimpleFont.__init__ = pdfsimplefont__init__
|
setattr(PDFSimpleFont, '__init__', pdfsimplefont__init__)
|
||||||
|
|
||||||
#
|
#
|
||||||
# pdfminer patches when creator is PScript5.dll
|
# pdfminer patches when creator is PScript5.dll
|
||||||
@@ -85,6 +97,11 @@ def pdftype3font__pscript5_get_ascent(self):
|
|||||||
return self.ascent * copysign(1.0, self.vscale)
|
return self.ascent * copysign(1.0, self.vscale)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_undefined_char(s: str) -> bool:
|
||||||
|
"""Check if a string is an undefined character."""
|
||||||
|
return s.startswith('(cid:') and s.endswith(')')
|
||||||
|
|
||||||
|
|
||||||
class LTStateAwareChar(LTChar):
|
class LTStateAwareChar(LTChar):
|
||||||
"""A subclass of LTChar that tracks text render mode at time of drawing."""
|
"""A subclass of LTChar that tracks text render mode at time of drawing."""
|
||||||
|
|
||||||
@@ -107,18 +124,18 @@ class LTStateAwareChar(LTChar):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
matrix,
|
matrix: Matrix,
|
||||||
font,
|
font: PDFFont,
|
||||||
fontsize,
|
fontsize: float,
|
||||||
scaling,
|
scaling: float,
|
||||||
rise,
|
rise: float,
|
||||||
text,
|
text: str,
|
||||||
textwidth,
|
textwidth: float,
|
||||||
textdisp,
|
textdisp: float | tuple[float | None, float],
|
||||||
ncs,
|
ncs: PDFColorSpace,
|
||||||
graphicstate,
|
graphicstate: PDFGraphicState,
|
||||||
textstate,
|
textstate: PDFTextState,
|
||||||
):
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
matrix,
|
matrix,
|
||||||
@@ -134,7 +151,7 @@ class LTStateAwareChar(LTChar):
|
|||||||
)
|
)
|
||||||
self.rendermode = textstate.render
|
self.rendermode = textstate.render
|
||||||
|
|
||||||
def is_compatible(self, obj):
|
def is_compatible(self, obj: object) -> bool:
|
||||||
"""Check if characters can be combined into a textline.
|
"""Check if characters can be combined into a textline.
|
||||||
|
|
||||||
We consider characters compatible if:
|
We consider characters compatible if:
|
||||||
@@ -142,23 +159,22 @@ class LTStateAwareChar(LTChar):
|
|||||||
- the Unicode mapping is unknown but both are part of the same font
|
- the Unicode mapping is unknown but both are part of the same font
|
||||||
"""
|
"""
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
both_unicode_mapped = isinstance(self._text, str) and isinstance(obj._text, str)
|
if not isinstance(obj, LTStateAwareChar):
|
||||||
try:
|
|
||||||
if both_unicode_mapped:
|
|
||||||
return self.rendermode == obj.rendermode
|
|
||||||
font0, _ = self._text
|
|
||||||
font1, _ = obj._text
|
|
||||||
return font0 == font1 and self.rendermode == obj.rendermode
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
return False
|
return False
|
||||||
|
both_unicode_mapped = not _is_undefined_char(
|
||||||
|
self._text
|
||||||
|
) and not _is_undefined_char(obj._text)
|
||||||
|
if both_unicode_mapped:
|
||||||
|
return self.rendermode == obj.rendermode
|
||||||
|
return self.fontname == obj.fontname and self.rendermode == obj.rendermode
|
||||||
|
|
||||||
def get_text(self):
|
def get_text(self) -> str:
|
||||||
"""Get text from this character."""
|
"""Get text from this character."""
|
||||||
if isinstance(self._text, tuple):
|
if _is_undefined_char(self._text):
|
||||||
return '\ufffd' # standard 'Unknown symbol'
|
return '\ufffd' # standard 'Unknown symbol'
|
||||||
return self._text
|
return self._text
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
"""Return a string representation of this object."""
|
"""Return a string representation of this object."""
|
||||||
return (
|
return (
|
||||||
f"<{self.__class__.__name__} "
|
f"<{self.__class__.__name__} "
|
||||||
@@ -174,19 +190,24 @@ class LTStateAwareChar(LTChar):
|
|||||||
class TextPositionTracker(PDFLayoutAnalyzer):
|
class TextPositionTracker(PDFLayoutAnalyzer):
|
||||||
"""A page layout analyzer that pays attention to text visibility."""
|
"""A page layout analyzer that pays attention to text visibility."""
|
||||||
|
|
||||||
def __init__(self, rsrcmgr, pageno=1, laparams=None):
|
textstate: PDFTextState
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
rsrcmgr: PDFResourceManager,
|
||||||
|
pageno: int = 1,
|
||||||
|
laparams: LAParams | None = None,
|
||||||
|
):
|
||||||
"""Initialize the layout analyzer."""
|
"""Initialize the layout analyzer."""
|
||||||
super().__init__(rsrcmgr, pageno, laparams)
|
super().__init__(rsrcmgr, pageno, laparams)
|
||||||
self.textstate = None
|
self.result: LTPage | None = None
|
||||||
self.result = None
|
|
||||||
self.cur_item = None # not defined in pdfminer code as it should be
|
|
||||||
|
|
||||||
def begin_page(self, page, ctm):
|
def begin_page(self, page: PDFPage, ctm: Matrix) -> None:
|
||||||
"""Begin processing of a page."""
|
"""Begin processing of a page."""
|
||||||
super().begin_page(page, ctm)
|
super().begin_page(page, ctm)
|
||||||
self.cur_item = LTPage(self.pageno, page.mediabox)
|
self.cur_item = LTPage(self.pageno, page.mediabox)
|
||||||
|
|
||||||
def end_page(self, page):
|
def end_page(self, page: PDFPage) -> None:
|
||||||
"""End processing of a page."""
|
"""End processing of a page."""
|
||||||
assert not self._stack, str(len(self._stack))
|
assert not self._stack, str(len(self._stack))
|
||||||
assert isinstance(self.cur_item, LTPage), str(type(self.cur_item))
|
assert isinstance(self.cur_item, LTPage), str(type(self.cur_item))
|
||||||
@@ -195,14 +216,28 @@ class TextPositionTracker(PDFLayoutAnalyzer):
|
|||||||
self.pageno += 1
|
self.pageno += 1
|
||||||
self.receive_layout(self.cur_item)
|
self.receive_layout(self.cur_item)
|
||||||
|
|
||||||
def render_string(self, textstate, seq, ncs, graphicstate):
|
def render_string(
|
||||||
|
self,
|
||||||
|
textstate: PDFTextState,
|
||||||
|
seq: PDFTextSeq,
|
||||||
|
ncs: PDFColorSpace,
|
||||||
|
graphicstate: PDFGraphicState,
|
||||||
|
) -> None:
|
||||||
"""Respond to render string event by updating text state."""
|
"""Respond to render string event by updating text state."""
|
||||||
self.textstate = textstate.copy()
|
self.textstate = textstate.copy()
|
||||||
super().render_string(self.textstate, seq, ncs, graphicstate)
|
super().render_string(self.textstate, seq, ncs, graphicstate)
|
||||||
|
|
||||||
def render_char(
|
def render_char(
|
||||||
self, matrix, font, fontsize, scaling, rise, cid, ncs, graphicstate
|
self,
|
||||||
):
|
matrix: Matrix,
|
||||||
|
font: PDFFont,
|
||||||
|
fontsize: float,
|
||||||
|
scaling: float,
|
||||||
|
rise: float,
|
||||||
|
cid: int,
|
||||||
|
ncs: PDFColorSpace,
|
||||||
|
graphicstate: PDFGraphicState,
|
||||||
|
) -> float:
|
||||||
"""Respond to render char event by updating text state."""
|
"""Respond to render char event by updating text state."""
|
||||||
try:
|
try:
|
||||||
text = font.to_unichr(cid)
|
text = font.to_unichr(cid)
|
||||||
@@ -227,21 +262,34 @@ class TextPositionTracker(PDFLayoutAnalyzer):
|
|||||||
self.cur_item.add(item)
|
self.cur_item.add(item)
|
||||||
return item.adv
|
return item.adv
|
||||||
|
|
||||||
def handle_undefined_char(self, font, cid):
|
def receive_layout(self, ltpage: LTPage) -> None:
|
||||||
"""Handle undefined character."""
|
|
||||||
# log.info('undefined: %r, %r', font, cid)
|
|
||||||
return (font.fontname, cid)
|
|
||||||
|
|
||||||
def receive_layout(self, ltpage):
|
|
||||||
"""Receive layout handler."""
|
"""Receive layout handler."""
|
||||||
self.result = ltpage
|
self.result = ltpage
|
||||||
|
|
||||||
def get_result(self):
|
def get_result(self) -> LTPage | None:
|
||||||
"""Get the result of the analysis."""
|
"""Get the result of the analysis."""
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
def get_page_analysis(infile, pageno, pscript5_mode):
|
@contextmanager
|
||||||
|
def patch_pdfminer(pscript5_mode: bool):
|
||||||
|
"""Patch pdfminer.six to work around bugs in PDFs created by PScript5."""
|
||||||
|
if pscript5_mode:
|
||||||
|
with patch.multiple(
|
||||||
|
'pdfminer.pdffont.PDFType3Font',
|
||||||
|
spec=True,
|
||||||
|
get_ascent=pdftype3font__pscript5_get_ascent,
|
||||||
|
get_descent=pdftype3font__pscript5_get_descent,
|
||||||
|
get_height=pdftype3font__pscript5_get_height,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def get_page_analysis(
|
||||||
|
infile: PathLike, pageno: int, pscript5_mode: bool
|
||||||
|
) -> LTPage | None:
|
||||||
"""Get the page analysis for a given page."""
|
"""Get the page analysis for a given page."""
|
||||||
rman = pdfminer.pdfinterp.PDFResourceManager(caching=True)
|
rman = pdfminer.pdfinterp.PDFResourceManager(caching=True)
|
||||||
disable_boxes_flow = None
|
disable_boxes_flow = None
|
||||||
@@ -253,36 +301,23 @@ def get_page_analysis(infile, pageno, pscript5_mode):
|
|||||||
)
|
)
|
||||||
interp = pdfminer.pdfinterp.PDFPageInterpreter(rman, dev)
|
interp = pdfminer.pdfinterp.PDFPageInterpreter(rman, dev)
|
||||||
|
|
||||||
patcher = None
|
with patch_pdfminer(pscript5_mode):
|
||||||
if pscript5_mode:
|
try:
|
||||||
patcher = patch.multiple(
|
with Path(infile).open('rb') as f:
|
||||||
'pdfminer.pdffont.PDFType3Font',
|
page_iter = PDFPage.get_pages(f, pagenos=[pageno], maxpages=0)
|
||||||
spec=True,
|
page = next(page_iter, None)
|
||||||
get_ascent=pdftype3font__pscript5_get_ascent,
|
if page is None:
|
||||||
get_descent=pdftype3font__pscript5_get_descent,
|
raise InputFileError(
|
||||||
get_height=pdftype3font__pscript5_get_height,
|
f"pdfminer could not process page {pageno} (counting from 0)."
|
||||||
)
|
)
|
||||||
patcher.start()
|
interp.process_page(page)
|
||||||
|
except PDFTextExtractionNotAllowed as e:
|
||||||
try:
|
raise EncryptedPdfError() from e
|
||||||
with Path(infile).open('rb') as f:
|
|
||||||
page_iter = PDFPage.get_pages(f, pagenos=[pageno], maxpages=0)
|
|
||||||
page = next(page_iter, None)
|
|
||||||
if page is None:
|
|
||||||
raise InputFileError(
|
|
||||||
f"pdfminer could not process page {pageno} (counting from 0)."
|
|
||||||
)
|
|
||||||
interp.process_page(page)
|
|
||||||
except PDFTextExtractionNotAllowed as e:
|
|
||||||
raise EncryptedPdfError() from e
|
|
||||||
finally:
|
|
||||||
if patcher is not None:
|
|
||||||
patcher.stop()
|
|
||||||
|
|
||||||
return dev.get_result()
|
return dev.get_result()
|
||||||
|
|
||||||
|
|
||||||
def get_text_boxes(obj):
|
def get_text_boxes(obj) -> Iterator[LTTextBox]:
|
||||||
"""Get the text boxes attached to the current node."""
|
"""Get the text boxes attached to the current node."""
|
||||||
for child in obj:
|
for child in obj:
|
||||||
if isinstance(child, (LTTextBox)):
|
if isinstance(child, (LTTextBox)):
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from argparse import ArgumentParser, Namespace
|
from argparse import ArgumentParser, Namespace
|
||||||
|
from collections.abc import Sequence, Set
|
||||||
from logging import Handler
|
from logging import Handler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, AbstractSet, NamedTuple, Sequence
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
@@ -351,7 +352,6 @@ def filter_pdf_page(page: PageContext, image_filename: Path, output_pdf: Path) -
|
|||||||
This hook will be called from child processes. Modifying global state
|
This hook will be called from child processes. Modifying global state
|
||||||
will not affect the main process or other child processes.
|
will not affect the main process or other child processes.
|
||||||
|
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
This is a :ref:`firstresult hook<firstresult>`.
|
This is a :ref:`firstresult hook<firstresult>`.
|
||||||
"""
|
"""
|
||||||
@@ -406,7 +406,7 @@ class OcrEngine(ABC):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def languages(options: Namespace) -> AbstractSet[str]:
|
def languages(options: Namespace) -> Set[str]:
|
||||||
"""Returns the set of all languages that are supported by the engine.
|
"""Returns the set of all languages that are supported by the engine.
|
||||||
|
|
||||||
Languages are typically given in 3-letter ISO 3166-1 codes, but actually
|
Languages are typically given in 3-letter ISO 3166-1 codes, but actually
|
||||||
@@ -466,7 +466,7 @@ def generate_pdfa(
|
|||||||
pdf_pages: list[Path],
|
pdf_pages: list[Path],
|
||||||
pdfmark: Path,
|
pdfmark: Path,
|
||||||
output_file: Path,
|
output_file: Path,
|
||||||
compression: str,
|
context: PdfContext,
|
||||||
pdf_version: str,
|
pdf_version: str,
|
||||||
pdfa_part: str,
|
pdfa_part: str,
|
||||||
progressbar_class,
|
progressbar_class,
|
||||||
@@ -484,11 +484,7 @@ def generate_pdfa(
|
|||||||
pdfmark: A PostScript file intended for Ghostscript with details on
|
pdfmark: A PostScript file intended for Ghostscript with details on
|
||||||
how to perform the PDF/A conversion.
|
how to perform the PDF/A conversion.
|
||||||
output_file: The name of the desired output file.
|
output_file: The name of the desired output file.
|
||||||
compression: One of ``'jpeg'``, ``'lossless'``, ``''``. For ``'jpeg'``,
|
context: The current context.
|
||||||
the PDF/A generator should convert all images to JPEG encoding where
|
|
||||||
possible. For lossless, all images should be converted to FlateEncode
|
|
||||||
(lossless PNG). If an empty string, the PDF generator should make its
|
|
||||||
own decisions about how to encode images.
|
|
||||||
pdf_version: The minimum PDF version that the output file should be.
|
pdf_version: The minimum PDF version that the output file should be.
|
||||||
At its own discretion, the PDF/A generator may raise the version,
|
At its own discretion, the PDF/A generator may raise the version,
|
||||||
but should not lower it.
|
but should not lower it.
|
||||||
@@ -514,6 +510,11 @@ def generate_pdfa(
|
|||||||
Note:
|
Note:
|
||||||
This is a :ref:`firstresult hook<firstresult>`.
|
This is a :ref:`firstresult hook<firstresult>`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Before version 15.0.0, the ``context`` was not provided and ``compression``
|
||||||
|
was provided instead. Plugins should now read the context object to determine
|
||||||
|
if compression is requested.
|
||||||
|
|
||||||
See Also:
|
See Also:
|
||||||
https://github.com/tqdm/tqdm
|
https://github.com/tqdm/tqdm
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
|
||||||
class OcrQualityDictionary:
|
class OcrQualityDictionary:
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
||||||
from subprocess import run as subprocess_run
|
from subprocess import run as subprocess_run
|
||||||
from typing import Callable, Mapping, Sequence, Union
|
from typing import Callable, Union
|
||||||
|
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
|
||||||
@@ -292,21 +293,12 @@ def _error_old_version(
|
|||||||
_error_trailer(**locals())
|
_error_trailer(**locals())
|
||||||
|
|
||||||
|
|
||||||
def _remove_leading_v(s: str) -> str:
|
|
||||||
if sys.version_info >= (3, 9):
|
|
||||||
return s.removeprefix('v')
|
|
||||||
|
|
||||||
if s.startswith('v'):
|
|
||||||
return s[1:]
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def check_external_program(
|
def check_external_program(
|
||||||
*,
|
*,
|
||||||
program: str,
|
program: str,
|
||||||
package: str,
|
package: str,
|
||||||
version_checker: Callable[[], str],
|
version_checker: Callable[[], Version],
|
||||||
need_version: str,
|
need_version: str | Version,
|
||||||
required_for: str | None = None,
|
required_for: str | None = None,
|
||||||
recommended: bool = False,
|
recommended: bool = False,
|
||||||
version_parser: type[Version] = Version,
|
version_parser: type[Version] = Version,
|
||||||
@@ -326,6 +318,8 @@ def check_external_program(
|
|||||||
version_parser: A class that should be used to parse and compare version
|
version_parser: A class that should be used to parse and compare version
|
||||||
numbers. Used when version numbers do not follow standard conventions.
|
numbers. Used when version numbers do not follow standard conventions.
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(need_version, Version):
|
||||||
|
need_version = version_parser(need_version)
|
||||||
try:
|
try:
|
||||||
found_version = version_checker()
|
found_version = version_checker()
|
||||||
except (CalledProcessError, FileNotFoundError) as e:
|
except (CalledProcessError, FileNotFoundError) as e:
|
||||||
@@ -339,11 +333,10 @@ def check_external_program(
|
|||||||
raise
|
raise
|
||||||
return
|
return
|
||||||
|
|
||||||
found_version = _remove_leading_v(found_version)
|
if found_version and found_version < need_version:
|
||||||
need_version = _remove_leading_v(need_version)
|
_error_old_version(
|
||||||
|
program, package, str(need_version), str(found_version), required_for
|
||||||
if found_version and version_parser(found_version) < version_parser(need_version):
|
)
|
||||||
_error_old_version(program, package, need_version, found_version, required_for)
|
|
||||||
if not recommended:
|
if not recommended:
|
||||||
raise MissingDependencyError(program)
|
raise MissingDependencyError(program)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterable, Iterator
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Iterable, Iterator, TypeVar
|
from typing import Any, Callable, TypeVar
|
||||||
|
|
||||||
from packaging.version import InvalidVersion, Version
|
from packaging.version import InvalidVersion, Version
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
+1
-4
@@ -1,7 +1,5 @@
|
|||||||
i a la Waterman
|
i a la Waterman
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
4 ons linzen
|
4 ons linzen
|
||||||
|
|
||||||
3 liter water
|
3 liter water
|
||||||
@@ -17,11 +15,10 @@ laurier, kruidnagel, kerrie, zout
|
|||||||
De linzgen wassen en in-l liter kokend wa-
|
De linzgen wassen en in-l liter kokend wa-
|
||||||
ter 1 dag laten weken, 2 liter water bij
|
ter 1 dag laten weken, 2 liter water bij
|
||||||
de linzen voegen, zonder het water waarin
|
de linzen voegen, zonder het water waarin
|
||||||
ze geweekt zijn af te gieten., De helft van
|
ze geweekt zijn af te gieten, De helft van
|
||||||
de uien bakken met laurier en Kruidnagel.
|
de uien bakken met laurier en Kruidnagel.
|
||||||
Alle uien, kerrie en zgout bij de linzen
|
Alle uien, kerrie en zgout bij de linzen
|
||||||
voegen, Alles aan de kook brengen, Van de
|
voegen, Alles aan de kook brengen, Van de
|
||||||
bloem met boter en melk een papje maken en
|
bloem met boter en melk een papje maken en
|
||||||
verder afmaken met de soep, Als de linzen
|
verder afmaken met de soep, Als de linzen
|
||||||
gfgaar Zijn is de soep klaar.
|
gfgaar Zijn is de soep klaar.
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-15
@@ -1,25 +1,11 @@
|
|||||||
Tarnose
|
Tarnose
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Bokale oa
|
Bokale oa
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Lehuntze
|
Lehuntze
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Mugerre
|
Mugerre
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Milafranga Komunikabideak
|
Milafranga Komunikabideak
|
||||||
|
|
||||||
BAIONA zeiteninsiie —
|
BAIONA zeiteninsiie —
|
||||||
@@ -27,4 +13,3 @@ BAIONA zeiteninsiie —
|
|||||||
7 Trenbideak -----
|
7 Trenbideak -----
|
||||||
|
|
||||||
t\ Basusarri — spmeans:20141004 ae: . _ ~
|
t\ Basusarri — spmeans:20141004 ae: . _ ~
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -1,2 +1 @@
|
|||||||
Covfefe is a perfectly cromulent word.
|
Covfefe is a perfectly cromulent word.
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
+1
-4
@@ -1,7 +1,5 @@
|
|||||||
Linzensoep a la Waterman
|
Linzensoep a la Waterman
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
4 ons linzen
|
4 ons linzen
|
||||||
|
|
||||||
3 liter water
|
3 liter water
|
||||||
@@ -17,11 +15,10 @@ laurier, kruidnagel, kerrie, zout
|
|||||||
De linzgen wassen en in-l liter kokend wa-
|
De linzgen wassen en in-l liter kokend wa-
|
||||||
ter 1 dag laten weken, 2 liter water bij
|
ter 1 dag laten weken, 2 liter water bij
|
||||||
de linzen voegen, zonder het water waarin
|
de linzen voegen, zonder het water waarin
|
||||||
ze geweekt zijn af te gieten., De helft van
|
ze geweekt zijn af te gieten, De helft van
|
||||||
de uien bakken met laurier en Kruidnagel.
|
de uien bakken met laurier en Kruidnagel.
|
||||||
Alle uien, kerrie en zgout bij de linzen
|
Alle uien, kerrie en zgout bij de linzen
|
||||||
voegen, Alles aan de kook brengen, Van de
|
voegen, Alles aan de kook brengen, Van de
|
||||||
bloem met boter en melk een papje maken en
|
bloem met boter en melk een papje maken en
|
||||||
verder afmaken met de soep, Als de linzen
|
verder afmaken met de soep, Als de linzen
|
||||||
gfgaar Zijn is de soep klaar.
|
gfgaar Zijn is de soep klaar.
|
||||||
|
|
||||||
+11
-17
@@ -5,27 +5,21 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||||
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
|
<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'/>
|
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.gzrr1v_b/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0'>
|
<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_carea' id='block_1_1' title="bbox 296 96 704 504">
|
<div class='ocr_photo' id='block_1_1' title="bbox 296 96 704 504"></div>
|
||||||
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 296 96 704 504">
|
|
||||||
<span class='ocr_line' id='line_1_1' title="bbox 296 96 704 504; baseline 0 296; x_size 169.33333; x_descenders 42.333332; x_ascenders 42.333336">
|
|
||||||
<span class='ocrx_word' id='word_1_1' title='bbox 296 96 704 504; x_wconf 95'> </span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
|
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
|
||||||
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 150 592 841 622">
|
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 150 592 841 622">
|
||||||
<span class='ocr_line' id='line_1_2' title="bbox 150 592 841 622; baseline 0 -6; x_size 30; x_descenders 6; x_ascenders 8">
|
<span class='ocr_line' id='line_1_1' title="bbox 150 592 841 622; baseline 0 -5; x_size 30; x_descenders 5; x_ascenders 7">
|
||||||
<span class='ocrx_word' id='word_1_2' title='bbox 150 592 230 616; x_wconf 95'>This</span>
|
<span class='ocrx_word' id='word_1_1' title='bbox 150 592 230 617; x_wconf 96'>This</span>
|
||||||
<span class='ocrx_word' id='word_1_3' title='bbox 260 592 384 616; x_wconf 95'>should</span>
|
<span class='ocrx_word' id='word_1_2' title='bbox 260 592 384 617; x_wconf 96'>should</span>
|
||||||
<span class='ocrx_word' id='word_1_4' title='bbox 413 592 449 616; x_wconf 95'>be</span>
|
<span class='ocrx_word' id='word_1_3' title='bbox 413 592 449 617; x_wconf 95'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_5' title='bbox 479 600 493 616; x_wconf 94'>a</span>
|
<span class='ocrx_word' id='word_1_4' title='bbox 479 599 493 617; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_6' title='bbox 523 592 668 622; x_wconf 94'>perfect</span>
|
<span class='ocrx_word' id='word_1_5' title='bbox 523 592 668 622; x_wconf 95'>perfect</span>
|
||||||
<span class='ocrx_word' id='word_1_7' title='bbox 698 592 841 616; x_wconf 61'>circle:</span>
|
<span class='ocrx_word' id='word_1_6' title='bbox 698 592 841 617; x_wconf 25'>circle.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
+1
-4
@@ -1,4 +1 @@
|
|||||||
|
This should be a perfect circle.
|
||||||
|
|
||||||
This should be a perfect circle:
|
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
+1
-4
@@ -1,4 +1 @@
|
|||||||
|
This should be a perfect circle.
|
||||||
|
|
||||||
This should be a perfect circle:
|
|
||||||
|
|
||||||
|
|||||||
+31
-31
@@ -5,11 +5,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||||
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
|
<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'/>
|
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.8je4vgpg/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
|
<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_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
<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">
|
<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">
|
<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">
|
||||||
@@ -136,9 +136,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
||||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
||||||
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
|
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
|
||||||
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
|
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
|
||||||
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
|
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
|
||||||
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
||||||
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
||||||
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
||||||
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
||||||
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
||||||
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
|
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,13 +165,13 @@
|
|||||||
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
||||||
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
||||||
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
|
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
|
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
|
||||||
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
||||||
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
|
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
||||||
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
||||||
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
||||||
@@ -256,8 +256,8 @@
|
|||||||
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
||||||
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
||||||
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
||||||
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
|
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
|
||||||
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
||||||
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
||||||
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
||||||
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
|
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
|
||||||
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
|
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
|
||||||
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
||||||
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
||||||
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
|
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
|
||||||
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
||||||
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
||||||
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
||||||
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
||||||
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
|
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
||||||
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
|
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,8 +668,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
||||||
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
||||||
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
||||||
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
|
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
|
||||||
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
|
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
||||||
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
|
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
|
||||||
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
||||||
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
||||||
@@ -829,9 +829,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
||||||
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
||||||
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
|
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
||||||
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
|
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
|
||||||
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
||||||
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
||||||
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
||||||
@@ -912,12 +912,12 @@
|
|||||||
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
||||||
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
||||||
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
|
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
|
||||||
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
|
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
|
||||||
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
||||||
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
||||||
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
||||||
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
|
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
|
||||||
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
||||||
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
||||||
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
||||||
@@ -928,12 +928,12 @@
|
|||||||
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
|
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
||||||
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
|
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
||||||
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
||||||
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
||||||
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
|
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
||||||
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
|
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
||||||
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
||||||
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
||||||
@@ -1022,8 +1022,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
||||||
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
|
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
|
||||||
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
+31
-31
@@ -5,11 +5,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||||
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
|
<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'/>
|
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.8je4vgpg/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
|
<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_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
<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">
|
<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">
|
<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">
|
||||||
@@ -136,9 +136,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
||||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
||||||
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
|
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
|
||||||
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
|
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
|
||||||
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
|
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
|
||||||
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
||||||
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
||||||
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
||||||
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
||||||
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
||||||
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
|
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,13 +165,13 @@
|
|||||||
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
||||||
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
||||||
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
|
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
|
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
|
||||||
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
||||||
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
|
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
||||||
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
||||||
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
||||||
@@ -256,8 +256,8 @@
|
|||||||
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
||||||
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
||||||
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
||||||
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
|
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
|
||||||
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
||||||
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
||||||
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
||||||
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
|
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
|
||||||
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
|
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
|
||||||
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
||||||
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
||||||
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
|
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
|
||||||
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
||||||
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
||||||
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
||||||
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
||||||
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
|
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
||||||
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
|
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,8 +668,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
||||||
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
||||||
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
||||||
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
|
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
|
||||||
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
|
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
||||||
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
|
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
|
||||||
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
||||||
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
||||||
@@ -829,9 +829,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
||||||
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
||||||
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
|
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
||||||
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
|
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
|
||||||
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
||||||
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
||||||
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
||||||
@@ -912,12 +912,12 @@
|
|||||||
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
||||||
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
||||||
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
|
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
|
||||||
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
|
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
|
||||||
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
||||||
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
||||||
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
||||||
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
|
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
|
||||||
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
||||||
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
||||||
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
||||||
@@ -928,12 +928,12 @@
|
|||||||
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
|
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
||||||
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
|
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
||||||
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
||||||
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
||||||
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
|
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
||||||
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
|
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
||||||
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
||||||
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
||||||
@@ -1022,8 +1022,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
||||||
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
|
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
|
||||||
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
+31
-31
@@ -5,11 +5,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||||
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
|
<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'/>
|
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.8je4vgpg/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
|
<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_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
<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">
|
<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">
|
<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">
|
||||||
@@ -136,9 +136,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
||||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
||||||
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
|
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
|
||||||
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
|
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
|
||||||
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
|
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
|
||||||
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
||||||
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
||||||
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
||||||
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
||||||
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
||||||
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
|
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,13 +165,13 @@
|
|||||||
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
||||||
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
||||||
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
|
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
|
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
|
||||||
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
||||||
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
|
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
||||||
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
||||||
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
||||||
@@ -256,8 +256,8 @@
|
|||||||
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
||||||
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
||||||
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
||||||
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
|
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
|
||||||
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
||||||
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
||||||
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
||||||
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
|
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
|
||||||
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
|
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
|
||||||
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
||||||
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
||||||
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
|
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
|
||||||
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
||||||
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
||||||
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
||||||
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
||||||
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
|
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
||||||
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
|
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,8 +668,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
||||||
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
||||||
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
||||||
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
|
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
|
||||||
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
|
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
||||||
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
|
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
|
||||||
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
||||||
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
||||||
@@ -829,9 +829,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
||||||
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
||||||
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
|
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
||||||
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
|
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
|
||||||
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
||||||
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
||||||
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
||||||
@@ -912,12 +912,12 @@
|
|||||||
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
||||||
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
||||||
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
|
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
|
||||||
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
|
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
|
||||||
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
||||||
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
||||||
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
||||||
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
|
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
|
||||||
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
||||||
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
||||||
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
||||||
@@ -928,12 +928,12 @@
|
|||||||
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
|
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
||||||
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
|
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
||||||
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
||||||
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
||||||
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
|
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
||||||
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
|
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
||||||
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
||||||
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
||||||
@@ -1022,8 +1022,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
||||||
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
|
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
|
||||||
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
+31
-31
@@ -5,11 +5,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||||
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
|
<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'/>
|
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.8je4vgpg/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
|
<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_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
<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">
|
<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">
|
<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">
|
||||||
@@ -136,9 +136,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
||||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
||||||
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
|
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
|
||||||
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
|
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
|
||||||
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
|
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
|
||||||
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
||||||
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
||||||
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
||||||
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
||||||
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
||||||
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
|
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,13 +165,13 @@
|
|||||||
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
||||||
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
||||||
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
|
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
|
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
|
||||||
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
||||||
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
|
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
||||||
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
||||||
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
||||||
@@ -256,8 +256,8 @@
|
|||||||
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
||||||
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
||||||
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
||||||
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
|
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
|
||||||
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
||||||
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
||||||
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
||||||
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
|
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
|
||||||
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
|
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
|
||||||
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
||||||
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
||||||
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
|
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
|
||||||
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
||||||
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
||||||
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
||||||
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
||||||
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
|
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
||||||
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
|
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,8 +668,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
||||||
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
||||||
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
||||||
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
|
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
|
||||||
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
|
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
||||||
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
|
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
|
||||||
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
||||||
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
||||||
@@ -829,9 +829,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
||||||
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
||||||
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
|
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
||||||
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
|
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
|
||||||
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
||||||
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
||||||
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
||||||
@@ -912,12 +912,12 @@
|
|||||||
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
||||||
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
||||||
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 47'>®</span>
|
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
|
||||||
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 96'>Utilizes</span>
|
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
|
||||||
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
||||||
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
||||||
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
||||||
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 94'>MHz</span>
|
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
|
||||||
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
<span class='ocrx_word' id='word_1_636' title='bbox 873 2649 954 2676; x_wconf 95'>80186</span>
|
||||||
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
<span class='ocrx_word' id='word_1_637' title='bbox 965 2649 994 2676; x_wconf 96'>16</span>
|
||||||
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
<span class='ocrx_word' id='word_1_638' title='bbox 1004 2648 1043 2676; x_wconf 96'>bit</span>
|
||||||
@@ -928,12 +928,12 @@
|
|||||||
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
|
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
||||||
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
|
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
||||||
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
||||||
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
||||||
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
|
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
||||||
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
|
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
||||||
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
||||||
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
||||||
@@ -1022,8 +1022,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
||||||
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
|
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
|
||||||
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
-1
@@ -120,4 +120,3 @@ Linn Electronics, Inc.
|
|||||||
|
|
||||||
18720 Oxnard Street, Tarzana, CA 91356
|
18720 Oxnard Street, Tarzana, CA 91356
|
||||||
(818) 708-8131 TELEX #298949 LINN UR
|
(818) 708-8131 TELEX #298949 LINN UR
|
||||||
|
|
||||||
+59
-59
@@ -5,11 +5,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
|
||||||
<meta name='ocr-system' content='tesseract 5.0.0-beta-20210916-12-g19cc9' />
|
<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'/>
|
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.rvbrparf/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0'>
|
<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_carea' id='block_1_1' title="bbox 582 131 1968 303">
|
<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">
|
<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">
|
<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">
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 347 380 2188 423">
|
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 347 380 2188 423">
|
||||||
<span class='ocr_header' id='line_1_3' title="bbox 347 380 2188 423; baseline -0.001 -12; x_size 38; x_descenders 8; x_ascenders 10">
|
<span class='ocr_header' id='line_1_3' title="bbox 347 380 2188 423; baseline -0.001 -12; x_size 38; x_descenders 8; x_ascenders 10">
|
||||||
<span class='ocrx_word' id='word_1_8' title='bbox 347 380 412 410; x_wconf 93'>The</span>
|
<span class='ocrx_word' id='word_1_8' title='bbox 347 380 412 410; x_wconf 93'>The</span>
|
||||||
<span class='ocrx_word' id='word_1_9' title='bbox 424 380 676 417; x_wconf 91'>LinnSequencer</span>
|
<span class='ocrx_word' id='word_1_9' title='bbox 424 380 676 417; x_wconf 90'>LinnSequencer</span>
|
||||||
<span class='ocrx_word' id='word_1_10' title='bbox 688 380 712 411; x_wconf 96'>is</span>
|
<span class='ocrx_word' id='word_1_10' title='bbox 688 380 712 411; x_wconf 96'>is</span>
|
||||||
<span class='ocrx_word' id='word_1_11' title='bbox 724 390 743 411; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_11' title='bbox 724 390 743 411; x_wconf 96'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_12' title='bbox 754 381 1005 423; x_wconf 96'>state-of-the-art</span>
|
<span class='ocrx_word' id='word_1_12' title='bbox 754 381 1005 423; x_wconf 96'>state-of-the-art</span>
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_15' title='bbox 1311 380 1525 418; x_wconf 96'>performance</span>
|
<span class='ocrx_word' id='word_1_15' title='bbox 1311 380 1525 418; x_wconf 96'>performance</span>
|
||||||
<span class='ocrx_word' id='word_1_16' title='bbox 1536 380 1602 411; x_wconf 96'>tool</span>
|
<span class='ocrx_word' id='word_1_16' title='bbox 1536 380 1602 411; x_wconf 96'>tool</span>
|
||||||
<span class='ocrx_word' id='word_1_17' title='bbox 1615 380 1663 411; x_wconf 96'>for</span>
|
<span class='ocrx_word' id='word_1_17' title='bbox 1615 380 1663 411; x_wconf 96'>for</span>
|
||||||
<span class='ocrx_word' id='word_1_18' title='bbox 1674 381 1725 410; x_wconf 97'>the</span>
|
<span class='ocrx_word' id='word_1_18' title='bbox 1674 381 1725 410; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_19' title='bbox 1737 380 1940 417; x_wconf 96'>professional</span>
|
<span class='ocrx_word' id='word_1_19' title='bbox 1737 380 1940 417; x_wconf 95'>professional</span>
|
||||||
<span class='ocrx_word' id='word_1_20' title='bbox 1952 380 2112 411; x_wconf 96'>musician.</span>
|
<span class='ocrx_word' id='word_1_20' title='bbox 1952 380 2112 411; x_wconf 96'>musician.</span>
|
||||||
<span class='ocrx_word' id='word_1_21' title='bbox 2127 381 2152 410; x_wconf 96'>It</span>
|
<span class='ocrx_word' id='word_1_21' title='bbox 2127 381 2152 410; x_wconf 96'>It</span>
|
||||||
<span class='ocrx_word' id='word_1_22' title='bbox 2164 380 2188 410; x_wconf 96'>is</span>
|
<span class='ocrx_word' id='word_1_22' title='bbox 2164 380 2188 410; x_wconf 96'>is</span>
|
||||||
@@ -136,9 +136,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
<div class='ocr_carea' id='block_1_5' title="bbox 349 748 2117 793">
|
||||||
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 349 748 2117 793">
|
||||||
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_10' title="bbox 349 748 2117 793; baseline 0 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 42'>©</span>
|
<span class='ocrx_word' id='word_1_84' title='bbox 349 755 367 774; x_wconf 50'>©</span>
|
||||||
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 90'>Ultra-fast</span>
|
<span class='ocrx_word' id='word_1_85' title='bbox 383 748 573 784; x_wconf 91'>Ultra-fast</span>
|
||||||
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 9'>32”</span>
|
<span class='ocrx_word' id='word_1_86' title='bbox 616 749 677 784; x_wconf 10'>32”</span>
|
||||||
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
<span class='ocrx_word' id='word_1_87' title='bbox 694 748 775 784; x_wconf 96'>disk</span>
|
||||||
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
<span class='ocrx_word' id='word_1_88' title='bbox 790 748 887 784; x_wconf 96'>drive</span>
|
||||||
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
<span class='ocrx_word' id='word_1_89' title='bbox 901 754 1012 785; x_wconf 96'>stores</span>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
<span class='ocrx_word' id='word_1_95' title='bbox 1638 748 1746 784; x_wconf 96'>holds</span>
|
||||||
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
<span class='ocrx_word' id='word_1_96' title='bbox 1761 759 1844 784; x_wconf 96'>over</span>
|
||||||
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
<span class='ocrx_word' id='word_1_97' title='bbox 1859 749 2000 791; x_wconf 96'>110,000</span>
|
||||||
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 97'>notes</span>
|
<span class='ocrx_word' id='word_1_98' title='bbox 2013 753 2117 784; x_wconf 96'>notes</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,13 +165,13 @@
|
|||||||
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
<div class='ocr_carea' id='block_1_7' title="bbox 349 855 2030 1016">
|
||||||
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
<p class='ocr_par' id='par_1_9' lang='eng' title="bbox 349 855 2030 1016">
|
||||||
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
<span class='ocr_header' id='line_1_12' title="bbox 350 855 1638 900; baseline 0.001 -9; x_size 45; x_descenders 9; x_ascenders 11">
|
||||||
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 51'>¢</span>
|
<span class='ocrx_word' id='word_1_101' title='bbox 350 863 367 881; x_wconf 46'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 95'>One</span>
|
<span class='ocrx_word' id='word_1_102' title='bbox 383 856 464 891; x_wconf 96'>One</span>
|
||||||
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
<span class='ocrx_word' id='word_1_103' title='bbox 478 866 520 891; x_wconf 95'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_104' title='bbox 534 855 580 891; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
<span class='ocrx_word' id='word_1_105' title='bbox 594 856 712 892; x_wconf 95'>tracks</span>
|
||||||
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_106' title='bbox 726 867 811 900; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 85'>be</span>
|
<span class='ocrx_word' id='word_1_107' title='bbox 823 856 869 892; x_wconf 84'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
<span class='ocrx_word' id='word_1_108' title='bbox 882 856 1212 892; x_wconf 96'>TRANSPOSED</span>
|
||||||
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
<span class='ocrx_word' id='word_1_109' title='bbox 1227 861 1264 892; x_wconf 96'>at</span>
|
||||||
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_110' title='bbox 1277 856 1338 892; x_wconf 96'>the</span>
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_8' title="bbox 382 1021 689 1065">
|
<div class='ocr_carea' id='block_1_8' title="bbox 382 1021 689 1065">
|
||||||
<p class='ocr_par' id='par_1_10' lang='eng' title="bbox 382 1021 689 1065">
|
<p class='ocr_par' id='par_1_10' lang='eng' title="bbox 382 1021 689 1065">
|
||||||
<span class='ocr_line' id='line_1_15' title="bbox 382 1021 689 1065; baseline 0.003 -8; x_size 45; x_descenders 8; x_ascenders 12">
|
<span class='ocr_line' id='line_1_15' title="bbox 382 1021 689 1065; baseline 0.003 -8; x_size 45; x_descenders 8; x_ascenders 12">
|
||||||
<span class='ocrx_word' id='word_1_135' title='bbox 382 1021 564 1065; x_wconf 96'>rhythmic</span>
|
<span class='ocrx_word' id='word_1_135' title='bbox 382 1021 564 1065; x_wconf 95'>rhythmic</span>
|
||||||
<span class='ocrx_word' id='word_1_136' title='bbox 577 1021 689 1058; x_wconf 96'>value.</span>
|
<span class='ocrx_word' id='word_1_136' title='bbox 577 1021 689 1058; x_wconf 96'>value.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -249,15 +249,15 @@
|
|||||||
<span class='ocrx_word' id='word_1_154' title='bbox 349 1203 367 1222; x_wconf 73'>©</span>
|
<span class='ocrx_word' id='word_1_154' title='bbox 349 1203 367 1222; x_wconf 73'>©</span>
|
||||||
<span class='ocrx_word' id='word_1_155' title='bbox 382 1195 560 1240; x_wconf 96'>Optional</span>
|
<span class='ocrx_word' id='word_1_155' title='bbox 382 1195 560 1240; x_wconf 96'>Optional</span>
|
||||||
<span class='ocrx_word' id='word_1_156' title='bbox 573 1201 709 1233; x_wconf 96'>remote</span>
|
<span class='ocrx_word' id='word_1_156' title='bbox 573 1201 709 1233; x_wconf 96'>remote</span>
|
||||||
<span class='ocrx_word' id='word_1_157' title='bbox 723 1196 874 1233; x_wconf 96'>control.</span>
|
<span class='ocrx_word' id='word_1_157' title='bbox 723 1196 874 1233; x_wconf 95'>control.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
<div class='ocr_carea' id='block_1_12' title="bbox 346 1288 1239 1491">
|
||||||
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
<p class='ocr_par' id='par_1_14' lang='eng' title="bbox 346 1288 749 1329">
|
||||||
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
<span class='ocr_line' id='line_1_19' title="bbox 346 1288 749 1329; baseline 0.002 -9; x_size 42; x_descenders 9; x_ascenders 10">
|
||||||
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 96'>Recording</span>
|
<span class='ocrx_word' id='word_1_158' title='bbox 346 1288 535 1329; x_wconf 95'>Recording</span>
|
||||||
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_159' title='bbox 547 1298 567 1321; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
<span class='ocrx_word' id='word_1_160' title='bbox 579 1288 749 1328; x_wconf 96'>Sequence</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -278,8 +278,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
<span class='ocrx_word' id='word_1_170' title='bbox 346 1379 411 1406; x_wconf 96'>then</span>
|
||||||
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
<span class='ocrx_word' id='word_1_171' title='bbox 422 1378 483 1412; x_wconf 96'>play</span>
|
||||||
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
<span class='ocrx_word' id='word_1_172' title='bbox 493 1387 562 1412; x_wconf 96'>your</span>
|
||||||
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 95'>MIDI</span>
|
<span class='ocrx_word' id='word_1_173' title='bbox 572 1379 659 1405; x_wconf 96'>MIDI</span>
|
||||||
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 95'>keyboard</span>
|
<span class='ocrx_word' id='word_1_174' title='bbox 671 1379 810 1412; x_wconf 96'>keyboard</span>
|
||||||
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
<span class='ocrx_word' id='word_1_175' title='bbox 821 1379 848 1406; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
<span class='ocrx_word' id='word_1_176' title='bbox 858 1379 923 1406; x_wconf 96'>time</span>
|
||||||
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_177' title='bbox 934 1384 963 1406; x_wconf 96'>to</span>
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_22' title="bbox 347 1417 1232 1452; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
<span class='ocrx_word' id='word_1_180' title='bbox 347 1417 414 1445; x_wconf 96'>click</span>
|
||||||
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 91'>track.</span>
|
<span class='ocrx_word' id='word_1_181' title='bbox 425 1418 506 1445; x_wconf 92'>track.</span>
|
||||||
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
<span class='ocrx_word' id='word_1_182' title='bbox 519 1418 609 1445; x_wconf 96'>When</span>
|
||||||
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_183' title='bbox 620 1419 665 1445; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
<span class='ocrx_word' id='word_1_184' title='bbox 676 1426 810 1452; x_wconf 96'>sequence</span>
|
||||||
@@ -297,15 +297,15 @@
|
|||||||
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
<span class='ocrx_word' id='word_1_187' title='bbox 995 1419 1101 1446; x_wconf 96'>around</span>
|
||||||
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
<span class='ocrx_word' id='word_1_188' title='bbox 1112 1423 1141 1446; x_wconf 96'>to</span>
|
||||||
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
<span class='ocrx_word' id='word_1_189' title='bbox 1152 1419 1201 1446; x_wconf 96'>bar</span>
|
||||||
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 88'>1,</span>
|
<span class='ocrx_word' id='word_1_190' title='bbox 1213 1419 1232 1450; x_wconf 85'>1,</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_23' title="bbox 346 1457 1223 1491; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 12'>you’</span>
|
<span class='ocrx_word' id='word_1_191' title='bbox 346 1457 430 1490; x_wconf 16'>you’</span>
|
||||||
<span class='ocrx_word' id='word_1_192' title='bbox 410 1453 432 1496; x_wconf 12'>ll</span>
|
<span class='ocrx_word' id='word_1_192' title='bbox 410 1453 436 1496; x_wconf 16'>ll</span>
|
||||||
<span class='ocrx_word' id='word_1_193' title='bbox 441 1457 506 1485; x_wconf 95'>hear</span>
|
<span class='ocrx_word' id='word_1_193' title='bbox 441 1457 506 1485; x_wconf 96'>hear</span>
|
||||||
<span class='ocrx_word' id='word_1_194' title='bbox 517 1458 590 1485; x_wconf 96'>what</span>
|
<span class='ocrx_word' id='word_1_194' title='bbox 517 1458 590 1485; x_wconf 96'>what</span>
|
||||||
<span class='ocrx_word' id='word_1_195' title='bbox 600 1466 654 1491; x_wconf 93'>you</span>
|
<span class='ocrx_word' id='word_1_195' title='bbox 600 1466 654 1491; x_wconf 93'>you</span>
|
||||||
<span class='ocrx_word' id='word_1_196' title='bbox 666 1458 865 1491; x_wconf 90'>played—only</span>
|
<span class='ocrx_word' id='word_1_196' title='bbox 666 1458 865 1491; x_wconf 91'>played—only</span>
|
||||||
<span class='ocrx_word' id='word_1_197' title='bbox 876 1458 910 1485; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_197' title='bbox 876 1458 910 1485; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_198' title='bbox 921 1458 1018 1491; x_wconf 96'>timing</span>
|
<span class='ocrx_word' id='word_1_198' title='bbox 921 1458 1018 1491; x_wconf 96'>timing</span>
|
||||||
<span class='ocrx_word' id='word_1_199' title='bbox 1029 1466 1115 1485; x_wconf 96'>errors</span>
|
<span class='ocrx_word' id='word_1_199' title='bbox 1029 1466 1115 1485; x_wconf 96'>errors</span>
|
||||||
@@ -356,12 +356,12 @@
|
|||||||
|
|
||||||
<p class='ocr_par' id='par_1_18' lang='eng' title="bbox 346 1616 1205 1965">
|
<p class='ocr_par' id='par_1_18' lang='eng' title="bbox 346 1616 1205 1965">
|
||||||
<span class='ocr_line' id='line_1_27' title="bbox 384 1616 1199 1648; baseline 0.001 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
<span class='ocr_line' id='line_1_27' title="bbox 384 1616 1199 1648; baseline 0.001 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_228' title='bbox 384 1616 471 1642; x_wconf 96'>FAST</span>
|
<span class='ocrx_word' id='word_1_228' title='bbox 384 1616 471 1642; x_wconf 95'>FAST</span>
|
||||||
<span class='ocrx_word' id='word_1_229' title='bbox 481 1616 671 1648; x_wconf 96'>FORWARD,</span>
|
<span class='ocrx_word' id='word_1_229' title='bbox 481 1616 671 1648; x_wconf 96'>FORWARD,</span>
|
||||||
<span class='ocrx_word' id='word_1_230' title='bbox 684 1617 844 1648; x_wconf 95'>REWIND,</span>
|
<span class='ocrx_word' id='word_1_230' title='bbox 684 1617 844 1648; x_wconf 95'>REWIND,</span>
|
||||||
<span class='ocrx_word' id='word_1_231' title='bbox 857 1616 912 1643; x_wconf 95'>and</span>
|
<span class='ocrx_word' id='word_1_231' title='bbox 857 1616 912 1643; x_wconf 95'>and</span>
|
||||||
<span class='ocrx_word' id='word_1_232' title='bbox 924 1616 1068 1643; x_wconf 96'>LOCATE</span>
|
<span class='ocrx_word' id='word_1_232' title='bbox 924 1616 1068 1643; x_wconf 96'>LOCATE</span>
|
||||||
<span class='ocrx_word' id='word_1_233' title='bbox 1079 1616 1199 1643; x_wconf 96'>controls</span>
|
<span class='ocrx_word' id='word_1_233' title='bbox 1079 1616 1199 1643; x_wconf 95'>controls</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_28' title="bbox 346 1655 1202 1689; baseline 0 -7; x_size 34; x_descenders 6; x_ascenders 9">
|
<span class='ocr_line' id='line_1_28' title="bbox 346 1655 1202 1689; baseline 0 -7; x_size 34; x_descenders 6; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_234' title='bbox 346 1663 409 1688; x_wconf 87'>may</span>
|
<span class='ocrx_word' id='word_1_234' title='bbox 346 1663 409 1688; x_wconf 87'>may</span>
|
||||||
@@ -401,12 +401,12 @@
|
|||||||
<span class='ocr_line' id='line_1_31' title="bbox 346 1773 1203 1808; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_line' id='line_1_31' title="bbox 346 1773 1203 1808; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_263' title='bbox 346 1774 448 1806; x_wconf 96'>record,</span>
|
<span class='ocrx_word' id='word_1_263' title='bbox 346 1774 448 1806; x_wconf 96'>record,</span>
|
||||||
<span class='ocrx_word' id='word_1_264' title='bbox 460 1774 506 1801; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_264' title='bbox 460 1774 506 1801; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_265' title='bbox 517 1773 577 1801; x_wconf 96'>first</span>
|
<span class='ocrx_word' id='word_1_265' title='bbox 510 1769 577 1812; x_wconf 95'>first</span>
|
||||||
<span class='ocrx_word' id='word_1_266' title='bbox 587 1774 662 1801; x_wconf 96'>track</span>
|
<span class='ocrx_word' id='word_1_266' title='bbox 589 1774 658 1801; x_wconf 95'>track</span>
|
||||||
<span class='ocrx_word' id='word_1_267' title='bbox 673 1774 726 1801; x_wconf 96'>will</span>
|
<span class='ocrx_word' id='word_1_267' title='bbox 673 1774 726 1801; x_wconf 96'>will</span>
|
||||||
<span class='ocrx_word' id='word_1_268' title='bbox 736 1774 799 1807; x_wconf 96'>play</span>
|
<span class='ocrx_word' id='word_1_268' title='bbox 736 1774 799 1807; x_wconf 96'>play</span>
|
||||||
<span class='ocrx_word' id='word_1_269' title='bbox 809 1774 836 1801; x_wconf 96'>in</span>
|
<span class='ocrx_word' id='word_1_269' title='bbox 809 1774 836 1801; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_270' title='bbox 847 1774 949 1808; x_wconf 97'>perfect</span>
|
<span class='ocrx_word' id='word_1_270' title='bbox 847 1774 949 1808; x_wconf 96'>perfect</span>
|
||||||
<span class='ocrx_word' id='word_1_271' title='bbox 961 1782 1026 1808; x_wconf 96'>sync</span>
|
<span class='ocrx_word' id='word_1_271' title='bbox 961 1782 1026 1808; x_wconf 96'>sync</span>
|
||||||
<span class='ocrx_word' id='word_1_272' title='bbox 1037 1774 1137 1808; x_wconf 96'>(unless</span>
|
<span class='ocrx_word' id='word_1_272' title='bbox 1037 1774 1137 1808; x_wconf 96'>(unless</span>
|
||||||
<span class='ocrx_word' id='word_1_273' title='bbox 1148 1782 1203 1807; x_wconf 96'>you</span>
|
<span class='ocrx_word' id='word_1_273' title='bbox 1148 1782 1203 1807; x_wconf 96'>you</span>
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_275' title='bbox 464 1813 492 1845; x_wconf 95'>it,</span>
|
<span class='ocrx_word' id='word_1_275' title='bbox 464 1813 492 1845; x_wconf 95'>it,</span>
|
||||||
<span class='ocrx_word' id='word_1_276' title='bbox 505 1821 537 1840; x_wconf 95'>or</span>
|
<span class='ocrx_word' id='word_1_276' title='bbox 505 1821 537 1840; x_wconf 95'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_277' title='bbox 547 1813 642 1840; x_wconf 95'>SOLO</span>
|
<span class='ocrx_word' id='word_1_277' title='bbox 547 1813 642 1840; x_wconf 95'>SOLO</span>
|
||||||
<span class='ocrx_word' id='word_1_278' title='bbox 653 1814 769 1841; x_wconf 96'>another</span>
|
<span class='ocrx_word' id='word_1_278' title='bbox 653 1814 769 1841; x_wconf 95'>another</span>
|
||||||
<span class='ocrx_word' id='word_1_279' title='bbox 779 1814 875 1847; x_wconf 94'>track).</span>
|
<span class='ocrx_word' id='word_1_279' title='bbox 779 1814 875 1847; x_wconf 94'>track).</span>
|
||||||
<span class='ocrx_word' id='word_1_280' title='bbox 889 1814 920 1840; x_wconf 96'>In</span>
|
<span class='ocrx_word' id='word_1_280' title='bbox 889 1814 920 1840; x_wconf 96'>In</span>
|
||||||
<span class='ocrx_word' id='word_1_281' title='bbox 930 1813 984 1841; x_wconf 96'>this</span>
|
<span class='ocrx_word' id='word_1_281' title='bbox 930 1813 984 1841; x_wconf 96'>this</span>
|
||||||
@@ -523,8 +523,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_353' title='bbox 691 2212 718 2235; x_wconf 96'>at</span>
|
<span class='ocrx_word' id='word_1_353' title='bbox 691 2212 718 2235; x_wconf 96'>at</span>
|
||||||
<span class='ocrx_word' id='word_1_354' title='bbox 729 2208 841 2242; x_wconf 96'>specific</span>
|
<span class='ocrx_word' id='word_1_354' title='bbox 729 2208 841 2242; x_wconf 96'>specific</span>
|
||||||
<span class='ocrx_word' id='word_1_355' title='bbox 851 2209 943 2242; x_wconf 97'>points</span>
|
<span class='ocrx_word' id='word_1_355' title='bbox 851 2209 943 2242; x_wconf 97'>points</span>
|
||||||
<span class='ocrx_word' id='word_1_356' title='bbox 955 2208 1049 2236; x_wconf 96'>within</span>
|
<span class='ocrx_word' id='word_1_356' title='bbox 955 2208 1049 2236; x_wconf 97'>within</span>
|
||||||
<span class='ocrx_word' id='word_1_357' title='bbox 1060 2217 1076 2236; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_357' title='bbox 1060 2217 1076 2236; x_wconf 97'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_358' title='bbox 1086 2216 1228 2242; x_wconf 96'>sequence,</span>
|
<span class='ocrx_word' id='word_1_358' title='bbox 1086 2216 1228 2242; x_wconf 96'>sequence,</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
<span class='ocr_line' id='line_1_50' title="bbox 1295 1565 1577 1593; baseline 0.004 -1; x_size 34.748871; x_descenders 6.7488689; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
<span class='ocrx_word' id='word_1_423' title='bbox 1295 1565 1441 1592; x_wconf 96'>unwanted</span>
|
||||||
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 94'>sections,</span>
|
<span class='ocrx_word' id='word_1_424' title='bbox 1452 1565 1577 1593; x_wconf 93'>sections,</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -668,8 +668,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
<span class='ocrx_word' id='word_1_454' title='bbox 1458 1768 1562 1796; x_wconf 96'>section</span>
|
||||||
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
<span class='ocrx_word' id='word_1_455' title='bbox 1574 1769 1666 1802; x_wconf 96'>(verse,</span>
|
||||||
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
<span class='ocrx_word' id='word_1_456' title='bbox 1679 1769 1788 1801; x_wconf 96'>chorus,</span>
|
||||||
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 95'>etc.)</span>
|
<span class='ocrx_word' id='word_1_457' title='bbox 1800 1769 1865 1802; x_wconf 96'>etc.)</span>
|
||||||
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 95'>in</span>
|
<span class='ocrx_word' id='word_1_458' title='bbox 1876 1768 1904 1795; x_wconf 96'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
<span class='ocrx_word' id='word_1_459' title='bbox 1914 1768 2066 1796; x_wconf 96'>individual</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
<span class='ocr_line' id='line_1_55' title="bbox 1296 1808 2215 1841; baseline 0 -6; x_size 32; x_descenders 5; x_ascenders 8">
|
||||||
@@ -677,11 +677,11 @@
|
|||||||
<span class='ocrx_word' id='word_1_461' title='bbox 1463 1808 1528 1835; x_wconf 96'>then</span>
|
<span class='ocrx_word' id='word_1_461' title='bbox 1463 1808 1528 1835; x_wconf 96'>then</span>
|
||||||
<span class='ocrx_word' id='word_1_462' title='bbox 1538 1816 1587 1835; x_wconf 96'>use</span>
|
<span class='ocrx_word' id='word_1_462' title='bbox 1538 1816 1587 1835; x_wconf 96'>use</span>
|
||||||
<span class='ocrx_word' id='word_1_463' title='bbox 1597 1808 1643 1835; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_463' title='bbox 1597 1808 1643 1835; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_464' title='bbox 1653 1809 1799 1835; x_wconf 96'>CREATE</span>
|
<span class='ocrx_word' id='word_1_464' title='bbox 1653 1809 1799 1835; x_wconf 95'>CREATE</span>
|
||||||
<span class='ocrx_word' id='word_1_465' title='bbox 1810 1808 1911 1835; x_wconf 96'>SONG</span>
|
<span class='ocrx_word' id='word_1_465' title='bbox 1810 1808 1911 1835; x_wconf 96'>SONG</span>
|
||||||
<span class='ocrx_word' id='word_1_466' title='bbox 1923 1808 2050 1836; x_wconf 96'>function</span>
|
<span class='ocrx_word' id='word_1_466' title='bbox 1923 1808 2050 1836; x_wconf 96'>function</span>
|
||||||
<span class='ocrx_word' id='word_1_467' title='bbox 2060 1812 2089 1835; x_wconf 95'>to</span>
|
<span class='ocrx_word' id='word_1_467' title='bbox 2060 1812 2089 1835; x_wconf 96'>to</span>
|
||||||
<span class='ocrx_word' id='word_1_468' title='bbox 2103 1808 2215 1836; x_wconf 92'>“chain”</span>
|
<span class='ocrx_word' id='word_1_468' title='bbox 2103 1808 2215 1836; x_wconf 93'>“chain”</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_line' id='line_1_56' title="bbox 1295 1847 2135 1881; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_line' id='line_1_56' title="bbox 1295 1847 2135 1881; baseline 0.001 -7; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_469' title='bbox 1295 1848 1370 1874; x_wconf 96'>them</span>
|
<span class='ocrx_word' id='word_1_469' title='bbox 1295 1848 1370 1874; x_wconf 96'>them</span>
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
<span class='ocrx_word' id='word_1_477' title='bbox 1377 1887 1412 1914; x_wconf 96'>all</span>
|
||||||
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
<span class='ocrx_word' id='word_1_478' title='bbox 1422 1887 1468 1914; x_wconf 96'>the</span>
|
||||||
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
<span class='ocrx_word' id='word_1_479' title='bbox 1478 1891 1552 1920; x_wconf 96'>parts</span>
|
||||||
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 96'>into</span>
|
<span class='ocrx_word' id='word_1_480' title='bbox 1563 1887 1621 1914; x_wconf 95'>into</span>
|
||||||
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_481' title='bbox 1632 1895 1649 1914; x_wconf 96'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
<span class='ocrx_word' id='word_1_482' title='bbox 1659 1895 1718 1914; x_wconf 96'>new</span>
|
||||||
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
<span class='ocrx_word' id='word_1_483' title='bbox 1729 1895 1870 1920; x_wconf 96'>sequence.</span>
|
||||||
@@ -829,9 +829,9 @@
|
|||||||
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
<div class='ocr_carea' id='block_1_23' title="bbox 347 2427 2145 2507">
|
||||||
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
<p class='ocr_par' id='par_1_31' lang='eng' title="bbox 347 2427 2145 2507">
|
||||||
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_67' title="bbox 347 2427 1468 2461; baseline 0.002 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 77'>*</span>
|
<span class='ocrx_word' id='word_1_573' title='bbox 347 2432 361 2446; x_wconf 75'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
<span class='ocrx_word' id='word_1_574' title='bbox 373 2427 612 2454; x_wconf 91'>Non-destructive</span>
|
||||||
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 84'>recording—existing</span>
|
<span class='ocrx_word' id='word_1_575' title='bbox 622 2427 914 2461; x_wconf 87'>recording—existing</span>
|
||||||
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
<span class='ocrx_word' id='word_1_576' title='bbox 924 2432 1002 2455; x_wconf 96'>notes</span>
|
||||||
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
<span class='ocrx_word' id='word_1_577' title='bbox 1013 2436 1057 2455; x_wconf 96'>are</span>
|
||||||
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
<span class='ocrx_word' id='word_1_578' title='bbox 1068 2432 1116 2455; x_wconf 96'>not</span>
|
||||||
@@ -863,7 +863,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_24' title="bbox 372 2510 1090 2543">
|
<div class='ocr_carea' id='block_1_24' title="bbox 372 2510 1090 2543">
|
||||||
<p class='ocr_par' id='par_1_32' lang='eng' title="bbox 372 2510 1090 2543">
|
<p class='ocr_par' id='par_1_32' lang='eng' title="bbox 372 2510 1090 2543">
|
||||||
<span class='ocr_line' id='line_1_69' title="bbox 372 2510 1090 2543; baseline 0.001 -6; x_size 35.625; x_descenders 8.90625; x_ascenders 8.90625">
|
<span class='ocr_line' id='line_1_69' title="bbox 372 2510 1090 2543; baseline 0.001 -6; x_size 35.625; x_descenders 8.90625; x_ascenders 8.90625">
|
||||||
<span class='ocrx_word' id='word_1_599' title='bbox 372 2510 500 2542; x_wconf 95'>ERASE,</span>
|
<span class='ocrx_word' id='word_1_599' title='bbox 372 2510 500 2542; x_wconf 96'>ERASE,</span>
|
||||||
<span class='ocrx_word' id='word_1_600' title='bbox 513 2511 660 2542; x_wconf 93'>REPEAT,</span>
|
<span class='ocrx_word' id='word_1_600' title='bbox 513 2511 660 2542; x_wconf 93'>REPEAT,</span>
|
||||||
<span class='ocrx_word' id='word_1_601' title='bbox 673 2511 883 2543; x_wconf 91'>PLAY/STOP,</span>
|
<span class='ocrx_word' id='word_1_601' title='bbox 673 2511 883 2543; x_wconf 91'>PLAY/STOP,</span>
|
||||||
<span class='ocrx_word' id='word_1_602' title='bbox 896 2519 927 2538; x_wconf 96'>or</span>
|
<span class='ocrx_word' id='word_1_602' title='bbox 896 2519 927 2538; x_wconf 96'>or</span>
|
||||||
@@ -874,10 +874,10 @@
|
|||||||
<div class='ocr_carea' id='block_1_25' title="bbox 347 2556 1768 2590">
|
<div class='ocr_carea' id='block_1_25' title="bbox 347 2556 1768 2590">
|
||||||
<p class='ocr_par' id='par_1_33' lang='eng' title="bbox 347 2556 1768 2590">
|
<p class='ocr_par' id='par_1_33' lang='eng' title="bbox 347 2556 1768 2590">
|
||||||
<span class='ocr_header' id='line_1_70' title="bbox 347 2556 1768 2590; baseline 0.001 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_70' title="bbox 347 2556 1768 2590; baseline 0.001 -8; x_size 34; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_604' title='bbox 347 2561 361 2575; x_wconf 86'>¢</span>
|
<span class='ocrx_word' id='word_1_604' title='bbox 347 2561 361 2575; x_wconf 87'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_605' title='bbox 372 2556 433 2583; x_wconf 80'>Two</span>
|
<span class='ocrx_word' id='word_1_605' title='bbox 372 2556 433 2583; x_wconf 79'>Two</span>
|
||||||
<span class='ocrx_word' id='word_1_606' title='bbox 443 2556 612 2583; x_wconf 95'>TRIGGER</span>
|
<span class='ocrx_word' id='word_1_606' title='bbox 443 2556 612 2583; x_wconf 95'>TRIGGER</span>
|
||||||
<span class='ocrx_word' id='word_1_607' title='bbox 623 2556 797 2584; x_wconf 95'>OUTPUTS</span>
|
<span class='ocrx_word' id='word_1_607' title='bbox 623 2556 797 2584; x_wconf 96'>OUTPUTS</span>
|
||||||
<span class='ocrx_word' id='word_1_608' title='bbox 808 2565 871 2590; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_608' title='bbox 808 2565 871 2590; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_609' title='bbox 881 2557 915 2584; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_609' title='bbox 881 2557 915 2584; x_wconf 96'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_610' title='bbox 925 2557 1119 2590; x_wconf 96'>programmed</span>
|
<span class='ocrx_word' id='word_1_610' title='bbox 925 2557 1119 2590; x_wconf 96'>programmed</span>
|
||||||
@@ -911,10 +911,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
<div class='ocr_carea' id='block_1_27' title="bbox 347 2648 2100 2727">
|
||||||
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
<p class='ocr_par' id='par_1_35' lang='eng' title="bbox 347 2648 2100 2727">
|
||||||
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_72' title="bbox 347 2648 1664 2682; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 42'>®</span>
|
<span class='ocrx_word' id='word_1_630' title='bbox 347 2654 360 2667; x_wconf 45'>®</span>
|
||||||
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 95'>Utilizes</span>
|
<span class='ocrx_word' id='word_1_631' title='bbox 372 2648 483 2675; x_wconf 94'>Utilizes</span>
|
||||||
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2677; x_wconf 96'>ultra</span>
|
<span class='ocrx_word' id='word_1_632' title='bbox 493 2648 564 2680; x_wconf 96'>ultra</span>
|
||||||
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
<span class='ocrx_word' id='word_1_633' title='bbox 573 2648 744 2682; x_wconf 96'>high-speed,</span>
|
||||||
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
<span class='ocrx_word' id='word_1_634' title='bbox 766 2649 772 2676; x_wconf 95'>8</span>
|
||||||
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
|
<span class='ocrx_word' id='word_1_635' title='bbox 783 2649 862 2675; x_wconf 95'>MHz</span>
|
||||||
@@ -928,12 +928,12 @@
|
|||||||
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
<span class='ocrx_word' id='word_1_643' title='bbox 1512 2648 1664 2682; x_wconf 96'>operation.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_73' title="bbox 347 2694 2100 2727; baseline 0.001 -7; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 52'>*</span>
|
<span class='ocrx_word' id='word_1_644' title='bbox 347 2699 361 2713; x_wconf 44'>*</span>
|
||||||
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_645' title='bbox 372 2694 504 2721; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_646' title='bbox 515 2702 578 2727; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_647' title='bbox 589 2694 623 2721; x_wconf 95'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
<span class='ocrx_word' id='word_1_648' title='bbox 633 2694 764 2727; x_wconf 95'>specified</span>
|
||||||
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 92'>in</span>
|
<span class='ocrx_word' id='word_1_649' title='bbox 774 2694 802 2721; x_wconf 93'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
<span class='ocrx_word' id='word_1_650' title='bbox 814 2695 1172 2722; x_wconf 91'>BEATS-PER-MINUTE</span>
|
||||||
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
<span class='ocrx_word' id='word_1_651' title='bbox 1183 2703 1215 2722; x_wconf 93'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
<span class='ocrx_word' id='word_1_652' title='bbox 1225 2695 1567 2722; x_wconf 92'>FRAMES-PER-BEAT</span>
|
||||||
@@ -960,7 +960,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_29' title="bbox 347 2777 2174 2811">
|
<div class='ocr_carea' id='block_1_29' title="bbox 347 2777 2174 2811">
|
||||||
<p class='ocr_par' id='par_1_37' lang='eng' title="bbox 347 2777 2174 2811">
|
<p class='ocr_par' id='par_1_37' lang='eng' title="bbox 347 2777 2174 2811">
|
||||||
<span class='ocr_header' id='line_1_75' title="bbox 347 2777 2174 2811; baseline 0.001 -8; x_size 33; x_descenders 5; x_ascenders 9">
|
<span class='ocr_header' id='line_1_75' title="bbox 347 2777 2174 2811; baseline 0.001 -8; x_size 33; x_descenders 5; x_ascenders 9">
|
||||||
<span class='ocrx_word' id='word_1_664' title='bbox 347 2782 360 2796; x_wconf 74'>¢</span>
|
<span class='ocrx_word' id='word_1_664' title='bbox 347 2782 360 2796; x_wconf 78'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_665' title='bbox 372 2777 504 2804; x_wconf 95'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_665' title='bbox 372 2777 504 2804; x_wconf 95'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_666' title='bbox 515 2785 578 2810; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_666' title='bbox 515 2785 578 2810; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_667' title='bbox 588 2777 622 2804; x_wconf 95'>be</span>
|
<span class='ocrx_word' id='word_1_667' title='bbox 588 2777 622 2804; x_wconf 95'>be</span>
|
||||||
@@ -968,10 +968,10 @@
|
|||||||
<span class='ocrx_word' id='word_1_669' title='bbox 751 2777 934 2811; x_wconf 96'>numerically,</span>
|
<span class='ocrx_word' id='word_1_669' title='bbox 751 2777 934 2811; x_wconf 96'>numerically,</span>
|
||||||
<span class='ocrx_word' id='word_1_670' title='bbox 946 2777 1101 2811; x_wconf 96'>adjustable</span>
|
<span class='ocrx_word' id='word_1_670' title='bbox 946 2777 1101 2811; x_wconf 96'>adjustable</span>
|
||||||
<span class='ocrx_word' id='word_1_671' title='bbox 1111 2777 1139 2804; x_wconf 95'>in</span>
|
<span class='ocrx_word' id='word_1_671' title='bbox 1111 2777 1139 2804; x_wconf 95'>in</span>
|
||||||
<span class='ocrx_word' id='word_1_672' title='bbox 1149 2778 1239 2805; x_wconf 96'>tenths</span>
|
<span class='ocrx_word' id='word_1_672' title='bbox 1149 2778 1239 2805; x_wconf 95'>tenths</span>
|
||||||
<span class='ocrx_word' id='word_1_673' title='bbox 1250 2778 1282 2805; x_wconf 96'>of</span>
|
<span class='ocrx_word' id='word_1_673' title='bbox 1250 2778 1282 2805; x_wconf 96'>of</span>
|
||||||
<span class='ocrx_word' id='word_1_674' title='bbox 1290 2786 1307 2805; x_wconf 93'>a</span>
|
<span class='ocrx_word' id='word_1_674' title='bbox 1290 2786 1307 2805; x_wconf 93'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_675' title='bbox 1317 2777 1567 2805; x_wconf 91'>Beat-Per-Minute</span>
|
<span class='ocrx_word' id='word_1_675' title='bbox 1317 2777 1567 2805; x_wconf 92'>Beat-Per-Minute</span>
|
||||||
<span class='ocrx_word' id='word_1_676' title='bbox 1577 2777 1748 2809; x_wconf 96'>increments,</span>
|
<span class='ocrx_word' id='word_1_676' title='bbox 1577 2777 1748 2809; x_wconf 96'>increments,</span>
|
||||||
<span class='ocrx_word' id='word_1_677' title='bbox 1760 2785 1792 2804; x_wconf 96'>or</span>
|
<span class='ocrx_word' id='word_1_677' title='bbox 1760 2785 1792 2804; x_wconf 96'>or</span>
|
||||||
<span class='ocrx_word' id='word_1_678' title='bbox 1803 2777 1839 2810; x_wconf 96'>by</span>
|
<span class='ocrx_word' id='word_1_678' title='bbox 1803 2777 1839 2810; x_wconf 96'>by</span>
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
<div class='ocr_carea' id='block_1_31' title="bbox 347 2861 1792 2940">
|
||||||
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
<p class='ocr_par' id='par_1_39' lang='eng' title="bbox 347 2861 1792 2940">
|
||||||
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
<span class='ocr_header' id='line_1_77' title="bbox 347 2861 1792 2895; baseline 0.001 -8; x_size 33; x_descenders 6; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 62'>¢</span>
|
<span class='ocrx_word' id='word_1_687' title='bbox 347 2866 360 2880; x_wconf 59'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
<span class='ocrx_word' id='word_1_688' title='bbox 372 2861 504 2887; x_wconf 96'>TEMPO</span>
|
||||||
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
<span class='ocrx_word' id='word_1_689' title='bbox 515 2861 696 2888; x_wconf 96'>CHANGES</span>
|
||||||
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_690' title='bbox 707 2869 771 2894; x_wconf 96'>may</span>
|
||||||
@@ -1011,7 +1011,7 @@
|
|||||||
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
<span class='ocrx_word' id='word_1_700' title='bbox 1679 2861 1792 2888; x_wconf 84'>desired.</span>
|
||||||
</span>
|
</span>
|
||||||
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
<span class='ocr_header' id='line_1_78' title="bbox 347 2906 1507 2940; baseline 0.002 -8; x_size 33; x_descenders 7; x_ascenders 8">
|
||||||
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 76'>¢</span>
|
<span class='ocrx_word' id='word_1_701' title='bbox 347 2911 360 2925; x_wconf 79'>¢</span>
|
||||||
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
<span class='ocrx_word' id='word_1_702' title='bbox 371 2906 434 2938; x_wconf 96'>Any</span>
|
||||||
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
<span class='ocrx_word' id='word_1_703' title='bbox 444 2906 539 2932; x_wconf 96'>TIME</span>
|
||||||
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
<span class='ocrx_word' id='word_1_704' title='bbox 550 2906 763 2933; x_wconf 96'>SIGNATURE</span>
|
||||||
@@ -1022,8 +1022,8 @@
|
|||||||
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
<span class='ocrx_word' id='word_1_709' title='bbox 1046 2915 1109 2940; x_wconf 96'>may</span>
|
||||||
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
<span class='ocrx_word' id='word_1_710' title='bbox 1120 2907 1154 2934; x_wconf 96'>be</span>
|
||||||
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
<span class='ocrx_word' id='word_1_711' title='bbox 1164 2907 1288 2940; x_wconf 96'>changed</span>
|
||||||
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 96'>within</span>
|
<span class='ocrx_word' id='word_1_712' title='bbox 1299 2907 1393 2934; x_wconf 95'>within</span>
|
||||||
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 96'>a</span>
|
<span class='ocrx_word' id='word_1_713' title='bbox 1404 2915 1420 2934; x_wconf 95'>a</span>
|
||||||
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
<span class='ocrx_word' id='word_1_714' title='bbox 1431 2915 1507 2940; x_wconf 96'>song.</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
Tesseract Open Source OCR Engine v5.0.0-beta-20210916-12-g19cc9 with Leptonica
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user